Page MenuHomePhorge

No OneTemporary

diff --git a/README.md b/README.md
index e395922..9db77b0 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,43 @@
INTALLATION PROCEDURE
=====================
This package uses [Composer](http://getcomposer.org) to install and maintain required PHP libraries.
1. Install Composer
Execute this in the project root directory:
-$ curl -s http://getcomposer.org/installer | php
+`$ curl -s http://getcomposer.org/installer | php`
This will create a file named composer.phar in the project directory.
2. Install Dependencies
-$ php composer.phar install
+`$ php composer.phar install`
+
+2a. Link Roundcube framework and plugins
+
+If free-busy data is to be pulled from IMAP directly, the Roundcube framework, config
+and Kolab-specific plugins are required. Symlink them into the project directory:
+
+```
+$ ln -s /usr/share/roundcubemail/program/lib/Roundcube lib/Roundcube
+$ ln -s /usr/share/roundcubemail/plugins lib/plugins
+$ ln -s /etc/roundcubemail/defaults.inc.php config/defaults.inc.php
+$ ln -s /etc/roundcubemail/config.inc.php config/config.inc.php
+```
3. Create local config
Copy the config template file to config/config.ini:
-$ cp config/config.ini.sample config/config.ini
+`$ cp config/config.ini.sample config/config.ini`
Edit the local config/config.ini file according to your setup and taste.
4. Give write access for the webserver user to the 'log' folder:
-$ chown <www-user> log
+`$ chown <www-user> log`
-6. Configure your webserver to point to the 'web' directory of this package as document root.
+5. Configure your webserver to point to the 'web' directory of this package as document root.
diff --git a/composer.json b/composer.json
index 769b88d..91cad3b 100644
--- a/composer.json
+++ b/composer.json
@@ -1,25 +1,25 @@
{
"name": "kolab/free-busy",
"description": "Kolab Free/Busy Service",
"license": "AGPL-3.0",
- "version": "0.1.3",
+ "version": "0.1.4",
"repositories": [
{
"type": "pear",
"url": "http://pear.php.net/"
},
{
"type": "vcs",
"url": "git://git.kolab.org/git/pear/Net_LDAP3"
}
],
"require": {
"php": ">=5.3.3",
"monolog/monolog": "1.2.*",
"kolab/Net_LDAP3": "dev-master",
"pear-pear/Net_LDAP2":">=2.0.12",
"desarrolla2/cache": "dev-master",
"sabre/vobject" : "2.0.*"
},
"minimum-stability": "dev"
}
\ No newline at end of file
diff --git a/config/config.ini.sample b/config/config.ini.sample
index 778f15a..eeb7347 100644
--- a/config/config.ini.sample
+++ b/config/config.ini.sample
@@ -1,61 +1,86 @@
;; Kolab Free/Busy Service configuration
;; Require HTTP authentication to access this service
[httpauth]
;; Example for static auth credentials
; type = static
; username = "<user>"
; password = "<pass>"
;; Example for LDAP-based authentication
; type = ldap
; host = ldap://localhost:389
; bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
; bind_pw = "<service-bind-pw>"
; base_dn = "dc=yourdomain,dc=com"
; filter = "(&(|(mail=%s)(alias=%s)(uid=%s))(objectclass=inetorgperson))" ; optional, %s is replaced by the username
;; Allow privileged access from these IPs
[trustednetworks]
allow = 127.0.0.1,
192.168.0.0/16,
10.10.*,
::1
;; Logging configuration
[log]
driver = file ; supported drivers: file, syslog
path = ./log
name = freebusy
level = 300 ; (100 = Debug, 200 = Info, 300 = Warn, 400 = Error, 500 = Critical)
;; Directories to resolve email addresses and their f/b source locations
;; try local filesystem first
[directory "local"]
type = static
filter = "@yourdomain"
fbsource = file:/var/lib/kolab-freebusy/%s.ifb
+;; check if primary email address hits a cache file (saves LDAP lookups)
+[directory "local-cache"]
+type = static
+fbsource = file:/var/cache/kolab-freebusy/%s.ifb
+expires = 10m
+
;; local Kolab directory server
[directory "kolab-ldap"]
type = ldap
host = ldap://localhost:389
bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
bind_pw = "<service-bind-pw>"
base_dn = "dc=yourdomain,dc=com"
filter = "(&(objectClass=kolabInetOrgPerson)(|(uid=%s)(mail=%s)(alias=%s)))"
attributes = mail, sn
lc_attributes = sn
-fbsource = file:/www/kolab-freebusy/data/%mail.ifb
+fbsource = file:/var/lib/kolab-freebusy/%mail.ifb
+loglevel = 200 ; Info
+
+;; resolve Kolab resources from LDAP and fetch calendar from IMAP
+[directory "kolab-resources"]
+type = ldap
+host = ldap://localhost:389
+bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
+bind_pw = "<service-bind-pw>"
+base_dn = "ou=Resources,dc=yourdomain,dc=com"
+filter = "(&(objectClass=kolabsharedfolder)(mail=%s))"
+attributes = mail, kolabtargetfolder
+fbsource = "imap://cyrus-admin:<admin-pass>@localhost/%kolabtargetfolder?acl=lrs"
+cacheto = /var/cache/kolab-freebusy/%mail.ifb
+expires = 10m
loglevel = 100 ; Debug
;; external MS Exchange 2010 server
[directory "exchange"]
type = static
filter = "@microsoft.com$"
fbsource = https://externalhost/free-busy/%s.ics
format = Exchange2010
+;; further examples of fbsource URIs
+; - fetch data from another server by HTTP(s)
+; fbsource = "https://fb-service-user:imap-password@kolab-server/freebusy/%mail.ifb"
+; - read directoy from a users calendars (all) using IMAP proxy authentication
+; fbsource = "imap://%mail:<admin-pass>@localhost/?proxy_auth=cyrus-admin"
diff --git a/lib/Kolab/FreeBusy/Directory.php b/lib/Kolab/FreeBusy/Directory.php
index 8ac2c24..95ce87a 100644
--- a/lib/Kolab/FreeBusy/Directory.php
+++ b/lib/Kolab/FreeBusy/Directory.php
@@ -1,107 +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)) {
+ 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']);
$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/FormatExchange2010.php b/lib/Kolab/FreeBusy/FormatExchange2010.php
index 55a04dd..f1a09fa 100644
--- a/lib/Kolab/FreeBusy/FormatExchange2010.php
+++ b/lib/Kolab/FreeBusy/FormatExchange2010.php
@@ -1,158 +1,158 @@
<?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 Sabre\VObject\Reader as VCalReader;
use Sabre\VObject\FreeBusyGenerator;
use Sabre\VObject\ParseException;
use Desarrolla2\Cache\Cache;
use Desarrolla2\Cache\Adapter\File as FileCache;
use \SimpleXMLElement;
/**
* Implementation of a data converter reading Exchange 2010 Internet Calendar Publishing files
*/
class FormatExchange2010 extends Format
{
private $tzmap;
/**
* @see Format::toVCalendar()
*/
public function toVCalendar($input)
{
// convert Microsoft timezone identifiers to Olson standard
// do this before parsing to create correct DateTime values
$input = preg_replace_callback('/(TZID[=:])([-\w ]+)\b/i', array($this, 'convertTZID'), $input);
try {
// parse vcalendar data
$calendar = VCalReader::read($input);
// map X-MICROSOFT-CDO-* attributes into iCal equivalents
foreach ($calendar->VEVENT as $vevent) {
if ($busystatus = reset($vevent->select('X-MICROSOFT-CDO-BUSYSTATUS'))) {
$vevent->STATUS->value = $busystatus->value;
}
}
// feed the calendar object into the free/busy generator
// we must specify a start and end date, because recurring events are expanded. nice!
$utc = new \DateTimezone('UTC');
$fbgen = new FreeBusyGenerator(
new \DateTime('now - 8 weeks 00:00:00', $utc),
new \DateTime('now + 16 weeks 00:00:00', $utc),
$calendar
);
// get the freebusy report
$freebusy = $fbgen->getResult();
- $freebusy->PRODID = '-//kolab.org//NONSGML Kolab Server 3//EN';
+ $freebusy->PRODID = Utils::PRODID;
$freebusy->METHOD = 'PUBLISH';
// serialize to VCALENDAR format
return $freebusy->serialize();
}
catch (ParseException $e) {
Logger::get('format.Exchange2010')->addError("iCal parse error: " . $e->getMessage());
}
return false;
}
/**
* preg_replace callback function to map Timezone identifiers
*/
private function convertTZID($m)
{
if (!isset($this->tzmap)) {
$this->getTZMAP();
}
$key = strtolower($m[2]);
if ($this->tzmap[$key]) {
$m[2] = $this->tzmap[$key];
}
return $m[1] . $m[2] . $m[3];
}
/**
* Generate a Microsoft => Olson Timezone mapping table from an official source
*/
private function getTZMAP()
{
if (!isset($this->tzmap)) {
$log = Logger::get('format.Exchange2010');
$cache = new Cache(new FileCache(sys_get_temp_dir()));
// read from cache
$this->tzmap = $cache->get('windows-timezones');
// fetch timezones map from source
if (empty($this->tzmap)) {
$this->tzmap = array();
$zones_url = 'http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml';
if ($xml = @file_get_contents($zones_url)) {
try {
$zonedata = new SimpleXMLElement($xml, LIBXML_NOWARNING | LIBXML_NOERROR);
foreach ($zonedata->windowsZones[0]->mapTimezones[0]->mapZone as $map) {
$other = strtolower(strval($map['other']));
$region = strval($map['territory']);
$words = explode(' ', $other);
$olson = explode(' ', strval($map['type']));
// skip invalid entries
if (empty($other) || empty($olson))
continue;
// create an entry for all substrings
for ($i = 1; $i <= count($words); $i++) {
$last = $i == count($words);
$key = join(' ', array_slice($words, 0, $i));
if ($region == '001' || ($last && empty($this->tzmap[$key]))) {
$this->tzmap[$key] = $olson[0];
}
}
}
// cache the mapping for one week
$cache->set('windows-timezones', $this->tzmap, 7 * 86400);
$log->addInfo("Updated Windows Timezones Map from source", array($zones_url));
}
catch (\Exception $e) {
$log->addError("Failed parse Windows Timezones Map: " . $e->getMessage());
}
}
else {
$log->addError("Failed to load Windows Timezones Map from source", array($zones_url));
}
}
}
return $this->tzmap;
}
}
diff --git a/lib/Kolab/FreeBusy/Source.php b/lib/Kolab/FreeBusy/Source.php
index 3a256a3..a0a915d 100644
--- a/lib/Kolab/FreeBusy/Source.php
+++ b/lib/Kolab/FreeBusy/Source.php
@@ -1,88 +1,116 @@
<?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;
/**
* Abstract class to fetch free/busy data from a specific source
*/
abstract class Source
{
protected $config = array();
+ protected $cached = false;
/**
* Factory method creating an instace of Source according to config
*
+ * @param string Source URI
* @param array Hash array with config
*/
- public static function factory($url)
+ public static function factory($url, $conf)
{
$config = parse_url($url);
$config['url'] = $url;
switch ($config['scheme']) {
- case 'file': return new SourceFile($config);
+ case 'file': return new SourceFile($config + $conf);
case 'imap':
- case 'imaps': return new SourceIMAP($config);
+ case 'imaps': return new SourceIMAP($config + $conf);
case 'http':
- case 'https': return new SourceURL($config);
+ case 'https': return new SourceURL($config + $conf);
}
Logger::get('source')->addError("Invalid source configuration: " . $url);
return null;
}
/**
* Default constructor
*/
public function __construct($config)
{
$this->config = $config;
}
/**
* Retrieve free/busy data for the given user
*
* @param array Hash array with user attributes
*/
abstract public function getFreeBusyData($user, $extended);
/**
* Replace all %varname strings in config with values from $user
*/
protected function getUserConfig($user)
{
$config = array();
foreach ($this->config as $k => $val) {
if (is_string($val) && strpos($val, '%') !== false) {
$val = preg_replace_callback(
'/%\{?([a-z0-9]+)\}?/',
function($m) use ($user) { return $user[$m[1]]; },
$val);
}
$config[$k] = $val;
}
return $config;
}
+
+ /**
+ * Helper method to check if a cached file exists and is still valid
+ *
+ * @param array Hash array with (replaced) config properties
+ * @return string Cached free-busy data or false if cache file doesn't exist or is expired
+ */
+ protected function getCached($config)
+ {
+ if ($config['cacheto'] && file_exists($config['cacheto'])) {
+ if (empty($config['expires']) || filemtime($config['cacheto']) + Utils::getOffsetSec($config['expires']) >= time()) {
+ $this->cached = true;
+ return file_get_contents($config['cacheto']);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the value of the 'cached' flag
+ */
+ public function isCached()
+ {
+ return $this->cached;
+ }
}
\ No newline at end of file
diff --git a/lib/Kolab/FreeBusy/SourceFile.php b/lib/Kolab/FreeBusy/SourceFile.php
index af25b6e..5106322 100644
--- a/lib/Kolab/FreeBusy/SourceFile.php
+++ b/lib/Kolab/FreeBusy/SourceFile.php
@@ -1,47 +1,50 @@
<?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;
/**
* Implementation of a Free/Busy data source reading from the local file system
*/
class SourceFile extends Source
{
/**
* @see Source::getFreeBusyData()
*/
public function getFreeBusyData($user, $extended)
{
// get source config with placeholders replaced
$config = $this->getUserConfig($user);
// deliver file contents if found
if (is_readable($config['path'])) {
- return file_get_contents($config['path']);
+ // check expiration if configured
+ if (empty($this->config['expires']) || filemtime($config['path']) + Utils::getOffsetSec($this->config['expires']) > time()) {
+ return file_get_contents($config['path']);
+ }
}
// not found
return false;
}
}
diff --git a/lib/Kolab/FreeBusy/SourceIMAP.php b/lib/Kolab/FreeBusy/SourceIMAP.php
index c545423..494a4d5 100644
--- a/lib/Kolab/FreeBusy/SourceIMAP.php
+++ b/lib/Kolab/FreeBusy/SourceIMAP.php
@@ -1,41 +1,257 @@
<?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 Sabre\VObject;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\FreeBusyGenerator;
+use Sabre\VObject\ParseException;
+
+// 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/');
+
+
+
/**
* Implementation of a Free/Busy data source reading from IMAP
* (not yet implemented!)
*/
class SourceIMAP extends Source
{
+ private $folders = array();
+
+ public function __construct($config)
+ {
+ 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'));
+ }
+
/**
* @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']));
+ }
+ }
+
+ // 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;
+ }
+
+ // make \libvcalendar class available
+ \libcalendaring::get_ical();
+
+ $utc = new \DateTimezone('UTC');
+ $dtstart = new \DateTime('now - 8 weeks 00:00:00', $utc);
+ $dtend = new \DateTime('now + 16 weeks 00:00:00', $utc);
+ $calendar = VObject\Component::create('VCALENDAR');
+
+ $query = array(array('dtstart','>',$dtstart), array('dtend','<',$dtend));
+ foreach ($folders as $folder) {
+ $log->debug('Reading Kolab folder: ' . $folder->name, $folder->get_folder_info());
+
+ // skip other user's shared calendars
+ if (!$read_all && $folder->get_namespace() == 'other') {
+ continue;
+ }
+
+ // set ACL (temporarily)
+ if ($config['acl']) {
+ $folder->_old_acl = $folder->get_myrights();
+ $imap->set_acl($folder->name, $config['user'], $config['acl']);
+ }
+
+ foreach ($folder->select($query) as $event) {
+ $log->debug('Found event', $event);
+
+ if ($event['cancelled'])
+ continue;
+
+ // TODO: only consider shared namespace events if user is a confirmed participant
+ if (!$read_all && $folder->get_namespace() == 'shared') {
+ continue; // skip all for now
+ }
+
+ // copied from libvcalendar::_to_ical()
+ $ve = VObject\Component::create('VEVENT');
+
+ // all-day events end the next day
+ if ($event['allday'] && !empty($event['end'])) {
+ $event['end'] = clone $event['end'];
+ $event['end']->add(new \DateInterval('P1D'));
+ $event['end']->_dateonly = true;
+ }
+ if (!empty($event['start']))
+ $ve->add(\libvcalendar::datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday']));
+ if (!empty($event['end']))
+ $ve->add(\libvcalendar::datetime_prop('DTEND', $event['end'], false, (bool)$event['allday']));
+
+ if (!empty($event['free_busy']))
+ $ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
+
+ if ($event['free_busy'] == 'tentative')
+ $ve->add('STATUS', 'TENTATIVE');
+ else if (!empty($event['status']))
+ $ve->add('STATUS', $event['status']);
+
+ if ($event['recurrence']) {
+ 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']));
+
+ // 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(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z')));
+ }
+ }
+ }
+ // add RDATEs
+ if (!empty($rdates)) {
+ $sample = \libvcalendar::datetime_prop('RDATE', $rdates[0]);
+ $rdprop = new VObject\Property\MultiDateTime('RDATE', null);
+ $rdprop->setDateTimes($rdates, $sample->getDateType());
+ $ve->add($rdprop);
+ }
+ }
+
+ // append to vcalendar container
+ $calendar->add($ve);
+ }
+ }
+
+ $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->ORGANIZER = 'mailto:' . $user['mail'];
+
+ // serialize to VCALENDAR format
+ return $freebusy->serialize();
+ }
+ // remove (temporary) cache file again
+ else if (!empty($config['cacheto']) && file_exists($config['cacheto'])) {
+ unlink($config['cacheto']);
+ }
+
+ 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);
+ $ssl = $config['scheme'] == 'imaps' || $port == 993;
+
+ // 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)) {
+ foreach ($folders as $folder) {
+ $imap->set_acl($folder->name, $config['user'], $folder->_old_acl);
+ }
+ }
- // TODO: implement this
+ $imap->close();
}
}
diff --git a/lib/Kolab/FreeBusy/Utils.php b/lib/Kolab/FreeBusy/Utils.php
index 3fea324..0d76ab6 100644
--- a/lib/Kolab/FreeBusy/Utils.php
+++ b/lib/Kolab/FreeBusy/Utils.php
@@ -1,139 +1,204 @@
<?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>
+ * 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
+ * @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;
}
// 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);
if (strlen($upper) && !$ipv6) {
if ($ipbin >= ip2long(trim($lower)) && $ipbin <= ip2long(trim($upper))) {
return true;
}
}
// subnet/length is given
list($subnet, $bits) = explode('/', $range);
// 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;
+ }
+
+ /**
+ * Returns an apparent empty Free/Busy list for the given user
+ */
+ public static function dummyVFreebusy($user)
+ {
+ $now = time();
+ $dtformat = 'Ymd\THis\Z';
+
+ // NOTE: The following settings should probably correspond with
+ // whatever period of time kolab-freebusyd thinks it should use.
+
+ // Should probably be a setting. For now, do 8 weeks in the past
+ $start = $now - (60 * 60 * 24 * 7 * 8);
+ // Should probably be a setting. For now, do 16 weeks into the future
+ $end = $now + (60 * 60 * 24 * 7 * 16);
+
+ $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, $start) . "\n";
+ $dummy .= "DTEND:" . gmdate($dtformat, $end) . "\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;
+ }
}
\ No newline at end of file
diff --git a/public_html/index.php b/public_html/index.php
index 14c549c..5f8058b 100644
--- a/public_html/index.php
+++ b/public_html/index.php
@@ -1,138 +1,115 @@
<?php
/**
* Kolab Server Free/Busy Service Endpoint
*
* This is the public API to provide Free/Busy information for Kolab users.
*
- * @version 0.1.3
+ * @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('../'));
// 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
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'];
// 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));
}
// check HTTP authentication
if (!$trusted_ip && $config->httpauth) {
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
$url = array_filter(explode('/', $uri));
$user = strtolower(array_pop($url));
$action = strtolower(array_pop($url));
$extended = false;
// remove file extension
if (preg_match('/^(.+)\.([ipx]fb)$/i', $user, $m)) {
$user = $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");
- $now = time();
- $dtformat = 'Ymd\THis\Z';
-
- // NOTE: The following settings should probably correspond with
- // whatever period of time kolab-freebusyd thinks it should use.
-
- // Should probably be a setting. For now, do 8 weeks in the past
- $start = $now - (60 * 60 * 24 * 7 * 8);
- // Should probably be a setting. For now, do 16 weeks into the future
- $end = $now + (60 * 60 * 24 * 7 * 16);
-
// Return an apparent empty Free/Busy list.
- print "BEGIN:VCALENDAR\n";
- print "VERSION:2.0\n";
- print "PRODID:-//kolab.org//NONSGML Kolab Server 3//EN\n";
- print "METHOD:PUBLISH\n";
- print "BEGIN:VFREEBUSY\n";
- print "ORGANIZER:MAILTO:" . $user . ".ifb\n";
- print "DTSTAMP:" . gmdate($dtformat) . "\n";
- print "DTSTART:" . gmdate($dtformat, $start) . "\n";
- print "DTEND:" . gmdate($dtformat, $end) . "\n";
- print "COMMENT:This is a dummy vfreebusy that indicates an empty calendar\n";
- print "FREEBUSY:19700101T000000Z/19700101T000000Z\n";
- print "END:VFREEBUSY\n";
- print "END:VCALENDAR\n";
+ print Utils::dummyVFreebusy($user);
}
}
// exit with error
# header($_SERVER['SERVER_PROTOCOL'] . " 500 Internal Server Error", true);

File Metadata

Mime Type
text/x-diff
Expires
Mon, Sep 15, 8:23 PM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
287610
Default Alt Text
(38 KB)

Event Timeline