Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2482086
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
45 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/Kolab/Config.php b/lib/Kolab/Config.php
index c83e146..7a81527 100644
--- a/lib/Kolab/Config.php
+++ b/lib/Kolab/Config.php
@@ -1,378 +1,378 @@
<?php
/**
* Model class to give access to service configuration
*
* This file is part of the Kolab PHP Utilities library
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab;
/**
* Wrapper class for service configuration
*/
class Config
{
const STRING = 0;
const BOOL = 1;
const INT = 2;
const FLOAT = 3;
const ARR = 4;
const HASH = 5;
const EMAIL = 6;
protected static $instance;
protected $env = '';
protected $basedir = '../config';
protected $data = array();
protected $valid = false;
/**
* Singelton getter
*
* @param string Path to load config from
*/
public static function get_instance($env = '', $dir = null)
{
if (!isset(self::$instance)) {
self::$instance = new Config($env);
if ($dir) self::$instance->basedir = $dir;
}
if (!self::$instance->valid) {
self::$instance->load('config.ini');
}
return self::$instance;
}
/**
* Default constructor
*/
function __construct($env = '')
{
$this->env = $env;
}
/**
* Load config from the given .ini file
*/
protected function load($file, $use_env = true)
{
// check for relative path
if (!is_readable($file) && is_readable($this->basedir . '/' . $file)) {
$file = $this->basedir . '/' . $file;
}
$inifile = $this->resolve_path($file, $use_env);
if ($raw = self::parse_ini_file($inifile, true)) {
foreach ($raw as $section => $values) {
$sub = null;
if (preg_match('/^(\w+)\s+"?(.+)"?$/i', $section, $m)) {
$section = $m[1];
$sub = trim($m[2], '"');
}
if (!empty($sub) && !empty($values)) {
$config[$section][$sub] = $values;
}
else if (!empty($values) && is_array($values)) {
$config[$section] = $values;
}
}
$this->register($config);
$this->valid = !empty($this->data);
}
else {
trigger_error("Failed to parse configuration from $inifile", E_USER_ERROR);
}
}
/**
* Helper method to resolve the absolute path to the given config file.
* This also takes the 'env' property into account.
*/
protected function resolve_path($file, $use_env)
{
if ($file[0] != '/' && $this->basedir[0] == '/') {
$file = realpath($this->basedir . '/' . $file);
}
// check if <file>-env.ini exists
if ($file && $use_env && !empty($this->env)) {
$envfile = preg_replace('/\.(ini|conf)$/', '-' . $this->env . '.\\1', $file);
if (is_file($envfile))
return $envfile;
}
return $file;
}
/**
* Replacement for PHP's parse_ini_file()
*/
protected static function parse_ini_file($filename)
{
$raw = array();
foreach (file($filename) as $_line) {
if ($_line[0] == ';') // skip comments
continue;
// chop off comments at the end of a line
$_line = preg_replace('/;[^\'"]*$/', '', $_line);
if (preg_match('/^\[([a-z0-9-_\.]+[^\]]*)\]/', $_line, $matches)) {
$_cur_section = $matches[1];
$raw[$_cur_section] = array();
unset($_cur_key);
}
if (preg_match('/^([a-z0-9\.-_]+)\s*=\s*(.*)/', $_line, $matches)) {
if (isset($_cur_section) && !empty($_cur_section)) {
$_cur_key = $matches[1];
$value = isset($matches[2]) ? trim($matches[2], ' "') : '';
if (preg_match('/^(true|yes|on)$/', $matches[2])) // convert boolean values right away
$value = true;
$raw[$_cur_section][$matches[1]] = $value;
}
}
else if (preg_match('/^\s+(.*)$/', $_line, $matches)) {
if (isset($_cur_key) && !empty($_cur_key)) {
$raw[$_cur_section][$_cur_key] .= $matches[1];
}
}
}
return $raw;
}
/**
* Dump the hierarchical structure of config options into a flat list with keys delimited by dots
*/
public function register($config, $prefix = '')
{
// merge the new config values over existing data
if (empty($prefix)) {
$this->data = array_replace_recursive($this->data, $config);
}
else if (is_array($config)) {
$pkey = rtrim($prefix, '.');
- $this->data[$pkey] = is_array($this->data[$pkey]) ? array_replace_recursive((array)$this->data[$pkey], $config) : $config;
+ $this->data[$pkey] = is_array($this->data[$pkey] ?? null) ? array_replace_recursive((array)$this->data[$pkey], $config) : $config;
}
foreach ((array)$config as $key => $val) {
if (is_array($val)) {
$this->register($val, "$prefix$key.");
}
else {
$this->data[$prefix.$key] = $val;
}
}
// resolve references in config options (e.g. %(foo.bar))
if (empty($prefix)) {
array_walk_recursive($this->data, array($this, 'resolve_reference'));
}
}
/**
* Callback to resolve references in the given config option value
*/
protected function resolve_reference(&$value, $key)
{
if (is_string($value)) {
$value = preg_replace_callback('/%[({]([\w.]+)[})]/i', array($this, 'replace_reference'), $value);
}
}
/**
* Callback function to replace the given reference with the read config value
*/
protected function replace_reference($m)
{
return $this->data[$m[1]];
}
/**
* Magic getter for direct read-only access to config options
*/
public function __get($name)
{
return $this->data[$name];
}
/**
* Magic isset check
*/
public function __isset($name)
{
return array_key_exists($name, $this->data);
}
/**
* Common getter for config options with fallback to default values
*
* @param string Config option name
* @param mixed Default value if option isn't set in config
* @param integer Expected variable type
* @return mixed Config option value
*/
public function get($name, $default = null, $type = null)
{
switch ($name) {
case 'output.tempdir':
case 'session.savepath':
// return an absolute path for relative directory properties
if (isset($this->data[$name]) && $this->data[$name][0] != '/') {
if (!($value = realpath(INSTALL_PATH . '/' . $this->data[$name]))) {
$value = INSTALL_PATH . '/' . $this->data[$name];
}
break;
}
default:
$value = array_key_exists($name, $this->data) ? $this->data[$name] : $default;
}
// convert value to the requested type
return $type ? self::convert($value, $type) : $value;
}
/**
* Adjust, override a config option
*/
public function set($name, $value)
{
$this->data[$name] = $value;
if (is_array($value)) {
$this->register($this->data[$name], $name.'.');
}
}
/**
* Determines whether we have a valid configuration loaded
*
* @return boolean True if valid, False otherwise
*/
public function valid()
{
return !empty($this->data);
}
/**
* Convert the given (string) value to the requested type
*
* @param string Config value
* @param int Output type (one of this class constants)
* @return mixed The converted value
*/
public static function convert($value, $type)
{
// convert value to the requested type
switch ($type) {
case self::INT:
return intval($value);
case self::FLOAT:
return floatval($value);
case self::BOOL:
return (bool)preg_match('/^(true|1|on|enabled|yes)$/i', $value);
case self::ARR:
return array_filter(preg_split('/,\s*/', strval($value)));
case self::HASH:
$arr = array();
if (preg_match_all('/([\w]+)\s*=\s*([^,]*)(,\s*|$)/', trim(strval($value), '{} '), $matches, PREG_SET_ORDER)) {
foreach ($matches as $m) {
$arr[$m[1]] = is_numeric($m[2]) ? floatval($m[2]) : $m[2];
}
}
return $arr;
case self::EMAIL:
if (preg_match('/(\S+|("[^"]+"))@\S+/', $value, $m)) {
$email = trim($m[0], '<>');
}
return $email;
}
return $value;
}
/**
* Shortcut method to convert to a boolean value
*/
public static function boolean($value)
{
return self::convert($value, self::BOOL);
}
/**
* Shortcut method to convert to a integer value
*/
public static function intval($value)
{
return self::convert($value, self::INT);
}
/**
* Shortcut method to convert to a float value
*/
public static function floatval($value)
{
return self::convert($value, self::FLOAT);
}
/**
* Shortcut method to convert to an array value
*/
public static function arr($value)
{
return self::convert($value, self::ARR);
}
/**
* Shortcut method to convert to a hash array value
*/
public static function hash($value)
{
return self::convert($value, self::HASH);
}
/**
* Convenience method to check whether a certain value is part of an option (list)
*
* @param mixed Value to compare
* @param mixed Config option value
* @param boolean Treat undefined options as 'match'
* @return boolean True of the given value is listed in config
*/
public static function in_array($value, $option, $or_not_set = false)
{
// return true if option is not set (means 'allow all')
if (!isset($option) && $or_not_set) {
return true;
}
return in_array($value, self::convert($option, self::ARR));
}
}
diff --git a/lib/Kolab/FreeBusy/Directory.php b/lib/Kolab/FreeBusy/Directory.php
index 95ce87a..6b5c2db 100644
--- a/lib/Kolab/FreeBusy/Directory.php
+++ b/lib/Kolab/FreeBusy/Directory.php
@@ -1,120 +1,120 @@
<?php
/**
* This file is part of the Kolab Server Free/Busy Service
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\FreeBusy;
use Kolab\Config;
/**
* Abstract class representing an address directory for free/busy data lookups
*/
abstract class Directory
{
protected $config;
/**
* Factory method creating an instace of Directory according to config
*
* @param array Hash array with config
*/
public static function factory($config)
{
switch (strtolower($config['type'])) {
case 'ldap':
return new DirectoryLDAP($config);
case 'static':
case 'external':
return new DirectoryStatic($config);
default:
Logger::get('directory')->addError("Invalid directory type '" . $config['type'] . "'!");
}
return null;
}
/**
* Resolve the given username to a Entity object
*
* @param string Username/Email to resolve
* @return object Entity if found, otherwise False
*/
abstract public function resolve($user);
/**
* Retrieve free/busy data for the given user.
*
* @param string Username or email to resolve
* @param boolean Get extemded free-busy if possible
* @return string VCalendar container if found, False otherwise
*/
public function getFreeBusyData($user, $extended = false)
{
// resolve user record first
if ($user = $this->resolve($user)) {
$fbsource = $this->config['fbsource'];
if ($source = Source::Factory($fbsource, $this->config)) {
// forward request to Source instance
if ($data = $source->getFreeBusyData($this->postprocessAttrib($user), $extended)) {
// send data through the according format converter
- $converter = Format::factory($this->config['format']);
+ $converter = Format::factory($this->config['format'] ?? null);
$data = $converter->toVCalendar($data);
// cache the generated data
if ($data && $this->config['cacheto'] && !$source->isCached()) {
$path = preg_replace_callback(
'/%\{?([a-z0-9]+)\}?/',
function($m) use ($user) { return $user[$m[1]]; },
$this->config['cacheto']
);
if (!@file_put_contents($path, $data, LOCK_EX)) {
Logger::get('directory')->addError("Failed to write to cache file '" . $path . "'!");
}
}
}
return $data;
}
}
return false;
}
/**
* Modify attribute values according to config
*/
protected function postprocessAttrib($attrib)
{
if (!empty($this->config['lc_attributes'])) {
foreach (Config::convert($this->config['lc_attributes'], Config::ARR) as $key) {
if (!empty($attrib[$key]))
$attrib[$key] = strtolower($attrib[$key]);
}
}
return $attrib;
}
-}
\ No newline at end of file
+}
diff --git a/lib/Kolab/FreeBusy/DirectoryLDAP.php b/lib/Kolab/FreeBusy/DirectoryLDAP.php
index c011f0c..f255bc3 100644
--- a/lib/Kolab/FreeBusy/DirectoryLDAP.php
+++ b/lib/Kolab/FreeBusy/DirectoryLDAP.php
@@ -1,176 +1,176 @@
<?php
/**
* This file is part of the Kolab Server Free/Busy Service
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\FreeBusy;
// PEAR modules operate in global namespace
use \Net_LDAP3;
use \Kolab\Config;
use \Monolog\Logger as Monolog;
/**
* Implementation of an address lookup using an LDAP directory
*/
class DirectoryLDAP extends Directory
{
private $ldap;
private $logger;
private $ready = false;
/**
* Default constructor loading directory configuration
*/
public function __construct($config)
{
$this->config = $config;
$host = parse_url($config['host']);
$ldap_config = array(
'hosts' => array($config['host']),
'port' => $host['port'] ?: 389,
'use_tls' => $host['scheme'] == 'tls',
'root_dn' => $config['root_dn'] ?: $config['base_dn'],
'log_hook' => array($this, 'log'),
) + $config;
// instantiate Net_LDAP3 and connect with logger
$this->logger = Logger::get('ldap', intval($config['loglevel']));
$this->ldap = new Net_LDAP3($ldap_config);
// connect + bind to LDAP server
if ($this->ldap->connect()) {
$this->ready = $this->ldap->bind($config['bind_dn'], $config['bind_pw']);
}
if ($this->ready) {
$this->logger->addInfo("Connected to $config[host] with '$config[bind_dn]'");
}
else {
$this->logger->addWarning("Connectiion to $config[host] with '$config[bind_dn]' failed!");
}
}
/**
* Callback for Net_LDAP3 logging
*/
public function log($level, $msg)
{
// map PHP log levels to Monolog levels
static $loglevels = array(
LOG_DEBUG => Monolog::DEBUG,
LOG_NOTICE => Monolog::NOTICE,
LOG_INFO => Monolog::INFO,
LOG_WARNING => Monolog::WARNING,
LOG_ERR => Monolog::ERROR,
LOG_CRIT => Monolog::CRITICAL,
LOG_ALERT => Monolog::ALERT,
LOG_EMERG => Monolog::EMERGENCY,
);
$msg = is_array($msg) ? join('; ', $msg) : strval($msg);
$this->logger->addRecord($loglevels[$level], $msg);
}
/**
* @see Directory::resolve()
*/
public function resolve($user)
{
$result = array('s' => $user);
if ($this->ready) {
// extract domain name
list($u, $d) = explode('@', $user);
if (empty($d)) $d = $this->config['primary_domain'];
// resolve domain root dn
if (!empty($this->config['domain_filter'])) {
$dc = $this->ldap->domain_root_dn($d);
}
else {
$dc = 'dc=' . str_replace('.', ',dc=', $d);
}
// result attributes
$attribs = array_unique(array_merge(
- Config::convert($this->config['mail_attributes'], Config::ARR),
- Config::convert($this->config['attributes'], Config::ARR), // deprecated
- Config::convert($this->config['resolve_dn'], Config::ARR),
- Config::convert($this->config['resolve_attribute'], Config::ARR)
+ Config::convert($this->config['mail_attributes'] ?? null, Config::ARR),
+ Config::convert($this->config['attributes'] ?? null, Config::ARR), // deprecated
+ Config::convert($this->config['resolve_dn'] ?? null, Config::ARR),
+ Config::convert($this->config['resolve_attribute'] ?? null, Config::ARR)
));
// search with configured base_dn and filter
$replaces = array('%dc' => $dc, '%u' => $u);
$base_dn = strtr($this->config['base_dn'], $replaces);
$filter = str_replace('%s', Net_LDAP3::quote_string($user), strtr($this->config['filter'], $replaces));
$ldapresult = $this->ldap->search($base_dn, $filter, 'sub', $attribs);
// got a valid result
if ($ldapresult && $ldapresult->count()) {
$ldapresult->rewind();
$entry = Net_LDAP3::normalize_entry($ldapresult->current()); // get the first entry
$this->logger->addInfo("Found " . $ldapresult->count() . " entries for $filter", $entry);
// convert entry attributes to strings and add them to the final result hash array
$result += self::_compact_entry($entry);
// resolve DN attribute into the actual record
if (!empty($this->config['resolve_dn']) && array_key_exists($this->config['resolve_dn'], $result)) {
$k = $this->config['resolve_dn'];
$member_attr = $this->config['resolve_attribute'] ?: 'mail';
foreach ((array)$result[$k] as $i => $member_dn) {
if ($member_rec = $this->ldap->get_entry($member_dn, array($member_attr))) {
$member_rec = self::_compact_entry(Net_LDAP3::normalize_entry($member_rec));
$result[$k][$i] = $member_rec[$member_attr];
}
}
}
return $result;
}
$this->logger->addInfo("No entry found for $filter");
}
return false;
}
/**
* Helper method to convert entry attributes to simple values
*/
private static function _compact_entry($entry)
{
$result = array();
foreach ($entry as $k => $v) {
if (is_array($v) && count($v) > 1) {
$result[$k] = array_map('strval', $v);
}
else if (!empty($v)) {
$result[$k] = strval(is_array($v) ? $v[0] : $v);
}
}
return $result;
}
}
diff --git a/lib/Kolab/FreeBusy/SourceIMAP.php b/lib/Kolab/FreeBusy/SourceIMAP.php
index f8b0c1f..a1491a6 100644
--- a/lib/Kolab/FreeBusy/SourceIMAP.php
+++ b/lib/Kolab/FreeBusy/SourceIMAP.php
@@ -1,378 +1,379 @@
<?php
/**
* This file is part of the Kolab Server Free/Busy Service
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\FreeBusy;
use Kolab\Config;
use Sabre\VObject;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\FreeBusyGenerator;
use Sabre\VObject\ParseException;
/**
* Implementation of a Free/Busy data source reading from IMAP
* (not yet implemented!)
*/
class SourceIMAP extends Source
{
private $folders = array();
public function __construct($config)
{
if (empty($config['mail_attributes'])) {
if (!empty($config['attributes'])) {
Logger::get('imap')->addWarning("Use of deprecated 'attributes' option. Switch to 'mail_attributes'!");
$config['mail_attributes'] = $config['attributes'];
}
else {
$config['mail_attributes'] = 'mail';
}
}
parent::__construct($config);
// load the Roundcube framework with its autoloader
require_once KOLAB_FREEBUSY_ROOT . '/lib/Roundcube/bootstrap.php';
$rcube = \rcube::get_instance(\rcube::INIT_WITH_DB | \rcube::INIT_WITH_PLUGINS);
// Load plugins
$rcube->plugins->init($rcube);
$rcube->plugins->load_plugins(array(), array('libkolab','libcalendaring'));
// get libvcalendar instance
$this->libvcal = \libcalendaring::get_ical();
}
/**
* @see Source::getFreeBusyData()
*/
public function getFreeBusyData($user, $extended)
{
$log = Logger::get('imap', intval($this->config['loglevel']));
$config = $this->getUserConfig($user);
parse_str(strval($config['query']), $param);
$config += $param;
// log this...
$log->addInfo("Fetching data for ", $config);
// caching is enabled
if (!empty($config['cacheto'])) {
// check for cached data
if ($cached = $this->getCached($config)) {
$log->addInfo("Deliver cached data from " . $config['cacheto']);
return $cached;
}
// touch cache file to avoid multiple requests generating the same data
if (file_exists($config['cacheto'])) {
touch($config['cacheto']);
}
else {
file_put_contents($config['cacheto'], Utils::dummyVFreebusy($user['mail']));
$tempfile = $config['cacheto'];
}
}
// compose a list of user email addresses
$user_email = array();
foreach (Config::convert($this->config['mail_attributes'], Config::ARR) as $key) {
if (!empty($user[$key])) {
$user_email = array_merge($user_email, (array)$user[$key]);
}
}
// synchronize with IMAP and read Kolab event objects
if ($imap = $this->imap_login($config)) {
// target folder is specified in source URI
if ($config['path'] && $config['path'] != '/') {
$folders = array(\kolab_storage::get_folder(substr($config['path'], 1)));
$read_all = true;
}
else { // list all folders of type 'event'
$folders = \kolab_storage::get_folders('event', false);
$read_all = false;
}
$utc = new \DateTimezone('UTC');
$dtstart = Utils::periodStartDT();
$dtend = Utils::periodEndDT();
$calendar = new VObject\Component\VCalendar();
$seen = array();
$this->libvcal->set_timezone($utc);
$log->addInfo("Getting events from IMAP in range", array($dtstart->format('c'), $dtend->format('c')));
$query = array(array('dtstart','<=',$dtend), array('dtend','>=',$dtstart));
foreach ($folders as $folder) {
$count = 0;
$namespace = $folder->get_namespace();
$log->debug('Reading Kolab folder: ' . $folder->name, $folder->get_folder_info());
// skip other user's shared calendars
if (!$read_all && $namespace == 'other') {
continue;
}
// set ACL (temporarily)
- if ($config['acl']) {
+ if ($config['acl'] ?? null) {
$folder->_old_acl = $folder->get_myrights();
$imap->set_acl($folder->name, $config['user'], $config['acl']);
}
foreach ($folder->select($query) as $event) {
//$log->debug('Processing event', $event);
- if ($event['cancelled']) {
+ if ($event['cancelled'] ?? null) {
continue;
}
$event['namespace'] = $namespace;
// only consider shared namespace events if user is a confirmed participant (or organizer)
// skip declined events
if (!$this->check_participation($event, $user_email, $status)
|| ($status != 'ACCEPTED' && $status != 'TENTATIVE')
) {
$log->debug('Skip shared/declined event', array($event['uid'], $event['title']));
continue;
}
// translate all-day dates into absolute UTC times
// FIXME: use server timezone?
if ($event['allday']) {
$utc = new \DateTimeZone('UTC');
if (!empty($event['start'])) {
$event['start']->setTimeZone($utc);
$event['start']->setTime(0,0,0);
}
if (!empty($event['end'])) {
$event['end']->setTimeZone($utc);
$event['end']->setTime(23,59,59);
}
}
// avoid duplicate entries
$key = $event['start']->format('c') . '/' . $event['end']->format('c');
- if ($seen[$key]++) {
+ if ($seen[$key]) {
$log->debug('Skipping duplicate event at ' . $key, array($event['uid'], $event['title']));
continue;
}
+ $seen[$key] = true;
// copied from libvcalendar::_to_ical()
$ve = $this->to_vevent($event, $calendar, $user_email);
- if ($event['recurrence']) {
+ if ($event['recurrence'] ?? null) {
if ($exdates = $event['recurrence']['EXDATE'])
unset($event['recurrence']['EXDATE']);
if ($rdates = $event['recurrence']['RDATE'])
unset($event['recurrence']['RDATE']);
if ($event['recurrence']['FREQ'])
$ve->add('RRULE', \libcalendaring::to_rrule($event['recurrence']));
// consider recurrence exceptions
if (is_array($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
$exception['namespace'] = $namespace;
// register exdate for this occurrence
if ($exception['recurrence_date'] instanceof \DateTime) {
$exdates[] = $exception['recurrence_date'];
}
// add exception to vcalendar container
if (!$exception['cancelled'] && $this->check_participation($exception, $user_email, $status) && $status != 'DECLINED') {
$vex = $this->to_vevent($exception, $calendar, $user_email);
$vex->UID = $event['uid'] . '-' . $i;
$calendar->add($vex);
$log->debug("Adding event exception for processing:\n" . $vex->serialize());
}
}
}
// add EXDATEs each one per line (for Thunderbird Lightning)
if ($exdates) {
foreach ($exdates as $ex) {
if ($ex instanceof \DateTime) {
$exd = clone $event['start'];
$exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j'));
$exd->setTimeZone($utc);
$ve->add($this->libvcal->datetime_prop($calendar, 'EXDATE', $exd, true));
}
}
}
// add RDATEs
if (!empty($rdates)) {
foreach ((array)$rdates as $rdate) {
$ve->add($this->libvcal->datetime_prop($calendar, 'RDATE', $rdate));
}
}
}
// append to vcalendar container
$calendar->add($ve);
$count++;
$log->debug("Adding event for processing:\n" . $ve->serialize());
}
$log->addInfo("Added $count events from folder " . $folder->name);
}
$this->imap_disconnect($imap, $config, $folders);
// feed the calendar object into the free/busy generator
// we must specify a start and end date, because recurring events are expanded. nice!
$fbgen = new FreeBusyGenerator($dtstart, $dtend, $calendar);
// get the freebusy report
$freebusy = $fbgen->getResult();
$freebusy->PRODID = Utils::PRODID;
$freebusy->METHOD = 'PUBLISH';
$freebusy->VFREEBUSY->UID = date('YmdHi') . '-' . substr(md5($user_email[0]), 0, 16);
$freebusy->VFREEBUSY->ORGANIZER = 'mailto:' . $user_email[0];
// serialize to VCALENDAR format
return $freebusy->serialize();
}
// remove (temporary) cache file again
else if ($tempfile) {
unlink($tempfile);
}
return false;
}
/**
* Helper method to establish connection to the configured IMAP backend
*/
private function imap_login($config)
{
$rcube = \rcube::get_instance();
$imap = $rcube->get_storage();
$host = $config['host'];
$port = $config['port'] ?: ($config['scheme'] == 'imaps' ? 993 : 143);
// detect ssl|tls method
if ($config['scheme'] == 'imaps' || $port == 993) {
$ssl = 'imaps';
} elseif ($config['scheme'] == 'tls') {
$ssl = 'tls';
} else {
$ssl = false;
}
// enable proxy authentication
if (!empty($config['proxy_auth'])) {
$imap->set_options(array('auth_cid' => $config['proxy_auth'], 'auth_pw' => $config['pass']));
}
// authenticate user in IMAP
if (!$imap->connect($host, $config['user'], $config['pass'], $port, $ssl)) {
Logger::get('imap')->addWarning("Failed to connect to IMAP server: " . $imap->get_error_code(), $config);
return false;
}
// fake user object to rcube framework
$rcube->set_user(new \rcube_user('0', array('username' => $config['user'])));
return $imap;
}
/**
* Cleanup and close IMAP connection
*/
private function imap_disconnect($imap, $config, $folders)
{
// reset ACL
- if ($config['acl'] && !empty($folders)) {
+ if (($config['acl'] ?? null) && !empty($folders)) {
foreach ($folders as $folder) {
$imap->set_acl($folder->name, $config['user'], $folder->_old_acl);
}
}
$imap->close();
}
/**
* Helper method to build a Sabre/VObject from the gieven event data
*/
private function to_vevent($event, $cal, $user_email)
{
// copied from libvcalendar::_to_ical()
$ve = $cal->create('VEVENT');
$ve->UID = $event['uid'];
if (!empty($event['start']))
$ve->add($this->libvcal->datetime_prop($cal, 'DTSTART', $event['start'], false, false));
if (!empty($event['end']))
$ve->add($this->libvcal->datetime_prop($cal, 'DTEND', $event['end'], false, false));
if (!empty($event['free_busy']))
$ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
if ($this->check_participation($event, $user_email, $status) && $status) {
$ve->add('STATUS', $status);
}
return $ve;
}
/**
* Helper method to check the participation status of the requested user
*/
private function check_participation($event, $user_email, &$status = null)
{
if (is_array($event['organizer']) && !empty($event['organizer']['email'])) {
if (in_array($event['organizer']['email'], $user_email)) {
$is_organizer = true;
}
}
- if (!$is_organizer && is_array($event['attendees'])) {
+ if (!$is_organizer && is_array($event['attendees'] ?? null)) {
foreach ($event['attendees'] as $attendee) {
if (in_array($attendee['email'], $user_email)) {
$status = $attendee['status'];
return true;
}
}
}
if ($is_organizer || $event['namespace'] == 'personal') {
$status = 'ACCEPTED';
if ($event['free_busy'] == 'tentative') {
$status = 'TENTATIVE';
}
else if (!empty($event['status'])) {
$status = $event['status'];
}
return true;
}
return false;
}
}
diff --git a/lib/Kolab/FreeBusy/Utils.php b/lib/Kolab/FreeBusy/Utils.php
index 15dde17..640603d 100644
--- a/lib/Kolab/FreeBusy/Utils.php
+++ b/lib/Kolab/FreeBusy/Utils.php
@@ -1,280 +1,280 @@
<?php
/**
* This file is part of the Kolab Server Free/Busy Service
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\FreeBusy;
/**
* Static calss providing utility functions for the Free/Busy service
*/
class Utils
{
const PRODID = '-//kolab.org//NONSGML Kolab Free-Busy Service 3.2//EN';
/**
* Resolve the given directory to a real path ending with $append
*
* @param string Arbitrary directory directory path
* @param string Make path end with this string/character
* @return string Absolute file system path
*/
public static function abspath($dirname, $append = '')
{
if ($dirname[0] != '/')
$dirname = realpath(KOLAB_FREEBUSY_ROOT . '/' . $dirname);
return rtrim($dirname, '/') . $append;
}
/**
* Returns remote IP address and forwarded addresses if found
*
* @return string Remote IP address(es)
*/
public static function remoteIP()
{
$address = $_SERVER['REMOTE_ADDR'];
// use the NGINX X-Real-IP header, if set
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$address = $_SERVER['HTTP_X_REAL_IP'];
}
// use the X-Forwarded-For header, if set
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$address = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
return $address;
}
/**
* Checks if the given IP address is in one of the provided ranges
*
* @param string IP address
* @param array List of IP ranges/subnets to check against
* @return boolean True if in range, False if not
*/
public static function checkIPRange($ip, $ranges)
{
$ipv6 = strpos($ip, ':') !== false;
$ipbin = $ipv6 ? self::ip6net2bits($ip) : ip2long($ip);
foreach ((array)$ranges as $range) {
// don't compare IPv4 and IPv6 addresses/ranges
$rangev6 = strpos($range, ':') !== false;
if ($ipv6 != $rangev6) {
continue;
}
// special entries that allow all IPs
if ($range === '*' || $range === 'all' || $range === '0/0'
|| $range === '0.0.0.0/0' || $range === '0.0.0.0/0.0.0.0') {
return true;
}
// quick substring check (e.g. 192.168.0.)
if (( $ipv6 && strpos($ipbin, self::ip6net2bits($range)) === 0) ||
(!$ipv6 && strpos($ip, rtrim($range, '*')) === 0)) {
return true;
}
// range from-to specified (IPv4 only)
- list($lower, $upper) = explode('-', $range);
+ list($lower, $upper) = array_pad(explode('-', $range), 2, null);
if (strlen($upper) && !$ipv6) {
if ($ipbin >= ip2long(trim($lower)) && $ipbin <= ip2long(trim($upper))) {
return true;
}
}
// subnet/length is given
- list($subnet, $bits) = explode('/', $range);
+ list($subnet, $bits) = array_pad(explode('/', $range), 2, null);
// IPv6 subnet
if (strlen($bits) && $ipv6) {
$subnetbin = self::ip6net2bits($subnet);
if (substr($ipbin, 0, $bits) === substr($subnetbin, 0, $bits)) {
return true;
}
}
// IPv4 subnet
else if (strlen($bits)) {
$subnet = ip2long($subnet);
$mask = -1 << $bits;
$subnet &= $mask; // just in case the supplied subnet wasn't correctly aligned
if (($ipbin & $mask) == $subnet) {
return true;
}
}
}
return false;
}
/**
* Convert the given IPv6 address to a binary string representation.
* (from http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet)
*/
public static function ip6net2bits($inet)
{
$binaryip = '';
$unpacked = @unpack('A16', inet_pton($inet));
foreach (str_split($unpacked[1]) as $char) {
$binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
return $binaryip;
}
/**
* Returns number of seconds for a specified offset string.
*
* @param string String representation of the offset (e.g. 20min, 5h, 2days, 1week)
* @return int Number of seconds
*/
public static function getOffsetSec($str)
{
if (preg_match('/^([0-9]+)\s*([smhdw])/i', $str, $regs)) {
$amount = (int) $regs[1];
$unit = strtolower($regs[2]);
}
else {
$amount = (int) $str;
$unit = 's';
}
switch ($unit) {
case 'w':
$amount *= 7;
case 'd':
$amount *= 24;
case 'h':
$amount *= 60;
case 'm':
$amount *= 60;
}
return $amount;
}
/**
* Getter for the free/busy period start time
*
* @return int Unix timestamp
*/
public static function periodStart()
{
// use date from HTTP query
if (!empty($_GET['dtstart'])) {
return self::periodStartDT()->format('U');
}
// Should probably be a setting. For now, do 8 weeks in the past
return time() - (60 * 60 * 24 * 7 * 8);
}
/**
* Getter for the free/busy period start time
*
* @return object DateTime instance
*/
public static function periodStartDT()
{
// use date from HTTP query
if (!empty($_GET['dtstart']) &&
($dtstart = filter_input(INPUT_GET, 'dtstart', FILTER_SANITIZE_STRING))
) {
try {
return new \DateTime($dtstart, new \DateTimezone('UTC'));
}
catch (Exception $e) {
// ignore
}
}
// Should probably be a setting. For now, do 8 weeks in the past
return new \DateTime('now - 8 weeks 00:00:00', new \DateTimezone('UTC'));
}
/**
* Getter for the free/busy period end time
*
* @return int Unix timestamp
*/
public static function periodEnd()
{
// use date from HTTP query
if (!empty($_GET['dtend'])) {
return self::periodEndDT()->format('U');
}
// Should probably be a setting. For now, do 16 weeks into the future
return time() + (60 * 60 * 24 * 7 * 16);
}
/**
* Getter for the free/busy period end time
*
* @return object DateTime instance
*/
public static function periodEndDT()
{
// use date from HTTP query
if (!empty($_GET['dtend']) &&
($dtend = filter_input(INPUT_GET, 'dtend', FILTER_SANITIZE_STRING))
) {
try {
return new \DateTime($dtend, new \DateTimezone('UTC'));
}
catch (Exception $e) {
// ignore
}
}
// Should probably be a setting. For now, do 8 weeks in the past
return new \DateTime('now + 16 weeks 00:00:00', new \DateTimezone('UTC'));
}
/**
* Returns an apparent empty Free/Busy list for the given user
*/
public static function dummyVFreebusy($user)
{
$now = time();
$dtformat = 'Ymd\THis\Z';
$dummy = "BEGIN:VCALENDAR\n";
$dummy .= "VERSION:2.0\n";
$dummy .= "PRODID:" . self::PRODID . "\n";
$dummy .= "METHOD:PUBLISH\n";
$dummy .= "BEGIN:VFREEBUSY\n";
$dummy .= "ORGANIZER:MAILTO:" . $user . "\n";
$dummy .= "DTSTAMP:" . gmdate($dtformat) . "\n";
$dummy .= "DTSTART:" . gmdate($dtformat, self::periodStart()) . "\n";
$dummy .= "DTEND:" . gmdate($dtformat, self::periodEnd()) . "\n";
$dummy .= "COMMENT:This is a dummy vfreebusy that indicates an empty calendar\n";
$dummy .= "FREEBUSY:19700101T000000Z/19700101T000000Z\n";
$dummy .= "END:VFREEBUSY\n";
$dummy .= "END:VCALENDAR\n";
return $dummy;
}
}
diff --git a/public_html/index.php b/public_html/index.php
index 603b9a2..5973631 100644
--- a/public_html/index.php
+++ b/public_html/index.php
@@ -1,128 +1,128 @@
<?php
/**
* Kolab Server Free/Busy Service Endpoint
*
* This is the public API to provide Free/Busy information for Kolab users.
*
* @version 0.1.4
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('KOLAB_FREEBUSY_ROOT', realpath('../'));
// configure env for Roundcube framework
define('RCUBE_INSTALL_PATH', KOLAB_FREEBUSY_ROOT . '/');
define('RCUBE_CONFIG_DIR', KOLAB_FREEBUSY_ROOT . '/config/');
define('RCUBE_PLUGINS_DIR', KOLAB_FREEBUSY_ROOT . '/lib/plugins/');
// suppress error notices
ini_set('error_reporting', E_ALL &~ E_NOTICE);
// use composer's autoloader for both dependencies and local lib
$loader = require_once(KOLAB_FREEBUSY_ROOT . '/vendor/autoload.php');
$loader->set('Kolab', array(KOLAB_FREEBUSY_ROOT . '/lib')); // register Kolab namespace
$loader->setUseIncludePath(true); // enable searching the include_path (e.g. for PEAR packages)
use Kolab\Config;
use Kolab\FreeBusy\Utils;
use Kolab\FreeBusy\Logger;
use Kolab\FreeBusy\Directory;
use Kolab\FreeBusy\HTTPAuth;
// load config
$config = Config::get_instance(KOLAB_FREEBUSY_ROOT . '/config');
if ($config->valid()) {
// check for trusted IP first
$remote_ip = Utils::remoteIP();
$trusted_ip = $config->trustednetworks ? Utils::checkIPRange($remote_ip, $config->get('trustednetworks.allow', array(), Config::ARR)) : false;
$log = Logger::get('web');
- $uri = $_SERVER['REDIRECT_URL'];
+ $uri = $_SERVER['REDIRECT_URL'] ?? null;
// we're not always redirected here
if (empty($uri)) {
$uri = $_SERVER['REQUEST_URI'];
$log->addDebug('Request (direct): ' . $uri, array('ip' => $remote_ip, 'trusted' => $trusted_ip));
} else {
$log->addDebug('Request (redirect): ' . $uri, array('ip' => $remote_ip, 'trusted' => $trusted_ip));
}
- list($uri, $args) = explode('?', $uri);
+ list($uri, $args) = array_pad(explode('?', $uri), 2, null);
// check HTTP authentication
if (!$trusted_ip && $config->httpauth) {
$_SERVER['FREEBUSY_URI'] = urldecode(rtrim($uri, '/'));
if (!HTTPAuth::check($config->httpauth)) {
$log->addDebug("Abort with 401 Unauthorized");
header('WWW-Authenticate: Basic realm="Kolab Free/Busy Service"');
header($_SERVER['SERVER_PROTOCOL'] . " 401 Unauthorized", true);
exit;
}
}
#header('Content-type: text/calendar; charset=utf-8', true);
header('Content-type: text/plain; charset=utf-8', true);
// analyse request
- $user = $_SERVER['FREEBUSY_USER'];
+ $user = $_SERVER['FREEBUSY_USER'] ?? null;
$extended = !empty($_SERVER['FREEBUSY_EXTENDED']);
if (!$user) {
$url = array_filter(explode('/', $uri));
$user = strtolower(array_pop($url));
// remove file extension
if (preg_match('/^(.+)\.([ipx]fb)$/i', $user, $m)) {
$user = urldecode($m[1]);
$extended = $m[2] == 'xfb';
}
}
// iterate over directories
foreach ($config->directory as $key => $dirconfig) {
$log->addDebug("Trying directory $key", $dirconfig);
$directory = Directory::factory($dirconfig);
if ($directory && ($fbdata = $directory->getFreeBusyData($user, $extended))) {
$log->addInfo("Found valid data for user $user in directory $key");
echo $fbdata;
exit;
}
}
// return 404 if request was sent from a trusted IP
if ($trusted_ip) {
$log->addDebug("Returning '404 Not Found' for user $user");
header($_SERVER['SERVER_PROTOCOL'] . " 404 Not found", true);
}
else {
$log->addInfo("Returning empty Free/Busy list for user $user");
// Return an apparent empty Free/Busy list.
print Utils::dummyVFreebusy($user);
}
}
// exit with error
# header($_SERVER['SERVER_PROTOCOL'] . " 500 Internal Server Error", true);
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Mar 5, 4:27 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
416257
Default Alt Text
(45 KB)
Attached To
Mode
R28 freebusy
Attached
Detach File
Event Timeline
Log In to Comment