summaryrefslogtreecommitdiff
path: root/lib/Kolab/FreeBusy
diff options
context:
space:
mode:
authorThomas Bruederli <thomas@roundcube.net>2014-05-06 15:57:22 (GMT)
committerThomas Bruederli <thomas@roundcube.net>2014-05-06 15:57:22 (GMT)
commit886215e40e6fd4e5c8994d069bc390840ab0e0bf (patch)
tree3405c626db8274fbdb0067501cef86a56c2148db /lib/Kolab/FreeBusy
parent8599e8f0925aa994fca1cc0530950ff8b97b1efb (diff)
downloadkolab-freebusy-886215e40e6fd4e5c8994d069bc390840ab0e0bf.tar.gz
- Implement source type 'imap' that fetches free-busy data right from IMAP folders.
This requires the Roundcube framework, config and plugins to be symlinked. Described in README - Add caching option for all sources + expire option for file source.
Diffstat (limited to 'lib/Kolab/FreeBusy')
-rw-r--r--lib/Kolab/FreeBusy/Directory.php15
-rw-r--r--lib/Kolab/FreeBusy/FormatExchange2010.php2
-rw-r--r--lib/Kolab/FreeBusy/Source.php36
-rw-r--r--lib/Kolab/FreeBusy/SourceFile.php5
-rw-r--r--lib/Kolab/FreeBusy/SourceIMAP.php218
-rw-r--r--lib/Kolab/FreeBusy/Utils.php69
6 files changed, 335 insertions, 10 deletions
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
@@ -74,12 +74,25 @@ abstract class Directory
// 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;
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
@@ -69,7 +69,7 @@ class FormatExchange2010 extends Format
// 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
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
@@ -29,22 +29,24 @@ namespace Kolab\FreeBusy;
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);
@@ -85,4 +87,30 @@ abstract class Source
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
@@ -38,7 +38,10 @@ class SourceFile extends Source
// 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
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
@@ -23,19 +23,235 @@
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
@@ -5,7 +5,7 @@
*
* @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
@@ -28,6 +28,8 @@ namespace Kolab\FreeBusy;
*/
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
*
@@ -69,7 +71,7 @@ class Utils
* 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)
@@ -136,4 +138,67 @@ class Utils
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