Page MenuHomePhorge

No OneTemporary

Size
45 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline