summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2013-01-24 20:52:10 (GMT)
committerThomas Bruederli <bruederli@kolabsys.com>2013-01-24 20:52:10 (GMT)
commitaae8de625b88efdda5c587e30ac46929ab906609 (patch)
treed91d0968d874abddde01ab8879cff89f4f1b267a
parenta0eb1a853418b886d105169bec65a8e01c164119 (diff)
downloadkolab-freebusy-aae8de625b88efdda5c587e30ac46929ab906609.tar.gz
Implement an Exchange 2010 format converter using the Sabre VObject lib and timezone mappings from unicode.org
-rw-r--r--composer.json4
-rw-r--r--config/config.ini.sample7
-rw-r--r--lib/Kolab/FreeBusy/Directory.php10
-rw-r--r--lib/Kolab/FreeBusy/Format.php46
-rw-r--r--lib/Kolab/FreeBusy/FormatExchange2010.php135
5 files changed, 199 insertions, 3 deletions
diff --git a/composer.json b/composer.json
index 4343c2a..3dae001 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,9 @@
"require": {
"php": ">=5.3.3",
"monolog/monolog": "1.2.*",
- "kolab/Net_LDAP3": "dev-master"
+ "kolab/Net_LDAP3": "dev-master",
+ "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 3dfe7cf..fbcbdfa 100644
--- a/config/config.ini.sample
+++ b/config/config.ini.sample
@@ -51,3 +51,10 @@ attributes[] = mail
fbsource = file:/www/kolab-freebusy/data/%mail.ifb
loglevel = 100 ; Debug
+;; external MS Exchange 2010 server
+[directory "exchange"]
+type = static
+filter = "@microsoft.com$"
+fbsource = https://externalhost/free-busy/%s.ics
+format = Exchange2010
+
diff --git a/lib/Kolab/FreeBusy/Directory.php b/lib/Kolab/FreeBusy/Directory.php
index 67e6d8d..97fee75 100644
--- a/lib/Kolab/FreeBusy/Directory.php
+++ b/lib/Kolab/FreeBusy/Directory.php
@@ -52,8 +52,14 @@ abstract class Directory
if ($user = $this->resolve($user)) {
$fbsource = $this->config['fbsource'];
if ($source = Source::Factory($fbsource)) {
- // foward request to Source instance
- return $source->getFreeBusyData($user, $extended);
+ // forward request to Source instance
+ if ($data = $source->getFreeBusyData($user, $extended)) {
+ // send data through the according format converter
+ $converter = Format::factory($this->config['format']);
+ $data = $converter->toVCalendar($data);
+ }
+
+ return $data;
}
}
diff --git a/lib/Kolab/FreeBusy/Format.php b/lib/Kolab/FreeBusy/Format.php
new file mode 100644
index 0000000..b5ebfda
--- /dev/null
+++ b/lib/Kolab/FreeBusy/Format.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kolab\FreeBusy;
+
+/**
+ * Base class to handle free/busy data format conversion
+ */
+class Format
+{
+ protected $config;
+
+ /**
+ * Factory method creating an instace of Format according to the given type
+ *
+ * @param string Format identifier
+ */
+ public static function factory($type)
+ {
+ switch (strtolower($type)) {
+ case 'exchange2010':
+ return new FormatExchange2010;
+
+ default:
+ if (!empty($type)) {
+ Logger::get('format')->addError("Unknown format type '$type'!");
+ }
+ return new Format;
+ }
+
+ return null;
+ }
+
+ /**
+ * Convert the given free/busy data stream to iCal format
+ *
+ * @param string Input data stream
+ * @return string iCal formatted free/busy list
+ */
+ public function toVCalendar($input)
+ {
+ // default: no format changes
+ return $input;
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Kolab/FreeBusy/FormatExchange2010.php b/lib/Kolab/FreeBusy/FormatExchange2010.php
new file mode 100644
index 0000000..8aec7e4
--- /dev/null
+++ b/lib/Kolab/FreeBusy/FormatExchange2010.php
@@ -0,0 +1,135 @@
+<?php
+
+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!
+ $fbgen = new FreeBusyGenerator(
+ new \DateTime('now - 8 weeks'),
+ new \DateTime('now + 16 weeks'),
+ $calendar
+ );
+
+ // get the freebusy report
+ $freebusy = $fbgen->getResult();
+ $freebusy->PRODID->value = '-//kolab.org//NONSGML Kolab Server 3//EN';
+
+ // 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;
+ }
+}