Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1975020
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
38 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R28 freebusy
Attached
Detach File
Event Timeline
Log In to Comment