Page MenuHomePhorge

No OneTemporary

Size
357 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/composer.json b/composer.json
index 0df0f67..88976dc 100644
--- a/composer.json
+++ b/composer.json
@@ -1,33 +1,28 @@
{
"name": "kolab/irony",
"description": "iRony - The Kolab WebDAV/CalDAV/CardDAV Server",
"license": "AGPL-3.0",
- "version": "0.3.0",
+ "version": "0.4-dev",
"repositories": [
{
"type": "pear",
"url": "http://pear.php.net/"
},
- {
- "type": "vcs",
- "url": "https://github.com/thomascube/sabre-vobject.git"
- },
{
"type": "vcs",
"url": "https://git.kolab.org/diffusion/PNL/php-net_ldap.git"
}
],
"require": {
- "php": ">=5.3.3",
- "sabre/dav" : "1.8.9"
+ "php": ">=5.4.1",
+ "sabre/dav" : "2.1.*"
},
"require-dev": {
"pear/mail_mime": ">=1.8.9",
- "pear/mail_mime-decode": ">=1.5.5",
- "pear/http_request2": ">=2.1.1",
+ "pear/mail_mime-decode": "~1.5.5",
"pear-pear.php.net/net_idna2": ">=0.1.1",
"pear-pear.php.net/net_ldap2": ">=2.0.12",
"kolab/Net_LDAP3": "dev-master"
},
"minimum-stability": "dev"
}
diff --git a/config/dav.inc.php.sample b/config/dav.inc.php.sample
index b530680..4fe2efe 100644
--- a/config/dav.inc.php.sample
+++ b/config/dav.inc.php.sample
@@ -1,119 +1,124 @@
<?php
/*
+-------------------------------------------------------------------------+
| Configuration for the Kolab DAV server |
| |
| Copyright (C) 2013, Kolab Systems AG |
| |
| 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/>. |
| |
+-------------------------------------------------------------------------+
*/
$config = array();
// The HTTP path to the iRony root directory.
// Set to / if the service is registered as document root for a virtual host
$config['base_uri'] = '/iRony/';
// User agent string written to kolab storage MIME messages
$config['useragent'] = 'Kolab DAV Server libkolab/' . RCUBE_VERSION;
// Roundcube plugins. Not all are supported here.
$config['kolabdav_plugins'] = array('kolab_auth');
// Type of Auth cache. Supported values: 'db', 'apc' and 'memcache'.
// Note: This is only for username canonification map.
$config['kolabdav_auth_cache'] = 'db';
// lifetime of the Auth cache, possible units: s, m, h, d, w
$config['kolabdav_auth_cache_ttl'] = '1h';
// enable debug console showing the internal function calls triggered
// by http requests. This will write log to /var/log/iRony/console
$config['kolabdav_console'] = false;
// enable per-user debugging if /var/log/iRony/<username>/ folder exists
$config['kolabdav_user_debug'] = false;
// enable logging of full HTTP payload
// (bitmask of these values: 2 = HTTP Requests, 4 = HTTP Responses)
$config['kolabdav_http_log'] = 0;
+// expose iTip invitations from email inbox in CalDAV scheduling inbox.
+// this will make capable CalDAV clients process event invitations and
+// as a result, the invitation messages are removed from the email inbox.
+$config['kolabdav_caldav_inbox'] = false;
+
// Enables the CardDAV Directory Gateway Extension by exposing an
// LDAP-based address book in the pricipals address book collection.
// Properties of this option are the same as for $config['ldap_public'] entries.
/*
$config['kolabdav_ldap_directory'] = array(
'name' => 'Global Address Book',
'hosts' => 'localhost',
'port' => 389,
'use_tls' => false,
// If true the base_dn, bind_dn and bind_pass default to the user's credentials.
'user_specific' => false,
// It's possible to bind with the current user's credentials for individual address books.
// The login name is used to search for the DN to bind with
'search_base_dn' => 'ou=People,dc=example,dc=org',
'search_bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
'search_bind_pw' => 'Welcome2KolabSystems',
'search_filter' => '(&(objectClass=inetOrgPerson)(mail=%fu))',
// When 'user_specific' is enabled following variables can be used in base_dn/bind_dn config:
// %fu - The full username provided, assumes the username is an email
// address, uses the username_domain value if not an email address.
// %u - The username prior to the '@'.
// %d - The domain name after the '@'.
// %dc - The domain name hierarchal string e.g. "dc=test,dc=domain,dc=com"
// %dn - DN found by ldap search when search_filter/search_base_dn are used
'base_dn' => 'ou=People,dc=example,dc=org',
'bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
'bind_pass' => 'Welcome2KolabSystems',
'ldap_version' => 3,
'filter' => '(objectClass=inetOrgPerson)',
'search_fields' => array('displayname', 'mail'),
'sort' => array('displayname', 'sn', 'givenname', 'cn'),
'scope' => 'sub',
'searchonly' => true, // Set to false to enable listing
'sizelimit' => '1000',
'timelimit' => '0',
'fieldmap' => array(
// Roundcube => LDAP
'name' => 'displayName',
'surname' => 'sn',
'firstname' => 'givenName',
'middlename' => 'initials',
'prefix' => 'title',
'email:work' => 'mail',
'email:other' => 'alias',
'phone:main' => 'telephoneNumber',
'phone:work' => 'alternateTelephoneNumber',
'phone:mobile' => 'mobile',
'phone:work2' => 'blackberry',
'street' => 'street',
'zipcode' => 'postalCode',
'locality' => 'l',
'organization' => 'o',
'jobtitle' => 'title',
'photo' => 'jpegphoto',
// required for internal handling and caching
'uid' => 'nsuniqueid',
'changed' => 'modifytimestamp',
),
);
*/
// Enable caching for LDAP directory data.
// This is recommended with 'searchonly' => false to speed-up sychronization of multiple clients
// $config['kolabdav_ldap_cache'] = 'memcache';
// $config['kolabdav_ldap_cache_ttl'] = 600; // in seconds
diff --git a/lib/Kolab/CalDAV/CalendarBackend.php b/lib/Kolab/CalDAV/CalendarBackend.php
index 848c9dd..9f5a000 100644
--- a/lib/Kolab/CalDAV/CalendarBackend.php
+++ b/lib/Kolab/CalDAV/CalendarBackend.php
@@ -1,832 +1,1126 @@
<?php
/**
* SabreDAV Calendaring 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\CalDAV;
use \PEAR;
use \rcube;
use \rcube_charset;
+use \rcube_message;
use \kolab_storage;
use \kolab_storage_config;
use \libcalendaring;
use Kolab\Utils\DAVBackend;
use Kolab\Utils\VObjectUtils;
use Kolab\DAV\Auth\HTTPBasic;
use Sabre\DAV;
use Sabre\CalDAV;
use Sabre\VObject;
/**
* Kolab Calendaring backend.
*
* Checkout the Sabre\CalDAV\Backend\BackendInterface for all the methods that must be implemented.
*
*/
-class CalendarBackend extends CalDAV\Backend\AbstractBackend
+class CalendarBackend extends CalDAV\Backend\AbstractBackend implements CalDAV\Backend\SchedulingSupport
{
private $calendars;
private $folders;
private $aliases;
private $useragent;
private $subscribed = null;
/**
* Read available calendar folders from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type
$folders = array_merge(kolab_storage::get_folders('event', $this->subscribed), kolab_storage::get_folders('task', $this->subscribed));
$this->calendars = $this->folders = $this->aliases = array();
$order = 1;
+ $default_calendar_id = null;
foreach (kolab_storage::sort_folders($folders) as $folder) {
$id = $folder->get_uid();
$this->folders[$id] = $folder;
$fdata = $folder->get_imap_data(); // fetch IMAP folder data for CTag generation
$this->calendars[$id] = array(
'id' => $id,
'uri' => $id,
'{DAV:}displayname' => html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET),
'{http://apple.com/ns/ical/}calendar-color' => '#' . $folder->get_color('FF0000') . 'FF',
'{http://calendarserver.org/ns/}getctag' => sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']),
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Property\SupportedCalendarComponentSet(array(DAVBackend::$caldav_type_component_map[$folder->type])),
'{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Property\ScheduleCalendarTransp('opaque'),
'{http://apple.com/ns/ical/}calendar-order' => $order++,
);
+
+ if ($folder->default && $folder->type == 'event') {
+ $default_calendar_id = $id;
+ }
+
$this->aliases[$folder->name] = $id;
// these properties are used for sharing supprt (not yet active)
if (false && $folder->get_namespace() != 'personal') {
$rights = $folder->get_myrights();
$this->calendars[$id]['{http://calendarserver.org/ns/}shared-url'] = '/calendars/' . $folder->get_owner() . '/' . $id;
$this->calendars[$id]['{http://calendarserver.org/ns/}owner-principal'] = $folder->get_owner();
$this->calendars[$id]['{http://sabredav.org/ns}read-only'] = strpos($rights, 'i') === false;
}
}
+ // put default calendar on top of the list:
+ // {urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL is derived from the first item on this list
+ if ($default_calendar_id) {
+ $this->calendars = array($default_calendar_id => $this->calendars[$default_calendar_id]) + $this->calendars;
+ }
+
return $this->calendars;
}
/**
* Getter for a kolab_storage_folder representing the calendar for the given ID
*
* @param string Calendar ID
* @return object kolab_storage_folder instance
*/
public function get_storage_folder($id)
{
// resolve alias name
if ($this->aliases[$id]) {
$id = $this->aliases[$id];
}
if ($this->folders[$id]) {
DAVBackend::check_storage_folder($this->folders[$id]);
return $this->folders[$id];
}
else {
return DAVBackend::get_storage_folder($id, '');
}
}
/**
* Returns a list of calendars for a principal.
*
* Every calendars is an array with the following keys:
* * id, a unique id that will be used by other functions to modify the
* calendar. This can be the same as the uri or a database key.
* * uri, which the basename of the uri with which the calendar is
* accessed.
* * principaluri. The owner of the calendar. Almost always the same as
* principalUri passed to this method.
*
* Furthermore it can contain webdav properties in clark notation. A very
* common one is '{DAV:}displayname'.
*
* @param string $principalUri
* @return array
*/
public function getCalendarsForUser($principalUri)
{
console(__METHOD__, $principalUri);
+ if (!$this->is_current_pricipal($principalUri)) {
+ return array();
+ }
+
$this->_read_calendars();
$calendars = array();
foreach ($this->calendars as $id => $cal) {
$this->calendars[$id]['principaluri'] = $principalUri;
$calendars[] = $this->calendars[$id];
}
return $calendars;
}
/**
* Returns calendar properties for a specific node identified by name/uri
*
* @param string Node name/uri
* @return array Hash array with calendar properties or null if not found
*/
public function getCalendarByName($calendarUri)
{
console(__METHOD__, $calendarUri);
$this->_read_calendars();
$id = $calendarUri;
// resolve aliases (calendar by folder name)
if ($this->aliases[$calendarUri]) {
$id = $this->aliases[$calendarUri];
}
if ($this->calendars[$id] && empty($this->calendars[$id]['principaluri'])) {
$this->calendars[$id]['principaluri'] = 'principals/' . HTTPBasic::$current_user;
}
// retry with subscribed = false (#2701)
- if (empty($this->calendars[$id]) && $id != 'outbox' && $this->subscribed === null && rcube::get_instance()->config->get('kolab_use_subscriptions')) {
+ if (empty($this->calendars[$id]) && !in_array($id, array('inbox','outbox','notifications')) && $this->subscribed === null && rcube::get_instance()->config->get('kolab_use_subscriptions')) {
$this->subscribed = false;
unset($this->calendars);
return $this->getCalendarByName($calendarUri);
}
return $this->calendars[$id];
}
/**
* Creates a new calendar for a principal.
*
* If the creation was a success, an id must be returned that can be used to reference
* this calendar in other methods, such as updateCalendar.
*
* @param string $principalUri
* @param string $calendarUri
* @param array $properties
* @return void
*/
public function createCalendar($principalUri, $calendarUri, array $properties)
{
console(__METHOD__, $calendarUri, $properties);
return DAVBackend::folder_create('event', $properties, $calendarUri);
}
/**
* Updates properties for a calendar.
*
- * The mutations array uses the propertyName in clark-notation as key,
- * and the array value for the property value. In the case a property
- * should be deleted, the property value will be null.
- *
- * This method must be atomic. If one property cannot be changed, the
- * entire operation must fail.
- *
- * If the operation was successful, true can be returned.
- * If the operation failed, false can be returned.
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
*
- * Deletion of a non-existent property is always successful.
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
*
- * Lastly, it is optional to return detailed information about any
- * failures. In this case an array should be returned with the following
- * structure:
+ * Read the PropPatch documenation for more info and examples.
*
- * array(
- * 403 => array(
- * '{DAV:}displayname' => null,
- * ),
- * 424 => array(
- * '{DAV:}owner' => null,
- * )
- * )
- *
- * In this example it was forbidden to update {DAV:}displayname.
- * (403 Forbidden), which in turn also caused {DAV:}owner to fail
- * (424 Failed Dependency) because the request needs to be atomic.
- *
- * @param mixed $calendarId
- * @param array $mutations
- * @return bool|array
+ * @param string $path
+ * @param \Sabre\DAV\PropPatch $propPatch
+ * @return void
*/
- public function updateCalendar($calendarId, array $mutations)
+ public function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch)
{
- console(__METHOD__, $calendarId, $mutations);
+ console(__METHOD__, $calendarId, $propPatch);
- $folder = $this->get_storage_folder($calendarId);
- return DAVBackend::folder_update($folder, $mutations);
+ if ($folder = $this->get_storage_folder($calendarId)) {
+ DAVBackend::handle_propatch($folder, $propPatch);
+ }
}
/**
* Delete a calendar and all it's objects
*
* @param mixed $calendarId
* @return void
*/
public function deleteCalendar($calendarId)
{
console(__METHOD__, $calendarId);
$folder = $this->get_storage_folder($calendarId);
if ($folder && !kolab_storage::folder_delete($folder->name)) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting calendar folder $folder->name"),
true, false);
}
}
/**
* Returns all calendar objects within a calendar.
*
* Every item contains an array with the following keys:
* * id - unique identifier which will be used for subsequent updates
* * calendardata - The iCalendar-compatible calendar data (optional)
* * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
* * lastmodified - a timestamp of the last modification time
* * etag - An arbitrary string, surrounded by double-quotes. (e.g.: "abcdef"')
* * calendarid - The calendarid as it was passed to this function.
* * size - The size of the calendar objects, in bytes.
*
* Note that the etag is optional, but it's highly encouraged to return for
* speed reasons.
*
* If neither etag or size are specified, the calendardata will be
* used/fetched to determine these numbers. If both are specified the
* amount of times this is needed is reduced by a great degree.
*
* @param mixed $calendarId
* @return array
*/
public function getCalendarObjects($calendarId)
{
console(__METHOD__, $calendarId);
$events = array();
$query = $this->_event_filter_query();
$storage = $this->get_storage_folder($calendarId);
if ($storage) {
foreach ($storage->select($query) as $event) {
// post-filter events to suppress declined invitations
if (!$this->_event_filter_compare($event)) {
continue;
}
// get tags/categories from relations
$this->load_tags($event);
$events[] = array(
'id' => $event['uid'],
'uri' => VObjectUtils::uid2uri($event['uid'], '.ics'),
'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null,
'calendarid' => $calendarId,
'etag' => self::_get_etag($event),
'size' => $event['_size'],
);
}
}
return $events;
}
/**
* Returns information from a single calendar object, based on it's object
* uri.
*
* The returned array must have the same keys as getCalendarObjects. The
* 'calendardata' object is required here though, while it's not required
* for getCalendarObjects.
*
* @param mixed $calendarId
* @param string $objectUri
* @return array
*/
public function getCalendarObject($calendarId, $objectUri)
{
console(__METHOD__, $calendarId, $objectUri);
$uid = VObjectUtils::uri2uid($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
// attachment content is requested
if (preg_match('!^(.+).ics:attachment:(\d+):.+$!', $objectUri, $m)) {
$uid = VObjectUtils::uri2uid($m[1]);
$part = $m[2];
}
if ($storage && ($event = $storage->get_object($uid))) {
// deliver attachment content directly
if ($part && !empty($event['_attachments'])) {
foreach ($event['_attachments'] as $attachment) {
if ($attachment['id'] == $part) {
header('Content-Type: ' . $attachment['mimetype']);
header('Content-Disposition: inline; filename="' . $attachment['name'] . '"');
$storage->get_attachment($uid, $part, null, true);
exit;
}
}
}
// map attributes
$event['attachments'] = $event['_attachments'];
// compose an absolute URI for referencing object attachments
$base_uri = DAVBackend::abs_url(array(
CalDAV\Plugin::CALENDAR_ROOT,
preg_replace('!principals/!', '', $this->calendars[$calendarId]['principaluri']),
$calendarId,
VObjectUtils::uid2uri($event['uid'], '.ics'),
));
// get tags/categories from relations
$this->load_tags($event);
// default response
return array(
'id' => $event['uid'],
'uri' => VObjectUtils::uid2uri($event['uid'], '.ics'),
'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null,
'calendarid' => $calendarId,
'calendardata' => $this->_to_ical($event, $base_uri, $storage),
'etag' => self::_get_etag($event),
);
}
return array();
}
/**
* Creates a new calendar object.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function createCalendarObject($calendarId, $objectUri, $calendarData)
{
console(__METHOD__, $calendarId, $objectUri, $calendarData);
$uid = VObjectUtils::uri2uid($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
$object = $this->parse_calendar_data($calendarData, $uid);
if (empty($object) || empty($object['uid'])) {
throw new DAV\Exception('Parse error: not a valid iCalendar 2.0 object');
}
// if URI doesn't match the content's UID, the object might already exist!
if ($object['uid'] != $uid && $storage->get_object($object['uid'])) {
$objectUri = VObjectUtils::uid2uri($object['uid'], '.ics');
Plugin::$redirect_basename = $objectUri;
return $this->updateCalendarObject($calendarId, $objectUri, $calendarData);
}
// map attachments attribute
$object['_attachments'] = $object['attachments'];
unset($object['attachments']);
// remove categories from object data (only for tasks yet)
if ($object['_type'] == 'task') {
$tags = (array)$object['categories'];
unset($object['categories']);
}
$success = $storage->save($object, $object['_type']);
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving $object[_type] object to Kolab server"),
true, false);
throw new DAV\Exception('Error saving calendar object to backend');
}
// save tag relations on success (only available for tasks yet)
if ($object['_type'] == 'task') {
$this->save_tags($uid, $tags);
$object['categories'] = $tags; // add again for etag computation
}
// send Location: header if URI doesn't match object's UID (Bug #2109)
if ($object['uid'] != $uid) {
Plugin::$redirect_basename = VObjectUtils::uid2uri($object['uid'], '.ics');
}
// return new Etag
return $success ? self::_get_etag($object) : null;
}
/**
* Updates an existing calendarobject, based on it's uri.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function updateCalendarObject($calendarId, $objectUri, $calendarData)
{
console(__METHOD__, $calendarId, $objectUri, $calendarData);
$uid = VObjectUtils::uri2uid($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
$object = $this->parse_calendar_data($calendarData, $uid);
if (empty($object)) {
throw new DAV\Exception('Parse error: not a valid iCalendar 2.0 object');
}
// sanity check
if ($object['uid'] != $uid) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating calendar object: UID doesn't match object URI"),
true, false);
throw new DAV\Exception\NotFound("UID doesn't match object URI");
}
// 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;
}
// process attachments
if (/* user agent known to handle attachments inline */ !empty($object['attachments'])) {
$object['_attachments'] = $object['attachments'];
unset($object['attachments']);
// mark all existing attachments as deleted (update is always absolute)
foreach ($old['_attachments'] as $key => $attach) {
$object['_attachments'][$key] = false;
}
}
// remove categories from object data (only for tasks yet)
if ($object['_type'] == 'task') {
$tags = (array)$object['categories'];
unset($object['categories']);
}
// save object
$saved = $storage->save($object, $object['_type'], $uid);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to Kolab server"),
true, false);
Plugin::$redirect_basename = null;
throw new DAV\Exception('Error saving event object to backend');
}
// save tag relations on success (only available for tasks yet)
if ($object['_type'] == 'task') {
$this->save_tags($uid, $tags);
$object['categories'] = $tags; // add again for etag computation
}
// return new Etag
return self::_get_etag($object);
}
/**
* Deletes an existing calendar object.
*
* @param mixed $calendarId
* @param string $objectUri
* @return void
*/
public function deleteCalendarObject($calendarId, $objectUri)
{
console(__METHOD__, $calendarId, $objectUri);
$uid = VObjectUtils::uri2uid($objectUri, '.ics');
if ($storage = $this->get_storage_folder($calendarId)) {
if ($storage->delete($uid)) {
$this->save_tags($uid, null);
}
}
}
/**
* Performs a calendar-query on the contents of this calendar.
*
* The calendar-query is defined in RFC4791 : CalDAV. Using the
* calendar-query it is possible for a client to request a specific set of
* object, based on contents of iCalendar properties, date-ranges and
* iCalendar component types (VTODO, VEVENT).
*
* This method should just return a list of (relative) urls that match this
* query.
*
* The list of filters are specified as an array. The exact array is
* documented by Sabre\CalDAV\CalendarQueryParser.
*
* Note that it is extremely likely that getCalendarObject for every path
* returned from this method will be called almost immediately after. You
* may want to anticipate this to speed up these requests.
*
* Requests that are extremely common are:
* * requests for just VEVENTS
* * requests for just VTODO
* * requests with a time-range-filter on either VEVENT or VTODO.
*
* ..and combinations of these requests. It may not be worth it to try to
* handle every possible situation and just rely on the (relatively
* easy to use) CalendarQueryValidator to handle the rest.
*
* Note that especially time-range-filters may be difficult to parse. A
* time-range filter specified on a VEVENT must for instance also handle
* recurrence rules correctly.
* A good example of how to interprete all these filters can also simply
* be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
* as possible, so it gives you a good idea on what type of stuff you need
* to think of.
*
* @param mixed $calendarId
* @param array $filters
* @return array
*/
public function calendarQuery($calendarId, array $filters)
{
console(__METHOD__, $calendarId, $filters);
// build kolab storage query from $filters
$query = $this->_event_filter_query();
foreach ((array)$filters['comp-filters'] as $filter) {
if ($filter['name'] != 'VEVENT')
continue;
if (is_array($filter['time-range'])) {
if (!empty($filter['time-range']['end'])) {
$query[] = array('dtstart', '<=', $filter['time-range']['end']);
}
if (!empty($filter['time-range']['start'])) {
$query[] = array('dtend', '>=', $filter['time-range']['start']);
}
}
+ if (is_array($filter['prop-filters'])) {
+ foreach ($filter['prop-filters'] as $prop_filter) {
+ $match = $prop_filter['text-match'];
+ if ($match['value']) {
+ $op = $match['negate-condition'] ? '!=' : '=';
+ switch ($prop_filter['name']) {
+ case 'UID':
+ $query[] = array('uid', $op, $match['value']);
+ break;
+ }
+ }
+ }
+ }
}
$results = array();
if ($storage = $this->get_storage_folder($calendarId)) {
foreach ($storage->select($query) as $event) {
// post-filter events to suppress declined invitations
if ($this->_event_filter_compare($event)) {
$results[] = $event['uid'] . '.ics';
}
}
}
return $results;
}
/**
* Set User-Agent string of the connected client
*/
public function setUserAgent($uastring)
{
$ua_classes = array(
'ical' => 'iCal/\d',
'outlook' => 'iCal4OL/\d',
'lightning' => 'Lightning/\d',
);
foreach ($ua_classes as $class => $regex) {
if (preg_match("!$regex!", $uastring)) {
$this->useragent = $class;
break;
}
}
}
+ /********** SchedulingBackend methods ***********/
+
+ /**
+ * Returns a single scheduling object for the inbox collection.
+ *
+ * The returned array should contain the following elements:
+ * * uri - A unique basename for the object. This will be used to
+ * construct a full uri.
+ * * calendardata - The iCalendar object
+ * * lastmodified - The last modification date. Can be an int for a unix
+ * timestamp, or a PHP DateTime object.
+ * * etag - A unique token that must change if the object changed.
+ * * size - The size of the object, in bytes.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ * @return array
+ */
+ public function getSchedulingObject($principalUri, $objectUri)
+ {
+ console(__METHOD__, $principalUri, $objectUri);
+
+ if (!$this->is_current_pricipal($principalUri)) {
+ return array();
+ }
+
+ $uid = VObjectUtils::uri2uid($objectUri, '.ics');
+ list($msguid, $mime_id) = explode('-', $uid, 2);
+
+ $message = new rcube_message($msguid, 'INBOX');
+ if ($ical = $message->get_part_content($mime_id, null, false, 0, false)) {
+ if ($event = $this->parse_calendar_data($ical, null)) {
+ if ($event['_type'] == 'event') {
+ $event['_msguid'] = $msguid;
+ return array(
+ 'uri' => $objectUri,
+ 'calendardata' => $ical,
+ 'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null,
+ 'etag' => self::_get_etag($event),
+ );
+ }
+ }
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns all scheduling objects for the inbox collection.
+ *
+ * These objects should be returned as an array. Every item in the array
+ * should follow the same structure as returned from getSchedulingObject.
+ *
+ * The main difference is that 'calendardata' is optional.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getSchedulingObjects($principalUri)
+ {
+ console(__METHOD__, $principalUri);
+
+ $results = array();
+
+ // we can only access the current user's calendars (if enabled)
+ if (!$this->is_current_pricipal($principalUri) || !rcube::get_instance()->config->get('kolabdav_caldav_inbox', false)) {
+ return $results;
+ }
+
+ // find iTip messages in users email INBOX and extract the ics attachment.
+ foreach ($this->search_email_inbox() as $msg) {
+ list($msguid, $mime_id) = $msg;
+ $message = new rcube_message($msguid, 'INBOX');
+ if ($ical = $message->get_part_content($mime_id, null, false, 0, false)) {
+ if ($event = $this->parse_calendar_data($ical, null)) {
+ if ($event['_type'] != 'event') {
+ continue;
+ }
+
+ $event['_msguid'] = $msguid;
+ $results[] = array(
+ 'uri' => VObjectUtils::uid2uri($msguid . '-' . $mime_id, '.ics'),
+ 'calendardata' => $ical,
+ 'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null,
+ 'etag' => self::_get_etag($event),
+ );
+
+ break;
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Deletes a scheduling object from the inbox collection.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ * @return void
+ */
+ public function deleteSchedulingObject($principalUri, $objectUri)
+ {
+ console(__METHOD__, $principalUri, $objectUri);
+
+ $rcube = rcube::get_instance();
+
+ // we can only access the current user's inbox (if enabled)
+ if (!$this->is_current_pricipal($principalUri) || !$rcube->config->get('kolabdav_caldav_inbox', false)) {
+ return;
+ }
+
+ // TODO: get the referenced iTip message from email INBOX and
+ // copy it to the default calendar. This will also remove the message
+ // from the email inbox as the message is considered 'processed'.
+ $uid = VObjectUtils::uri2uid($objectUri, '.ics');
+ list($msguid, $mime_id) = explode('-', $uid, 2);
+
+ $message = new rcube_message($msguid, 'INBOX');
+ if ($ical = $message->get_part_content($mime_id, null, false, 0, false)) {
+ if ($object = $this->parse_calendar_data($ical, null)) {
+ if ($object['_type'] != 'event') {
+ return;
+ }
+
+ // get default calendar and search for an existing copy
+ $calendarId = reset(array_keys($this->_read_calendars()));
+ $storage = $this->get_storage_folder($calendarId);
+ $existing = $storage->get_object($object['uid']);
+
+ // copy meta data (starting with _) from old object
+ if (!empty($existing)) {
+ foreach ((array)$existing as $key => $val) {
+ if (!isset($object[$key]) && $key[0] == '_')
+ $object[$key] = $val;
+ }
+ }
+
+ // TODO: if iTip REPLY or CANCEL, only copy necessary properties
+
+ // map attachments attribute
+ $object['_attachments'] = $object['attachments'];
+ unset($object['attachments']);
+
+ $success = $storage->save($object, $object['_type'], $existing['uid']);
+ if (!$success) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving $object[_type] object to Kolab server"),
+ true, false);
+
+ throw new DAV\Exception('Error saving calendar object to backend');
+ }
+
+ // remove iTip message from email inbox
+ // TODO: move to Trash instead ?
+ $imap = $rcube->get_storage();
+ $imap->move_message($msguid, 'Trash', 'INBOX');
+ //console("DELETE $msguid", $imap->delete_message($msguid, 'INBOX'));
+ }
+ }
+ }
+
+ /**
+ * Creates a new scheduling object. This should land in a users' inbox.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ * @param string $objectData
+ * @return void
+ */
+ public function createSchedulingObject($principalUri, $objectUri, $objectData)
+ {
+ console(__METHOD__, $principalUri, $objectUri, $objectData);
+
+ // accept only for current user principal (we don't have permissions for other users)
+ if ($this->is_current_pricipal($principalUri)) {
+
+ }
+ else {
+ // send as iTip?
+ }
+ }
+
+ /**
+ * Return the ctag value for the scheduling inbox
+ */
+ public function getSchedulingInboxCtag($principalUri)
+ {
+ $rcube = rcube::get_instance();
+
+ if ($this->is_current_pricipal($principalUri) && $rcube->config->get('kolabdav_caldav_inbox', false)) {
+ // we could use the INBOX imap folder properties but these are likely subject to
+ // frequent changes without new invitations. Let's count potential iTip messages:
+ $candidates = $this->search_email_inbox();
+
+ if (count($candidates)) {
+ $fdata = $rcube->storage->folder_data('INBOX');
+ return sprintf('%d-%d-%d-%d',
+ $fdata['UIDVALIDITY'],
+ $fdata['HIGHESTMODSEQ'],
+ $fdata['UIDNEXT'],
+ count($candidates)
+ );
+ }
+ }
+
+ return "empty-000";
+ }
+
+ /**
+ *
+ */
+ protected function search_email_inbox()
+ {
+ $result = array();
+ $rcube = rcube::get_instance();
+ $imap = $rcube->get_storage();
+
+ // FIXME: search by content-type doesn't return any results from Cyrus IMAP
+ // $query = $imap->search_once('INBOX', 'UNDELETED OR OR HEADER Content-Type text/calendar HEADER Content-Type multipart/mixed HEADER Content-Type multipart/alternative');
+ $query = $imap->search_once('INBOX', 'UNDELETED');
+ if ($query && $query->count() > 0) {
+ foreach ($query->get() as $msguid) {
+ // get bodystructure and check for iTip parts
+ $message = new rcube_message($msguid, 'INBOX');
+ foreach ((array)$message->mime_parts as $part) {
+ if (self::part_is_itip($part)) {
+ $result[] = array($msguid, $part->mime_id);
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Checks if specified message part is a vcalendar data
+ *
+ * @param rcube_message_part Part object
+ * @return boolean True if part is of type vcard
+ */
+ protected static function part_is_itip($part)
+ {
+ return (
+ in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
+ ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
+ ) && !empty($part->ctype_parameters['method']);
+ }
+
+
/********** Data conversion utilities ***********/
/**
* Get object tags
*/
private function load_tags(&$event)
{
// tag relations are only available for tasks yet
if ($event['_type'] != 'task') {
return;
}
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($event['uid']);
if (!empty($tags)) {
$event['categories'] = array();
}
foreach ($tags as $tag) {
$event['categories'][] = $tag['name'];
// modify changed time if relation is newer
if ($tag['changed'] && !$event['changed'] || $tag['changed'] > $event['changed']) {
$event['changed'] = $tag['changed'];
}
}
}
/**
* Update object tags
*/
private function save_tags($uid, $tags)
{
$config = kolab_storage_config::get_instance();
$config->save_tags($uid, $tags);
}
/**
* Parse the given iCal string into a hash array kolab_format_event can handle
*
* @param string iCal data block
* @return array Hash array with event properties or null on failure
*/
private function parse_calendar_data($calendarData, $uid)
{
try {
$ical = libcalendaring::get_ical();
// use already parsed object
if (Plugin::$parsed_vevent && Plugin::$parsed_vevent->UID == $uid) {
$objects = $ical->import_from_vobject(Plugin::$parsed_vcalendar);
}
else {
$objects = $ical->import($calendarData);
}
// return the first object
if (count($objects)) {
+ $objects[0]['_method'] = $ical->method;
return $objects[0];
}
}
catch (VObject\ParseException $e) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "iCal data parse error: " . $e->getMessage()),
true, false);
}
return null;
}
/**
* Build a valid iCal format block from the given event
*
* @param array Hash array with event/task properties from libkolab
* @param string Absolute URI referenceing this event object
* @param object RECURRENCE-ID property when serializing a recurrence exception
* @return mixed VCALENDAR string containing the VEVENT data
* or VObject\VEvent object with a recurrence exception instance
* @see: \libvcalendar::export()
*/
private function _to_ical($event, $base_uri, $storage, $recurrence_id = null)
{
$ical = libcalendaring::get_ical();
$ical->set_prodid('-//Kolab//iRony DAV Server ' . KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN');
$ical->set_agent($this->useragent == 'ical' ? 'Apple' : '');
// list attachments as absolute URIs for Thunderbird
if ($this->useragent == 'lightning') {
$ical->set_attach_uri($base_uri . ':attachment:{{id}}:{{name}}');
$get_attachment = null;
}
else { // embed attachments for others
$get_attachment = function($id, $event) use ($storage) {
return $storage->get_attachment($event['uid'], $id);
};
}
$events = array($event);
// add more instances from exceptions (not recurrence) to the output
if (!empty($event['exceptions']) && empty($event['recurrence'])) {
$events = array_merge($events, $event['exceptions']);
}
return $ical->export($events, null, false, $get_attachment, false);
}
/**
* Wrapper for libcalendaring::get_user_emails()
*/
private function get_user_emails()
{
$emails = libcalendaring::get_instance()->get_user_emails();
if (empty($emails)) {
$emails = array(HTTPBasic::$current_user);
}
return $emails;
}
/**
* Provide basic query for kolab_storage_folder::select()
+ *
+ * @param boolean Filter for inbox events (i.e. status=NEEDS-ACTION)
+ * @return array List of query parameters for kolab_storage_folder::select()
*/
- private function _event_filter_query()
+ private function _event_filter_query($inbox = false)
{
// get email addresses of the current user
$user_emails = $this->get_user_emails();
- $query = array();
+ $query = $subquery = array();
// add query to exclude declined invitations
foreach ($user_emails as $email) {
- $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
- // $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
+ if ($inbox) {
+ $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':needs-action');
+ $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':needs-action');
+ }
+ else {
+ $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
+ }
+ }
+
+ if (!empty($subquery)) {
+ $query[] = array($subquery, 'OR');
}
return $query;
}
/**
* Check the given event if it matches the filter
*
+ * @param array Hash array with event properties
+ * @param boolean Filter for inbox events (i.e. status=NEEDS-ACTION)
* @return boolean True if matches, false if not
*/
- private function _event_filter_compare($event)
+ private function _event_filter_compare($event, $inbox = false)
{
static $user_emails;
if (!is_array($user_emails)) {
$user_emails = $this->get_user_emails();
}
if (is_array($event['attendees'])) {
foreach ($event['attendees'] as $attendee) {
- if (in_array($attendee['email'], $user_emails) && $attendee['status'] == 'DECLINED') {
- return false;
+ if (in_array($attendee['email'], $user_emails)) {
+ if ($attendee['status'] == 'DECLINED') {
+ return false;
+ }
+ else if ($inbox && $attendee['status'] == 'NEEDS-ACTION') {
+ return true;
+ }
}
}
}
- return true;
+ return !$inbox;
}
/**
* Generate an Etag string from the given event data
*
* @param array Hash array with event properties from libkolab
* @return string Etag string
*/
private static function _get_etag($event)
{
return sprintf('"%s-%d-%s"',
substr(md5($event['uid']), 0, 16),
$event['_msguid'],
!empty($event['categories']) ? substr(md5(join(',', (array)$event['categories'])), 0, 16) : '0'
);
}
+ /**
+ * Helpter method to determine whether the given principal URI
+ * matches the authenticated user principal.
+ */
+ private function is_current_pricipal($principalUri)
+ {
+ return $principalUri === 'principals/' . HTTPBasic::$current_user;
+ }
+
}
diff --git a/lib/Kolab/CalDAV/CalendarRootNode.php b/lib/Kolab/CalDAV/CalendarRootNode.php
index 62174a7..6af6e78 100644
--- a/lib/Kolab/CalDAV/CalendarRootNode.php
+++ b/lib/Kolab/CalDAV/CalendarRootNode.php
@@ -1,94 +1,53 @@
<?php
/**
* SabreDAV CalendarRootNode derived class for the 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\CalDAV;
use \Sabre\CalDAV;
-use \Sabre\DAVACL\PrincipalBackend;
-use \Sabre\DAVACL\AbstractPrincipalCollection;
/**
* Calendars collection
*
* This object is responsible for generating a list of calendar-homes for each
* user.
*
*/
-class CalendarRootNode extends AbstractPrincipalCollection
+class CalendarRootNode extends CalDAV\CalendarRoot
{
- /**
- * CalDAV backend
- *
- * @var Sabre\CalDAV\Backend\BackendInterface
- */
- protected $caldavBackend;
-
- /**
- * Constructor
- *
- * This constructor needs both an authentication and a caldav backend.
- *
- * By default this class will show a list of calendar collections for
- * principals in the 'principals' collection. If your main principals are
- * actually located in a different path, use the $principalPrefix argument
- * to override this.
- *
- * @param PrincipalBackend\BackendInterface $principalBackend
- * @param Backend\BackendInterface $caldavBackend
- * @param string $principalPrefix
- */
- public function __construct(PrincipalBackend\BackendInterface $principalBackend, CalDAV\Backend\BackendInterface $caldavBackend, $principalPrefix = 'principals')
- {
- parent::__construct($principalBackend, $principalPrefix);
- $this->caldavBackend = $caldavBackend;
- }
-
- /**
- * Returns the nodename
- *
- * We're overriding this, because the default will be the 'principalPrefix',
- * and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT
- *
- * @return string
- */
- public function getName()
- {
- return CalDAV\Plugin::CALENDAR_ROOT;
- }
/**
* This method returns a node for a principal.
*
* The passed array contains principal information, and is guaranteed to
* at least contain a uri item. Other properties may or may not be
* supplied by the authentication backend.
*
* @param array $principal
* @return \Sabre\DAV\INode
*/
public function getChildForPrincipal(array $principal)
{
return new UserCalendars($this->caldavBackend, $principal);
}
}
diff --git a/lib/Kolab/CalDAV/IMip.php b/lib/Kolab/CalDAV/IMip.php
deleted file mode 100644
index c08f37a..0000000
--- a/lib/Kolab/CalDAV/IMip.php
+++ /dev/null
@@ -1,141 +0,0 @@
-<?php
-
-/**
- * Extended CalDAV IMip handler for the Kolab DAV server
- *
- * @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\CalDAV;
-
-use \rcube;
-use \rcube_utils;
-use \Mail_mime;
-
-use Sabre\VObject;
-use Sabre\CalDAV;
-use Sabre\DAV;
-
-/**
- * iMIP handler.
- *
- * This class is responsible for sending out iMIP messages. iMIP is the
- * email-based transport for iTIP. iTIP deals with scheduling operations for
- * iCalendar objects.
- *
- * If you want to customize the email that gets sent out, you can do so by
- * extending this class and overriding the sendMessage method.
- *
- * @copyright Copyright (C) 2007-2013 Rooftop Solutions. All rights reserved.
- * @author Evert Pot (http://www.rooftopsolutions.nl/)
- * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
- */
-class IMip extends CalDAV\Schedule\IMip
-{
- public function __construct()
- {
-
- }
-
- /**
- * Sends one or more iTip messages through email.
- *
- * @param string $originator Originator Email
- * @param array $recipients Array of email addresses
- * @param VObject\Component $vObject
- * @param string $principal Principal Url of the originator
- * @return void
- */
- public function sendMessage($originator, array $recipients, VObject\Component $vObject, $principal)
- {
- $ics = $vObject->serialize();
- console(__METHOD__, $originator, $recipients, $principal, $ics);
-
- $rcube = rcube::get_instance();
- $sender = $rcube->user->get_identity();
- $sender_email = $sender['email'] ?: $rcube->get_user_email();
- $sender_name = $sender['name'] ?: $rcube->get_user_name();
-
- foreach($recipients as $recipient) {
- $subject = 'KolabDAV iTIP message';
- switch (strtoupper($vObject->METHOD)) {
- case 'REPLY' :
- $subject = 'Response for: ' . $vObject->VEVENT->SUMMARY;
- break;
- case 'REQUEST' :
- $subject = 'Invitation for: ' .$vObject->VEVENT->SUMMARY;
- break;
- case 'CANCEL' :
- $subject = 'Cancelled event: ' . $vObject->VEVENT->SUMMARY;
- break;
- }
-
- $sender = rcube_utils::idn_to_ascii($sender_email);
- $from = format_email_recipient($sender, $sender_name);
- $mailto = rcube_utils::idn_to_ascii($recipient);
-
- // compose multipart message using PEAR:Mail_Mime
- $message = new Mail_mime("\r\n");
- $message->setParam('text_encoding', 'quoted-printable');
- $message->setParam('head_encoding', 'quoted-printable');
- $message->setParam('head_charset', RCUBE_CHARSET);
- $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed");
-
- // compose common headers array
- $headers = array(
- 'To' => $mailto,
- 'From' => $from,
- 'Date' => date('r'),
- 'Reply-To' => $originator,
- 'Message-ID' => $rcube->gen_message_id(),
- 'X-Sender' => $sender,
- 'Subject' => $subject,
- );
- if ($agent = $rcube->config->get('useragent'))
- $headers['User-Agent'] = $agent;
-
- $message->headers($headers);
- $message->setContentType('text/calendar', array('method' => strval($vObject->method), 'charset' => RCUBE_CHARSET));
- $message->setTXTBody($ics);
-
- // send message through Roundcube's SMTP feature
- if (!$rcube->deliver_message($message, $sender, $mailto, $smtp_error)) {
- rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Failed to send iTIP message to " . $mailto),
- true, false);
- }
- }
- }
-
- /**
- * This function is reponsible for sending the actual email.
- *
- * @param string $to Recipient email address
- * @param string $subject Subject of the email
- * @param string $body iCalendar body
- * @param array $headers List of headers
- * @return void
- */
- protected function mail($to, $subject, $body, array $headers)
- {
- //mail($to, $subject, $body, implode("\r\n", $headers));
- }
-
-}
diff --git a/lib/Kolab/CalDAV/IMipPlugin.php b/lib/Kolab/CalDAV/IMipPlugin.php
new file mode 100644
index 0000000..5fae8f0
--- /dev/null
+++ b/lib/Kolab/CalDAV/IMipPlugin.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * Extended CalDAV IMip plugin for the Kolab DAV server
+ *
+ * @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/>.
+ */
+
+namespace Kolab\CalDAV;
+
+use \rcube;
+use \rcube_utils;
+use \Mail_mime;
+
+use Sabre\VObject;
+use Sabre\CalDAV;
+use Sabre\DAV;
+
+/**
+ * iMIP plugin.
+ *
+ * This class is responsible for sending out iMIP messages. iMIP is the
+ * email-based transport for iTIP. iTIP deals with scheduling operations for
+ * iCalendar objects.
+ */
+class IMipPlugin extends CalDAV\Schedule\IMipPlugin
+{
+
+ /**
+ * Event handler for the 'schedule' event.
+ *
+ * @param ITip\Message $iTipMessage
+ * @return void
+ */
+ function schedule(VObject\ITip\Message $iTipMessage)
+ {
+ console(__METHOD__, $iTipMessage->method, $iTipMessage->recipient, $iTipMessage->significantChange, $iTipMessage->scheduleStatus);
+
+ // Not sending any emails if the system considers the update insignificant.
+ if (!$iTipMessage->significantChange) {
+ if (!$iTipMessage->scheduleStatus) {
+ $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
+ }
+ return;
+ }
+
+ $recipient = preg_replace('!^mailto:!i', '', $iTipMessage->recipient);
+ $summary = strval($iTipMessage->message->VEVENT->SUMMARY);
+
+ $rcube = rcube::get_instance();
+ $sender = $rcube->user->get_identity();
+ $sender_email = $sender['email'] ?: $rcube->get_user_email();
+ $sender_name = $sender['name'] ?: $rcube->get_user_name();
+
+ $subject = 'KolabDAV iTIP message';
+ switch (strtoupper($iTipMessage->method)) {
+ case 'REPLY' :
+ $subject = 'Re: ' . $summary;
+ break;
+ case 'REQUEST' :
+ $subject = 'Invitation: ' .$summary;
+ break;
+ case 'CANCEL' :
+ $subject = 'Cancelled: ' . $summary;
+ break;
+ }
+
+ $sender = rcube_utils::idn_to_ascii($sender_email);
+ $from = format_email_recipient($sender, $sender_name);
+ $mailto = rcube_utils::idn_to_ascii($recipient);
+ $to = format_email_recipient($mailto, $iTipMessage->recipientName);
+
+ // copy some missing properties from master event to make it validate in our clients
+ if (Plugin::$parsed_vevent && strval(Plugin::$parsed_vevent->UID) == strval($iTipMessage->uid)) {
+ if (isset(Plugin::$parsed_vevent->DTEND)) {
+ $iTipMessage->message->VEVENT->DTEND = clone Plugin::$parsed_vevent->DTEND;
+ }
+ if (isset(Plugin::$parsed_vevent->STATUS)) {
+ $iTipMessage->message->VEVENT->STATUS = strval(Plugin::$parsed_vevent->STATUS);
+ }
+ }
+
+ // compose multipart message using PEAR:Mail_Mime
+ $message = new Mail_mime("\r\n");
+ $message->setParam('text_encoding', 'quoted-printable');
+ $message->setParam('head_encoding', 'quoted-printable');
+ $message->setParam('head_charset', RCUBE_CHARSET);
+ $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed");
+
+ // compose common headers array
+ $headers = array(
+ 'To' => $to,
+ 'From' => $from,
+ 'Date' => date('r'),
+ 'Reply-To' => $originator,
+ 'Message-ID' => $rcube->gen_message_id(),
+ 'X-Sender' => $sender,
+ 'Subject' => $subject,
+ );
+ if ($agent = $rcube->config->get('useragent'))
+ $headers['User-Agent'] = $agent;
+
+ $message->headers($headers);
+ $message->setContentType('text/calendar', array('method' => strval($iTipMessage->method), 'charset' => RCUBE_CHARSET));
+ $message->setTXTBody($iTipMessage->message->serialize());
+
+ // send message through Roundcube's SMTP feature
+ if ($rcube->deliver_message($message, $sender, $mailto, $smtp_error)) {
+ $iTipMessage->scheduleStatus = '1.1;Scheduling message sent via iMip';
+ }
+ else {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to send iTIP message to " . $mailto),
+ true, false);
+ }
+ }
+
+}
diff --git a/lib/Kolab/CalDAV/Plugin.php b/lib/Kolab/CalDAV/Plugin.php
index c23a06d..b584649 100644
--- a/lib/Kolab/CalDAV/Plugin.php
+++ b/lib/Kolab/CalDAV/Plugin.php
@@ -1,239 +1,229 @@
<?php
/**
* Extended CalDAV plugin for the Kolab DAV server
*
* @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\CalDAV;
use Sabre\DAV;
use Sabre\CalDAV;
use Sabre\VObject;
+use Sabre\HTTP;
+use Sabre\HTTP\URLUtil;
use Kolab\DAV\Auth\HTTPBasic;
/**
* Extended CalDAV plugin to tweak data validation
*/
class Plugin extends CalDAV\Plugin
{
// make already parsed text/calednar blocks available for later use
public static $parsed_vcalendar;
public static $parsed_vevent;
// allow the backend to force a redirect Location
public static $redirect_basename;
/**
* Initializes the plugin
*
* @param DAV\Server $server
* @return void
*/
public function initialize(DAV\Server $server)
{
parent::initialize($server);
- $server->subscribeEvent('afterCreateFile', array($this, 'afterWriteContent'));
- $server->subscribeEvent('afterWriteContent', array($this, 'afterWriteContent'));
+ $server->on('afterCreateFile', array($this, 'afterWriteContent'));
+ $server->on('afterWriteContent', array($this, 'afterWriteContent'));
}
/**
* Inject some additional HTTP response headers
*/
public function afterWriteContent($uri, $node)
{
// send Location: header to corrected URI
if (self::$redirect_basename) {
$path = explode('/', $uri);
array_pop($path);
array_push($path, self::$redirect_basename);
$this->server->httpResponse->setHeader('Location', $this->server->getBaseUri() . join('/', array_map('urlencode', $path)));
self::$redirect_basename = null;
}
}
/**
* Checks if the submitted iCalendar data is in fact, valid.
*
* An exception is thrown if it's not.
*
* @param resource|string $data
* @param string $path
+ * @param bool $modified Should be set to true, if this event handler
+ * changed &$data.
+ * @param RequestInterface $request The http request.
+ * @param ResponseInterface $response The http response.
+ * @param bool $isNew Is the item a new one, or an update.
* @return void
*/
- protected function validateICalendar(&$data, $path)
+ protected function validateICalendar(&$data, $path, &$modified, HTTP\RequestInterface $request, HTTP\ResponseInterface $response, $isNew)
{
// If it's a stream, we convert it to a string first.
if (is_resource($data)) {
$data = stream_get_contents($data);
}
+ $before = md5($data);
// Converting the data to unicode, if needed.
$data = DAV\StringUtil::ensureUTF8($data);
+ if ($before !== md5($data))
+ $modified = true;
+
try {
- // modification: Set options to be more tolerant when parsing extended or invalid properties
- $vobj = VObject\Reader::read($data, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
-
- // keep the parsed object in memory for later processing
- if ($vobj->name == 'VCALENDAR') {
- self::$parsed_vcalendar = $vobj;
- foreach ($vobj->getBaseComponents() ?: $vobj->getComponents() as $i => $vevent) {
- if ($vevent->name == 'VEVENT' || $vevent->name == 'VTODO') {
- self::$parsed_vevent = $vevent;
- break;
+ // If the data starts with a [, we can reasonably assume we're dealing
+ // with a jCal object.
+ if (substr($data,0,1) === '[') {
+ $vobj = VObject\Reader::readJson($data);
+
+ // Converting $data back to iCalendar, as that's what we
+ // technically support everywhere.
+ $data = $vobj->serialize();
+ $modified = true;
+ } else {
+ // modification: Set options to be more tolerant when parsing extended or invalid properties
+ $vobj = VObject\Reader::read($data, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
+
+ // keep the parsed object in memory for later processing
+ if ($vobj->name == 'VCALENDAR') {
+ self::$parsed_vcalendar = $vobj;
+ foreach ($vobj->getBaseComponents() ?: $vobj->getComponents() as $vevent) {
+ if ($vevent->name == 'VEVENT' || $vevent->name == 'VTODO') {
+ self::$parsed_vevent = $vevent;
+ break;
+ }
}
}
}
}
catch (VObject\ParseException $e) {
- throw new DAV\Exception\UnsupportedMediaType('This resource requires valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
+ throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
}
if ($vobj->name !== 'VCALENDAR') {
throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
}
+ $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+
// Get the Supported Components for the target calendar
- list($parentPath,$object) = DAV\URLUtil::splitPath($path);
- $calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'));
- $supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue();
+ list($parentPath) = URLUtil::splitPath($path);
+ $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
+
+ if (isset($calendarProperties[$sCCS])) {
+ $supportedComponents = $calendarProperties[$sCCS]->getValue();
+ }
+ else {
+ $supportedComponents = ['VTODO', 'VEVENT'];
+ }
$foundType = null;
$foundUID = null;
foreach($vobj->getComponents() as $component) {
switch($component->name) {
case 'VTIMEZONE':
continue 2;
case 'VEVENT':
case 'VTODO':
case 'VJOURNAL':
if (is_null($foundType)) {
$foundType = $component->name;
if (!in_array($foundType, $supportedComponents)) {
- throw new CalDAV\Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
+ throw new CalDAV\Exception\InvalidComponentType('This resource only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
}
if (!isset($component->UID)) {
throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
}
$foundUID = (string)$component->UID;
} else {
if ($foundType !== $component->name) {
throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
}
if ($foundUID !== (string)$component->UID) {
throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
}
}
break;
default:
throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
}
}
if (!$foundType)
throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
+
+ // We use an extra variable to allow event handles to tell us wether
+ // the object was modified or not.
+ //
+ // This helps us determine if we need to re-serialize the object.
+ $subModified = false;
+
+ $this->server->emit(
+ 'calendarObjectChange',
+ [
+ $request,
+ $response,
+ $vobj,
+ $parentPath,
+ &$subModified,
+ $isNew
+ ]
+ );
+
+ if ($subModified) {
+ // An event handler told us that it modified the object.
+ $data = $vobj->serialize();
+
+ // Using md5 to figure out if there was an *actual* change.
+ if (!$modified && $before !== md5($data)) {
+ $modified = true;
+ }
+ }
}
/**
* Returns a list of features for the DAV: HTTP header.
* Including 'calendar-schedule' to enable scheduling support in Thunderbird Lightning.
*
* @return array
*/
public function getFeatures()
{
$features = parent::getFeatures();
$features[] = 'calendar-schedule';
return $features;
}
- /**
- * Returns free-busy information for a specific address. The returned
- * data is an array containing the following properties:
- *
- * calendar-data : A VFREEBUSY VObject
- * request-status : an iTip status code.
- * href: The principal's email address, as requested
- *
- * @param string $email address
- * @param \DateTime $start
- * @param \DateTime $end
- * @param VObject\Component $request
- * @return array
- */
- protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request)
- {
- console(__METHOD__, $email, $start, $end);
-
- $email = preg_replace('/^mailto:/', '', $email);
-
- // pass-through the pre-generatd free/busy feed from Kolab's free/busy service
- if ($fburl = \kolab_storage::get_freebusy_url($email)) {
- // use PEAR::HTTP_Request2 for data fetching
- // @include_once('HTTP/Request2.php');
-
- try {
- $rcube = \rcube::get_instance();
- $request = new \HTTP_Request2($fburl);
- $request->setConfig(array(
- 'store_body' => true,
- 'follow_redirects' => true,
- 'ssl_verify_peer' => $rcube->config->get('kolab_ssl_verify_peer', true),
- ));
-
- $response = $request->send();
-
- // authentication required
- if ($response->getStatus() == 401) {
- $request->setAuth(HTTPBasic::$current_user, HTTPBasic::$current_pass);
- $response = $request->send();
- }
-
- // success!
- if ($response->getStatus() == 200) {
- $vcalendar = VObject\Reader::read($response->getBody(), VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
- return array(
- 'calendar-data' => $vcalendar,
- 'request-status' => '2.0;Success',
- 'href' => 'mailto:' . $email,
- );
- }
- }
- catch (\Exception $e) {
- // log failures
- \rcube::raise_error($e, true, false);
- }
- }
- else {
- // generate free/busy data from this user's calendars
- return parent::getFreeBusyForEmail($email, $start, $end, $request);
- }
-
- // return "not found"
- return array(
- 'request-status' => '3.7;Could not find principal',
- 'href' => 'mailto:' . $email,
- );
- }
}
\ No newline at end of file
diff --git a/lib/Kolab/CalDAV/ScheduleInbox.php b/lib/Kolab/CalDAV/ScheduleInbox.php
new file mode 100644
index 0000000..9514a46
--- /dev/null
+++ b/lib/Kolab/CalDAV/ScheduleInbox.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * Extended CalDAV scheduling inbox for the Kolab DAV server
+ *
+ * @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/>.
+ */
+
+namespace Kolab\CalDAV;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+
+/**
+ * Extended CalDAV scheduling inbox
+ */
+class ScheduleInbox extends CalDAV\Schedule\Inbox implements DAV\IProperties
+{
+ /**
+ * Updates properties on this node.
+ *
+ * @param PropPatch $propPatch
+ * @return void
+ * @see \Sabre\DAV\IProperties::propPatch()
+ */
+ function propPatch(DAV\PropPatch $propPatch)
+ {
+ // not implemented
+ throw new DAV\Exception\Forbidden("The Scheduling inbox doesn't allow property changes'");
+ }
+
+ /**
+ * Returns a list of properties for this nodes.
+ *
+ * The properties list is a list of propertynames the client requested,
+ * encoded in clark-notation {xmlnamespace}tagname
+ *
+ * @param array $properties
+ * @return array
+ */
+ function getProperties($properties)
+ {
+ $response = [];
+ foreach ($properties as $propname) {
+ // getctag is the only property we support
+ if ($propname == '{http://calendarserver.org/ns/}getctag') {
+ $response[$propname] = $this->caldavBackend->getSchedulingInboxCtag($this->principalUri);
+ }
+ }
+
+ return $response;
+ }
+}
diff --git a/lib/Kolab/CalDAV/SchedulePlugin.php b/lib/Kolab/CalDAV/SchedulePlugin.php
new file mode 100644
index 0000000..dbf4f0b
--- /dev/null
+++ b/lib/Kolab/CalDAV/SchedulePlugin.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * Extended CalDAV Schedule plugin for the Kolab DAV server
+ *
+ * @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/>.
+ */
+
+namespace Kolab\CalDAV;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+use Sabre\VObject;
+use Sabre\HTTP;
+
+/**
+ * Extended CalDAV Schedule plugin
+ */
+class SchedulePlugin extends CalDAV\Schedule\Plugin
+{
+ /**
+ * Returns free-busy information for a specific address. The returned
+ * data is an array containing the following properties:
+ *
+ * calendar-data : A VFREEBUSY VObject
+ * request-status : an iTip status code.
+ * href: The principal's email address, as requested
+ *
+ * @param string $email address
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @param VObject\Component $request
+ * @return array
+ */
+ protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request)
+ {
+ console(__METHOD__, $email, $start, $end);
+
+ $email = preg_replace('!^mailto:!i', '', $email);
+
+ // pass-through the pre-generatd free/busy feed from Kolab's free/busy service
+ if ($fburl = \kolab_storage::get_freebusy_url($email)) {
+ try {
+ $rcube = \rcube::get_instance();
+ $client = new HTTP\Client();
+ $client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, $rcube->config->get('kolab_ssl_verify_peer', true));
+
+ // authentication required
+ $client->on('error:401', function($request, $response, &$retry, $retryCount) {
+ if ($retryCount <= 1) {
+ // We're only going to retry exactly once.
+ $request->setHeader('Authorization', 'Basic ' . base64_encode(HTTPBasic::$current_user . ':' . HTTPBasic::$current_pass));
+ $retry = true;
+ }
+ });
+
+ $response = $client->send(new HTTP\Request('GET', $fburl));
+
+ // success!
+ if ($response->getStatus() == 200) {
+ $vcalendar = VObject\Reader::read($response->getBodyAsString(), VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
+ return array(
+ 'calendar-data' => $vcalendar,
+ 'request-status' => '2.0;Success',
+ 'href' => 'mailto:' . $email,
+ );
+ }
+ }
+ catch (\Exception $e) {
+ // log failures
+ \rcube::raise_error($e, true, false);
+ }
+ }
+ else {
+ // generate free/busy data from this user's calendars
+ return parent::getFreeBusyForEmail($email, $start, $end, $request);
+ }
+
+ // return "not found"
+ return array(
+ 'request-status' => '3.7;Could not find principal',
+ 'href' => 'mailto:' . $email,
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/Kolab/CalDAV/UserCalendars.php b/lib/Kolab/CalDAV/UserCalendars.php
index dcd2aff..cd17ea1 100644
--- a/lib/Kolab/CalDAV/UserCalendars.php
+++ b/lib/Kolab/CalDAV/UserCalendars.php
@@ -1,166 +1,139 @@
<?php
/**
* SabreDAV UserCalendars derived class for the 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\CalDAV;
use Sabre\DAV;
use Sabre\DAVACL;
use Sabre\CalDAV\Backend;
use Sabre\CalDAV\Schedule;
use Kolab\CalDAV\Calendar;
/**
* The UserCalenders class contains all calendars associated to one user
*
*/
-class UserCalendars extends \Sabre\CalDAV\UserCalendars implements DAV\IExtendedCollection, DAVACL\IACL
+class UserCalendars extends \Sabre\CalDAV\CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL
{
- private $outbox;
-
- /**
- * Returns a list of calendars
- *
- * @return array
- */
- public function getChildren()
- {
- $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
- $objs = array();
- foreach ($calendars as $calendar) {
- // TODO: (later) add sharing support by implenting this all
- if ($this->caldavBackend instanceof Backend\SharingSupport) {
- if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
- $objs[] = new SharedCalendar($this->caldavBackend, $calendar);
- }
- else {
- $objs[] = new ShareableCalendar($this->caldavBackend, $calendar);
- }
- }
- else {
- $objs[] = new Calendar($this->caldavBackend, $calendar);
- }
- }
-
- // add support for scheduling AKA free/busy
- $objs[] = new Schedule\Outbox($this->principalInfo['uri']);
-
- // TODO: add notification support (check with clients first, if anybody supports it)
- if ($this->caldavBackend instanceof Backend\NotificationSupport) {
- $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
- }
-
- return $objs;
- }
-
/**
* Returns a single calendar, by name
*
* @param string $name
* @return Calendar
*/
public function getChild($name)
{
- if ($name == 'outbox') {
- return new Schedule\Outbox($this->principalInfo['uri']);
+ // Special nodes
+ if ($name === 'inbox') {
+ return new ScheduleInbox($this->caldavBackend, $this->principalInfo['uri']);
}
- if ($calendar = $this->caldavBackend->getCalendarByName($name)) {
+
+ if (!in_array($name, array('inbox','outbox','notifications')) && ($calendar = $this->caldavBackend->getCalendarByName($name))) {
$calendar['principaluri'] = $this->principalInfo['uri'];
return new Calendar($this->caldavBackend, $calendar);
}
- throw new DAV\Exception\NotFound('Calendar with name \'' . $name . '\' could not be found');
+ return parent::getChild($name);
}
/**
* Checks if a calendar exists.
*
* @param string $name
* @return bool
*/
public function childExists($name)
{
+ // Special nodes
+ if ($name === 'inbox' || $name === 'outbox') {
+ return true;
+ }
+ if ($name === 'notifications') {
+ return false;
+ }
+
if ($this->caldavBackend->getCalendarByName($name)) {
return true;
}
return false;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* - 'privilege', a string such as {DAV:}read or {DAV:}write. These are currently the only supported privileges
* - 'principal', a url to the principal who owns the node
* - 'protected' (optional), indicating that this ACE is not allowed to be updated.
*
* @return array
*/
public function getACL()
{
// define rights for the user's calendar root (which is in fact INBOX)
return array(
array(
'privilege' => '{DAV:}read',
'principal' => $this->principalInfo['uri'],
'protected' => true,
),
array(
'privilege' => '{DAV:}write',
'principal' => $this->principalInfo['uri'],
'protected' => true,
),
/* TODO: implement sharing support
array(
'privilege' => '{DAV:}read',
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
'protected' => true,
),
array(
'privilege' => '{DAV:}write',
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
'protected' => true,
),
array(
'privilege' => '{DAV:}read',
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-read',
'protected' => true,
),
*/
);
}
/**
* Updates the ACL
*
* This method will receive a list of new ACE's.
*
* @param array $acl
* @return void
*/
public function setACL(array $acl)
{
// TODO: implement this
throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
}
}
diff --git a/lib/Kolab/CardDAV/ContactsBackend.php b/lib/Kolab/CardDAV/ContactsBackend.php
index 4563668..b0d3b3f 100644
--- a/lib/Kolab/CardDAV/ContactsBackend.php
+++ b/lib/Kolab/CardDAV/ContactsBackend.php
@@ -1,1150 +1,1166 @@
<?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 Sabre\VObject\DateTimeParser;
use Kolab\Utils\DAVBackend;
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
{
public static $supported_address_data = array(
array('contentType' => 'text/vcard', 'version' => '3.0'),
array('contentType' => 'text/vcard', 'version' => '4.0'),
);
public $ldap_directory;
private $sources;
private $folders;
private $aliases;
private $useragent;
private $subscribed = null;
// mapping of labelled X-AB properties to known vcard fields
private $xab_labelled_map = array(
'X-ABDATE' => array(
'anniversary' => 'X-ANNIVERSARY',
),
'X-ABRELATEDNAMES' => array(
'child' => 'X-CHILDREN',
'spouse' => 'X-SPOUSE',
'manager' => 'X-MANAGER',
'assistant' => 'X-ASSISTANT',
),
);
// known labels need to be quoted specially with _$!< >!$_
private $xab_known_labels = array('anniversary','child','parent','mother','father','brother','sister','friend','spouse','manager','assistant','partner','other');
// mapping of related types to internal contact properties
private $related_map = array(
'child' => 'children',
'spouse' => 'spouse',
'manager' => 'manager',
'assistant' => 'assistant',
);
/**
* 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->subscribed);
$this->sources = $this->folders = $this->aliases = array();
foreach (kolab_storage::sort_folders($folders) as $folder) {
$id = $folder->get_uid();
$fdata = $folder->get_imap_data(); // fetch IMAP folder data for CTag generation
$this->folders[$id] = $folder;
$this->sources[$id] = array(
'id' => $id,
'uri' => $id,
'{DAV:}displayname' => html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET),
'{http://calendarserver.org/ns/}getctag' => sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']),
'{urn:ietf:params:xml:ns:carddav}supported-address-data' => new CardDAV\Property\SupportedAddressData(self::$supported_address_data),
);
$this->aliases[$folder->name] = $id;
// map default folder to the magic 'all' resource
if ($folder->default)
$this->aliases['__all__'] = $id;
}
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)
{
// resolve alias name
if ($this->aliases[$id]) {
$id = $this->aliases[$id];
}
if ($this->folders[$id]) {
DAVBackend::check_storage_folder($this->folders[$id]);
return $this->folders[$id];
}
else {
return DAVBackend::get_storage_folder($id, 'contact');
}
}
/**
* Returns the list of addressbooks for a specific user.
*
* @param string $principalUri
* @return array
*/
public function getAddressBooksForUser($principalUri)
{
console(__METHOD__, $principalUri, $this->useragent);
$this->_read_sources();
// special case for the apple address book which only supports one (!) address book
if ($this->useragent == 'macosx' && count($this->sources) > 1) {
$source = $this->getAddressBookByName('__all__');
$source['principaluri'] = $principalUri;
return array($source);
}
$addressBooks = array();
foreach ($this->sources as $id => $source) {
$source['principaluri'] = $principalUri;
$addressBooks[] = $source;
}
return $addressBooks;
}
/**
* Returns properties for a specific node identified by name/uri
*
* @param string Node name/uri
* @return array Hash array with addressbook properties or null if not found
*/
public function getAddressBookByName($addressBookUri)
{
console(__METHOD__, $addressBookUri);
$this->_read_sources();
$id = $addressBookUri;
// return the magic *single* address book for Apple's Address Book App
if ($id == '__all__') {
$ctags = array();
foreach ($this->sources as $source) {
$ctags[] = $source['{http://calendarserver.org/ns/}getctag'];
}
return array(
'id' => '__all__',
'uri' => '__all__',
'{DAV:}displayname' => 'All',
'{http://calendarserver.org/ns/}getctag' => join(':', $ctags),
'{urn:ietf:params:xml:ns:carddav}supported-address-data' => new CardDAV\Property\SupportedAddressData(self::$supported_address_data),
);
}
// resolve aliases (addressbook by folder name)
if ($this->aliases[$addressBookUri]) {
$id = $this->aliases[$addressBookUri];
}
// retry with subscribed = false (#2701)
if (empty($this->sources[$id]) && $this->subscribed === null && rcube::get_instance()->config->get('kolab_use_subscriptions')) {
$this->subscribed = false;
unset($this->sources);
return $this->getAddressBookByName($addressBookUri);
}
return $this->sources[$id];
}
/**
- * Updates an addressbook's properties
+ * Updates properties for an address book.
*
- * See Sabre\DAV\IProperties for a description of the mutations array, as
- * well as the return value.
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
*
- * @param mixed $addressBookId
- * @param array $mutations
- * @see Sabre\DAV\IProperties::updateProperties
- * @return bool|array
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documenation for more info and examples.
+ *
+ * @param string $addressBookId
+ * @param \Sabre\DAV\PropPatch $propPatch
+ * @return void
*/
- public function updateAddressBook($addressBookId, array $mutations)
+ public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch)
{
- console(__METHOD__, $addressBookId, $mutations);
+ console(__METHOD__, $addressBookId, $propPatch);
if ($addressBookId == '__all__')
return false;
- $folder = $this->get_storage_folder($addressBookId);
- return $folder ? DAVBackend::folder_update($folder, $mutations) : false;
+ if ($folder = $this->get_storage_folder($addressBookId)) {
+ DAVBackend::handle_propatch($folder, $propPatch);
+ }
}
/**
* 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)
{
console(__METHOD__, $principalUri, $url, $properties);
return DAVBackend::folder_create('contact', $properties, $url);
}
/**
* Deletes an entire addressbook and all its contents
*
* @param int $addressBookId
* @return void
*/
public function deleteAddressBook($addressBookId)
{
console(__METHOD__, $addressBookId);
if ($addressBookId == '__all__')
return;
$folder = $this->get_storage_folder($addressBookId);
if ($folder && !kolab_storage::folder_delete($folder->name)) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting calendar folder $folder->name"),
true, false);
}
}
/**
* 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);
// recursively fetch contacts from all folders
if ($addressBookId == '__all__') {
$cards = array();
foreach ($this->sources as $id => $source) {
$cards = array_merge($cards, $this->getCards($id));
}
return $cards;
}
$groups_support = $this->useragent != 'thunderbird';
$query = array(array('type', '=', $groups_support ? array('contact','distribution-list') : 'contact'));
$cards = array();
if ($storage = $this->get_storage_folder($addressBookId)) {
foreach ($storage->select($query) as $contact) {
$cards[] = array(
'id' => $contact['uid'],
'uri' => VObjectUtils::uid2uri($contact['uid'], '.vcf'),
'lastmodified' => is_a($contact['changed'], 'DateTime') ? $contact['changed']->format('U') : null,
'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 = VObjectUtils::uri2uid($cardUri, '.vcf');
// search all folders for the given card
if ($addressBookId == '__all__') {
$contact = $this->get_card_by_uid($uid, $storage);
}
// read card data from LDAP directory
else if ($addressBookId == LDAPDirectory::DIRECTORY_NAME) {
if (is_object($this->ldap_directory)) {
$contact = $this->ldap_directory->getContactObject($uid);
}
}
else {
$storage = $this->get_storage_folder($addressBookId);
$contact = $storage->get_object($uid, '*');
}
if ($contact) {
return array(
'id' => $contact['uid'],
'uri' => VObjectUtils::uid2uri($contact['uid'], '.vcf'),
'lastmodified' => is_a($contact['changed'], 'DateTime') ? $contact['changed']->format('U') : null,
'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 = VObjectUtils::uri2uid($cardUri, '.vcf');
$storage = $this->get_storage_folder($addressBookId);
$object = $this->parse_vcard($cardData, $uid);
if (empty($object) || empty($object['uid'])) {
throw new DAV\Exception('Parse error: not a valid VCard object');
}
// if URI doesn't match the content's UID, the object might already exist!
$cardUri = VObjectUtils::uid2uri($object['uid'], '.vcf');
if ($object['uid'] != $uid && $this->getCard($addressBookId, $cardUri)) {
Plugin::$redirect_basename = $cardUri;
return $this->updateCard($addressBookId, $cardUri, $cardData);
}
$success = $storage->save($object, $object['_type']);
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
throw new DAV\Exception('Error saving contact card to backend');
}
// send Location: header if URI doesn't match object's UID (Bug #2109)
if ($object['uid'] != $uid) {
Plugin::$redirect_basename = $cardUri;
}
// 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 = VObjectUtils::uri2uid($cardUri, '.vcf');
$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);
throw new DAV\Exception\NotFound("UID doesn't match object URI");
}
if ($addressBookId == '__all__') {
$old = $this->get_card_by_uid($uid, $storage);
}
else {
if ($storage = $this->get_storage_folder($addressBookId))
$old = $storage->get_object($uid);
}
if (!$storage) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Unable to find storage folder for contact $addressBookId/$cardUri"),
true, false);
throw new DAV\Exception\NotFound("Invalid address book URI");
}
if (!$this->is_writeable($storage)) {
throw new DAV\Exception\Forbidden('Insufficient privileges to update this card');
}
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// save object
$saved = $storage->save($object, $object['_type'], $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);
Plugin::$redirect_basename = null;
throw new DAV\Exception('Error saving contact card to backend');
}
// 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);
$uid = VObjectUtils::uri2uid($cardUri, '.vcf');
if ($addressBookId == '__all__') {
$this->get_card_by_uid($uid, $storage);
}
else {
$storage = $this->get_storage_folder($addressBookId);
}
if (!$storage || !$this->is_writeable($storage)) {
throw new DAV\Exception\MethodNotAllowed('Insufficient privileges to delete this card');
}
if ($storage) {
return $storage->delete($uid);
}
return false;
}
/**
* Set User-Agent string of the connected client
*/
public function setUserAgent($uastring)
{
$ua_classes = array(
'thunderbird' => 'Thunderbird/\d',
'macosx' => '(Mac OS X/.+)?AddressBook/\d(.+\sCardDAVPlugin)?',
'ios' => '(iOS/\d|[Dd]ata[Aa]ccessd/\d)',
'vcard4' => '[Vv][Cc]ard([/ ])?4',
);
foreach ($ua_classes as $class => $regex) {
if (preg_match("!$regex!", $uastring)) {
$this->useragent = $class;
break;
}
}
}
/**
* Find an object and the containing folder by UID
*
* @param string Object UID
* @param object Return parameter for the kolab_storage_folder instance
* @return array|false
*/
private function get_card_by_uid($uid, &$storage)
{
$obj = kolab_storage::get_object($uid, 'contact');
if ($obj) {
$storage = kolab_storage::get_folder($obj['_mailbox']);
return $obj;
}
return false;
}
/**
* Internal helper method to determine whether the given kolab_storage_folder is writeable
*
*/
private function is_writeable($storage)
{
$rights = $storage->get_myrights();
return (strpos($rights, 'i') !== false || $storage->get_namespace() == 'personal');
}
/**
* Helper method to determine whether the connected client is an Apple device
*/
private function is_apple()
{
return $this->useragent == 'macosx' || $this->useragent == 'ios';
}
/**
* Helper method to determine whether the connected client supports VCard4
*/
private function is_vcard4()
{
return $this->useragent == 'vcard4';
}
/********** Data conversion utilities ***********/
private $phonetypes = array(
'main' => 'voice',
'homefax' => 'home,fax',
'workfax' => 'work,fax',
'mobile' => 'cell',
'other' => 'textphone',
);
private $improtocols = array(
'jabber' => 'xmpp',
);
/**
* 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 {
// use already parsed object
if (Plugin::$parsed_vcard && Plugin::$parsed_vcard->UID == $uid) {
$vobject = Plugin::$parsed_vcard;
}
else {
- 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
*/
public function to_vcard($contact)
{
$v4 = $this->is_vcard4();
$v4_prefix = $v4 ? '' : 'X-';
- $vc = VObject\Component::create('VCARD');
- $vc->version = $v4 ? '4.0' : '3.0';
- $vc->prodid = '-//Kolab//iRony DAV Server ' . KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+ $vc = new VObject\Component\VCard();
+ $vc->VERSION = $v4 ? '4.0' : '3.0';
+ $vc->PRODID = '-//Kolab//iRony DAV Server ' . KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
$vc->add('UID', $contact['uid']);
$vc->add('FN', $contact['name']);
// distlists are KIND:group
if ($contact['_type'] == 'distribution-list') {
// group cards are actually vcard version 4
if (!$this->is_apple()) {
$vc->version = '4.0';
$prop_prefix = '';
}
else {
// prefix group properties for Apple
$prop_prefix = 'X-ADDRESSBOOKSERVER-';
}
$vc->add($prop_prefix . 'KIND', 'group');
foreach ((array)$contact['member'] as $member) {
if ($member['uid'])
$value = 'urn:uuid:' . $member['uid'];
else if ($member['email'] && $member['name'])
$value = 'mailto:' . urlencode(sprintf('"%s" <%s>', addcslashes($member['name'], '"'), $member['email']));
else if ($member['email'])
$value = 'mailto:' . $member['email'];
$vc->add($prop_prefix . 'MEMBER', $value);
}
}
else if ($contact['surname'] . $contact['firstname'] . $contact['middlename'] . $contact['prefix'] . $contact['suffix'] != '') {
- $n = VObject\Property::create('N');
- $n->setParts(array($contact['surname'], $contact['firstname'], $contact['middlename'], $contact['prefix'], $contact['suffix']));
+ $n = $vc->create('N');
+ $n->setParts(array(strval($contact['surname']), strval($contact['firstname']), strval($contact['middlename']), strval($contact['prefix']), strval($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('ROLE', $contact['profession']);
if (!empty($contact['organization']) || !empty($contact['department'])) {
- $org = VObject\Property::create('ORG');
+ $org = $vc->create('ORG');
$org->setParts(array($contact['organization'], $contact['department']));
$vc->add($org);
}
// save as RELATED for VCard 4.0
if ($v4) {
foreach ($this->related_map as $type => $field) {
if (!empty($contact[$field])) {
foreach ((array)$contact[$field] as $value) {
- $vc->add(VObject\Property::create('RELATED', $value, array('type' => $type)));
+ $vc->add($vc->create('RELATED', $value, array('type' => $type)));
}
}
}
if (is_array($contact['related'])) {
foreach ($contact['related'] as $value) {
$vc->add('RELATED', $value);
}
}
}
else {
foreach (array_values($this->related_map) as $field) {
if (!empty($contact[$field])) {
$vc->add(strtoupper('X-' . $field), join(',', (array)$contact[$field]));
}
}
}
foreach ((array)$contact['email'] as $email) {
$types = array('INTERNET');
if (!empty($email['type']))
$types = array_merge($types, explode(',', strtoupper($email['type'])));
$vc->add('EMAIL', $email['address'], array('type' => $types));
}
foreach ((array)$contact['phone'] as $phone) {
$type = $this->phonetypes[$phone['type']] ?: $phone['type'];
$params = !empty($type) ? array('type' => explode(',', strtoupper($type))) : array();
$vc->add('TEL', $phone['number'], $params);
}
foreach ((array)$contact['website'] as $website) {
$params = !empty($website['type']) ? array('type' => explode(',', strtoupper($website['type']))) : array();
$vc->add('URL', $website['url'], $params);
}
$improtocolmap = array_flip($this->improtocols);
foreach ((array)$contact['im'] as $im) {
list($prot, $val) = explode(':', $im, 2);
if ($val) $vc->add('x-' . ($improtocolmap[$prot] ?: $prot), $val);
else $vc->add('IMPP', $im);
}
foreach ((array)$contact['address'] as $adr) {
$params = !empty($adr['type']) ? array('type' => strtoupper($adr['type'])) : array();
- $vadr = VObject\Property::create('ADR', null, $params);
+ $vadr = $vc->create('ADR', null, $params);
$vadr->setParts(array('','', $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']));
$vc->add($vadr);
}
if (!empty($contact['notes']))
$vc->add('NOTE', $contact['notes']);
if (!empty($contact['gender']))
$vc->add($this->is_apple() ? 'SEX' : $v4_prefix . 'GENDER', $contact['gender']);
// convert date cols to DateTime objects
foreach (array('birthday','anniversary') as $key) {
if (!empty($contact[$key]) && !$contact[$key] instanceof \DateTime) {
try {
$contact[$key] = new \DateTime(\rcube_utils::clean_datestr($contact[$key]));
}
catch (\Exception $e) {
$contact[$key] = null;
}
}
}
if (!empty($contact['birthday']) && $contact['birthday'] instanceof \DateTime) {
// FIXME: Date values are ignored by Thunderbird
$contact['birthday']->_dateonly = true;
- $vc->add(VObjectUtils::datetime_prop('BDAY', $contact['birthday'], false));
+ $vc->add(VObjectUtils::datetime_prop($vc, 'BDAY', $contact['birthday'], false));
}
if (!empty($contact['anniversary']) && $contact['anniversary'] instanceof \DateTime) {
$contact['anniversary']->_dateonly = true;
- $vc->add(VObjectUtils::datetime_prop($v4_prefix . 'ANNIVERSARY', $contact['anniversary'], false));
+ $vc->add(VObjectUtils::datetime_prop($vc, $v4_prefix . 'ANNIVERSARY', $contact['anniversary'], false));
}
if (!empty($contact['categories'])) {
- $cat = VObject\Property::create('CATEGORIES');
+ $cat = $vc->create('CATEGORIES');
$cat->setParts((array)$contact['categories']);
$vc->add($cat);
}
if (!empty($contact['freebusyurl'])) {
$vc->add('FBURL', $contact['freebusyurl']);
}
if (is_array($contact['lang'])) {
foreach ($contact['lang'] as $value) {
$vc->add('LANG', $value);
}
}
if (!empty($contact['photo'])) {
- $vc->PHOTO = base64_encode($contact['photo']);
- $vc->PHOTO->add('BASE64', null);
+ $vc->PHOTO = $contact['photo'];
+ $vc->PHOTO['ENCODING'] = 'B';
+ // $vc->PHOTO['TYPE'] = 'GIF';
+ // if ($v4) convert to uri; See VCardConverter::convertBinaryToUri()
}
// add custom properties
foreach ((array)$contact['x-custom'] as $prop) {
$vc->add($prop[0], $prop[1]);
}
// send some known fields as itemN.X-AB* for Apple clients
if ($this->is_apple()) {
$this->_to_apple($contact, $vc);
}
- if (!empty($contact['changed']))
- $vc->add(VObjectUtils::datetime_prop('REV', $contact['changed'], true));
+ if (!empty($contact['changed']) && is_a($contact['changed'], 'DateTime')) {
+ $vc->REV = $contact['changed']->format('Ymd\\THis\\Z');
+ }
+
+ // convert to VCard4.0
+ // if ($v4) {
+ // $vc->convert(VObject\Document::VCARD40);
+ //}
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(
'_type' => 'contact',
'uid' => strval($vc->UID),
'name' => strval($vc->FN),
'x-custom' => array(),
);
if ($vc->REV) {
- try { $contact['changed'] = $vc->REV->getDateTime(); }
+ try {
+ $contact['changed'] = DateTimeParser::parseDateTime(strval($vc->REV));
+ }
catch (\Exception $e) {
- try { $contact['changed'] = new \DateTime(strval($vc->REV)); }
- catch (\Exception $e) { }
+ // ignore
}
}
// normalize apple-style properties
$this->_from_apple($vc);
$phonetypemap = array_flip($this->phonetypes);
$phonetypemap['fax,home'] = 'homefax';
$phonetypemap['fax,work'] = 'workfax';
// map attributes to internal fields
foreach ($vc->children as $prop) {
if (!($prop instanceof VObject\Property))
continue;
+ $value = strval($prop);
+
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;
+ $contact['notes'] = $value;
break;
case 'TITLE':
- $contact['jobtitle'] = $prop->value;
+ $contact['jobtitle'] = $value;
break;
case 'NICKNAME':
- $contact[strtolower($prop->name)] = $prop->value;
+ $contact[strtolower($prop->name)] = $value;
break;
case 'ORG':
list($contact['organization'], $contact['department']) = $prop->getParts();
break;
case 'CATEGORY':
case 'CATEGORIES':
$contact['categories'] = $prop->getParts();
break;
case 'EMAIL':
$types = array_values(self::prop_filter($prop->offsetGet('type'), 'internet,pref', true));
- $contact['email'][] = array('address' => $prop->value, 'type' => strtolower($types[0] ?: 'other'));
+ $contact['email'][] = array('address' => $value, 'type' => strtolower($types[0] ?: 'other'));
break;
case 'URL':
$types = array_values(self::prop_filter($prop->offsetGet('type'), 'internet,pref', true));
- $contact['website'][] = array('url' => $prop->value, 'type' => strtolower($types[0]));
+ $contact['website'][] = array('url' => $value, 'type' => strtolower($types[0]));
break;
case 'TEL':
$types = array_values(self::prop_filter($prop->offsetGet('type'), 'voice,pref', true));
$types_ = strtolower(join(',', $types));
$type = isset($phonetypemap[$types_]) ? $types_ : strtolower($types[0]);
- $contact['phone'][] = array('number' => $prop->value, 'type' => $phonetypemap[$type] ?: $type);
+ $contact['phone'][] = array('number' => $value, 'type' => $phonetypemap[$type] ?: $type);
break;
case 'ADR':
$types = array_values(self::prop_filter($prop->offsetGet('type'), 'pref', true));
$adr = array('type' => strtolower(!empty($types) ? strval($types[0]) : $prop->parameters[0]->name));
list(,, $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']) = $prop->getParts();
$contact['address'][] = $adr;
break;
case 'BDAY':
- $contact['birthday'] = new \DateTime($prop->value);
+ $contact['birthday'] = new \DateTime($value);
$contact['birthday']->_dateonly = true;
break;
case 'ANNIVERSARY':
case 'X-ANNIVERSARY':
- $contact['anniversary'] = new \DateTime($prop->value);
+ $contact['anniversary'] = new \DateTime($value);
$contact['anniversary']->_dateonly = true;
break;
case 'SEX':
case 'GENDER':
case 'X-GENDER':
- $contact['gender'] = $prop->value;
+ $contact['gender'] = $value;
break;
case 'ROLE':
case 'X-PROFESSION':
- $contact['profession'] = $prop->value;
+ $contact['profession'] = $value;
break;
case 'X-MANAGER':
case 'X-ASSISTANT':
case 'X-CHILDREN':
case 'X-SPOUSE':
- $contact[strtolower(substr($prop->name, 2))] = explode(',', $prop->value);
+ $contact[strtolower(substr($prop->name, 2))] = explode(',', $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'][] = ($this->improtocols[$protocol] ?: $protocol) . ':' . preg_replace('/^[a-z]+:/i', '', $prop->value);
+ $contact['im'][] = ($this->improtocols[$protocol] ?: $protocol) . ':' . preg_replace('/^[a-z]+:/i', '', $value);
break;
case 'IMPP':
$prot = null;
- if (preg_match('/^[a-z]+:/i', $prop->value))
- list($prot, $val) = explode(':', $prop->value, 2);
+ if (preg_match('/^[a-z]+:/i', $value))
+ list($prot, $val) = explode(':', $value, 2);
else
- $val = $prop->value;
+ $val = $value;
$type = strtolower((string)$prop->offsetGet('X-SERVICE-TYPE'));
$protocol = $type && (!$prot || $prot == 'aim') ? ($this->improtocols[$type] ?: $type) : $prot;
$contact['im'][] = ($this->improtocols[$protocol] ?: $protocol) . ':' . urldecode($val);
break;
case 'PHOTO':
$param = $prop->offsetGet('encoding') ?: $prop->parameters[0];
- if ($param->value && (strtolower($param->value) == 'b' || strtolower($param->value) == 'base64') || strtolower($param->name) == 'base64') {
- $contact['photo'] = base64_decode($prop->value);
+ if (($pvalue = $param->getValue()) && (strtolower($pvalue) == 'b' || strtolower($pvalue) == 'base64') || strtolower($param->name) == 'base64') {
+ $contact['photo'] = $value;
}
break;
// VCard 4.0 properties
case 'FBURL':
- $contact['freebusyurl'] = $prop->value;
+ $contact['freebusyurl'] = $value;
break;
case 'LANG':
- $contact['lang'][] = $prop->value;
+ $contact['lang'][] = $value;
break;
case 'RELATED':
$type = strtolower($prop->offsetGet('type'));
if ($field = $this->related_map[$type]) {
- $contact[$field][] = $prop->value;
+ $contact[$field][] = $value;
}
else {
- $contact['related'][] = $prop->value;
+ $contact['related'][] = $value;
}
break;
case 'KIND':
case 'X-ADDRESSBOOKSERVER-KIND':
- if (strtolower($prop->value) == 'group') {
+ if (strtolower($value) == 'group') {
$contact['_type'] = 'distribution-list';
}
break;
case 'MEMBER':
case 'X-ADDRESSBOOKSERVER-MEMBER':
- if (strpos($prop->value, 'urn:uuid:') === 0) {
- $contact['member'][] = array('uid' => substr($prop->value, 9));
+ if (strpos($value, 'urn:uuid:') === 0) {
+ $contact['member'][] = array('uid' => substr($value, 9));
}
- else if (strpos($prop->value, 'mailto:') === 0) {
- $member = reset(\rcube_mime::decode_address_list(urldecode(substr($prop->value, 7))));
+ else if (strpos($value, 'mailto:') === 0) {
+ $member = reset(\rcube_mime::decode_address_list(urldecode(substr($value, 7))));
if ($member['mailto'])
$contact['member'][] = array('email' => $member['mailto'], 'name' => $member['name']);
}
break;
// custom properties
case 'CUSTOM1':
case 'CUSTOM2':
case 'CUSTOM3':
case 'CUSTOM4':
default:
if (substr($prop->name, 0, 2) == 'X-' || substr($prop->name, 0, 6) == 'CUSTOM') {
$prefix = $prop->group ? $prop->group . '.' : '';
- $contact['x-custom'][] = array($prefix . $prop->name, strval($prop->value));
+ $contact['x-custom'][] = array($prefix . $prop->name, strval($value));
}
break;
}
}
if (is_array($contact['im']))
$contact['im'] = array_unique($contact['im']);
return $contact;
}
/**
* Convert Apple-style item1.X-AB* properties to flat X-AB*-Label values
*/
private function _from_apple($vc)
{
foreach ($this->xab_labelled_map as $propname => $known_map) {
foreach ($vc->select($propname) as $prop) {
$labelkey = $prop->group ? $prop->group . '.X-ABLABEL' : 'X-ABLABEL';
$labels = $vc->select($labelkey);
- $field = !empty($labels) && ($label = reset($labels)) ? strtolower(trim($label->value, '_$!<>')) : null;
+ $field = !empty($labels) && ($label = reset($labels)) ? strtolower(trim(strval($label), '_$!<>')) : null;
if ($field) {
$prop->group = null;
$prop->name = ($known_map[$field] ?: $propname . '-' . strtoupper($field));
unset($vc->{$labelkey});
}
}
// must be an apple client :-)
$this->useragent = 'macosx';
}
}
/**
* Translate custom fields back to Apple-style item1.X-AB* properties
*/
private function _to_apple($contact, $vc)
{
$this->item_count = 1;
foreach ($this->xab_labelled_map as $propname => $known_map) {
// convert known vcard properties into labelled ones
foreach (array_flip($known_map) as $name => $label) {
if ($vc->{$name}) {
$this->_replace_with_labelled_prop($vc, $name, $propname, $label);
}
}
// translate custom properties with a matching prefix to labelled items
foreach ((array)$contact['x-custom'] as $prop) {
$name = $prop[0];
if (strpos($name, $propname) === 0) {
$label = strtolower(substr($name, strlen($propname)+1));
$this->_replace_with_labelled_prop($vc, $name, $propname, $label);
}
}
}
}
/**
* Helper method to replace a named property with a labelled one
*/
private function _replace_with_labelled_prop($vc, $name, $propname, $label)
{
$group = 'item' . ($this->item_count++);
$prop = clone $vc->{$name};
$prop->name = $propname;
$prop->group = $group;
$vc->add($prop);
- $ablabel = new VObject\Property('X-ABLabel');
- $ablabel->name = 'X-ABLabel';
+ $ablabel = $vc->create('X-ABLabel');
$ablabel->group = $group;
- $ablabel->value = in_array($label, $this->xab_known_labels) ? '_$!<'.ucfirst($label).'>!$_' : ucfirst($label);
+ $ablabel->setValue(in_array($label, $this->xab_known_labels) ? '_$!<'.ucfirst($label).'>!$_' : ucfirst($label));
$vc->add($ablabel);
unset($vc->{$name});
}
/**
* 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 prop_filter($arr, $values, $inverse = false)
{
if (!is_array($values)) {
$values = explode(',', $values);
}
// explode single, comma-separated value
if (count($arr) == 1 && strpos($arr[0], ',')) {
$arr = explode(',', $arr[0]);
}
$result = array();
$keep = array_flip((array)$values);
if (!empty($arr)) {
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), $contact['_msguid']);
}
}
diff --git a/lib/Kolab/CardDAV/LDAPDirectory.php b/lib/Kolab/CardDAV/LDAPDirectory.php
index 4030e11..7968ab9 100644
--- a/lib/Kolab/CardDAV/LDAPDirectory.php
+++ b/lib/Kolab/CardDAV/LDAPDirectory.php
@@ -1,578 +1,577 @@
<?php
/**
* CardDAV Directory class providing read-only access
* to an LDAP-based global address book.
*
* This implements the CardDAV Directory Gateway Extension suggested by Apple Inc.
* http://tools.ietf.org/html/draft-daboo-carddav-directory-gateway-02
*
* @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/>.
*/
namespace Kolab\CardDAV;
use \rcube;
use \rcube_ldap;
use \rcube_ldap_generic;
use Sabre\DAV;
use Sabre\DAVACL;
use Sabre\CardDAV\Property;
use Kolab\Utils\VObjectUtils;
/**
* CardDAV Directory Gateway implementation
*/
class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, DAV\IProperties, DAVACL\IACL
{
const DIRECTORY_NAME = 'ldap-directory';
private $config;
private $ldap;
private $carddavBackend;
private $principalUri;
private $addressBookInfo = array();
private $cache;
private $query;
private $filter;
/**
* Default constructor
*/
function __construct($config, $principalUri, $carddavBackend = null)
{
$this->config = $config;
$this->principalUri = $principalUri;
$this->addressBookInfo = array(
'id' => self::DIRECTORY_NAME,
'uri' => self::DIRECTORY_NAME,
'{DAV:}displayname' => $config['name'] ?: "LDAP Directory",
'{urn:ietf:params:xml:ns:carddav}supported-address-data' => new Property\SupportedAddressData(),
'principaluri' => $principalUri,
);
// used for vcard serialization
$this->carddavBackend = $carddavBackend ?: new ContactsBackend();
$this->carddavBackend->ldap_directory = $this;
// initialize cache
$rcube = rcube::get_instance();
if ($rcube->config->get('kolabdav_ldap_cache')) {
$this->cache = $rcube->get_cache_shared('kolabdav_ldap');
// expunge cache every now and then
if (rand(0,10) === 0) {
$this->cache->expunge();
}
}
}
private function connect()
{
if (!isset($this->ldap)) {
$this->ldap = new rcube_ldap($this->config, $this->config['debug']);
$this->ldap->set_pagesize($this->config['sizelimit'] ?: 10000);
}
return $this->ldap->ready ? $this->ldap : null;
}
/**
* Set parsed addressbook-query object for filtering
*/
function setAddressbookQuery($query)
{
$this->query = $query;
$this->filter = $this->addressbook_query2ldap_filter($query);
}
/**
* Returns the name of the node.
*
* This is used to generate the url.
*
* @return string
*/
function getName()
{
return self::DIRECTORY_NAME;
}
/**
* Returns a specific child node, referenced by its name
*
* This method must throw Sabre\DAV\Exception\NotFound if the node does not
* exist.
*
* @param string $name
* @return DAV\INode
*/
function getChild($cardUri)
{
console(__METHOD__, $cardUri);
$uid = VObjectUtils::uri2uid($cardUri, '.vcf');
$record = null;
// get from cache
$cache_key = $uid;
if ($this->cache && ($cached = $this->cache->get($cache_key))) {
return new LDAPCard($this->carddavBackend, $this->addressBookInfo, $cached);
}
if ($contact = $this->getContactObject($uid)) {
$obj = array(
'id' => $contact['uid'],
'uri' => VObjectUtils::uid2uri($contact['uid'], '.vcf'),
'lastmodified' => $contact['_timestamp'],
'carddata' => $this->carddavBackend->to_vcard($contact),
'etag' => self::_get_etag($contact),
);
// cache this object
if ($this->cache) {
$this->cache->set($cache_key, $obj);
}
return new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj);
}
throw new DAV\Exception\NotFound('Card not found');
}
/**
* Read contact object from LDAP
*/
function getContactObject($uid)
{
$contact = null;
if ($ldap = $this->connect()) {
$ldap->reset();
// used cached uid mapping
$cached_index = $this->cache ? $this->cache->get('index') : array();
if ($cached_index[$uid]) {
$contact = $ldap->get_record($cached_index[$uid][0], true);
}
else { // query for uid
$result = $ldap->search('uid', $uid, 1, true, true);
if ($result->count) {
$contact = $result[0];
}
}
if ($contact) {
$this->_normalize_contact($contact);
}
}
return $contact;
}
/**
* Returns an array with all the child nodes
*
* @return DAV\INode[]
*/
function getChildren()
{
console(__METHOD__, $this->query, $this->filter);
$children = array();
// return cached index
if (!$this->query && !$this->config['searchonly'] && $this->cache && ($cached_index = $this->cache->get('index'))) {
foreach ($cached_index as $uid => $c) {
$obj = array(
'id' => $uid,
'uri' => VObjectUtils::uid2uri($uid, '.vcf'),
'etag' => $c[1],
'lastmodified' => $c[2],
);
$children[] = new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj);
}
return $children;
}
// query LDAP if we have a search query or listing is allowed
if (($this->query || !$this->config['searchonly']) && ($ldap = $this->connect())) {
// set pagesize from query limit attribute
if ($this->query && $this->query->limit) {
$this->ldap->set_pagesize(intval($this->query->limit));
}
// set the prepared LDAP filter derived from the addressbook-query
if ($this->query && !empty($this->filter)) {
$ldap->set_search_set($this->filter);
}
else {
$ldap->set_search_set(null);
}
$results = $ldap->list_records(null);
$directory_index = array();
// convert results into vcard blocks
foreach ($results as $contact) {
$this->_normalize_contact($contact);
$obj = array(
'id' => $contact['uid'],
'uri' => VObjectUtils::uid2uri($contact['uid'], '.vcf'),
'lastmodified' => $contact['_timestamp'],
'carddata' => $this->carddavBackend->to_vcard($contact),
'etag' => self::_get_etag($contact),
);
// cache record
$cache_key = $contact['uid'];
if ($this->cache) {
$this->cache->set($cache_key, $obj);
}
$directory_index[$contact['uid']] = array($contact['ID'], $obj['etag'], $contact['_timestamp']);
// add CardDAV node
$children[] = new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj);
}
// cache the full listing
if (empty($this->filter) && $this->cache) {
$this->cache->set('index', $directory_index);
}
}
return $children;
}
/**
* Returns a list of properties for this node.
*
* The properties list is a list of propertynames the client requested,
* encoded in clark-notation {xmlnamespace}tagname
*
* If the array is empty, it means 'all properties' were requested.
*
* @param array $properties
* @return array
*/
public function getProperties($properties)
{
console(__METHOD__, $properties);
$response = array();
foreach ($properties as $propertyName) {
if (isset($this->addressBookInfo[$propertyName])) {
$response[$propertyName] = $this->addressBookInfo[$propertyName];
}
else if ($propertyName == '{DAV:}getlastmodified') {
$response[$propertyName] = new DAV\Property\GetLastModified($this->getLastModified());
}
}
return $response;
}
/**
* Returns the last modification time, as a unix timestamp
*
* @return int
*/
function getLastModified()
{
console(__METHOD__);
return time();
}
/**
* Deletes the entire addressbook.
*
* @return void
*/
public function delete()
{
throw new DAV\Exception\MethodNotAllowed('Deleting directories is not allowed');
}
/**
* Renames the addressbook
*
* @param string $newName
* @return void
*/
public function setName($newName)
{
throw new DAV\Exception\MethodNotAllowed('Renaming directories not allowed');
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
public function getOwner()
{
return $this->principalUri;
}
/**
* Returns a group principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getGroup()
{
return null;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to be updated.
*
* @return array
*/
public function getACL()
{
$acl = array(
array(
'privilege' => '{DAV:}read',
'principal' => $this->principalUri,
'protected' => true,
),
);
return $acl;
}
/**
* Updates the ACL
*
* @param array $acl
* @return void
*/
function setACL(array $acl)
{
throw new DAV\Exception\MethodNotAllowed('Changing ACL for directories is not allowed');
}
/**
* Returns the list of supported privileges for this node.
*
* If null is returned from this method, the default privilege set is used,
* which is fine for most common usecases.
*
* @return array|null
*/
function getSupportedPrivilegeSet()
{
return null;
}
/**
- * Updates properties on this node,
+ * Updates properties on this node.
*
- * @param array $mutations
- * @return bool|array
+ * @param PropPatch $propPatch
+ * @return void
*/
- function updateProperties($mutations)
+ public function propPatch(DAV\PropPatch $propPatch)
{
- console(__METHOD__, $mutations);
- return false;
+ console(__METHOD__, $propPatch);
+ // NOP
}
-
/**
* Post-process the given contact record from rcube_ldap
*/
private function _normalize_contact(&$contact)
{
if (is_numeric($contact['changed'])) {
$contact['_timestamp'] = intval($contact['changed']);
$contact['changed'] = new \DateTime('@' . $contact['changed']);
}
else if (!empty($contact['changed'])) {
try {
$contact['changed'] = new \DateTime($contact['changed']);
$contact['_timestamp'] = intval($contact['changed']->format('U'));
}
catch (Exception $e) {
$contact['changed'] = null;
}
}
// map col:subtype fields to a list that the vcard serialization function understands
foreach (array('email' => 'address', 'phone' => 'number', 'website' => 'url') as $col => $prop) {
foreach (rcube_ldap::get_col_values($col, $contact) as $type => $values) {
foreach ((array)$values as $value) {
$contact[$col][] = array($prop => $value, 'type' => $type);
}
}
unset($contact[$col.':'.$type]);
}
$addresses = array();
foreach (rcube_ldap::get_col_values('address', $contact) as $type => $values) {
foreach ((array)$values as $adr) {
// skip empty address
$adr = array_filter($adr);
if (empty($adr))
continue;
$addresses[] = array(
'type' => $type,
'street' => $adr['street'],
'locality' => $adr['locality'],
'code' => $adr['zipcode'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
unset($contact['address:'.$type]);
}
$contact['address'] = $addresses;
}
/**
* Translate the given AddressBookQueryParser object into an LDAP filter
*/
private function addressbook_query2ldap_filter($query)
{
$criterias = array();
foreach ($query->filters as $filter) {
$ldap_attrs = $this->map_property2ldap($filter['name']);
$ldap_filter = ''; $count = 0;
// unknown attribute, skip
if (empty($ldap_attrs)) {
continue;
}
foreach ((array)$filter['text-matches'] as $matcher) {
// case-insensitive matching
if (in_array($matcher['collation'], array('i;unicode-casemap', 'i;ascii-casemap'))) {
$matcher['value'] = mb_strtolower($matcher['value']);
}
$value = rcube_ldap_generic::quote_string($matcher['value']);
$ldap_match = '';
// this assumes fuzzy search capabilities of the LDAP backend
switch ($matcher['match-type']) {
case 'contains':
$wp = $ws = '*';
break;
case 'starts-with':
$ws = '*';
break;
case 'ends-with':
$wp = '*';
break;
default:
$wp = $ws = '';
}
// OR query for all attributes involved
if (count($ldap_attrs) > 1) {
$ldap_match .= '(|';
}
foreach ($ldap_attrs as $attr) {
$ldap_match .= "($attr=$wp$value$ws)";
}
if (count($ldap_attrs) > 1) {
$ldap_match .= ')';
}
// negate the filter
if ($matcher['negate-condition']) {
$ldap_match = '(!' . $ldap_match . ')';
}
$ldap_filter .= $ldap_match;
$count++;
}
if ($count > 1) {
$criterias[] = '(' . ($filter['test'] == 'allof' ? '&' : '|') . $ldap_filter . ')';
}
else if (!empty($ldap_filter)) {
$criterias[] = $ldap_filter;
}
}
return empty($criterias) ? '' : sprintf('(%s%s)', $query->test == 'allof' ? '&' : '|', join('', $criterias));
}
/**
* Map a vcard property to an LDAP attribute
*/
private function map_property2ldap($propname)
{
$attribs = array();
// LDAP backend not available, abort
if (!($ldap = $this->connect())) {
return $attribs;
}
$vcard_fieldmap = array(
'FN' => array('name'),
'N' => array('surname','firstname','middlename'),
'ADR' => array('street','locality','region','code','country'),
'TITLE' => array('jobtitle'),
'ORG' => array('organization','department'),
'TEL' => array('phone'),
'URL' => array('website'),
'ROLE' => array('profession'),
'BDAY' => array('birthday'),
'IMPP' => array('im'),
);
$fields = $vcard_fieldmap[$propname] ?: array(strtolower($propname));
foreach ($fields as $field) {
if ($ldap->coltypes[$field]) {
$attribs = array_merge($attribs, (array)$ldap->coltypes[$field]['attributes']);
}
}
return $attribs;
}
/**
* 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), $contact['_timestamp']);
}
}
diff --git a/lib/Kolab/CardDAV/Plugin.php b/lib/Kolab/CardDAV/Plugin.php
index 1441d29..053fee0 100644
--- a/lib/Kolab/CardDAV/Plugin.php
+++ b/lib/Kolab/CardDAV/Plugin.php
@@ -1,204 +1,225 @@
<?php
/**
* Extended CardDAV plugin for the Kolab DAV server
*
* @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 Sabre\DAV;
use Sabre\DAVACL;
use Sabre\CardDAV;
use Sabre\VObject;
/**
* Extended CardDAV plugin to tweak data validation
*/
class Plugin extends CardDAV\Plugin
{
// make already parsed vcard blocks available for later use
public static $parsed_vcard;
// allow the backend to force a redirect Location
public static $redirect_basename;
/**
* Initializes the plugin
*
* @param DAV\Server $server
* @return void
*/
public function initialize(DAV\Server $server)
{
parent::initialize($server);
- $server->subscribeEvent('beforeMethod', array($this, 'beforeMethod'));
- $server->subscribeEvent('afterCreateFile', array($this, 'afterWriteContent'));
- $server->subscribeEvent('afterWriteContent', array($this, 'afterWriteContent'));
+ $server->on('beforeMethod', array($this, 'beforeMethod'));
+ $server->on('afterCreateFile', array($this, 'afterWriteContent'));
+ $server->on('afterWriteContent', array($this, 'afterWriteContent'));
}
/**
* Adds all CardDAV-specific properties
*
- * @param string $path
+ * @param DAV\PropFind $propFind
* @param DAV\INode $node
- * @param array $requestedProperties
- * @param array $returnedProperties
* @return void
*/
- public function beforeGetProperties($path, DAV\INode $node, array &$requestedProperties, array &$returnedProperties)
+ public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
{
// publish global ldap address book for this principal
if ($node instanceof DAVACL\IPrincipal && empty($this->directories) && \rcube::get_instance()->config->get('kolabdav_ldap_directory')) {
$this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPDirectory::DIRECTORY_NAME;
}
- parent::beforeGetProperties($path, $node, $requestedProperties, $returnedProperties);
+ parent::propFindEarly($propFind, $node);
}
/**
* Handler for beforeMethod events
*/
public function beforeMethod($method, $uri)
{
if ($method == 'PUT' && $this->server->httpRequest->getHeader('If-None-Match') == '*') {
// In-None-Match: * is only valid with PUT requests creating a new resource.
// SOGo Conenctor for Thunderbird also sends it with update requests which then fail
// in the Server::checkPreconditions().
// See https://issues.kolab.org/show_bug.cgi?id=2589 and http://www.sogo.nu/bugs/view.php?id=1624
// This is a work-around for the buggy SOGo connector and should be removed once fixed.
if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird/') > 0) {
unset($_SERVER['HTTP_IF_NONE_MATCH']);
}
}
}
/**
* Inject some additional HTTP response headers
*/
public function afterWriteContent($uri, $node)
{
// send Location: header to corrected URI
if (self::$redirect_basename) {
$path = explode('/', $uri);
array_pop($path);
array_push($path, self::$redirect_basename);
$this->server->httpResponse->setHeader('Location', $this->server->getBaseUri() . join('/', array_map('urlencode', $path)));
self::$redirect_basename = null;
}
}
/**
* Checks if the submitted iCalendar data is in fact, valid.
*
* An exception is thrown if it's not.
*
* @param resource|string $data
* @return void
*/
protected function validateVCard(&$data)
{
// If it's a stream, we convert it to a string first.
if (is_resource($data)) {
$data = stream_get_contents($data);
}
// Converting the data to unicode, if needed.
$data = DAV\StringUtil::ensureUTF8($data);
try {
- VObject\Property::$classMap['REV'] = 'Sabre\\VObject\\Property\\DateTime';
$vobj = VObject\Reader::read($data, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
if ($vobj->name == 'VCARD')
$this->parsed_vcard = $vobj;
}
catch (VObject\ParseException $e) {
throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
}
if ($vobj->name !== 'VCARD') {
throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
}
if (!isset($vobj->UID)) {
throw new DAV\Exception\BadRequest('Every vcard must have a UID.');
}
}
+ /**
+ * Converts a vcard blob to a different version, or jcard.
+ *
+ * (optimized version that skips parsing and re-serialization if possible)
+ *
+ * @param string $data
+ * @param string $target
+ * @return string
+ */
+ protected function convertVCard($data, $target)
+ {
+ $version = 'vcard3';
+ if (is_string($data) && preg_match('/VERSION:(\d)/', $data, $m)) {
+ $version = 'vcard' . $m[1];
+ }
+
+ // no conversion needed
+ if ($target == $version) {
+ return $data;
+ }
+
+ return parent::convertVCard($data, $target);
+ }
+
/**
* This function handles the addressbook-query REPORT
*
* This report is used by the client to filter an addressbook based on a
* complex query.
*
* @param \DOMNode $dom
* @return void
*/
protected function addressbookQueryReport($dom)
{
$node = $this->server->tree->getNodeForPath(($uri = $this->server->getRequestUri()));
console(__METHOD__, $uri);
// fix some bogus parameters in queries sent by the SOGo connector.
// issue submitted in http://www.sogo.nu/bugs/view.php?id=2655
$xpath = new \DOMXPath($dom);
$xpath->registerNameSpace('card', Plugin::NS_CARDDAV);
$filters = $xpath->query('/card:addressbook-query/card:filter');
if ($filters->length === 1) {
$filter = $filters->item(0);
$propFilters = $xpath->query('card:prop-filter', $filter);
for ($ii=0; $ii < $propFilters->length; $ii++) {
$propFilter = $propFilters->item($ii);
$name = $propFilter->getAttribute('name');
// attribute 'mail' => EMAIL
if ($name == 'mail') {
$propFilter->setAttribute('name', 'EMAIL');
}
$textMatches = $xpath->query('card:text-match', $propFilter);
for ($jj=0; $jj < $textMatches->length; $jj++) {
$textMatch = $textMatches->item($jj);
$collation = $textMatch->getAttribute('collation');
// 'i;unicasemap' is a non-standard collation
if ($collation == 'i;unicasemap') {
$textMatch->setAttribute('collation', 'i;unicode-casemap');
}
}
}
}
// query on LDAP node: pass along filter query
if ($node instanceof LDAPDirectory) {
$query = new CardDAV\AddressBookQueryParser($dom);
$query->parse();
// set query and ...
$node->setAddressbookQuery($query);
}
// ... proceed with default action
parent::addressbookQueryReport($dom);
}
}
\ No newline at end of file
diff --git a/lib/Kolab/CardDAV/UserAddressBooks.php b/lib/Kolab/CardDAV/UserAddressBooks.php
index 6beb78a..56f76f0 100644
--- a/lib/Kolab/CardDAV/UserAddressBooks.php
+++ b/lib/Kolab/CardDAV/UserAddressBooks.php
@@ -1,136 +1,131 @@
<?php
/**
* SabreDAV UserAddressBooks derived class for the 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 Sabre\DAV;
use Sabre\DAVACL;
use Sabre\CardDAV;
/**
* UserAddressBooks class
*
* The UserAddressBooks collection contains a list of addressbooks associated with a user
*/
class UserAddressBooks extends \Sabre\CardDAV\UserAddressBooks implements DAV\IExtendedCollection, DAV\IProperties, DAVACL\IACL
{
// pseudo-singleton instance
private $ldap_directory;
/**
* Returns a list of addressbooks
*
* @return array
*/
public function getChildren()
{
$addressbooks = $this->carddavBackend->getAddressbooksForUser($this->principalUri);
$objs = array();
foreach($addressbooks as $addressbook) {
$objs[] = new AddressBook($this->carddavBackend, $addressbook);
}
if (rcube::get_instance()->config->get('kolabdav_ldap_directory')) {
$objs[] = $this->getLDAPDirectory();
}
return $objs;
}
/**
* Returns a single addressbook, by name
*
* @param string $name
* @return \AddressBook
*/
public function getChild($name)
{
if ($name == LDAPDirectory::DIRECTORY_NAME) {
return $this->getLDAPDirectory();
}
if ($addressbook = $this->carddavBackend->getAddressBookByName($name)) {
$addressbook['principaluri'] = $this->principalUri;
return new AddressBook($this->carddavBackend, $addressbook);
}
throw new DAV\Exception\NotFound('Addressbook with name \'' . $name . '\' could not be found');
}
/**
* Getter for the singleton instance of the LDAP directory
*/
private function getLDAPDirectory()
{
if (!$this->ldap_directory) {
$rcube = rcube::get_instance();
$config = $rcube->config->get('kolabdav_ldap_directory');
$config['debug'] = $rcube->config->get('ldap_debug');
$this->ldap_directory = new LDAPDirectory($config, $this->principalUri, $this->carddavBackend);
}
return $this->ldap_directory;
}
/**
* Returns the list of properties
*
* @param array $requestedProperties
* @return array
*/
public function getProperties($requestedProperties)
{
// console(__METHOD__, $requestedProperties);
$response = array();
foreach ($requestedProperties as $prop) {
switch($prop) {
case '{urn:ietf:params:xml:ns:carddav}supported-address-data':
$response[$prop] = new CardDAV\Property\SupportedAddressData(ContactsBackend::$supported_address_data);
break;
}
}
return $response;
}
/**
- * Updates properties such as the display name and description
+ * Updates properties on this node.
*
- * @param array $mutations
- * @return array
+ * @param PropPatch $propPatch
+ * @return void
*/
- public function updateProperties($mutations)
+ public function propPatch(DAV\PropPatch $propPatch)
{
- $errors = array();
-
- foreach ($mutations as $prop => $val) {
- $errors[403][$prop] = null;
- }
-
- return $errors;
+ console(__METHOD__, $propPatch);
+ // NOP
}
}
diff --git a/lib/Kolab/DAV/Auth/HTTPBasic.php b/lib/Kolab/DAV/Auth/HTTPBasic.php
index f152cc8..bb9d9a2 100644
--- a/lib/Kolab/DAV/Auth/HTTPBasic.php
+++ b/lib/Kolab/DAV/Auth/HTTPBasic.php
@@ -1,242 +1,247 @@
<?php
/**
* SabreDAV Auth Backend implementation 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\DAV\Auth;
use \rcube;
use \rcube_imap_generic;
use \rcube_user;
use \rcube_utils;
use Sabre\DAV;
/**
*
*/
class HTTPBasic extends DAV\Auth\Backend\AbstractBasic
{
// Make the current user name available to all classes
public static $current_user = null;
public static $current_pass = null;
/**
* Validates a username and password
*
* This method should return true or false depending on if login
* succeeded.
*
* @param string $username
* @param string $password
* @return bool
*/
protected function validateUserPass($username, $password)
{
$rcube = rcube::get_instance();
$host = $this->_select_host($username);
// use shared cache for kolab_auth plugin result (username canonification)
$cache = $rcube->get_cache_shared('kolabdav_auth');
$cache_key = sha1($username . '::' . $host);
if (!$cache || !($auth = $cache->get($cache_key))) {
$auth = $rcube->plugins->exec_hook('authenticate', array(
'host' => $host,
'user' => $username,
'pass' => $password,
));
if ($cache && !$auth['abort']) {
$cache->set($cache_key, array(
'user' => $auth['user'],
'host' => $auth['host'],
+ 'dn' => $_SESSION['kolab_dn'],
+ 'vars' => $_SESSION['kolab_auth_vars'],
));
}
// LDAP server failure... send 503 error
if ($auth['kolab_ldap_error']) {
throw new ServiceUnavailable('The service is temporarily unavailable (LDAP failure)');
}
}
else {
$auth['pass'] = $password;
+ // set some session vars from kolab_auth
+ $_SESSION['kolab_dn'] = $auth['dn'];
+ $_SESSION['kolab_auth_vars'] = $auth['vars'];
}
// authenticate user against the IMAP server
$user_id = $auth['abort'] ? 0 : $this->_login($auth['user'], $auth['pass'], $auth['host'], $error);
if ($user_id) {
self::$current_user = $auth['user'];
self::$current_pass = $auth['pass'];
return true;
}
// IMAP server failure... send 503 error
if ($error == rcube_imap_generic::ERROR_BAD) {
throw new ServiceUnavailable('The service is temporarily unavailable (Storage failure)');
}
return false;
}
/**
* Returns information about the currently logged in username.
*
* If nobody is currently logged in, this method should return null.
*
* @return string|null
*/
public function getCurrentUser()
{
// return the canonic user name
return self::$current_user;
}
/**
* Storage host selection
*/
protected function _select_host($username)
{
// Get IMAP host
$rcube = rcube::get_instance();
$host = $rcube->config->get('default_host', 'localhost');
if (is_array($host)) {
list($user, $domain) = explode('@', $username);
// try to select host by mail domain
if (!empty($domain)) {
foreach ($host as $storage_host => $mail_domains) {
if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
$host = $storage_host;
break;
}
else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
$host = is_numeric($storage_host) ? $mail_domains : $storage_host;
break;
}
}
}
// take the first entry if $host is not found
if (is_array($host)) {
list($key, $val) = each($default_host);
$host = is_numeric($key) ? $val : $key;
}
}
return rcube_utils::parse_host($host);
}
/**
* Authenticates a user in IMAP and returns Roundcube user ID.
*/
protected function _login($username, $password, $host, &$error = null)
{
if (empty($username)) {
return null;
}
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
$login_lc = $rcube->config->get('login_lc');
$default_port = $rcube->config->get('default_port', 143);
// parse $host
$a_host = parse_url($host);
if ($a_host['host']) {
$host = $a_host['host'];
$ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (!empty($a_host['port'])) {
$port = $a_host['port'];
}
else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) {
$port = 993;
}
}
if (!$port) {
$port = $default_port;
}
// Convert username to lowercase. If storage backend
// is case-insensitive we need to store always the same username
if ($login_lc) {
if ($login_lc == 2 || $login_lc === true) {
$username = mb_strtolower($username);
}
else if (strpos($username, '@')) {
// lowercase domain name
list($local, $domain) = explode('@', $username);
$username = $local . '@' . mb_strtolower($domain);
}
}
// Here we need IDNA ASCII
// Only rcube_contacts class is using domain names in Unicode
$host = rcube_utils::idn_to_ascii($host);
$username = rcube_utils::idn_to_ascii($username);
// user already registered?
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
}
// authenticate user in IMAP
if (!$storage->connect($host, $username, $password, $port, $ssl)) {
$error = $storage->get_error_code();
return null;
}
// No user in database, but IMAP auth works
if (!is_object($user)) {
if ($rcube->config->get('auto_create_user')) {
// create a new user record
$user = rcube_user::create($username, $host);
if (!$user) {
rcube::raise_error(array(
'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to create a user record",
), true, false);
return null;
}
}
else {
rcube::raise_error(array(
'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__,
'message' => "Access denied for new user $username. 'auto_create_user' is disabled",
), true, false);
return null;
}
}
// overwrite config with user preferences
$rcube->user = $user;
$rcube->config->set_user_prefs((array)$rcube->user->get_prefs());
$rcube->password = $password;
setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8');
return $user->ID;
}
}
diff --git a/lib/Kolab/DAV/File.php b/lib/Kolab/DAV/File.php
index b7e0b20..d7a6dfd 100644
--- a/lib/Kolab/DAV/File.php
+++ b/lib/Kolab/DAV/File.php
@@ -1,224 +1,201 @@
<?php
/**
* SabreDAV File Backend implementation for Kolab.
*
* @author Aleksander Machniak <machniak@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\DAV;
use \Exception;
use \DateTime;
/**
* File class
*/
class File extends Node implements \Sabre\DAV\IFile, \Sabre\DAV\IProperties
{
/**
* Updates the data
*
* The data argument is a readable stream resource.
*
* After a succesful put operation, you may choose to return an ETag. The
* etag must always be surrounded by double-quotes. These quotes must
* appear in the actual string you're returning.
*
* Clients may use the ETag from a PUT request to later on make sure that
* when they update the file, the contents haven't changed in the mean
* time.
*
* If you don't plan to store the file byte-by-byte, and you return a
* different object on a subsequent GET you are strongly recommended to not
* return an ETag, and just return null.
*
* @param resource $data
* @throws Sabre\DAV\Exception
* @return string|null
*/
public function put($data)
{
$filedata = $this->fileData($this->path, $data);
try {
$this->backend->file_update($this->path, $filedata);
}
catch (Exception $e) {
$this->throw_exception($e);
}
try {
$this->data = $this->backend->file_info($this->path);
}
catch (Exception $e) {
$this->throw_exception($e);
}
return $this->getETag();
}
/**
* Returns the file content
*
* This method may either return a string or a readable stream resource
*
* @throws Sabre\DAV\Exception
* @return mixed
*/
public function get()
{
try {
$fp = fopen('php://temp', 'bw+');
$this->backend->file_get($this->path, array(), $fp);
rewind($fp);
}
catch (Exception $e) {
$this->throw_exception($e);
}
return $fp;
}
/**
* Delete the current file
*
* @throws Sabre\DAV\Exception
* @return void
*/
public function delete()
{
try {
$this->backend->file_delete($this->path);
}
catch (Exception $e) {
$this->throw_exception($e);
}
// reset cache
if ($this->parent) {
$this->parent->children = null;
}
}
/**
* Returns the size of the node, in bytes
*
* @return int
*/
public function getSize()
{
return $this->data['size'];
}
/**
* Returns the ETag for a file
*
* An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
* The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
*
* Return null if the ETag can not effectively be determined
*
* @return mixed
*/
public function getETag()
{
return sprintf('"%s-%d"', substr(md5($this->path . ':' . $this->data['size']), 0, 16), $this->data['modified']);
}
/**
* Returns the mime-type for a file
*
* If null is returned, we'll assume application/octet-stream
*
* @return mixed
*/
public function getContentType()
{
return $this->data['type'];
}
/**
- * Updates properties on this node,
+ * Updates properties on this node.
*
- * The properties array uses the propertyName in clark-notation as key,
- * and the array value for the property value. In the case a property
- * should be deleted, the property value will be null.
+ * This method received a PropPatch object, which contains all the
+ * information about the update.
*
- * This method must be atomic. If one property cannot be changed, the
- * entire operation must fail.
+ * To update specific properties, call the 'handle' method on this object.
+ * Read the PropPatch documentation for more information.
*
- * If the operation was successful, true can be returned.
- * If the operation failed, false can be returned.
- *
- * Deletion of a non-existent property is always successful.
- *
- * Lastly, it is optional to return detailed information about any
- * failures. In this case an array should be returned with the following
- * structure:
- *
- * array(
- * 403 => array(
- * '{DAV:}displayname' => null,
- * ),
- * 424 => array(
- * '{DAV:}owner' => null,
- * )
- * )
- *
- * In this example it was forbidden to update {DAV:}displayname.
- * (403 Forbidden), which in turn also caused {DAV:}owner to fail
- * (424 Failed Dependency) because the request needs to be atomic.
- *
- * @param array $mutations
- * @return bool|array
+ * @param PropPatch $propPatch
+ * @return void
*/
- function updateProperties($mutations)
+ function propPatch(\Sabre\DAV\PropPatch $propPatch)
{
// not supported
return false;
}
/**
* Returns a list of properties for this node.
*
* The properties list is a list of propertynames the client requested,
* encoded in clark-notation {xmlnamespace}tagname
*
* If the array is empty, it means 'all properties' were requested.
*
* Note that it's fine to liberally give properties back, instead of
* conforming to the list of requested properties.
* The Server class will filter out the extra.
*
* @param array $properties
* @return void
*/
function getProperties($properties)
{
$result = array();
if ($this->data['created']) {
$result['{DAV:}creationdate'] = \Sabre\HTTP\Util::toHTTPDate(new DateTime('@'.$this->data['created']));
}
return $result;
}
}
diff --git a/lib/Kolab/DAVACL/PrincipalBackend.php b/lib/Kolab/DAVACL/PrincipalBackend.php
index 908aca3..4fd3630 100644
--- a/lib/Kolab/DAVACL/PrincipalBackend.php
+++ b/lib/Kolab/DAVACL/PrincipalBackend.php
@@ -1,255 +1,273 @@
<?php
/**
* SabreDAV Principals Backend implementation 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\DAVACL;
use \rcube;
+use \rcube_utils;
+use \libcalendaring;
+use \kolab_storage;
use Sabre\DAV\Exception;
use Sabre\DAV\URLUtil;
use Kolab\DAV\Auth\HTTPBasic;
/**
* Kolab Principal Backend
*/
-class PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface
+class PrincipalBackend extends \Sabre\DAVACL\PrincipalBackend\AbstractBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface
{
/**
* Sets up the backend.
*/
public function __construct()
{
}
/**
* Returns a pricipal record for the currently authenticated user
*/
public function getCurrentUser()
{
// console(__METHOD__, HTTPBasic::$current_user);
if (HTTPBasic::$current_user) {
$user_email = rcube::get_instance()->get_user_email();
+ $emails = libcalendaring::get_instance()->get_user_emails();
+
return array(
'uri' => 'principals/' . HTTPBasic::$current_user,
'{DAV:}displayname' => HTTPBasic::$current_user,
'{http://sabredav.org/ns}email-address' => $user_email,
- '{http://calendarserver.org/ns/}email-address-set' => $user_email,
+ '{DAV:}alternate-URI-set' => array_map(function($email) {
+ return 'mailto:' . $email;
+ }, $emails),
);
}
return false;
}
/**
* Returns a list of principals based on a prefix.
*
* This prefix will often contain something like 'principals'. You are only
* expected to return principals that are in this base path.
*
* You are expected to return at least a 'uri' for every user, you can
* return any additional properties if you wish so. Common properties are:
* {DAV:}displayname
* {http://sabredav.org/ns}email-address - This is a custom SabreDAV
* field that's actualy injected in a number of other properties. If
* you have an email address, use this property.
*
* @param string $prefixPath
* @return array
*/
public function getPrincipalsByPrefix($prefixPath)
{
console(__METHOD__, $prefixPath);
$principals = array();
if ($prefixPath == 'principals') {
// TODO: list users from LDAP
// we currently only advertise the authenticated user
if ($user = $this->getCurrentUser()) {
$principals[] = $user;
}
}
return $principals;
}
/**
* Returns a specific principal, specified by it's path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
* @param string $path
* @return array
*/
public function getPrincipalByPath($path)
{
// console(__METHOD__, $path);
list($prefix,$name) = explode('/', $path);
if ($prefix == 'principals' && $name == HTTPBasic::$current_user) {
return $this->getCurrentUser();
}
- else if ($prefix == 'principals' && \rcube_utils::check_email($name, false)) {
- // TODO: do a user lookup in LDAP
+ else if ($prefix == 'principals' && rcube_utils::check_email($name, false)) {
list($localname,$domain) = explode('@', $name);
+ $displayname = ucfirst(str_replace('.', ' ', $localname));
+
+ // Do a user lookup in LDAP
+ foreach (kolab_storage::search_users($name, 1, array('email'), 1) as $user) {
+ $displayname = $user['displayname'];
+ }
+
return array(
'uri' => $path,
- '{DAV:}displayname' => $localname,
+ '{DAV:}displayname' => $displayname,
'{http://sabredav.org/ns}email-address' => $name,
- '{http://calendarserver.org/ns/}email-address-set' => $name,
);
}
return null;
}
/**
* Returns the list of members for a group-principal
*
* @param string $principal
* @return array
*/
public function getGroupMemberSet($principal)
{
// TODO: for now the group principal has only one member, the user itself
list($prefix, $name) = URLUtil::splitPath($principal);
- $principal = $this->getPrincipalByPath($prefix);
+ $principal = $this->getPrincipalByPath($principal);
if (!$principal) throw new Exception('Principal not found');
return array(
$prefix
);
}
/**
* Returns the list of groups a principal is a member of
*
* @param string $principal
* @return array
*/
public function getGroupMembership($principal)
{
list($prefix,$name) = URLUtil::splitPath($principal);
$group_membership = array();
if ($prefix == 'principals') {
$principal = $this->getPrincipalByPath($principal);
if (!$principal) throw new Exception('Principal not found');
// TODO: implement full calendar delegation (with information from LDAP kolabDelegate)
return array(
// Calendar delegation is not supported by our backend
//'principals/'.$name.'/calendar-proxy-read',
//'principals/'.$name.'/calendar-proxy-write',
// The addressbook groups are not supported in Sabre,
// see http://groups.google.com/group/sabredav-discuss/browse_thread/thread/ef2fa9759d55f8c#msg_5720afc11602e753
//'principals/'.$name.'/addressbook-proxy-read',
//'principals/'.$name.'/addressbook-proxy-write',
);
}
return $group_membership;
}
/**
* Updates the list of group members for a group principal.
*
* The principals should be passed as a list of uri's.
*
* @param string $principal
* @param array $members
* @return void
*/
public function setGroupMemberSet($principal, array $members)
{
throw new Exception('Setting members of the group is not supported yet');
}
- function updatePrincipal($path, $mutations)
+ function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch)
{
return 0;
}
/**
* This method is used to search for principals matching a set of
* properties.
*
* This search is specifically used by RFC3744's principal-property-search
* REPORT. You should at least allow searching on
* http://sabredav.org/ns}email-address.
*
* The actual search should be a unicode-non-case-sensitive search. The
* keys in searchProperties are the WebDAV property names, while the values
* are the property values to search on.
*
* If multiple properties are being searched on, the search should be
* AND'ed.
*
* This method should simply return an array with full principal uri's.
*
* If somebody attempted to search on a property the backend does not
* support, you should simply return 0 results.
*
* @param string $prefixPath
* @param array $searchProperties
+ * @param string $test
* @return array
*/
- function searchPrincipals($prefixPath, array $searchProperties)
+ function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
{
console(__METHOD__, $prefixPath, $searchProperties);
- $email = null;
+ $email = null; $name = null;
$results = array();
$current_user = $this->getCurrentUser();
- foreach($searchProperties as $property => $value) {
+ foreach ($searchProperties as $property => $value) {
// check search property against the current user
if ($current_user[$property] == $value) {
$results[] = $current_user['uri'];
continue;
}
switch($property) {
case '{http://sabredav.org/ns}email-address':
$email = $value;
break;
case '{DAV:}displayname':
+ $name = $value;
+ break;
+
default :
// Unsupported property
return array();
}
}
- // we only support search by email
- if (!empty($email)) {
- // TODO: search via LDAP
+ // search users via LDAP
+ if (empty($results) && (!empty($email) || !empty($name))) {
+ foreach (kolab_storage::search_users($email ?: $name, 2, array('email'), 10) as $user) {
+ $results[] = 'principals/' . $user['email'];
+ }
}
return array_unique($results);
}
}
diff --git a/lib/Kolab/Utils/DAVBackend.php b/lib/Kolab/Utils/DAVBackend.php
index 615d6b8..a556a56 100644
--- a/lib/Kolab/Utils/DAVBackend.php
+++ b/lib/Kolab/Utils/DAVBackend.php
@@ -1,261 +1,291 @@
<?php
/**
* Utility class providing a simple API to PHP's APC cache
*
* @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\Utils;
use \rcube;
use \kolab_storage;
use \rcube_utils;
use \rcube_charset;
use Sabre\DAV;
/**
*
*/
class DAVBackend
{
const IMAP_UID_KEY = '/shared/vendor/kolab/uniqueid';
const IMAP_UID_KEY_PRIVATE = '/private/vendor/kolab/uniqueid';
const IMAP_UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
public static $caldav_type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
/**
* Getter for a kolab_storage_folder with the given UID
*
* @param string Folder UID (saved in annotation)
* @param string Kolab folder type (for selecting candidates)
* @return object \kolab_storage_folder instance
*/
public static function get_storage_folder($uid, $type)
{
foreach (kolab_storage::get_folders($type, false) as $folder) {
if ($folder->get_uid() == $uid) {
self::check_storage_folder($folder);
return $folder;
}
}
self::check_storage_folder(null);
}
/**
* Check the given storage folder instance for validity and throw
* the right exceptions according to the error state.
*/
public static function check_storage_folder($folder)
{
if ($folder == null) {
throw new DAV\Exception\NotFound('The requested collection was not found');
}
if (!$folder->valid || $folder->get_error()) {
$error = $folder->get_error();
if ($error === kolab_storage::ERROR_IMAP_CONN) {
throw new DAV\Exception\ServiceUnavailable('The service is temporarily unavailable (Storage failure)');
}
else if ($error === kolab_storage::ERROR_CACHE_DB) {
throw new DAV\Exception\ServiceUnavailable('The service is temporarily unavailable (Cache failure)');
}
else if ($error === kolab_storage::ERROR_NO_PERMISSION) {
throw new DAV\Exception\Forbidden('Access to this collection is not permitted');
}
else if ($error === kolab_storage::ERROR_INVALID_FOLDER) {
throw new DAV\Exception\NotFound('The requested collection was not found');
}
throw new DAV\Exception('Internal Server Error');
}
}
/**
* Build an absolute URL with the given parameters
*/
public static function abs_url($parts = array())
{
$schema = 'http';
$default_port = 80;
if (rcube_utils::https_check()) {
$schema = 'https';
$default_port = 443;
}
$url = $schema . '://' . $_SERVER['HTTP_HOST'];
if ($_SERVER['SERVER_PORT'] != $default_port)
$url .= ':' . $_SERVER['SERVER_PORT'];
if (dirname($_SERVER['SCRIPT_NAME']) != '/')
$url .= dirname($_SERVER['SCRIPT_NAME']);
$url .= '/' . join('/', array_map('urlencode', $parts));
return $url;
}
+ /**
+ * Set callback handler for property changes on the given folder
+ *
+ * @param object $folder kolab_storage_folder instance to operate on
+ * @param oject $propPatch PropPatch instance with the property updates
+ */
+ public static function handle_propatch($folder, \Sabre\DAV\PropPatch $propPatch)
+ {
+ $propPatch->handle(
+ array('{DAV:}displayname','{http://apple.com/ns/ical/}calendar-color'),
+ function($mutations) use ($folder) {
+ $result = DAVBackend::folder_update($folder, $mutations);
+ if (is_array($result)) {
+ $ret = array();
+ foreach ($result as $code => $props) {
+ foreach (array_keys($props) as $prop) {
+ $ret[$prop] = $code;
+ }
+ }
+ }
+ else {
+ $ret = $result;
+ }
+ return $ret;
+ });
+
+ // silently accept the other/non-supported properties
+ $propPatch->setRemainingResultCode(204);
+ }
+
/**
* Updates properties for a recourse (kolab folder)
*
* The mutations array uses the propertyName in clark-notation as key,
* and the array value for the property value. In the case a property
* should be deleted, the property value will be null.
*
* This method must be atomic. If one property cannot be changed, the
* entire operation must fail.
*
* If the operation was successful, true is returned.
* If the operation failed, detailed information about any
* failures is returned.
*
* @param object $folder kolab_storage_folder instance to operate on
- * @param array $mutations Hash array with propeties to change
- * @return bool|array
+ * @param object $mutations Hash array with propeties to change
+ * @return void
*/
public static function folder_update($folder, array $mutations)
{
$errors = array();
$updates = array();
foreach ($mutations as $prop => $val) {
switch ($prop) {
case '{DAV:}displayname':
// abort if name didn't change
if ($val == html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET)) {
break;
}
// restrict renaming to personal folders only
if ($folder->get_namespace() == 'personal') {
$parts = preg_split('!(\s*/\s*|\s+[»:]\s+)!', $val);
$updates['oldname'] = $folder->name;
$updates['name'] = array_pop($parts);
$updates['parent'] = join('/', $parts);
}
else {
$updates['displayname'] = $val;
}
break;
case '{http://apple.com/ns/ical/}calendar-color':
$newcolor = substr(trim($val, '#'), 0, 6);
if (strcasecmp($newcolor, $folder->get_color())) {
$updates['color'] = $newcolor;
}
break;
case '{urn:ietf:params:xml:ns:caldav}calendar-description':
default:
// unsupported property
$errors[403][$prop] = null;
}
}
// execute folder update
if (!empty($updates)) {
// 'name' and 'parent' properties are always required
if (empty($updates['name'])) {
$parts = explode('/', $folder->name);
$updates['name'] = rcube_charset::convert(array_pop($parts), 'UTF7-IMAP');
$updates['parent'] = join('/', $parts);
$updates['oldname'] = $folder->name;
}
if (!kolab_storage::folder_update($updates)) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error updating properties for folder $folder->name:" . kolab_storage::$last_error),
true, false);
return false;
}
}
return empty($errors) ? true : $errors;
}
/**
* Creates a new resource (i.e. IMAP folder) of a given type
*
* If the creation was a success, an id must be returned that can be used to reference
* this resource in other methods.
*
* @param array $properties
* @param string $type
* @param string $uid
* @return false|string
*/
public function folder_create($type, array $properties, $uid)
{
$props = array(
'type' => $type,
'name' => '',
'subscribed' => true,
);
foreach ($properties as $prop => $val) {
switch ($prop) {
case '{DAV:}displayname':
$parts = explode('/', $val);
$props['name'] = array_pop($parts);
$props['parent'] = join('/', $parts);
break;
case '{http://apple.com/ns/ical/}calendar-color':
$props['color'] = substr(trim($val, '#'), 0, 6);
break;
case '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set':
$type_map = array_flip(self::$caldav_type_component_map);
$comp_types = $val->getValue();
$comp_type = $comp_types[0];
if (!empty($type_map[$comp_type]))
$props['type'] = $type = $type_map[$comp_type];
break;
case '{urn:ietf:params:xml:ns:caldav}calendar-description':
default:
// unsupported property
}
}
// use UID as name if it doesn't seem to be a real UID
// TODO: append number to default "Untitled" folder name if one already exists
if (empty($props['name'])) {
$props['name'] = strlen($uid) < 16 ? $uid : 'Untitled';
}
if (!($fname = kolab_storage::folder_update($props))) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating a new $type folder '$props[name]':" . kolab_storage::$last_error),
true, false);
return false;
}
// save UID in folder annotations
if ($folder = kolab_storage::get_folder($fname)) {
$folder->set_uid($uid);
}
return $uid;
}
}
diff --git a/lib/Kolab/Utils/DAVLogger.php b/lib/Kolab/Utils/DAVLogger.php
index 4cbbcb2..b35641c 100644
--- a/lib/Kolab/Utils/DAVLogger.php
+++ b/lib/Kolab/Utils/DAVLogger.php
@@ -1,221 +1,220 @@
<?php
/**
* Utility class logging DAV requests
*
* @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\Utils;
use \rcube;
use Sabre\DAV;
use Kolab\DAV\Auth\HTTPBasic;
/**
* Utility class to log debug information about processed DAV requests
*/
class DAVLogger extends DAV\ServerPlugin
{
const CONSOLE = 1;
const HTTP_REQUEST = 2;
const HTTP_RESPONSE = 4;
private $rcube;
private $server;
private $method;
private $loglevel;
/**
* Default constructor
*/
public function __construct($level = 1)
{
$this->rcube = rcube::get_instance();
$this->loglevel = $level;
}
/**
* This initializes the plugin.
* This method should set up the required event subscriptions.
*
* @param Server $server
*/
public function initialize(DAV\Server $server)
{
$this->server = $server;
- $server->subscribeEvent('beforeMethod', array($this, '_beforeMethod'), 15);
- $server->subscribeEvent('exception', array($this, '_exception'));
- $server->subscribeEvent('exit', array($this, '_exit'));
+ $server->on('beforeMethod', array($this, '_beforeMethod'), 15);
+ $server->on('exception', array($this, '_exception'));
+ $server->on('exit', array($this, '_exit'));
// replace $server->httpResponse with a derived class that can do logging
$server->httpResponse = new HTTPResponse();
}
/**
* Handler for 'beforeMethod' events
*/
- public function _beforeMethod($method, $uri)
+ public function _beforeMethod($request, $response)
{
- $this->method = $method;
+ $this->method = $request->getMethod();
// turn on per-user http logging if the destination file exists
if ($this->loglevel < 2 && $this->rcube->config->get('kolabdav_user_debug', false)
&& ($log_dir = $this->user_log_dir()) && file_exists($log_dir . '/httpraw')) {
$this->loglevel |= (self::HTTP_REQUEST | self::HTTP_RESPONSE);
}
// log full HTTP request data
if ($this->loglevel & self::HTTP_REQUEST) {
- $request = $this->server->httpRequest;
- $content_type = $request->getHeader('CONTENT_TYPE');
+ $content_type = $request->getHeader('Content-Type');
if (strpos($content_type, 'text/') === 0 || strpos($content_type, 'application/xml') === 0) {
- $http_body = $request->getBody(true);
+ $http_body = $request->getBodyAsString();
// Hack for reading php:://input because that stream can only be read once.
// This is why we re-populate the request body with the existing data.
$request->setBody($http_body);
}
else if (!empty($content_type)) {
$http_body = '[binary data]';
}
// catch all headers
$http_headers = array();
foreach ($this->get_request_headers() as $hdr => $value) {
if (strtolower($hdr) == 'authorization') {
$method = preg_match('/^((basic|digest)\s+)/i', $value, $m) ? $m[1] : '';
$value = $method . str_repeat('*', strlen($value) - strlen($method));
}
$http_headers[$hdr] = "$hdr: $value";
}
- $this->write_log('httpraw', $request->getMethod() . ' ' . $request->getUri() . ' ' . $_SERVER['SERVER_PROTOCOL'] . "\n" .
+ $this->write_log('httpraw', $request->getMethod() . ' ' . $request->getUrl() . ' ' . $_SERVER['SERVER_PROTOCOL'] . "\n" .
join("\n", $http_headers) . "\n\n" . $http_body);
}
// log to console
if ($this->loglevel & self::CONSOLE) {
- $this->write_log('console', $method . ' ' . $uri);
+ $this->write_log('console', $this->method . ' ' . $request->getUrl());
}
}
/*
* Wrapper function in case apache_request_headers() is not available
*
* @return array
*/
public function get_request_headers()
{
if (function_exists('apache_request_headers')) {
return apache_request_headers();
}
$return = array();
foreach ($_SERVER as $key => $value) {
if (preg_match('/^HTTP_(.*)/',$key,$regs)) {
// restore original letter case
$key = str_replace('_',' ',$regs[1]);
$key = ucwords(strtolower($key));
$key = str_replace(' ','-',$key);
// build return array
$return[$key] = $value;
}
}
return $return;
}
/**
* Handler for 'exception' events
*/
public function _exception($e)
{
// log to console
$this->console(get_class($e) . ' (EXCEPTION)', $e->getMessage() /*, $e->getTraceAsString()*/);
}
/**
* Handler for 'exit' events
*/
public function _exit()
{
if ($this->loglevel & self::CONSOLE) {
$time = microtime(true) - KOLAB_DAV_START;
if (function_exists('memory_get_usage'))
$mem = round(memory_get_usage() / 1024 / 1024, 1) . 'MB';
if (function_exists('memory_get_peak_usage'))
$mem .= '/' . round(memory_get_peak_usage() / 1024 / 1024, 1) . 'MB';
$this->write_log('console', sprintf("/%s: %0.4f sec; %s", $this->method, $time, $mem));
}
// log full HTTP reponse
if ($this->loglevel & self::HTTP_RESPONSE) {
$this->write_log('httpraw', "RESPONSE: " . $this->server->httpResponse->dump());
}
}
/**
* Wrapper for rcube::cosole() to write per-user logs
*/
public function console(/* ... */)
{
if ($this->loglevel & self::CONSOLE) {
$msg = array();
foreach (func_get_args() as $arg) {
$msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
}
$this->write_log('console', join(";\n", $msg));
}
}
/**
* Wrapper for rcube::write_log() that can write per-user logs
*/
public function write_log($filename, $msg)
{
// dump data per user
if ($this->rcube->config->get('kolabdav_user_debug', false)) {
if ($this->user_log_dir()) {
$filename = HTTPBasic::$current_user . '/' . $filename;
}
else {
return; // don't log
}
}
rcube::write_log($filename, $msg);
}
/**
* Get the per-user log directory
*/
private function user_log_dir()
{
$log_dir = $this->rcube->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
$user_log_dir = $log_dir . '/' . HTTPBasic::$current_user;
return HTTPBasic::$current_user && is_writable($user_log_dir) ? $user_log_dir : false;
}
}
\ No newline at end of file
diff --git a/lib/Kolab/Utils/HTTPResponse.php b/lib/Kolab/Utils/HTTPResponse.php
index 4f4e72d..0c98576 100644
--- a/lib/Kolab/Utils/HTTPResponse.php
+++ b/lib/Kolab/Utils/HTTPResponse.php
@@ -1,92 +1,51 @@
<?php
/**
* Utility class representing a HTTP response with logging capabilities
*
* @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\Utils;
/**
* This class represents a HTTP response.
*/
class HTTPResponse extends \Sabre\HTTP\Response
{
- private $status;
- private $body = '';
- private $headers = array();
-
- /**
- * Sends an HTTP status header to the client.
- *
- * @param int $code HTTP status code
- * @return bool
- */
- public function sendStatus($code)
- {
- $this->status = $this->getStatusMessage($code, $this->defaultHttpVersion);
- return parent::sendStatus($code);
- }
-
- /**
- * Sets an HTTP header for the response
- *
- * @param string $name
- * @param string $value
- * @param bool $replace
- * @return bool
- */
- public function setHeader($name, $value, $replace = true) {
- $this->headers[$name] = $value;
- return parent::setHeader($name, $value, $replace);
- }
-
- /**
- * Sends the entire response body
- *
- * This method can accept either an open filestream, or a string.
- *
- * @param mixed $body
- * @return void
- */
- public function sendBody($body)
- {
- if (is_resource($body)) {
- fpassthru($body);
- $this->body = '[binary data]';
- }
- else {
- echo $body;
- $this->body .= $body;
- }
- }
-
/**
* Dump the response data for logging
*/
public function dump()
{
$result_headers = '';
foreach ($this->headers as $hdr => $value) {
- $result_headers .= "\n$hdr: " . $value;
+ $result_headers .= "\n$value[0]: " . $this->getHeader($hdr);
+ }
+
+ $body = $this->body;
+
+ // get response body as string for text/* data
+ if (is_resource($this->body) && strpos($this->getHeader('content-type'), 'text/') === 0) {
+ @fseek($this->body, 0);
+ $body = stream_get_contents($this->body);
}
- return $this->status . $result_headers . "\n\n" . $this->body;
+ return $this->status . " " . $this->statusText . $result_headers . "\n\n" . $body;
}
}
\ No newline at end of file
diff --git a/lib/Kolab/Utils/VObjectUtils.php b/lib/Kolab/Utils/VObjectUtils.php
index 82f934b..ad88be9 100644
--- a/lib/Kolab/Utils/VObjectUtils.php
+++ b/lib/Kolab/Utils/VObjectUtils.php
@@ -1,84 +1,98 @@
<?php
/**
* Utility class providing functions for VObject data encoding
*
* @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\Utils;
use Sabre\VObject\Property;
/**
* Helper class proviting utility functions for VObject data encoding
*/
class VObjectUtils
{
/**
* Convert an object URI into a valid UID value
*/
public static function uri2uid($uri, $suffix = '')
{
$base = basename($uri, $suffix);
$uid = strtr($base, array('%2F' => '/'));
// assume full URL encoding
if (preg_match('/%[A-F0-9]{2}/', $uid)) {
return urldecode($base);
}
return $uid;
}
/**
* Encode an object UID into a valid URI
*/
public static function uid2uri($uid, $suffix = '')
{
$encode = strpos($uid, '/') !== false;
return ($encode ? urlencode($uid) : $uid) . $suffix;
}
/**
* 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)
+ public static function datetime_prop($root, $name, $dt, $utc = false)
{
- $vdt = new Property\DateTime($name);
- $vdt->setDateTime($dt, $dt->_dateonly ? Property\DateTime::DATE : ($utc ? Property\DateTime::UTC : Property\DateTime::LOCALTZ));
+ if ($utc) {
+ $dt->setTimeZone(new \DateTimeZone('UTC'));
+ }
+
+ $vdt = $root->createProperty($name, null, null, $dt->_dateonly ? 'DATE' : 'DATE-TIME');
+ $value = $dt;
+
+ if ($dt->_dateonly) {
+ // $vdt['VALUE'] = 'DATE';
+ // set date value as string as a temporary fix for
+ // https://github.com/fruux/sabre-vobject/issues/217
+ $value = $dt->format('Y-m-d');
+ }
+
+ $vdt->setValue($value);
+
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
diff --git a/public_html/index.php b/public_html/index.php
index cfebb82..2919763 100644
--- a/public_html/index.php
+++ b/public_html/index.php
@@ -1,197 +1,197 @@
<?php
/**
* iRony, the Kolab WebDAV/CalDAV/CardDAV Server
*
* This is the public API to provide *DAV-based access to the Kolab Groupware backend
*
- * @version 0.3-dev
+ * @version 0.4-dev
* @author Thomas Bruederli <bruederli@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/>.
*/
// define some environment variables used throughout the app and libraries
define('KOLAB_DAV_ROOT', realpath('../'));
-define('KOLAB_DAV_VERSION', '0.3.0');
+define('KOLAB_DAV_VERSION', '0.4-dev');
define('KOLAB_DAV_START', microtime(true));
define('RCUBE_INSTALL_PATH', KOLAB_DAV_ROOT . '/');
define('RCUBE_CONFIG_DIR', KOLAB_DAV_ROOT . '/config/');
define('RCUBE_PLUGINS_DIR', KOLAB_DAV_ROOT . '/lib/plugins/');
// suppress error notices
ini_set('error_reporting', E_ALL &~ E_NOTICE &~ E_STRICT);
/**
* Mapping PHP errors to exceptions.
*
* While this is not strictly needed, it makes a lot of sense to do so. If an
* E_NOTICE or anything appears in your code, this allows SabreDAV to intercept
* the issue and send a proper response back to the client (HTTP/1.1 500).
*/
function exception_error_handler($errno, $errstr, $errfile, $errline ) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
//set_error_handler("exception_error_handler");
// use composer's autoloader for dependencies
$loader = require_once(KOLAB_DAV_ROOT . '/vendor/autoload.php');
$loader->set('Kolab', array(KOLAB_DAV_ROOT . '/lib')); // register iRony namespace(s)
$loader->setUseIncludePath(true); // enable include_path to load PEAR classes from their default location
// load the Roundcube framework with its autoloader
require_once KOLAB_DAV_ROOT . '/lib/Roundcube/bootstrap.php';
// Roundcube framework initialization
$rcube = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
$rcube->config->load_from_file(RCUBE_CONFIG_DIR . 'dav.inc.php');
// Load plugins
$plugins = (array)$rcube->config->get('kolabdav_plugins', array('kolab_auth'));
$required = array('libkolab', 'libcalendaring');
$rcube->plugins->init($rcube);
$rcube->plugins->load_plugins($plugins, $required);
// enable logger
if ($rcube->config->get('kolabdav_console') || $rcube->config->get('kolabdav_user_debug')) {
$logger = new \Kolab\Utils\DAVLogger((\Kolab\Utils\DAVLogger::CONSOLE | $rcube->config->get('kolabdav_http_log', 0)));
}
// convenience function, you know it well :-)
function console()
{
global $logger;
if ($logger) {
call_user_func_array(array($logger, 'console'), func_get_args());
}
}
// Make sure this setting is turned on and reflects the root url of the *DAV server.
$base_uri = $rcube->config->get('base_uri', slashify(substr(dirname($_SERVER['SCRIPT_FILENAME']), strlen($_SERVER['DOCUMENT_ROOT']))));
// add filename to base URI when called without mod_rewrite (e.g. /dav/index.php/calendar)
if (strpos($_SERVER['REQUEST_URI'], 'index.php'))
$base_uri .= 'index.php/';
// create the various backend instances
$auth_backend = new \Kolab\DAV\Auth\HTTPBasic();
$principal_backend = new \Kolab\DAVACL\PrincipalBackend();
$services = array();
foreach (array('CALDAV','CARDDAV','WEBDAV') as $skey) {
if (getenv($skey))
$services[$skey] = 1;
}
// no config means *all* services
if (empty($services))
$services = array('CALDAV' => 1, 'CARDDAV' => 1, 'WEBDAV' => 1);
// add chwala directories to include path for autoloading
if ($services['WEBDAV']) {
$include_path = ini_get('include_path') . PATH_SEPARATOR;
$include_path .= KOLAB_DAV_ROOT . '/lib/FileAPI' . PATH_SEPARATOR;
$include_path .= KOLAB_DAV_ROOT . '/lib/FileAPI/kolab' . PATH_SEPARATOR;
$include_path .= KOLAB_DAV_ROOT . '/lib/FileAPI/ext';
set_include_path($include_path);
}
// Build the directory tree
// This is an array which contains the 'top-level' directories in the WebDAV server.
if ($services['CALDAV'] || $services['CARDDAV']) {
$nodes = array(
new \Kolab\CalDAV\Principal\Collection($principal_backend),
);
if ($services['CALDAV']) {
$caldav_backend = new \Kolab\CalDAV\CalendarBackend();
$caldav_backend->setUserAgent($_SERVER['HTTP_USER_AGENT']);
$nodes[] = new \Kolab\CalDAV\CalendarRootNode($principal_backend, $caldav_backend);
}
if ($services['CARDDAV']) {
$carddav_backend = new \Kolab\CardDAV\ContactsBackend();
$carddav_backend->setUserAgent($_SERVER['HTTP_USER_AGENT']);
$nodes[] = new \Kolab\CardDAV\AddressBookRoot($principal_backend, $carddav_backend);
}
if ($services['WEBDAV']) {
$nodes[] = new \Kolab\DAV\Collection(\Kolab\DAV\Collection::ROOT_DIRECTORY);
}
}
// register WebDAV service as root
else if ($services['WEBDAV']) {
$nodes = new \Kolab\DAV\Collection('');
}
// the object tree needs in turn to be passed to the server class
$server = new \Sabre\DAV\Server($nodes);
$server->setBaseUri($base_uri);
// connect logger
if (is_object($logger)) {
$server->addPlugin($logger);
}
// register some plugins
$server->addPlugin(new \Sabre\DAV\Auth\Plugin($auth_backend, 'KolabDAV'));
$server->addPlugin(new \Sabre\DAVACL\Plugin());
if ($services['CALDAV']) {
- $caldav_plugin = new \Kolab\CalDAV\Plugin();
- $caldav_plugin->setIMipHandler(new \Kolab\CalDAV\IMip());
- $server->addPlugin($caldav_plugin);
+ $server->addPlugin(new \Kolab\CalDAV\Plugin());
+ $server->addPlugin(new \Kolab\CalDAV\SchedulePlugin());
+ $server->addPlugin(new \Kolab\CalDAV\IMipPlugin(''));
}
if ($services['CARDDAV']) {
$server->addPlugin(new \Kolab\CardDAV\Plugin());
}
if ($services['WEBDAV']) {
// the lock manager is responsible for making sure users don't overwrite each others changes.
$locks_backend = new \Kolab\DAV\Locks\Chwala(\Kolab\DAV\Collection::ROOT_DIRECTORY);
$server->addPlugin(new \Sabre\DAV\Locks\Plugin($locks_backend));
// intercept some of the garbage files operation systems tend to generate when mounting a WebDAV share
$server->addPlugin(new \Kolab\DAV\TempFilesPlugin(KOLAB_DAV_ROOT . '/temp'));
}
// HTML UI for browser-based access (recommended only for development)
if (getenv('DAVBROWSER')) {
$server->addPlugin(new \Sabre\DAV\Browser\Plugin());
}
// log exceptions in iRony error log
-$server->subscribeEvent('exception', function($e){
+$server->on('exception', function($e){
if (!($e instanceof \Sabre\DAV\Exception) || $e->getHTTPCode() == 500) {
rcube::raise_error(array(
'code' => 500,
'type' => 'php',
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage() . " (error 500)\n" . $e->getTraceAsString(),
), true, false);
}
});
// finally, process the request
$server->exec();
// trigger log
-$server->broadcastEvent('exit', array());
+$server->emit('exit', array());
diff --git a/test/Resource/CalDAV/scheduleprops/1.xml b/test/Resource/CalDAV/scheduleprops/1.xml
new file mode 100644
index 0000000..64d8c08
--- /dev/null
+++ b/test/Resource/CalDAV/scheduleprops/1.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+<D:prop>
+<C:schedule-inbox-URL/>
+<C:schedule-outbox-URL/>
+<C:calendar-user-address-set/>
+</D:prop>
+</D:propfind>
diff --git a/test/Resource/CardDAV/vcardput/1.vcf b/test/Resource/CardDAV/vcardput/1.vcf
index 0e578ff..5353d8e 100644
--- a/test/Resource/CardDAV/vcardput/1.vcf
+++ b/test/Resource/CardDAV/vcardput/1.vcf
@@ -1,10 +1,10 @@
BEGIN:VCARD
VERSION:3.0
N:Thompson;Default;;;
FN:Default Thompson
-EMAIL;TYPE=INTERNET;TYPE=WORK:lthompson@example.com
+EMAIL;TYPE=INTERNET,WORK:lthompson@example.com
TEL;TYPE=WORK:1-555-555-5555
TEL;TYPE=CELL:1-444-444-4444
ADR;TYPE=WORK:;;1245 Test;Sesame Street;California;11111;USA
UID:1
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/10.vcf b/test/Resource/CardDAV/vcardput/10.vcf
index 161c2d7..bb1e5b9 100644
--- a/test/Resource/CardDAV/vcardput/10.vcf
+++ b/test/Resource/CardDAV/vcardput/10.vcf
@@ -1,25 +1,25 @@
BEGIN:VCARD
VERSION:3.0
N:Here;Custom;Fields;All;
FN:All Custom Fields Here
NICKNAME:custome
ORG:Major League Co.;Macosx server group
TITLE:QA Engineer
-EMAIL;type=INTERNET;type=WORK;type=pref:custom@example.com
+EMAIL;type=INTERNET,WORK;type=pref:custom@example.com
TEL;type=WORK;type=pref:777-777-7777
TEL;type=CELL:8888888888
item1.ADR;type=WORK;type=pref:;;1 Goroku St.;Mountain Top;CA;99999;USA
item1.X-ABADR:us
NOTE: Many customer fields are added
item2.URL;type=pref:http://www.example.com/~magic
item2.X-ABLabel:_$!<HomePage>!$_
BDAY;value=date:1999-03-18
X-AIM;type=WORK;type=pref:custom@example.com
item3.X-ABDATE;type=pref:1995-05-21
item3.X-ABLabel:_$!<Anniversary>!$_
item4.X-ABRELATEDNAMES;type=pref:Aho Sak
item4.X-ABLabel:_$!<Friend>!$_
item5.X-ABRELATEDNAMES:Sanma
item5.X-ABLabel:_$!<Assistant>!$_
UID:AFBB77B8-0438-4825-A1DB-A75D76B6C3A8:ABPerson
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/11.vcf b/test/Resource/CardDAV/vcardput/11.vcf
index 1b55319..0a5896e 100644
--- a/test/Resource/CardDAV/vcardput/11.vcf
+++ b/test/Resource/CardDAV/vcardput/11.vcf
@@ -1,11 +1,11 @@
BEGIN:VCARD
VERSION:3.0
N:Thompson;Default;;;
FN:Default Thompson
-EMAIL;TYPE=INTERNET;TYPE=WORK:lthompson@example.com
+EMAIL;TYPE=INTERNET,WORK:lthompson@example.com
TEL;TYPE=WORK:1-555-555-5555
TEL;TYPE=CELL:1-444-444-4444
ADR;TYPE=WORK:;;1245\, Test;Sesame Street\; On
The Hill;California;11111;USA
UID:11
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/12.vcf b/test/Resource/CardDAV/vcardput/12.vcf
index 48abb4d..8538465 100644
--- a/test/Resource/CardDAV/vcardput/12.vcf
+++ b/test/Resource/CardDAV/vcardput/12.vcf
@@ -1,11 +1,11 @@
BEGIN:VCARD
VERSION:3.0
N:Thompson;Default;;;
FN:Default Thompson
-EMAIL;TYPE=INTERNET;TYPE=WORK:lthompson@example.com
+EMAIL;TYPE=INTERNET,WORK:lthompson@example.com
TEL;TYPE=WORK:1-555-555-5555
TEL;TYPE=CELL:1-444-444-4444
ADR;TYPE=WORK:;;1245\, Test;Sesame Street\nOn
The Hill;California;11111;USA
UID:12
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/13.vcf b/test/Resource/CardDAV/vcardput/13.vcf
index 02c8b82..6038ef6 100644
--- a/test/Resource/CardDAV/vcardput/13.vcf
+++ b/test/Resource/CardDAV/vcardput/13.vcf
@@ -1,13 +1,13 @@
BEGIN:VCARD
VERSION:3.0
N:Thompson;Default;;;
FN:Default Thompson
-EMAIL;TYPE=INTERNET;TYPE=WORK:lthompson@example.com
+EMAIL;TYPE=INTERNET,WORK:lthompson@example.com
TEL;TYPE=WORK:1-555-555-5555
TEL;TYPE=CELL:1-444-444-4444
ADR;TYPE=WORK:;;1245 Test;Sesame Street;California;11111;USA
X-APPLE-STRUCTURED-LOCATION;VALUE=URI:geo:123.123,123.123
X-Test:Just some text\, <- here.
X-Test:geo:123.123,123.123
UID:13
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/14.vcf b/test/Resource/CardDAV/vcardput/14.vcf
index e55f806..abdcec1 100644
--- a/test/Resource/CardDAV/vcardput/14.vcf
+++ b/test/Resource/CardDAV/vcardput/14.vcf
@@ -1,11 +1,11 @@
BEGIN:VCARD
VERSION:3.0
N:Thompson;Default;;;
FN:Default Thompson
-EMAIL;TYPE=INTERNET;TYPE=WORK:lthompson@example.com
+EMAIL;TYPE=INTERNET,WORK:lthompson@example.com
TEL;TYPE=WORK:1-555-555-5555
TEL;TYPE=CELL:1-444-444-4444
ADR;TYPE=WORK:;;1245 Test;Sesame Street;California;11111;USA
UID:14
X-Test;CN=Hello^'World^' want a ^?:test
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/14a.vcf b/test/Resource/CardDAV/vcardput/14a.vcf
index 8ba1746..b64ff66 100644
--- a/test/Resource/CardDAV/vcardput/14a.vcf
+++ b/test/Resource/CardDAV/vcardput/14a.vcf
@@ -1,11 +1,11 @@
BEGIN:VCARD
VERSION:3.0
N:Thompson;Default;;;
FN:Default Thompson
-EMAIL;TYPE=INTERNET;TYPE=WORK:lthompson@example.com
+EMAIL;TYPE=INTERNET,WORK:lthompson@example.com
TEL;TYPE=WORK:1-555-555-5555
TEL;TYPE=CELL:1-444-444-4444
ADR;TYPE=WORK:;;1245 Test;Sesame Street;California;11111;USA
UID:14
X-Test;CN=Hello^'World^' want a ^^?:test
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/18.vcf b/test/Resource/CardDAV/vcardput/18.vcf
index 2a130d1..ee96787 100644
--- a/test/Resource/CardDAV/vcardput/18.vcf
+++ b/test/Resource/CardDAV/vcardput/18.vcf
@@ -1,10 +1,10 @@
BEGIN:VCARD
VERSION:3.0
N:Thompson;Non-default;;;
FN:Non-default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:nlthompson@example.com
+EMAIL;type=INTERNET,WORK;type=pref:nlthompson@example.com
TEL;type=WORK;type=pref:1-555-555-5555
TEL;type=CELL:1-444-444-4444
ADR;type=WORK;type=pref:;;12456 Test;Sesame Street;California;11111;USA
UID:16
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/2.vcf b/test/Resource/CardDAV/vcardput/2.vcf
index 719d422..2c9ce4d 100644
--- a/test/Resource/CardDAV/vcardput/2.vcf
+++ b/test/Resource/CardDAV/vcardput/2.vcf
@@ -1,16 +1,16 @@
BEGIN:VCARD
VERSION:3.0
PRODID:-//Apple Inc.//AddressBook 6.1//EN
N:Contact;Mulberry;;;
FN:Mulberry Contact
NICKNAME:mulberry
ORG:Apple Inc.;
-EMAIL;TYPE=INTERNET;TYPE=WORK:mulberry@example.com
+EMAIL;TYPE=INTERNET,WORK:mulberry@example.com
TEL;TYPE=HOME:777-777-7777
TEL;TYPE=WORK:8888888888
TEL;TYPE=FAX:5555555555
ADR;TYPE=WORK:;;1234 Infinite Circle;Exampletino\, CA 99999;USA;;
NOTE:This is a contact created in Mulberry.
URL;TYPE=HOMEPAGE:http://www.example.com/~magic
UID:2
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/3.vcf b/test/Resource/CardDAV/vcardput/3.vcf
index e7c0055..4213c9c 100644
--- a/test/Resource/CardDAV/vcardput/3.vcf
+++ b/test/Resource/CardDAV/vcardput/3.vcf
@@ -1,11 +1,11 @@
BEGIN:VCARD
VERSION:3.0
N:Kawado;Saeko;;;
FN:Snow Leopard
ORG:Snow Leopard;
-EMAIL;TYPE=INTERNET;TYPE=WORK:snowleopard@example.com
+EMAIL;TYPE=INTERNET,WORK:snowleopard@example.com
TEL;TYPE=WORK:777-777-7777
ADR;TYPE=WORK:;;1 Fidel Ave. Suite 100;Mountain Top;CA;99999;USA
X-ABSHOWAS:COMPANY
UID:3
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/5.vcf b/test/Resource/CardDAV/vcardput/5.vcf
index bc5ccbc..c889541 100644
--- a/test/Resource/CardDAV/vcardput/5.vcf
+++ b/test/Resource/CardDAV/vcardput/5.vcf
@@ -1,18 +1,18 @@
BEGIN:VCARD
VERSION:3.0
N:InfoIn;All;;;
FN:All InfoIn
ORG:allinfo Company;
-EMAIL;TYPE=INTERNET;TYPE=WORK:allinfomationin@example.com
+EMAIL;TYPE=INTERNET,WORK:allinfomationin@example.com
TEL;TYPE=WORK:777-777-7777
TEL;TYPE=CELL:8888888888
ADR;TYPE=WORK:;;1 Gally Street Apt #2;Mountain Top;CA;99999;USA
X-YAHOO:saeko.where@example.com
X-YAHOO-ID:saeko.where@example.com
-item1.X-ABLabel:_$!<Assistant>!$_
-item2.X-ABLabel:_$!<Friend>!$_
+item1.X-ABLABEL:_$!<Assistant>!$_
+item2.X-ABLABEL:_$!<Friend>!$_
item1.X-ABRELATEDNAMES:Shane
item2.X-ABRELATEDNAMES:Mayumi Yan
NOTE: Address book server test contact that hsa note field filled in.
UID:5
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/6.vcf b/test/Resource/CardDAV/vcardput/6.vcf
index d0d70a1..394b0d5 100644
--- a/test/Resource/CardDAV/vcardput/6.vcf
+++ b/test/Resource/CardDAV/vcardput/6.vcf
@@ -1,992 +1,992 @@
BEGIN:VCARD
VERSION:3.0
N:Picture;With;;;
FN:With Picture
-EMAIL;TYPE=INTERNET;TYPE=WORK:withpicture@example.com
+EMAIL;TYPE=INTERNET,WORK:withpicture@example.com
TEL;TYPE=WORK:777-777-7777
TEL;TYPE=CELL:8888888888
ADR;TYPE=WORK:;;1234 Golly Street;Sunnyside;CA;99999;USA
PHOTO;BASE64:
/9j/4AAQSkZJRgABAQAAAQABAAD/7QA8UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAB8cAVoAAx
sl RxwCAAACAAIcAhkAC1Bob3RvIEJvb3RoAP/iG6hJQ0NfUFJPRklMRQABAQAAG5hhcHBsAgA
AAG1u dHJSR0IgWFlaIAfaAAEAEwAJADEABGFjc3BBUFBMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAD2 1gABAAAAANMtYXBwbFYcEOZVYuhIRg5LwLIi62wAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAA AAAAEXJYWVoAAAFQAAAAFGdYWVoAAAFkAAAAFGJYWVoAAAF4AAAAFHd0cHQAAA
GMAAAAFGNoYWQA AAGgAAAALHJUUkMAAAHMAAAIDGdUUkMAAAnYAAAIDGJUUkMAABHkAAAIDGF
hcmcAABnwAAAAIGFh Z2cAABoQAAAAIGFhYmcAABowAAAAIHZjZ3QAABpQAAAAMG5kaW4AABqA
AAAAOGRlc2MAABq4AAAA ZGRzY20AABscAAAALm1tb2QAABtMAAAAKGNwcnQAABt0AAAAJFhZW
iAAAAAAAAB7vQAAQXsAAAJL WFlaIAAAAAAAAFYqAACp0AAAFF9YWVogAAAAAAAAJO8AABS1AA
C8glhZWiAAAAAAAADz2AABAAAA ARYIc2YzMgAAAAAAAQu3AAAFlv//81cAAAcpAAD91///+7f
///2mAAAD2gAAwPZjdXJ2AAAAAAAA BAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUA
SgBPAFQAWQBeAGMAaABtAHIAdwB8AIEA hgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0
ADVANoA4ADlAOoA8AD1APsBAQEHAQwBEgEY AR4BJQErATEBOAE+AUUBSwFSAVkBYAFmAW0BdQ
F8AYMBigGSAZkBoQGoAbABuAHAAcgB0AHYAeAB 6QHxAfoCAgILAhQCHAIlAi4CNwJAAkoCUwJ
cAmYCcAJ5AoMCjQKXAqECqwK1Ar8CygLUAt8C6gL0 Av8DCgMVAyADKwM3A0IDTQNZA2UDcAN8
A4gDlAOgA6wDuQPFA9ID3gPrA/gEBAQRBB4ELAQ5BEYE VARhBG8EfASKBJgEpgS0BMIE0QTfB
O4E/AULBRoFKAU3BUcFVgVlBXQFhAWTBaMFswXDBdMF4wXz BgMGFAYkBjUGRQZWBmcGeAaJBp
oGqwa9Bs4G4AbyBwMHFQcnBzkHTAdeB3AHgweWB6gHuwfOB+EH 9AgICBsILwhCCFYIagh+CJI
Ipgi6CM4I4wj3CQwJIQk2CUsJYAl1CYoJoAm1CcsJ4An2CgwKIgo5 Ck8KZQp8CpIKqQrACtcK
7gsFCx0LNAtLC2MLewuTC6sLwwvbC/MMDAwkDD0MVgxuDIcMoQy6DNMM 7Q0GDSANOg1UDW4Ni
A2iDbwN1w3xDgwOJw5CDl0OeA6TDq8Oyg7mDwIPHg86D1YPcg+OD6sPyA/k EAEQHhA7EFgQdh
CTELEQzhDsEQoRKBFGEWQRgxGhEcAR3xH+Eh0SPBJbEnoSmhK5EtkS+RMZEzkT WRN6E5oTuxP
bE/wUHRQ+FF8UgRSiFMQU5RUHFSkVSxVtFZAVshXVFfcWGhY9FmAWgxanFsoW7hcS FzUXWRd9
F6IXxhfqGA8YNBhZGH0YoxjIGO0ZExk4GV4ZhBmqGdAZ9hodGkMaahqQGrca3hsGGy0b VBt8G
6MbyxvzHBscQxxsHJQcvRzmHQ4dNx1gHYodsx3dHgYeMB5aHoQerh7YHwMfLR9YH4Mfrh/Z IA
QgMCBbIIcgsyDeIQohNyFjIY8hvCHpIhUiQiJwIp0iyiL4IyUjUyOBI68j3SQMJDokaSSXJMYk
9SUkJVQlgyWzJeImEiZCJnImoybTJwMnNCdlJ5Ynxyf4KCooWyiNKL4o8CkiKVUphym5KewqHy
pS KoUquCrrKx4rUiuGK7or7iwiLFYsiiy/LPQtKS1eLZMtyC39LjMuaS6eLtQvCy9BL3cvri/
kMBsw UjCJMMEw+DEwMWcxnzHXMg8ySDKAMrgy8TMqM2MznDPVNA80SDSCNLw09jUwNWo1pTXf
Nho2VTaQ Nss3BjdCN343uTf1ODE4bTiqOOY5IzlgOZ052joXOlQ6kjrPOw07SzuJO8c8BjxEP
IM8wj0BPUA9 fz2/Pf4+Pj5+Pr4+/j8/P38/wEAAQEFAgkDEQQVBR0GIQcpCDEJOQpFC00MWQ1
hDm0PeRCFEZUSo ROxFMEV0RbhF/EZARoVGykcOR1NHmUfeSCNIaUivSPVJO0mBScdKDkpVSpt
K4ksqS3FLuEwATEhM kEzYTSBNaE2xTfpOQk6MTtVPHk9nT7FP+1BFUI9Q2VEkUW5RuVIEUk9S
mlLlUzFTfFPIVBRUYFSt VPlVRlWSVd9WLFZ6VsdXFFdiV7BX/lhMWJpY6Vk4WYZZ1VokWnRaw
1sTW2NbslwDXFNco1z0XURd lV3mXjdeiV7aXyxffl/QYCJgdGDHYRlhbGG/YhJiZWK5YwxjYG
O0ZAhkXGSxZQVlWmWvZgRmWWav ZwRnWmewaAZoXGiyaQlpX2m2ag1qZGq8axNra2vDbBtsc2z
LbSNtfG3Vbi5uh27gbzpvk2/tcEdw oXD7cVZxsHILcmZywXMcc3hz03QvdIt053VDdaB1/HZZ
drZ3E3dwd854K3iJeOd5RXmjegJ6YHq/ ex57fXvcfDx8m3z7fVt9u34bfnx+3H89f55//4Bgg
MKBI4GFgeeCSYKrgw6DcIPThDaEmYT8hWCF w4YnhouG74dUh7iIHYiBiOaJTImxihaKfIrii0
iLrowUjHuM4o1Ija+OF45+juWPTY+1kB2QhZDu kVaRv5IokpGS+pNkk82UN5ShlQuVdZXglkq
WtZcgl4uX95himM6ZOpmmmhKafprrm1ebxJwxnJ+d DJ15neeeVZ7DnzGfoKAPoH2g7KFbocui
OqKqoxqjiqP6pGqk26VMpbymLqafpxCngqf0qGWo2KlK qbyqL6qiqxWriKv7rG+s461WrcuuP
66zryivnbARsIew/LFxseeyXbLTs0mzv7Q2tK21JLWbthK2 ibcBt3m38bhpuOG5WrnSuku6xL
s+u7e8MLyqvSS9nr4ZvpO/Dr+JwATAf8D6wXbB8cJtwunDZsPi xF/E3MVZxdbGU8bRx07HzMh
KyMnJR8nGykXKxMtDy8LMQszBzUHNwc5CzsLPQ8/D0ETQxtFH0cjS StLM007T0NRT1NbVWNXb
1l7W4tdl1+nYbdjx2XXZ+tp/2wPbiNwO3JPdGd2e3iTeqt8x37fgPuDF 4Uzh0+Ja4uLjauPy5
HrlAuWL5hPmnOcl56/oOOjC6Uzp1upg6urrdev/7IrtFu2h7izuuO9E79Dw XPDp8XXyAvKP8x
zzqvQ39MX1U/Xh9m/2/veM+Bv4qvk5+cn6Wfro+3j8CPyZ/Sn9uv5L/tz/bmN1 cnYAAAAAAAA
EAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0A cgB3AHwA
gQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2gDgAOUA6gDwAPUA+wEB AQcBD
AESARgBHgElASsBMQE4AT4BRQFLAVIBWQFgAWYBbQF1AXwBgwGKAZIBmQGhAagBsAG4AcAB yA
HQAdgB4AHpAfEB+gICAgsCFAIcAiUCLgI3AkACSgJTAlwCZgJwAnkCgwKNApcCoQKrArUCvwLK
AtQC3wLqAvQC/wMKAxUDIAMrAzcDQgNNA1kDZQNwA3wDiAOUA6ADrAO5A8UD0gPeA+sD+AQEBB
EE HgQsBDkERgRUBGEEbwR8BIoEmASmBLQEwgTRBN8E7gT8BQsFGgUoBTcFRwVWBWUFdAWEBZM
FowWz BcMF0wXjBfMGAwYUBiQGNQZFBlYGZwZ4BokGmgarBr0GzgbgBvIHAwcVBycHOQdMB14H
cAeDB5YH qAe7B84H4Qf0CAgIGwgvCEIIVghqCH4IkgimCLoIzgjjCPcJDAkhCTYJSwlgCXUJi
gmgCbUJywng CfYKDAoiCjkKTwplCnwKkgqpCsAK1wruCwULHQs0C0sLYwt7C5MLqwvDC9sL8w
wMDCQMPQxWDG4M hwyhDLoM0wztDQYNIA06DVQNbg2IDaINvA3XDfEODA4nDkIOXQ54DpMOrw7
KDuYPAg8eDzoPVg9y D44Pqw/ID+QQARAeEDsQWBB2EJMQsRDOEOwRChEoEUYRZBGDEaERwBHf
Ef4SHRI8ElsSehKaErkS 2RL5ExkTORNZE3oTmhO7E9sT/BQdFD4UXxSBFKIUxBTlFQcVKRVLF
W0VkBWyFdUV9xYaFj0WYBaD FqcWyhbuFxIXNRdZF30XohfGF+oYDxg0GFkYfRijGMgY7RkTGT
gZXhmEGaoZ0Bn2Gh0aQxpqGpAa txreGwYbLRtUG3wboxvLG/McGxxDHGwclBy9HOYdDh03HWA
dih2zHd0eBh4wHloehB6uHtgfAx8t H1gfgx+uH9kgBCAwIFsghyCzIN4hCiE3IWMhjyG8Ieki
FSJCInAinSLKIvgjJSNTI4EjryPdJAwk OiRpJJckxiT1JSQlVCWDJbMl4iYSJkImciajJtMnA
yc0J2UnlifHJ/goKihbKI0ovijwKSIpVSmH Kbkp7CofKlIqhSq4KusrHitSK4YruivuLCIsVi
yKLL8s9C0pLV4tky3ILf0uMy5pLp4u1C8LL0Ev dy+uL+QwGzBSMIkwwTD4MTAxZzGfMdcyDzJ
IMoAyuDLxMyozYzOcM9U0DzRINII0vDT2NTA1ajWl Nd82GjZVNpA2yzcGN0I3fje5N/U4MTht
OKo45jkjOWA5nTnaOhc6VDqSOs87DTtLO4k7xzwGPEQ8 gzzCPQE9QD1/Pb89/j4+Pn4+vj7+P
z8/fz/AQABAQUCCQMRBBUFHQYhBykIMQk5CkULTQxZDWEOb Q95EIURlRKhE7EUwRXRFuEX8Rk
BGhUbKRw5HU0eZR95II0hpSK9I9Uk7SYFJx0oOSlVKm0riSypL cUu4TABMSEyQTNhNIE1oTbF
N+k5CToxO1U8eT2dPsU/7UEVQj1DZUSRRblG5UgRST1KaUuVTMVN8 U8hUFFRgVK1U+VVGVZJV
31YsVnpWx1cUV2JXsFf+WExYmljpWThZhlnVWiRadFrDWxNbY1uyXANc U1yjXPRdRF2VXeZeN
16JXtpfLF9+X9BgImB0YMdhGWFsYb9iEmJlYrljDGNgY7RkCGRcZLFlBWVa Za9mBGZZZq9nBG
daZ7BoBmhcaLJpCWlfabZqDWpkarxrE2tra8NsG2xzbMttI218bdVuLm6HbuBv Om+Tb+1wR3C
hcPtxVnGwcgtyZnLBcxxzeHPTdC90i3TndUN1oHX8dll2tncTd3B3zngreIl453lF eaN6Anpg
er97Hnt9e9x8PHybfPt9W327fht+fH7cfz1/nn//gGCAwoEjgYWB54JJgquDDoNwg9OE NoSZh
PyFYIXDhieGi4bvh1SHuIgdiIGI5olMibGKFop8iuKLSIuujBSMe4zijUiNr44Xjn6O5Y9N j7
WQHZCFkO6RVpG/kiiSkZL6k2STzZQ3lKGVC5V1leCWSpa1lyCXi5f3mGKYzpk6maaaEpp+muub
V5vEnDGcn50MnXmd555VnsOfMZ+goA+gfaDsoVuhy6I6oqqjGqOKo/qkaqTbpUylvKYupp+nEK
eC p/SoZajYqUqpvKovqqKrFauIq/usb6zjrVaty64/rrOvKK+dsBGwh7D8sXGx57JdstOzSbO
/tDa0 rbUktZu2EraJtwG3ebfxuGm44blaudK6S7rEuz67t7wwvKq9JL2evhm+k78Ov4nABMB/
wPrBdsHx wm3C6cNmw+LEX8TcxVnF1sZTxtHHTsfMyErIyclHycbKRcrEy0PLwsxCzMHNQc3Bz
kLOws9Dz8PQ RNDG0UfRyNJK0szTTtPQ1FPU1tVY1dvWXtbi12XX6dht2PHZddn62n/bA9uI3A
7ck90Z3Z7eJN6q 3zHft+A+4MXhTOHT4lri4uNq4/LkeuUC5YvmE+ac5yXnr+g46MLpTOnW6mD
q6ut16//siu0W7aHu LO6470Tv0PBc8OnxdfIC8o/zHPOq9Df0xfVT9eH2b/b+94z4G/iq+Tn5
yfpZ+uj7ePwI/Jn9Kf26 /kv+3P9uY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AM
gA3ADsAQABFAEoATwBUAFkA XgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtw
C8AMEAxgDLANAA1QDaAOAA5QDq APAA9QD7AQEBBwEMARIBGAEeASUBKwExATgBPgFFAUsBUgF
ZAWABZgFtAXUBfAGDAYoBkgGZAaEB qAGwAbgBwAHIAdAB2AHgAekB8QH6AgICCwIUAhwCJQIu
AjcCQAJKAlMCXAJmAnACeQKDAo0ClwKh AqsCtQK/AsoC1ALfAuoC9AL/AwoDFQMgAysDNwNCA
00DWQNlA3ADfAOIA5QDoAOsA7kDxQPSA94D 6wP4BAQEEQQeBCwEOQRGBFQEYQRvBHwEigSYBK
YEtATCBNEE3wTuBPwFCwUaBSgFNwVHBVYFZQV0 BYQFkwWjBbMFwwXTBeMF8wYDBhQGJAY1BkU
GVgZnBngGiQaaBqsGvQbOBuAG8gcDBxUHJwc5B0wH XgdwB4MHlgeoB7sHzgfhB/QICAgbCC8I
QghWCGoIfgiSCKYIugjOCOMI9wkMCSEJNglLCWAJdQmK CaAJtQnLCeAJ9goMCiIKOQpPCmUKf
AqSCqkKwArXCu4LBQsdCzQLSwtjC3sLkwurC8ML2wvzDAwM JAw9DFYMbgyHDKEMugzTDO0NBg
0gDToNVA1uDYgNog28DdcN8Q4MDicOQg5dDngOkw6vDsoO5g8C Dx4POg9WD3IPjg+rD8gP5BA
BEB4QOxBYEHYQkxCxEM4Q7BEKESgRRhFkEYMRoRHAEd8R/hIdEjwS WxJ6EpoSuRLZEvkTGRM5
E1kTehOaE7sT2xP8FB0UPhRfFIEUohTEFOUVBxUpFUsVbRWQFbIV1RX3 FhoWPRZgFoMWpxbKF
u4XEhc1F1kXfReiF8YX6hgPGDQYWRh9GKMYyBjtGRMZOBleGYQZqhnQGfYa HRpDGmoakBq3Gt
4bBhstG1QbfBujG8sb8xwbHEMcbByUHL0c5h0OHTcdYB2KHbMd3R4GHjAeWh6E Hq4e2B8DHy0
fWB+DH64f2SAEIDAgWyCHILMg3iEKITchYyGPIbwh6SIVIkIicCKdIsoi+CMlI1Mj gSOvI90k
DCQ6JGkklyTGJPUlJCVUJYMlsyXiJhImQiZyJqMm0ycDJzQnZSeWJ8cn+CgqKFsojSi+ KPApI
ilVKYcpuSnsKh8qUiqFKrgq6yseK1Irhiu6K+4sIixWLIosvyz0LSktXi2TLcgt/S4zLmku ni
7ULwsvQS93L64v5DAbMFIwiTDBMPgxMDFnMZ8x1zIPMkgygDK4MvEzKjNjM5wz1TQPNEg0gjS8
NPY1MDVqNaU13zYaNlU2kDbLNwY3Qjd+N7k39TgxOG04qjjmOSM5YDmdOdo6FzpUOpI6zzsNO0
s7 iTvHPAY8RDyDPMI9AT1APX89vz3+Pj4+fj6+Pv4/Pz9/P8BAAEBBQIJAxEEFQUdBiEHKQgx
CTkKR QtNDFkNYQ5tD3kQhRGVEqETsRTBFdEW4RfxGQEaFRspHDkdTR5lH3kgjSGlIr0j1STtJ
gUnHSg5K VUqbSuJLKktxS7hMAExITJBM2E0gTWhNsU36TkJOjE7VTx5PZ0+xT/tQRVCPUNlRJ
FFuUblSBFJP UppS5VMxU3xTyFQUVGBUrVT5VUZVklXfVixWelbHVxRXYlewV/5YTFiaWOlZOF
mGWdVaJFp0WsNb E1tjW7JcA1xTXKNc9F1EXZVd5l43Xole2l8sX35f0GAiYHRgx2EZYWxhv2I
SYmViuWMMY2BjtGQI ZFxksWUFZVplr2YEZllmr2cEZ1pnsGgGaFxosmkJaV9ptmoNamRqvGsT
a2trw2wbbHNsy20jbXxt 1W4ubodu4G86b5Nv7XBHcKFw+3FWcbByC3JmcsFzHHN4c9N0L3SLd
Od1Q3Wgdfx2WXa2dxN3cHfO eCt4iXjneUV5o3oCemB6v3see3173Hw8fJt8+31bfbt+G358ft
x/PX+ef/+AYIDCgSOBhYHngkmC q4MOg3CD04Q2hJmE/IVghcOGJ4aLhu+HVIe4iB2IgYjmiUy
JsYoWinyK4otIi66MFIx7jOKNSI2v jheOfo7lj02PtZAdkIWQ7pFWkb+SKJKRkvqTZJPNlDeU
oZULlXWV4JZKlrWXIJeLl/eYYpjOmTqZ ppoSmn6a65tXm8ScMZyfnQydeZ3nnlWew58xn6CgD
6B9oOyhW6HLojqiqqMao4qj+qRqpNulTKW8 pi6mn6cQp4Kn9KhlqNipSqm8qi+qoqsVq4ir+6
xvrOOtVq3Lrj+us68or52wEbCHsPyxcbHnsl2y 07NJs7+0NrSttSS1m7YStom3Abd5t/G4abj
huVq50rpLusS7Pru3vDC8qr0kvZ6+Gb6Tvw6/icAE wH/A+sF2wfHCbcLpw2bD4sRfxNzFWcXW
xlPG0cdOx8zISsjJyUfJxspFysTLQ8vCzELMwc1BzcHO Qs7Cz0PPw9BE0MbRR9HI0krSzNNO0
9DUU9TW1VjV29Ze1uLXZdfp2G3Y8dl12fraf9sD24jcDtyT 3Rndnt4k3qrfMd+34D7gxeFM4d
PiWuLi42rj8uR65QLli+YT5pznJeev6DjowulM6dbqYOrq63Xr /+yK7Rbtoe4s7rjvRO/Q8Fz
w6fF18gLyj/Mc86r0N/TF9VP14fZv9v73jPgb+Kr5OfnJ+ln66Pt4 /Aj8mf0p/br+S/7c/25w
YXJhAAAAAAADAAAAAmZmAADypwAADVkAABPQAAALA3BhcmEAAAAAAAMA AAACZmYAAPKnAAANW
QAAE9AAAAsDcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACwN2Y2d0 AAAAAAAAAAEAAQ
AAAAAAAAABAAAAAQAAAAAAAAABAAAAAQAAAAAAAAABAABuZGluAAAAAAAAADAA AKPAAABXwAA
ASsAAAJ5AAAAlQAAAEwAAAFBAAABUQAACMzMAAjMzAAIzM2Rlc2MAAAAAAAAACkNp bmVtYSBI
RAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABIAAAAc AE
MAaQBuAGUAbQBhACAASABEAABtbW9kAAAAAAAABhAAAJIjAgAqqcBCT4AAAAAAAAAAAAAAAAAA
AAAAdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUsIEluYy4sIDIwMTAA/+EAQEV4aWYAAE1NACoAAA
AI AAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAeAAAAAA/9sAQwA
CAgIC AgECAgICAgICAwMGBAMDAwMHBQUEBggHCAgIBwgICQoNCwkJDAoICAsPCwwNDg4ODgkL
EBEPDhEN Dg4O/9sAQwECAgIDAwMGBAQGDgkICQ4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4OD
g4ODg4ODg4O Dg4ODg4ODg4ODg4ODg4O/8AAEQgB4AKAAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQ
EBAAAAAAAAAAAB AgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhN
RYQcicRQygZGhCCNC scEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RV
VldYWVpjZGVmZ2hpanN0 dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4u
brCw8TFxsfIycrS09TV1tfY 2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQ
EAAAAAAAABAgMEBQYHCAkKC//E ALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXE
TIjKBCBRCkaGxwQkjM1LwFWJy0QoW JDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZX
WFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5u
sLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp 6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A+To438
n5HVipGCR1q/Em6JiwOc9PrUEKbY8KCd3SrsUZ UfNk7sZA9ayje2jCbjHboWoh5gIJCsp71PH
ErQltrDcfXrTQvz4I4zwAeasLE8bcA4HTNNU07u+o oKLd0yaJFEWQCGwCD6CtJBFsOCSx6nNV
wuxE+Q5HC59KsxxuACNufUjpWjp33ZLm0TwpsdgnO3HG fWrSjEnG7nr+VQRIWYqQ3J4q7GMNn
OCTilaNxzaT1Y+NWSJQw6Lipo13RM3pgfjUa5RzuO5asxKi kEkYA6VqtI+pLaWhZZRJFGDlyM
kY71MgUIecgjn61HbguCjoyg9+lSxoVbDHbnkkjpWaj0fQldri g52+4yaFj2y8AqvbmlCls4O
DnAJ9Ksqylxxz0Fa81mrIt3jHTqKkW/DZ5PJxUgQhmByXBz+lAAUA k9D2qcbAFZTuPcntUOUm
7Iq17SYigNGoYbiF7UhGJfQAZz2zUiliduRwdtTkYQhSFXd1NDk927lR Scit85kG3gnHFLhwA
FHOc/Wp4wNuVHHc1IACxB7D16VE2+a6QX3uiEJlD8pyTkj0p2394QQSQMYF SnOSMhe3Sm7Cjq
mecck1fNeOpMoprRgFOCOetOcMSrHg9elPAxIN2MHkcdqcRltuDg85rG0m9EFu zI/LRWwAScZ
Jz1qdYQ6EkYZe1KiFnG0qPXNPwSQpJI7Y705yfRltSe5XdHLbu2enpT1HzorHOBjH tUmxhkkP
j+VTJGwYFgNp6k9qXncIzTViAxgAjsPU0Kn4euas+WcOx4GaBHk7sL1GM96q65Xd3KjF W0ZFj
aSQQ425NSrGSARwTkA9s0qRqCSenQVZCrsO0MOelRKSTSSM3q9Csy5Us3LnH51IYyYs5zg8 U4
JyTtJwanCsEwMHDZxjkD3q4JNle8rscgySOM/xe9O2sMdTjJx9KmSJt7FgASevtT1j3MCucg9/
SpcnfVGcVGz1K5A3K5545+tChshh1PUmrYjUxlQCy57elBSQPwowDjmm9VuNP3WV+EYEhsZwD7
1J 5J8hPmBPU+9WFBEhLAcgHkd6MFQvTI45PQVV9dUOzbuiqy7QY14wc59qV41LYHJz61Oqqck
5Ck55 70BAH3N+B9aXMloWm4+RD5fBU8Z70/CGIIRkgCpH+UZYZUd6REClRz64PUVUWum5Ls3e
40KOWPJJ ySO1O2ARk7SecjHrUx4G7aCuc1IIt5G3PqwpXfVA1FSRXXPkruO3PApArlxhgFAw1
WOA4UgLzyT/ ACppUuu3B2selWvd3RDTTd1Yr/OZM54PU+1QqDu5OwDocdauldockfTNQAttBl
AKdiB1pdW2i3Hm V0tCA9HwDgNjFVHAM24Da2CRn0q80eWPGFIz9DVdo+u75h0yKiN4PRgrRV7
3KgDbl24B5xnvUbB9 x2jAH86uBBgsM5XvnpUBDEE7lJ7itLoEkiIKwO5Dgn1HWlbywxDk5HQ+
9TE7odqDaucjPUVGw+Ut wxPYCsVBbBy8xG65jcHJycDHeq4j3LuHAA+6fX1q9jeMgDJ6HFQbS
YsZ6frXRBNJ3JbtuisBiTK8 8jj3pXZyjsOCCOoqfaDKCo+975pxVhHtwAMYJNZ631FzK+xS2h
gxYcqvr1qIYDbiCMj86uGPDBWH TuO9Q+UCgC/fz1NaXhs2a+0SVkQBR5me5/Wo2CkMGyvHU/W
rRTM23DZzyewprKwKbgSMdMVlzLWx GiaZWZjgncMdAKh2MZPu4wME5qw6cL8v3RSNteQqnUD5
qSduhUndaFJ0zDg9R1xUBjDJtwdpXIIq 4yb3L7H9wTTHXbBtTHzdCaq/K77icVoomeYiybHO0
d6rSJt6EEdiexrTKtxsXLY5Paq7RMJgSAV9 AO9aRloxwhzbsoup2LzkHjFV5EJCg4Cgg5J71o
vHvZQT8oyenJqJok3spy2RkUmna19yNFKyZmkF Qp77TkfWmYAUKwK854rQ8tGiBLLnuPU01kX
G/cCc8nHFRey1KbVrWM4oPvAhWB4J7etV2RfkUckN lauunm4JAHJOMVXlj2gEHoM5qKcYq/mV
Dkdmys20naecZGRVcqQAduATyPStJAB8q7Xz6dxULsm0 gRk49T1pTcl0M5rXRGaYwqAschhgj
pz2pohVSH5DAYPPerk4Aj2k8KPkJqMqhVsjA+8aykoyV1qy KrbdmcPbIxtyuVDk8fnU6rIGwd
ofkdO3rTIInVztB4HrV6AMW3Pznnnr9K0Ti/e6lxS2JI0Cpuxu I6Cre1WXBIyM8jtUaBWcFM4
PcnirO35gvOSMgY61cZrZjcHGWwi7iuCcj+Ee1X9hXaW4GOcnvUSq DtAIYDgEVbRCBubJwcVS
kk77EX5unyFQnapVjwfWrigjkHDcHJ5qFVIjD4G0jgkd6srGGm5IU4ye a05+idg93axKCN6kr
vBPIFWU2iR8grn7opIIwTtDAgc4qYRESj5DhelJVOgRkh6K5X5uFHT3NWM5 D56ds0kSkCMEdu
hqVEEm4ZAb+lNpLV7DhK716CAgAZIXb6ipCSUYfx54I9KcEGdzfMO3vUwUsQdp 9zUOSvciDs7
iRhfJAI3AEDOeasEqVZQhB3YzTFXksFOMAjPQ1KuWl5GOetOUrlNSXoBQHaOjcfnU ka5U8YHb
PenlOeDnjipowcruCliMAAVkqivoTKWtyNFwrZHy+3rTRlnYpww9RVlk25yehAxTdv8A dGQD0
qlO+xoprls9xrMCB8wyf0pCuWHfHenCPMuzGOeuKmSM7kBIZTyfX6VF9GPVOxCvLZPOD27V KE
bIJypB4PtTyquWCgkcdOtSohG35WbB4FKLk7hLyIwrGU7umck461OqnIBABxxgUoV15QBhyRxU
6eYeoyMfLxRe1rpBza3bsQZQjcfu9TUygsrkKewB7U7CBcbcZOOaeyB9wG4ZYdDilLlRLV43aI
TG uPmJ6cD0p21sBODjgcdqnxmPIU8cD6UuTtw2D6cYzQ46FySk1cjCfu8MMgHjFAQlNoGVz1q
4qghS VyT2oC4QYVjk5JHalC9rijOMVpoVkUhyQC4x0FWQB5bYG3d1Y/yp+z+JAct/KpnX5Azj
bz0NNTaY k4vcr7ZNoyQwHT61NtO44OM/p7U5doVdx5IPSpgCEAGCTxWmluxUdFqyMAhcHO3GK
cSfJORkAjp6 U9cFgMH/AGs96fszGV2n1xUzXSwKdyIE7GUjjqPWmMMjoTk8+9TjG7ZyzY5FLt
3KNwwR29KXw9Ac bNELITCVOGbsQKaAxkI6AdzVkgliFU7R3pdqm4YlSoOMZp82gcyk7EGNyjc
QwxggU0A+aXCYY+tW lTO3b3PXFKEKJwCcVSfvWsVztIgbe23cPy70/Y2CBnPXr0qVfmBVl4Jy
MU9IwAR0HbJ6Ucz6IzTa 1kyLYMDncWHTHU0NkAKFxjjOOlP2Ethn4HpSAbnz144BPNNy1CMdd
dSsWy43jdj26UhH948jsBUp QtIwxtIPT0pWX94GUHO386huI3Ho0UXjV3ZS5Uk5AzTRGFkJOc
dvarGBvxgk7eD6VHs3RKTuDZwc 96J3ve5bTatcpMSrPgD73PFJJHnHG47cjjvVh4w82GO3NN2
lmbkbRwvrRGpZak8qbK4RdoPc56U1 Yssc8Lj8an2FWA2kDrmjaSJF7Acgd6qE2nZAoroVzkRh
MHryR601lLK/RecdKsbVQhs5BOMZppUD LDoWyM/zpuUn5lUuVN6FFDhsgcKecUOQVODhifu1b
KjcSBt9TUJQqRlcE8Z60m1fUUpq2xAQxG4Y yM5yKiY8jcV6YOBirexgvIyvUkVXKhotuNp7E+
9J27ExlddyJkzGqA8g4+opgQ+YTuyoOfc1MQm/ axIIXtSIqHDchgp4zS5ujJcXazKnl43NnIL
fjTCWBJ+UZzxire0OMgYXnPNRMpZQNpx9KtSuh3Kp QH0BA4Gar/NuJVlK5wAfSr5HAJwQOw6i
oiBsJI2nODx1pqL6FOS27lRlj3ZUnntmogqtuyCBwTV4 qBDwPusB7moggxgMApOSKz5o3sDir
XKJiGAD/A3B9cVG4zCCAADkZx61oFi2F2NtyeSKg6wfKowD 0I6+9a03fWRn71ym0YEW3AyOp9
aq7Au4ckdh6f8A160Xj/dDdnPbNQeSCDuJIz69TVWuro0VmUHC 7AuGG3pVfyQ1xt3YBB4NaDQ
7Mt1LHHNRybWK7mULjhhwBUPmbuiZtRdkzLwF2quA5GR9KrvG29lY ckdfStJkJj3BVYLxkVCV
dUBxuycnik5a3BN82hQ2c/N8317VAY2JZuMY45q+6b0Jyc+npVRl2Abu F6hu2ayjPmlpv6BNy
scWkR3jy2LAnB9quLjesZZX+nGDTY1jLkYZT6E9asDHnMccqeKelryZTjL1 FjYKFB7cYq2u1S
u70xn29KYqLlTtPIz06VONznDHCYHUd61cby00RKcXezJgQASANuMgDrU6FimE yVwBmmIMbVy
B2JI4qwmVOM454GOoqXZIlOUVoTJhjtY8A4/GrCqC+FPze/NV0CEMNxGfSrKAL8oy W65PaiKT
WzE27WluWgxVtnG4kc46+1Wgrn7o4zwT2qqh+QO3UNjOPWrhP7kBWUELzSu001oRy20S J0VVG
7JYHoKkjAAdz8pNNjKsFVVZiF69qVmDBAM5x2rSMeZe87lXT0JFXmMs4GB/KnLkRhgxyR2/ Wl
2guoxyeeakj27UGQp64PaovJO6Qk3HbUWP7/Xv8ue9TkOzPtA4PpQEBnDg8Dr6GpSPkLA8Yxij
nTZq7qwJnYA+SSOwxzUwVlkG44OM/SowMptTjB6mpwoZSTnI7Z7VTb6iWj2H5wDlctjIPpTRyw
zn 3xT1RSRklcdc1IFCsGzjI9O9ZxUNbAnbWxXCKThSR3GT0zU5XaVXOWPp3pwX93uCk/NyalR
RyWyp BOM9jROS66E8zvoNU4I+Vl55JqULnc/IYniiMeZHuHQHk9jT1x5mARzzjHIp8umxq3ro
PXBVQMgj v2qQLu3jBUfWpPLOVw67SvI9akXaRxlm74PWnLTUjpdkIiHnsDkgHIqdFYxFmVcjv
ilCsMSLxz1N SspDc4xjJFQotyuLV69CGMkPyOnftineWwkIC5FPKDY69SDSKjFzjKgf3jRbRt
sbgl5Cbdy4YEYA BqTaCjIOdvB9alVG8xsLlcYA9akCYIAOecDNPmu97FJx7kYwD042kcdqVc7
djfMAOB3p3zbgOPfj rUobLDK9tpx2pRi7aohNX1GgKAoIB9Tj9Kbty6gZHGf/AK1WNoQhscA0
9VHILA5746VVkXGViEr8 4VO1WAFADt0xg+1TBQCCRnjOaDGrx/KOSOM9KhN3DmVyodyvlRkg5
6cmnsN8gYqykjBx61N5W5lH LEccdTStGQQAhD9GJqrpyDroQfO0ez15/AUpOTt2HINSiLPAzu
we9PIIBADZI4/xofI1sNMhKYkB /g6/So1VPMU/MCw/vVcyWhIBDY4IqIgAqMHjP+RUwV2xvfc
RP9Y3QkYwaXG+ZW3Acf8A6qGUlAFw CD940Z287d+ey0cvYjldrpjH+/k/dJzSIcHd/CR8pqZQ
Cxdl3Hdzjpmoir8jrk4A9PWtFcE00MIA BLN8xP6VEdwZW4wPxqc4Uj5fl7Z7VEyjcSu7BPU9A
aUtgcrtXehCQxcE460wuRKqEdRgVOR84Bx0 /P3prYAUYDY6kVCk7lOGlyFolCbm5btUBBVs42
qvT6mpMNwwbryBUwAywyCSfyrR3jZkxauVAoK4 bOM8HNC4yybfmPUipNuUAYZOfzoCsZAOAev
/ANamo33L+FXW5WZFEhHX0BpxXzHcgDdx19BUzEbi WXBJPHeo0UtJvCsgHBU+lTL4dTOfe5Xd
B5pZuMHIUd6iYZbvgkVbkG1TznHf0qFkzI46D+92qeZ7 iSRWcZn2nIbODzTCpGSMHB61YZf3b
ZGAO+eTVd4yA2MnPvQk927DVlKxG6jBJwcnk+lQmMFTjJA7 9KlkBWLnO3pinEfIQpHT8qpNtI
NU79Cru2odvA6NSMjcBTkY61JtJhDNjlhkAdKeU+UgOFbPJP8A KrejuOL10RVK7IyGI64B9Kj
2Ah9wPTKk1O0bFgjnbu6d6btAdhtLc4P+NLRa9yb62bKm3EQDA4xk ketMaM43Y/i6Y5FW2UHG
Bmo34PAy9KU7i1asirIGKY7kjgVEQSp3HbjjPqKsSKQTj0BNMPzbsYLd 89Kq+qsVfuQk5YFun
QAjioyp2KQDyc1aZThkG3JHSo/4FTBOD8xH6UlG6egnLQolN2dvQDnNVGBC 4AUg9citd1YLjK
nJxgcVUkiUQMG4YccU4ya06Cg0tWZ3lLu+X7uM4BqrInzttcH6VpHcG/2T271W aMoF55Kk+9S
k3rsaK6bszPkiITczAFuQPQVUMSlPmOQvH1rSePMALZLH37VWkiXJ2HaOvNVzWRPN b1OQ4YLt
VTjrxU8cQMiylcDrnHenIvzljjcTgAd6nXIjYN0B5XuKxTd9URHR3S3ARE5wAARmpo1Q hiQ2R
j5T1BpQF25DfLwQfapFXDuFBwTz64rW11ZsfM7bCoMHOCTjn61OpUv8yjJ6EdqbyoI5JPPW pl
GG46EdamMW90Wo6PUkCAN/CBnJOKsKp2fKC7dGqNYgmCT1NXIwPIC9VPP4+lVTcWmupM5K2jJI
1O1QBwTnj6VZjj2jcWyccj3qJUwqYJXnvVlVUBSCBk9D2qV7quZubY5RkEDIAHGKlVeMbSpzzU
aq AQc4GOBVhAH4JwelP3+UpxaW+g+NldSDw+MEU5QWlBAx2xjrTEVftDY5I61Nyr5IJPt0pcj
TLj7z 0JkY45IBI6YqQr8gHUDpxUaIABtB5PP1q7tGPccYNS0oscFyvQYnzH5cZPTFTKORwRxw
PWkC5BOM g8DFSrncFPUcim1bUUnfW4g+VMhSc9c1IY8y7ucA9DQY9/GTz3HSplAEOQDknvT3W
gruyY1FyhLn AHIx61LtBjDf7XNNCtySMZ6gU5cnae/92okubqNJLYkCbVIGMY605OeSAPl60r
DYM9MYxzTwMvgH APX0ojZ7scZN6MlVCyKXGAv4VKFUSsSQBnK+4qJA7Bg2Qo6L6fWrqrGdxUb
l449KblyqzYWdvIjV RvwPnDcn8KmweODgccinEYlAwMdqUoTIoLdOabWt2Jt3syHbmTcOucED
uaVtoVQ3BPGTUxjIz6hs 5qTblG+Xjd+lU5K1xrTV7FYJljlzgHOe1TbeSVVs49e9O4JcsMKT0
FSKQrjB4789awblbRAoys2Q KSrD5Tk8gnpVhUAKkLuGCTShN69yw6Uqn5ioBU9+atJuzsN2+H
uC5VyzggehqVETaBtzz69RTkiM krPj5SvQ84qRlOQxI2jsKeknbqNWTsmO2AH3HamfMpXcBnG
Tx0qTkwED0zn0pu3OCRnjinFa6kNN qzDsc8DI5pwB35Jy3b3p3BTIBA6nNAQl+CM96hrW76By
WtYjA+YKpw3XB600LIsvIOO4P6VM0Yxu AO49TT8HA7GqUU3ce22hCMkDPDdzSYUjef8AgIqXy
y2Rnae9DR7EHoOopJR72BRs1Z7kecJxt56i onGEOB8zDtUyoAvBOKQ7BKvXJAzzTh1sCa2K4f
BVf4R0A/nTF5OC+M9qtMIyWO3B6A1CF3bcA8Lx xThNrQJLaxExHmE9RxgetM25cAErg8ZqfGE
IYYbFRH5SgK5yOPXNTzeYN3diEjAOBnLcYprIRIVY YGOtT7S67sYYjcaZ5TYJIOc4BpJWepLT
tbYYRmIKFI3dKhIw6jnuM9quKH/dgjgc5xUG3Mm/rk/d x0q0+bQtVJR0RFyGVSQcDgUMAXkJU
gk5OP5VPtDOcckHtUcyEoSAVJPA9Kq8X6kc3NIr7cyAMDgj jHtRkrICSAT7VMuPkJyG757Gkb
PJK5J6YpO70YSd20VHUvHu3AnPboajbCOFOArHBq2w27VAHTp6 UEApjAPf6VU2r2HotEUnU7m
Kjfnt6VGwcnbt2kDnjp71aIDMVOdxOQe2Ka6kZ+916VLp3KXLzLuZ +Nr84Y+9H7sx4XhzyRVn
y9z/ACglsY3f1qLYBDjGCT1o91oLK+4xwTGGjAbPUCoMZi+YgOOntUxy nVie4x2pzKu7jJDHG
aHZLREu97FSSNmdVLDdnAIFMbchCEAsBzVqRS0eSOfT+tRTZMTv6kA4pqVk KMddSmR8wDZwR8
xqPgP0zxx71ZCKpyQcBe571AB1yRuHFVKSauXBLUjI5BK4Lc4PVaj2neVcbV6H AqVVLMcfNxy
TTSDuBJ68GpgrPUma7kZCMDjIG7hqgWT5dp5OCOP51aZVdODx6VF5ahSxI3H5sY7V cWr+YrNL
QiK8DeOT0AqHY2/Zt4znJqwSx9MY4JFRYYHrhRyfrR7NpPQUm7FKdRvHXKjk+tVH+b7o IbsfQ
VpMv7sBgOhzkVAy/IW4AwO1EZpoqLcUrGbJBjb1YdiOlVJUGG2Ho2AOta7BQW6nB5qjKWZA Rh
cHJOOvvSjNteRK35n+Jx2zLkFgB1x3qwvlvCSDknuPT3pkKctv+ZmH5VbWFfKP8K+1S3Hlu0VT
UktBiKcxqMHaMfWpxuMhJGMcYNBVdyYyScZ9qlCjJOfl3Zya0m9UiHT1fMJ/GyhSnOSakRQck5
JH 8IPSnCIhiSmSGxmpYgysWABOemKlRvKyKUktiVUVlIySOuM81NHGpmj+96gg96EXeylDjHH
Tp9au Ip3cYP0p8zQRu9ESAHY2SBhh19KeFbaVHzKCOlNwjxkMD8w5we9Tqu3Z8hPfg011E2kt
h3lg4JyA Dz7VKuQcfLkHPPYUKMyZJ2jqM96kCq4BAZiTyajTVMvVR1YKpbBHIz27VaWMKvJye
3vUIiKybRke v+NWgCFBPJxzTlJP4SVGSSdxIwjOWyRnselWE+Y8fNkcUxAxVTtB+g6Va8sYIT
txnNRdKSTKbYCJ ieMrgjgVKsZYNjIG7IzzRGDu4BG08GpVXMiu+evABpzk2LXdkUauHOCMHqK
mUbZguCR0JPalYEKw PUdcCnxgNEzEYI9TWU00hWd7oQJ8mCc4HBqQIqKpzu29/Q0oBOFwATzU
xBCMduOeRUtONrlNydk7 EcQDIykEtnj29qmaPJXAK4680KOFbBJI7elPCbo1wduFJwfSq91dC
rPo7Eq/eUBflxxUwABOcgf1 pqACFN5UsecipkAMg3Hg88UoyVr2JVm7Dgm9t3UEZFP2hFBJxu
55pFB3gKrFgOlPKMV3N3PB7VTv a7GoJvYRow+5lYDP5mlXIZOp4pdjBFUtlRnGKkERBBIbnt3
FO91ua83LFJ7BtT5jwBnFO2jPsB19 KkCkgLtO3GfrSgEvgkAEdMcmkrcy0FGzW+gxQCoKgtzj
ilwROARySefapgm04UfNnnFKfvqSvXpU 3voTe70/EWNZd3P44qRwfOAAHPUYpEVmkYDpnjH6i
pwrB9nU4x9KmWjJu1Ig8vLAFTg9DninKCV2 gd+CalUk8A/MTmpFIbKt1rbm01RTjK9yAAyOfu
qRwB604ghAuMMcGpU6kDGD3I5FIqfMWJwVOMno aj3t0hTjeRCsZXJbGPrSYPmYP3SMZqcbiow
uQvf8aUhCVycEH7o61oovqGlyAwhFzye3JpPvNjnb jg1Ic/eLA59+lIMeaG2nG39ahxa3KbaV
2RqpPzbhjBycVEUxGoI5PB561YOzgdCOvvTijMSy8qDk AGs3U5CFzXuykw7DnHQe1NHCHjBzz
U+MlQwwCOPemMm98DGM9a1TuhtQZAdzEuudpOee1K8ZIjA4 GcZqVdzPtBG0j06CkMTAbVy2Dk
k0ot37F1IrS7IGDrnHQjFOKtt4BJH8qs+WPLKnOexzTFVlQKCc k9x0FJz0uZOloVlGZc5xx0P
WmlNrLkH1B9qteWeD0JGaULx1DN2HpVRWliZR5VdbFXauQAOvJwaJ NocgDdnpk/pVp4gTkgjn
gd6Ro2YLhQB6nnmnyor3U1YoFPn5ZeuAaYYzgAjLDg81om3KgMSOpxgV EFBfKc84PPSn7ttAU
le5RcEsdq/IMc1AEZpGypxnj3960XSQEkqcZwABTGDIuSuG7E96b006im2m upRMYDE49vrTcE
5bBzjvVzbwCwJpCpCBQMjHQ1jJNNXHvdGeRj5lznOPlqNVUMhUqX27SDVpk53Y YAVDJEzSDkK
DVJSW5KtaxTkwBgEE9+KjI4AwTkVZljjyfm+YDGPemmPKKB/31VruXzqBUGCp2ggA bagkTajD
kAt69/SrTnAcFW4P51WkHAbady9vWkpMKl3a5G2DHs7tzioZFbzNoAHPYfrUrhjhlO1Q e9MZR
5rLuJY9MdqqTWmolFpX6EGMZAYDuD60MCVBf0z9aTB3KpHzECjDAEMQCtJQ1LuyMhsZUcdO el
JsUplWAYDGPUVKcHpjgcj1NLjMK7gFbkkdOKpS+8ht21Kzrh/7xxx6YpOAuOhz0xUuR5YGRkcH
im7kGSfWhQbQlq7MpFSZNxOBjvUbkBdxGQTzV0puk44P9KqyREbSMYBPXvWnkJK7V9ikwwrDac
no KoyRjA7FupNarRsAxIJdT+BqnIoHJwxNYqd37o2r2OSVAH4GTjC+9SiIlP4hgYPPWiNd77O
hJzU6 AeYcnkeveiEWndijKXzGoCHGep5H0qdAAEfHBHBqRFH3mU4fJ4pUA2hdp5Hr0q/djqhR
ldXGkjyj jOSct7VYgwfmZl4OD6Gh1Uop2MrVaSNBGcDHPU9DRNqMdQ91LUBh5o3UjgnIq0oKH
LL8nG0+ntUK Bvk3LtYrk8dCKuqSQC/3OpBpJ6aIbTi7t7iCE+XkcEHOB6VNGgx0ZVbqSc0Ko8
xtpLcYODVlVxDw 2OOQR0qm77j1T1ZGisFYZQgcDAqzHt2lMYx/OnIMrx2qxGAJMbcEc896xem
w48sVdoYqc8DIIx15 pQgXDEcHg896kMbs8gzhQePpUmzKkcsMnJPrVQlpZDjo9BYlcxsqgZHT
PcetWUXy4+oJzSxx4hQn IYggCp0T5cBSXHOaiSbIkuXYYiK7cH5icnBqQja4KA4VtuMVKNq4b
GWyMYqRkPmAg9fmxVySSByf qVwuGJbI78+lPXaTtwMZ/KnbSzgYYnOfrTRw6sw+XoR3xWerRV
lF3e5MFKjOw4HJz1p+A8oySccq PX60IPlDBWwTzk9KaquFJXnBwPU1Lbej0Jv5jWB8racx5xg
+gqeMkZx82FI3evtTQvmJgttbpzTx Gylg2CMYAFOyvqbuCfUkiX92rZGO6+lW1TIwQduetQxD
5WVvbOfSreAm0cgYzyaOfo2TNxi9NRMv t447fWpduIvusefu55FQ5YMDj3z1Bq0qsTubPsBVq
KWz0Ks762EjjBwwPc5zUvJcAggAZ3HtQPvk 8BfSpQrllHXNS7bszlve43nb6+9TKp2EgBsDOA
OaRYz9OM4zV+9vZb65SaZY1YRqg8pNowPp396i bk9LaepajzPRmfg5BKnlue1TMmZcAHJ5APp
T+NoCdz370pG1x/Hx2pqLbuK1t9xgRkXeCCCecVN5 b+Q/UH1oQMEVWUk54o2s0yqrMuBg5PU1
pZ6CteWoKo3biMBTyaVTmMhyFOPSgxs0PzffHWpApxyM Y4FTy8q3KlFNakQiJYdQQeT/ACqRB
hQuQMg4BGakAyoyOB3po5OSmMjr7VTTb1Rnz31vcRQNmRyp 600xqJ+clgORnmpAMoB1Q8ijaD
8zHH+16099UNR1uV2QbuhHp/jQFXaC/BwenFWFUFJG5PHY1Hsd WBbgEdetZ7p3ZUrIiCEZC9y
MsfaoWOZCMhc9fSrbgEsGVjxke9QDbnLAL8vfvRG27VxKz2IlTOD0 471GUUkHIABq0QGTABPP
H+FIsW4LkHimhQ5lqyExlV29s5yKcFcENwDg9qlGPPK5G0E4HrQYyeit nPXNXSWqvsZ2VyFjn
eo4bIIzT1jTbvY4PYetTGIh8hRtH51IqEE5TaeM0pRXQ1bKeHIPy5xx0p6R F2Bx1HDdqvqik5
cbDg/Q0rRhIDg4GOtQ5JWJk/spbmf5Q3Hb3PGaaqDewPYY/Grvl7lDZAyMA4pp UGfnhh7VUUK
a7lVe7MM7fWqzrlchcAHJxWg0YGdoJWhYSVK4ySMnPtWqilqNNJ7FBl+fAzzVdoix GQSoHb1r
U8k5Axs5PXtUHlPg8fMvUe9C0d0yYyd7GcIct3z6VE8e5dwyW9PSr5+7u2kjdULJzuCk HJyPW
p13Zo7bGcVLSsM/MePpUEifeHBx1NX3Tjbzu/vYqCQARAEEsfQ1knF7MUU07FCRG3AhR24x z7
1BJGBvYZX6mtArlivO4HNUpPvsCOR3q1JPboHNLmKkoUx7sbiRyAagkGYgeSw4xVvZhRgZbHT2
qsy7mIH8XQ0Jx7g+V9SEgDkc+tRBSCHHHPGe9Tsq5ywO7HrUHP8AF8wWko3ZCiloiJgGdjkfSg
nI baARxjingMYCP4euMc0mEIB3gY+8K1lBRHzJ6kIUs2NuHz29aCrdcZI4/OnHGVfJPGBj+dO
KEqVA IyecnvTSW9ynOzTSGPGViAwCOnHWoXTbg5Hr9atlGVCHXPriq7cI2MZDDnqKE3bUUOaW
xHtJTBHI NV5CMgEgHkirjcy/eBJ64qPYm8qCG5y2am9tQta6KbqcbsZz1xWdKFXLMpJA4wa0Z
G/dfM2FJzmq L7TMSGAX0PUirjFv3rCglbU5gI4j4252nGBT448oqgdfzpVJVmABGehNTopfGC
MjP41Ki0op7E87 WiSBFILqc8HP41Oqh2OGAOMn61AgKguMlu/vVqMAptcYz07YFU6cbrW4NRX
QkWPFxgkMAD+NTeSS BlwOfTpTWXaVyCX3Yx7VaUOzZUcZwKlT00exMVZjfKZ12hup/MVaUMAd
zKADjBFLs4I6joAvUmpo 1fG35ST1z0xVJprc0cm9LhkqAFAzj045qZIzu5OSq80KAQC4LKRzg
4xU6D98Aepx1pN8q0H7vKHl k7XBwAatJy24gknoaiWMMw56enSrMarGuQ3bvzUWctLi0+YBVz
xn73Q9c1LHDlgTnGfyqeJN5DYA 559qkCncBjAPT3odrWW5MXK46OPERJIJAyfzqXy2EkpJxkZ
XHepY9oX58cHp6VJtVwMkjBznPWiD aRUpcysiDYcAYIPoamcr5XzIyYbrUwUljkg5GcntTfvb
gAGX3qGtSoK2+xERgccseSSOBTNi7Mry QclR71Y45DEc+1Q+VsACFsk561UU+4rx6gpIifeQA
TwKQ7vOKFW2Zxu9Klk+cfLjrxxSkseA3XkV KvLYPdWohUxlVGNucZqXbk5GB3570iYYDJyOuK
lUZJwQDnafapd47sFG7HwuGcHIIJqYrtUEjLHr UIj+f3x0FWAm2QJnIA7mhRb1iNQSJljUgFs
57c1Y2ttByB/eqCIh5VHXvVlQFOfU80Wa3KkmtEOw fLCLHjA9OvNSiP7rbjkjkUcHAwSMcVYV
Ay7gu0k8Amna7uhX8hnlqNp5zjBz0zT/AC+eF6DvUywn eRn5QevvTzFzuAI7EUdRwik73KqJw
OwBxzUm3EXylQcd6mCOFYcD5snPenhVVtxIJJ5HoablO+hT ty2GBGf72MZyGFBDbUyoGfQVaA
O3gAY60hVyCSFDdv8AGjfcmMYy1IgvGADnPfvUvlZbHY5PFAUF l3blyM9elLsbJJyOMGs5pqV
0LlTVrkBQNnnDdMZpWGCB9457DrUoQE4OOBTdjbeBuOeNv86pK8bi Sa0uR45XgAE8CkIBf7wH
J4x0q0wIAUqOnJxTAozyRhTx70lrrsDepDtG0IWGc9cU0g+bjBbH86nO 3eSRnB4pAp39MN/Oh
WauKz2ZAc4XbjJ5INQFQxOEJIHGKuH74yVB9cVGw67APbFXDUFHl1KoG+P7 rEg9qeiHYeMHPe
pwhRctyp7U8gKv3TkcDjrmhtDjdvQhaLMZKDcQc5pyIVZOOCMkGpEVs9CFxyB6 1KxJcFQMdia
SvYSp3umRffBKr3wMVIsMjElmGSMkY6elShQsauDkk8gdKlCKWGCSV6gd6Iu2liL8 qsVvLDqF
79/rR5DNK2WBAGDnpVsR8YwQcdaapYAZIPvjrT1GpaalIoVwB0757U7ZldxXqcVa2jyv xxz3p
AoVsO6sMZzVSiujLeq0KjRfMQAVPoaaAWHVSPbg1awAz91PWo9i7cdT1wKztzego766FYqd 2W
bPrxUZRSd24sOTnNWNowzFSOw5poT5cDA7E1qlrvYTSKDoVL7cbcA59feqzLuwWyfpxmr5DZIT
BUdPpUUiZB2kYbr7VMnfdjaa3RmvtbHynHtVSRFwDtJwOMelakiquFIKkjqaqSg+Ruxls4z2xU
cz a0KppddTKnD4QqOW6VVmQguA3zZwR71pN0wc5zwKqug2sfmOTkVV7eQNqKM6XIjBHBIxVdg
Rkg5I 4Aq267gcsCR0A71Ds3SMWXaCRn61T1WoorS7Km1uMjGBTTwhJXBPXjrUroVbuU3du9I+
Fl6qeMjN Du3ZA4EDDahYnAI6U0xlWOFGcDPvT8HY2c7u2akWMeXnlznrmqlLRGclGG/UrBAXY
N6jgdqD8vQE 89TUmAXYchup96UHjdkYA446U1d9DW15e9sNyfNAxkE/pUPAOBjGcgYqdchieK
YynfkjOWolG8bh F2TRXIJyAueM8VAAAzNnDn9assA4OzcSc57Ux1VoY/UDqO1EWuVXFdJGfKo
LfN2GMVVdQI/ubsdV xzV+VSCwAPzEduaqyZIZsFfqaqN09xt30RzGAJMLjG3gVLGEEMW/qD1F
RjcAHI/H1qYHLsevoPT1 p662MG4t2JT8+CFIUHjijZnag5GOfpUwLBlxjaVwvFCEHIzyp61nG
pNPRF35rpIlJbJPbjFWowRG Cx6dPU1WBLI2Msc1OoHkDJI9vSnyXVy46R2LCKwk4X5BjBqym4
Lk/Xiod6MwyG4461OAw5ByB2NK K0ZEo/zDtuUKg7geRg1ZjU+WeMgcD1piA+YdwGVHHFTRjbt
ZeMjBz70NN2Vxuy2JwihcNkEdcd81 OBGG+ZgMdSKjjyVxtYe5HWrJVMJ0PsOtTfQbtzWHqFWJ
eCDmrCK0sakcbT0oVB5TKcbhknNWYubf BOCcEKOopw1iDVtLD0QmIsANucY71Kik4yuec0kSF
pHboB2p20E919M1K6hyXYIvzcZYHoe30pyx gBU+73P0qZFORtIyD0ppVmlbAANKTb6g1LoyB+
5Tpu7imMjYJzyeRgdqtDhcFc4HT0ppHHHzZ6Yq nIlS0sQlSqk8YA/Ko/KbKMCPKUZNWCreWpz
kEcj3pVLbSqr94c8dKhKUXdMcFf0GgDfgsPy6VIqj YAx9/rS7dibQCQeckU5fvBQBnPU1HNdm
junoPCrvycBck4FTKrFchhgnqeabG2VZTjgYB9aspGdg A9OnaiLktBx3HJGFB44z1xViNCbgF
VO0DAz2poBOAAcdqtqTvUMM5AB7Uc0ktyLvcUJ83TcAOcVM qAzEnK46Aninhf3hO3p39akZN8
vPyj1p01Lc0i9fUaqkZ6lieBUyBgpyj+1SxodqrjBweT3qVIiQ OowepqrX1sZyiQ4byk2jnHc
U5YlLtvGT2IqYxsYiFOSDg0oRliA4PPPrTXky3tqRIMlh2xT3Rtqc YGOM09FDOF6Lg80/6AkA
49aOV7ia1uV9u9tv3mI5pAjE/N0I5qwvysTlVOeQe9GwBiOpxnFG2iJc tdNCHywGIJGDxkd6Z
tZQWC7WHBH1qyCAjAcjOeabyMbs5I5JrJxaditdSEqdoUDH071GinkjDDIz kdasBVY/KrA4OM
96Ty+VxnC9MGr5rKxMo2SZAIyMgDqeSe1OdSBwuQDwKslQfYAdD1pgjxyTwOAK FZopR01Kyod
58wbe3P8AOoWAVioBGF5Iq+yhYMN14yfemsmC3HOaIvleocz3K+C2FI4zmnYJjAIz /M1KEIVe
Rzxn0p6ALJubOBkCqnZvTUGVwu1WDKQM+vNIFO8hmG32FW3BHJwf6UhwULdeelLmsiYu T6EeB
nIDA+hFCBstg/NiplT5iMb365BqTO2MkDDg5YU4zsu4KWpCBzywGCM5qUL83IBUDjFOdV3D 5h
83r2qMKu9s8jtg0KSlqx8rlqgKl5QUX5TyM1HtGWPAOec1OMoMZIA6AdqaQoRjz9aLvoK1tyP5
JeGwvFQuqqgIP3jx7VPgHAPOB2ppTn5iMKck4qnZE31K7Rgjbn5aidh5RIyGLVaPKjjqQTgVA4
GT gYPai19Ane5WkwUO3GSvPvVUjEOHDZP3jVzcykEKHGeRUEjP8oIzlc5os27Ar7dDPlUBDyT
gd6qh Q4DE5GOlaLKpBBbnghu2KpOGbG3AwcYx1pabLc1im42RVZQMAfMQccVRdWBPB4/WtHcC
zYwMYxkV UcNhgSFPv1NRJyi1clU0tDPkj+QsoxjoRVKUtvywAbPKitBowoIfIBHFQSKu7JUlg
vBFVfTVaj52 ml0KZA8rHUkVWdNx4OCSO1XnRwm47QPYVHngYUgjgn6UczW24Nte8iu0P7/ZnO
By3bNI5bdtVtuD gD2qVl3KnUk54B5qEI7H5R8o4IPWq1t7zH5takYBUMAQ3PIApuTsG5SOMmp
2iCsSScDrTCcp9OmK 0g7aolct9iONgQoZSM8g4psgCjG8D0OamyA2DgAdD601gwTGwnjk1Upa
3JUnFpWKyjM2Mjj0HFQs NpUNkGrMqb4s459qrEMgALDnnkVKSfW7E3e8iGQ5Dbhxk45qkcM+5
RkYzzVxtxJBxVSRvLj27eB2 xzTUWjVL3fM5bdu5cnGOmasjBb5Qc/xH0qoqhQyuQ4H3CKsJIq
hVJwT61PNFrTc5X8PmTqHDFj8w xxjvUy/eUkblzVbcywkEgNnj3qdJGdQw2/THH1ocnbUtO9r
lsMoBKgc9DntU0aYLfOOeTxVdFVnC qdwPX2q2VUZ/hI6AnNSk0xNJK1ydAqbSRxyOKmjznAO5
umPSqy8sMg7Satp/ruy55zitG0tLjUep MnDHnBJwc1aViCxwuMDjFVwvPykEnnpVmI/MOMA9c
9jUTit3qhuS5kmizHubO4AAHGMVJ5W6bI4x 1zTVA8zkkDA6dc1PuYBduGDdRilG0dEaJuOqWp
biIZUyMnGAR3FWlCrKMc5HNQxJt5bk9QasY3Ho cHr60Kd2Qvi2FRMLnc3XkZqwi55JyAM9OlM
wwwCNzZxVpcKi5BII5NJspxaIwGCAhSvzCkO4NIcY GR19anZQ0eAxGeQKiAwzNznPSmttUSo6
7jDhkI54HNR5CDBU4PerI3BMHBweOKMA4LY9MGlJ8ysy tnsVuSS33jjj0pVA8oZBJ6ilB2DC4
INOB/eqTzgHJ7VlPRWG4ybsNwwXJztqVI1eQnngc+9NJY8h SwUZwKmjBUHaDu6Gm9tB8slG1h
8cYAVlIPHI9KsqpUsRjnqPSkAywBG0Hmp40Y4YDA60r31Y7skT HIPHNWSmTnBOaIugAUk44Pr
VgKCRnuOT2FF1zaISs2EasEAYc/xfWrKAZGcHmnImX5yVx2qcLjrg 8gCtZVL6Gb3uMjAMmATk
dasAMFORwMjNKoDBmA4AxkCngcKAwx1JxUX1vsauOl0AjcL1JJ9KFG0Y bIXvkVJltz5I+Xoe2
KeD8/zY2gdxQrtXHPoQhQWODz2o34l+bgjqcdak48wgKeRUY5yx6elVGCl8 RUrdxMKzNn74pp
B5ZSCdv+RUu1g7BsEY5I44p4iw5wGOzGMd6TcY7shuKGbRtweh6cUzZuBxk461 OVBUNyAOcUg
VA+TwGHOegqVKz0E3G2pA0Z8xSMgEYBpmwGIBQwYnAFWvLyowR14B707YWkDKBn17 VV0yeezv
cg8vJUhcr60YViQcepx2qTBE2F+YL05qdkUpv4B6ips2y29SiYmfaoOR700pubGCSPvE VcyNm
Rgtn8hQyhWyMHjPHen72w9ehRUMp4AGBzTtpJZccfSrZ2luAWyM8DrUQD4JOeveq3VxWSaZ Bz
zuXk80pUByGyAeKsnHA3AknnjimOiqAyEhQO/NRe+yFomRB9qrtBye9OC7lc8nJAFLwpC53ev1
qVscgpgZ7etVDcmWm5HgkHLKoPHIpiqozk5Iz0p2wZY88nIBpM7fw4OKculgcLIUlQvUMM8Uxs
bQ B0J6UhHXdxj7tHCt8x68jntUR0eg9JdAfaCSDjB/OmMOuDkdzUoG5GJI44qDJDY4xyTQr7d
Qcbaj PmKEH2PSmO373joD1p5kyDzk9uKiOdhGCxYZAHatNiZJW0KsknynYnUHr2qEhfKcsSTg
Cp8DhQck dRUDhxk4BBIzipUo27AovRFaR1wV2NtJ49qruy4wePWrxUkglehPJPBqiVLMpdG+b
7px6U5W0sSr NWuVSueR8ueSTVaVSJDjGR2PXFWn+8wY8fyqq4ypK5OTyc9qvlu9TXZ3KjKTzz
z0qu4ZigVSMAZP rVp0G9gAwBxwarMWG0cHA/GsrXbS/EpQbTdyuyHYp2tg9vaoVAOBwT3qSQM
yr1X09qjbakxIBAzx n9avl7mKg2vQYygTIRkgdcetN2/OCGUDrjH60/c+8k4BPI96coACse+d
wqm31HV5lYjZSWzncoFQ uA+8N8oB6nip+fLZsgI3AJpjnEm3cuTgn2qU+wlK1m27orMCFUBSx
x+VNbMkZDvhQw6cVZbAcAq3 3aikAyApHyjn1NUpN6JWDmTXvIrOjAMVO4KePeo5DuAyMjPQda
mLAqSoJBODz+tVmz5WBx6moa7h K60KrszHgcjrVd0weeHHUE9amcMMAAnjPFR7gG3OTuz3rdt
pJi5mk9NDjY1Ckclhk4qXc2NyqDt9 qrhpRMWG0fUVKoLBN5Kk4wKiCVtWh6W1ZIrSyFQw9uB6
1cVNiYPHTr61WCDBJfkn5cfrVtGVsDna RkUpaPRFqXRLQtRYCAMRz3FTRsr8569/Wq4XbsBwy
Afw9asqMOjDHoBRrvuQ5R5diYKckndhTxir kYJfBPbA9qpBf3ipuIx6981eiwG69u9DslqSk7
E8ZXcSf4atxIoB5OOvTvVVQQdv97HarUZ+RgCB kd6Tu9i7K9rFkDKqGJyeQfWrUa4b1I6jv9a
rK24qehA/KrW3d1ID4AIHem22yox0szSiI81iFBIG M468VYRfnXBHIyQfWqUHAByx46DrV5cG
3yMdce9Q6coshLWxajUAZHJx6Ui8rypIJp4+VVLNjIwf rSlHVSgOQeh9Kuj5lJ20FH3nIxnPH
HQU+OMAgbx170mweuOak2bHBYnJPWsp2u9SddiCQAyqOCQM getRyBW4UFcdfxq2RkhsDHqKjc
BztIyM8047JNGvw6opjG3aMEr0p467SAwJ6etTCNR04zR067Sf aiatr1BSSV+hCissnB6jlT1
xVuLcVCYBJ6eppoXBUgdanjC5zv29xU3VtiZrm1QqowYHGOO4zVmM MiNkYAIH1qOMLvwWOSep
q2MK23IDDgZp8y2aKtZEkZBwRkgdhU678BAvPeo1Q7hhGUnnHtWiqsij GDgUKTWwJxWlhY0Uq
ufl9asKijhQTzknPFRhTjnk+1Tx8NnaQQO1G+7EpNdAC4fKjqeRTzy7FlAz 0FSMoDhiflJ5FI
yoZFzk4HUVSSkwbjfmeg4YECqy+5NOIBU4GfWgKwiOG3YNOLED5lJ4OQDUcqWy CPLuiLYdvA2
+uaCoD9Rn0xUm7jHQ459qYUYliGUn2PJptoq4m1TgnJLdvSpNrA5UbgBg4pyYwO2R wTQEGeG3
AHHB70tGzPnSViHJdQudqmjywAOSeOD2qUtiYkqRxT02lVyeAMe5qrS3aNZaLyGKpEee G7ChE
lckMdo7YFTortIG424x0p4HBA7fr7VHMldEvV6WINiqAARuxz7+9KBxlVJI7VYSIPg9Oxpw T5
SF7HiiTRL1ZXEZZCSoUHpTNjM53AD+oq0uPKAz17UeUcdOn6ZpN2dmVJJPVFOOPapY7d2etMZN
x6g55NWyCpyB8gqJl3AsMD1HcVUVuwk9SuF8sZUA7h82e1MJAZQG3ADk4q75bmMg7QD047VWZd
pb ONhPFQnd2QWTYwEmMFgHOeMDt60jH5G2jvnHvTmXLjnGeDjvRsweOctiqtswirDQDu+cgHr
UDKPM IU85qUgrGAw7kdaXaSm4FQT7d6ak4vUhy1uRKocc5ODUe07sEEkH5cmpTGynIdeOuPWm
kqOW6ZOM VUXqW72uMDsuflBye3Smg5U9h796kC/IvB55qJicjAx6ZqHHrbUl6sj2YZm3A/3eK
h+UPnIyM5NT uGB7HsDjrUL8IitsPckU0iZOxXZ/vBRlgM8dSarNllBww4yee9Wn3NI23A4yOK
idiJFBHUdaFo9E XIqtln28mPqaqPvxkk+w7c1eGwyDaedvIzUMoIySc5I6DpTcrbg5JOyM9lG
7aMbcYyahZG3ZwVxV mYMQfmG44NVnL7MZ4GOPem17oc7lqyo4PlhmPJPTvUG0AvjnuAetWJFb
eCQWAOKrudr5b5cjj6UO zVjRp21e5AyDb5jbunK1FIC8KnGORnI6VZO10DFiD2FRE7l2nqR0q
rS00MG0upVIyhZM5AOc0pGY 9+04IyamG0M3XpyMd6Yy/uxgNucDPoKTTk7GvM7bkW0AxlsgDj
BNI4ILYVSfUjpVoRAHghiOlMyN r7sY5FVdOWphKzd2yg24yE5zkYxUbxnJIzx0NWnQhAcgnHG
B1FNO4xkk4XGF9aG23p0HzRVuUqMC FIGM9yRxWczvgkD25H51fdWWQ5yQx6elQOVKkAAkrkjH
elGV9C9tTPJJU/KzkH17VCwBRhkjPTNX MAnBBGRyAarSZ2hgQMcEYpxbb1By10RxAR2DHaw2n
vU4wEDclx0FRZPnbVHB65NSr/q/mwSPSnaa 3WhnGMpOxIkYcqSSM9BVpOAAgyqnGfWokRTJye
/SrUaFM9CSc1LlZ2RUn/Kx6D5k3E46H1q2QTMv zDnt6UyPY0jZHfr6U9UbILevH0rSTbI5dUi
wIxuDHd+BqyobrjPNQxbVcjdk+/NTICrBdrgE5JJr K8upspa7k6glsAkHHFXUXDIQM4B/Gq0b
KNvRmHJx9atBQNrBjgHOParcrWZLm5aNXLShCFYkg55A q6FGdzHn+9VWMANuySAMgetXI+Y8k
YO4Yz6U7q17ii2mWkAVVbgn1FX0j4Udx6iqaj7gHJ3VeiBc 5OSOq4qJau6HfXUsIw2gn5yemO
lWUzk4Xgd6gj4QYUdenpVgYJYsTszwB1py2JaSe4w/cGfv/Snq uSpY8gZ5HWpAAyBipCA85PW
m8Ky7eR79qhtWdhJdLDFUsxIZcdcYoCnBJUkAcU/bmQtwVHXbQwBD Ku7pwM1Tir6s0TuiLAJw
x6DnFRNwTwc9qsBMQvkEnIwaTaRJkjGBSja9nqJct9AHysw4J7HHFSIm HHTBHFNXAycZOOpNW
YwrsgByMZIrOTsrlNW+YRhUm5G8Hgn3qxg7iDgcYyajYbQchWXqNvepskjb jt8opxSdmPVsmj
JyAWB9eOlaKKTlgRgcEVSjGcHaScfSr6qCPQEZNXNx6E3k1ZE4BXBYZHQU7LC4 BYZBPUUiqRG
AgOc5INToEIGckg4UZ6VKskNNpa7j2GSBjjGQaYNocEhvb3pyZUYLAH3puO+B6del LZgiTIEn
Q7j2NJ87yMT0xmmgsOh4A5yOTTgrErjOP50k3uUoqIMGRiw+6Rkk01sja5xu64p0mTEQ 3AzTC
6tHySCOo9adkyYRfVCnJ6nnPrT14kBBAznioYwc7gc46A1KhJcswBXODijW+5vbSyQ/hijb Xb
Pp2qVVTzM+vWmqpUZ3LjPSpgqFxjj+9k03boZbxtIMLtAyQc+tSIAYzn5cdz3pCwLYHJ7Yp4AL
bW4XPPNZuPViau0mRgMsxKsGHoKmCEkYU8ZIHpT9pU7sYXPFNAdZyVILMc/hVKTa3LsmtBCpOF
AA 9OKWVBv2jPIyTmpI4280biAR2oCKfvE49aUY31bM07S1ICi8ZyQOvNQ7EycffPUE9KsnAfB
ySTz7 VGUVXbB3MT070WaepSVr3ZWdWCbGJwOgqJjgJ3HHFWnDOw5xyOMUwqMtjHDdT60NpbkS
baRX24wQ jYJORTXIwoIwR0+vensjEAjPShozgFCpwDmnyxHBJdSAoG+YZb0GajcELjcAMdKsY
/dsCME4Ix2p N2YBjacdeOarm122G073IQqrGzFWwegJpjBSwYKcE8CrBi/dAksuB0NDLk9cY6
ij3G7CUnzXuVDu EgBztHTimEDByxBPSrRGXwrDH8xVXYJGAPU84pytYaW5HJneMsCOPzqFhuY
p1OcD6VaaLGOMAdjT HCBgWIBApSfREN32Kflny8HOcYB71DJEzHjqBz9atOPnznGDnk9KgYP5
Xy/Mfapemty48zXmUplI A2qzNjGRVd1eNRvUirrABn6g8d6pS7i+SSf5E04663G7oqspI3EHn
mqzFVG4DGD3qydwbaPSoJSu SvGT2xVySJnLVXKxJLKvzZ5JNV2A8vjax28DFWJOm3BJPTFV8f
vOeBSslcE7FfgpknbjtSAF5c4y CPpxVna2SdoIz3FRHaGyM5zx70k09A50lfYiKBo/pwDTQCG
yvOGxx2qQ7NuQrZDcinMP3jHGMnkV TSSVyXKzSRE2M7h1PXHemeWrAF+GI4FSbQhAOPahm4x0
JPGe1OVraAo2d7laSNsIy8YHQiq0iHLs WAK4wParxU8gsT61CwBWQMM/Lx70SlqiXOy5Uigy7
pGLbj0JPaqjhWkbI2kdavHeocYx04NUyjA8 nODyfU04xS2ZTs7vqUnwp3AgfhVaQFXzjapGST
V+URhlGeRnI9OKpH51XJ4C8+1XGTetxKz1OGRH XcDx9asx7gCQBuxk8UkSs7Efe+lWYlZRtI3
Ang+tSut3sZ36WEVC0W98KCQRjirKFgQMlhu5zTNp Jz2z8q5596sRxEfJyxJyOcmlLk5WaWtH
XYcrMJNvUHjA7VaUoyqSTlSAfc1GigTjIx681ZQASKcZ BGcinFWjYVknZDgpzvZhjdgECrfBA
ywztzjHeokUgbsrgcnI71IpJVeV3d+KUWzRt3LUWBGDxuPT /CrKhmlIJGR6DqKgQgkbcZA54q
ypDMrEgHGc1lZpkJ66Muop2oCQCO/Y1cQfKpPfpz2qpGcBQCNn bPfNaMIUJn5S49appvcce7L
MKhSGHIJAq3GoTGc/n0qpHxKp6gnIFX41LMGOMHnFJvuOTV7lvomF GQCSakKkrljnIzxUXcbT
nd2FS4G1T/CB07inTTRCg1G6Q4DCZVlYDOOKcf3sYIHJ75oiXfvDEKM8 U4K2xQqkjtVcqva+o
S27jUTG49e4pxUKvuRUyjLnbkAg5yKaDlScZGCOnNZ2uaJ63IGTjklh/CV7 077rDb1I5BpzKT
EqnjIwKbtZVGeDuwM85qVDogatuAWNwDnHHSnLgElTtBPODTiCGXJUIO+KcQuM cHb7VUlbR7C
+1zE6BSGGak27skZHGefamAEFdoABGcVJEH5LE4PAzUpMaSaumWYlZpc89KuqSpxg A9PpVeKN
hGfmJz6VOi5JwNpB6k048vQNd2WflKhs8qe3enqh3lhwe5ojAEahirDHHvQFY4YHK45q k7PQN
3qSMVyCoJbvzQ5yFDEKew70oAIBIPB4pCSc5HIPU0nFNXQknsMD4O1OWxRhiAd4JHtSqB5i j2
60joAcp1+vWqtdGrS5rCgk5JwfrSkArzgE0iqQ5I5AGKcuHwOgrFtqW4SsnoMUjzAVOTnpUyqG
JwCPX61EmFbI5yOanVVZCyMc5zyf0qpRd7kys3cVIyRyGGeetTlcgEYz1I9qSPlQ3HTgCpQPl4
wW 6fhQpS26GbkubXoL1YN1xx0p2wALngE8jv8AWm/MHx3qwqq0ZKZLdDROTXU21vZAflwFcNg
88dKV hsdWxkbevvSbEaIspJYHOD3FO/e+YMKOnSiLu9EDbuG0GNmw27sD3poG3g5IzkAU8OPL
Kty9O2bc EDBxnk1V7XQa21GsmSrE4JPP0qDy/wB4SMjHUntU+zd1O0DjOaTYFLMx9MUrPl3Mp
S7MpucDLDq2 aaykJkcgmp9mVbODgcH2qEAhMA9+/an00NY2IAJGyCpOc5x2oZWBOVJ+Xip1yM
jIBPrTWLAgsCeM ZqVo7ESqW2RXwfLz91elRONuBjIq0yfNkg8DpTCAxUbWHGDRfUV0lcjHCjq
fQGmhOehJY1JJuWba vX0IzTdpI3Ecn+VDWgOOt9rkBB5JGDjBqLBRzgEsB1q2R8nIO4+1NbJb
aCuc8gindsV1LS5TzkkS McgVE8fI+Uk9B6GrLIxkZuFxycimFQYyRnccY5q7NLRjjZ7FNlLFt
q4GOaruDtUc5A4Aq0V4JAY5 zxUcnyxqXAXAqLaA+XoUXHOTgMB0qjJkSABgcDpjmtGQ5XcF5I
wapgNuycZHBJq1dxLc5LqUmH38 kBic1E5LNkgA1cypUsu0MQeTVNwBGScnPIqJxu1czTUpabl
BlIkKkcN055qHC72KqST2PNWySGJ6 ZHGRUJQEHBye+PSteW6syp9rkZHyg7wdvHHeowu5VB+X
6npUhIJxyF7+xpm3EuQS6Zx71PK72bJj OVmhNuEO5gR600xksfm3dKlUAsVwCucj3oCBo22sR
g0c043JipRd0MCoVBAbKnp61CoBlJ6kt0qx 5alzgkY5+tMkTanyDJ3ZOPXFOD08hyV3vqViWD
MMADtUAAMbnDcNzV4gBFU/ewMmq7HAYdM0SfMy HorWKTJukZ+QO/1qlInPJBbqeKuurByM4H8
6glRfMwW249aXO47Mr2fZmcY/nbcpJbuKptxPg4K4 xj3rSk3IWwQwHT2qhIrlCRg554HSrU0x
p6WbOTRcKu0jp2p4R3dQWGcZwBjFNRW81t3HHIFW/LO1 WBz+FVCavqzBSEKuBlyBsOOPepVGE
B3HjgGlBA2ZXI6kZ61MFOSI+M8gHtUxkn0L1vqCLgjcCy9R irMbbhnAwTUfV1Gff6VMgJjOSM
54IpuF1sXZrccBhsn0xU6hWCtnPHWmqNxIALHpx3qVFAAUgg9R T0tpuVa7J9gRMqu7ng1cjBf
kId+MYqBcjb0VcdxVpEODIWAHQYqOawrW1ZcjRvKAPQfdJFXIlKx9 MYPaq0R/dBScqOM/yq6u
4qQDjP3ie1SnKXkTF9WW4wjELnHynbmrUKu6Zx04OPWqgTJ3dyOFFWEY jOcnHUDvRGBV7rQux
qI5h1IFSnIQHI46+hqDkhSowD1zUysFbOM9unQ1ST1ZKvfclU7HyTtB7GpT kP8AKcHGc9qiLo
8Y5B569Kk+UF9z5AXFZ6PdGkeZvTQAoZSQ3OOMVKoHA+YZ96jXbs4buB0p+AFT HT1zV811a5k
m0mMdVDKc5YcAf1pz7gABjOeR60qjdIWOGbOOBUgHO0927dai7RbnZLuQqp8ojg5P GR0qRVzk
cYByPWgrmRuRweacOH9QAMkUpO6Ls2rdRyZaQEHoOKurlmxjKkn8KrxjlvlYgAbWHcVO AQ27J
UEZxUyV3oQ4q+hOmAo6lhxyasCNkbkbh0z6VXADMVGMjn61bRSF+6SDxgdacUoaopavUkVf lD
EEAHGKtqPkLbhtHU4qFVygKhgw65OalQ7WOcFcYx6Vo5NqyCK0EB+Qb+mc8Gnkcnn60KMLuJVu
OlPADkAg5xSaT6FNkZjxFuORnktTGyd+CBg8Va2g5zzjpzUAYBSpPKjAx3rNNvVgm5q1iMbg+5
iM GnjY0nJxx60vCg7uv8vakCZBCg9eM9vam2xqKd7jesgUY+tSqm0gZKkN/OmquJV5+cE1MQS
MBWIB 5x3o5ul7FbpxFO8QFRjcDgEDFTAd8980i8QscHr370oYttIwfUY6VUddzLWyViQEuwcL
x0x71MIg sg3NhvQVAMEDBBqYN+93MpB6VN0ti32RIVO9FXovBNOZk8rvjOajDHL4VmPfnpQAo
CyNknqc0Nu+ hDTtqA2vIxHy9+e9SM7PGGQY9vaoWc7vkA2njHpT2J4weh4IpN6mjSVhCwNtzk
c8imhwW5yaeXV0 wfvHgimAgwbOMnnPpSdiXqtRjfMSNw44ppXBIII4AP1p3zFCSm0n1poAdwV
JIHv1pxTfUUpRURjf MgGCec5HpTOOOScHpUw+U9Rnp9aiI+bB45qvIhNNJiEjzCOCc81XIJGS
cKDxVlgmCCGwTkmmEHYT jgnkGpSHpazI2PAZjls5pnU4XIyM89qlbaoye3603PzBR6Von0BbX
IzGVjB3fMcd6a6Ig3KCSelS KzZG5lJ9AKYzPvIUA49qmfNcabvZkRVmPrxVQgnGTsA447fWrW
87sdW9B6VC5UA9s9c1pG6bVimn flRXO7zc8kAdKjdQ/BwQRwakKbpAobnvzUZyCNvPH5Vb1ZF
7PzKpxxuBHWqMwMaHaR16H0q6yZbh sZHWqskZLbWHzMRg1DXW44pLR9ShKm0jZjOOAPTvUGxV
XGc8cZPNaJUeaQB8o79aqsuCx2nJOR/9 al7Rtb6jnJJ2RU2ZTpxVTb5bHGSxIAJq6cknquByP
eoTBlGG4jnijp7zMltuVCoZs45BxkHFMZSk m3ORjmrAjw4Qk8rwfemiP5sSAkY4x60pWS3NFN
dWRk7uCNvYkDpQFMbcuNwbn3pxXClgSQH55qRg JFXbg85xRaL3FvsiJRuGQcDviggAHoV60rB
vNwcbc9vamlDjCnIIyfarml1JnFt3RWLF5gwUehpr Iw+7gZ7kVO+PLHzDcBjAqEqWAUsQuKUo
p67B73QpNgxnI46E1TYARu4+bB6E81fZFIAzuJ549qqS EFCF7cHjpVJLboS+YzphnG4jBPQCq
sh4I5znn/CtB9rjGMcdfWqZCGFjyR156k0TjJx2L9pFnGxq fMyh3DoWzmr6HcQQQFAwR3Bquh
CgkJtbPSrCcHOzAPIFOfvPbQlwdlbQWPltpzkDPPerQ2uQO55x moFyowV+dhj6CrMZBTGVz6i
m77obTTv1HoFHyqMnv3qUR9DuBA6rimffjXt1JPSpVQqc57c81DXN G97DSVrtEyMQqEkKu3nj
vUyguQeD6moE6fPkZXFW40AT7/A7DvUyShHmsTzpeoLndllZgep9Kuxf fycBTxj1qJcCIY6Ed
KtoFKHbhmBHAosmjWUls0WQAYuAQc4q8v8Aqxz04J/CqcI2yNuGVbnirMfz L8wwrGny+RC3Vn
oWoyAkec/1FWdzcEDI6Zx0quNm8YVsgY61ZjdmBjYqFz6cmla3vA9XsWBtaPqd 3bmgEp83U1G
pAZeQewFKchGJKjFXzW6jhF7E4O0DcAR6YpxfdI+evrUIztyD9OetPjIYnOBjrxWc 5ajhuy4j
qVPTcKC/Izg8c4qsjEYHr1yKnj3CbgruPXNTFxTuE421uSGTbGxOcZwR0pQ7bgzfIoGe e9MRC
7fMAFJ5Bp7YRxhhwMYNXzK7FzReg7ftckc5OPrTxnhR/EC2KRM8Zwdwz0xUyBAoAB3j37VL 5V
uipTfzHxgkK2GxjHWrKLulYMCcjiohyAEBwD1qZQwAOwnI5GeRSbbegJ29SzHtwOhHtU652lhk
A8CoUVi+1Qx4GKsxAlCjL3wKm7QNO5KNyE4yccE1MBhF4OGODxzSLtzhfQk89TU6nCrlcccE0+
Zl uOl7CkDYAuOOB60zHzgsCMDGalx3GOCOaNoJ6d+lUpaWIWzGlQ2WRgcDr6VAIv3zn7pz909
asNhV AAw3tSEjILAsfSqvJBzS2uQgLyGbBznrSgENkfTmnhwWOFGW9qMZbcSOOwqeWW7NNle4
nzHqvfqK lBwSCc5NICVQkkFhxSkAkEDnpUu7Rj1JFEag4ySQD1oJONvyqTzigY243DGfTrSg5
5OA3ao6XLjo rpEgwgCPtIJzkdqewPOc7ycDPtURIJyMA+lAJ2A9fTmjVoTpteQ9tyq2Dg/zpU
IOM8nv6Co2bCqN wIHWmFsSkLycVbV1YqK5r3JgMHJ4BPp1pWkUbcMMDrxUCsW65GKU/KNhIPv
ik4rqDi21Zkm8M5bg c0ib/M3ZGMYxTVwEw2cdqeWOMEZHqO1K6UtBuTfxEZb5iSdvfB70hJ3c
MOOOO9KQpTK85PNLwW2j gHkE0732I0TuxXOFUNgjPaowuG9QelBY7SFBLHoKZLuZRs3Z6U1Cx
UJ3W+4b2LfMDjPHsKc2S3yq eOtKuSfmUjaMGmM4XLhjz0FK6bIabdyPq3HU8nNBC4BwST6dqX
n5W9RSbcgNjJIwMHGKvbUf2tQY plQDxVc56qeTxxU7ECPbtwRxn1qAqWIw23mnF9w212GFSkY
cDJ796iZg7AYI7kVLkrGCxJOcYNVn G2HGCSD97NKMXJtsnrfqRsjLKCpGMdahYAZ67/6VZcFz
klRxjmomAyOo9CD1q4yktFuaOStoVZFz GTg9MjHpVcjMmCSvy4BNWWO5sjdjoQDVeQMMEqxI4
Jq0/PYmWjViswKqRkMw64qiwyuOcZ4z3q8c FsqGI/izUEqKHXnk9ABWMnrsTblVmUjtV+Qy54
PNV2V/KbBHtVtwTKOhUHHAqux2uu4jGeKuyd+4 4SkVfmyoztz1zUhB8wc4UDNPZVKlgcYH5VA
MqMhs/XuKHZ2Ha6GFS0yqxAXGSR3NIwJlBQYOMmpZ FJxnpmkUjadoOQehqua2qKsnoiMKqRtu
3EgcEGmFxJz82QMHFTgATkHJ7gE9qCdqA4C4PTFDehjz cujK2Ar9Dnjj1qOUhRgnqRyKsSAZI
zyevtUDpiQK/OSM1EprqDty6FcqfNyD7/SqEjYkPOT0IFXZ AVZtp5J69uKrv5bFixPJ5x61p0
7jgktbGcyAuD0x0xVZowzlOq+1XziOTP3htwRjoaqSkHkcYGM1 HLK4ubojkPvFSdoBOSas7AE
G0hhnINVQyliuGzjqDxU68lAXA44AFaJPe47qysTocN84wvYmpgBt +XrVc5zgncegx61OmTwS
OmD9aSuVyJa3LKKWVWI9gRUirhMkr1PB54qJcLgAE+ozTxh2ySAc0byu LVq0noSInO7D8HFXc
qrKuDuyfxqvgg4JHJ4xUi/NIm48jk570NXd29hP3tyyhOzcAF6EAjpV2I5l VhwCpJB61VjIyM
9CfzFXEJyWGBk4A9KzS966QnGz9S4p3Jwd3Ye1PQlY2DfezkVXjOG5DAjjd2q2 CDyPmboT2NU
48m5TdtCZJMKWB3ZGPpUgXMI2MRjjr1NV4353KoAqRcsSobGTnPvUweumg7u9yzuV IVJznNPV
wR94bD0JFVSdu0EhlBwaeJPkZgv0HYVnpzXKUmuhYXJlYKSBjIzUqs3mKmVAI5qgJDuL Drjmn
7jhXJwCPTkVcXd2Bxv1NNJCi4Cgj37UAsxP971FUxk42tz2FWQcsDnaaizT0Iah0ZaB3oWd WJ
xnIPFSId4XYhZsc+xzVRSuOjYX361ZR/3gI4B9KqT8tRtcsdUWN37wg/fLZAP8qnwS2SSv4VWU
rHPl+hGAT0qwHYkuMYGOKrXroVzNapElvn5vfnPrVwN0KnnuPSq6BQ4Byccn2q0CPlLcA5/Cp5
rv QiW5OobOQTz29KtRYHBP3ujZqCLDtlQ2QOM1MgOz5x0PGKe+5SldpFsBTtwCOevapgmcr19
ahjIZ eFbC9h3qbpJ0OSPzqW+g1fqxBncV56/ypwIJUknOaQGTaQVBJ6cUuMR5wOOM4qXbqH2h
8h/djODg 4x3pmSX6hR6kcUobcSCp68e9DcnJ4wO9XDXQE9NXqMCFSem007arjKsowOlCnEXBy
e3eg7SQccjj IqZKT6h7smOHC4PBPOaRi20fIVGOtPx8qgkHFBVi2T09qmLSdw5etyJC4TcQcZ
qYAFjgZPUUDqVw fpSjIO3+8OvrRJ3Fa6GthGVQNxx1FNb50IB255Apejlt2frScoMDBOemKd0
hpW1AYCjcDzySaXHz LkjIprPztJwAOKRQTk8nPr3pNuw3orpbgN3mfMOvHBoBfcegUHAzRk7t
vUA8HFRKWyQScn1quYnV u5M0jgHBBPXpTEOF+Y9cjFMwBwTlh70pY5zlcAdMUou1mSmuxIvTq
SM9u1OWQZIYYOOCahVwEzkf 7VIXDBmAyQcGm3HZlpq6uWc5XaSBgdqrlmLDaQe5UCm4IkHzA8
c0xxhmBb5s8e1NJb7mkZa+RYZm DYztOO9J95sFlA+lRo+Y8MAcjJ9qRc7gwzjHc9KOVcphysn
C4TBIJ6cVH0bdznpjNGcNjB/wppKs oKsAwoW4SWgz5wzEnoKbzuwSAByD9aduycAn+lNwfIO5
TnGAattpotJ7MhcsPQ9jx0ph3NCM7Tg4 HvTmHOPmI9B2NM+QykANjHek3Ym12J8ipuY5YdR61
XOCQcgDv7VY6jrtB65qu+ORxzyKT9NQd9iB yGZhk5A4A9ar7vMxnltozU7DJ3Y4Ze3aoSAIsj
vxmqjazuVFt6FR2YSbVBIIzxUBVjggexBFWzgI eRnHBqrsJhUcjPOfWiTe6DmuyqyEKxbJOeB
3qFsHB2MMHknuasOSM5Ukg4TmoWGGIySOhyetOXMt yNempWPO5cYBppXC7QOVP5VI3DAAdev4
U1wGHOVLfeOajV69DRu6sR43HGfb6UwoVcZBwB1FThcM No3etRF3MhJVuTz9K0jonZkp6asGJ
6gD61Ccs5DKT6e1WAGw2VyQeKaWGSp6gVN0tLApLsVnBZkI IL4IpoX5NxYFyp21MQS3GMdc1A
WLnHAYfnSvpfYznC+xA5xGobBJGRxVR24G5So7/Wr5Q7SrMAcd COao/el28HgHPrTg9BK1io4
VmByCx5J7VUkVckHOPbvV9ioUjgLyBntVNvv4PHcVd3fmKsrHCs4A JzgZ/WrMeUAYENniqabV
y+dw6gVZQltwyFOKpT1aREoO1uxZGS2cFsnp6VaHXd2FV0w0KnOCDzip kxkgnPpUxipFddSwv
LbtrKNufxqwpRh3BPP1NV494Qcg/hUi4Dlic44wDTabauO/ctIMoRjkdR70 DktuBJ2kU2Mkjd
jaAck+9ShxtJ2nGaiKd9iabd9CaD5XxlSFOBmrkZUzAnIxyeaqJgK2whcc5Izi rS+WyI/8Q4I
FTK6Zc1zKz3L4KKp3MuC1OQpuPPHb1NVlEXKHnHfPalVUMm4E+mKrlb6kq0VroW1K hyACMjgV
JvZSMkZA9Kq4c5JcZB5x3qfefLBGCp7YqOZCV97DkcEAPkjrkU7IIJw3QnGagLjACgk9 8elER
z8vTHB5qancqK5ndE8TAncrY453U9STMy9eeKqqymTAbkA4GamRmEgUZ6c+1S79zVxu9S+j EA
MNp4yTUyliqsDwRVRGJj3YwmfwqxG6s+3BCf1pRm9yHpsifB3evParablTIxgjBz2qoGw+CDx3
NTxk4OMkHsa0c+rFzXLgctDtYAkdxU6nJJ45HPtVRFQg5OCe2atg7WPBA71CcFsOL5XoWULMdp
Gc 9cHrVtSTyV9s+lUo5GVCzHHt6VajYSBSSVHrn3rSLb2QSve7LaEqCBkHpmrqBnChuD1qivM
pI5bv zVlM7QxbDZwTUyfUvR77l0ElxnoBjjvSsZQpyQAvbFQopwDv5I4HpVgIOm73+tVFRQnK
2lwDOQu4 HI6gdxT+DyuWA5poOMNuDMBgAVIMNICflUjpWc22tQe1xSR8u0Y96XaQVx827nFRq
ylM5LY6c9KA SGJXp/DSSsKy3Q8d/lxzxgUhQEv/AHjjimCbkECpl4O853N29KPeiXyu2ogwSM
UpyJNrEEegphwT nawPQDNPUuGDHAOeuOtCVtSLu+40BSm3BGCSDSZO7g5OOfanNnftA3c9R0o
OMt8vQYzVPcL3XcEC 9W4PrjioyzHKtnAPBFPCj7rHr71ExypAPvRC8gTtLVXHMGCbmXcN/FRj
7/3sAngUMSyKCTtHPB61 GrfvcnIB9fWlumXyt6Cl8ZCgkg4HvQCGIDDoeTmmM2COmF60ze2d4
6UPVE8rS1JSwK8Yz3JoJ2jI PamEfPnIwByaVFCpkHdgnrTjZLUNtRCyliBkg9hT1wApXHI5qM
AGZmORTcAcjI9OetVdNBKVlZD2 bDdB1496UMG6kEDvQHyvPODzx2pnmDAIHOKSetlqTZtbD94
4BXHbPrTgxZsggKBgjFV925+AQONw PWpMp3DdOvrVyaTVg9nJLUkZcg5fIJ4+lIGb5sLxnGaY
7sQMD5T3xSKxZckMOefShXaHeyuLnEpU jk8k0g3k/eGMU0yKxwMjPIJqHe3lY/DP8qOVvoFlL
R6ExJD5TaOMHioWIDLgjOMU4EBipbf6ketR yKu4NtIbriohDUGmpWSGEkqVZgSOtQORuAU4PY
VK23ZuIYHBzUJx5anBGRyaq73CU2RFzu29s8kd 6rktjpgZqd3BO0YA3Yz61DKWCAYAI5px7Eq
pJ+RHKoVMnG4DkVUkOIQR261MzfvWOC2Tk88VH+7U 53cEcjvTk+hUYpLUiYq0obepxwBVZ2Bc
hlzj0qaTBwwBHQAmoHUMc9+MAGs7q7uJWIcrszg49M00 kH72eRz7VMyAOFHT161C3DgkgjIAq
opJ3KcvdtEQBOmcHd601lRZGwe2BmnkHeTzuBz9aQkSOR1G 0+1PmSepOuxGM5x3HvUL5DtgDI
+YHHWrGQkJbGD71Fu3tvAbrg47U4xdr20HKKWr0IZMiMltxBPH PIpgUKpZc8HP1FPk3FVCklS
agAMjKORj360ON1uRb3dxj/MCWIAJ4XviqzIFlPUMOOv6VYZguAyM AD96q5ZndiQpHUcc+9Uk
76E3sUpN2/bwRnioZVxl3bPGAF71ZfiWQH5cEYPYmqsvzYK9O496HC5b fVs84R2AYbcYOFwOt
XI2yyMM4Ixu9aqLtL71O4dMmrkbjcowvfpTSM38VlqWo/uBSWUE8GrUYVYz 13AcelVY8BWx85
x8oqePJwSu0AHH0qlrpcat1LYHyqADgHjmphjaQeR3xwahRlEOcHOelPicknO3 g4pxTWpa12R
cGQcDC80qsFnJwORxTIySoB6E8kdqmVdvyfKMEjLd6ykv5jO6UtSRPM2nbgKfUVMv yocHAzjJ
FQJ8seSx9zUyNubJ5XPSqjF9Byk2ix0ZmXG3PrVpWROSBheCfU1XQuWZSVUDse9SDIJY gHIII
Hb0rKXM3YrSyvuKwypKZVsjK+tSOSFyGyR1wOlKq/KRnnHBoEacqWJPpnpU86e41JOOpHuV UG
0Zz0yakRgRjODnJHemupOdowMcH3pEG6QgZB6YNNxTSuyNL7kvfK4BHUkVYiO5m3lcD8KhWPLg
kZ9hVlQHKlcHPUd6hJPRG0dOpImAdgJXsQasxjawAGMnkGq6lVnJxnnH0q3zkjPfGarpsRK6aT
ZN jhufoT0p6SNtwe3GcVCqknaCCAM4qYcKwzuz0AoXK99y5rSxbUq8jZbGDkVYT98nU7emapR
D5iww uF6N3q3CwEG0NnDU5abGMrpouZIk6ggdqtRsocgkbQetU+d+TgjHFW1I2ZI4OM02rW7F
u76FtHUZ AxgHHH86tR4xk8gjNUozk/KeM81YXk/Id3baPSlyrqLmaL4KhAnDDqMVIpz8wByD6
1TUsspzjHGB irHKwBjkY5PpSa6I1cLJXe5L0bOQAO38qcSdyjrtPJxUR8tlC5OCMHB704AZXL
EgHnnvT6ahovUc NoZiCMN93NOVy0Q2j5sc0HhBgZOO3egHK4zjNRZ9NQUtbsXy1IOVx2z2Bpx
YiM45IOKZvJbB4GOD mlj5AJbJx0Bq9be8TNO97kjcw5CkECpVKeWrMGJpMAZIIwRSbcBiTkbe
3SpWqNJO6sKANrMemaib 3O0HvmnAtnnOBxStngEgVPK76C5bakIyvzDP3T15pBhgMEBunNLkg
YKlU7saGIVc8Z6gVdnuydLX IwSHbcQRTWcA4bkAce9O6zcDJpSMYbIwexHUVOtx8sXKxGCDtI
AzjPPvTGxtH97HSrG1WTnqTyem KgUDcSwJYHr2qU3dg1Faobt/dkK2455poyJFye3SnbSJSRy
SM8dKMDhwwIwQRWkX3JlorDRsaQBm IGOcGmlSAQvOemKGCbG2LknjOaQBgpGeBjH+FCknqNKy
0Yi/LhS3Q8GpG7kYAHQ0w5CnC5oPmEjI CjuSOKaSbuym2noxQABnrntUaSFUwcjnBzSkgnCk4
xnPpSAkldwHQ9qclbSxMpRa11JztI+U9+/r TWwS3BHPTPeoySytIBkdsUwlSmWbIx196m7urF
RejfQA2JFO0/WnbhkgLgYz7ZpOkQbjpjJ70xiG CjPA5JrR8vVC01Y8bcBdrHb6Hk1GTtbC88c
k9qeCQTg9fX1qNmEfmF1znHA71mnYzbTZE+OVyTnt moWYlAQOg4FSN9/c+cnOMdqhyuOWyQO1
XZNjjdvQY3IC7MN14qFiRIBgZxzUhwW3Bvm9PWoiSykA c5wKlvUbkra7EGQf4TkDFQE4dhlcH
n8KmLDldrKCeCTVZnLYLHg8VTTew4yitRsg+UbTu9PYmo3A DKBj1OKexAcEHPXP0pGZnjGAAo
GCSOtDctmRyIiJXlSDuzzjtSPFkDq2R1HSl5ySwJHTI/lQ27dw VAz0PahSl00HGTjezIG/1QQ
NkmmbFVjyW7A1Lg7MdRnP0qPYCHCkqpbrnpTcrIUX2InG6Tg4cdc9 KEyqjcCpPB96kbAcgr0H
B9aYR+7BwxGM4pSldFtXepAWKv1xlc4qAtnayDDemanckS/dzjp9KiZQ qgMTg+ntVJkVHFWTd
iuQxQn5iOpx2qB8/MAwBY9D1/CpnPyl92Ez09aquTztb/cNJe90DlS1uR5U qM5J7+1Z5YbiGB
9jVuRm5jUZ+YZPr61TZAFDbScngD0pyavYSTtc87ixsQKeec1bGFkAABHXFVVE eGVD1PAHarE
IXzVdcjj5iTVuV7a3Q4zsrXL0ZGwFR0PHtVhRluCc5yQD2qqpyfkOBnknvVuJwpAB DNmjmdrk
pxT0JwvzblBOexqwuSuCAORg1GxYxZACkHrUiBccg8UuZ213CKb1J4wc7WPy+pHWpesi gnAx3
7mqyHMyhmyAMfWrAwYgwHI9KLNOzHLe5MpZiFA2/L0I6VMpfy92Ax74qFEK3GSTyKl2soAG QW
Hc0khtxvYsfwDOemCfepg42luhJxk1AjbSdxUknke9TKA2A3A5ODWeq0Y07bCgsfunJ9RUqFto
wQSSckimxhAxxuAx1oVgZAT8v8PNOydgt0SFCfMRlyoGSe1SAbWUg7gOtOU5+RjjJ/SpG3kHao
Ax 6VEm7ibb0sEe5pl9WP6VPG6rMAOQM9KbhgDgcjgZFS42MQMZzSdt0OmlLRk6KNnbGD1qfLG
PCkHA HBqBAzIvb1qwF4AXk45xTstFcTjrrqSKuCOeCcilCsJFJI46cdRSD7ucccZqQuCOCMk9
xT1T0Qcz Ww/7pJDAsTVoEBME8DgEVAGAO9zyFySasRhfL6gA9aTk1rYuMu5YTaxXBx3ye9Wt2
EUDOD1NVkOY 0Cge9WAm9VVux4NTz2SbBSu0pFuNj5pXv296mVtrj5s8cYqpGnIOcsTwAasoGO
1gvfJ47VpHlLdk X42/ixn6mp9w5YHPYiqiq52v0XH51KuTwpyAMfWhW2M5RVxYx84fIwRwDVl
eFAAzk5qNUBReMEDF OG7ewOQc5z2oqT1Vyp3k7IcMSDAzx+pp5RgrOoBbHI9cVEnXjIXNWTnp
0PbNEk4kt9GRkBsDjjqO 9TZ4Hy9+/eoVADswyTmrC/NtJBA7kdqzfmXJJWHgfJ0Aye/anAMmV
fBBHQd6a2FOSS3vS9cAKWAH WoSuHKnsxrcIOABngnvTAGLDPzZPb0qXAaPAwwz1xSqsm8sF2g
DtWnoKM+UY4zDuOcg4AqJgzDJA Crz05qwpyQGIHHpUZ3BieAOmCKIprcIbtkeDuBxgdeRTScg
579KmGSF/rTHUjAGCR6UpJXHFvqQy sVxgY4wajA3YBOPpUx5cAjnsKT58j5Rj6U0tBOViA/cA
2sB2+lLtJ3K65QnjHepmXbGV5bPSohu2 YY5XOFIq17y2Ji7bETDkAYVccEimlWEYCjJBqT5WB
G7GTwaQ/e8vuO9Zv3XsU5PsNziIE5zmo9+S 3JxnkUuG3AE5UnimswHyMMA9CKtJkpxTIyq/L8
56dzTkILfN8x6AU18bhyO/5Uwbig5ByOaJ2a3N LpbEzPhBheD61EwJwSvy5xigMxIUMoXqCaY
pVW+ZiRjjB604NWtYjl0JQ5CFcj5egIpuAYznAx2P Wm5J3E8YOM0EER5B+b0qWS7XGFxtLc4J
xj0puQckndmlCtyTnHVhTAp2sBjHqa0TQ1roGVMuVJK+ lQyJG+4rzxzg0YAPI4J4NNYDeckjn
J96lysEbt77ERDBVJ2kN7VAygMfmIbPNSSBmjyT3zxUZXA4 OeRyfemtdWJu6K8pG3uSeeDUWc
lQpUkL1x0qRvlySRuP3jUBKHAGAGPrUt23GrdCJmcPuK9BgDFO WQNER8qjOeaR/vZznj0/Wq+
D5oKk4xkVbUXZFOKepIW+YgnHtTCW3HjoKUDdzjnuKQLiQYbBXjBo uuUTmrWGniMdeTk+9MJ4
OMnNSjHkr8pHXgnkVEWO7GKaV9h0+wmNqKW6kdTTONjYJAx1PQU4McKv 3hg5FI20dTgHsam7u
ZXalZlUsSm3BbnOc9qZIpB2McjBqVyQ3TAH3ahYhw5LY+tDjLmukO22hA4A jC5DA8darZzCVU
g4qZ/kUMMEHggVBjO48DPFVLSxMdXcpyttQ8nPt+tVXBK5ByoH61ckC7diqSPX PaqTDGRk4z0
9qLrSxS+E873DDLjkHr61ZBAZTn0yBVNPMSLLkMw68VbQLnewbLeh9KttWeu5MlFL YtxZMf8A
cye9XE27lLEDA7VWAX95huBg/jVhcmXPDLu6+lZX1Ha6si+CGfIzsPr61KhYgchWxjFV k++Cc
jP6VYUETbu2Oc09X1M9k0TbNhwPvZ/Onpy5Cgpz0NMT95yxAAGOalj25JXcwzyaqTdrGu6e pY
yWPXHTBqQAsm7dkZOKQGP+LIAqQhfLYLu3LgVClZp2JtrqIiuYyM5I6mpSVZGL5OBxikBwxUL1
pGJUggZBGD35pNu+ptT31LDBBIGbcvy9/wCVOxmJeCfm5FNAKSGRnVs44IqcDLqVOBu5FHM1pc
mT UXYWJW8ggkZB+WpxH8oIz7/X0pvUlcEYPapVA3kFuOg96h3uKKbTuKu8ybenPBqyPmZecHG
fwFRK p8sgDBLcH1qwEIwTwONpppsG0x6lQOCORkj0qVeUBGTnsOtIEyoUDgdDUu0cc7ferc4/
ChSn1uOU MMhuB6HqKcqgHBJPNCr8ud2cjJPvUrR4iXJ5HXFTKdnbuNSkxVCiFSTu9vSpxgfNn
BBweKhjDNE4 4GSCPcVJgBSCQ+DzSTsF3HZlhJdhK5VgQSMCpkyQobrgc+9VyoI3cg8AAVYi++
RgMq9xSvFJ2RSU Wty4vEn38EDr71bUs6hQSQOpqmoBw4PzA4YZq0GGFVSPT/8AXTitBa6WLPI
UnPBqwDsKFSpK9cVX TOFB5wKlVl8s4GT0U+9PltoNp2SLOTuPoTjpRywPPfrUKu2BgEjOalU/
Jg/LzjFTblKcZfMmxggE fMOTin7v3hAy2T+VIuGVW3bh6ipo1IBLAZxwacprRii0tGhgzgjHO
4U8u+WG3cM/dWl24+Y04ZKc MBkVDs9UFpIcqqFBLKWPYnpinDIbbuyG9KaFyvBUHGfpT0IT5g
u4nsaNWOT6CEqVA6Y7CmsHLDaa njjDOcgHnkCmvHj5lO0981UJpj92/L37jX5iwAcetMVJATu
+nSnlmA5KrkdMdabglQRyP4setONl qOV4dhpyOMd/yphA5K8MDx78VO5UJnPzMckZqA8rnBAP
SktRNcqIlU4GcjnrT2+UsecZ4NLtHmIr HaOetOCsVyQdopylsTe0tSPBJAOGGDUDcr2AHarB7
YOc9hUfJcgEKO5I71PNbcSTTuQNjaNqkf7W KjOBOSzAqR+dSSKVLbRkgjimsOWPdTjGKZUOVO
7GMdx+XqOeahLDOD97vmpNoL8sOnzVD97cMDOe TVWSiHKlsN35KnaRtGMnpTc8sScZ6cVL90f
ezngccVFsHkkNnJPrUc0dWyelkIfkUMRz0znpUZ3Z P8QJ7dqe2ApVidh6+1R4USPubGcc1am7
rqSko6XBV+TLHgHtQeMlckgfMc0mVDE8njof0pSNifK2 T/EAaTlc0vZaiFiQo6Meuab8vz8+m
OafuUkZ4brVZztAJHQYFCvs0TFJ6j5JFEeGXnOD61DLJhQM Yz09hTmysW7GTjnio2BJBOMnnG
OlOLsEbbkLMyhj2A/WomJIU4PQcVLksXyNvOCCKrmTpubsCcUW bb6icbIhD/vgS2QTnHrUTcS
c4Ck8EipJGDSbeAP61AVyqqwPc7aqST0M5JrXYhYsVJYgYPXFKxOz jGGFMJCyDP3M8j09ajZg
pDLz754FG0i0uVXHKF3jduyDliDTiwMvUDPTNR5bJLDHYUAZTLYyD0FW pNi57eg4l/MHQL61E
5bIAHyjnipC22M54FBcCQggEgUlUaaVgTTVkMVnk5VMHPTHNMfJX7w3HrxT s/6QCFJ9SDS9mA
AIzxmolP3hSdtCsC7Lt28k5we1QSMPL5UlurAVJJncSAxHIAHeoDgDoTxVx01K dmiLg78gjJy
BVNiRJlgd3UDPSrbLsUEA8jpmqLuASwDHI70Xv0IptJtdCCRz5uw4UpyWHQ1TkGSC HBLA5C+l
XsgIzMADjvWax3YUIw56miM0nsXDV3jsefgsdyqA2CMmrYIJBQ9OoPrVE8bemDx8tWou GYEHn
APtVSXKm0ZO+7Lkb87cjsT71fUdSCAx5wRVJFXaB/FjrVuHzDArEZOMKfWlOF2pJmr6P8y/ CM
k5G4ntUw7gg5OKrRsGbK/fU5zVsllJOMZpRSvqZuVxdoB2qee2R1q1Fnapwc96gQ9CQeCfxqVS
ehO3PSrUtGW4uSLIO1/mUN6CpF3NMyg7ecmokGIyZDnI5PvU+R5KgZzt59aye+w3Lp3H4AgHyN
vI 496OhRcEHoc+tIUyuDkDqM9qfGr/ADcBiT37U6clcUb8trljaXkYsjEHgVJGCMEABc8imAk
8dV9q mCfLwGU+9EovUHK2iJV2gSEg5zzzUyqiDPUkcVCnO5SQRjJqUZDoPLPy8DIrNRaZpfrc
kxu43fLn irURJTDjPcCmooDcYbdzUyFVHGS2cE0m76WJVncevy52thsd6kT5W5xkjqaAuHI6g
9falLELgYLd qqEuZWQpaj8YQ/KRxgUuFCjG4kfeye9NXeXxnJ6gEdalDqwwQWOMDFDlYfNdWF
QcKSpBPGc0+NC6 ttZeeo9KaC2wK3GetKMK+A4I9Kh33Qoxdrkq7gy57DOfWrKoFAIJBPqaqDI
GMHaauhFIyT8w6iql JrdFyjH0J02qSeemTzVuL7o+6Sc4/wAKpKMsArAnuvtVwMoIO3kdqfK7
DUL6J2LYKiNQCUYdQak4 bIVl3dsVCq7hllKn1PvU8S+UVbaWAGMUaJeZLvF6EyAiLYzADvx3q
RQ5Vdy7sDkAVBltxYLu5B9a urnduHLEdBScrPUavFkijg49cY9KeciPackn3pi7mXGw89frUy
5ITIPHU0l7u4pNSWgg2hQvJ5pP 48EEc5Ap5X5/lIOeRUjACIc8HuaTbsKLaV7jVXqcc5wBU2H
BALIGbtiq4DBCSAQORzT18wgMWXP0 5FO19bml2KU2S7MnB64NOHQgAbS2eaaH/eZyC+O/ak3M
2FwPU4FJvUqbskrgAPNy2ScVLujSMdTn oO31qJQyynOCMc5605l6HJOeAPaok13M5STdhPmbI
KgDPWmncI8feHsKk3nagTHTkmo3yF3D8QKa auJybeoE7iWyDTSV8gZbOcZA9akDAgnYQ2PmNR
Md6gEr1p7ly9NBhUFyfmGcVWJAdN3G5Sx9varT kAnad1QO6nICgYwCTVQeu1xOV9LXRA25lDt
yQe1RcKflbLHn1qct+76dT2qry78HC4xnFO7e+xSl aOpICokwcEc9BUPl7W4PGM5zS5AJCtjt
zSFl3lmPTr2FKO9kzNq5Ex2oFTO71J4NRlgX5baQ2SKl b7vp+FRyDcAcgYHJIpy8h82uhG+Sv
Xv+dIMrIQenFOJPlhQM49RmmseORgk5B9KTTaDV3EO7Chuu MZHalAKklRnHQ0jOhHIYkYyaaA
AM5LZGeDxT3uNu0dR2CS2c5x26VAyjJGCRwMelTDduy+BG1RBR v45GeTnpSunsRGdncQ4VQoB
LBehNQMWdQVG3Pb0qRwDyxII6EVC5XYBuG4ngU0+ZBZXuyJziY9el VpCpi3A855qeRT5hySST
1qCQgDYwwM9cVUddAT8yFyoj3kc54yeoqq0iAfezj3qZshcHHI4qux3D sW6dKpIm1yLPRTkA5
xmlfDRDhT601gUTaTvXoAOpNRD/AFIYqwGfm56VN0nqU+e+iHdQGyMU9Xyh ABHPU1CcrMQeRn
IPajcxK7gADyMd6q/NuOUko7DiwAAI5I70mSI0zxnjB60qYxjGD15pA+ZMMVPH 5UOavawlG+o
8sBCdqncO/vTH3gglgvHUU1nGNwJb6Um/12kHqTWV12FJtLRaCZOAVB5PXqKrswGA e/A4qRid
pAHocCq8m4nkAHvmtORNh53IflDFdxyF71Ucose7nkcVO/31BBBJqCVVI2Kc+nvRdpak ucb7l
Vy5baBkd8VRlYHhvlIORirjMSwAypA6mqLsvmY43E8H0pWQk9Tz8KpjJDFgO3pVqHAtx8pL E5
BzUChIpujONw4FW12F8E4I4rZWWjRKqPlS3LaffXH3ic5q1HzGrfN16dhVKHkxkZy3I5rSjc4J
I4J5A7VHLJbMq6jqSqQANox6+1XomDIN3JAPPUVUTmRQoAJHQirCLhlGCAOtFr+opJPUlBUqrD
Ab r7VYXPDsASTnj2qCM/OwwAM8ZGashQ5BcgqAfu+tN8yfkaaLUdGHkUZxnGasqo2jLbiQcKO
tRKmR hRgYJ68inqSsiY+7twKE+bWxklfW5YjJ+VcEt/dNTKMIG3Luzk8dPaoVBV+Dn196lUDa
3t/D9KV0 nojWy2JMDbyQzHONtTooG4knHHNQJ83JUDPIOKtpvEI3Lyeg/pR0stxxajoJySxwT
zjIq1FxKuQe mMGoUwQepOMfSnqCZPXuSKmWq1Y3YljO6VVIKnGB9KtooE45xkY57VXUkyLkj7
tWgcle+BzUXtsT JvZDt+JTnJGe1TrzGVAJXvnqKrZ6dAOpzT45SBuyDn06VVna6C2mhOA20FT
vA9OopSxVidnBHGO9 RlnKrg4PAp+T0JyAODjrSk4p6iV7oAflUkdD3NTHbkA9AB0FRBXJUYYn
HFSP9xF3Ddijmje472ej JshpAoI2jgipRu3KVyQDzVSMb1O1stnJJqzGxKk5xxwaUk7jcGti0
rLn7uCOM1ZyXG48jPFUlYlF Lde4Aq2kgEcfqRyKIxafcIt3ukWs7Y8iTI3DrVtQfmzzz1qih4
AwMdR71dDkNwcelKo5dR2luiwM LwBxjgk1OnyKQoJPc5qIEFACBjP409VyMgj3oik9bDb93sX
EwARzxwOaUfK5xkIetRxklxjr+hqT d8w6An1ovyuxLbt5D9+F6cdD9KVgNw3nGRkc05mwoPyk
49Ka+PL+YZ44OKEwjN20QDiU+XkjPc9a d8y5bB3E0ijcvAKnGeOhoEjPnkL+FQm3ctvqG1DI2
44yOMUb1BAGSSOQO1DGMg8nIOevWmgHYflx 82QfarUrIIO6Ji29BjAwcc+lDEeQCOe4qBR+/w
C4X1p+cuMlR7VD3FyNvUXdhSPvetB3CPkYY8ke lIWZlzlR+HWmM2VGQSc4x7U1eW5Suh7PkAA
84wfeo88KBgtio2YuxXBPTBpWxwOVApyVlqLlVtRj MzEHG1c1G2OR8pB/SnM/zYbkED8KjYnb
kcjuaq7W+hU1MhLgkgnA7ZFRMyD937Zp8o7gYOM4JppC qSxxnGckcVNnYhxttqRA5k6gEdc0O
+EVtpIPX2prHKliQD14FIZCUyFHsDVKydxxetxh6sXIOeQB 2pn8POQemeooOGl3g4bFMBONzZ
HfHpmktdWKT5XYT5WRc5B9QakPEHPXvTHIkIKnnoP60gLNuwVJ zTTuKb2YnO3HGO9KcKByOBk
H1qPc3CjB5546UbS7ZzznI+lVJdxTaaJWc/LyvTn2qsTjncCM9hUj KB909ByM1GwG5iAC23JW
iDgtCIciegh5UAj+HOf8arSKoZT0zz9KezNtzjaCBkU1pQFJzyOoHaqb lH4TTkb0WxGzNhQAC
MZ5qkfkQ5+YZ4PrU5kZyw+ZT6mqrnaFUnJ5zU6MXLb1GM5zlVLehqEswTgd uuOKeS6ttGCeB0
qs7Yf7rZ579az0voC0VmhpYEEpyAc47ioeWYgA46nmnOxDKyjjZ27mmBSGyCCc c4q5WWw7voR
lHKgHAyN2acmQoO7JOce1NO5txPUnJA9qe7jyVClSx7DtTdSTtZD57jix8tS5AJOS MUpaPYxC
nkY96rjO5QQWJ/KkYsEyeB3I70pQ6tEQsP6SgjB5GRS5bzmGR6jiogf3bHIB/XFMZ2ST KsOec
EVad2kiUkk2JJuaYOHGc9qa7Ycg84OCKc+DEoCkAdvX3qq0owxwPcGh2Za1WiB2ONxB3AcE 9q
ol9wH8QBzuFTOTI20KykcnP8qqMCAQflwOhppxvZkxtfXcYSqtnJYEdfT0qhNuMrbQCoHX1qy7
rsIRh1/Sq0j7V27i2eeB0NCdpbWEna7OBjLrcgyqT34q1GvKNtYk8cGq0QO/5zyw5J9Par8ZKQ
4O OeFNaKKixSUlui1Ep+UYPX8h6VdiQjoG5PQ1SjOUwuR6k96vh2CrgjJ65FJyadkEd9CdOHH
zZPoO 1WFKYBBye9V1XEoKsGJ9quRgA7SoBx19aXNd6blOcbk65CBSQT16danwR8qg5POagVm2
qrLjaMA1 bQqqhiRzwKcrp6oIpLzQ7bv27Th8/NS/MzgNkbR9MilC7SCp5H86k2tkM4JyP1oTX
cjn7f8ABJUK mJwGABHyk09B8rBcnA596ZCRtA6sOOKl5Ac7eSeah26LUpSbRYiZSNjIV2jjNT
DhVPU44HpVYOS3 KHgdh14qbBaMGs0rajknFpskTJkPG0A9+9T4POM7SaroMRbG5HHNT7hyw3d
fWolZsaqST10JsgKN 3UcVKpCSYBPoc1WDYOeWBxUpbc20AMc9/Wrd4vTQcnZ2ROhyWx90HmhW
B42j1BHpTchF54Oe5pqn bHzwTVpXQ7LcslsJk5G09RSo75IBAye4qBWIX5sjihWxkEgqTkGs7
LZ7iastNS3ucyZJyMYGKAxW VlCsx3dahYgIdvXsKlUkrgHBbrSdkitbX2LEfJ+VgpI6Y61MgH
mEBlyBwPWqu4hl4yvX3qWJjjad pbGOByKLahHa5cQncp64HfvVxVBTIK4Pas9XPC5B9asoWUA
44oS0ugvZdjQUgyEggLwB7VaQlgwO Ce+KqIyBgRk9yD0NWFbdHlQQSMginH3mRz3diynKEnJJ
6j0qwvRMZyOoHeqiHG7nBzzmp0fadwPT pnvUSUraGlu7Lqv84HAOQcCpFTPLNli35VSUBRuOd
zcA5zVkNgYzk5GTWlraoU79CT+IqzAk+lOG DjPJHUCoULeYN3DE9COtSBwDsIAbqTRPunqTKo
3uPBZQT37Ugww+Vc8cgUxm+fgHavSkLYOTgYqI pmlpJXH7gCFw24npSbgVIJOOn1phb5g4PX7
xNIpBlZ/4c4ApuKWqTJfw3bJozxtY5WkYgHK5Oema YDjIU8Z545oYFkUYJwucipi7SuxqUn6D
95Em08EHnjv6Um853bScnr6U0H58nrjvSFvlBwVB7Vcm mKKtqOBbcCMNgc0yXnaPmzu9aQnK7
kYEHoaYzhXGWww6A0t3oHQa7EggjAA4Pf6UxORxuzt6mnkk RAZU46Co2ZSSQ4HajWzByfLYgl
B6McA9aYPvYGcEkYNSO4XGQSe1QNJlkHXvn1oTk42Y76CEcjJI I9aYx+ckEnjIx6U44bkHnJx
moJTtAABxkHNS7kryEcsWLAjoBjvSlMKScn3qMsN5zxk9D2pu5t5x 86YzxTk5dNAVO7JFPy9V
zt49qUYCYHPTkVAhJhztOfbtS5AMgXOd2atxSbQSbegrsAMLxzxmgFsj CnkZyD0PpTfTuBjJp
ruTFlfuk4oa6WFKAfMcsx9M8UwkdCc9uO9Mzt+UHqcZ9qiB2uTtNEvImEls h7FtoA5UdCB1qA
suWGC307VI0hOcjAYZzmq2TlyF6EfjSd+U1jGUU7sVmLP2UDnJFVnALYIJ460r 5KjqSfQVCzA
uCxGSOMdqmDa0Iv1GEnc2OhqBlHzbvl9z2pzOvGC27GDVc5GSx3Y4xVxWlx2k1dDM qBvHHPy8
00u2SMKG3dMdKRSGB9M5xUJIOCTknk+xqFHmTZTk0tR2SsuQMAr1NIULOjA/MwzxUTZV 9ucZ7
ntR5gV9xJwea0SaWhE6bvpuSMWWTHLEelM8xS6gZzjBz3o85TICSMAHFRNIPLD8ccAYqEm9 ZI
E7rRakm4lmXCnnrURxj5/vZqMFcsTkEnkZxikkC79yt823nPNN6MnlvuSNJk8YHHB9aqM20tuO
4Me3YCpN/wA7AgAYyc1XLsycYwy9SKuF9hW5XqhrFN+QTtHU5qvM3ICjJbgnOcUjEH5FPINRMQ
xA OVLcYB5qrK97jvHQgZPk7YI7dAaqsCsjgHB75q1ICq8ZfmqTjc53EqCKOfm0voEXdO7OOUj
zF+cE dh9KvoWZo2ypXb0xWdGSQPfjNXot4QKE4J3DNae0bCSdrMuwhS6hsEYyAK0Yvmj2kEkd
CKpxkM46 YI5AFXEUmIEMN4GM1EbvYybu9CULmJeAFBwR0NXN5BH8LDgk1XjVSSVyST8wzU0RG
4koTim02+Yt O/QskZB2LtVj1z1qyhOeQox61XRsDplQcfn3qyAQxkwS3AI/rSumrMWpOQxLbg
Cc4BoX7+WcD19q i39CPqQaeOHPQkgcVlGLvfsEJ6PsTL94lTjI6irEbkPh0bb3NU1CrErZIP8
AFz3q0h3x8sc7uRVS lfRAn1LC8KDnHf6UsbjyyRwM/eqKM8HJOOQOakT5QCOVzihrqUlH5lkN
lV6EYORilUkgbSD9KgDB 3AY4bGB71LnfGAg24GBUxTiU5cr0Jy6CNskAjoM0oBOSGHTg1CcEB
iMPtAyacSQgZSG5xTdltcLt 6LQduywyS+DnOaejfvgx5zke1V1ZuQBk+1OVgHJUcAHNNyfUuN
+m5cJJZRjnufpSYJCsB823oKgD 5YkkA4596AxKHJwAO1TTk73YlK3Uubl8vkbGyAMmpI+ZgDk
ADHJqmADLw+H9D3qRXy4HIyOppyin 1J3TLSkh/vcY71KrfviRnnrz1qupDfNnvU6fMVGPmB7C
lJdy+ZblxXQsDjCscD2q4gG1MsDxjGf0 rOU4IBx1/KrcQIj5OBUPR2XUlzvHVl5MmTDcY6GrU
U3zFTngZGKoxv8Au++OhA61ZR8OuVx7049g 1toaMUihQzKST61JGCy7eAR0zVIM2Bt+bnGM1Y
DAxnIII75oQ4rtoXg2FI9B19KkjI8xx3H61VQq eTnb9alXaXwThQM5HehWdkNJJWJvNcxgsrA
5wD6CnqGWQh+gXvUcZDKFB/E05WzknIIPQ1T0dloR a6FBUgDPvTyxzkABQOtQh1VjgcHOBSKw
8woT9PpQrvVjlAnRlCHHzHdTSPkLcjB4/CmsF8vjOAOv rQFQIM7sEHPNZ8yvuCjyq7GrIS2Oz
HJp5kCH5mHFRLsU4zyBwfamsCAG3AnFaJxbsXzJsl+8MFgx BHSnbsSAEEjHGahRv3OSQWzk4p
u4MSec9AM1m1d3uNkiyElRwo7DFMO4jAIZsZBo6qG4zSAZdWJH XnFXzaaEuKirsRuyngk5zUT
OhdCFIwMZ9akdzvPTj261AwO08HcOnHFKyW+j9SkrLcj5YAqRjFMw pXcGAYHAqQ8n5QNpHFQk
FkBQAE8nNCm0gspDWUFm5xnmomBO7d0B59qlwA5Qg9Ofr7VCTtRiSfoe 9SpXVkyXroMccEHHT
J4pmQY1XcFwMk0u0MysTheuKiPRiULHGMA1XLrqxqSTSaAyeXFxjrT92Y2X bgdSR2qDBO4MNo
B4pwZmwQQvrx1FVaSsyZWtckLZTtx6VE4PlkZwueBipN4VT0AHTPpVZiruMucd cE4pRm7jWm4
0nbCQxIPIFNZgB8xzwKQMecjg9CaiLDCknGOufSm5N9NAlKLsx+4spG4Edj6VE7bl U5LAc5X6
0jN8+ARsx1A601G29BgHORUpNq4p8rSbImIR2AYgHqPSoWJ3YAPrT3YEMRjrmoCQS+GI JpXuh
+VyNiS/ODt4+tRZwgxktnk1IwYsF3Dbjiq0nB2c4BBq4S3QOXQa/CnjC54yOajI/d5AP5VI ZC
FcfeUtkH6VBJJIXDBTt9BUpyetiOW+vRDB8wCk5HXIppAMTBSODhaQvmQj7vFQOcgqeBjPHHPp
VR10RXs5Rt9485wSBgg85/nR5gORj5VqKNS0LKTgHpzSNjIXJVfX6UNO+opqO7HeYfMACk57+t
IW QuScHA7U3eABxyehqsTlAAGxuzk+lOe3mRTS5idpBtDYZc9agbhz1Cg8Uudm4daiZgAoYdO
BikuZ 7lp+RDkl8bSoHrUL7V2u2ckZ4PepGV+X5yemKruchQykHr7YrX7VhcuuhHIwzlTgexql
Ix2Nhhz0 Bq2xBBGRuPI4qhKxDk4yB2p00kyVHXU5WIPgA/Kd3TFacbHYM9CeMCscvm4AVmZFP
PbNaceMgsTg t096cIppSe4oQdveZfhU+YeeM8CrUf3mYfdPvVeIncdw4Pp2qyqho9xOPl49qa
31B1d7F2LiIgkZ xjHerYOVAY4XntyKpL9xVzweCcd6tDccLwAG5JqJKzUuwKVtbEqn5gUxgcc
+lWI/u5LZ5yBnrVcB t+Bjk849KsIACUK4zycGkpNsi2t7DwckqMktzmnD5hnkc4564qLJLjgh
exFPBbaMnopyfeqcXpct Su/dHsSzsNwx64qxESyBcgKO4qqB8vUcDp3qdXAI4KkfeqbJ+Zb8y
yrNsYgfP7VOrYUBmJY/rVJW RQ5JIG4Y96lWQPGNnDdBnvUTve2we0didWywC7SM5b2NWFbMi/
3vSqQLo42kBe4I5NO4JZt25uow ad76kc3NKzLgIIO7JK/ewabuwMgg5GcHtVbAC5IIGRg5608
Hk4H+9mk7l1Hd9hxyuMnGTz9alU4P tnJNV0O1/mDHJyfapI2UrlclT696mV2hy12J94BYYLEn
I9qkJTyzhtxJ6f1qtyZSHODnqKdgAk5z 7UJx0voJSTdkWS6hyQcfhT4h+9y2eDwKpqGExYsCM
8CrCtgEEng9fU1F7PRluLtYthgpcjIOe9Tx bsh0OCfWqZYspPXBH4VYVzs+8oDD8jWqatohpO
yRfUlQTkAk4GatRnIznORgVnIExgszccc1Yhbk FidvQKDWTjzEWT2NKNmO4NxgdRVgHLrwTn0
qipWRv3bDGOc1aTy9u1m556U3ZO1hve5cUtuKqOCc VbVSAehPQ1TiK+UD2xVuMk9hgqKcm3r0
QStLoTJklSwzxjC1Lg7lAIVQOM1ArFMsMdOB7VIrgxqS ec8j0qk29hWcWTpkKMMB6e9SEneDk
HnBqEFdoK9hkCnZLLlVbJGcmm2+oJXWmhPG+ZmIC8c8iohg yjcBk9KcG2Rg8ZxyPSkDqcDgMe
pFTFNXaCKiODYO1jgLwR60KN8h+b5cZ+tMYswXaBz0NMLFc5Bc AYODilyaaGyjpZMmZflxwoH
eoxjaf4mB4pT+8AIOBtzg1AASCoO0g9SaairE+9Lcn2gcgH5l7dBT VZApbhiDjg0zls5bjouO
KamDn5WH9fepkronybJfMAk5+Yd81FuYEkHPPbpQW/esAMHbQMFkG5Sc Z4pOKaG1FbiYfYGYk
U08qPmKgDn2pWJVflHbpmoHLAAjPv71XvSe4K+iYhOGyOw6CkyMhiMEjqPW lXOct1DcZpjDL9
T1zUpp+odbXGM2wl1OXPvUBILIT09aectk4/D0prAbQOrY6elKLj1FfR3ISylT jJCjFRMGVXJ
OB1B9amcEJtUbsjJA9fSmHLR/Mp+lOUtdBR03GphHLHJZhjFNdnyNoAH0pGKonck/ 3jSO7Lyg
GAvI9qbb0E0ubUiZ8SHcSQTgYHFNJBBUsu7AIGKc3zbWUnGM/SqxY88jIPHHJzTSG7dx zNghi
eAOMDrUTsrSA/eHTA707dmM7uD0AqEKxXIXAB60PVXvYmLsrLccVUIpOc9jnioyYtuBk8fN z3
pxb5twO4jjHamEfeIGB/OhOyTuDhciZsqQoBNMJznO0euRTyPnycAZ60xuWAJGAfSnv0KurWKs
jFcYPA4zTGwTubn/ABqdii43Ju5/CoGBEXHXGcelSlfQr2t0QSMVGMAsB0qEO5UnIz6YqTlosD
rx zUMshUkDH9aqTSurERtuiOTb5YycMDjNV9xwTkccHFSuysfm7rkc4qH5TDwCCPyoVt2Cm3o
MGQD1 BPIJody7M2cLn86YzKYs8l88AGneYwjw6YHvROVlexXNqMZkYKqdcVG+WTb368GgqPu5
wB3qMD94 R3xkGtoqDs0wtZWuPATGMtk9CTTC43MAR+NGAB7fyqJt2PlGQeR71D1l7pDV72Gqj
LIdxyR0qJ2O MLgt/EfSnklfvZHrmoyDypXhl7VMoO+pm5JNMrsMxkrgtjvWZNkQspPzAf5NXi
eOAwYDnFUJs5Zc 5OevrW9JvpsXFOMrtHNwKAFBYMScjHetCF87Wbam0YOfSstFO7cSPp6VfVs
NgDdu5OPSs4qLd0Ja q5qxvldxwUzzVhY2Mqsp3DHOKrQqRlScjsPWrilhGQN3J/Orje/MiG0r
8rLqgbQeQccg9jUiFjuG CTuAxUMZwuHwSefepkJDll4c0ScloKCstSVMA8ghgeecUoblMZwR6
9aiAORgFjnluxp5faQGG054 PrSUXzaDdtEiZT8uPm5PQ9qkz8uN25h1+tRIMyckZzjJp7IpkL
Ek5ORipvFsttbk5kAx90NSugd9 24rzwPUU0FQG3dB1NM3dGB+mO9KnJ3skCT3uTO2U2EfKp6m
pgVUgJ1B4J71W3YOMFjn9KlKn76hv qe1Nyv5BBPUsB3Eg+RmKipN4+V8HHt2qDLBSQQSOpp4I
P8SjPNRFue5UoqLLDOcKu0AkdaaNzcEg rjFMUEDcpL8/L3NOyrEhmAA6g0pNJ3QLmXmKWY5+Y
eoxTg4C9Qfp61CVBG9enYCpDgKTtx9fypXY udOKDzB5I5781OHEjEoM88VWiKbyB+JNTK+0n5
MDoDVW622CKS0sS+ZsfdkYPQd6nV2JGcdPTqaq 7htUnle/t7ipVQkKwJ2rkY96W+5bk3HYvq3
yqdw3Z5WrMcnzdm4x0qjGNoBYjGeo71bDDIUfpURm m7WEloWlbDYxwO9SIwyrAH3qBSNp3ckH
oPWrMZ3rgYJBzVRnZbBbsi3HgkscYPAAq2iN8wAHHVjV FFZl5YDvirIBwu3K/Ng5NEn5iXxWu
aSsoVFBwvcCrKEAtnP0qggJjKnbkDOcdasoDsUg9PWnN3Wj HZbEjt/CDwecjtVlSBncPmA5NQ
BcFQVOegp4Y43ngHr7Uoy7FxkraEylcHHBzyDVne7TFVIA61VB woB4PBI9alz1ZelEo66slyb
RNht+5iMY4wKe+1o9xPscDqarA7WQYzxzz3pzEhwGUnjnFFpJlXF3 7DtByMjHrim7ShJHIJzz
3oY7WG7oB6UhfAIyNmeDTadrIcJauw7cDxtJUDk/0qPy24xnceoPamBg rHJy3UUquTIxJx7+t
JOUdkEnLuOG3zfmDeoxSfLuY/NjPODTfMCyHnIIwaY2GYkZLEYIBoctrkPV 6inAYsMkAcjPNI
rKr+uO9NHQEEbv4hmgFsDcoGGyaHJNWKkiRmQrnkccGmE/KAOVApCQCoYgZ7Zq PI3YXcDnj2p
uy3Jd2tGDZGC2cHrn1pgYgZPrn1pc8sXwwzjHpUXykswOc9qS5Wxy0Qrk7QFYMSOv qaiJUD+8
3c04hD1JJzxg9KRVz0IwTyPSiVSCTfYhy2ZDIu3ocE8ioy2EAY4APX3qRvl2AfMp61Vc HcB94
fw+1SrSdmXGLkxjjdIkhODjilIOCScHOPanHBAOctTWweeDx+taXb2JmrvcR8KjBcg9OarF WB
5wf73tUzMSncc9fWoGJJIUEnHBPem/dVib2dxkpbegUAn1HekYZbkbexx0p45kUlh6sMVHklWO
Cc96zUrrQtSXYa0ZTJUg4681EWcOA3TsfWnO+IgT1xyKaSdv3SCeMU+l5BJNIRmAYgjd6H1phK
rG ASMAdCacQPN5YEY4xUUgHBIJA6e9OEbbmfuN2sV3zv4OVJPJ7Uxcqwyy7duQcdanLNvZSpw
AOKrn DBsrsGOAaOddTXV+hXZgcYYFgckAdagchs7Rk9Qo61OxCyA8MT1qttOeCO+D60JrbuDV
newwg7SQ Ac8cjpUBkJypAU55OKldiqbXHU5qqQGlY5znGMd6laXRiovqIwKuwIwAfvAVCPMAI
fLMTkGldynG QADgmohvZevGOfrWkNVdm0bN3X/DjxzGcMAT3Peo844OSxHPtS7ioIABY8gYpA
Bt3Akdcj3ppva+ gOpaLQ0BgAOdw6/jSO+F2hTkcDmlLsY1XK/722ojlZgQMjj5vU1XNd3fQzu
t2DszSnI2gDkY61Dy Mj+NutWS+ZCRjOOpqgWBdwFJOeTQouXQatezRC5LHpgDg8VSc7Fz6nqa
tyA/Pk5x29aoykkHd8oL fLQuW2oSbscvEXB3N0HUHvWhEAATjHGV5rNgceUzltwPQetXomAUM
Qxz94U3F3aasT8PwmxGSU3b vmOMe9XEfaoHKjOBjnis+3G4ggjYq45/lV5CCwyvy579qFCysh
Xu7dS6Dld2R14NS5CEHIJJ4qoA d3UYHBFWVIbjac4x9fehwT3ZK0JFdgeOFzyalCKTnDH0qDG
Fznv0qSMFgRg8HjNVL+6Va71Jd7KR 8wJPQVKWAYAdCcE+hqujqJRwNygjPapAGweMemai10KS
8rEgQK2c8Hkg+tKPuEkY5qNVwCS3U5NS bCBgklQeRUu17FRaHn5lxzn+I+tSoSFIBwB6+tVVf
ORjBzhfcd6njUM5xuwf1pTWlnsOcblnduxu UghsfWkOBM67h97oBSKTGCf4u+ecUiBmf5uAe9
EYJq/RCT0vHYsLuIBJKrgjPYGmtIwTG0DI5OKX DDojAY6noTTWfcF44OaElo0aQemmogB24BO
Ac/SpWHDBWzznB71Dvw5wCASMj3p7EOcjIYcnHpV2 bJcFLqSEKZAo4HU49KcBtAcE4xzk0wMh
JxnJ64peUCYO5AORWEZu9gndLyLOAxwMFs8VLGWwvXB6 1SXesi844I5qaORiegwBjNU4y5bvY
bskncuq245XO0Hmrsajdy3zHkc9KooPlJ6BQB9TU8LZ5IJI bn3FTvpHYpu0dy6rkoqjBz6CrM
YbaT3PcVVTZ1Ugk9u9TI58xQcL/e9qpXtYWltGaEO5lJIG7PQd atKAY1bsRwKqL9xQA/Xr61Z
ickfwlQeKXJLdMSRbT5ARnag6GrAJLp/e9+9VUfeCcHnpxU5diBu5 7Ad6qFuZ33HFSUtiz5jb
23HhjwKeoVQwLfMBjGelVjzjJwOwqUMdw45NJJLbQNVt1LQZWIODtI5N P3oMYI56k9BVNZOgJ
z3x61MAShYY+hokrEyunqywHUD5myCeo705STjHQAgfWq2/quDjscUuS020 EjA5Oe9EdtirW2
Jzgq28YYjjPpTcbcDdxjkVG0mTg8sMA+1EcqLMSQxAPr1os+XQlybdkOZwJPmX K5GPpTGYLJx
1I9aN5KjcFVevNMbeu45UjORx+lCkirJhkiHaMZ9x1pCMMmWGTyCO1RF22MxTPWgP iE8AEf5x
R1KvfdKw7euCcMMDFMySmwsGJ6GoSSAOhXv7GgsxYlfl9eKTTuHLtaxMSWy5IJHA9qYC dzHID
DpxUO7Oed3c4oDgxkkgMBjFVJMlwWo9m3AABgAevqaZtYMTn5c8UEqDhSffmoncbM5AGMcV F7
6WFboPXBlHOcGkLnO3BxUGSGU5Bx0AphdnYnjj0qlRT2Y4au9ywx+U8/w9PxqNiDEW6cimLKS4
LbdvUkd6Vj5iuSAVJ7UJNOwnF3SYwcg/UEVGzH94QdpzgDFPHTYDt96TehfO5cdTQ3dkR0ukVi
Qs Pynvj3pN/wC5YL1zg56ipyEaL1btioXVgzbSvXoKlyj3EktmyAqythTzjmkYsiK2Rk4PSpM
KEOSd x/Wq5yHAAyB0qU5PU2u7DmZc5wCfWo+THubnBwMUH+7kDI4yKR2xGAOfWr5Laoydo2Eb
arBiSCeM VCzFmQHsMfWpc8c9e2KjfcTkOARV3TKtdDcgjdzkngVUk28lsgnoKmztnbecY+6Kj
b5n3EcEcmpb irNAlZ67FVmUM7gDBHeqzE+XnrjJwPrVqZx8uQSuB+VU2fdJgKetJy5TS7b22K
7kc43FTzzVd5dq Y4Ge+OtTSBQ4CnHt61C5VtpAwAeAeorRxW7J5o3vYPl3hB+vvULZIC7Tx1N
K2dmWyQDmkVkZujZ6 4rNcqRN1zXEEny8DvlcjtURkG4b/AJVPb0pzkYK5yWGQF7VCS0jbSR26
itb2d7D5Fa6HEkMSD26e ntTS+dwB2kcn2PpTG2iUjOPm7/zpp/iLE57+5qtRJXsmSFsBGP3jz
VRzk+hxyfWpBvaLocVE5UPG OhBxTp3TEmkRuwE2Wxg+lULggN1wMA81ckZQxU4K5+UiqExOeR
uQjrjrQtdNrgpdzlLcLt2nue/r V5CDcKT17g1TVmEYBKqSCcEdKnjyp6goRxn1pxmpRV9xcrR
sqSIwFwcHnFWY2YcBwQKzF/eR9Tw2 G9quptC5Vvm4xnuB1qFbW7ErPQ1g4OC3c4qZJcSbkPAH
f+VUIwSmVPfuasglUzgjjOe1WlGWjMuV bssmYmFRgcYzxzUivuIXlXHBqHLBjnAA6kipQqgoc
8k4J9aJuNti+aNuxOFUHO3cR90jvUse0yY2 sGAHWq4DKxDAlQ3B9KkQcr13Dk1k0mm2UpO2g5
QDPySd2fyoO7eu4nBFI3EhOcL0zT2zxyDjnFCY 7dmPGDjAO5epqQf3s8YxioEJ8tTzyOR3zUg
OUwzYHXP0qbu9rijpLQnX7xLHginKwMeXGCTUKA7Q Mg7jU552pj5geBim7LSxd3fcUM28bySN
pwBSfOCAD25GKjLOW4+THHI6ikjU/OfmLA8VUYpapgpv XUnBAj+YZYnj2pW2FxtIA24z60Fw2
Mqxz/FTCVYZA2gHPX9KNXrYyTe9iRGRdxYEgDjFODqUyAWV uuOxqH5ZGbAIBHTPenKxCJzg9R
7Vnyxi79S0kt1qyePGdrA5xjJqWNV285GTjmojk5crtxUqkG2Y k7z14FP2lxxeti4BmE4557V
ZR8EYZeOnpioIyFOBgAjBz3qUDg8AjHbtWSnG9jaLi4l8EBV6MT/d 71YABbI45qjCFb7pIGOR
6VawAB97d654q+ZNkpR2vY0oxmHk8dRnvU0WDJllwM96pRD5gCe3HPtV 5WXCBlOSOue1Jc1tD
Nx6LUn8shixb5cZODUytmIdMDoTVZDuBwdwJzipWKiLGCCCCaHZ9Q12LHmf N8oUEAcmpFLlmx
jAPp0quDuUkDL5FTLkPgZG73pylG4XdrJC5AHzcMD1/pUqOcDLDkflUDZMzIeA uDk96RDgtxw
OgzV8nPqXJpKzJiWLAnB9cVIZG3HLA9s1AN23Bz/hSY2p3Yk5Yil7t0Q5XsiZD++D MwPHPvSg
nBGB1yCahyHwRkDHQinA/MduSDyD6CqcmU273uSbsyKcbgTSOxy2eFx61Hu3IMHJA59q iJBY5
BIPIJrJJp3C33Em4mDryp549aZuA/2Wx0Paos4yM9emKXdn+JQQOBjvTkne4pX7A0uZXB5R ue
D0pA2EBDHA61Az5IIQ5Jwab0Hy/Pxzmh9UEGm9SYuBk5xjj61BG4JI7Z/E0zcMckEn17VCXKue
wzwR0rS9o2YOyLJkG4rjn69KYGCtlhnnj0quXbeSB8vGfembgWVhnJPc1HvdGOfTm1sWgyqAc5
5z SkqVDD5SeeKr72YLhcADoKRSzsRnr93j86Ol7hJR6FjLAAgDIyMAU3ewjQFSF7ketRlmwOm
WPIpy ksm0YUA5we9Ta24003ddBWcA5/p0NIrAKC4UkjsKQkLxtLHdwR6U6QgQBgCAOORVwu1a
xKvIYGHz Y5HsaiJcyZCkc80rn5xkgITkgdqjLeYCoODn8aU421Y5JSkrobI53YZCee1QgrwcN
05Ge9PYqsIU /M/cUw5ZAQRVr3V6kPYYxAm7n1wcUEgRMU4U+v8AOgsMKWIzjkVE6kng4Hfnis
5PYbswZioBOHx3 FR7gz4Gdx7D2okKiJuTnoTUAbEhAPAAJwOa2i9DSUdE0xzk7y2OAeMjp9aq
vuZAM9QMAVYEm5Sg6 EZqAgeVgj5T3704zsmmjL3272sRHgnLDaowc1VdiR8hHP3j3xUrqC2GY
sSc9euO1VmYZZh8ox0NZ JXC+mpAzjaFK98Bqru2A5XGCcZxxUhy0o+7jGM9qiYbot3Bwegq52
SL1lZCMvzjJ+UDmoCxzkrgA 8mlYtglgctyfbFJIVXf8w3HHFZp9EJaaDfNydwPU56U1RmZstt
PPXvTQSFHzDI6D1p24hgGGTtNW 9CXGw1SAdg5XPJpPlDFEJPuaa20LlGPH60Fj8zgj3x2oe+r
G72uMbgkk5QnjFR5DjAPQ9+tBYHcx Ofl4AqM+oPPc+tW5XVzJS11K8u3e/GFU9aoyktje6g5/
MetWJGYI2RuGc/Sq0pVTlFJ479jVOTlG yZautf8AI5SMtKRwR6A96uIMRkvztzgVQi3ZJzkMc
DHarS+aCfYDGRSg76tk2cn2NGDLg46Edv4q tIWBIUFRjlj/ACrOjkOGz94A4CjGKtJlgNxIXP
AzTs7astrlgaSEh1O4qDyCatpuEqh2Jfb09az0 KeV3yDgjPSraksDI5Jx6U4prXoRFrYuId0o
BJDHOM96srgJt5z3HrVIK7Mr5AYcirEeTnuTgDAqb LdBbXcsB2aZfmO2p1yIjtOXzwB1NVeQw
QnA5O4jgVMpwwYn5QOtHu9LBp3JlbABCtjHPFNLfJtJy yjkAc89KGZxvAPyjHNIoOZGLBvXFV
G6V7jSVtCdSuxcNtcgcmn/dIUAMevFVQ5WLOFHapFLBtzDg 8E9vrWbpu92aOF47FknCphfwBp
4bMYf5s98/yqJPvbicANjrSNIFyAOjc0lByZMVJqyROwVlO0nr gUAKrhQ2GJyMmo8hWYtnO7j
3pw+djgEknjFTy9wimtCQFi/JGAccjrSgkvkfdB6VF1YAnjvz0pFP +kDdwo6Z7Vak0i5OKehI
h/dEcbi3GO9Skny2+XjcMYqsXCttB6HginjcioSCV7+9Z1IpPzGnqtCy WDMx3deasBsY2jO0Y
PvVPzE3jIyuKmhYs+Typ9qhpJik4vVFoHJ4ywPbNXkPyYbIU/mazyMNuALD 2PWrYKkDkqfQ+l
Obi2mNSfQvRHPygMBmrCu+/DLuAbg/1qou/goRluoNWkIWXG8EDgiiKT95jcW0 y7ESQSPvdDV
xQUVQysR0+maoQPjb1GRmrMbnbnJGPXmh/F5Ecr6F4Hy4Tzk9x3pUJfGSMDqMVEGx uZj6dafv
XaQrAgcHHWk0gu7WJsjzRtJI9qN37veu4E9aqpIN2Oq59amG5WY7sqT0rT4dRqNupYST OflJx
xUhZVjU4JzwKqxHI3MduTkink/u2wQcdDU/MTUWyRmZioDdjg0FyBjBJI7VArDcCM7s4p4J bB
bIbg1EnYNVp0LGW2bgdxxzx3pAxL9QGPXiq4O0sOeT60iFlLbs4/h9abcXqg0ZMZSIsdTjBIHF
DkeXGD90Dj3quzjeQVYgjpS5XYDg5B6Z6Umr6hyajzIWITjIYYNM5MjAnoRzTC5eY7SMk/Lx+V
Re Zg8tznJq3toilFW0JJCGIAb5hy3tVdnKMGwfmPTPanMAZHYA5LDAFQOT5uCc4PGaFq7MqPY
eSDnI ZQOBk1A7nkxlQMdSP1p5ZjGSRk96hZ9q/Njbjn1xTjduxKuhWcsgBK9QRjvSbgH6Ajoc
DvUGcqCO gPSlyhcBj3zwalvoyZJvRonZgAAThgeSPWlVgp6E88c1XEuG+ccnue9CnccqRgdhT
vrqwTsixE37 3kEHOeTU/mZyEA39c1TEkaz7gWz2p6kFgUYBei+tDjZ3a0CUepbA+QkMCB3xUR
J3krx7Z4qNiyFl PcY49aUMhUZ4yPWnDmtcLisQVLEdeMmmvnawXHHem4DHhjjPAph8wgYyfXF
S9VqNOMhJCSEPAzxU OSHb+HnPPSpSNpyPTjPrUEj/ACsAOeBTbVhJdENdhtTZtb+8RUO4AlSe
e1K4ZITk44qDqSw+4OAe tVpYdl0ZKzA5yTioy2NwyMjofam9X5yQR096iZmUsTjavAoTjazHK
0WrClhuwOucZHpTHD7Gw459 ulIz/ug2BuIOAOtQblRck/Nt6ZqeRPYUY3I2l+ZdpUgDP4VXMg
MZAUnBJBpAzCViFyOhpu/94eR0 xjFXyRWlhcq16jH24Vty4B6Adc1UkkI3JhQAe4709w/PKbB
3xVZ93m/Ng88E0RjHVFQ31FILxgZA 44GKrF1D/Nz1GfU1K+4jeO3FRsBtAce+aatorkyu3zDQ
Dx37kU0LtZiSSccc08ZUqOQDn/8AVTXO 11BGPX/ClOWtgc3JaC7gykg59qiB4yWUKRnn0pec4
4A6moZWG0EAYB5I6CiGl0jPmauhQY1QYPP6 VGZG344A9xTSf3TBCAcdD2pj/KQcj0OPWnNt6W
uNxtqROWxhsbjxnHBqnL5iySE8j+H2qVjtUt82 BkcnpVZyzEnO0gZwTUrnirrcqk3FnMoFZSM
nIPPsam3MqZyc5G6qyFDtByB61KSwK7fulT1Nb02p PkbFCfOXBOD1YZ+nXFXbeUmND/F/Ks1N
vm4PzY4AFWomKjA4IGeaThFaW1JaSdjTjZSCFHG45zzj 61MjgySDJOOlZsMrgeUFwauI24ggB
eOR60rSStYIxcXroaCy5QY5bp+FWklGAOQV61nAbnyCQe5q VQiyHcwI4ORRypPbUtST3NMN8h
JU5wM5qRJS3yMPlAwDjrVNGBJ35H49qkRwQevqvtVciT0Kv5bF osQNucZ7U9WY4I645I7mq6k
EAntyc08PlvMAxyciqTSexE2nsSxmQjgKTnk4qT5sNuPfp61WL/IM kkjsKkTax3fMWHbNZOLS
ci2tidB90BiQRkg1YVhjoMZ9KqAgAqM5wATUwRRCGLcdeKzlNaLqTKLW o/zMR53oVye3NPDNs
4YAbutV2UeWQAVwRtzTztb5NxY5HT+VaqXLG44vlSaHnaYiQQW3cnPFCnbJ nIJ9xTPlRDkHIy
eaQujfN944zgVF27dhqzW1yQyq2xM5OetOUzSOSBhe31qDzNpBMZweoxzVjcfL BQrzyBUybto
S5vsSA4ZiSd2fwqyPmiYAhXBqiCS+Xz6nHaplY4CgEg1EU76Cv1ZfRxsIZieRg1bj dTM2RgDu
azI5CJAxPy9GJHQ1c3Hy02+mC2M5ob63K5tDQibDgEH5hn6VZjI6Lwcck1RR+FUN8xGc elWVY
7h3GODRF2K23Reib94MOMhcgYq6shBIY8HI+tZiY3jHLY5x2qwrbiRnqauotnuKW2rLgkYf KC
ckdDVhjj5Mc7dwqisgG2Rh16D+dTGTc6nhvQ09biU1cs+YDtBHG3kjvThKMb9rECqxZhHy6g/T
ofSnBm8plbkg8epNFkDkm7ItiRWdgSMgYwO9GflxkYYVAM7gDwSckgU7cAcFx0NZKLvfcXXQlO
0K Q3zD245pDNu2kcHOPoKrmRQwRiXGM8HrTy2JMAqAOuacoq1maKyWo9WZXBYgnHSpeDzkjnH
Xiq6u zA4BJHT3pWkURnggZ5FKdrabmUtWmTl03D+Jj+mKiZsPkKykng9qg3MjoY1Zwe4/lQST
FvOd3YZp xSTsy2rInQZJzjI6c1FvVAey+4/Wog5zvORk460oCnJORgc+/NUopJpkKbUrsVpAE
65weTUb8kk4 B64PWlDYBPBXd0qu7MA2GXI6Aj9KlRUmXFtrQC7MeCMY4pCcsSQ3A7UbiAASOn
YUxuFBJ4H605WS CU76XGNjgYO3tUeflyGUYPzE9qCQyEbu361BuInA5IAzz3qqabjdFNN2J8/
KOdx7VIoB+YEe1VlI LKd2MHjPepuTu4ycZPNQp3eo5/DclHI5OR1z/SnoQuGGNpORVeMHAJwC
vT2pzMwj3ZJx6Cq5r3I2 67lnfucjIzjOD3pPMbLJ8u/OR/hUO5XjQ888/WlDK7Es2DnPPYYo0
S1JSTeqJTy69jjioSSzHk4z g4pxfMTHjp8opm44APB6kd6Oa6uO3RBIQGGM4Ax1qBypJy4Az0
70rP8AJ1yQ3PtUZfJwAMepHein a9hra41i+0Asufeo9xMJXIUDBzimtJkAnp1J9aY7Mq4JB7c
d6bTfQGlLqIznLZ7ntwTUDOC3cknI 9qldm8oAKC3fFVVYb2JIyetSo21auIWORTKQWzkHmoWw
ASGzx1NTOyiM44B6MB0qoVAlJzv+Xrmq VrdhOXYQsHQ4wr+lQH5lY5A5HSib5ckZBGDTVfgsS
o6np1qpNdCYySdrETAFT83IPrVZidzNkHtj FTtIARhSQc81A58uMOgJJ4ORRdtFvfyItwYspB
OB2prL/o4LHljnGeKU4YnjBx9KgYqI+pLA560J pNJEtdEDsfl5AGaRtvmMfmPI6GmuS6rgZwc
emTSrw4O5cE/NmrltZEpaNtjQ5MZPJB7Y5qFlxGzI cHuD3pWYtKcDJPSq7FgwQsGIB6Ci2uhX
PZWHO+bfI4yO45FRO2QQx4OMmnsy8YOFPXNRMwwU424y OKm7a02Jb6WK7kqzBsHBwM96rvgxb
jnrxUhO6Rh94YBzmqUhbywp+bk4Naq0ZJX1Hfn0OaVmBw2O OelSCUNtKHcvIqrHiQ78nKnnnr
VxDhfkwPTIpKKb5uoc0bNrqTq+VYDO4kfjVrBJzzjg7qrKSrkM ue4x2qwjbshc4PJJ9aG1cSl
ZbFtWCyEg5weoqyGBjHXLHNUlZFBAYccEVZRiHxjgD07VD91Dbk1c vrgOBndyCMVZYgMB1GDt
OKpoTgBCCMfL/jVgblxlgSDyxHFCfVCk3uWkYr8p5JGcjoanJwPTjnmq qEgKQRycCnqxySxUj
2FUtWJa6l778RAYcenehThixIK5waqKQV3Ipz9aCxymDjHUUkpWZcWnp0L2 7AZGxjtx1oBHlk
LgHpzVXeTtZ+BgYAp4kB+bgc85ocJLYOVK6SLqkbwrNyVxxSkjYOcDIyKqiQ7g WxwMcDvS7+j
jle4qfZtvUtKz3LIYgkHLAHpUuWRC4AA3DHvVQElcHC89TR5hGd2WA5FW7WaQra/o WncswLDA
waiB+fPPHpQZAzEHAGR/+qotxwAMH09qhb6ExbtZqxM8hwzbScfrSJJ1G5Rxx7VXL/Ju H5e1K
N4fGzI7nFTy2+I2SsXQxfcSdw3DO2pEdhls/TFU1fYjKDwx9KsIxchPup9OtTNSvqZKbitC 5F
Io2jgluevFXRKTIEJwO2Ky4yAcKQVBJJx0qzC28lyDg5/Cl7jfMLmcmaQbCb8jhueeatLIzHrw
elZqksSCpVeM81ZSQKMD5gXz9KSSvoyoq2xphwp8wn5c4wOoqZWbPDDJ5OOlZccn77BG5Tzx7V
dV wMEcZGTU8/Ky57abl9GUjG7d7elPWTqvJJ6Gs9HbqBxg4zU0LuwUEEgLkt6GtL2V2yaitZp
F4N9z JJyccVMCobnkY79RVIvhVPTj73anKxKgnJ5GeetO8rXuKMVqjQV/lPzDJqMygwg7SWHB
qMSKof5c 5OMelJhd+7kDvRdaihZbkhZty52gZ9Ke0gyeRjFQAOJSeo4xn0ppYYAP0pWTVjRS1
0LBmIfCHHAJ JoeXEhLdevHvVbdjnHQflSblKN1YD35NDjGJPLbct7yqqPfJpTKM4PUHt3quGV
iOchvSmJgH7rHa MHmp5Y31E9XqtiXeS5JXqeMUrKzSAbdox92oXZnU+x4pu5mdxnGDketXEpw
2bH7nLjO3AFNdSpPB L9aYWGGJPWo5pG2BOcj86Uua+hV76iuRvBz0HamBizNwRt9+tBx8rbXA
2VGwKsPmOxuQauLurDbW 0RJCBIpz82OcUAEHhgVNMBO4MT14z6VG2clFDDkEZqeS79A5+ZrXY
mfgLjv+lSEEREr8pz371CCu SGBwB0qMj90x5GW4PahxStYlSu73LasScMw47Cl8zJTuuccGqw
bDh8hmyQRinq6iNhkKO+exqGnb QN3ZFkkLEMck9famkgEvkF81XD8rn1/KpGkVkJKkDuaTi1o
Db2JCB5YGTu6ioncHaeQT1/wpuT5K tzwcnJqMOzBtykKTnPvVJcruhRl+A5mC8DJz3pm9lG11
IZumaU4AJYhR1x3qJ2BbeMgD5sH071Sd 9LCnNRFYKVCg9Bgiq7MFlIc4HT6Uu9ch+vHTvUBYt
IzldyZArWF0rkpNK4M5KEg9DnH0qENmDpli Tx60skp35UYUcEe1RO4weOMYX1qJPsK1la2orE
BNmd2R3/hqvwAVB4xkihpBkDn0YUw4ZGyMccYq oRvuNq71GHexJHzY9aiJf5m4PTIpzMyOCAW
Ug/yqPK+WE2NnBwc0QulsU7Kz6EW0+SwLZ9R+NRs3 y4ZsnPBFOH3/AJQRxyDURwM78Ar2pv3m
J2bI3I3MAQA3vyKiLHJC8sODTz87EEHnmo1IBw3IzQ2k tR8sVqwHACBucc+1Rs23O0n3zTcru
Y5O4DkZpnmbc56+9K2l0Q5/IduZW+XHGSeKruQWDZySOR6V MMKBkj3NVzlpCSyZI+UAdKFbuV
HRa7jG27cLxnrk1CThWHJwOQKkcbtuMFfb3qu0qguFU5HynFXL
l5dTOd1qxgYfaS4U4KY4NUJA23byzdMDrVl2D2+5c9gT71Rk++cB85yMmiEXF2Jb5ndH/9k=
UID:6
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/8.vcf b/test/Resource/CardDAV/vcardput/8.vcf
index ae9c0aa..3f36dd7 100644
--- a/test/Resource/CardDAV/vcardput/8.vcf
+++ b/test/Resource/CardDAV/vcardput/8.vcf
@@ -1,10 +1,10 @@
BEGIN:VCARD
VERSION:3.0
N:Śuterry;HiAscii;;;
FN:HiAscii Śuterry
-EMAIL;TYPE=INTERNET;TYPE=WORK;TYPE=pref:hiascii@example.com
+EMAIL;TYPE=INTERNET,WORK;TYPE=pref:hiascii@example.com
TEL;TYPE=WORK;TYPE=pref:777-777-7777
TEL;TYPE=CELL:8888888888
ADR;TYPE=WORK;TYPE=pref:;;1 ïlena;Paris;Paris;77777;France
UID:8
END:VCARD
diff --git a/test/Resource/CardDAV/vcardput/9.vcf b/test/Resource/CardDAV/vcardput/9.vcf
index 016f2ed..2bff011 100644
--- a/test/Resource/CardDAV/vcardput/9.vcf
+++ b/test/Resource/CardDAV/vcardput/9.vcf
@@ -1,11 +1,11 @@
BEGIN:VCARD
VERSION:3.0
N:バイト;ダブル;;;
FN:ダブル バイト
-EMAIL;TYPE=INTERNET;TYPE=WORK:doublebytes@example.com
+EMAIL;TYPE=INTERNET,WORK:doublebytes@example.com
TEL;TYPE=WORK:777-777-7777
TEL;TYPE=CELL:8888888888
ADR;TYPE=WORK:;;1-23-4 Irohacho #2;Tokyo;Japan;33-3333;Japan
NOTE:日本ですよ。
UID:9
END:VCARD
diff --git a/test/runtests.sh b/test/runtests.sh
index e9082c2..574335c 100755
--- a/test/runtests.sh
+++ b/test/runtests.sh
@@ -1,36 +1,37 @@
#!/bin/bash
#
# 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/>.
##
# Initializes the CalDAVTester environment and runs all test scripts
#
CALDAVTESTER=$1
PYTHONPATH=`$CALDAVTESTER/run.py -p`
export PYTHONPATH="$PYTHONPATH:$CALDAVTESTER/../pycalendar/src"
$CALDAVTESTER/testcaldav.py --print-details-onfail -s serverinfo.xml \
CardDAV/current-user-principal.xml \
CardDAV/ab-client.xml \
CardDAV/propfind.xml \
CardDAV/put.xml \
CardDAV/directory-gateway.xml \
CalDAV/current-user-principal.xml \
CalDAV/caldavIOP.xml \
CalDAV/ctag.xml \
- CalDAV/attachments.xml
+ CalDAV/attachments.xml \
+ CalDAV/scheduleprops.xml
diff --git a/test/scripts/tests/CalDAV/caldavIOP.xml b/test/scripts/tests/CalDAV/caldavIOP.xml
index 0665bda..74beb6d 100644
--- a/test/scripts/tests/CalDAV/caldavIOP.xml
+++ b/test/scripts/tests/CalDAV/caldavIOP.xml
@@ -1,360 +1,367 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE caldavtest SYSTEM "caldavtest.dtd">
<!--
Copyright (c) 2006-2013 Apple Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<caldavtest>
<require-feature>
<feature>caldav</feature>
</require-feature>
<start/>
<test-suite name='1. Event Creation'>
<test name='1.1'>
<description>Create new single-instance meeting titled "Meeting 1.1" with the location "Durham".</description>
<request end-delete='yes'>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_1.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/1/1_1.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='1.2'>
<description>Create new meeting titled "Meeting 1.2" recurring every Monday from 10:00 AM to 11:00 AM for 4 weeks.</description>
<request end-delete='yes'>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_2.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/1/1_2.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='1.3'>
<description>Create new single-instance meeting titled "Meeting 1.3" with 2 other attendees.</description>
<request end-delete='yes'>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_3.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/1/1_3.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='1.4'>
<description>Create new single-instance meeting titled "Meeting 1.4" with an alarm set to trigger 15 minutes prior to the schedule time of the meeting.</description>
<request end-delete='yes'>
<method>PUT</method>
- <ruri>$calendarpath1:/1-1_4.ics</ruri>
+ <ruri>$calendarpath1:/1-1_4-new.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/1/1_4.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
+ <verify>
+ <callback>header</callback>
+ <arg>
+ <name>Location</name>
+ <value>$calendarpath1:/1-1_4.ics</value>
+ </arg>
+ </verify>
</request>
</test>
</test-suite>
<test-suite name='2. Event Modification'>
<test name='2.1'>
<description>Modify the title of meeting "Meeting 1.1" to "Meeting 1.1bis".</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_1.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_1.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='2.2'>
<description>Modify the location of the meeting "Meeting 1.1bis" to "Seattle bis".</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_1.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_2.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='2.3'>
<description>Reschedule meeting "Meeting 1.1bis" to the next day.</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_1.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_3.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='2.4'>
<description>Add an attendee to "Meeting 1.1bis".</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_1.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_4.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='2.5'>
<description>Add an alarm to "Meeting 1.1bis".</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_1.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_5.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='2.6'>
<description>Modify the title of the 1st instance of the recurring meeting created in 1.2.</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_2.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_6.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='2.7'>
<description>Modify the participation status of 1st instance to DECLINED.</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_3.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_7.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='2.8'>
<description>Cancel the 4th instance of the recurring meeting created in 1.2.</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_2.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_8.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='2.9'>
<description>One client changes "Meeting 1.1bis" to a different time, second client 'refreshes' its display to see the modification.</description>
<request end-delete='yes'>
<method>PUT</method>
<ruri>$calendarpath1:/1-1_1.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/2/2_9.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
</test-suite>
<test-suite name='4. Event Deletion'>
<test name='4.1'>
<description>Delete a single non-recurring meeting.</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/4-4_1.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/4/4_1.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
<request>
<method>DELETE</method>
<ruri>$calendarpath1:/4-4_1.ics</ruri>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='4.2'>
<description>Delete a single recurring meeting with no overridden instances.</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/4-4_2.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/4/4_2.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
<request>
<method>DELETE</method>
<ruri>$calendarpath1:/4-4_2.ics</ruri>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='4.3'>
<description>Delete a single recurring meeting with overridden instances.</description>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/4-4_3.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/4/4_3.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
<request>
<method>DELETE</method>
<ruri>$calendarpath1:/4-4_3.ics</ruri>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='4.4'>
<description>Delete a non-overridden instance of a recurring meeting.</description>
<request end-delete='yes'>
<method>PUT</method>
<ruri>$calendarpath1:/4-4_4.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/4/4_4a.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/4-4_4.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/4/4_4b.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
<test name='4.5'>
<description>Delete an overridden instance of a recurring meeting.</description>
<request end-delete='yes'>
<method>PUT</method>
<ruri>$calendarpath1:/4-4_5.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/4/4_5a.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
<request>
<method>PUT</method>
<ruri>$calendarpath1:/4-4_5.ics</ruri>
<data>
<content-type>text/calendar; charset=utf-8</content-type>
<filepath>Resource/CalDAV/caldaviop/4/4_5b.txt</filepath>
</data>
<verify>
<callback>statusCode</callback>
</verify>
</request>
</test>
</test-suite>
<end>
<request user="$userid1:" pswd="$pswd1:">
<method>DELETEALL</method>
<ruri>$calendarpath1:/</ruri>
</request>
<request user="$userid1:" pswd="$pswd1:">
<method>DELETEALL</method>
<ruri>$inboxpath1:/</ruri>
</request>
<request user="$userid2:" pswd="$pswd2:">
<method>DELETEALL</method>
<ruri>$calendarpath2:/</ruri>
</request>
<request user="$userid2:" pswd="$pswd2:">
<method>DELETEALL</method>
<ruri>$inboxpath2:/</ruri>
</request>
<request user="$userid3:" pswd="$pswd3:">
<method>DELETEALL</method>
<ruri>$calendarpath3:/</ruri>
</request>
<request user="$userid3:" pswd="$pswd3:">
<method>DELETEALL</method>
<ruri>$inboxpath3:/</ruri>
</request>
</end>
</caldavtest>
diff --git a/test/scripts/tests/CalDAV/scheduleprops.xml b/test/scripts/tests/CalDAV/scheduleprops.xml
new file mode 100644
index 0000000..cc00c37
--- /dev/null
+++ b/test/scripts/tests/CalDAV/scheduleprops.xml
@@ -0,0 +1,255 @@
+<?xml version="1.0" standalone="no"?>
+
+<!DOCTYPE caldavtest SYSTEM "caldavtest.dtd">
+
+<!--
+ Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<caldavtest>
+ <description>Test properties defined by caldav-schedule</description>
+
+ <require-feature>
+ <feature>caldav</feature>
+ </require-feature>
+
+ <start/>
+
+ <test-suite name='Inbox/Outbox'>
+ <test name='1'>
+ <description>PROPFIND inbox/outbox URLs</description>
+ <request>
+ <method>PROPFIND</method>
+ <ruri>$principal1:</ruri>
+ <header>
+ <name>Depth</name>
+ <value>0</value>
+ </header>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/1.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>okprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL</value>
+ <value>{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL</value>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-user-address-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ </test-suite>
+
+ <test-suite name='free-busy-set' ignore='yes'>
+ <test name='1'>
+ <description>PROPFIND inbox</description>
+ <request>
+ <method>PROPFIND</method>
+ <ruri>$inboxpath1:/</ruri>
+ <header>
+ <name>Depth</name>
+ <value>0</value>
+ </header>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/2.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>okprops</name>
+ <value><![CDATA[{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set$<href xmlns="DAV:">$calendarpath1:/</href>]]></value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ <test name='2'>
+ <description>PROPPATCH valid calendar</description>
+ <request>
+ <method>PROPPATCH</method>
+ <ruri>$inboxpath1:/</ruri>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/3.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>okprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ <test name='3'>
+ <description>PROPPATCH invalid calendar</description>
+ <request>
+ <method>PROPPATCH</method>
+ <ruri>$inboxpath1:/</ruri>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/4.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>badprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ <test name='4'>
+ <description>PROPPATCH delete it - fails</description>
+ <request>
+ <method>PROPPATCH</method>
+ <ruri>$inboxpath1:/</ruri>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/5.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>badprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ <test name='5'>
+ <description>PROPPATCH valid calendar - reset it</description>
+ <request>
+ <method>PROPPATCH</method>
+ <ruri>$inboxpath1:/</ruri>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/3.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>okprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ <test name='6'>
+ <description>Setup some calendars</description>
+ <request end-delete='yes'>
+ <method>MKCALENDAR</method>
+ <ruri>$calendarhome1:/test1/</ruri>
+ <verify>
+ <callback>statusCode</callback>
+ </verify>
+ </request>
+ <request end-delete='yes'>
+ <method>MKCALENDAR</method>
+ <ruri>$calendarhome1:/test2/</ruri>
+ <verify>
+ <callback>statusCode</callback>
+ </verify>
+ </request>
+ </test>
+ <test name='7'>
+ <description>PROPPATCH - valid</description>
+ <request>
+ <method>PROPPATCH</method>
+ <ruri>$inboxpath1:/</ruri>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/6.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>okprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ <test name='8'>
+ <description>Delete a calendar</description>
+ <request>
+ <method>DELETE</method>
+ <ruri>$calendarhome1:/test2/</ruri>
+ <verify>
+ <callback>statusCode</callback>
+ </verify>
+ </request>
+ </test>
+ <test name='9'>
+ <description>PROPPATCH - valid new calendar, invalid existing</description>
+ <request>
+ <method>PROPPATCH</method>
+ <ruri>$inboxpath1:/</ruri>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/7.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>badprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ <test name='10'>
+ <description>PROPPATCH - invalid new calendar, invalid existing</description>
+ <request>
+ <method>PROPPATCH</method>
+ <ruri>$inboxpath1:/</ruri>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/8.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>badprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ <test name='11'>
+ <description>PROPPATCH valid calendar - reset it</description>
+ <request>
+ <method>PROPPATCH</method>
+ <ruri>$inboxpath1:/</ruri>
+ <data>
+ <content-type>text/xml; charset=utf-8</content-type>
+ <filepath>Resource/CalDAV/scheduleprops/3.xml</filepath>
+ </data>
+ <verify>
+ <callback>propfindItems</callback>
+ <arg>
+ <name>okprops</name>
+ <value>{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set</value>
+ </arg>
+ </verify>
+ </request>
+ </test>
+ </test-suite>
+
+ <end/>
+
+</caldavtest>
diff --git a/test/serverinfo.xml b/test/serverinfo.xml
index d53e00f..4ab96fb 100644
--- a/test/serverinfo.xml
+++ b/test/serverinfo.xml
@@ -1,815 +1,815 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE serverinfo SYSTEM "serverinfo.dtd">
<!--
Copyright (c) 2006-2013 Apple Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<serverinfo>
<host>localhost</host>
<nonsslport>80</nonsslport>
<sslport>443</sslport>
<authtype>basic</authtype>
<features>
<!-- Generic WebDAV extensions -->
<feature>COPY Method</feature> <!-- COPY method -->
<feature>MOVE Method</feature> <!-- MOVE method -->
<feature>Extended MKCOL</feature> <!-- Extended MKCOL -->
<!-- ACL related -->
<!-- <feature>ACL Method</feature> --> <!-- ACL method -->
<!-- <feature>acl-principal-prop-set REPORT</feature> --> <!-- ACL acl-principal-prop-set REPORT -->
<!-- <feature>principal-match REPORT</feature> --> <!-- ACL principal-match REPORT -->
<!-- <feature>principal-property-search REPORT</feature> --> <!-- ACL principal-property-search REPORT -->
<!-- <feature>principal-search-property-set REPORT</feature> --> <!-- ACL principal-search-property-set REPORT -->
<!-- <feature>add-member</feature> --> <!-- Add-member used to create resources -->
<!-- <feature>brief</feature> --> <!-- Brief header for PROPFIND, REPORT -->
<!-- <feature>bulk-post</feature> --> <!-- Bulk POST requests -->
<feature>ctag</feature> <!-- ctag extension -->
<feature>current-user-principal</feature> <!-- current-user-principal extension -->
<!-- <feature>directory listing</feature> --> <!-- GET on collection -->
<!-- <feature>extended-principal-search</feature> --> <!-- Extended principal-property-search REPORT extension -->
<!-- <feature>expand-property</feature> --> <!-- Expand property REPORT -->
<!-- <feature>only-proxy-groups</feature> --> <!-- Group-membership only includes delegated-to groups -->
<!-- <feature>limits</feature> --> <!-- max-collections and max-resources limits -->
<!-- <feature>prefer</feature> --> <!-- Prefer header overall support -->
<!-- <feature>prefer-minimal</feature> --> <!-- Prefer header return=minimal -->
<!-- <feature>prefer-representation</feature> --> <!-- Prefer header return=representation -->
<!-- <feature>prefer-noroot</feature> --> <!-- Prefer header depth-noroot -->
<!-- <feature>quota</feature> --> <!-- WebDAV QUOTA -->
<!-- <feature>quota-on-resources</feature> --> <!-- WebDAV QUOTA on calendar and address book object resources -->
<!-- <feature>resource-id</feature> --> <!-- WebDAV BIND DAV:resource-id property -->
<!-- <feature>sync-report</feature> --> <!-- WebDAV collection sync REPORT -->
<!-- <feature>sync-report-home</feature> --> <!-- WebDAV collection sync REPORT on Homes -->
<feature>well-known</feature> <!-- well-known feature -->
<!-- <feature>per-object-ACLs</feature> --> <!-- ACL for objects in calendar/address books -->
<!-- <feature>regular-collection</feature> --> <!-- Regular collections allowed in calendar/address book homes -->
<!-- CalDAV specific extension -->
<feature>caldav</feature> <!-- Basic CalDAV feature enabler -->
<!-- <feature>auto-accept</feature> --> <!-- Auto-accept for rooms & locations -->
<!-- <feature>auto-accept-modes</feature> --> <!-- Auto-accept modes -->
<!--<feature>dropbox</feature> --> <!-- dropbox extension -->
<!-- <feature>default-alarms</feature> --> <!-- default alarms extension -->
<!-- <feature>EMAIL parameter</feature> --> <!-- Server normalizes cuaddress and adds EMAIL parameter -->
<!-- <feature>extended-freebusy</feature> --> <!-- Extended freebusy response -->
<!-- <feature>implicit-scheduling</feature> --> <!-- CalDAV scheduling - implicit -->
<!-- <feature>location-resource-tracking</feature> --> <!-- Server tracks who makes unscheduled changes to locations and resources -->
<!-- <feature>managed-attachments</feature> --> <!-- CalDAV Managed Attachments -->
<!-- <feature>maskuid</feature> --> <!-- maskuid extension -->
<!-- <feature>partitioning</feature> --> <!-- Partitioned server -->
<!-- <feature>no-duplicate-uids</feature> --> <!-- duplicate UIDs in same home not supported -->
<!-- <feature>partstat-timestamp</feature> --> <!-- Time stamps when PARTSTAT changes extension -->
<!-- <feature>private-comments</feature> --> <!-- private-comments extension -->
<!-- <feature>private-events</feature> --> <!-- private-events extension -->
<!-- <feature>proxy</feature> --> <!-- calendar-user-proxy extension -->
<!-- <feature>proxy-authz</feature> --> <!-- sudo user extension -->
<!-- <feature>remove-duplicate-alarms</feature> --> <!-- Server removes any duplicate alarms on PUT -->
<!-- <feature>query-extended</feature> --> <!-- calendar-query-extended extension -->
<!-- <feature>shared-calendars</feature> --> <!-- Shared calendars extension -->
- <!--><feature>schedule-inbox</feature> --> <!-- Scheduling Extension -->
+ <feature>schedule-inbox</feature> <!-- Scheduling Extension -->
<!-- <feature>schedule-changes</feature> --> <!-- schedule-changes property extension -->
<!-- <feature>split-calendars</feature> --> <!-- Calendars are split by component type -->
<!-- <feature>supported-component-sets</feature> --> <!-- CALDAV:supported-calendar-component-sets on calendar homes -->
<!-- <feature>supported-component-sets-one</feature> --> <!-- Only single component calendars allowed to be created -->
<!-- <feature>timerange-low-limit</feature> --> <!-- Time-range only valid one year back -->
<!-- <feature>timerange-high-limit</feature> --> <!-- Time-range only valid 5 years ahead -->
<!-- <feature>timezone-service</feature> --> <!-- Timezone service extension for Wiki-->
<!-- <feature>timezone-std-service</feature> --> <!-- Timezone standard service extension -->
<!-- <feature>vavailability</feature> --> <!-- VAVAILABILITY on inbox -->
<!-- <feature>webcal</feature> --> <!-- Internet calendar subscription via GET on calendar collection -->
<!-- CardDAV specific extension -->
<feature>carddav</feature> <!-- Basic CardDAV feature enabler -->
<!-- <feature>default-addressbook</feature> --> <!-- Default address book behavior -->
<!-- <feature>global-addressbook</feature> --> <!-- Global address book for each user -->
<!-- <feature>shared-addressbooks</feature> --> <!-- Shared address books extension -->
<feature>directory-gateway</feature> <!-- Directory gateway extension -->
</features>
<substitutions>
<!-- relative path to caldav root-->
<substitution>
<key>$root:</key>
<value>/iRony/</value>
</substitution>
<!-- Useful xpath shortcuts for verifiers -->
<substitution>
<key>$multistatus-response-prefix:</key>
<value>/{DAV:}multistatus/{DAV:}response</value>
</substitution>
<substitution>
<key>$multistatus-href-prefix:</key>
<value>/{DAV:}multistatus/{DAV:}response/{DAV:}href</value>
</substitution>
<substitution>
<key>$verify-response-prefix:</key>
<value>{DAV:}response/{DAV:}propstat/{DAV:}prop</value>
</substitution>
<substitution>
<key>$verify-property-prefix:</key>
<value>/{DAV:}multistatus/{DAV:}response/{DAV:}propstat/{DAV:}prop</value>
</substitution>
<substitution>
<key>$verify-bad-response:</key>
<value>/{DAV:}multistatus/{DAV:}response/{DAV:}status</value>
</substitution>
<substitution>
<key>$verify-error-response:</key>
<value>/{DAV:}multistatus/{DAV:}response/{DAV:}error</value>
</substitution>
<substitution>
<key>$CALDAV:</key>
<value>urn:ietf:params:xml:ns:caldav</value>
</substitution>
<substitution>
<key>$CARDDAV:</key>
<value>urn:ietf:params:xml:ns:carddav</value>
</substitution>
<substitution>
<key>$CS:</key>
<value>http://calendarserver.org/ns/</value>
</substitution>
<!-- Server configuration settings -->
<!-- $host: and $hostssl: are implicitly added by CalDAVTester based
on the host/nonsslport/sslport values and ssl command line switch -->
<!-- relative path to main principal collection-->
<substitution>
<key>$principalcollection:</key>
<value>$root:principals/</value>
</substitution>
<!-- the core recored type collections-->
<substitution>
<key>$uidstype:</key>
<value>__uids__</value>
</substitution>
<substitution>
<key>$userstype:</key>
<value>users</value>
</substitution>
<substitution>
<key>$groupstype:</key>
<value>groups</value>
</substitution>
<substitution>
<key>$locationstype:</key>
<value>locations</value>
</substitution>
<substitution>
<key>$resourcestype:</key>
<value>resources</value>
</substitution>
<!-- relative path to record type principal collections-->
<substitution>
<key>$principals_uids:</key>
<value>$principalcollection:$uidstype:/</value>
</substitution>
<substitution>
<key>$principals_users:</key>
<value>$principalcollection:$userstype:/</value>
</substitution>
<substitution>
<key>$principals_groups:</key>
<value>$principalcollection:$groupstype:/</value>
</substitution>
<substitution>
<key>$principals_resources:</key>
<value>$principalcollection:$resourcestype:/</value>
</substitution>
<substitution>
<key>$principals_locations:</key>
<value>$principalcollection:$locationstype:/</value>
</substitution>
<!-- relative path to calendars collection-->
<substitution>
<key>$calendars:</key>
<value>$root:calendars/</value>
</substitution>
<!-- relative path to record type calendar collections-->
<substitution>
<key>$calendars_uids:</key>
<value>$calendars:$uidstype:/</value>
</substitution>
<substitution>
<key>$calendars_users:</key>
<value>$calendars:$userstype:/</value>
</substitution>
<substitution>
<key>$calendars_groups:</key>
<value>$calendars:$groupstype:/</value>
</substitution>
<substitution>
<key>$calendars_resources:</key>
<value>$calendars:$resourcestype:/</value>
</substitution>
<substitution>
<key>$calendars_locations:</key>
<value>$calendars:$locationstype:/</value>
</substitution>
<!-- primary calendar name-->
<substitution>
<key>$calendar:</key>
<value>Calendar</value>
</substitution>
<!-- primary tasks-only calendar name-->
<substitution>
<key>$tasks:</key>
<value>Tasks</value>
</substitution>
<!-- inbox name-->
<substitution>
<key>$inbox:</key>
<value>inbox</value>
</substitution>
<!-- outbox name-->
<substitution>
<key>$outbox:</key>
<value>outbox</value>
</substitution>
<!-- dropbox name-->
<substitution>
<key>$dropbox:</key>
<value>dropbox</value>
</substitution>
<!-- attachments name-->
<substitution>
<key>$attachments:</key>
<value>dropbox</value>
</substitution>
<!-- notification name-->
<substitution>
<key>$notification:</key>
<value>notification</value>
</substitution>
<!-- freebusy name-->
<substitution>
<key>$freebusy:</key>
<value>freebusy</value>
</substitution>
<!-- server-to-server inbox-->
<substitution>
<key>$servertoserver:</key>
<value>$root:inbox</value>
</substitution>
<!-- timezone service-->
<substitution>
<key>$timezoneservice:</key>
<value>$root:timezones</value>
</substitution>
<!-- timezone std service-->
<substitution>
<key>$timezonestdservice:</key>
<value>$root:stdtimezones</value>
</substitution>
<!-- relative path to calendars collection-->
<substitution>
<key>$addressbooks:</key>
<value>$root:addressbooks/</value>
</substitution>
<!-- relative path to record type calendar collections-->
<substitution>
<key>$addressbooks_uids:</key>
<value>$addressbooks:$uidstype:/</value>
</substitution>
<substitution>
<key>$addressbooks_users:</key>
<value>$addressbooks:$userstype:/</value>
</substitution>
<substitution>
<key>$addressbooks_groups:</key>
<value>$addressbooks:$groupstype:/</value>
</substitution>
<!-- primary addressbook name -->
<substitution>
<key>$addressbook:</key>
<value>Contacts</value>
</substitution>
<!-- global-addressbook name -->
<substitution>
<key>$global_addressbook:</key>
<value>global-addressbook</value>
</substitution>
<!-- directory name -->
<substitution>
<key>$directory:</key>
<value>$addressbooks:ldap-directory/</value>
</substitution>
<!-- POST add-member URI suffix -->
<substitution>
<key>$add-member:</key>
<value>;add-member</value>
</substitution>
<!-- user id for admin user -->
<substitution>
<key>$useradmin:</key>
<value>admin</value>
</substitution>
<!-- guid for admin user -->
<substitution>
<key>$useradminguid:</key>
<value>admin</value>
</substitution>
<!-- password for admin user -->
<substitution>
<key>$pswdadmin:</key>
<value>admin</value>
</substitution>
<!-- relative path to admin principal resource-->
<substitution>
<key>$principal_admin:</key>
<value>$principalcollection:$useradmin:/</value>
</substitution>
<substitution>
<key>$principaluri_admin:</key>
<value>$principalcollection:$useradminguid:/</value>
</substitution>
<!-- user id for apprentice user -->
<substitution>
<key>$userapprentice:</key>
<value>apprentice</value>
</substitution>
<!-- guid for apprentice user -->
<substitution>
<key>$userapprenticeguid:</key>
<value>apprentice</value>
</substitution>
<!-- password for apprentice user -->
<substitution>
<key>$pswdapprentice:</key>
<value>apprentice</value>
</substitution>
<!-- relative path to apprentice principal resource-->
<substitution>
<key>$principal_apprentice:</key>
<value>$principalcollection:$userapprentice:/</value>
</substitution>
<substitution>
<key>$principaluri_apprentice:</key>
<value>$principalcollection:$userapprenticeguid:/</value>
</substitution>
<!-- user id for proxy user -->
<substitution>
<key>$userproxy:</key>
<value>superuser</value>
</substitution>
<!-- password for proxy user -->
<substitution>
<key>$pswdproxy:</key>
<value>superuser</value>
</substitution>
<!-- User accounts -->
<repeat count="2">
<!-- user id -->
<substitution>
<key>$userid%d:</key>
<value>dav.user%02d@example.org</value>
</substitution>
<!-- user guid -->
<substitution>
<key>$userguid%d:</key>
<value>$userid%d:</value>
</substitution>
<!-- user name -->
<substitution>
<key>$username%d:</key>
<value>DAV User%02d</value>
</substitution>
<!-- user name URI encoded -->
<substitution>
<key>$username-encoded%d:</key>
<value>DAV%%20User%02d</value>
</substitution>
<!-- first name -->
<substitution>
<key>$firstname%d:</key>
<value>DAV</value>
</substitution>
<!-- last name -->
<substitution>
<key>$lastname%d:</key>
<value>User %02d</value>
</substitution>
<!-- password -->
<substitution>
<key>$pswd%d:</key>
<value>12345</value>
</substitution>
<!-- relative path to user principal resource-->
<substitution>
<key>$principal%d:</key>
<value>$principalcollection:$userid%d:/</value>
</substitution>
<substitution>
<key>$principaluri%d:</key>
- <value>$principalcollection:dav.user%02d%%40example.org/</value>
+ <value>$principalcollection:dav.user%02d@example.org/</value>
</substitution>
<substitution>
<key>$principal%dnoslash:</key>
<value>$principalcollection:$userid%d:</value>
</substitution>
<!-- relative path to user calendar home-->
<substitution>
<key>$calendarhome%d:</key>
<value>$calendars:$userguid%d:</value>
</substitution>
<!-- relative path to user alternate calendar home-->
<substitution>
<key>$calendarhomealt%d:</key>
<value>$calendars:$userid%d:</value>
</substitution>
<!-- relative path to user calendar-->
<substitution>
<key>$calendarpath%d:</key>
<value>$calendarhome%d:/$calendar:</value>
</substitution>
<!-- relative path to user alternate calendar-->
<substitution>
<key>$calendarpathalt%d:</key>
<value>$calendarhomealt%d:/$calendar:</value>
</substitution>
<!-- relative path to user tasks calendar-->
<substitution>
<key>$taskspath%d:</key>
<value>$calendarhome%d:/$tasks:</value>
</substitution>
<!-- relative path to user inbox-->
<substitution>
<key>$inboxpath%d:</key>
<value>$calendarhome%d:/$inbox:</value>
</substitution>
<!-- relative path to user outbox-->
<substitution>
<key>$outboxpath%d:</key>
<value>$calendarhome%d:/$outbox:</value>
</substitution>
<!-- relative path to user dropbox-->
<substitution>
<key>$dropboxpath%d:</key>
<value>$calendarhome%d:/$dropbox:</value>
</substitution>
<!-- relative path to user notification-->
<substitution>
<key>$notificationpath%d:</key>
<value>$calendarhome%d:/$notification:</value>
</substitution>
<!-- relative path to user freebusy-->
<substitution>
<key>$freebusypath%d:</key>
<value>$calendarhome%d:/$freebusy:</value>
</substitution>
<substitution>
<key>$email%d:</key>
<value>$userid%d:</value>
</substitution>
<!-- calendar user address of user-->
<substitution>
<key>$cuaddr%d:</key>
<value>mailto:$email%d:</value>
</substitution>
<substitution>
<key>$cuaddralt%d:</key>
<value>$principaluri%d:</value>
</substitution>
<substitution>
<key>$cuaddraltnoslash%d:</key>
<value>$principals_uids:$userguid%d:</value>
</substitution>
<substitution>
<key>$cuaddrurn%d:</key>
<value>urn:uuid:$userguid%d:</value>
</substitution>
<!-- relative path to user addressbook home-->
<substitution>
<key>$addressbookhome%d:</key>
<value>$addressbooks:$userguid%d:</value>
</substitution>
<!-- relative path to user addressbook-->
<substitution>
<key>$addressbookpath%d:</key>
<value>$addressbooks:$userguid%d:/$addressbook:</value>
</substitution>
<!-- directory name -->
<substitution>
<key>$directory%d:</key>
<value>$addressbooks:$userguid%d:/ldap-directory/</value>
</substitution>
</repeat>
<!-- Public accounts -->
<repeat count="0">
<!-- user id -->
<substitution>
<key>$publicuserid%d:</key>
<value>public%02d</value>
</substitution>
<!-- user guid -->
<substitution>
<key>$publicuserguid%d:</key>
<value>public%02d</value>
</substitution>
<!-- user name -->
<substitution>
<key>$publicusername%d:</key>
<value>Public %02d</value>
</substitution>
<!-- password -->
<substitution>
<key>$publicpswd%d:</key>
<value>public%02d</value>
</substitution>
<!-- relative path to user principal resource-->
<substitution>
<key>$publicprincipal%d:</key>
<value>$principals_users:$publicuserid%d:/</value>
</substitution>
<substitution>
<key>$publicprincipaluri%d:</key>
<value>$principals_uids:$publicuserguid%d:/</value>
</substitution>
<!-- relative path to user calendar home-->
<substitution>
<key>$publiccalendarhome%d:</key>
<value>$calendars:$publicuserguid%d:</value>
</substitution>
<!-- relative path to user calendar-->
<substitution>
<key>$publiccalendarpath%d:</key>
<value>$calendars:$publicuserguid%d:/$calendar:</value>
</substitution>
<substitution>
<key>$publicemail%d:</key>
<value>$publicuserid%d:@example.com</value>
</substitution>
<!-- calendar user address of user-->
<substitution>
<key>$publiccuaddr%d:</key>
<value>mailto:$publicemail%d:</value>
</substitution>
<substitution>
<key>$publiccuaddralt%d:</key>
<value>$publicprincipaluri%d:</value>
</substitution>
<substitution>
<key>$publiccuaddrurn%d:</key>
<value>urn:uuid:$publicuserguid%d:</value>
</substitution>
</repeat>
<!-- Resource accounts -->
<repeat count="0">
<substitution>
<key>$resourceid%d:</key>
<value>resource%02d</value>
</substitution>
<!-- resource guid-->
<substitution>
<key>$resourceguid%d:</key>
<value>resource%02d</value>
</substitution>
<!-- resource name-->
<substitution>
<key>$resourcename%d:</key>
<value>Resource %02d</value>
</substitution>
<!-- relative path to first resource calendar home-->
<substitution>
<key>$rcalendarhome%d:</key>
<value>$calendars:$resourceguid%d:</value>
</substitution>
<!-- relative path to first resource calendar home-->
<substitution>
<key>$rcalendarpath%d:</key>
<value>$calendars:$resourceguid%d:/$calendar:</value>
</substitution>
<!-- relative path to first resource inbox-->
<substitution>
<key>$rinboxpath%d:</key>
<value>$calendars:$resourceguid%d:/$inbox:</value>
</substitution>
<!-- relative path to first resource outbox-->
<substitution>
<key>$routboxpath%d:</key>
<value>$calendars:$resourceguid%d:/$outbox:</value>
</substitution>
<!-- relative path to first resource principal resource-->
<substitution>
<key>$rprincipal%d:</key>
<value>$principals_resources:$resourceid%d:/</value>
</substitution>
<substitution>
<key>$rprincipaluri%d:</key>
<value>$principals_uids:$resourceguid%d:/</value>
</substitution>
<substitution>
<key>$rcuaddralt%d:</key>
<value>$rprincipaluri%d:</value>
</substitution>
<substitution>
<key>$rcuaddrurn%d:</key>
<value>urn:uuid:$resourceguid%d:</value>
</substitution>
</repeat>
<!-- Location accounts -->
<repeat count="0">
<substitution>
<key>$locationid%d:</key>
<value>location%02d</value>
</substitution>
<!-- location guid-->
<substitution>
<key>$locationguid%d:</key>
<value>location%02d</value>
</substitution>
<!-- location name-->
<substitution>
<key>$locationname%d:</key>
<value>Location %02d</value>
</substitution>
<!-- relative path to first location calendar home-->
<substitution>
<key>$lcalendarhome%d:</key>
<value>$calendars:$locationguid%d:</value>
</substitution>
<!-- relative path to first location calendar home-->
<substitution>
<key>$lcalendarpath%d:</key>
<value>$calendars:$locationguid%d:/$calendar:</value>
</substitution>
<!-- relative path to first location inbox-->
<substitution>
<key>$linboxpath%d:</key>
<value>$calendars:$locationguid%d:/$inbox:</value>
</substitution>
<!-- relative path to first location outbox-->
<substitution>
<key>$loutboxpath%d:</key>
<value>$calendars:$locationguid%d:/$outbox:</value>
</substitution>
<!-- relative path to first location principal resource-->
<substitution>
<key>$lprincipal%d:</key>
<value>$principals_resources:$locationid%d:/</value>
</substitution>
<substitution>
<key>$lprincipaluri%d:</key>
<value>$principals_uids:$locationguid%d:/</value>
</substitution>
<substitution>
<key>$lcuaddralt%d:</key>
<value>$lprincipaluri%d:</value>
</substitution>
<substitution>
<key>$lcuaddrurn%d:</key>
<value>urn:uuid:$locationguid%d:</value>
</substitution>
</repeat>
<!-- Group accounts -->
<repeat count="0">
<substitution>
<key>$groupid%d:</key>
<value>group%02d</value>
</substitution>
<!-- group guid-->
<substitution>
<key>$groupguid%d:</key>
<value>group%02d</value>
</substitution>
<!-- group name-->
<substitution>
<key>$groupname%d:</key>
<value>Group %02d</value>
</substitution>
<!-- relative path to first group principal resource-->
<substitution>
<key>$gprincipal%d:</key>
<value>$principals_resources:$groupid%d:/</value>
</substitution>
<substitution>
<key>$gprincipaluri%d:</key>
<value>$principals_uids:$groupguid%d:/</value>
</substitution>
<substitution>
<key>$gcuaddralt%d:</key>
<value>$gprincipaluri%d:</value>
</substitution>
<substitution>
<key>$gcuaddrurn%d:</key>
<value>urn:uuid:$groupguid%d:</value>
</substitution>
</repeat>
<!-- User with non-ascii name -->
<substitution>
<key>$i18nid:</key>
<value>i18nuser</value>
</substitution>
<!-- group guid-->
<substitution>
<key>$i18nguid:</key>
<value>i18nuser</value>
</substitution>
<!-- group name-->
<substitution>
<key>$i18nname:</key>
<value>まだ</value>
</substitution>
<!-- password -->
<substitution>
<key>$i18npswd:</key>
<value>i18nuser</value>
</substitution>
<!-- relative path to user calendar-->
<substitution>
<key>$i18ncalendarpath:</key>
<value>$calendars:$i18nguid:/$calendar:</value>
</substitution>
<substitution>
<key>$i18nemail:</key>
<value>$i18nid:@example.com</value>
</substitution>
<!-- CUAddrs -->
<substitution>
<key>$i18ncuaddr:</key>
<value>mailto:$i18nemail:</value>
</substitution>
<substitution>
<key>$i18ncuaddrurn:</key>
<value>urn:uuid:$i18nguid:</value>
</substitution>
<!-- relative path to disabled group principal resource-->
<substitution>
<key>$principaldisabled:</key>
<value>$principals_groups:disabledgroup/</value>
</substitution>
<substitution>
<key>$principaluridisabled:</key>
<value>$principals_uids:disabledgroup/</value>
</substitution>
<!-- calendar user address of disabled group-->
<substitution>
<key>$cuaddrdisabled:</key>
<value>$principals_uids:disabledgroup/</value>
</substitution>
<!-- calendar user address of second user-->
<substitution>
<key>$cuaddr2:</key>
<value>MAILTO:$email2:</value>
</substitution>
</substitutions>
</serverinfo>

File Metadata

Mime Type
text/x-diff
Expires
Sat, Jan 31, 9:53 AM (22 h, 20 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426232
Default Alt Text
(357 KB)

Event Timeline