composer.json config/config.ini.sample lib/Kolab

Thomas Brüderli bruederli at kolabsys.com
Thu Jan 24 21:52:32 CET 2013


 composer.json                             |    4 
 config/config.ini.sample                  |    7 +
 lib/Kolab/FreeBusy/Directory.php          |   10 +-
 lib/Kolab/FreeBusy/Format.php             |   46 ++++++++++
 lib/Kolab/FreeBusy/FormatExchange2010.php |  135 ++++++++++++++++++++++++++++++
 5 files changed, 199 insertions(+), 3 deletions(-)

New commits:
commit aae8de625b88efdda5c587e30ac46929ab906609
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jan 24 21:52:10 2013 +0100

    Implement an Exchange 2010 format converter using the Sabre VObject lib and timezone mappings from unicode.org

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;
+	}
+}





More information about the commits mailing list