summaryrefslogtreecommitdiff
path: root/lib/Kolab
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2013-03-07 16:07:08 (GMT)
committerThomas Bruederli <bruederli@kolabsys.com>2013-03-07 16:07:08 (GMT)
commit6b8cdd9ab0d884fa27a0ad4da39e4981bdecf513 (patch)
treef551dc55fdd84ff90b158efad96cadfafcdef24b /lib/Kolab
parent598cc5aa7625257f2bd06bdc96836d633b49f452 (diff)
downloadiRony-6b8cdd9ab0d884fa27a0ad4da39e4981bdecf513.tar.gz
Add support for CardDAV
Diffstat (limited to 'lib/Kolab')
-rw-r--r--lib/Kolab/CardDAV/ContactsBackend.php687
-rw-r--r--lib/Kolab/Utils/VObjectUtils.php69
2 files changed, 756 insertions, 0 deletions
diff --git a/lib/Kolab/CardDAV/ContactsBackend.php b/lib/Kolab/CardDAV/ContactsBackend.php
new file mode 100644
index 0000000..5340be8
--- /dev/null
+++ b/lib/Kolab/CardDAV/ContactsBackend.php
@@ -0,0 +1,687 @@
+<?php
+
+/**
+ * SabreDAV Contacts backend for Kolab.
+ *
+ * @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\CardDAV;
+
+use \rcube;
+use \rcube_charset;
+use \kolab_storage;
+use Sabre\DAV;
+use Sabre\CardDAV;
+use Sabre\VObject;
+use Kolab\Utils\VObjectUtils;
+
+/**
+ * Kolab Contacts backend.
+ *
+ * Checkout the Sabre\CardDAV\Backend\BackendInterface for all the methods that must be implemented.
+ */
+class ContactsBackend extends CardDAV\Backend\AbstractBackend
+{
+ private $sources;
+ private $folders;
+ private $useragent;
+
+
+ /**
+ * Read available contact folders from server
+ */
+ private function _read_sources()
+ {
+ // already read sources
+ if (isset($this->sources))
+ return $this->sources;
+
+ // get all folders that have "contact" type
+ $folders = kolab_storage::get_folders('contact');
+ $this->sources = $this->folders = array();
+
+ // convert to UTF8 and sort
+ $names = array();
+ foreach ($folders as $folder) {
+ $folders[$folder->name] = $folder;
+ $names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP');
+ }
+
+ asort($names, SORT_LOCALE_STRING);
+
+ foreach ($names as $utf7name => $name) {
+ $id = urlencode($utf7name);
+ $folder = $this->folders[$id] = $folders[$utf7name];
+ $fdata = $folder->get_imap_data(); // fetch IMAP folder data for CTag generation
+ $this->sources[$id] = array(
+ 'id' => $id,
+ 'uri' => $id,
+ '{DAV:}displayname' => $name,
+ '{http://calendarserver.org/ns/}getctag' => sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], time(), $fdata['UIDNEXT']),
+ '{urn:ietf:params:xml:ns:caldav}supported-address-data' => new CardDAV\Property\SupportedAddressData(),
+ );
+
+ }
+
+ return $this->sources;
+ }
+
+
+ /**
+ * Getter for a kolab_storage_folder representing the address book for the given ID
+ *
+ * @param string Folder ID
+ * @return object kolab_storage_folder instance
+ */
+ public function get_storage_folder($id)
+ {
+ if ($this->folders[$id]) {
+ return $this->folders[$id];
+ }
+ else {
+ $storage = kolab_storage::get_folder(urldecode($id));
+ return !PEAR::isError($this->storage) ? $storage : null;
+ }
+ }
+
+
+ /**
+ * Returns the list of addressbooks for a specific user.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getAddressBooksForUser($principalUri)
+ {
+ $this->_read_sources();
+
+ $addressBooks = array();
+ foreach ($this->sources as $id => $source) {
+ $source['principaluri'] = $principalUri;
+ $addressBooks[] = $source;
+ }
+
+ return $addressBooks;
+ }
+
+
+ /**
+ * Updates an addressbook's properties
+ *
+ * See Sabre\DAV\IProperties for a description of the mutations array, as
+ * well as the return value.
+ *
+ * @param mixed $addressBookId
+ * @param array $mutations
+ * @see Sabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateAddressBook($addressBookId, array $mutations)
+ {
+ // TODO: implement this
+ return false;
+ }
+
+ /**
+ * Creates a new address book
+ *
+ * @param string $principalUri
+ * @param string $url Just the 'basename' of the url.
+ * @param array $properties
+ * @return void
+ */
+ public function createAddressBook($principalUri, $url, array $properties)
+ {
+ // TODO: implement this
+ }
+
+ /**
+ * Deletes an entire addressbook and all its contents
+ *
+ * @param int $addressBookId
+ * @return void
+ */
+ public function deleteAddressBook($addressBookId)
+ {
+ // TODO: implement this
+ }
+
+ /**
+ * Returns all cards for a specific addressbook id.
+ *
+ * This method should return the following properties for each card:
+ * * carddata - raw vcard data
+ * * uri - Some unique url
+ * * lastmodified - A unix timestamp
+ * * etag - A unique etag. This must change every time the card changes.
+ * * size - The size of the card in bytes.
+ *
+ * If these last two properties are provided, less time will be spent
+ * calculating them. If they are specified, you can also ommit carddata.
+ * This may speed up certain requests, especially with large cards.
+ *
+ * @param mixed $addressbookId
+ * @return array
+ */
+ public function getCards($addressbookId)
+ {
+ console(__METHOD__, $addressbookId);
+
+ $query = array();
+ $cards = array();
+ if ($storage = $this->get_storage_folder($addressbookId)) {
+ foreach ((array)$storage->select($query) as $contact) {
+ $cards[] = array(
+ 'id' => $contact['uid'],
+ 'uri' => $contact['uid'] . '.vcf',
+ 'lastmodified' => $contact['changed']->format('U'),
+// 'calendarid' => $addressbookId,
+ 'etag' => self::_get_etag($contact),
+ 'size' => $contact['_size'],
+ );
+ }
+ }
+
+ return $cards;
+ }
+
+ /**
+ * Returns a specfic card.
+ *
+ * The same set of properties must be returned as with getCards. The only
+ * exception is that 'carddata' is absolutely required.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return array
+ */
+ public function getCard($addressBookId, $cardUri)
+ {
+ console(__METHOD__, $addressBookId, $cardUri);
+
+ $uid = basename($cardUri, '.vcf');
+ $storage = $this->get_storage_folder($addressBookId);
+
+ if ($storage && ($contact = $storage->get_object($uid))) {
+ return array(
+ 'id' => $contact['uid'],
+ 'uri' => $contact['uid'] . '.vcf',
+ 'lastmodified' => $contact['changed']->format('U'),
+// 'calendarid' => $calendarId,
+ 'carddata' => $this->_to_vcard($contact),
+ 'etag' => self::_get_etag($contact),
+ );
+ }
+
+ return array();
+ }
+
+ /**
+ * Creates a new card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag is for the
+ * newly created resource, and must be enclosed with double quotes (that
+ * is, the string itself must contain the double quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function createCard($addressBookId, $cardUri, $cardData)
+ {
+ console(__METHOD__, $addressbookId, $cardUri, $cardData);
+
+ $uid = basename($cardUri, '.vcf');
+ $storage = $this->get_storage_folder($addressBookId);
+ $object = $this->parse_vcard($cardData, $uid);
+
+ if ($object['uid'] == $uid) {
+ $success = $storage->save($object, 'contact');
+ if (!$success) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving contact object to Kolab server"),
+ true, false);
+ }
+ }
+ else {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error creating contact object: UID doesn't match object URI"),
+ true, false);
+ }
+
+ // return new Etag
+ return $success ? self::_get_etag($object) : null;
+ }
+
+ /**
+ * Updates a card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag should
+ * match that of the updated resource, and must be enclosed with double
+ * quotes (that is: the string itself must contain the actual quotes).
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function updateCard($addressBookId, $cardUri, $cardData)
+ {
+ console(__METHOD__, $addressbookId, $cardUri, $cardData);
+
+ $uid = basename($cardUri, '.vcf');
+ $storage = $this->get_storage_folder($addressBookId);
+ $object = $this->parse_vcard($cardData, $uid);
+
+ // sanity check
+ if ($object['uid'] != $uid) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error creating contact object: UID doesn't match object URI"),
+ true, false);
+
+ return null;
+ }
+
+ // copy meta data (starting with _) from old object
+ $old = $storage->get_object($uid);
+ foreach ((array)$old as $key => $val) {
+ if (!isset($object[$key]) && $key[0] == '_')
+ $object[$key] = $val;
+ }
+
+ // save object
+ $saved = $storage->save($object, 'contact', $uid);
+ if (!$saved) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving contact object to Kolab server"),
+ true, false);
+
+ return null;
+ }
+
+ // return new Etag
+ return self::_get_etag($object);
+ }
+
+ /**
+ * Deletes a card
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return bool
+ */
+ public function deleteCard($addressBookId, $cardUri)
+ {
+ console(__METHOD__, $addressbookId, $cardUri);
+ // TODO: implement this
+ }
+
+
+ /**
+ * Set User-Agent string of the connected client
+ */
+ public function setUserAgent($uastring)
+ {
+ $ua_classes = array(
+ 'thunderbird' => 'Thunderbird/\d',
+ );
+
+ foreach ($ua_classes as $class => $regex) {
+ if (preg_match("!$regex!", $uastring)) {
+ $this->useragent = $class;
+ break;
+ }
+ }
+ }
+
+
+ /********** Data conversion utilities ***********/
+
+ private $phonetypes = array(
+ 'main' => 'voice',
+ 'homefax' => 'fax',
+ 'workfax' => 'fax',
+ 'mobile' => 'cell',
+ 'other' => 'textphone',
+ );
+
+
+ /**
+ * Parse the given VCard string into a hash array kolab_format_contact can handle
+ *
+ * @param string VCard data block
+ * @return array Hash array with contact properties or null on failure
+ */
+ private function parse_vcard($cardData, $uid)
+ {
+ try {
+ VObject\Property::$classMap['REV'] = 'Sabre\\VObject\\Property\\DateTime';
+
+ $vobject = VObject\Reader::read($cardData, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
+
+ if ($vobject && $vobject->name == 'VCARD') {
+ $contact = $this->_to_array($vobject);
+ if (!empty($contact['uid'])) {
+ return $contact;
+ }
+ }
+ }
+ catch (VObject\ParseException $e) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "VCard data parse error: " . $e->getMessage()),
+ true, false);
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Build a valid VCard format block from the given contact record
+ *
+ * @param array Hash array with contact properties from libkolab
+ * @return string VCARD string containing the contact data
+ */
+ private function _to_vcard($contact)
+ {
+ $vc = VObject\Component::create('VCARD');
+ $vc->version = '3.0';
+ $vc->prodid = '-//Kolab DAV Server ' .KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . CardDAV\Version::VERSION . '//EN';
+
+ $vc->add('UID', $contact['uid']);
+ $vc->add('FN', $contact['name']);
+
+ $n = VObject\Property::create('N');
+ $n->setParts(array($contact['surname'], $contact['firstname'], $contact['middlename'], $contact['prefix'], $contact['suffix']));
+ $vc->add($n);
+
+ if (!empty($contact['nickname']))
+ $vc->add('NICKNAME', $contact['nickname']);
+ if (!empty($contact['jobtitle']))
+ $vc->add('TITLE', $contact['jobtitle']);
+ if (!empty($contact['profession']))
+ $vc->add('X-PROFESSION', $contact['profession']);
+
+ if (!empty($contact['organization']) || !empty($contact['department'])) {
+ $org = VObject\Property::create('ORG');
+ $org->setParts(array($contact['organization'], $contact['department']));
+ $vc->add($org);
+ }
+
+ // TODO: save as RELATED
+ if (!empty($contact['assistant']))
+ $vc->add('X-ASSISTANT', join(',', (array)$contact['assistant']));
+ if (!empty($contact['manager']))
+ $vc->add('X-MANAGER', join(',', (array)$contact['manager']));
+ if (!empty($contact['spouse']))
+ $vc->add('X-SPOUSE', $contact['spouse']);
+ if (!empty($contact['children']))
+ $vc->add('X-CHILDREN', join(',', (array)$contact['children']));
+
+ foreach ((array)$contact['email'] as $email) {
+ // TODO: add types
+ $vc->add('EMAIL', $email, array('type' => 'INTERNET'));
+ }
+
+ foreach ((array)$contact['phone'] as $phone) {
+ $type = $this->phonetypes[$phone['type']] ?: $phone['type'];
+ $vc->add('TEL', $phone['number'], array('type' => strtoupper($type)));
+ }
+
+ foreach ((array)$contact['website'] as $website) {
+ $vc->add('URL', $website['url'], array('type' => strtoupper($website['type'])));
+ }
+
+ foreach ((array)$contact['im'] as $im) {
+ list($prot, $val) = explode(':', $im);
+ if ($val) $vc->add('x-' . $prot, $val);
+ else $vc->add('IMPP', $im);
+ }
+
+ foreach ((array)$contact['address'] as $adr) {
+ $vadr = VObject\Property::create('ADR', null, array('type' => strtoupper($adr['type'])));
+ $vadr->setParts(array('','', $adr['street'], $adr['locality'], $adr['region'], $adr['zipcode'], $adr['country']));
+ $vc->add($vadr);
+ }
+
+ if (!empty($contact['notes']))
+ $vc->add('NOTE', $contact['notes']);
+
+ if (!empty($contact['gender']))
+ $vc->add('X-GENDER', $contact['gender']);
+
+ if (!empty($contact['birthday']) && $contact['birthday'] instanceof \DateTime) {
+ $contact['birthday']->_dateonly = true;
+ $vc->add(VObjectUtils::datetime_prop('BDAY', $contact['birthday'], false));
+ }
+ if (!empty($contact['anniversary']) && $contact['birthday'] instanceof \DateTime) {
+ $contact['anniversary']->_dateonly = true;
+ $vc->add(VObjectUtils::datetime_prop('X-ANNIVERSARY', $contact['anniversary'], false));
+ }
+
+ if ($contact['categories']) {
+ $cat = VObject\Property::create('CATEGORIES');
+ $cat->setParts((array)$contact['categories']);
+ $vc->add($cat);
+ }
+
+ if (!empty($contact['freebusyurl']))
+ $vc->add('FBURL', $contact['freebusyurl']);
+
+ if (!empty($contact['photo'])) {
+ $vc->PHOTO = base64_encode($contact['photo']);
+ $vc->PHOTO->add('BASE64', null);
+ }
+
+ if (!empty($contact['changed']))
+ $vc->add(VObjectUtils::datetime_prop('REV', $contact['changed'], true));
+
+ return $vc->serialize();
+ }
+
+ /**
+ * Convert the given Sabre\VObject\Component\Vcard object to a libkolab compatible contact format
+ *
+ * @param object Vcard object to convert
+ * @return array Hash array with contact properties
+ */
+ private function _to_array($vc)
+ {
+ $contact = array(
+ 'uid' => strval($vc->UID),
+ 'name' => strval($vc->FN),
+ 'changed' => $vc->REV ? $vc->REV->getDateTime() : null,
+ );
+
+ $phonetypemap = array_flip($this->phonetypes);
+
+ // map attributes to internal fields
+ foreach ($vc->children as $prop) {
+ if (!($prop instanceof VObject\Property))
+ continue;
+
+ switch ($prop->name) {
+ case 'N':
+ list($contact['surname'], $contact['firstname'], $contact['middlename'], $contact['prefix'], $contact['suffix']) = $prop->getParts();
+ break;
+
+ case 'NOTE':
+ $contact['notes'] = $prop->value;
+ break;
+
+ case 'TITLE':
+ case 'NICKNAME':
+ $contact[strtolower($prop->name)] = $prop->value;
+ break;
+
+ case 'ORG':
+ list($contact['categories'], $contact['department']) = $prop->getParts();
+ break;
+
+ case 'CATEGORY':
+ case 'CATEGORIES':
+ $contact['categories'] = $prop->getParts();
+ break;
+
+ case 'EMAIL':
+ $types = self::array_filter($prop->offsetGet('type'), 'internet,pref', true);
+ // console(array_map('strtolower', $types));
+ $contact['email'][] = $prop->value;
+ break;
+
+ case 'URL':
+ $types = self::array_filter($prop->offsetGet('type'), 'internet,pref', true);
+ $contact['website'][] = array('url' => $prop->value, 'type' => strtolower($types[0]));
+ break;
+
+ case 'TEL':
+ $types = self::array_filter($prop->offsetGet('type'), 'internet,pref', true);
+ $type = strtolower($types[0]);
+ $contact['phone'][] = array('number' => $prop->value, 'type' => $phonetypemap[$type] ?: $type);
+ break;
+
+ case 'ADR':
+ $type = $prop->offsetGet('type');
+ $adr = array('type' => strval($type));
+ list(,, $adr['street'], $adr['locality'], $adr['region'], $adr['zipcode'], $adr['country']) = $prop->getParts();
+ $contact['address'][] = $adr;
+ break;
+
+ case 'BDAY':
+ $contact['birthday'] = new \DateTime($prop->value);
+ $contact['birthday']->_dateonly = true;
+ break;
+
+ case 'X-ANNIVERSARY':
+ $contact['anniversary'] = new \DateTime($prop->value);
+ $contact['anniversary']->_dateonly = true;
+ break;
+
+ case 'X-GENDER':
+ case 'X-PROFESSION':
+ case 'X-SPOUSE':
+ $contact[strtolower(substr($prop->name, 2))] = $prop->value;
+ break;
+
+ case 'X-MANAGER':
+ case 'X-ASSISTANT':
+ case 'X-CHILDREN':
+ $contact[strtolower(substr($prop->name, 2))] = explode(',', $prop->value);
+ break;
+
+ case 'X-JABBER':
+ case 'X-ICQ':
+ case 'X-MSN':
+ case 'X-AIM':
+ case 'X-YAHOO':
+ case 'X-SKYPE':
+ $protocol = strtolower(substr($prop->name, 2));
+ $contact['im'][] = $protocol. ':' . $prop->value;
+ break;
+
+ case 'PHOTO':
+ $param = $prop->parameters[0];
+ if ($param->value && strtolower($param->value) == 'b' || strtolower($param->name) == 'base64') {
+ $contact['photo'] = base64_decode($prop->value);
+ }
+ break;
+
+ case 'CUSTOM1':
+ case 'CUSTOM2':
+ case 'CUSTOM3':
+ case 'CUSTOM4':
+ default:
+ if (substr($prop->name, 0, 2) == 'X-' || substr($prop->name, 0, 6) == 'CUSTOM')
+ $contact['x-custom'][] = array($prop->name, strval($prop->value));
+ break;
+ }
+ }
+
+ return $contact;
+ }
+
+ /**
+ * Extract array values by a filter
+ *
+ * @param array Array to filter
+ * @param keys Array or comma separated list of values to keep
+ * @param boolean Invert key selection: remove the listed values
+ *
+ * @return array The filtered array
+ */
+ private static function array_filter($arr, $values, $inverse = false)
+ {
+ if (!is_array($values)) {
+ $values = explode(',', $values);
+ }
+
+ $result = array();
+ $keep = array_flip((array)$values);
+
+ foreach ($arr as $key => $val) {
+ if ($inverse != isset($keep[strtolower($val)])) {
+ $result[$key] = $val;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Generate an Etag string from the given contact data
+ *
+ * @param array Hash array with contact properties from libkolab
+ * @return string Etag string
+ */
+ private static function _get_etag($contact)
+ {
+ return sprintf('"%s-%d"', substr(md5($contact['uid']), 0, 16), time(), $contact['_msguid']);
+ }
+
+}
diff --git a/lib/Kolab/Utils/VObjectUtils.php b/lib/Kolab/Utils/VObjectUtils.php
new file mode 100644
index 0000000..dadd7be
--- /dev/null
+++ b/lib/Kolab/Utils/VObjectUtils.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Kolab\Utils;
+
+use Sabre\VObject\Property;
+
+/**
+ * Helper class proviting utility functions for VObject data encoding
+ */
+class VObjectUtils
+{
+
+ /**
+ * Helper method to correctly interpret an all-day date value
+ */
+ public static function convert_datetime($prop)
+ {
+ if (empty($prop)) {
+ return null;
+ }
+ else if ($prop instanceof Property\MultiDateTime) {
+ $dt = array();
+ $dateonly = ($prop->getDateType() & Property\DateTime::DATE);
+ foreach ($prop->getDateTimes() as $item) {
+ $item->_dateonly = $dateonly;
+ $dt[] = $item;
+ }
+ }
+ else if ($prop instanceof Property\DateTime) {
+ $dt = $prop->getDateTime();
+ if ($prop->getDateType() & Property\DateTime::DATE) {
+ $dt->_dateonly = true;
+ }
+ }
+ else if ($prop instanceof \DateTime) {
+ $dt = $prop;
+ }
+
+ return $dt;
+ }
+
+
+ /**
+ * Create a Sabre\VObject\Property instance from a PHP DateTime object
+ *
+ * @param string Property name
+ * @param object DateTime
+ */
+ public static function datetime_prop($name, $dt, $utc = false)
+ {
+ $vdt = new Property\DateTime($name);
+ $vdt->setDateTime($dt, $dt->_dateonly ? Property\DateTime::DATE : ($utc ? Property\DateTime::UTC : Property\DateTime::LOCALTZ));
+ return $vdt;
+ }
+
+ /**
+ * Copy values from one hash array to another using a key-map
+ */
+ public static function map_keys($values, $map)
+ {
+ $out = array();
+ foreach ($map as $from => $to) {
+ if (isset($values[$from]))
+ $out[$to] = $values[$from];
+ }
+ return $out;
+ }
+
+} \ No newline at end of file