From c739e047c1a1532007bd7aab01218ac99db1c1bd Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Mon, 10 Nov 2014 16:32:15 +0100 Subject: [PATCH 1/7] Replace vendorized Sabre/VObject 2.1 lib with a dependency to VObject 3.3 (#3881); adapt libvcalendar + tests to the new API --- plugins/libcalendaring/composer.json | 5 +- .../lib/Sabre/VObject/Component.php | 405 ------ .../lib/Sabre/VObject/Component/VAlarm.php | 108 -- .../lib/Sabre/VObject/Component/VCalendar.php | 244 ---- .../lib/Sabre/VObject/Component/VCard.php | 107 -- .../lib/Sabre/VObject/Component/VEvent.php | 70 - .../lib/Sabre/VObject/Component/VFreeBusy.php | 68 - .../lib/Sabre/VObject/Component/VJournal.php | 46 - .../lib/Sabre/VObject/Component/VTodo.php | 68 - .../lib/Sabre/VObject/DateTimeParser.php | 181 --- .../lib/Sabre/VObject/Document.php | 109 -- .../lib/Sabre/VObject/ElementList.php | 172 --- .../lib/Sabre/VObject/FreeBusyGenerator.php | 322 ----- .../libcalendaring/lib/Sabre/VObject/Node.php | 187 --- .../lib/Sabre/VObject/Parameter.php | 102 -- .../lib/Sabre/VObject/ParseException.php | 12 - .../lib/Sabre/VObject/Property.php | 453 ------- .../lib/Sabre/VObject/Property/Compound.php | 125 -- .../lib/Sabre/VObject/Property/DateTime.php | 245 ---- .../Sabre/VObject/Property/MultiDateTime.php | 180 --- .../lib/Sabre/VObject/Reader.php | 223 ---- .../lib/Sabre/VObject/RecurrenceIterator.php | 1144 ----------------- .../lib/Sabre/VObject/Splitter/ICalendar.php | 111 -- .../VObject/Splitter/SplitterInterface.php | 39 - .../lib/Sabre/VObject/Splitter/VCard.php | 76 -- .../lib/Sabre/VObject/StringUtil.php | 61 - .../lib/Sabre/VObject/TimeZoneUtil.php | 482 ------- .../lib/Sabre/VObject/Version.php | 24 - .../lib/Sabre/VObject/includes.php | 41 - .../libcalendaring/lib/get_sabre_vobject.sh | 12 - .../lib/sabre-vobject-2.1.0.tar.gz | Bin 86355 -> 0 bytes plugins/libcalendaring/libvcalendar.php | 343 ++--- plugins/libcalendaring/tests/libvcalendar.php | 24 +- 33 files changed, 191 insertions(+), 5598 deletions(-) delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Component.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Document.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/ElementList.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Node.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Parameter.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/ParseException.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Property.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Reader.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Splitter/VCard.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/TimeZoneUtil.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/Version.php delete mode 100644 plugins/libcalendaring/lib/Sabre/VObject/includes.php delete mode 100755 plugins/libcalendaring/lib/get_sabre_vobject.sh delete mode 100644 plugins/libcalendaring/lib/sabre-vobject-2.1.0.tar.gz diff --git a/plugins/libcalendaring/composer.json b/plugins/libcalendaring/composer.json index 07b8a660..ea50ad1b 100644 --- a/plugins/libcalendaring/composer.json +++ b/plugins/libcalendaring/composer.json @@ -19,7 +19,8 @@ } ], "require": { - "php": ">=5.3.0", - "roundcube/plugin-installer": ">=0.1.3" + "php": ">=5.4.0", + "roundcube/plugin-installer": ">=0.1.3", + "sabre/vobject": "~3.3.3" } } diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component.php b/plugins/libcalendaring/lib/Sabre/VObject/Component.php deleted file mode 100644 index 1c1d9244..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Component.php +++ /dev/null @@ -1,405 +0,0 @@ - 'Sabre\\VObject\\Component\\VAlarm', - 'VCALENDAR' => 'Sabre\\VObject\\Component\\VCalendar', - 'VCARD' => 'Sabre\\VObject\\Component\\VCard', - 'VEVENT' => 'Sabre\\VObject\\Component\\VEvent', - 'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal', - 'VTODO' => 'Sabre\\VObject\\Component\\VTodo', - 'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy', - ); - - /** - * Creates the new component by name, but in addition will also see if - * there's a class mapped to the property name. - * - * @param string $name - * @param string $value - * @return Component - */ - static public function create($name, $value = null) { - - $name = strtoupper($name); - - if (isset(self::$classMap[$name])) { - return new self::$classMap[$name]($name, $value); - } else { - return new self($name, $value); - } - - } - - /** - * Creates a new component. - * - * By default this object will iterate over its own children, but this can - * be overridden with the iterator argument - * - * @param string $name - * @param ElementList $iterator - */ - public function __construct($name, ElementList $iterator = null) { - - $this->name = strtoupper($name); - if (!is_null($iterator)) $this->iterator = $iterator; - - } - - /** - * Turns the object back into a serialized blob. - * - * @return string - */ - public function serialize() { - - $str = "BEGIN:" . $this->name . "\r\n"; - - /** - * Gives a component a 'score' for sorting purposes. - * - * This is solely used by the childrenSort method. - * - * A higher score means the item will be lower in the list. - * To avoid score collisions, each "score category" has a reasonable - * space to accomodate elements. The $key is added to the $score to - * preserve the original relative order of elements. - * - * @param int $key - * @param array $array - * @return int - */ - $sortScore = function($key, $array) { - - if ($array[$key] instanceof Component) { - - // We want to encode VTIMEZONE first, this is a personal - // preference. - if ($array[$key]->name === 'VTIMEZONE') { - $score=300000000; - return $score+$key; - } else { - $score=400000000; - return $score+$key; - } - } else { - // Properties get encoded first - // VCARD version 4.0 wants the VERSION property to appear first - if ($array[$key] instanceof Property) { - if ($array[$key]->name === 'VERSION') { - $score=100000000; - return $score+$key; - } else { - // All other properties - $score=200000000; - return $score+$key; - } - } - } - - }; - - $tmp = $this->children; - uksort($this->children, function($a, $b) use ($sortScore, $tmp) { - - $sA = $sortScore($a, $tmp); - $sB = $sortScore($b, $tmp); - - if ($sA === $sB) return 0; - - return ($sA < $sB) ? -1 : 1; - - }); - - foreach($this->children as $child) $str.=$child->serialize(); - $str.= "END:" . $this->name . "\r\n"; - - return $str; - - } - - /** - * Adds a new component or element - * - * You can call this method with the following syntaxes: - * - * add(Node $node) - * add(string $name, $value, array $parameters = array()) - * - * The first version adds an Element - * The second adds a property as a string. - * - * @param mixed $item - * @param mixed $itemValue - * @return void - */ - public function add($item, $itemValue = null, array $parameters = array()) { - - if ($item instanceof Node) { - if (!is_null($itemValue)) { - throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); - } - $item->parent = $this; - $this->children[] = $item; - } elseif(is_string($item)) { - - $item = Property::create($item,$itemValue, $parameters); - $item->parent = $this; - $this->children[] = $item; - - } else { - - throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); - - } - - } - - /** - * Returns an iterable list of children - * - * @return ElementList - */ - public function children() { - - return new ElementList($this->children); - - } - - /** - * Returns an array with elements that match the specified name. - * - * This function is also aware of MIME-Directory groups (as they appear in - * vcards). This means that if a property is grouped as "HOME.EMAIL", it - * will also be returned when searching for just "EMAIL". If you want to - * search for a property in a specific group, you can select on the entire - * string ("HOME.EMAIL"). If you want to search on a specific property that - * has not been assigned a group, specify ".EMAIL". - * - * Keys are retained from the 'children' array, which may be confusing in - * certain cases. - * - * @param string $name - * @return array - */ - public function select($name) { - - $group = null; - $name = strtoupper($name); - if (strpos($name,'.')!==false) { - list($group,$name) = explode('.', $name, 2); - } - - $result = array(); - foreach($this->children as $key=>$child) { - - if ( - strtoupper($child->name) === $name && - (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group)) - ) { - - $result[$key] = $child; - - } - } - - reset($result); - return $result; - - } - - /** - * This method only returns a list of sub-components. Properties are - * ignored. - * - * @return array - */ - public function getComponents() { - - $result = array(); - foreach($this->children as $child) { - if ($child instanceof Component) { - $result[] = $child; - } - } - - return $result; - - } - - /** - * Validates the node for correctness. - * - * The following options are supported: - * - Node::REPAIR - If something is broken, and automatic repair may - * be attempted. - * - * An array is returned with warnings. - * - * Every item in the array has the following properties: - * * level - (number between 1 and 3 with severity information) - * * message - (human readable message) - * * node - (reference to the offending node) - * - * @param int $options - * @return array - */ - public function validate($options = 0) { - - $result = array(); - foreach($this->children as $child) { - $result = array_merge($result, $child->validate($options)); - } - return $result; - - } - - /* Magic property accessors {{{ */ - - /** - * Using 'get' you will either get a property or component, - * - * If there were no child-elements found with the specified name, - * null is returned. - * - * @param string $name - * @return Property - */ - public function __get($name) { - - $matches = $this->select($name); - if (count($matches)===0) { - return null; - } else { - $firstMatch = current($matches); - /** @var $firstMatch Property */ - $firstMatch->setIterator(new ElementList(array_values($matches))); - return $firstMatch; - } - - } - - /** - * This method checks if a sub-element with the specified name exists. - * - * @param string $name - * @return bool - */ - public function __isset($name) { - - $matches = $this->select($name); - return count($matches)>0; - - } - - /** - * Using the setter method you can add properties or subcomponents - * - * You can either pass a Component, Property - * object, or a string to automatically create a Property. - * - * If the item already exists, it will be removed. If you want to add - * a new item with the same name, always use the add() method. - * - * @param string $name - * @param mixed $value - * @return void - */ - public function __set($name, $value) { - - $matches = $this->select($name); - $overWrite = count($matches)?key($matches):null; - - if ($value instanceof Component || $value instanceof Property) { - $value->parent = $this; - if (!is_null($overWrite)) { - $this->children[$overWrite] = $value; - } else { - $this->children[] = $value; - } - } elseif (is_scalar($value)) { - $property = Property::create($name,$value); - $property->parent = $this; - if (!is_null($overWrite)) { - $this->children[$overWrite] = $property; - } else { - $this->children[] = $property; - } - } else { - throw new \InvalidArgumentException('You must pass a \\Sabre\\VObject\\Component, \\Sabre\\VObject\\Property or scalar type'); - } - - } - - /** - * Removes all properties and components within this component. - * - * @param string $name - * @return void - */ - public function __unset($name) { - - $matches = $this->select($name); - foreach($matches as $k=>$child) { - - unset($this->children[$k]); - $child->parent = null; - - } - - } - - /* }}} */ - - /** - * This method is automatically called when the object is cloned. - * Specifically, this will ensure all child elements are also cloned. - * - * @return void - */ - public function __clone() { - - foreach($this->children as $key=>$child) { - $this->children[$key] = clone $child; - $this->children[$key]->parent = $this; - } - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php deleted file mode 100644 index 2f86c44f..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php +++ /dev/null @@ -1,108 +0,0 @@ -TRIGGER; - if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') { - $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); - $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START'; - - $parentComponent = $this->parent; - if ($related === 'START') { - - if ($parentComponent->name === 'VTODO') { - $propName = 'DUE'; - } else { - $propName = 'DTSTART'; - } - - $effectiveTrigger = clone $parentComponent->$propName->getDateTime(); - $effectiveTrigger->add($triggerDuration); - } else { - if ($parentComponent->name === 'VTODO') { - $endProp = 'DUE'; - } elseif ($parentComponent->name === 'VEVENT') { - $endProp = 'DTEND'; - } else { - throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); - } - - if (isset($parentComponent->$endProp)) { - $effectiveTrigger = clone $parentComponent->$endProp->getDateTime(); - $effectiveTrigger->add($triggerDuration); - } elseif (isset($parentComponent->DURATION)) { - $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); - $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); - $effectiveTrigger->add($duration); - $effectiveTrigger->add($triggerDuration); - } else { - $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); - $effectiveTrigger->add($triggerDuration); - } - } - } else { - $effectiveTrigger = $trigger->getDateTime(); - } - return $effectiveTrigger; - - } - - /** - * Returns true or false depending on if the event falls in the specified - * time-range. This is used for filtering purposes. - * - * The rules used to determine if an event falls within the specified - * time-range is based on the CalDAV specification. - * - * @param \DateTime $start - * @param \DateTime $end - * @return bool - */ - public function isInTimeRange(\DateTime $start, \DateTime $end) { - - $effectiveTrigger = $this->getEffectiveTriggerTime(); - - if (isset($this->DURATION)) { - $duration = VObject\DateTimeParser::parseDuration($this->DURATION); - $repeat = (string)$this->repeat; - if (!$repeat) { - $repeat = 1; - } - - $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat); - - foreach($period as $occurrence) { - - if ($start <= $occurrence && $end > $occurrence) { - return true; - } - } - return false; - } else { - return ($start <= $effectiveTrigger && $end > $effectiveTrigger); - } - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php deleted file mode 100644 index 9de67982..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php +++ /dev/null @@ -1,244 +0,0 @@ -children as $component) { - - if (!$component instanceof VObject\Component) - continue; - - if (isset($component->{'RECURRENCE-ID'})) - continue; - - if ($componentName && $component->name !== strtoupper($componentName)) - continue; - - if ($component->name === 'VTIMEZONE') - continue; - - $components[] = $component; - - } - - return $components; - - } - - /** - * If this calendar object, has events with recurrence rules, this method - * can be used to expand the event into multiple sub-events. - * - * Each event will be stripped from it's recurrence information, and only - * the instances of the event in the specified timerange will be left - * alone. - * - * In addition, this method will cause timezone information to be stripped, - * and normalized to UTC. - * - * This method will alter the VCalendar. This cannot be reversed. - * - * This functionality is specifically used by the CalDAV standard. It is - * possible for clients to request expand events, if they are rather simple - * clients and do not have the possibility to calculate recurrences. - * - * @param DateTime $start - * @param DateTime $end - * @return void - */ - public function expand(\DateTime $start, \DateTime $end) { - - $newEvents = array(); - - foreach($this->select('VEVENT') as $key=>$vevent) { - - if (isset($vevent->{'RECURRENCE-ID'})) { - unset($this->children[$key]); - continue; - } - - - if (!$vevent->rrule) { - unset($this->children[$key]); - if ($vevent->isInTimeRange($start, $end)) { - $newEvents[] = $vevent; - } - continue; - } - - $uid = (string)$vevent->uid; - if (!$uid) { - throw new \LogicException('Event did not have a UID!'); - } - - $it = new VObject\RecurrenceIterator($this, $vevent->uid); - $it->fastForward($start); - - while($it->valid() && $it->getDTStart() < $end) { - - if ($it->getDTEnd() > $start) { - - $newEvents[] = $it->getEventObject(); - - } - $it->next(); - - } - unset($this->children[$key]); - - } - - foreach($newEvents as $newEvent) { - - foreach($newEvent->children as $child) { - if ($child instanceof VObject\Property\DateTime && - $child->getDateType() == VObject\Property\DateTime::LOCALTZ) { - $child->setDateTime($child->getDateTime(),VObject\Property\DateTime::UTC); - } - } - - $this->add($newEvent); - - } - - // Removing all VTIMEZONE components - unset($this->VTIMEZONE); - - } - - /** - * Validates the node for correctness. - * An array is returned with warnings. - * - * Every item in the array has the following properties: - * * level - (number between 1 and 3 with severity information) - * * message - (human readable message) - * * node - (reference to the offending node) - * - * @return array - */ - /* - public function validate() { - - $warnings = array(); - - $version = $this->select('VERSION'); - if (count($version)!==1) { - $warnings[] = array( - 'level' => 1, - 'message' => 'The VERSION property must appear in the VCALENDAR component exactly 1 time', - 'node' => $this, - ); - } else { - if ((string)$this->VERSION !== '2.0') { - $warnings[] = array( - 'level' => 1, - 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', - 'node' => $this, - ); - } - } - $version = $this->select('PRODID'); - if (count($version)!==1) { - $warnings[] = array( - 'level' => 2, - 'message' => 'The PRODID property must appear in the VCALENDAR component exactly 1 time', - 'node' => $this, - ); - } - if (count($this->CALSCALE) > 1) { - $warnings[] = array( - 'level' => 2, - 'message' => 'The CALSCALE property must not be specified more than once.', - 'node' => $this, - ); - } - if (count($this->METHOD) > 1) { - $warnings[] = array( - 'level' => 2, - 'message' => 'The METHOD property must not be specified more than once.', - 'node' => $this, - ); - } - - $allowedComponents = array( - 'VEVENT', - 'VTODO', - 'VJOURNAL', - 'VFREEBUSY', - 'VTIMEZONE', - ); - $allowedProperties = array( - 'PRODID', - 'VERSION', - 'CALSCALE', - 'METHOD', - ); - $componentsFound = 0; - foreach($this->children as $child) { - if($child instanceof Component) { - $componentsFound++; - if (!in_array($child->name, $allowedComponents)) { - $warnings[] = array( - 'level' => 1, - 'message' => 'The ' . $child->name . " component is not allowed in the VCALENDAR component", - 'node' => $this, - ); - } - } - if ($child instanceof Property) { - if (!in_array($child->name, $allowedProperties)) { - $warnings[] = array( - 'level' => 2, - 'message' => 'The ' . $child->name . " property is not allowed in the VCALENDAR component", - 'node' => $this, - ); - } - } - } - - if ($componentsFound===0) { - $warnings[] = array( - 'level' => 1, - 'message' => 'An iCalendar object must have at least 1 component.', - 'node' => $this, - ); - } - - return array_merge( - $warnings, - parent::validate() - ); - - } - */ - -} - diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php deleted file mode 100644 index 0fc8b702..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php +++ /dev/null @@ -1,107 +0,0 @@ -select('VERSION'); - if (count($version)!==1) { - $warnings[] = array( - 'level' => 1, - 'message' => 'The VERSION property must appear in the VCARD component exactly 1 time', - 'node' => $this, - ); - if ($options & self::REPAIR) { - $this->VERSION = self::DEFAULT_VERSION; - } - } else { - $version = (string)$this->VERSION; - if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') { - $warnings[] = array( - 'level' => 1, - 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', - 'node' => $this, - ); - if ($options & self::REPAIR) { - $this->VERSION = '4.0'; - } - } - - } - $fn = $this->select('FN'); - if (count($fn)!==1) { - $warnings[] = array( - 'level' => 1, - 'message' => 'The FN property must appear in the VCARD component exactly 1 time', - 'node' => $this, - ); - if (($options & self::REPAIR) && count($fn) === 0) { - // We're going to try to see if we can use the contents of the - // N property. - if (isset($this->N)) { - $value = explode(';', (string)$this->N); - if (isset($value[1]) && $value[1]) { - $this->FN = $value[1] . ' ' . $value[0]; - } else { - $this->FN = $value[0]; - } - - // Otherwise, the ORG property may work - } elseif (isset($this->ORG)) { - $this->FN = (string)$this->ORG; - } - - } - } - - return array_merge( - parent::validate($options), - $warnings - ); - - } - -} - diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php deleted file mode 100644 index 2375c531..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php +++ /dev/null @@ -1,70 +0,0 @@ -RRULE) { - $it = new VObject\RecurrenceIterator($this); - $it->fastForward($start); - - // We fast-forwarded to a spot where the end-time of the - // recurrence instance exceeded the start of the requested - // time-range. - // - // If the starttime of the recurrence did not exceed the - // end of the time range as well, we have a match. - return ($it->getDTStart() < $end && $it->getDTEnd() > $start); - - } - - $effectiveStart = $this->DTSTART->getDateTime(); - if (isset($this->DTEND)) { - - // The DTEND property is considered non inclusive. So for a 3 day - // event in july, dtstart and dtend would have to be July 1st and - // July 4th respectively. - // - // See: - // http://tools.ietf.org/html/rfc5545#page-54 - $effectiveEnd = $this->DTEND->getDateTime(); - - } elseif (isset($this->DURATION)) { - $effectiveEnd = clone $effectiveStart; - $effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) ); - } elseif ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) { - $effectiveEnd = clone $effectiveStart; - $effectiveEnd->modify('+1 day'); - } else { - $effectiveEnd = clone $effectiveStart; - } - return ( - ($start <= $effectiveEnd) && ($end > $effectiveStart) - ); - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php deleted file mode 100644 index 7afe9fdb..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php +++ /dev/null @@ -1,68 +0,0 @@ -select('FREEBUSY') as $freebusy) { - - // We are only interested in FBTYPE=BUSY (the default), - // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. - if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') { - continue; - } - - // The freebusy component can hold more than 1 value, separated by - // commas. - $periods = explode(',', (string)$freebusy); - - foreach($periods as $period) { - // Every period is formatted as [start]/[end]. The start is an - // absolute UTC time, the end may be an absolute UTC time, or - // duration (relative) value. - list($busyStart, $busyEnd) = explode('/', $period); - - $busyStart = VObject\DateTimeParser::parse($busyStart); - $busyEnd = VObject\DateTimeParser::parse($busyEnd); - if ($busyEnd instanceof \DateInterval) { - $tmp = clone $busyStart; - $tmp->add($busyEnd); - $busyEnd = $tmp; - } - - if($start < $busyEnd && $end > $busyStart) { - return false; - } - - } - - } - - return true; - - } - -} - diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php deleted file mode 100644 index 23288787..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php +++ /dev/null @@ -1,46 +0,0 @@ -DTSTART)?$this->DTSTART->getDateTime():null; - if ($dtstart) { - $effectiveEnd = clone $dtstart; - if ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) { - $effectiveEnd->modify('+1 day'); - } - - return ($start <= $effectiveEnd && $end > $dtstart); - - } - return false; - - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php deleted file mode 100644 index b1579cf7..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php +++ /dev/null @@ -1,68 +0,0 @@ -DTSTART)?$this->DTSTART->getDateTime():null; - $duration = isset($this->DURATION)?VObject\DateTimeParser::parseDuration($this->DURATION):null; - $due = isset($this->DUE)?$this->DUE->getDateTime():null; - $completed = isset($this->COMPLETED)?$this->COMPLETED->getDateTime():null; - $created = isset($this->CREATED)?$this->CREATED->getDateTime():null; - - if ($dtstart) { - if ($duration) { - $effectiveEnd = clone $dtstart; - $effectiveEnd->add($duration); - return $start <= $effectiveEnd && $end > $dtstart; - } elseif ($due) { - return - ($start < $due || $start <= $dtstart) && - ($end > $dtstart || $end >= $due); - } else { - return $start <= $dtstart && $end > $dtstart; - } - } - if ($due) { - return ($start < $due && $end >= $due); - } - if ($completed && $created) { - return - ($start <= $created || $start <= $completed) && - ($end >= $created || $end >= $completed); - } - if ($completed) { - return ($start <= $completed && $end >= $completed); - } - if ($created) { - return ($end > $created); - } - return true; - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php b/plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php deleted file mode 100644 index 03600506..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php +++ /dev/null @@ -1,181 +0,0 @@ -setTimeZone(new \DateTimeZone('UTC')); - return $date; - - } - - /** - * Parses an iCalendar (rfc5545) formatted date and returns a DateTime object - * - * @param string $date - * @return DateTime - */ - static public function parseDate($date) { - - // Format is YYYYMMDD - $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches); - - if (!$result) { - throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date); - } - - $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new \DateTimeZone('UTC')); - return $date; - - } - - /** - * Parses an iCalendar (RFC5545) formatted duration value. - * - * This method will either return a DateTimeInterval object, or a string - * suitable for strtotime or DateTime::modify. - * - * @param string $duration - * @param bool $asString - * @return DateInterval|string - */ - static public function parseDuration($duration, $asString = false) { - - $result = preg_match('/^(?P\+|-)?P((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$/', $duration, $matches); - if (!$result) { - throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration); - } - - if (!$asString) { - $invert = false; - if ($matches['plusminus']==='-') { - $invert = true; - } - - - $parts = array( - 'week', - 'day', - 'hour', - 'minute', - 'second', - ); - foreach($parts as $part) { - $matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0; - } - - - // We need to re-construct the $duration string, because weeks and - // days are not supported by DateInterval in the same string. - $duration = 'P'; - $days = $matches['day']; - if ($matches['week']) { - $days+=$matches['week']*7; - } - if ($days) - $duration.=$days . 'D'; - - if ($matches['minute'] || $matches['second'] || $matches['hour']) { - $duration.='T'; - - if ($matches['hour']) - $duration.=$matches['hour'].'H'; - - if ($matches['minute']) - $duration.=$matches['minute'].'M'; - - if ($matches['second']) - $duration.=$matches['second'].'S'; - - } - - if ($duration==='P') { - $duration = 'PT0S'; - } - $iv = new \DateInterval($duration); - if ($invert) $iv->invert = true; - - return $iv; - - } - - - - $parts = array( - 'week', - 'day', - 'hour', - 'minute', - 'second', - ); - - $newDur = ''; - foreach($parts as $part) { - if (isset($matches[$part]) && $matches[$part]) { - $newDur.=' '.$matches[$part] . ' ' . $part . 's'; - } - } - - $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur); - if ($newDur === '+') { $newDur = '+0 seconds'; }; - return $newDur; - - } - - /** - * Parses either a Date or DateTime, or Duration value. - * - * @param string $date - * @param DateTimeZone|string $referenceTZ - * @return DateTime|DateInterval - */ - static public function parse($date, $referenceTZ = null) { - - if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) { - return self::parseDuration($date); - } elseif (strlen($date)===8) { - return self::parseDate($date); - } else { - return self::parseDateTime($date, $referenceTZ); - } - - } - - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Document.php b/plugins/libcalendaring/lib/Sabre/VObject/Document.php deleted file mode 100644 index 50a662ee..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Document.php +++ /dev/null @@ -1,109 +0,0 @@ -value syntax, in which case - * properties will automatically be created, or you can just pass a list of - * Component and Property object. - * - * @param string $name - * @param array $children - * @return Component - */ - public function createComponent($name, array $children = array()) { - - $component = Component::create($name); - foreach($children as $k=>$v) { - - if ($v instanceof Node) { - $component->add($v); - } else { - $component->add($k, $v); - } - - } - return $component; - - } - - /** - * Factory method for creating new properties - * - * This method automatically searches for the correct property class, based - * on its name. - * - * You can specify the parameters either in key=>value syntax, in which case - * parameters will automatically be created, or you can just pass a list of - * Parameter objects. - * - * @param string $name - * @param mixed $value - * @param array $parameters - * @return Property - */ - public function createProperty($name, $value = null, array $parameters = array()) { - - return Property::create($name, $value, $parameters); - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/ElementList.php b/plugins/libcalendaring/lib/Sabre/VObject/ElementList.php deleted file mode 100644 index 1c203708..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/ElementList.php +++ /dev/null @@ -1,172 +0,0 @@ -vevent where there's multiple VEVENT objects. - * - * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). - * @author Evert Pot (http://evertpot.com/) - * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License - */ -class ElementList implements \Iterator, \Countable, \ArrayAccess { - - /** - * Inner elements - * - * @var array - */ - protected $elements = array(); - - /** - * Creates the element list. - * - * @param array $elements - */ - public function __construct(array $elements) { - - $this->elements = $elements; - - } - - /* {{{ Iterator interface */ - - /** - * Current position - * - * @var int - */ - private $key = 0; - - /** - * Returns current item in iteration - * - * @return Element - */ - public function current() { - - return $this->elements[$this->key]; - - } - - /** - * To the next item in the iterator - * - * @return void - */ - public function next() { - - $this->key++; - - } - - /** - * Returns the current iterator key - * - * @return int - */ - public function key() { - - return $this->key; - - } - - /** - * Returns true if the current position in the iterator is a valid one - * - * @return bool - */ - public function valid() { - - return isset($this->elements[$this->key]); - - } - - /** - * Rewinds the iterator - * - * @return void - */ - public function rewind() { - - $this->key = 0; - - } - - /* }}} */ - - /* {{{ Countable interface */ - - /** - * Returns the number of elements - * - * @return int - */ - public function count() { - - return count($this->elements); - - } - - /* }}} */ - - /* {{{ ArrayAccess Interface */ - - - /** - * Checks if an item exists through ArrayAccess. - * - * @param int $offset - * @return bool - */ - public function offsetExists($offset) { - - return isset($this->elements[$offset]); - - } - - /** - * Gets an item through ArrayAccess. - * - * @param int $offset - * @return mixed - */ - public function offsetGet($offset) { - - return $this->elements[$offset]; - - } - - /** - * Sets an item through ArrayAccess. - * - * @param int $offset - * @param mixed $value - * @return void - */ - public function offsetSet($offset,$value) { - - throw new \LogicException('You can not add new objects to an ElementList'); - - } - - /** - * Sets an item through ArrayAccess. - * - * This method just forwards the request to the inner iterator - * - * @param int $offset - * @return void - */ - public function offsetUnset($offset) { - - throw new \LogicException('You can not remove objects from an ElementList'); - - } - - /* }}} */ - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php b/plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php deleted file mode 100644 index 96d0be5a..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php +++ /dev/null @@ -1,322 +0,0 @@ -setTimeRange($start, $end); - } - - if ($objects) { - $this->setObjects($objects); - } - - } - - /** - * Sets the VCALENDAR object. - * - * If this is set, it will not be generated for you. You are responsible - * for setting things like the METHOD, CALSCALE, VERSION, etc.. - * - * The VFREEBUSY object will be automatically added though. - * - * @param Component $vcalendar - * @return void - */ - public function setBaseObject(Component $vcalendar) { - - $this->baseObject = $vcalendar; - - } - - /** - * Sets the input objects - * - * You must either specify a valendar object as a strong, or as the parse - * Component. - * It's also possible to specify multiple objects as an array. - * - * @param mixed $objects - * @return void - */ - public function setObjects($objects) { - - if (!is_array($objects)) { - $objects = array($objects); - } - - $this->objects = array(); - foreach($objects as $object) { - - if (is_string($object)) { - $this->objects[] = Reader::read($object); - } elseif ($object instanceof Component) { - $this->objects[] = $object; - } else { - throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects'); - } - - } - - } - - /** - * Sets the time range - * - * Any freebusy object falling outside of this time range will be ignored. - * - * @param DateTime $start - * @param DateTime $end - * @return void - */ - public function setTimeRange(\DateTime $start = null, \DateTime $end = null) { - - $this->start = $start; - $this->end = $end; - - } - - /** - * Parses the input data and returns a correct VFREEBUSY object, wrapped in - * a VCALENDAR. - * - * @return Component - */ - public function getResult() { - - $busyTimes = array(); - - foreach($this->objects as $object) { - - foreach($object->getBaseComponents() as $component) { - - switch($component->name) { - - case 'VEVENT' : - - $FBTYPE = 'BUSY'; - if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) { - break; - } - if (isset($component->STATUS)) { - $status = strtoupper($component->STATUS); - if ($status==='CANCELLED') { - break; - } - if ($status==='TENTATIVE') { - $FBTYPE = 'BUSY-TENTATIVE'; - } - } - - $times = array(); - - if ($component->RRULE) { - - $iterator = new RecurrenceIterator($object, (string)$component->uid); - if ($this->start) { - $iterator->fastForward($this->start); - } - - $maxRecurrences = 200; - - while($iterator->valid() && --$maxRecurrences) { - - $startTime = $iterator->getDTStart(); - if ($this->end && $startTime > $this->end) { - break; - } - $times[] = array( - $iterator->getDTStart(), - $iterator->getDTEnd(), - ); - - $iterator->next(); - - } - - } else { - - $startTime = $component->DTSTART->getDateTime(); - if ($this->end && $startTime > $this->end) { - break; - } - $endTime = null; - if (isset($component->DTEND)) { - $endTime = $component->DTEND->getDateTime(); - } elseif (isset($component->DURATION)) { - $duration = DateTimeParser::parseDuration((string)$component->DURATION); - $endTime = clone $startTime; - $endTime->add($duration); - } elseif ($component->DTSTART->getDateType() === Property\DateTime::DATE) { - $endTime = clone $startTime; - $endTime->modify('+1 day'); - } else { - // The event had no duration (0 seconds) - break; - } - - $times[] = array($startTime, $endTime); - - } - - foreach($times as $time) { - - if ($this->end && $time[0] > $this->end) break; - if ($this->start && $time[1] < $this->start) break; - - $busyTimes[] = array( - $time[0], - $time[1], - $FBTYPE, - ); - } - break; - - case 'VFREEBUSY' : - foreach($component->FREEBUSY as $freebusy) { - - $fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY'; - - // Skipping intervals marked as 'free' - if ($fbType==='FREE') - continue; - - $values = explode(',', $freebusy); - foreach($values as $value) { - list($startTime, $endTime) = explode('/', $value); - $startTime = DateTimeParser::parseDateTime($startTime); - - if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') { - $duration = DateTimeParser::parseDuration($endTime); - $endTime = clone $startTime; - $endTime->add($duration); - } else { - $endTime = DateTimeParser::parseDateTime($endTime); - } - - if($this->start && $this->start > $endTime) continue; - if($this->end && $this->end < $startTime) continue; - $busyTimes[] = array( - $startTime, - $endTime, - $fbType - ); - - } - - - } - break; - - - - } - - - } - - } - - if ($this->baseObject) { - $calendar = $this->baseObject; - } else { - $calendar = Component::create('VCALENDAR'); - $calendar->version = '2.0'; - $calendar->prodid = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN'; - $calendar->calscale = 'GREGORIAN'; - } - - $vfreebusy = Component::create('VFREEBUSY'); - $calendar->add($vfreebusy); - - if ($this->start) { - $dtstart = Property::create('DTSTART'); - $dtstart->setDateTime($this->start,Property\DateTime::UTC); - $vfreebusy->add($dtstart); - } - if ($this->end) { - $dtend = Property::create('DTEND'); - $dtend->setDateTime($this->end,Property\DateTime::UTC); - $vfreebusy->add($dtend); - } - $dtstamp = Property::create('DTSTAMP'); - $dtstamp->setDateTime(new \DateTime('now'), Property\DateTime::UTC); - $vfreebusy->add($dtstamp); - - foreach($busyTimes as $busyTime) { - - $busyTime[0]->setTimeZone(new \DateTimeZone('UTC')); - $busyTime[1]->setTimeZone(new \DateTimeZone('UTC')); - - $prop = Property::create( - 'FREEBUSY', - $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z') - ); - $prop['FBTYPE'] = $busyTime[2]; - $vfreebusy->add($prop); - - } - - return $calendar; - - } - -} - diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Node.php b/plugins/libcalendaring/lib/Sabre/VObject/Node.php deleted file mode 100644 index bee68ec2..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Node.php +++ /dev/null @@ -1,187 +0,0 @@ -iterator)) - return $this->iterator; - - return new ElementList(array($this)); - - } - - /** - * Sets the overridden iterator - * - * Note that this is not actually part of the iterator interface - * - * @param ElementList $iterator - * @return void - */ - public function setIterator(ElementList $iterator) { - - $this->iterator = $iterator; - - } - - /* }}} */ - - /* {{{ Countable interface */ - - /** - * Returns the number of elements - * - * @return int - */ - public function count() { - - $it = $this->getIterator(); - return $it->count(); - - } - - /* }}} */ - - /* {{{ ArrayAccess Interface */ - - - /** - * Checks if an item exists through ArrayAccess. - * - * This method just forwards the request to the inner iterator - * - * @param int $offset - * @return bool - */ - public function offsetExists($offset) { - - $iterator = $this->getIterator(); - return $iterator->offsetExists($offset); - - } - - /** - * Gets an item through ArrayAccess. - * - * This method just forwards the request to the inner iterator - * - * @param int $offset - * @return mixed - */ - public function offsetGet($offset) { - - $iterator = $this->getIterator(); - return $iterator->offsetGet($offset); - - } - - /** - * Sets an item through ArrayAccess. - * - * This method just forwards the request to the inner iterator - * - * @param int $offset - * @param mixed $value - * @return void - */ - public function offsetSet($offset,$value) { - - $iterator = $this->getIterator(); - $iterator->offsetSet($offset,$value); - - // @codeCoverageIgnoreStart - // - // This method always throws an exception, so we ignore the closing - // brace - } - // @codeCoverageIgnoreEnd - - /** - * Sets an item through ArrayAccess. - * - * This method just forwards the request to the inner iterator - * - * @param int $offset - * @return void - */ - public function offsetUnset($offset) { - - $iterator = $this->getIterator(); - $iterator->offsetUnset($offset); - - // @codeCoverageIgnoreStart - // - // This method always throws an exception, so we ignore the closing - // brace - } - // @codeCoverageIgnoreEnd - - /* }}} */ - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Parameter.php b/plugins/libcalendaring/lib/Sabre/VObject/Parameter.php deleted file mode 100644 index 87445179..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Parameter.php +++ /dev/null @@ -1,102 +0,0 @@ -name = strtoupper($name); - $this->value = $value; - - } - - /** - * Returns the parameter's internal value. - * - * @return string - */ - public function getValue() { - - return $this->value; - - } - - - /** - * Turns the object back into a serialized blob. - * - * @return string - */ - public function serialize() { - - if (is_null($this->value)) { - return $this->name; - } - $src = array( - '\\', - "\n", - ); - $out = array( - '\\\\', - '\n', - ); - - // quote parameters according to RFC 5545, Section 3.2 - $quotes = ''; - if (preg_match('/[:;,]/', $this->value)) { - $quotes = '"'; - } - - return $this->name . '=' . $quotes . str_replace($src, $out, $this->value) . $quotes; - - } - - /** - * Called when this object is being cast to a string - * - * @return string - */ - public function __toString() { - - return $this->value; - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/ParseException.php b/plugins/libcalendaring/lib/Sabre/VObject/ParseException.php deleted file mode 100644 index 66b49c60..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/ParseException.php +++ /dev/null @@ -1,12 +0,0 @@ - 'Sabre\\VObject\\Property\\DateTime', - 'CREATED' => 'Sabre\\VObject\\Property\\DateTime', - 'DTEND' => 'Sabre\\VObject\\Property\\DateTime', - 'DTSTAMP' => 'Sabre\\VObject\\Property\\DateTime', - 'DTSTART' => 'Sabre\\VObject\\Property\\DateTime', - 'DUE' => 'Sabre\\VObject\\Property\\DateTime', - 'EXDATE' => 'Sabre\\VObject\\Property\\MultiDateTime', - 'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\DateTime', - 'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\DateTime', - 'TRIGGER' => 'Sabre\\VObject\\Property\\DateTime', - 'N' => 'Sabre\\VObject\\Property\\Compound', - 'ORG' => 'Sabre\\VObject\\Property\\Compound', - 'ADR' => 'Sabre\\VObject\\Property\\Compound', - 'CATEGORIES' => 'Sabre\\VObject\\Property\\Compound', - ); - - /** - * Creates the new property by name, but in addition will also see if - * there's a class mapped to the property name. - * - * Parameters can be specified with the optional third argument. Parameters - * must be a key->value map of the parameter name, and value. If the value - * is specified as an array, it is assumed that multiple parameters with - * the same name should be added. - * - * @param string $name - * @param string $value - * @param array $parameters - * @return Property - */ - static public function create($name, $value = null, array $parameters = array()) { - - $name = strtoupper($name); - $shortName = $name; - $group = null; - if (strpos($shortName,'.')!==false) { - list($group, $shortName) = explode('.', $shortName); - } - - if (isset(self::$classMap[$shortName])) { - return new self::$classMap[$shortName]($name, $value, $parameters); - } else { - return new self($name, $value, $parameters); - } - - } - - /** - * Creates a new property object - * - * Parameters can be specified with the optional third argument. Parameters - * must be a key->value map of the parameter name, and value. If the value - * is specified as an array, it is assumed that multiple parameters with - * the same name should be added. - * - * @param string $name - * @param string $value - * @param array $parameters - */ - public function __construct($name, $value = null, array $parameters = array()) { - - if (!is_scalar($value) && !is_null($value)) { - throw new \InvalidArgumentException('The value argument must be scalar or null'); - } - - $name = strtoupper($name); - $group = null; - if (strpos($name,'.')!==false) { - list($group, $name) = explode('.', $name); - } - $this->name = $name; - $this->group = $group; - $this->setValue($value); - - foreach($parameters as $paramName => $paramValues) { - - if (!is_array($paramValues)) { - $paramValues = array($paramValues); - } - - foreach($paramValues as $paramValue) { - $this->add($paramName, $paramValue); - } - - } - - } - - /** - * Updates the internal value - * - * @param string $value - * @return void - */ - public function setValue($value) { - - $this->value = $value; - - } - - /** - * Returns the internal value - * - * @param string $value - * @return string - */ - public function getValue() { - - return $this->value; - - } - - /** - * Turns the object back into a serialized blob. - * - * @return string - */ - public function serialize() { - - $str = $this->name; - if ($this->group) $str = $this->group . '.' . $this->name; - - foreach($this->parameters as $param) { - - $str.=';' . $param->serialize(); - - } - - $src = array( - '\\', - "\n", - "\r", - ); - $out = array( - '\\\\', - '\n', - '', - ); - - // avoid double-escaping of \, and \; from Compound properties - if (method_exists($this, 'setParts')) { - $src[] = '\\\\,'; - $out[] = '\\,'; - $src[] = '\\\\;'; - $out[] = '\\;'; - } - - $str.=':' . str_replace($src, $out, $this->value); - - $out = ''; - while(strlen($str)>0) { - if (strlen($str)>75) { - $out.= mb_strcut($str,0,75,'utf-8') . "\r\n"; - $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8'); - } else { - $out.=$str . "\r\n"; - $str=''; - break; - } - } - - return $out; - - } - - /** - * Adds a new componenten or element - * - * You can call this method with the following syntaxes: - * - * add(Parameter $element) - * add(string $name, $value) - * - * The first version adds an Parameter - * The second adds a property as a string. - * - * @param mixed $item - * @param mixed $itemValue - * @return void - */ - public function add($item, $itemValue = null) { - - if ($item instanceof Parameter) { - if (!is_null($itemValue)) { - throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject'); - } - $item->parent = $this; - $this->parameters[] = $item; - } elseif(is_string($item)) { - - $parameter = new Parameter($item,$itemValue); - $parameter->parent = $this; - $this->parameters[] = $parameter; - - } else { - - throw new \InvalidArgumentException('The first argument must either be a Node a string'); - - } - - } - - /* ArrayAccess interface {{{ */ - - /** - * Checks if an array element exists - * - * @param mixed $name - * @return bool - */ - public function offsetExists($name) { - - if (is_int($name)) return parent::offsetExists($name); - - $name = strtoupper($name); - - foreach($this->parameters as $parameter) { - if ($parameter->name == $name) return true; - } - return false; - - } - - /** - * Returns a parameter, or parameter list. - * - * @param string $name - * @return Node - */ - public function offsetGet($name) { - - if (is_int($name)) return parent::offsetGet($name); - $name = strtoupper($name); - - $result = array(); - foreach($this->parameters as $parameter) { - if ($parameter->name == $name) - $result[] = $parameter; - } - - if (count($result)===0) { - return null; - } elseif (count($result)===1) { - return $result[0]; - } else { - $result[0]->setIterator(new ElementList($result)); - return $result[0]; - } - - } - - /** - * Creates a new parameter - * - * @param string $name - * @param mixed $value - * @return void - */ - public function offsetSet($name, $value) { - - if (is_int($name)) parent::offsetSet($name, $value); - - if (is_scalar($value)) { - if (!is_string($name)) - throw new \InvalidArgumentException('A parameter name must be specified. This means you cannot use the $array[]="string" to add parameters.'); - - $this->offsetUnset($name); - $parameter = new Parameter($name, $value); - $parameter->parent = $this; - $this->parameters[] = $parameter; - - } elseif ($value instanceof Parameter) { - if (!is_null($name)) - throw new \InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a \\Sabre\\VObject\\Parameter. Add using $array[]=$parameterObject.'); - - $value->parent = $this; - $this->parameters[] = $value; - } else { - throw new \InvalidArgumentException('You can only add parameters to the property object'); - } - - } - - /** - * Removes one or more parameters with the specified name - * - * @param string $name - * @return void - */ - public function offsetUnset($name) { - - if (is_int($name)) parent::offsetUnset($name); - $name = strtoupper($name); - - foreach($this->parameters as $key=>$parameter) { - if ($parameter->name == $name) { - $parameter->parent = null; - unset($this->parameters[$key]); - } - - } - - } - - /* }}} */ - - /** - * Called when this object is being cast to a string - * - * @return string - */ - public function __toString() { - - return (string)$this->value; - - } - - /** - * This method is automatically called when the object is cloned. - * Specifically, this will ensure all child elements are also cloned. - * - * @return void - */ - public function __clone() { - - foreach($this->parameters as $key=>$child) { - $this->parameters[$key] = clone $child; - $this->parameters[$key]->parent = $this; - } - - } - - /** - * Validates the node for correctness. - * - * The following options are supported: - * - Node::REPAIR - If something is broken, and automatic repair may - * be attempted. - * - * An array is returned with warnings. - * - * Every item in the array has the following properties: - * * level - (number between 1 and 3 with severity information) - * * message - (human readable message) - * * node - (reference to the offending node) - * - * @param int $options - * @return array - */ - public function validate($options = 0) { - - $warnings = array(); - - // Checking if our value is UTF-8 - if (!StringUtil::isUTF8($this->value)) { - $warnings[] = array( - 'level' => 1, - 'message' => 'Property is not valid UTF-8!', - 'node' => $this, - ); - if ($options & self::REPAIR) { - $this->value = StringUtil::convertToUTF8($this->value); - } - } - - // Checking if the propertyname does not contain any invalid bytes. - if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { - $warnings[] = array( - 'level' => 1, - 'message' => 'The propertyname: ' . $this->name . ' contains invalid characters. Only A-Z, 0-9 and - are allowed', - 'node' => $this, - ); - if ($options & self::REPAIR) { - // Uppercasing and converting underscores to dashes. - $this->name = strtoupper( - str_replace('_', '-', $this->name) - ); - // Removing every other invalid character - $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name); - - } - - } - - // Validating inner parameters - foreach($this->parameters as $param) { - $warnings = array_merge($warnings, $param->validate($options)); - } - - return $warnings; - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php b/plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php deleted file mode 100644 index 26f09006..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php +++ /dev/null @@ -1,125 +0,0 @@ - ';', - 'ADR' => ';', - 'ORG' => ';', - 'CATEGORIES' => ',', - ); - - /** - * The currently used delimiter. - * - * @var string - */ - protected $delimiter = null; - - /** - * Get a compound value as an array. - * - * @param $name string - * @return array - */ - public function getParts() { - - if (is_null($this->value)) { - return array(); - } - - $delimiter = $this->getDelimiter(); - - // split by any $delimiter which is NOT prefixed by a slash. - // Note that this is not a a perfect solution. If a value is prefixed - // by two slashes, it should actually be split anyway. - // - // Hopefully we can fix this better in a future version, where we can - // break compatibility a bit. - $compoundValues = preg_split("/(?value); - - // remove slashes from any semicolon and comma left escaped in the single values - $compoundValues = array_map( - function($val) { - return strtr($val, array('\,' => ',', '\;' => ';')); - }, $compoundValues); - - return $compoundValues; - - } - - /** - * Returns the delimiter for this property. - * - * @return string - */ - public function getDelimiter() { - - if (!$this->delimiter) { - if (isset(self::$delimiterMap[$this->name])) { - $this->delimiter = self::$delimiterMap[$this->name]; - } else { - // To be a bit future proof, we are going to default the - // delimiter to ; - $this->delimiter = ';'; - } - } - return $this->delimiter; - - } - - /** - * Set a compound value as an array. - * - * - * @param $name string - * @return array - */ - public function setParts(array $values) { - - // add slashes to all semicolons and commas in the single values - $values = array_map( - function($val) { - return strtr($val, array(',' => '\,', ';' => '\;')); - }, $values); - - $this->setValue( - implode($this->getDelimiter(), $values) - ); - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php b/plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php deleted file mode 100644 index 95e9b020..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php +++ /dev/null @@ -1,245 +0,0 @@ -setValue($dt->format('Ymd\\THis')); - $this->offsetUnset('VALUE'); - $this->offsetUnset('TZID'); - $this->offsetSet('VALUE','DATE-TIME'); - break; - case self::UTC : - $dt->setTimeZone(new \DateTimeZone('UTC')); - $this->setValue($dt->format('Ymd\\THis\\Z')); - $this->offsetUnset('VALUE'); - $this->offsetUnset('TZID'); - $this->offsetSet('VALUE','DATE-TIME'); - break; - case self::LOCALTZ : - $this->setValue($dt->format('Ymd\\THis')); - $this->offsetUnset('VALUE'); - $this->offsetUnset('TZID'); - $this->offsetSet('VALUE','DATE-TIME'); - $this->offsetSet('TZID', $dt->getTimeZone()->getName()); - break; - case self::DATE : - $this->setValue($dt->format('Ymd')); - $this->offsetUnset('VALUE'); - $this->offsetUnset('TZID'); - $this->offsetSet('VALUE','DATE'); - break; - default : - throw new \InvalidArgumentException('You must pass a valid dateType constant'); - - } - $this->dateTime = $dt; - $this->dateType = $dateType; - - } - - /** - * Returns the current DateTime value. - * - * If no value was set, this method returns null. - * - * @return \DateTime|null - */ - public function getDateTime() { - - if ($this->dateTime) - return $this->dateTime; - - list( - $this->dateType, - $this->dateTime - ) = self::parseData($this->value, $this); - return $this->dateTime; - - } - - /** - * Returns the type of Date format. - * - * This method returns one of the format constants. If no date was set, - * this method will return null. - * - * @return int|null - */ - public function getDateType() { - - if ($this->dateType) - return $this->dateType; - - list( - $this->dateType, - $this->dateTime, - ) = self::parseData($this->value, $this); - return $this->dateType; - - } - - /** - * This method will return true, if the property had a date and a time, as - * opposed to only a date. - * - * @return bool - */ - public function hasTime() { - - return $this->getDateType()!==self::DATE; - - } - - /** - * Parses the internal data structure to figure out what the current date - * and time is. - * - * The returned array contains two elements: - * 1. A 'DateType' constant (as defined on this class), or null. - * 2. A DateTime object (or null) - * - * @param string|null $propertyValue The string to parse (yymmdd or - * ymmddThhmmss, etc..) - * @param \Sabre\VObject\Property|null $property The instance of the - * property we're parsing. - * @return array - */ - static public function parseData($propertyValue, VObject\Property $property = null) { - - if (is_null($propertyValue)) { - return array(null, null); - } - - $date = '(?P[1-2][0-9]{3})(?P[0-1][0-9])(?P[0-3][0-9])'; - $time = '(?P[0-2][0-9])(?P[0-5][0-9])(?P[0-5][0-9])'; - $regex = "/^$date(T$time(?PZ)?)?$/"; - - if (!preg_match($regex, $propertyValue, $matches)) { - throw new \InvalidArgumentException($propertyValue . ' is not a valid \DateTime or Date string'); - } - - if (!isset($matches['hour'])) { - // Date-only - return array( - self::DATE, - new \DateTime($matches['year'] . '-' . $matches['month'] . '-' . $matches['date'] . ' 00:00:00', new \DateTimeZone('UTC')), - ); - } - - $dateStr = - $matches['year'] .'-' . - $matches['month'] . '-' . - $matches['date'] . ' ' . - $matches['hour'] . ':' . - $matches['minute'] . ':' . - $matches['second']; - - if (isset($matches['isutc'])) { - $dt = new \DateTime($dateStr,new \DateTimeZone('UTC')); - $dt->setTimeZone(new \DateTimeZone('UTC')); - return array( - self::UTC, - $dt - ); - } - - // Finding the timezone. - $tzid = $property['TZID']; - if (!$tzid) { - // This was a floating time string. This implies we use the - // timezone from date_default_timezone_set / date.timezone ini - // setting. - return array( - self::LOCAL, - new \DateTime($dateStr) - ); - } - - // To look up the timezone, we must first find the VCALENDAR component. - $root = $property; - while($root->parent) { - $root = $root->parent; - } - if ($root->name === 'VCALENDAR') { - $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid, $root); - } else { - $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid); - } - - $dt = new \DateTime($dateStr, $tz); - $dt->setTimeZone($tz); - - return array( - self::LOCALTZ, - $dt - ); - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php b/plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php deleted file mode 100644 index f01491b6..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php +++ /dev/null @@ -1,180 +0,0 @@ -offsetUnset('VALUE'); - $this->offsetUnset('TZID'); - switch($dateType) { - - case DateTime::LOCAL : - $val = array(); - foreach($dt as $i) { - $val[] = $i->format('Ymd\\THis'); - } - $this->setValue(implode(',',$val)); - $this->offsetSet('VALUE','DATE-TIME'); - break; - case DateTime::UTC : - $val = array(); - foreach($dt as $i) { - $i->setTimeZone(new \DateTimeZone('UTC')); - $val[] = $i->format('Ymd\\THis\\Z'); - } - $this->setValue(implode(',',$val)); - $this->offsetSet('VALUE','DATE-TIME'); - break; - case DateTime::LOCALTZ : - $val = array(); - foreach($dt as $i) { - $val[] = $i->format('Ymd\\THis'); - } - $this->setValue(implode(',',$val)); - $this->offsetSet('VALUE','DATE-TIME'); - $this->offsetSet('TZID', $dt[0]->getTimeZone()->getName()); - break; - case DateTime::DATE : - $val = array(); - foreach($dt as $i) { - $val[] = $i->format('Ymd'); - } - $this->setValue(implode(',',$val)); - $this->offsetSet('VALUE','DATE'); - break; - default : - throw new \InvalidArgumentException('You must pass a valid dateType constant'); - - } - $this->dateTimes = $dt; - $this->dateType = $dateType; - - } - - /** - * Returns the current DateTime value. - * - * If no value was set, this method returns null. - * - * @return array|null - */ - public function getDateTimes() { - - if ($this->dateTimes) - return $this->dateTimes; - - $dts = array(); - - if (!$this->value) { - $this->dateTimes = null; - $this->dateType = null; - return null; - } - - foreach(explode(',',$this->value) as $val) { - list( - $type, - $dt - ) = DateTime::parseData($val, $this); - $dts[] = $dt; - $this->dateType = $type; - } - $this->dateTimes = $dts; - return $this->dateTimes; - - } - - /** - * Returns the type of Date format. - * - * This method returns one of the format constants. If no date was set, - * this method will return null. - * - * @return int|null - */ - public function getDateType() { - - if ($this->dateType) - return $this->dateType; - - if (!$this->value) { - $this->dateTimes = null; - $this->dateType = null; - return null; - } - - $dts = array(); - foreach(explode(',',$this->value) as $val) { - list( - $type, - $dt - ) = DateTime::parseData($val, $this); - $dts[] = $dt; - $this->dateType = $type; - } - $this->dateTimes = $dts; - return $this->dateType; - - } - - /** - * This method will return true, if the property had a date and a time, as - * opposed to only a date. - * - * @return bool - */ - public function hasTime() { - - return $this->getDateType()!==DateTime::DATE; - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Reader.php b/plugins/libcalendaring/lib/Sabre/VObject/Reader.php deleted file mode 100644 index a001b2bf..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Reader.php +++ /dev/null @@ -1,223 +0,0 @@ -add($parsedLine); - - if ($nextLine===false) - throw new ParseException('Invalid VObject. Document ended prematurely.'); - - } - - // Checking component name of the 'END:' line. - if (substr($nextLine,4)!==$obj->name) { - throw new ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"'); - } - next($lines); - - return $obj; - - } - - // Properties - //$result = preg_match('/(?P[A-Z0-9-]+)(?:;(?P^(?([^:^\"]|\"([^\"]*)\")*))?"; - $regex = "/^(?P$token)$parameters:(?P.*)$/i"; - - $result = preg_match($regex,$line,$matches); - - if (!$result) { - if ($options & self::OPTION_IGNORE_INVALID_LINES) { - return null; - } else { - throw new ParseException('Invalid VObject, line ' . ($lineNr+1) . ' did not follow the icalendar/vcard format'); - } - } - - $propertyName = strtoupper($matches['name']); - $propertyValue = preg_replace_callback('#(\\\\(\\\\|N|n))#',function($matches) { - if ($matches[2]==='n' || $matches[2]==='N') { - return "\n"; - } else { - return $matches[2]; - } - }, $matches['value']); - - $obj = Property::create($propertyName, $propertyValue); - - if ($matches['parameters']) { - - foreach(self::readParameters($matches['parameters']) as $param) { - $obj->add($param); - } - - } - - return $obj; - - - } - - /** - * Reads a parameter list from a property - * - * This method returns an array of Parameter - * - * @param string $parameters - * @return array - */ - static private function readParameters($parameters) { - - $token = '[A-Z0-9-]+'; - - $paramValue = '(?P[^\"^;]*|"[^"]*")'; - - $regex = "/(?<=^|;)(?P$token)(=$paramValue(?=$|;))?/i"; - preg_match_all($regex, $parameters, $matches, PREG_SET_ORDER); - - $params = array(); - foreach($matches as $match) { - - if (!isset($match['paramValue'])) { - - $value = null; - - } else { - - $value = $match['paramValue']; - - if (isset($value[0]) && $value[0]==='"') { - // Stripping quotes, if needed - $value = substr($value,1,strlen($value)-2); - } - - $value = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) { - if ($matches[2]==='n' || $matches[2]==='N') { - return "\n"; - } else { - return $matches[2]; - } - }, $value); - - } - - $params[] = new Parameter($match['paramName'], $value); - - } - - return $params; - - } - - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php b/plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php deleted file mode 100644 index 8bd4ed22..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php +++ /dev/null @@ -1,1144 +0,0 @@ - 0, - 'MO' => 1, - 'TU' => 2, - 'WE' => 3, - 'TH' => 4, - 'FR' => 5, - 'SA' => 6, - ); - - /** - * Mappings between the day number and english day name. - * - * @var array - */ - private $dayNames = array( - 0 => 'Sunday', - 1 => 'Monday', - 2 => 'Tuesday', - 3 => 'Wednesday', - 4 => 'Thursday', - 5 => 'Friday', - 6 => 'Saturday', - ); - - /** - * If the current iteration of the event is an overriden event, this - * property will hold the VObject - * - * @var Component - */ - private $currentOverriddenEvent; - - /** - * This property may contain the date of the next not-overridden event. - * This date is calculated sometimes a bit early, before overridden events - * are evaluated. - * - * @var DateTime - */ - private $nextDate; - - /** - * This counts the number of overridden events we've handled so far - * - * @var int - */ - private $handledOverridden = 0; - - /** - * Creates the iterator - * - * You should pass a VCALENDAR component, as well as the UID of the event - * we're going to traverse. - * - * @param Component $vcal - * @param string|null $uid - */ - public function __construct(Component $vcal, $uid=null) { - - if (is_null($uid)) { - if ($vcal->name === 'VCALENDAR') { - throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well'); - } - $components = array($vcal); - $uid = (string)$vcal->uid; - } else { - $components = $vcal->select('VEVENT'); - } - foreach($components as $component) { - if ((string)$component->uid == $uid) { - if (isset($component->{'RECURRENCE-ID'})) { - $this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component; - $this->overriddenDates[] = $component->{'RECURRENCE-ID'}->getDateTime(); - } else { - $this->baseEvent = $component; - } - } - } - - ksort($this->overriddenEvents); - - if (!$this->baseEvent) { - throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid); - } - - $this->startDate = clone $this->baseEvent->DTSTART->getDateTime(); - - $this->endDate = null; - if (isset($this->baseEvent->DTEND)) { - $this->endDate = clone $this->baseEvent->DTEND->getDateTime(); - } else { - $this->endDate = clone $this->startDate; - if (isset($this->baseEvent->DURATION)) { - $this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value)); - } elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) { - $this->endDate->modify('+1 day'); - } - } - $this->currentDate = clone $this->startDate; - - $rrule = (string)$this->baseEvent->RRULE; - - $parts = explode(';', $rrule); - - // If no rrule was specified, we create a default setting - if (!$rrule) { - $this->frequency = 'daily'; - $this->count = 1; - } else foreach($parts as $part) { - - list($key, $value) = explode('=', $part, 2); - - switch(strtoupper($key)) { - - case 'FREQ' : - if (!in_array( - strtolower($value), - array('secondly','minutely','hourly','daily','weekly','monthly','yearly') - )) { - throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value)); - - } - $this->frequency = strtolower($value); - break; - - case 'UNTIL' : - $this->until = DateTimeParser::parse($value); - - // In some cases events are generated with an UNTIL= - // parameter before the actual start of the event. - // - // Not sure why this is happening. We assume that the - // intention was that the event only recurs once. - // - // So we are modifying the parameter so our code doesn't - // break. - if($this->until < $this->baseEvent->DTSTART->getDateTime()) { - $this->until = $this->baseEvent->DTSTART->getDateTime(); - } - break; - - case 'COUNT' : - $this->count = (int)$value; - break; - - case 'INTERVAL' : - $this->interval = (int)$value; - if ($this->interval < 1) { - throw new \InvalidArgumentException('INTERVAL in RRULE must be a positive integer!'); - } - break; - - case 'BYSECOND' : - $this->bySecond = explode(',', $value); - break; - - case 'BYMINUTE' : - $this->byMinute = explode(',', $value); - break; - - case 'BYHOUR' : - $this->byHour = explode(',', $value); - break; - - case 'BYDAY' : - $this->byDay = explode(',', strtoupper($value)); - break; - - case 'BYMONTHDAY' : - $this->byMonthDay = explode(',', $value); - break; - - case 'BYYEARDAY' : - $this->byYearDay = explode(',', $value); - break; - - case 'BYWEEKNO' : - $this->byWeekNo = explode(',', $value); - break; - - case 'BYMONTH' : - $this->byMonth = explode(',', $value); - break; - - case 'BYSETPOS' : - $this->bySetPos = explode(',', $value); - break; - - case 'WKST' : - $this->weekStart = strtoupper($value); - break; - - } - - } - - // Parsing exception dates - if (isset($this->baseEvent->EXDATE)) { - foreach($this->baseEvent->EXDATE as $exDate) { - - foreach(explode(',', (string)$exDate) as $exceptionDate) { - - $this->exceptionDates[] = - DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone()); - - } - - } - - } - - } - - /** - * Returns the current item in the list - * - * @return DateTime - */ - public function current() { - - if (!$this->valid()) return null; - return clone $this->currentDate; - - } - - /** - * This method returns the startdate for the current iteration of the - * event. - * - * @return DateTime - */ - public function getDtStart() { - - if (!$this->valid()) return null; - return clone $this->currentDate; - - } - - /** - * This method returns the enddate for the current iteration of the - * event. - * - * @return DateTime - */ - public function getDtEnd() { - - if (!$this->valid()) return null; - $dtEnd = clone $this->currentDate; - $dtEnd->add( $this->startDate->diff( $this->endDate ) ); - return clone $dtEnd; - - } - - /** - * Returns a VEVENT object with the updated start and end date. - * - * Any recurrence information is removed, and this function may return an - * 'overridden' event instead. - * - * This method always returns a cloned instance. - * - * @return Component\VEvent - */ - public function getEventObject() { - - if ($this->currentOverriddenEvent) { - return clone $this->currentOverriddenEvent; - } - $event = clone $this->baseEvent; - unset($event->RRULE); - unset($event->EXDATE); - unset($event->RDATE); - unset($event->EXRULE); - - $event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType()); - if (isset($event->DTEND)) { - $event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType()); - } - if ($this->counter > 0) { - $event->{'RECURRENCE-ID'} = (string)$event->DTSTART; - } - - return $event; - - } - - /** - * Returns the current item number - * - * @return int - */ - public function key() { - - return $this->counter; - - } - - /** - * Whether or not there is a 'next item' - * - * @return bool - */ - public function valid() { - - if (!is_null($this->count)) { - return $this->counter < $this->count; - } - if (!is_null($this->until) && $this->currentDate > $this->until) { - - // Need to make sure there's no overridden events past the - // until date. - foreach($this->overriddenEvents as $overriddenEvent) { - - if ($overriddenEvent->DTSTART->getDateTime() >= $this->currentDate) { - - return true; - } - } - return false; - } - return true; - - } - - /** - * Resets the iterator - * - * @return void - */ - public function rewind() { - - $this->currentDate = clone $this->startDate; - $this->counter = 0; - - } - - /** - * This method allows you to quickly go to the next occurrence after the - * specified date. - * - * Note that this checks the current 'endDate', not the 'stardDate'. This - * means that if you forward to January 1st, the iterator will stop at the - * first event that ends *after* January 1st. - * - * @param DateTime $dt - * @return void - */ - public function fastForward(\DateTime $dt) { - - while($this->valid() && $this->getDTEnd() <= $dt) { - $this->next(); - } - - } - - /** - * Returns true if this recurring event never ends. - * - * @return bool - */ - public function isInfinite() { - - return !$this->count && !$this->until; - - } - - /** - * Goes on to the next iteration - * - * @return void - */ - public function next() { - - $previousStamp = $this->currentDate->getTimeStamp(); - - // Finding the next overridden event in line, and storing that for - // later use. - $overriddenEvent = null; - $overriddenDate = null; - $this->currentOverriddenEvent = null; - - foreach($this->overriddenEvents as $index=>$event) { - if ($index > $previousStamp) { - $overriddenEvent = $event; - $overriddenDate = clone $event->DTSTART->getDateTime(); - break; - } - } - - // If we have a stored 'next date', we will use that. - if ($this->nextDate) { - if (!$overriddenDate || $this->nextDate < $overriddenDate) { - $this->currentDate = $this->nextDate; - $currentStamp = $this->currentDate->getTimeStamp(); - $this->nextDate = null; - } else { - $this->currentDate = clone $overriddenDate; - $this->currentOverriddenEvent = $overriddenEvent; - } - $this->counter++; - return; - } - - while(true) { - - // Otherwise, we find the next event in the normal RRULE - // sequence. - switch($this->frequency) { - - case 'hourly' : - $this->nextHourly(); - break; - - case 'daily' : - $this->nextDaily(); - break; - - case 'weekly' : - $this->nextWeekly(); - break; - - case 'monthly' : - $this->nextMonthly(); - break; - - case 'yearly' : - $this->nextYearly(); - break; - - } - $currentStamp = $this->currentDate->getTimeStamp(); - - - // Checking exception dates - foreach($this->exceptionDates as $exceptionDate) { - if ($this->currentDate == $exceptionDate) { - $this->counter++; - continue 2; - } - } - foreach($this->overriddenDates as $check) { - if ($this->currentDate == $check) { - continue 2; - } - } - break; - - } - - - - // Is the date we have actually higher than the next overiddenEvent? - if ($overriddenDate && $this->currentDate > $overriddenDate) { - $this->nextDate = clone $this->currentDate; - $this->currentDate = clone $overriddenDate; - $this->currentOverriddenEvent = $overriddenEvent; - $this->handledOverridden++; - } - $this->counter++; - - - /* - * If we have overridden events left in the queue, but our counter is - * running out, we should grab one of those. - */ - if (!is_null($overriddenEvent) && !is_null($this->count) && count($this->overriddenEvents) - $this->handledOverridden >= ($this->count - $this->counter)) { - - $this->currentOverriddenEvent = $overriddenEvent; - $this->currentDate = clone $overriddenDate; - $this->handledOverridden++; - - } - - } - - /** - * Does the processing for advancing the iterator for hourly frequency. - * - * @return void - */ - protected function nextHourly() { - - if (!$this->byHour) { - $this->currentDate->modify('+' . $this->interval . ' hours'); - return; - } - } - - /** - * Does the processing for advancing the iterator for daily frequency. - * - * @return void - */ - protected function nextDaily() { - - if (!$this->byHour && !$this->byDay) { - $this->currentDate->modify('+' . $this->interval . ' days'); - return; - } - - if (isset($this->byHour)) { - $recurrenceHours = $this->getHours(); - } - - if (isset($this->byDay)) { - $recurrenceDays = $this->getDays(); - } - - do { - - if ($this->byHour) { - if ($this->currentDate->format('G') == '23') { - // to obey the interval rule - $this->currentDate->modify('+' . $this->interval-1 . ' days'); - } - - $this->currentDate->modify('+1 hours'); - - } else { - $this->currentDate->modify('+' . $this->interval . ' days'); - - } - - // Current day of the week - $currentDay = $this->currentDate->format('w'); - - // Current hour of the day - $currentHour = $this->currentDate->format('G'); - - } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); - - } - - /** - * Does the processing for advancing the iterator for weekly frequency. - * - * @return void - */ - protected function nextWeekly() { - - if (!$this->byHour && !$this->byDay) { - $this->currentDate->modify('+' . $this->interval . ' weeks'); - return; - } - - if ($this->byHour) { - $recurrenceHours = $this->getHours(); - } - - if ($this->byDay) { - $recurrenceDays = $this->getDays(); - } - - // First day of the week: - $firstDay = $this->dayMap[$this->weekStart]; - - do { - - if ($this->byHour) { - $this->currentDate->modify('+1 hours'); - } else { - $this->currentDate->modify('+1 days'); - } - - // Current day of the week - $currentDay = (int) $this->currentDate->format('w'); - - // Current hour of the day - $currentHour = (int) $this->currentDate->format('G'); - - // We need to roll over to the next week - if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) { - $this->currentDate->modify('+' . $this->interval-1 . ' weeks'); - - // We need to go to the first day of this week, but only if we - // are not already on this first day of this week. - if($this->currentDate->format('w') != $firstDay) { - $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]); - } - } - - // We have a match - } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); - } - - /** - * Does the processing for advancing the iterator for monthly frequency. - * - * @return void - */ - protected function nextMonthly() { - - $currentDayOfMonth = $this->currentDate->format('j'); - if (!$this->byMonthDay && !$this->byDay) { - - // If the current day is higher than the 28th, rollover can - // occur to the next month. We Must skip these invalid - // entries. - if ($currentDayOfMonth < 29) { - $this->currentDate->modify('+' . $this->interval . ' months'); - } else { - $increase = 0; - do { - $increase++; - $tempDate = clone $this->currentDate; - $tempDate->modify('+ ' . ($this->interval*$increase) . ' months'); - } while ($tempDate->format('j') != $currentDayOfMonth); - $this->currentDate = $tempDate; - } - return; - } - - while(true) { - - $occurrences = $this->getMonthlyOccurrences(); - - foreach($occurrences as $occurrence) { - - // The first occurrence thats higher than the current - // day of the month wins. - if ($occurrence > $currentDayOfMonth) { - break 2; - } - - } - - // If we made it all the way here, it means there were no - // valid occurrences, and we need to advance to the next - // month. - $this->currentDate->modify('first day of this month'); - $this->currentDate->modify('+ ' . $this->interval . ' months'); - - // This goes to 0 because we need to start counting at hte - // beginning. - $currentDayOfMonth = 0; - - } - - $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence); - - } - - /** - * Does the processing for advancing the iterator for yearly frequency. - * - * @return void - */ - protected function nextYearly() { - - $currentMonth = $this->currentDate->format('n'); - $currentYear = $this->currentDate->format('Y'); - $currentDayOfMonth = $this->currentDate->format('j'); - - // No sub-rules, so we just advance by year - if (!$this->byMonth) { - - // Unless it was a leap day! - if ($currentMonth==2 && $currentDayOfMonth==29) { - - $counter = 0; - do { - $counter++; - // Here we increase the year count by the interval, until - // we hit a date that's also in a leap year. - // - // We could just find the next interval that's dividable by - // 4, but that would ignore the rule that there's no leap - // year every year that's dividable by a 100, but not by - // 400. (1800, 1900, 2100). So we just rely on the datetime - // functions instead. - $nextDate = clone $this->currentDate; - $nextDate->modify('+ ' . ($this->interval*$counter) . ' years'); - } while ($nextDate->format('n')!=2); - $this->currentDate = $nextDate; - - return; - - } - - // The easiest form - $this->currentDate->modify('+' . $this->interval . ' years'); - return; - - } - - $currentMonth = $this->currentDate->format('n'); - $currentYear = $this->currentDate->format('Y'); - $currentDayOfMonth = $this->currentDate->format('j'); - - $advancedToNewMonth = false; - - // If we got a byDay or getMonthDay filter, we must first expand - // further. - if ($this->byDay || $this->byMonthDay) { - - while(true) { - - $occurrences = $this->getMonthlyOccurrences(); - - foreach($occurrences as $occurrence) { - - // The first occurrence that's higher than the current - // day of the month wins. - // If we advanced to the next month or year, the first - // occurrence is always correct. - if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { - break 2; - } - - } - - // If we made it here, it means we need to advance to - // the next month or year. - $currentDayOfMonth = 1; - $advancedToNewMonth = true; - do { - - $currentMonth++; - if ($currentMonth>12) { - $currentYear+=$this->interval; - $currentMonth = 1; - } - } while (!in_array($currentMonth, $this->byMonth)); - - $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); - - } - - // If we made it here, it means we got a valid occurrence - $this->currentDate->setDate($currentYear, $currentMonth, $occurrence); - return; - - } else { - - // These are the 'byMonth' rules, if there are no byDay or - // byMonthDay sub-rules. - do { - - $currentMonth++; - if ($currentMonth>12) { - $currentYear+=$this->interval; - $currentMonth = 1; - } - } while (!in_array($currentMonth, $this->byMonth)); - $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); - - return; - - } - - } - - /** - * Returns all the occurrences for a monthly frequency with a 'byDay' or - * 'byMonthDay' expansion for the current month. - * - * The returned list is an array of integers with the day of month (1-31). - * - * @return array - */ - protected function getMonthlyOccurrences() { - - $startDate = clone $this->currentDate; - - $byDayResults = array(); - - // Our strategy is to simply go through the byDays, advance the date to - // that point and add it to the results. - if ($this->byDay) foreach($this->byDay as $day) { - - $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]]; - - // Dayname will be something like 'wednesday'. Now we need to find - // all wednesdays in this month. - $dayHits = array(); - - $checkDate = clone $startDate; - $checkDate->modify('first day of this month'); - $checkDate->modify($dayName); - - do { - $dayHits[] = $checkDate->format('j'); - $checkDate->modify('next ' . $dayName); - } while ($checkDate->format('n') === $startDate->format('n')); - - // So now we have 'all wednesdays' for month. It is however - // possible that the user only really wanted the 1st, 2nd or last - // wednesday. - if (strlen($day)>2) { - $offset = (int)substr($day,0,-2); - - if ($offset>0) { - // It is possible that the day does not exist, such as a - // 5th or 6th wednesday of the month. - if (isset($dayHits[$offset-1])) { - $byDayResults[] = $dayHits[$offset-1]; - } - } else { - - // if it was negative we count from the end of the array - $byDayResults[] = $dayHits[count($dayHits) + $offset]; - } - } else { - // There was no counter (first, second, last wednesdays), so we - // just need to add the all to the list). - $byDayResults = array_merge($byDayResults, $dayHits); - - } - - } - - $byMonthDayResults = array(); - if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) { - - // Removing values that are out of range for this month - if ($monthDay > $startDate->format('t') || - $monthDay < 0-$startDate->format('t')) { - continue; - } - if ($monthDay>0) { - $byMonthDayResults[] = $monthDay; - } else { - // Negative values - $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; - } - } - - // If there was just byDay or just byMonthDay, they just specify our - // (almost) final list. If both were provided, then byDay limits the - // list. - if ($this->byMonthDay && $this->byDay) { - $result = array_intersect($byMonthDayResults, $byDayResults); - } elseif ($this->byMonthDay) { - $result = $byMonthDayResults; - } else { - $result = $byDayResults; - } - $result = array_unique($result); - sort($result, SORT_NUMERIC); - - // The last thing that needs checking is the BYSETPOS. If it's set, it - // means only certain items in the set survive the filter. - if (!$this->bySetPos) { - return $result; - } - - $filteredResult = array(); - foreach($this->bySetPos as $setPos) { - - if ($setPos<0) { - $setPos = count($result)-($setPos+1); - } - if (isset($result[$setPos-1])) { - $filteredResult[] = $result[$setPos-1]; - } - } - - sort($filteredResult, SORT_NUMERIC); - return $filteredResult; - - } - - protected function getHours() - { - $recurrenceHours = array(); - foreach($this->byHour as $byHour) { - $recurrenceHours[] = $byHour; - } - - return $recurrenceHours; - } - - protected function getDays() - { - $recurrenceDays = array(); - foreach($this->byDay as $byDay) { - - // The day may be preceeded with a positive (+n) or - // negative (-n) integer. However, this does not make - // sense in 'weekly' so we ignore it here. - $recurrenceDays[] = $this->dayMap[substr($byDay,-2)]; - - } - - return $recurrenceDays; - } -} - diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php deleted file mode 100644 index 657cfb81..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php +++ /dev/null @@ -1,111 +0,0 @@ -children as $component) { - if (!$component instanceof VObject\Component) { - continue; - } - - // Get all timezones - if ($component->name === 'VTIMEZONE') { - $this->vtimezones[(string)$component->TZID] = $component; - continue; - } - - // Get component UID for recurring Events search - if($component->UID) { - $uid = (string)$component->UID; - } else { - // Generating a random UID - $uid = sha1(microtime()) . '-vobjectimport'; - } - - // Take care of recurring events - if (!array_key_exists($uid, $this->objects)) { - $this->objects[$uid] = VObject\Component::create('VCALENDAR'); - } - - $this->objects[$uid]->add(clone $component); - } - - } - - /** - * Every time getNext() is called, a new object will be parsed, until we - * hit the end of the stream. - * - * When the end is reached, null will be returned. - * - * @return Sabre\VObject\Component|null - */ - public function getNext() { - - if($object=array_shift($this->objects)) { - - // create our baseobject - $object->version = '2.0'; - $object->prodid = '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN'; - $object->calscale = 'GREGORIAN'; - - // add vtimezone information to obj (if we have it) - foreach ($this->vtimezones as $vtimezone) { - $object->add($vtimezone); - } - - return $object; - - } else { - - return null; - - } - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php deleted file mode 100644 index c0126883..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php +++ /dev/null @@ -1,39 +0,0 @@ -input = $input; - - } - - /** - * Every time getNext() is called, a new object will be parsed, until we - * hit the end of the stream. - * - * When the end is reached, null will be returned. - * - * @return Sabre\VObject\Component|null - */ - public function getNext() { - - $vcard = ''; - - do { - - if (feof($this->input)) { - return false; - } - - $line = fgets($this->input); - $vcard .= $line; - - } while(strtoupper(substr($line,0,4))!=="END:"); - - $object = VObject\Reader::read($vcard); - - if($object->name !== 'VCARD') { - throw new \InvalidArgumentException("Thats no vCard!", 1); - } - - return $object; - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php b/plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php deleted file mode 100644 index ea88e1e8..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php +++ /dev/null @@ -1,61 +0,0 @@ -'Australia/Darwin', - 'AUS Eastern Standard Time'=>'Australia/Sydney', - 'Afghanistan Standard Time'=>'Asia/Kabul', - 'Alaskan Standard Time'=>'America/Anchorage', - 'Arab Standard Time'=>'Asia/Riyadh', - 'Arabian Standard Time'=>'Asia/Dubai', - 'Arabic Standard Time'=>'Asia/Baghdad', - 'Argentina Standard Time'=>'America/Buenos_Aires', - 'Armenian Standard Time'=>'Asia/Yerevan', - 'Atlantic Standard Time'=>'America/Halifax', - 'Azerbaijan Standard Time'=>'Asia/Baku', - 'Azores Standard Time'=>'Atlantic/Azores', - 'Bangladesh Standard Time'=>'Asia/Dhaka', - 'Canada Central Standard Time'=>'America/Regina', - 'Cape Verde Standard Time'=>'Atlantic/Cape_Verde', - 'Caucasus Standard Time'=>'Asia/Yerevan', - 'Cen. Australia Standard Time'=>'Australia/Adelaide', - 'Central America Standard Time'=>'America/Guatemala', - 'Central Asia Standard Time'=>'Asia/Almaty', - 'Central Brazilian Standard Time'=>'America/Cuiaba', - 'Central Europe Standard Time'=>'Europe/Budapest', - 'Central European Standard Time'=>'Europe/Warsaw', - 'Central Pacific Standard Time'=>'Pacific/Guadalcanal', - 'Central Standard Time'=>'America/Chicago', - 'Central Standard Time (Mexico)'=>'America/Mexico_City', - 'China Standard Time'=>'Asia/Shanghai', - 'Dateline Standard Time'=>'Etc/GMT+12', - 'E. Africa Standard Time'=>'Africa/Nairobi', - 'E. Australia Standard Time'=>'Australia/Brisbane', - 'E. Europe Standard Time'=>'Europe/Minsk', - 'E. South America Standard Time'=>'America/Sao_Paulo', - 'Eastern Standard Time'=>'America/New_York', - 'Egypt Standard Time'=>'Africa/Cairo', - 'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg', - 'FLE Standard Time'=>'Europe/Kiev', - 'Fiji Standard Time'=>'Pacific/Fiji', - 'GMT Standard Time'=>'Europe/London', - 'GTB Standard Time'=>'Europe/Istanbul', - 'Georgian Standard Time'=>'Asia/Tbilisi', - 'Greenland Standard Time'=>'America/Godthab', - 'Greenwich Standard Time'=>'Atlantic/Reykjavik', - 'Hawaiian Standard Time'=>'Pacific/Honolulu', - 'India Standard Time'=>'Asia/Calcutta', - 'Iran Standard Time'=>'Asia/Tehran', - 'Israel Standard Time'=>'Asia/Jerusalem', - 'Jordan Standard Time'=>'Asia/Amman', - 'Kamchatka Standard Time'=>'Asia/Kamchatka', - 'Korea Standard Time'=>'Asia/Seoul', - 'Magadan Standard Time'=>'Asia/Magadan', - 'Mauritius Standard Time'=>'Indian/Mauritius', - 'Mexico Standard Time'=>'America/Mexico_City', - 'Mexico Standard Time 2'=>'America/Chihuahua', - 'Mid-Atlantic Standard Time'=>'Etc/GMT-2', - 'Middle East Standard Time'=>'Asia/Beirut', - 'Montevideo Standard Time'=>'America/Montevideo', - 'Morocco Standard Time'=>'Africa/Casablanca', - 'Mountain Standard Time'=>'America/Denver', - 'Mountain Standard Time (Mexico)'=>'America/Chihuahua', - 'Myanmar Standard Time'=>'Asia/Rangoon', - 'N. Central Asia Standard Time'=>'Asia/Novosibirsk', - 'Namibia Standard Time'=>'Africa/Windhoek', - 'Nepal Standard Time'=>'Asia/Katmandu', - 'New Zealand Standard Time'=>'Pacific/Auckland', - 'Newfoundland Standard Time'=>'America/St_Johns', - 'North Asia East Standard Time'=>'Asia/Irkutsk', - 'North Asia Standard Time'=>'Asia/Krasnoyarsk', - 'Pacific SA Standard Time'=>'America/Santiago', - 'Pacific Standard Time'=>'America/Los_Angeles', - 'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel', - 'Pakistan Standard Time'=>'Asia/Karachi', - 'Paraguay Standard Time'=>'America/Asuncion', - 'Romance Standard Time'=>'Europe/Paris', - 'Russian Standard Time'=>'Europe/Moscow', - 'SA Eastern Standard Time'=>'America/Cayenne', - 'SA Pacific Standard Time'=>'America/Bogota', - 'SA Western Standard Time'=>'America/La_Paz', - 'SE Asia Standard Time'=>'Asia/Bangkok', - 'Samoa Standard Time'=>'Pacific/Apia', - 'Singapore Standard Time'=>'Asia/Singapore', - 'South Africa Standard Time'=>'Africa/Johannesburg', - 'Sri Lanka Standard Time'=>'Asia/Colombo', - 'Syria Standard Time'=>'Asia/Damascus', - 'Taipei Standard Time'=>'Asia/Taipei', - 'Tasmania Standard Time'=>'Australia/Hobart', - 'Tokyo Standard Time'=>'Asia/Tokyo', - 'Tonga Standard Time'=>'Pacific/Tongatapu', - 'US Eastern Standard Time'=>'America/Indianapolis', - 'US Mountain Standard Time'=>'America/Phoenix', - 'UTC+12'=>'Etc/GMT-12', - 'UTC-02'=>'Etc/GMT+2', - 'UTC-11'=>'Etc/GMT+11', - 'Ulaanbaatar Standard Time'=>'Asia/Ulaanbaatar', - 'Venezuela Standard Time'=>'America/Caracas', - 'Vladivostok Standard Time'=>'Asia/Vladivostok', - 'W. Australia Standard Time'=>'Australia/Perth', - 'W. Central Africa Standard Time'=>'Africa/Lagos', - 'W. Europe Standard Time'=>'Europe/Berlin', - 'West Asia Standard Time'=>'Asia/Tashkent', - 'West Pacific Standard Time'=>'Pacific/Port_Moresby', - 'Yakutsk Standard Time'=>'Asia/Yakutsk', - - // Microsoft exchange timezones - // Source: - // http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx - // - // Correct timezones deduced with help from: - // http://en.wikipedia.org/wiki/List_of_tz_database_time_zones - 'Universal Coordinated Time' => 'UTC', - 'Casablanca, Monrovia' => 'Africa/Casablanca', - 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', - 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', - 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', - 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', - 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', - 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', - 'Prague, Central Europe' => 'Europe/Prague', - 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo', - 'West Central Africa' => 'Africa/Luanda', // This was a best guess - 'Athens, Istanbul, Minsk' => 'Europe/Athens', - 'Bucharest' => 'Europe/Bucharest', - 'Cairo' => 'Africa/Cairo', - 'Harare, Pretoria' => 'Africa/Harare', - 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki', - 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem', - 'Baghdad' => 'Asia/Baghdad', - 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait', - 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', - 'East Africa, Nairobi' => 'Africa/Nairobi', - 'Tehran' => 'Asia/Tehran', - 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess - 'Baku, Tbilisi, Yerevan' => 'Asia/Baku', - 'Kabul' => 'Asia/Kabul', - 'Ekaterinburg' => 'Asia/Yekaterinburg', - 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', - 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', - 'Kathmandu, Nepal' => 'Asia/Kathmandu', - 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', - 'Astana, Dhaka' => 'Asia/Dhaka', - 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', - 'Rangoon' => 'Asia/Rangoon', - 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', - 'Krasnoyarsk' => 'Asia/Krasnoyarsk', - 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', - 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk', - 'Kuala Lumpur, Singapore' => 'Asia/Singapore', - 'Perth, Western Australia' => 'Australia/Perth', - 'Taipei' => 'Asia/Taipei', - 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', - 'Seoul, Korea Standard time' => 'Asia/Seoul', - 'Yakutsk' => 'Asia/Yakutsk', - 'Adelaide, Central Australia' => 'Australia/Adelaide', - 'Darwin' => 'Australia/Darwin', - 'Brisbane, East Australia' => 'Australia/Brisbane', - 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney', - 'Guam, Port Moresby' => 'Pacific/Guam', - 'Hobart, Tasmania' => 'Australia/Hobart', - 'Vladivostok' => 'Asia/Vladivostok', - 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan', - 'Auckland, Wellington' => 'Pacific/Auckland', - 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji', - 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu', - 'Azores' => 'Atlantic/Azores', - 'Cape Verde Is.' => 'Atlantic/Cape_Verde', - 'Mid-Atlantic' => 'America/Noronha', - 'Brasilia' => 'America/Sao_Paulo', // Best guess - 'Buenos Aires' => 'America/Argentina/Buenos_Aires', - 'Greenland' => 'America/Godthab', - 'Newfoundland' => 'America/St_Johns', - 'Atlantic Time (Canada)' => 'America/Halifax', - 'Caracas, La Paz' => 'America/Caracas', - 'Santiago' => 'America/Santiago', - 'Bogota, Lima, Quito' => 'America/Bogota', - 'Eastern Time (US & Canada)' => 'America/New_York', - 'Indiana (East)' => 'America/Indiana/Indianapolis', - 'Central America' => 'America/Guatemala', - 'Central Time (US & Canada)' => 'America/Chicago', - 'Mexico City, Tegucigalpa' => 'America/Mexico_City', - 'Saskatchewan' => 'America/Edmonton', - 'Arizona' => 'America/Phoenix', - 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess - 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess - 'Alaska' => 'America/Anchorage', - 'Hawaii' => 'Pacific/Honolulu', - 'Midway Island, Samoa' => 'Pacific/Midway', - 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', - - // The following list are timezone names that could be generated by - // Lotus / Domino - 'Dateline' => 'Etc/GMT-12', - 'Samoa' => 'Pacific/Apia', - 'Hawaiian' => 'Pacific/Honolulu', - 'Alaskan' => 'America/Anchorage', - 'Pacific' => 'America/Los_Angeles', - 'Pacific Standard Time' => 'America/Los_Angeles', - 'Mexico Standard Time 2' => 'America/Chihuahua', - 'Mountain' => 'America/Denver', - 'Mountain Standard Time' => 'America/Chihuahua', - 'US Mountain' => 'America/Phoenix', - 'Canada Central' => 'America/Edmonton', - 'Central America' => 'America/Guatemala', - 'Central' => 'America/Chicago', - 'Central Standard Time' => 'America/Mexico_City', - 'Mexico' => 'America/Mexico_City', - 'Eastern' => 'America/New_York', - 'SA Pacific' => 'America/Bogota', - 'US Eastern' => 'America/Indiana/Indianapolis', - 'Venezuela' => 'America/Caracas', - 'Atlantic' => 'America/Halifax', - 'Central Brazilian' => 'America/Manaus', - 'Pacific SA' => 'America/Santiago', - 'SA Western' => 'America/La_Paz', - 'Newfoundland' => 'America/St_Johns', - 'Argentina' => 'America/Argentina/Buenos_Aires', - 'E. South America' => 'America/Belem', - 'Greenland' => 'America/Godthab', - 'Montevideo' => 'America/Montevideo', - 'SA Eastern' => 'America/Belem', - 'Mid-Atlantic' => 'Etc/GMT-2', - 'Azores' => 'Atlantic/Azores', - 'Cape Verde' => 'Atlantic/Cape_Verde', - 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT. - 'Morocco' => 'Africa/Casablanca', - 'Central Europe' => 'Europe/Prague', - 'Central European' => 'Europe/Sarajevo', - 'Romance' => 'Europe/Paris', - 'W. Central Africa' => 'Africa/Lagos', // Best guess - 'W. Europe' => 'Europe/Amsterdam', - 'E. Europe' => 'Europe/Minsk', - 'Egypt' => 'Africa/Cairo', - 'FLE' => 'Europe/Helsinki', - 'GTB' => 'Europe/Athens', - 'Israel' => 'Asia/Jerusalem', - 'Jordan' => 'Asia/Amman', - 'Middle East' => 'Asia/Beirut', - 'Namibia' => 'Africa/Windhoek', - 'South Africa' => 'Africa/Harare', - 'Arab' => 'Asia/Kuwait', - 'Arabic' => 'Asia/Baghdad', - 'E. Africa' => 'Africa/Nairobi', - 'Georgian' => 'Asia/Tbilisi', - 'Russian' => 'Europe/Moscow', - 'Iran' => 'Asia/Tehran', - 'Arabian' => 'Asia/Muscat', - 'Armenian' => 'Asia/Yerevan', - 'Azerbijan' => 'Asia/Baku', - 'Caucasus' => 'Asia/Yerevan', - 'Mauritius' => 'Indian/Mauritius', - 'Afghanistan' => 'Asia/Kabul', - 'Ekaterinburg' => 'Asia/Yekaterinburg', - 'Pakistan' => 'Asia/Karachi', - 'West Asia' => 'Asia/Tashkent', - 'India' => 'Asia/Calcutta', - 'Sri Lanka' => 'Asia/Colombo', - 'Nepal' => 'Asia/Kathmandu', - 'Central Asia' => 'Asia/Dhaka', - 'N. Central Asia' => 'Asia/Almaty', - 'Myanmar' => 'Asia/Rangoon', - 'North Asia' => 'Asia/Krasnoyarsk', - 'SE Asia' => 'Asia/Bangkok', - 'China' => 'Asia/Shanghai', - 'North Asia East' => 'Asia/Irkutsk', - 'Singapore' => 'Asia/Singapore', - 'Taipei' => 'Asia/Taipei', - 'W. Australia' => 'Australia/Perth', - 'Korea' => 'Asia/Seoul', - 'Tokyo' => 'Asia/Tokyo', - 'Yakutsk' => 'Asia/Yakutsk', - 'AUS Central' => 'Australia/Darwin', - 'Cen. Australia' => 'Australia/Adelaide', - 'AUS Eastern' => 'Australia/Sydney', - 'E. Australia' => 'Australia/Brisbane', - 'Tasmania' => 'Australia/Hobart', - 'Vladivostok' => 'Asia/Vladivostok', - 'West Pacific' => 'Pacific/Guam', - 'Central Pacific' => 'Asia/Magadan', - 'Fiji' => 'Pacific/Fiji', - 'New Zealand' => 'Pacific/Auckland', - 'Tonga' => 'Pacific/Tongatapu', - ); - - /** - * List of microsoft exchange timezone ids. - * - * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx - */ - public static $microsoftExchangeMap = array( - 0 => 'UTC', - 31 => 'Africa/Casablanca', - - // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. - // I'm not even kidding.. We handle this special case in the - // getTimeZone method. - 2 => 'Europe/Lisbon', - 1 => 'Europe/London', - 4 => 'Europe/Berlin', - 6 => 'Europe/Prague', - 3 => 'Europe/Paris', - 69 => 'Africa/Luanda', // This was a best guess - 7 => 'Europe/Athens', - 5 => 'Europe/Bucharest', - 49 => 'Africa/Cairo', - 50 => 'Africa/Harare', - 59 => 'Europe/Helsinki', - 27 => 'Asia/Jerusalem', - 26 => 'Asia/Baghdad', - 74 => 'Asia/Kuwait', - 51 => 'Europe/Moscow', - 56 => 'Africa/Nairobi', - 25 => 'Asia/Tehran', - 24 => 'Asia/Muscat', // Best guess - 54 => 'Asia/Baku', - 48 => 'Asia/Kabul', - 58 => 'Asia/Yekaterinburg', - 47 => 'Asia/Karachi', - 23 => 'Asia/Calcutta', - 62 => 'Asia/Kathmandu', - 46 => 'Asia/Almaty', - 71 => 'Asia/Dhaka', - 66 => 'Asia/Colombo', - 61 => 'Asia/Rangoon', - 22 => 'Asia/Bangkok', - 64 => 'Asia/Krasnoyarsk', - 45 => 'Asia/Shanghai', - 63 => 'Asia/Irkutsk', - 21 => 'Asia/Singapore', - 73 => 'Australia/Perth', - 75 => 'Asia/Taipei', - 20 => 'Asia/Tokyo', - 72 => 'Asia/Seoul', - 70 => 'Asia/Yakutsk', - 19 => 'Australia/Adelaide', - 44 => 'Australia/Darwin', - 18 => 'Australia/Brisbane', - 76 => 'Australia/Sydney', - 43 => 'Pacific/Guam', - 42 => 'Australia/Hobart', - 68 => 'Asia/Vladivostok', - 41 => 'Asia/Magadan', - 17 => 'Pacific/Auckland', - 40 => 'Pacific/Fiji', - 67 => 'Pacific/Tongatapu', - 29 => 'Atlantic/Azores', - 53 => 'Atlantic/Cape_Verde', - 30 => 'America/Noronha', - 8 => 'America/Sao_Paulo', // Best guess - 32 => 'America/Argentina/Buenos_Aires', - 60 => 'America/Godthab', - 28 => 'America/St_Johns', - 9 => 'America/Halifax', - 33 => 'America/Caracas', - 65 => 'America/Santiago', - 35 => 'America/Bogota', - 10 => 'America/New_York', - 34 => 'America/Indiana/Indianapolis', - 55 => 'America/Guatemala', - 11 => 'America/Chicago', - 37 => 'America/Mexico_City', - 36 => 'America/Edmonton', - 38 => 'America/Phoenix', - 12 => 'America/Denver', // Best guess - 13 => 'America/Los_Angeles', // Best guess - 14 => 'America/Anchorage', - 15 => 'Pacific/Honolulu', - 16 => 'Pacific/Midway', - 39 => 'Pacific/Kwajalein', - ); - - /** - * This method will try to find out the correct timezone for an iCalendar - * date-time value. - * - * You must pass the contents of the TZID parameter, as well as the full - * calendar. - * - * If the lookup fails, this method will return the default PHP timezone - * (as configured using date_default_timezone_set, or the date.timezone ini - * setting). - * - * Alternatively, if $failIfUncertain is set to true, it will throw an - * exception if we cannot accurately determine the timezone. - * - * @param string $tzid - * @param Sabre\VObject\Component $vcalendar - * @return DateTimeZone - */ - static public function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) { - - // First we will just see if the tzid is a support timezone identifier. - try { - return new \DateTimeZone($tzid); - } catch (\Exception $e) { - } - - // Next, we check if the tzid is somewhere in our tzid map. - if (isset(self::$map[$tzid])) { - return new \DateTimeZone(self::$map[$tzid]); - } - - // Maybe the author was hyper-lazy and just included an offset. We - // support it, but we aren't happy about it. - if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) { - return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2],0,2),'0')); - } - - if ($vcalendar) { - - // If that didn't work, we will scan VTIMEZONE objects - foreach($vcalendar->select('VTIMEZONE') as $vtimezone) { - - if ((string)$vtimezone->TZID === $tzid) { - - // Some clients add 'X-LIC-LOCATION' with the olson name. - if (isset($vtimezone->{'X-LIC-LOCATION'})) { - - $lic = (string)$vtimezone->{'X-LIC-LOCATION'}; - - // Libical generators may specify strings like - // "SystemV/EST5EDT". For those we must remove the - // SystemV part. - if (substr($lic,0,8)==='SystemV/') { - $lic = substr($lic,8); - } - - try { - return new \DateTimeZone($lic); - } catch (\Exception $e) { - } - - } - // Microsoft may add a magic number, which we also have an - // answer for. - if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { - $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->value; - - // 2 can mean both Europe/Lisbon and Europe/Sarajevo. - if ($cdoId===2 && strpos((string)$vtimezone->TZID, 'Sarajevo')!==false) { - return new \DateTimeZone('Europe/Sarajevo'); - } - - if (isset(self::$microsoftExchangeMap[$cdoId])) { - return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]); - } - } - - } - - } - - } - - if ($failIfUncertain) { - throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid); - } - - // If we got all the way here, we default to UTC. - return new \DateTimeZone(date_default_timezone_get()); - - } - -} diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Version.php b/plugins/libcalendaring/lib/Sabre/VObject/Version.php deleted file mode 100644 index 2a64140f..00000000 --- a/plugins/libcalendaring/lib/Sabre/VObject/Version.php +++ /dev/null @@ -1,24 +0,0 @@ -=}G#HW*}&Dj2*V+wn}c zVj(r4SV&yG*yG*Gf4|Q;-4X&jncS+`Z0+u-X37}#%X#nToIYMTAAXt#(bNfkT1dx} z@fV-|Y2Z0LJmjBvwtwQ~!590@{r1sOXMZ zkG>7N3Fwy zmhrEq`SbJXp2V5PvUVR$f0k}qYu5McjlEC32l>O%QTx~ZAD_4Vzu9hm(f-tgJoo&U z`~P$Luh0F|pNCN_Kg}L0{vZ5C{BLgS|FC(m|HU67#Q$>req7D<}TX2!h? zy(r!@T>W)Dh}=6Pl}VcHxzYXJ=ji_*^*rnUY3$tlN&Rsde3Csr=KuRgM^F5}b@bW) z|A;4W!g=P*Wk;+QD+B*Kh7dK;uDA5h1O2mY7>OJEtCUA(p`VIc&5gpDKhI((^`lUz zAfeyZKP&^on93QBk)QIJc~B942(bsj=V0=$exCFH-U3D;2hpd>zdqvshx^ULC;s1P zH9!0RAMmioYmmV-kYcCV*l*Puhqe8qUE@#R2z%it;$FrHc{e`JvLJXAQ^&n~aAGfE zcZTcJ)CWIZh-SS}|E4b9Mh_CRs=`l=KM9NqQwI(&95Is*BEEn-mCzZCpl%3APN$A7Q?z7|0vmo_<=ip9fF4HR~$hO}1 zBR8WQUbumiB+tF+blg~C6GnR>4JDSg-{$*l?Cr$PQl>KA+}too+ecfbgMV!{om2k< zO+NSCI7*^fDiCF$Ia0dxW3hDR(0Z6Hrx2;Y86iA>Hj^dTzvgQp}l zcS}nD9$SZE5T#inh7k^Su?J&W`e9V42g<1A%eA(aYt)*r%F9i%)hdcpCbM#OZh!k1 zJQezNi4fq2!hUD8V(eQCT=$vIlIjRG#3G?LxI=UQXb|OOeL@e4aa|J?f7NMelD?*LYQH zD!UoWWpq!An^<=piG-Ds4~CVL<91mzCr*M806+0PWws<3rJ&rhm8-|moy74IuHaZ0 z>U>1cZj=QchR?uJD&aUm6s<_Xw1cpQwmH7)4#-Iw8P$=rniOT((@_QzE*8lN#8y{>JZjmDNa*)UI94uVtKLd0F~TkHgj zn9H1cv6F8gKagQ3yF`N@!UX-$^Y48xLx3#?W3})u5#Mp>@bByq)AF4|=2lw(M=YH% z+;EsHc!YU!Ik{Pfuqu@2)20dwNZ1V|;Vz_mM<)9KTahuxcSNQnh$!a41)O`w56D)_ z%TuTwt5D7?b5vN+At5TKkd<|14bLhfT9HL=ZCrp5j{9Yyvm3!NBbXupjJv>z;PQb0 z|0AyqaX^S2Csn9{=*+><&%mGg1~384!74J@kV!WHV&PMa7pquj!_nCM$r{3rt^Q8~ zYiLf^G(#06xcwmIyxGlo$r=IRv0 z>RP5yIPJW)4mcZ`UwMfOQVXA(5Oe@DNky~$XPlM^SvczmBnLIX0|W>aNjRuAwu0JA z{NtdOBT{`7J0g*E*r%*~elv@sWdSDZVj?B0DF24xFK1IhG-nP9G##o#jXtX+nb9-t zI+PEcV(UDR)BcQ~F5X^HcFrc1tQ=P>ktbOxP&E?lgdsT$K?OF1K*qfYd1b6Z5fRut zN#o2-pUujUBX?+_{s4gNVYFMi!(U z4)(Y6|8}!|_&NXo1D;;AdIZ{Aq++MHE1HeQQH_!y1hy>uAkLQ4w_<0JrmLj0x5wYr zak%X58b&t=1WzP1jktSH=*bvMoHl@u)2x7Wh+1^2BvBT-3N}sskU|;-Kp+kd%u!tA zPRq#Se2-5vgHG}l!PAsZ1B53)3_b@P6?|p{x*rG!ASii$%IP^xLX_(mhQLGMv`iHT zLQRevA+{~)42~rgDR@n13Mh+Lxnyu6`L3+;Vj@rcF@^K2^`@b>MG-ay$iQici1<7q z*svswoDgv7Wv=`u(UM~lPv`zDXuTfh?Lie0Au7auoD;qiBx|wj%vp*u8#!}B>GQk- z@bD=Jiq~JU=>0U|nh?*A;zaMgbRLWH4$c_9hJxuRc)N_$p$oZSHjso5dsC+`%YaG@ z4^X>aE4QV*cz~%snOIC_6e@#CRMm)Q*l(?gn2b*C>+aYT_EWWx3+jY8uZQaM05dHdZVE|woWeX5x#iQonYn*UXNaOhd09f;bLq~CP3lBI=>iL zSP(mmyT~)mNfpURZ*bWM;;afRB8DSd46Jj@#xQ$SrHy&Dr&Gn~RGgdR-dp_IJ+TIs zeZza6TK16k1C#}xyFdJyDAdtnSIcR2zcfzBM*`HgUH8)Po>F=AbGj7iQ0*59SXhcm>$afarJ6yJui?=OYq=ntGRG^PET@o$^AH%ae(1*_XB{&PJm?%YK5C z4`4}m&WsBp>3)>nEccJ=gNC|T2VtSti zJ{mIH5ZGvZLkp8USRYk!{no_y(EKpx&>nY5Boj!nXP1X!UkKGM@hgVr*}yt8hdqfvpPTii_lfrSZT#12G#gL)|Jv===lkD3;^8yej;Nfzm2w)d zfAukp_g}t0$$X^I6X1s|`CEYz6Dr6_-9b=u{dyG76>!|!Ftz$Boc{&;gznA6(EuGL`WdWt{?~AhBn({P?f~4-tbjI5* z(q*t0&)jzVpuKC*{P*`gC-y!%;jq=q?3RkcUbUT2c$5tYYg8~0sBtv2D@n#(G5 zgCOx;UdhP^X}Ii!#NH!~CwNWwWh4?mDGC!*1p)l=!&)?(DWLQ|uSyhxCiGjMN6|cx z4C_})%${?F=Ev)TN0oJ$EV#Nmm3aq&^t6bJekCPmu0;f!4F_E3 zAb3pm%_*T{x+01c9DZL6eUQ_{Ph}ycxlD`2Nznk#dyPiT2ZD_dB*pebx*Y4yQbxi~ zT2&=JPXd0&3e#&V#&;{&pbXtdDyS-~o*-$ty}dPFX#QV_j`#;3(l1yydwMc`|AOyZ zUl_me4B|Vi@&)E?LGNne#3EZ!#rl>7NY1eqB6Lq#+hqc^7+L{eef1TW(x@V2oKiNB z#X^2?$Pi%^bk&R0kCA$DAyuRqT>RE7oS0|UuyfVxj{62+(*zi*)2cTNpsl{u@6`78 z*dXQokL#I(L1*XNgIBw){ey#pR{QYir5^J`4Jg)`%Aj+B%!2&Sl|jHS zo9hL5BVM3QQQ`35=-@z)Sub!M#h`B3*Nn4f3~R=MbLe(6QU-yH1iw*|0)CHjJ-H1>$us&mUE}yVIc?%$={P?74ev zNOutl9$9;HIy_cnlIFShgEYCW1R6$BF{>NHCXtNtbfLT2Mrq z`fk#)%H4CmIt4$Pq7%9udo3#aFWWC`je}aVRna8UU8G9>bruU6?)@lB z$`)Fo>c5+2d}G4K&sSUFqiO)4Uw8*v$Dp*+2}n5lb-P-tAP_uq)Nd()my}=E`nKNyU|XvTa%pT@~b2 zUtJ{eSy@Z6vPFcnTrM3yAhr-)`OfDVVhx8r!;XrAv3BZbNEKyTD?}?|bsRg7JC*ct zB`f0f8&P>XIyWo3ITM>ilknLSpfGibxk+JO&hQaYv7m13c;FxwMA03xf08H#EM3KAi(Q0Pj;+ca_`CNN^7YE!z?P^9u%f<(WBsyLz*!t+D-wRV z869)iO{83xs1QHOj>8qLSkCCuf{#vuC-|;1=S3^Tt-(+xM>70EEU8ZfyFgrlql;nr znrSrWTQGGLtMU4fBeO@Ch-)6U45epKkSWlDgZVBnQNaTVXMOuJePj+s6Y)c?jwuA5 zUyS7q9pbi#f42r(AyH2+Xy@`pgbV)%6bWc(vUPV^f3lC-K*@#H#VB5tr_-|KC{lkO z6e2pUsB?t-*%h@)KK_j5>qT@qozv&zyiN)~fH_hx$vQr5nGQ1OE*ox?0s?3yxP-91$dq^`W0UhAB>FKVg9Qd!!P9#)$R$g0%-#896vAOvS&`XWamszrQr37|1&S$S6Q{aPyVfhk{(0=I z7D?5}aUUU$%~2Z6I?A_n7}{e#x4cgA4U>r(j+;1S0q12eUj#%P){p>a{vyp6;^^f5;t+n5Y-PGvX8^urU zjTVkJxvu9GX_0CKqrfbRO+cncF0b;*t)8+HB4cTkNGi-Y?-|^wC;|wfl&B(vpeFAU zbNs2Gk-%M9hgs+@0P*rsTA4Ho`oz;bwD;SMW~2G8G%D9DFXpVIfM(j1NF)hi=_y~lV2}aOUoZUGizsP;8BVDd zz@)3TGZjzr-kF?|I?^H_7v%WFlb5#u8XUp67(q1!(hB4RfiS(gMnL3q?(a!1L-25l z<=mN;moU_*n(J_;yDS=2*XU>6>9}szP+RZ?8TG{3beK^1rOp!i!w7MkxANZV$hbaF zD5YoSxwCmI`x+$q8XkFLE@bnld!k*{05+}1$+b}E*K zgM9oG9-hX3b^4c>@bbB@IcqBR-l-JrHAkY^>a<&(Mgstw2IXVkLEK0zG9qWMyQI3H z=PWsKc*1v5;=9NP*Hy>`BR< z!r5yXqzjy=hU&mdR2m6^?AOID1*yTUYG&t}+$@MxKB~IFSl%05wQF0`J@8X%l+@jS zYx;C@p)6+MtX5LpCqLi~M+jy?uUbJ6<334hr1JBHAM!Gg49QnKvdLR5-s7w8;L?0e z606zPxtSlVN6l8d)o9!rO)z(9_cUwIP{zS)U1}=r;RsCR_w_h6UtFNO=96-nv z$ASn-Da9+oPk9pcYb+d7r}FD<^WK(i-8cYgG^uPjWQMgUq(5vRE;*i3ublq#ibCdf zVTCHhAhgMHuKZC=7lhnW36uX?17S0YQ^L(5wSmK`zabz>;OCZ@xfLbtdz=}>h1&V7 zr5u!c30yuiZm&oV??yv2kB>U!XpH^5ALNG!A`r7&*z#s~tVH(Wsm}h=AEAUCoC#>C zz6KZiCp?5CeaQz=Bq%d3iaH9#mAAi)KU3>rmNBM=th$My z(BjiUU)|ua_J8bs`$OACw($L#ze45aTSIFbKQJUtN!I}G|w|<&YU?HsNfYs}Zn^4iK|4m}!8qvPT#&ax$V%3`gCF%|uZ_uXnOX7GGT3k(BChxh{ux zws+V^Ld;!6mT<5UTEMP|4tPqvk(`quA>( z@xVp~$wC#WF*j}hmHSMHC0lk-nbaVKZ^LCu`L+*9Tfd?lVCqkpM+7p+sJbB6`KdDw zDJhEF9DWr@VbL~GunX|{6@41;`^X);Q)(`44ajN&b%LI&OD*0Qb$N)j23S>=n5PGNuharv1Z%{u{%f#)QCL z;;67h!qDW2(#v5~PphIrMoh&YkUn7r z-A>Oi8&KIrlTl}4D+9w3u({A$@;bA>VIgA>aisQPt0F-mL@_`a<&qiYf9?K@9(jTW zP!t}FW+B~iXEQv7$Y7?7l+X$|l9#C1B=yqyTcb9vTHJy+O^?I!_SZ~!TPHnhZ zsn?x)<=I)IN$xPUTBBAQJhL}8RMAtt_N-rD2Mds2mJq~knYKK~%|m`Y=UaNKks*_c zG+rJsBvcvRK*;mgyiPfY#H{A;>^2L;2UhCw4rB(IP%D$2RHS3X%>t}IAqdSXA z)^HKZ06ElL;Z*}@tmgzm@CcP!dKs)=&-nTiQn6=tIjs6#c*l%8{)vqH)>-H|E@quN^( znJOx`RH>Lr5Z37*?ym~{+^&a0JkA7TYh?>WgCk~ZL@r6xa){q*LHQrJgzQEa5lTD> zg~g2%5<=@ScPUaX5JDYvCl7%wbqFDGhLnVJcGae=DJ28mumUhcigD%)Y(nCeU^~TK zn3;UHx_v!U&x_p1WC=klpdNdY<|!`SLi{~3A1E`j@40X0E`-whq6URrzci=2ZKBEY?xC5?G9pd_m2GPKdk0q2$ zqkfHf$AN66%PwYTHi(yeYDIwTJOVG;25W29&uGtb;a=m_fF3CWRl!%Ko_~maCig#a zZDLR_L%V0UT$+hY9}4C?YPeA)-)Yy>B}K6+(a_VP=*e(XM+2%*pobYK9G;w?MG+3D ze3&1~F+i>0?oK&`lfJWqn0^-oWC`?8WKypK`|OM_4_!YpEYdmR;Fcv1vGRhV?h31m zrz$VX&=shcMsXzG7bbO&p?>>y4;{xlf<5NDX(>fcTdy~o^?svC&kS_#%~OoDw||r*_kG&0 zH|raXr@t)m5j=nO5)Jd<@7USB7CX$7t4SyATI}det|py}Yq5hex!Sru$n{))%{&#p3^MG-{9jUk~v7?{{mnFu)~uylV^s`r}XV z-z64k$~ryW+3lU4f<@+9wYtXQ1H-Kdf=&U7lt9b|QZ&Lnhf{L39iNih_bG^_;0myE z+~3(fKr1kA!-Cc}aFbk_1~X^%xkbK>{1N%0i0{mTI?6LIL-n?`OvbgMHCLhitddXE zK=exk`73^s_CLSpy8QZt@p9&)2O0Z+oiI|`{%;Tty!E*MAK*#e|0ZF&dv2{fv4e}j zj}0z;domb$>y=agAS+dqh5UR~ObjGd;`Kssub$t9@$E|u?b9A}C%$$pyNyDxl) z%8aH%@}_DX()bu)@OUA}y=?aBq{p0GY6%|@>z3D3d^1LW{))r)$NjLy(KtXvogkUoG5W0yJo0;06N$Qp+ zY^jt;1+*|>mcHRy1Bk9wLM?b{H9`GiG9l)7CC!SMhsy#>;A*X;d?SB_@C!Z)AqAdA zuDb;gy}5Kp&IMqRn-yDN9H%r(Q0Q)1xmf9 zbwZZ@L+gsK-xdRa9oqR7vQ;iwY9ZXlfez5EZ3Vk&n4ynN`lSQl$@Z~ z1jdLNBnjQUBO0+r3F?EvKL5+b5ab5VVf&GWjz=rs_w+&!e}9DX(HDyV2Cn+m>%lqb zsjBWEM9W}^ZW-0GtV1dih?f(h3G_xl`2;#W@Nqyx{BM|9XF7Gp6B@O{6i1%Ly@$5Z zOonH@mj*B=`Rtk4}4$UYi9(p6Cj=|wPIjG z4=K1N70ke24t1cX%Ry>!Z6GzCMs=VeUf0qRE*j;1kth)Y;!RgLZmg0EdY$hOiW0iC z1otIJrP{~uV5^Cp_Jk$}Amo5hB2O+?E(O4Hf3_S#C-l+?645%7z3{sAkE_<-{$@$P zwLTe`Vx87Wr#JCVMR5v#?3N#-suelJ#g_t@F*Rw-h)0oNQB0*XJDvj3Sf4tS6&P*-O-R-wN{kllNSRzjwJ*s;e;38R$c%z zqY!i?zme2QBboYXB_x7O;Q;ot-o+|!J$6K;P)pHsZzro)4E1pL9?hqM-Wl}>rAA;g zeiAS%CT>6O4rw?&W^V>oMuAuqg%;P@x+#h{q0?^nx<{Se9v7M2KqvqY83GP6aeL-2>SLA=K2B^h!GoMMw; zbf_%AB3;l|#R`0WypI1x9@G6_L=XHe^t}c2ftUQ``2Y2ddd&V~V{^0lc>jNZhsBnJ z^ARE}sWB$01?}?Cp&TF~nizJ{6lM!laS*99e+<@0%Z4F5;H=}U4e$8Mxg{LKFoa&; zTg$A^W%)o}=I7+H^^w(rAa@f&D{-v(7*yWj$Q?LdNNc`4-l;Sz+aoY*=HFvMIq~h} zaOF;)f?$mV$dSM!pakWir~^K1pbH?-eKl} z!LoK0X6s?3Ez?nm%#f z`kCvg3%Q&iQl$-iB24fPL5syqF z0(~&msLm-+zxT;302WhtwD4p!%o&|>9B1fdHKq=+7d8r~xUVo+11&|qbCG9T6!aGF zp-e_?dvtstG?C^BcJY2V<^JYu3W0O&kHA5l=8z?0u_qYhgmN&L2@jL0$pKiGSPDf< zf;4ijA!H7pRcLTzgFrew-hHy$zg3a=B=FwJs?=D=O6W8+8mg61Dp<{I0s`2RCMZX? zo~zFW^tlh7fkQX2R#-q#%y);a5Kyn?X=X$8+m!pgxw)yj;{leQ3|;tt$OSZXuB%X) zR5~Pc4F{K`VW`ihOlX z{OnNIJQ~9mq8wqkQZc`2G8S0sPF~`%bp{TL7eMOX;%C@Fe27$-Y%$07u@A^yWr0_L z9zfF})#Zsn&@m4|o`fEEgzL)2{f%a$PL}l(8F(IiWo3h5df{PUpHC}S&JE!wFDuTp zVvj1qz_sFf6}a$LYJmN}(Vjcruv2sQLSdi;vh#I(+SI${5gG)1H+FJz_+IYJ0 zlx}5=c>U|Ye`W5tIAJGR=BgBS=H)iC@e&Eq4X8yoGmqXYVr0=6ME+9jILz2;WSmP& zI}XMmg7EXI6PfU9|MfYG%2WQ8IsoG|hO!sCe0l=SLkj|-Hj<;EPd1?- zOgtGYx~H++bI>uMcp1E`PrQmi7-^z#5TyB)pLlXDuFwZ8;K9rS%8Ph5Rh|L9GvbTr zN@p7`v8_jA2;=^8SlU(4cu4saZuxvI79I*CwS+zkC$}VLEM_*!e&AlX9$~u>pXm{G zV~0W*FIX`Rq}Iv-q+bKm#PX6e9jr!y7_dLGsvgXQ9|9EFF5QbukmJy_;h?Fcrw=`N zFv6_u-D2c49I`KGtze812&SVzATZXk14jxV`Vxju9df*);pC(ZFqFTMc7V2F6D*!# znXsg_60OePO#E8mmy^D15q2 zOTC1pX}kxXa2>-V;x=$W)py}|Ga$}5ABac*>1-xZZVL**C0x2F_X=ZGApA52U!p)@ ze&sk>GLi4U-^KL4EPU4Y-?v$8MxY&MC=mkXjpt(kV4hs^hH~H7p1p*)Q^DScQaF0* z`7ip!Syo{}ctlak%&?7xrg4T#;2Ju`oIJnc_>Kg6@MOY%VB(VnX`#w#!NpS+|I!DA zXaHf55vqv(6I0U=nZP0YbqN%#qw@;jB*7cx-`J_fag9ppm_i?DlPjGNpu)5c|H+B| zlIb`{Hi%JP8Xj^0+>x|>C~DE8Nf2dV7J%a^Q(mTwOb87?jtmH8v?O{B5e*4HHMk_) z2FCE=Gq2F(=2R0$vkOeI+o2N=VqXTu+yr8joYC-(OGBSV${tDO0U~rEB&gC5ZHZko zkOR{L=)n1ik$(YP*AkN%mw9|ef_(^8U^B$tvPB}043UeEQ?aLM(<2s{4oFc7s z#*_u~hVe-064Rbx#+*vV>M7Mkc#J4gyfkx`Q68uvUSc7ok4z)fIbb*@XLMd(!UK&( zXhDL6xq!|`&L{WG#ki%&Irw%p%ye~wfbZQ^F$@K@zb#z^Wyjn1Qv4pG9wm>-;~hBh>mA0l1_VotNxq8CbtU0`-> z2Krwk=2@RJ94||TX+vkfMgcZ{#eE@dp=qpV8}g)fF@k`1EAI@9YQloTusBbYG2=mo zk^f*L1>WBJwbSo+w%`1^B!(k6KQ^Y9y9b@#yI;r1#L-tLyn1^m(44{?Ifh)qk%+G3Q#rC&vWlwaA&u&XfhF_1+_EN>o*+5hl^JC0e9cOaUy zl`v__TtpO>&x=L=ieSZ~@&+u|GvF*lAf$8HSJ_p%6*%|A#4=Eu9emNI8f)0eLQFFI zD9S-Cow3ve#@hy@=ipE;6L_BhxPN^qqMc$7H1fX)x-B}8R)gT~lDt|XuqkX9UQzU| zR^(y7#fVU>LYIc+i3$n8r_mA;ZmP^jMK>I|`&hfymE%l+x}%R@IGqiymKgYur6*hz z*^;sxhpr%mk+Oc}t6(IGj9C6kC7zVP0*Z4G$PrtYmUh{t zf*)zzvV1sWX)Da9F293EVV?~%$2QE4q3L6E!NZrgf)KEGO0a(!`}r;BjAqJ;2w?ok;!Q=t$tcOeNM!U1O$a0vT+;BQ zxN7`Fj`HKVp(Hnowg+ZhAZs|Jxvr3DrBVM0!v=8z{WItp&wHO)H$cTD%2`D>HH4wf zt|@1b<%Wjq6`*91e`q{&iYO+#2uV7NQpBi?e!Z766h-n8N*A8<)5GIQq~OF};v!%g zaGoQH^akh&MF}Yw9Qh1qzLZ79S)vRsPe?;4mNfDz&x6J9q!1&OB6kQxXb4Q7P2pn8 z_aX=P>X}0hnrjH&EoGIkS)@Bmk^>{SWwG}ye>eoznQ@!IA&kYQQLk%AbQlZ4sm^2; zGF;#WEDiu8GE+>(ejPt(G9quZsyr4RQ+ZV;m+>7i3)xojw^vX1E&2NQ)iaL`0KI z?6^=`rS}XD0c(Zd&S+D7yy@G>Z3PT#lt* zuRGo?f`qa%uK4*F~E8G&bmB`j}(=H)^fcn{T0%oPV9-Ttq!QD5JT$N$>gc(ng}fG6A8|NHNk ztnY@{?ClM};qq8}FJHn~I;JBsBG!3(Eo54G(y zcN4u+Q9sA+!~LVf15)##v){#ZHKZ-Vx3WUurKl8P!~grhpWIUA=eGE3WqZ|X5Rz2^ z`wSW|*LpoZdt(`c-(oY=eBTm=`PPcblztVm!!QlDu#;|267?d#KA9)JD1@q!t=d#V2>R&qnngQ!V<;)ehrFDRFkn+ zYxwVEc19*(Jpqh)9&3D?xoIj1@4caP^D=dVS~X)Y-0f3w?AQ9-J=ZUGG+WJ-<(3%R zlQLCK253B8pTfZpVvK;IwwN_8IkJ0A#nvIEwH z0jswWWvF}!ti~dmc}BIILYQZRq)Ry7Qt_Mv={qD8WP*K4Dk4>;Y-K`Kr#fn)+J?~q zy6F=>Q8_aI7C2J^VfnO3L~IcYf`!_9LMKNXgqVY~(MWNU#R4?c?9`u;sRXo+>=GL} z^n=K@-4?s&Jr@1As>kDC0|rQ&YpCOr{Fh_CzJ}(KD=V(MhUK@_#n;zIlCLAJ6Dd`} z*l$Wjaj0P6I(|-2CHt;{?eXTKtTw|)Anvf1z$S|;`io*omr8Q?KpFUiH)o>w0vk?G z1x4l1Gd5zx5SSNr`Zwc=j3(c?;VHDRBASG_o3sU~6Y=$V(vtPbiZgjIU4=7{9byI` zJF=}HN5{xNi|sRpFv$n50E}$FBhNXM!#-e8PYDVvSi`ckMa0PI2wszePQ)xiI%8X< zaNrZlLG5b%09A?1b0e%umF0H0eQM~B93BUS9hMyTj!`8iABC}2I%sl8@D@iPH1LRu zV{wi{_Pc=weJpD8v}nda#uu!ClrdjOD!W7lEKVlpk?Z&YV5Nkq81$Zi$CGbU& zmZD2i)({&D9AXxxNP$J3%V+8st{&`A;iNq3Vs-^IV@3O3mZ*nKXvf(q2BTVu2uvAU zjuD0@VF!8>#b|XXsfBtCUuxyfM7cP9f=0KX_{&ueubNhly~eUX(vm+}6^M^qmqV_| z&5PhJ>fGh1L~uG+w+t~?qS??Q)-s4Hava}cxu?k-NoP2WagdmVhaM~%55g1P4XBSL zn$}{=Y&LAYW#8D$6zSD(@ zz*d%*Mkhjd8g%?7snpE9G<0vME5qEP&^+qH2VEk8I~8+yQXrL*qe@9srHAN_8xQY9 zV3Wb~yYl%ZTyLZsT=ZIW!@ut3ttK8>AF*vpr6JBhJ zCO=<(bGYBFcK18Gd&^*T%o~g3KxYmEnlt3iOm;9p2Q0YLVP=eF+CUZLs1UY?G2*;2 zT7#u}jRu0IuiQ`{$5h7JG}Yw@HaoK~2FI2_5KDFntjJ?u)%)h1`bsYmO9>`0tUb81 zQSyTaE;Y3W&nh}3ZtAzpy3&{NpUy26851y}pArvy2|`H!(K$`? z8m;1h3=fQZ!5S_sB1&VJohS#FF8=^pCdW{&|2ScGm`F@TJ{bu4OfL0I&QhwDR=?ZY z!qB?XVj##=Ch47&vJ zu!S0P`q>0ikS0t;GJxmOd&(iw56NOHiPK|V+l)*KsvgeHDv^q%Dqk4wXls+LN0ubHW%U z=_{-_kA#VKywu*^8FIdy{C`CWkV$^-%6_aW6BTZ9H};F2Dj(ww7;xB%NFb6DYE?N6@@)`n-@cGfeS0t z|Ni&C01ka4o#1I)B2=VAnF(Nn7z0e5U`1OrKP}zwWo=n@&$$h>gV9eYGg=XB;yDHd z5_e*iP2`Q?F)U;L9NYjuWm4Dw6k<4=_yJ^OoiGyFD6E5e{IlSo5ha-}qND^*XGzG&Pm~iI%qYNxy(aYz@1J;#>3NLDj=N8>=Gz!af zDtv>*EdYFxB`Xk6AI5nImtBirJWYimyMcI^~C%PtGi)S|1PvttOd+WNK5H zB+96A&y+%)o(fD63hB8p<_V~Ze}<^iu+LSz`H66`(XTco8w$-~`h61z25fiybCNQE zmQ0tvsG=}2Zlfqnm()bXX$A}EP2>KOg#JdMkEsETqEhRHQ^;;;2MkiNu|5%}nMrzu z%NsK{Q0hGx$asf$fm%Wv=|@p6(HT%4l$9}>6Bm<7R4D%ONa88zf?43iUCLwR(L=Hl zVx)~kgE=H8Jm5*9AxMr>BnM@fd3QWT1V_m$@C-ZVyF2SoN1v;YI?gi2QXs5GP;>}e zpFe*#v54~N4Kh3Rups|+qHt5(ibG_E;vlz?iOTjZzuguWb zTAE{pumSaMTtIN5z%&MRDtO?@ALF-NV~u1g5-rM?3vc}RWIP4!|L5`qDenK#s5e{9 zNB@rpc#5t+H6AYJ29oaoQQKT^uIuaHY_uNzKOW*q_y34kpXbJW@Z32Bks@MF6yFfR z)w9qiA&tPot(NVPINodwb|v&Z7?T<;+u_dqjk#+uka1s)7`zRR8$x|4Qm_kiU&^we z77|(oH6f38N*Kb63nwt@FR8>HJ$D=o6oJ*tR1Gi{DrMG0syK^rFnnaGT+D?t?Vg`g z--eu04V(szxG9=gkET)Mx7N+%BME3T2+EiMWRI| zTzL+tev&LiS4+o@ykn}Vq*A}nCm(CWplfq9^o!Wu*la{El@}LMW^zMX%vh@|VYr4# zy7^1e6z>RJLRR2vng!d;UZ`VEu5KVGPtiq`uIPIZ7E-#HL-?6WV0#ys-ViOK$UdYe zg6{D}!?AMhmlQG>Z(i&==ASdKLO591ymH}-oE|cU9}EIU!3YIW0c2y1W#XE#;avEp zE`8FhI9*)Gtk~YnH>qJ^f}1?}VsINK18foCerQ0>KincBHq-EVABp3Ps{r*@GHtjZ zCo=3~v{GUDwNV@H!EQynBk&P~@1sz}9Oe2L-daRrqV&D!u>!m&B{~M57ex_IL%?Gt z4E02+j3J~mXoWmw5JHZZIuMJ-AB*BMZrh`s&RfA2v)H%lK$!GT>=ZBywu5Q%XU7v= z=T_frL}t+F&BrPwlBYSyht^_@VFT6}_%)t^m6oUP z4I*<^Bf*M!{}H}fiNUcP&A3cp16Igo&wE(+S=DbaXFbLut!g!y~(i*FRWAs$1Z7qjF9-BbzK z3+Vz?GQ|$}ER3-Lws*WWaE%p50I)q}7W6Vw(*yGoIgryph>}A&azng1mPv`Z!J?_e z$?lGlH30$^j%LHiDjVZ*hB4)5vX7CloOHqJATcA0FpxNIkSrY>V2uTwYcAhjf2Ps5jJCIT*{cWSLUGB#o(P+HV zsmU&&WKozmT>xiL7$;bY=MgImgy z+-wMd&X9sGe3@OXsZ@<6!3td(xr^ZxYXU<-`{VRefk5D7YI5PcS zn|iIyw5gy|EO?(K7qjlQPMDJ@GqaV6WT}y5#x+9LNTeA(r6r8S)NtWSGN^HGb(0!+ zA_^wIC3MGdV(@VUKv3@9H8lZbZK{$54u6DtqWn>;0JEyYrm`-WsgqWkpSeT%($7aq zUYqXxMtj5t{^z^6d#pMzcgo}f{Iu?g-6QOwaS_UvJnFc|YURbb z9Zm`7cSFbm1G+9XxFO4kuCUCee+6|@_!~rf{bQ&^Uj7*0A~JxY3SAG{c)|9Vu>uZH zTU}NkU(so==tHHjiZui^J;GYY%ByA5k=Jx=i#;BZ0@&94S8Qsr@_GC%1OQ|1N__RX z`C&w?41R403GST6M6$ONP`GYwCAZXW?;VoMMgN!75E>$+K_uzbyFzhZIi(+RWF|Mjm)!O&Q<6HO| zsRuL%r}2D#!UIU?=B!cF7^8y>s|Mb&^_b+D_lOiri_plQ60SuF!UNXJ@vjV!_#IvP zW6^YI1`h5$&NxUW0Fdse&L?{SsnqJo%a#mX0qn6KdR8a}vVz+IL^N?YA1GYNN{wpG zs4&h2qHHk5L3Ie)-zX;vUtLIcO?C-pY`{cQ7SF2AGFPFpxM~1TCWtgJA*hz(eYN6??t%QSSwuM6Mc|< zp4b>A4|ww!+5XmBXUB{7nq!>)o-RhhNFv7N6?&v@S+$(9L9}T^{699Dt(gB$v$nDMsQ-U}XQBH4V3?)*mvV#vsiwiIX*^jFMnLRK`in-{a6Ovi zZi$CT`z8$l2-q_HK$5lpy&Z$*|1C}MiK3n8I$SS9VrYakMC}DF4#GJhDgs@Jq3?u{ zMHcefV;FhXUK!f}pOY}0jUzo3mDXVJc5A2ms&lf}Kjq>9GGbE6G~UM`NUX+RP2TrF zL6H7Z@<2J?(&B_PeDVUwe`BdK=%z{u3=wUAT0tWVgxpf}b>0NNhhT_v zE!`PKS9BBM-C$zJaS6xkXVn%kmpJ{vxoUJJ@q&S^oqTo75`$Jil(!vggWD^rXr z66fSdBZ>hxbN6sQ4IBsCgM~%kJ7%%X42bzEN6koRr+v~(GAR@`D~(GY2)Cc4<1mdH zJpNccY=h#NO)%1dq$`A(Www_XuudJ4@G4BOcT{a5HPc439a`jih)qRzm@L=sxC<;q zTZrc~AybB(iC+~(83^RuAz5pH7-W7MjAkJjN7Xv^nVVJ98p>(n7y!BH{ckgfb~l{T zWkv&UAYFvJoQ;Oe${TIu|1pF6OgN=knAO(s+d6tE@a(#y+bl#lcAU2UjuGQ2*|=fV zb*AT4Ke$-CoQ_9p%*f||C*)pMX|1b!M!>&|Dsvf_G1zN@hyq^7S})9>UtRm>$yKOL z#iQj~VDrryr+Ucb?)H&d1x8(hqe&yHb|XS-}otAqLcIh0#;E3zVDe z>6MtDFg9*dO8)7+gbVZkJah5F{C{iB&2@wSZ>#pm|3AR9K>Z)bj79tZ3R^8`OG-oX z?Oh231dH4XwrLc4g>Pn8E%JiHPIbD&koCY1g(F{f2eYqjcLX|~x&5%FcVMw$`boU? z)W};WI9dtgKI&?9PTD;qU^OvIKRZ$UDAyzGHZrZi@4R~1e|OZ~Lg1|cP?_pg6FiYN zq>>fFNc71${I&}hH2K5HLFa8}cdzquudDJq5?jFs`o2VK#$&;SvorE5vc8VjDA&sC zt03+!F$lzEdAU5G7$eC$kni#2Zg=UAWGm3R#nL&34xqvm-4FHVNDE^-R432Ek%Y2T zw=7F_#oDFEGoj8m^zQ_rHdU;$@F(CxiQ8dHQ>nd2T>H58o?zHVn$MXs2%z8gOwHM6 zp+A~U9q{+WtyJchD4rn3P>vSwCZWgA#_OdLYbTfVyD!gl1xbPL5GdeYs5*0=a4DDTja}Xw}+bXX!U1 zty%n2`26qtLw~XM-|LNyxc;}^Xg;3*5AZDL{0FSKm%|^N`o6`J9|87w>^~m+xsQE0 zw5?eU_0xO`qj*0~yo+u$*pqI?Kw-;|9Gd1pX=eLy|7fq<@9v1k#G9-p1IIQs*z0zb z=IC#8W4Rb8`i%$2&f;z|IILLt>`3E08eDGvM`NcVh2L;y4AVqAFbW_f`32wrWMPbk zA>fq|UuH!c7gontTi9IQvbcRPbX{!L+sw+`CbL|Ty$9c zMnRQnHp{y!?iqF{cUq22_r8z(Bx7mLU;yEig?eQ-3a(lE-4N_mi|ss2x)}U|EZp>W z{cd2)X2v{m^=ks$T)EsOA z`3yF`_nRNLwzf*Ylze`F?9s0;7Iwq zhT&aPav0+8*703idDkNEv{xG+%P?+v4ZtcAHv)LmPp6#B@{Lf2E4c_uOnvC7t1}Hy ztWFN2Hws83!A1fO=h2~ZVpPN;w5t%^(swl}ypWB%f=y-b&P8oUy2nVD546itb{Ndt z9FuEN$X`y}a|^+%-mC2xUU6}WY)6qClG|}HoHh4H6DY(D5JaqXG?k{v#~TZ0?$qXN z4_fO`d-XuH(r!~VNY)9(#;kn?8I|?K4v&>yWn|~0{6iU74!jpap(u@z=!mR4N*Ps| zRuaQ6^2@b<=Wu3mf?{;>M<>QDG$Xy4-R9rC-6Z!Y~= zfUn-LBGAgz!B_k2Rp<PiC4B|P*hW=D0=UOe-X9^o4@%_T+KgQo;bX!1=4g`VLxET`!`T~*T; zy11oO@8-zl3rC9vUkT?4dZ`zxmXbvs)WxJLRK59njIaCr(uG1L& zB$yGF6%OS{a7#C5nQ*T((ExxoG`UF(A!!NNC4zHHl-Ba>R@v-Ax(~r%Xxo}~og^|Y z9ciBSPq34%2v)e7AJY&I(ZJ{_jg9@Vr5F8vGf6}z8i$JU+!|u!)h#+sa-{6&nQhg< zG35G);xO@+A(r+Q5j0^UqT^C5m2SLK){s#mD$hZyP%XX5Zktc2Kl!h=*+-sQ^;rk#_piX1YBOP3Y%9R%yQ8dXW zNjcSbOFH{f=%k588t9{mHk#>ToF*zEYX}s`;SFGuK$eq4N+!n|K}0dH{?4#$w0E*c ztWs4cw&I!tKjBxHxTv2^6G}Vjej@+hF8x4osS0Vy$1ALwcIb=7ARIN6{;v$MA8Hnb zEg6{gInI94l35HWV;Cu?DD!&ABJQMepJ`W^xM<=JUQWnM`oARIc>a(#Ms7BcUVX|+ z4<=~^qu5i3P(e2}&{gGCg@)3>H+5Dhb_-{NrtwL1MpV0u2#pqj0^!710n+f(9IZyz zx(xR+b6qCOiNVI0F!E2So5(5uXSwK#O8)7SVgJ+GXm0BA|N3VA(f;Qlp4{?(h5<|l z-?4@~Jt4UV{RVN4M(&lPdT}{;r2-F;Da`>!E;ckK+|-}YFm=$DGjb5OSko{4>7|_W zk%r`Bfs=qImOPpHAcTdGq>;eRCV_uV%2HwTg&RUBBe6WRt50Q!(&~a$BP`8>4>Y<%DSt5mZyfipwZR2`|wH{LGQlQNk(6 z;#0^!-@hU$QM%Yf`+gfT-x3R7mo6oojI5GwF{D(5iH*5 z)`AcSRqK@-gi{O&F!Ukd<&7OBt0eb2?%_`Dk#3=X0MQC5Z;v20;w=x6xk|%Gk`K@X zIyXYhM&6bP#?Dg8iKzPQI(H7^5E>JGVlQXYvUxg)@DS@MX0CP2cHIpby?Y_TMe@%8 zM~){eG}`17F-Q?Fk)L^5dCr^A_~FU5bri^(p6&Eh+@`e@F9brsi$lrN3uk%?zx9v_ zEZ`Y@0+yl1)B`N^IlLxpON>3C(Wqu#c0UN}5%t8DxU z)Tm4i{CI<4Tt{dpV2+Nl@BA718S>4BjdG-7oHyAnm_(0?1OQNr`*3V=n<%w(f;+ju4-&S3VH{IT?twrlh~ zvaKzMd85!Nh_Wl0JJOtLmJ|?ewA-{%+LkHxtddyb3KAL`a~VFlmL2;b6mNP)31%mE zo!4kAR`Uw7-zB>m#Egt$Nm}EZ0N7-X7a4NeMqwHp|%kM9xYYDABx+qCx9BiSN`L8;KX?`$$BegiCQb z-c`cJ(mGam6mPXGH}tT21dC93j`nTknpw=m-}!qob}BgB=h58J@&f*P9q-2qb684Mg!HGa?A z^*lm>`NEhkXNH*Tfj=dn1=7na#$u@(SW0wAq2&=H66if6r`nLRZv0Hua^q&2Oytj#rP@mo=A*-&Er@&?KoA;(i)ASlv zkc z!IEJ}2hyB~K}8BC5G#or1xd;?ADLY5Dl>|a9~$k6I@#gwb3Wm*3id3si%Yqw1X@dT zZJeJId?<`xv|1OtTw#qAz&2VT1=U_V(@@N8AydNZLXJ7ThVdp(C}}E*GdW(|Oj7PW zdAA*H@i>}enKq zyELZw-P(4%p($-2S9+U0AE5OFZ9vpAY&@}jwGQNK`Gp|nS~tjJGPk{{2z`K?k-Ru$ zyO5sBbTkQ`wh`YcZ49op}N`DD04mI`gR&^_UJq$g75S5U+Wsi560d zzk1vQiNz|7K)b?YNnrV8XD8Nga*J$jqLMk3f5?)ld6Uh0ER4rbLxlhXNf<4v6JXil zIpw9Qkz0ROMRf!aRLL`xH-?@P!gC2T^PvIBROFy9TRbg#ndiMLTh4S)H3cKlX*?Yn z1mRQ^7KC-hGy%YNCSYWw+KYsbg|Umw?4{U+E2bt*7vN~)VkRhFG@Y5IjBhd(g*CuD zWK8)JnJ-*x8KTi8lWYkeOw_@iAmsX^NFTJV+kudyn;Z5_{E#~TN=6bwKFha^jqE>C z^@Ihu?XyBaV4fBdi_WHr zyUjQ$X&pq;t@Py-iWur(WXi$C362O(FZD8WlVXW0@RrI_ZZF(u{SGYbgY&c5atxlS(Pz6S$5U5jb&(>s%$grq@bW+6EjI| zgdWnS%SxJNdKQW$cc#F=7O{z`5f77w9zU<5L~vQ7l{$+xPzZ*xd}wlONlDR+Ba+-d z(}Px;Xs+lgdyh&}X8{|>jd)NE0oY=tC^g z5^XaFsg*ey^MNP4jz!TtJQ?~&)7)Tul;v8vz8X1c$GiHW`TL6zl6F_4mElR!`C(Y_aXsE`O(!25S!-xJGH~jG77Yyt}cq#}Vqzu|z zSm{BZGz>Xnh=DAM78@TkunX#{nWe;^!ecb1o=;7*_?15S>_5R{cfQy!@MQS^ZEiH0 zvG^~w`sSnk=L0->?LR>q$?QMRG1dgw{S3j^`J1s?)FP(<&Kq6rIu{p#bAd6TWIsFE z11}-A%DE45K;W*8#%DG+IK_zQXVhIAl_@}3SDA$!*I-i(HCD8G-J{NKkC3!_+{CXh zTwa(>!kIm|g3CSRSSNQC7h^(`{zm>;l6Kq>n?*oJG9zGZnf(8RGo4QsE^voNLrKM% zRO(AIW|Y-(Q5GMmqYk;Jdso~AfX2c=)HHQ?5WQCv{!duFq)y(lMPaT)fEnojPi+!B zqvL|S+%%3pU2GA~lZRm;|6jq8wcBi6D`0B}S<-0CZb*z3_*afc}+uhlVQSMy}}VG?>oNa&*EB%2l6Dz5r&dk$twGXx$dF z))G)QwrH}+lE~&04XA%6$4WBxNSvXdTRdu~ORmVQiLb}y@*(}#?(V!PpSF2{&P@S~ zjW5j4^C97QSC7%VLs4$|&*e(_P_C-^VH^Xlp=HkSt-(EJ=xoDjK{$wAw9q{k_P1+m zyqFTwX>c zx@x0;o`t-Kf=T^?L0oj!nF_ zZg?PCCGs7O)Qaf@Sv2-&-=V> zVjY76FC1^Pi6q7{pF$~d0{JI-RN20urf7f0yDexN?O=uTCV%^z72SC0yOfMW$*xcR zOP;4oq)bHbJ&@v9v`_Y(U>x+OCau%&fr$93i-H!)lNIFiRTd;Y*pk>@rI6khJ$sap z?jTvLXpkwqg(lKsFKI*6$9PWy(D=Pvh(C4w&_o|*bWrC+_g1u!VtK+~kdScX!v{^p zw*0|c)^dnE*OQ8r)qEnN8}Qps>L%7v>4O)06YJ?iIU#jPeBz+<+X9^*0sARAMn>cy zT7j9DabRU-;gfGf+xBP#^2SJco%%G7R{=!*8uLTs z=Kn%RP^rb{f3DS9tyuieCVBbD|2@Eym;V!sPmeJV)K%2em_hbGzt-2d_u^djFN9pg)xb%|u<7IuYv9gxe3=|)P$aW+PpkM!XfD5mu0aKBqZ zjpp?L1etl0%cB&eZ@lP`z6Nz5X2va#dyOrI!8tC_SU%&45=7;2g49Bbn&=P&7B>XHL_Fm^-3R44}o!;FV*d{9p z#`N9eQsD+N!oo{QQ*MEbP|4fbY>J^2heH}Co2DIsLq2pMnzlqvFavrkbN1 z0vKOSj7N%>g%Q_4fF6cqC_`$jA_8nFnOan;i}|5Z&JrbR!;_(S@)eU%^={c$64H{I z(xenlzVal8_>+Jnz+7G+{8XiIltqZS=CfR+6F*#uYL`pZ(&~3xTbR6E7xht3LTs%p z^{Ds}R!hpK#2|pwBb4ST6-VLwsAfEynPfqgQY}3rjTlTy?o{4E&Xy9TcuF!dv5}2h zfXAKupgVa!^-2Ey`sH*hOtX@qQ^`S2vPp(lj9Pd`LgDE&LJUCB`{)FX*;24E@+*+` zIpiuB)j9?qIlnkxN|t2Y9#bOvg8jlK;^BX^@QZR$W4uABKoqkA<#x-5O~&SyoXFQUOlbPko@oyUDY8bEEtUseJ5gj9{} zmedU>`BmcHTh&U_omsWWCI=5Vxrz85q+d-tG44S~dZ})eo@1+61h%>EzM*0A{Hn(V zDHatr4JXhpsw9+#P?EfXX$Ui(P@ep|`l6;20y*V8me_0=X$5J$x@C>eAYAKUh7luz za;?1CDwk%{^U70nX`_|#&XL#9vrrglD>+U=iy&LD#avM)FMx?bdi?{&0gumavB5ejx1An z;;&Wtvzob>(MheDzR1VS9o%i4j3*mHMMY^OBUUEjU3p|>DtA^oP%UP`nlt&!+qq5x zJjI}TS!yLQbUE|v5z&CfG)>zh;>twB##1~ucgEqTuxBj3xZtGARKpK(fnoh@u6Bq= z`IUd+5RqI`M=zunnisH6QE7Z*sHoq7+;cfVGn8136W!>xKx0Y@ShDqypr*Sn^OAE7 zSp1ghx-rsT?uAivk}Cnl@w$YX0*>P)!+{d{Vi=Vv=#pW|wkP{u&}#(GCPSQRB8nXGNn27z9oo|tzeBKLLRLD{&4GQ! zD@i6ZC>eNStoR7kQ<6LyZ=W>NTz4D?m;HeUUCthOpMNjk0hiAB4a@1X8zVz5Yv!cXWb_oJ{0sbE>b z;S`^R)G+%AZlm`fx0dO6meDA0IFv+RRpk&t3}VsLWj7CPFJrNkQ&$6_x!Qt{T-@pO zs$HC|y2~={_}+)oRLb|Ets3cxEX{{f;7F`gJh?*T`T=}AGl~!k7)_j(H!K^@eJ-G~ zTfPdwqb3n5=CoPlFMhzW10)KF$imm2BJ}rLl;MoJ%k! z-K9bOpjdwllsP%llrScvP%&7?6rHhFnNA;-w2l|fz~+SfIkWu9Z4x*arv%RlZ7i+*d*ywn z@=LAqtn%@P)h8IVIMOcM|N8Z{*|aT9!2wDSKC@6vYj9~p%qh4JScky8kvWttG7H?; z6?U)?>fj6?77NH?od8Qj&Us{<$!OT>!_S#FBts^o+(G_wXor{b>S(AG=d)rZ;gQuI zrBkwirHZzSv1*3!!(`ydw!$zSB)mRK@nR6q7%d`~7|zzb^~DSd6r;#_s~~aFGOW;p_JEN_vX< zf2_^b2L^fEY$f`C!QZ<7N2A&LpVorsYJC3U{vQR`pIdk?F#mgPV?7@Kv)O7s#{YbX zC*A)?*!B|Q2Ew}p=OB(1N+<^{It1!OR|q@I?G9`vx}Z8?skV!!<{G9-M_Mh?#BXC( zvFYeG@m?SjtXmt+Rt@=s$S|0SA6 zOnqpHoJWC6j&%G+Gx=Ebf*Cn*jKs=tcccC#x%h=v-}WvpXQA*pn$q7@&zUlpBUXOT zCdceQ$@O$_dF2%LTUwB-)cv?W2`uLBPx>Y><*z?iP0jPU-ALOYWR{_b=e*U(^Ng=C zr2nMp*ZEJDeqBgqfWy9WZs`DGCh&y6;VDqa=bGsVQU46T^y zX=Va!DtJ{oFCZC^Fy;<6&QZ?><*blw$jhqSNJ4l{P-{5BIS494e>B7B1h6UXNSwi2mRkd-wHu#ygYIlo zZcr@(exg1L22MtOBi70i!NOQeZ^$M)pFxEi2XzdjFFM3CXNnPz2r$X&OhGitY>3c+ zh5V#7lyQO86=Dq8YG>{U#8tL+=1!yTpKyEI1oLC;TEsL&a^1AhbtM0mL!k8Nw@oKG!6`fZwSnJPC3mX4TRb(k_rcI&by z>g|@}E$YZjH|m{Kt_!^X*IUh2D|Y{1e~kb5AkRYX{~RmC z87N=>xxFR^1tEL`9GLDlPdFxB-NPAS2bs=~Ik9W8A;FU)Wa?`2<=P#hdDppfV_iC< z37p1^8imZK&c1?h?UqBT(Gz3n*du6*8)t;zx;Nz-RKm6Sb$ph0U>sA7r*DZyYHgqjnH1`0xnbmce`+A~*PLf6#1j$B#s zW)6ZA8#q`ovu`iviCBmNm;ODUEUQ&IANi=LC|QuhRynb0mX^K4?arP#Ny|xpy8`L? zhDmiv0aCLOuSq+f;%*zwLj3D@-%ISj{|jlS8E*&GX*Ob6nKlR4Dhx|JG_-}AV9Q#M zHzXh^;=iI~&vc;{K@t-0)a-k_mm$lQZE7}_SU_u#R5PrAJe7VHjdXd0VQ_D!Bnj z?lwt_OL&O<%Sxt`zO29nWG!oi;dfuw@?zIDZS(aLfD3+YW!-Yly0nLk%cX{{$m*8K zHIp~zlZ#(SwRL{P5+|)vruX2b9mY4EIzwdXJKwaf#pRHJ4Z(2O9W?7F(b$|cy)QUNM%8=CGHmYn)Gqc<`2sUVO+ zRJ2xZZ^z>y8B7p$oAqFg{^jL(3>iM1>7ZI&)kg8bEHcrCg(Kn~K#Z#7ABOaZ25%hd zyAy{vhz`1Zfkw%x-{*7Z^>0>ug(G6$Q!HZv3e_H+N$=b5!yeAv#(niXb zz7$XJvmr=M9tKL=8enNF^rw`Uimshx!{H$rd2$<;ggL39iwzst`x1b$6xS@Rtzm-| zAg;+s5MvA@q8PX9Ra3J^BLL(meT4Z{(7ITBK=d$vfpwxEtXi#&|08^U;^i;VgjmPd zV+iY^9+@w%_Lx_lp!qA8I(;C+$)s|U-hTQ!*=SqQ&VEntT#^oz zDS1gb!zMzu2czm+T<}N_aTZsE768Q;NPlk}E*{p~;UlA(egOU{yPlo$Pp1UD)*9u5 zL~+-1^`=O%X>{}yfHO)FGWRt@jMaS5@B7xs_pf-;AUB}3>Q z>~wljOd4siK-(iPmL56?3vjQm7@G6iav@X51?1XkA?Ex@jwCU#QruzfchV|9l{vao z-xFCX5%~lXR4UV9WW{ittL-ELn6k^EBiYGR-_7*1&N&;fNs&jDbrKO>NB**!-F#0$ z{jUiINTtNHDi@yJ;N|55}&oH) z_IaFT?kvt^vy$v5vTR?pl2zEv=^EBVn3>hC!VQs32pkVxl~bkMyemh25e?26(nV8* zqySIRh2&BWlW92x1A^>4m||omG6J3x40g|59N8 z6ROO^Ke5gn5vY*9+~7H-!y}KzJbbQ5M9g7WWVa3x7tnx9@=C-5Ud5LqUd9~oG0x10 zGy{O(%D`Zk8BF}+dSg1In8ug^LY3x`u|}jo(nO(~fUYR!prqSbMo2Rha1bWBn;E~Q zs7>ih`e&&hIo-oBs>iqkxaLL zxO3Q+Oob$eeNfYloT&q@)_!mmS|w`G0)pUDRY<}nJ~`b+P6*+?sHqcSr5&4)`NP8_ zLUmkk;7;rjIaCiEX9xp6w+B-{pb@8HzS`fOsFP)FH^#D{GGdyu9AIgOVU$ymE;%~O z^FS5=XH-Cwb+*sI@JzC5!aE0eurs!1D0)KgGP0wQPbq63~KlF!C|j^x_dzGjJrFhd%FkSW3wZWWD^KmOxa<`hx(I9wH;$X z9Vvh)OX>g-VZlF};Ld0W+3`@uD3*^7@Hn#X1J^a$oIOH+Y?>f1Bx_P45V*9jU4Isj ztf9s`1}jGGQ(9!qSegK4)Fw`M05A$D?Xr~^8x!M65FUHv{*F;Pof}+Bi7N!#$}J`2 z9%T`iKLpF=qdDzaCKB~ku1EFCWg0R_Ete>9{Cu9eZ?&qiwY z0on`jr0`cvE=yWJzH%oMjQTg8OmDR=;Gs-8tqwMyt~Zl;c3uQaysv%S+S*#SmaRYj zu;e!%rpw0A0(1)RSzZD9Yqe7USltR|XCXL-!@F`lcXA`vAhN#jcUDMecvw(qtg0Fr zMDH60>jP?P0GPGsf{6kx7~$7y%DvDYQf=Ra?pB+-*+U!Y*TRA0Ui1A8$EA?Ml-Kw}560#lGaE@j46iHE7q7@zt5_-Z5r{m$uIRRwfv8e@(ppzzw#fT=< z6;v~U5-??qgpfu`U>!S-kq(71)7K;-K@R_Gyp+qt0+;k}wEGm2fdwEmG(E2(EK&*_ zK)i`7=QjS?b3U0+4}!jO8Xav%mU@hb168(2t-P_iik3plFT1aI58BJ|#6yDYAH+h7 zDs64VjOz%`M#_sJwc9)ouhL{J#y`w%F9ASGNs)L>9}jPV%x`@a?p(`++ceA#yTi>i zWE~oJr?NR?^>YuGFGgh7EP6JEP=P0+o%$Dpc$vUE$}`DZP8ft3_7+bZYV1Ff4{v>~VzxrW$_50P;pQN0IvH`vT9v#N2)I}S`MoTDCPjwhuw*4UKZoYJpvIj(g}XoGc3mm`?+RoS#sBN$;H=YDq^uf2F-F zz7H*bFhKEe)V|A*3(`-4H`fGmHMyRPN*@-clB#DgpmyK@`3J24^?dUKkO!eHJ!%VN z`*=QjYL*WUZkNEoVo=DC1#xvOdkkVOIP~dIs0jLdtaC$U-lU#490`S06qk4)2d57l z?qZ9wJLXv+`XS|9D8d~eprw$fRhwdnU@-GhF&NA6Yp_x2(A%zwP?Nm!zz!(*kPcim3U(2l8L zYXxm4$X;OYP|X9m_{bhu)w<0r4COoC?G$s!ojJEYih`-)wFK%ojP_6}v)JA=0W4SD z0TRIaGtF2T<1o;@#XrqFF1g`8C{)hvoS(B?gtRdaQ`rFuj@X6?sYv>JDcNQq4lPbk zt|BKE3_$^y>4q$P!Z)Yp)sJj+4rUn{KZuucJunSa;Z9w9L`k(AJVe}WWRDi*Zo~5f zz`2TMH0DVBgkn_C=!c05l^zRsMp{ems>DchNkR#uMnvdtJ-APEH#LXnC!`00(QPJm zaIAYaKz)Re_8Ne|U1$h?19fvg4MsDdjd*!Q-SKP`*Pr9K@sbW zIVTed+#&gkX``us9d9ZzfVnvxgaAF4E$!e0bTe0JrZ2(rJ8^x5(&59~u2e?t3}R;D zS3_V-GaT8Z*{o@@Gs=KO2~7zTFtX8)?@3e~y9uR349yV~DEovyxGWY=d}%V^7WI?~ zxDlHgW1Pg?N>LjnBVxF!Btr+W?rK$d>+|=3@jUxeGi8Sy8 z`~`6;;NPL`!ap~TbA<&U5(fE$w+GQPMrIsMjM_IvsszoI;6RobVZmVMbKv7d2sm4F|EN7C z_eg^QKGMHWHXIz*xxA9VL1St|TrA!KSgKezm`DT8eo8uda}84ZHmBDi8?@A}!9wR} zWHKl&h17p`5H~n6Z3Jld0@MvFJXLHBdFzc@EYei^Ryex=en5f0J;v^n21n658r&Vm z)_PWWZ2Q=I(%4|bEo5YkX5o>&A$=!}3wAzWN}(0yl}tr}`$I1F49t$50ljlzqM3;w zy5Lof0m+;T8~>Bn`1bJeW2^!Ta&H5_k@TmY166mm%;%)I&salm5jT*qoG{0RqR*MV;Awr=M^-*~C z`%o(<2mrkKaTH?>#Hz2;K|dhhtt)X*4;{2|EcF_;8xCE(D3h>VdsADN1hWz&#@- z%k7-qes59o1wk~}{aa|k2>SprBY3(m90-HH)ItRspg}B0hJ0+87FbSU$M+3Q1dYEc zK_ab}7^yFj z8w3zl$y~Oye^~CHlz;A)`)|sxdL^6#a1zra9}%ZgFTDD+vT6ApFltAf#~ZnRLS}K! zQ1cinf+lhQuw-T-xko1~FPGH9t)(!BiW!?~@lK9Wc!9}0DX7U#iQ>n=y3zED$dlf3 z%*_c!riOh&YMPT79-m-#qMDR^kbpwVd|v9)QVr?V&s}`gluNzASL@O%zyb2ACA~WC z;H!)!<#-ol5iSLI@?w<)j{|!w-A%>#YM89)ct)UqE2U(t z<7+~R)YlD~1D>)g()%X9|JfOO`j>TW3D7ctx(C%Igg}9*|W>9}H02Jy#r% zg-}OOq-$;oFN;f!0bOYyeRyhVt%P;#lY4<7FZOH$?bDwF)XCScIvQ-5q7=d22Mb zuE$yrMN*#@buyfp>(E`#+dM8g>a{hWYrYFN(i^j7ByyjkOU%kQPp^@r|>zrUg zoYuCaUXm-NPT{220lV9@?5C_ZYM#i4iZ-+X^Td~r*;<}h4i&TsK?$_FqR}ZpXT^+O zr!h-H3<~k&%DENxS~3K-O4KNloR|&WIE+PHlZ~ZGGiF;w46IZF1A&quY>zyV9 zXBM48ZzKQ4;dX(m@z53gVX0iXLZ?(Nag7f7ld5#c9~8Rf5>@IzA$~b7QTb=d?8}mP zuECRw0-tzSo`2&p`69><(SiaUp{T!Q71hcVB8hMddcY%Eg4$5TeLmO#elMLvlI7Nqf?DH(vl=Si_@DXrA-y(a#0^1G@zUq~xNsS?Hm`C8AuPDp^V{RTGZ69eE zm7lFJHD=x_>G+POSsD1wiSP}0@*t3KtU3^IA&f$+V2x4(OE((h*WOck38 z?HO$MGW!L!AcJA6okiGpT?tWp8mO+U^c{=Vr-$a2Rp_*oik0-Li+||&V$3u zuBonPu{aJe&j;{@_Dc;(bL?tVLoBXmu!GI$RaP>@aXWvfX4f%ryro&PEMcrhO}28~ ze5sXO>viy zsKtLZF`1vURwZ9}&@5#Vb8ktry%cg)?&lwqoJw&B-61eaMrT|0Qn?bVW}W6~nS2Y; zj02!KWDz1$RT8`WJQl3^2( zd&2pOF5_C2^Bbwq#?~?eklGfNTJuB?Ji~=G!w~&Lb|<6C!J#pbV#m9cP3K*YI(f4g zSb;P4ucIh1eueSSlikd zrQ4vKw_XaI*=HoC9Iv6sIHj(o)7MFrHam(iIbi^Fe;4TfvvJv(HNp!T2*@;ym>xqG zg{qw!CHvex(~&g21x`k)=ug-Q2}TwI`5{PAN(59k`@}*0d`WGAz--c=MjmgrRYJ8h z46u|iw9E*tj0DFhv`}(NV@%2|s;%OW%8R6>*Ly&bNLCM$%>2hKMVMmhMhb1@ zQ22VaPq;Hhu@XXXyBMz_|5&kgbrStR3G`CAc?PUrrBj`^9K?^qv;V?{a;u?H( z2(adk@$81X%wNJtA$*|OdZY1zrM=e7ck7VrKlAmtPfOpeOQvYe*W)`ZJWq#)Acu2o5fhe+c!NnoJpO2sIn6=U zC{Z)bFn$N5yK^gNeF>zDME~$ZGCXT?WPrqFlZ+AkF!wL%JXc}~FBB?zl+l^V^oKzopc=*+ogAxqdAfRSNN zN}Il#m3rcOYCy=+EI{8Og^E*lragM9;Sgj3QsHn^=!h7cgty z$B;WWXD&l-BAwN@0Q#tkh@Dy8w4b@V3a|HmI1o|>|=&9_FMtJ2>M31ov;@ZiRg!`Jn z3$pX{%?R;Rm9diJuU1NPk0^SQiJfV>&`UFnGy>-;o`b(NX@iDg;?e1D;-@WX4(xA1 z#&3@xjPfl?YEeOLY%zYfGF}E0bg;gY0Gi=)WrP$unoH2)4z}+zw(LhEGob!5;13g) zV%*rT>l~AYe79qa2UJWirPD+3aP z1-yH4iE1*N8b>vrLc|xK5=bNl-25VH2UTne^`P+$08944tT?=KC-5QqJj!aDS zM&#Hj>`B}C@dFwg(9u4Xcv#ugLmwsDDO~t8Xh_XEa|Sl(52dM6V>y)5!QlhJG^dW< z%$alHdR`QckB`=LWYo;EOViAlaZ@Tw@~*U6PCbS^EFf)NV8IjEsqH7q24&@u?>=r?n#vYhhffEn2twIh| z8-ss4_5?P~cZmlwHnp|YKy^iIUgX0wbIuFJ$VdO0e12vqxyTgWP{JeP>qc?Vs3pdk zDpxC8D1c9D4Xgox(os7A7W5p$f?*zCgpU;8E7m|#yM!egpx-{O8anPr<{q51u& zQVbm_BL$$!;G?3JC4%zZmMH^@nQ79(*T@g&l`@WSjEilDgv-WM0pq(%#*Fihjpp;8 zm*75ORCU<*51bobAJZeo!U^0!L?0j+^u|k$FwS?vPa1d*EojDcZ=CM%E|I-dow zRjUg9R<6<@R?%;JF-n`lBjdd8z$e2qFRv_NqLE)Nc){S%@87yr-#R&(=oh?@%I`}Z{*KSt(c)12ZwQ6 z)(xc4xc{hmvtBkXZPt6k`bi8#jJ0`DZ=~AZNEGRZEsZ)R8{+5`&@gFu4Av|BD2YqL z_GwIBjmb0Z+tjOoG@-Jkw{5!Z7rHXWQHoMa{Ysd5D&-rmax0OWj2}-%+eb>JLK=4g zt!WrsOBUn3(ZCp3PAVA|WIuL;i)3AVcDZ5|2?SolbZvkqZ!}u}5i>JsbiZVuwG^1eC~dt;T$@ zaiasbGc<5sJgljrDK)8Kcy2`Uvs`Jce$-Ppl7W+cFr*^S9D|WFF+0-8y+Xrc9^15P z9r!o$ISm&}y*a>XQ6;3l&s-co3KJy0G(mfbx(+y78NZnlZ#j7;ME6Y6ddqr zuo!_+V@5?v+JKR%!D~>|zg#D}=oxzP(Dr9bf{^mfAUq@&WRI>VD%ncv%9SGB6b8ix zosEA3w|2dqi64gUnIr=MyU&0}B*H{%ka*6aG6-}&YLFd*R-$1{@G3@N$hgiDd%})* zVLXMN_~+-4)`MxRW$dq&E3wdIykS=DMa)f78W#mYe6E0B5Dp2&6c`P>Opf5eCG1^0 zsj(L2TQ}e;!RKp=+HDvsGfLw~$A(_1e@qXMB$Gsnsqq%_bBRfg9Bg`!?1jqsL_&%i zhY83;E+}x`W>c09iu~io<^&*CtsnU68|I<&5D9uW%8vy!SRdiGxD;^(NlBSyt|;>u z{CumdG8w65B--Nkf-osOO0g0Ly_x3U083a|WGIU6~*U^pOo zDdl*I?*FkfN-&+6vp7jkK)}#*n9Vqge;?(xnqhdg!1gY95Cu-C<3eHH1Kes2WNuml zy?z{)qS}vEt&&hNY4dOofHXCtGBz_2iG!UYGq06XF$}VKzN084bN7@0uOF;B?3{#g zm@r7F^w~Z^3>V@k_KObzWgNHkE%k%Fg(z}zV=MM(?2}CZ+=M;C4S{KE&U~bf0qS$E z-62G5m|kKIH0X<^J(c6-k;brgIvPuEhe`Hr05-Ad@B$M;c!fA<%j!mrg@aAlM@b#V zJJ1_Ut7@#Z6fa7rHS=6@vshuTrICkzFy*hy*70Gle|m7T-|g*gYaF!5I!3ug+|oel zm=iO+bE!g(2X4Z3#gwpwOqXTfKG3-0l^wZ;ffHajc#-mhrVYYCir0YqRB$F`*|2mS z6b%8D0LExDF767C5#tb zoH$-_%}Sy88bO16iAcdOoyGj|nfJlm!j)?pE~?U~)~mI(k$bj=Y2epbI+L~I$;h1& znzJ@nA2sr9Znp4W^63BK-}OelwYgc_Xs&ON_YLxM^M6_ko~!Yh0dGgT419l@Rxazi zKDj^1Q*ixvdCH~g;L0VWFeQ9FnQL* z9yk^*{0Fx1ANazPa{+AQl#k>cisiqzcqx`d9S!Sf7IF;l5>9-N3Vz^@8e(B!aAy-K zg;$h{SZ{--ga?x+uu3DX=$U5dF^su|LFg7>9{2%kxWXC+ixhGXs#=HaH&%l5PE(hZ zg-k%DCm20Fqz%U&17C@EHrJcXk^()E9YR=M0?{W} zN*#MjekfX8)%FbSsZD0b_ivAk2|~NV*jDI}P6_J_jX*s7khdWl+F_Q_DxylGknjc^wj@lcPA@}QjvK?(1IjpKv|O)Bvp+w z((-AkHS){bUejz6Xgm#K%Ar4o(I)j5UfT7Qu{$99m1QlcS|!e$5L!eo%cU&P^vUgs z5dXl5@~Ml^V)5nvj>>bcoZC}WKZKY;P3XZHxk&SfV@2LWy^qq)6ZqP0(_A$xa>}*T zS(sX;ab%euFWN`3D|K97-bGtc04Q9V4`{eCn(uFffm|^WfjCL&`G=vPhs*$1Naw#t zgIi7iK&gsO+A!}ZFNs_LAqvnUrorRgaP@hT5?p0Q1V}NLUMiW4W~#U&udoAPi!OV3 z>7I)i1@cDT~TFMgU;jclZV)KbyZZNa4wKIc9~IipHViQlYk z6Y}5Bk3tK03f})>TwtDiV8;EgzS-K)?|ScmoIZv6oloM84YxjE zKq(tAvV#l9y55Gks*uTEYeiV5Bieq0}|u8ivpw=?a+vraW=U&~7~5X_>n=!1)Jclg@|y z^`LjcMc@2fajna9(fhjYNNJa&`2FrL!4Jh5q37l!uOR(@3wx@F><^v{`roWK*LC{e z*la%1|A%9{WJqA>IPOZggV!vTzEQTAJ(+rmMJhbXu=m%oK-i~OCMfU!l4TOaT zhXK6ozTQ1()6ziq$(2m{g*!d8khOI@X2j&+xgg6Y(BEzb_Icm5D3GpCK{4PfpzW6g zm^><)Dxl+}JV{1}>-??B<+wdRpzxMD$Hyys@|FDwGG%a&m9MYnuvO=df4(9E+9g=)LnwVRHyx<#wRL&nMJd%4OTN|tNo_0`qywzigm1T8CW zjcjofB)POpxk4n{U~c$Vy zQV~_D1faM;XQ8t4P9Iw=^@}&Q&^3=?Ju%b`Mzxp8?OMpU4fQ z={zJ{gC9(R3!^baq-;gh7?B1jAV@o#P#n8)1oP`-jxHt=i)YixL^Jv+71027gdq|XPRCl2ngupTuAtka5;liy zT+;n)dj6F5?9g#$nTLy-P&x^jg>Q!K-QuM*0kauIDRu|WWJG9fPW&zbj?=-&A%GWS zcA4+|=jmSO;{ejn|5jsTJtqHev>Ka_=l=sddCz}LABJi|yj@HK+Y3i5@l_=B!5cRJ z=0FUA(Wr9ef%tr?G!RmW{_^1{q-k7i(5o)%rS6G|ctu8dWp#(n=+bu3-;!J&$*;e| z-(i*B`U!(`;Z!|my84yyAMz&TG?rbW8C8BVwkN_UM?_9uHixW6 zW35(St8XYHfINi$O+*k&os(m08-j}3Bf?tQpaL4=97|g-N}U_;B$R$>UtA9Dp<4a|OdLHsc@8gUj^~G`9XD`7 zwJKTu|d%fSAf z+$GcY*7nRL$AYPHcNX{)19<6sg3v<(-f)^+)zmGk{IeZ|_Km6PkqxOx4am(t0F;LI zXh1M$WU8M$(M$5*3qP}twX*MgBE)-Du1sH@Zo3+sFU@-%R^^zGK|%@@x&q#OrkN4ude#OePJgLh<#Nn%mR1nnodImwYMfpDAkZ2CL>A$y);#` z8XA?poY~}mN;P*_N#=SO>8v!gDnoKj0_u{)@HlQTQ%*1NY7j9N4B2S#{s zibO6WWc&klTA=p^FY?6Rb{r7=WY;mCS!Ph)+TPd>k{D-lWce!HIjCAWSdat%+7I0` zH&D-p1A9zfCZAqEll#i0@2GX0iSZbx8)Qnh)NrPjzp;LCY%}5Fq@*(&TtNw~;yHol zFrDNbPf!2hUwR7mANT>BcL0tQ1ltX+W>XCwQN@G_26pKAx3&gH!4!4UPFJ!$GzKS; znc|i9;7a0MI3tylH`X&E8T4wO?h^ENMrsRJ>36`u9$dO=4RXVq*|#azz;NabTopZg z{upDPB^`~VRaco>Zx)8eQ-||6emL+I&XfQ>lS$dOZyir%rAW0*@W1pgeD%B{wSIOo zI@_}e8}++VtDA;Ya4);^6@)sr$G)Ms*e(-S9hK`{*b}h-NjQi2ccl{L3e)*lvbSsk zxKL-ijsw@)v%SQla@!yI<1+=>k8guS5bW4vI~=HISl@Ohj)7mJ%mIE>i-!c&(pl;^ z{+S&pXG-6{x;32(*av)53llIVuL^#i+7pHRrOIg-!qAhPEYL{Z3#4xL8FWMlwC5_q zgp>X@TvVj9SG~57@0FVT{fF4+dR@8o)Rpfeo3OJsnQGEGC6$mXzI8n3_Zhi?r)^Y1 zcx(jOY^zE@n2zsbh&AOL6!AOugG{}bu#bbN8~&^-3K|)nZoD2v+?Llx@+0|V3cK-`{0rm`sWa%Zg7dww&EUy z7!(i$$8Gr)levz=p;sOAW>5<3c$HZQx|YBWZb^Y>PoHiyYX7hC^m+?=xu|Zm{$Jx+ z)ea}0qz2`-w`nZvXe`#y8O{bgF3F`cnxNpFk;>Gbzg zE|Win)=$-`mQFku{G14qZ2RPz;d(Y$?ja)d-66dYUzRO0>NB6bVmDK4m|s@$Q%0lDjg3Z)S;<%5&KT(1&>oj9 zirn}w`7b&Aoiaf(e{gl_kI3h@E;)s5WpMHB@X{Gw1UBRWKk|PMeeaeu$7ES#M8A1Ox(D@O35!EoYm(c)@ zscfmTw~S#Q6EgUlbL|u8UHOyW0E+&(`>TM+EECmE&dN5{I%SgbNzU zq`fAJAGj(aDrQtp*k@(ypR*g=ox=D~HxkvLAJjW0u)wgfs@9PM8g|&O1U3B8g%4Xt znO2-(#yb%ORoUVKN*R6zp(PW>M8k?pXF6vyYv}}Jz=ar5A%j?h z-HFQ98{6{<{Qf}(5KL_aBVxBE`F1Wr%Y0{l>A1gm-hsE$F*@Xwh|c;LO+cmlaWqsy7pk?F#Qd}>+T zKLrEJ>Lv5D?6M?n-q`KwFeCsbW3j>1iX1!i?jzOE??NcrBzr}beoj@qj>$hcYbk9= zI|d%Eq>IkX#HpI}c}!WZm5i&B@WbmoII00AB<+X0K< zmT9eEYN|%9Rzp*_RlVJ~ZvOQQcqm}nEzY!K9Z9vO951vZq*HkH)q99vcjcQ>v<3FDn%obyG(e8MVF<*S$=rbl z3qo)Lf^n$L=&GB8+12br$sYOVaMr`MTrF?BsWXK+ol&wDUD9leab#$;W%)R(z?ElY zvk@jzMN2z$BQ%Wb<`U0Pv|zBP1%pyqm_H<1ISAd7TG60SlBKfR5Dk)8p%tJ4N@`EA zs@5}DLog`-VfSqdL}^NSgNQ)LRyCN#C9YJW2iK)B`S1VD+^JS6F7o5z8#;xPW9x60 zaULc`D3k11E6|8mo_$M{{%Cdy+RCKaB4mx!plWBDA5aL99phZg282%@O|)JU)JDf( zA2c0YIyZu6V->nXhzqS<7Xz2vq_vg}QY<0*%9;+>!X#}BE+&b$^PIfXc9kNK{KH9~{MFUn5(zgo2x%dqXZjg!}mCjr|*;%5eD> zRtyoB$y*SmG~o`WI)l{NH#BWWNMoyuuqf1A5xr1pTF5i-%EP!u&QQ*+NZQ)-r?b#n zqt2UAh7)02QL@wr?lGFO77ju3}ybC2a*$b?tHN z&QX<}NU4K0oko$$tCUzTe*Q%ojO1EbXPJ=vYh%th*CVDWxtfW6(9P>g2GEjW&dj^9 zlRQroOA)CAlk3G*f+`=C(3^>);=Ei6B(wQF-6*VNXyv7&NwXv-F|lg{$0(U(Bvvz< zZA6;g6nxc3sWRJYC8H?{=lk_1_dUQcxb*vg5WkxMX)wxS8MSvyV=Hvf!LVAjA{o9* zL#C67Rb{B7$%~mrq$wP9Ikf`6Bzd)NSXzo#G6|)bb%1e@N%HiVa!%gdidHU(a}zo` z1Ngc4nyWOlO_NnGZhZ-skO?eIl5G3cUe{1V>Bl6ie%*hWRV^-+rvmN-03}rD3qiK1nufh!NH#{Q$_|O~6Z$!c!lF6c`vp{lZ?@wDA*V@7z?> zcnT&Mi!w8sw4D`K4D1G`MU^{>3;n6%hIx@>$-i8MdQ>D%$K+E1?h)V}VFH{HwzPNbbm}?NC)3hFUR|g=Q#Oiq(j#LzppUGIE>jF@*`Cfh|Z;M=%qyrw z1D#1pA*<%0n(c~uc6cqUnONtRFg zNRt#Ln)6Z|=MBmW)G1i;Cspqd**eGOl&11vDvH#;be)AEsv}q~VQS@MQ!UFK#$q5a zi>=&dTDINVXx8daS4Lz}z2T~4o~4Gh5+1p{u`V0VzU-pQCtIWQRn4ZF^~4*CEOXrT zLV}i~+cKG-^}h|c63s$LmE{LC4WMEQRi+{D)O$%)X^&jBFdgf)<5^dPcO~?V+_`?{ zA>C-UD$GYn(28&x4vUj2H*(=jxw|q8pb_=aP*|~;g;rhtteae|E1zQ~R2%8HwPxxC zYvY-6mC9`vw3&X>XsJUsnEk9jQqfb(M;tEHyY`@+`0t$ z7!S5tPiE;hq>6FIazm;a=hGSu8CW`JwIMIJkzZSv7R|tAZAc)A^HU9JXeQ2Svzfv0 zY|1F3Gf|CNG8eNcOWxbhp+OIdVc#jPU z<;`5iy0k)Z-k`pjb)v7=()ff8wZ6$3q46w3{@7~fkTf=Hs!kwgZD2js#M@a`2F*q> zN%Mv}g1C6Np^YFe^0uBSCF1h_W>YB;*CTASv{}Z4_)WD$ObJrg02x=VG}o09848h> z+Mh`!Rj;eFiYcF(n@Rz*uBf@86f)?{n(6|?m2q`-$uqR=b#*cFsM6~y=uBGHdP~_c z1_f??Ln#qcl{cR$-(^KOd^T{?a^N!z{5pkrv_52+9%9yUhzKW}Xa ziL)`X52oGLdGL4s%o;j^};p)J$wR6JfSq)xIhS{a!9mLYgAL>7X7F`01`T2`ce z(>tMAx2=)yU(F`gx$S}-`&6GM4+;nQUY&CrQWzh-ITADEjjfQ5$r#Vwi&+4X*B~_< z&hnJiI^{J^F;_EYW_PeeHM)&>E^m(%f`Hho`k*_I=hws3vB=+_TTfu*yXPmK$nuQo zpkZwwP8y_zW($TfUTu#z7{lbMc)`QWfprAGEhu*>c8M}X3g*REa0K9y5buUuvmoejIS#Zf8W+$U8Ib@7 zAxP7%I}QL0AwoLBxa2%2pM0@|li=KyGit+*F@_|>xV!R{=^T`BbOSIcvQ|Ebc)ZpV zC6A68gAT$bmvK=cRHHuL&>uTDm~fuV95NW;hp|0Tvwze4*P$~yZ?_?i)O#FNJpFaz zEaFv@V&?7Jw`UZ~SWqpvSY6&uoS-tYf4{{WDnj4&2BR6oHY8NVKR+h}gnOvm6kiH( zAAkUf1PJ7EmdIhJCQoFKNZTiRsk|U7O3JM$+3#Yhos?GiSViJE zQIzJt!*Goh-Az^5T#gCGT7!{`M*!rgD*danx4T{0JKXN{cMlIr)V~c7$jA6`V4_tu zwkBnEmxl6Ry}{4%tY7l;1jvt+4llK-=gIAWg_wKhLb_3ALFNY`2J%F4%K0r1o@dKOrH#f{w^kkbiHY83z(X#yg+s@eLpmS6Cpg9?HPoX>9R`GWa%ggVm7S5{S9{0 z#19jxQ8{upEI}B4!4xF*08a|l*Ni>p!|wQ5Y>*(or}K=T&bddMr}4D$=bV-HO8Tjv zZW@m^PtJnU&p`VKqcig`-v?oeRi>8TSY)0aZ!K^lwK3&lj2Ba7?Z`V{_$-SoX%ftq zfw{uNmfsmL0Q7c*sfnv#zB3U1c14Z!dw=m?^Tq#V2`J|Y{{KIj@&9X^>#_L%^;WC( z82|SHp4{<&qnu76^%CGePj^a>FS?m7qb#PR$t(N9`Q~x~unl2Kegw&KRH!I-T1*pU z@kFkeCAN-vV)cfOr#9F&O>eC>p*E9Co7`|5@ZTW_dp2?mjUM+qFL(EL`|qGxTB?+g z_Dwwb_CLFbh4T@BIr4whH`g1F`~LwRw*T2K-(~BJfxKgZy*U=JBvDgWN>|JjAioUw`C7J|k|x>p9L4k{nR(*9FPn)PeL3V9xO^r4m^} zIq(HY?ie(e*dNyE>CSHN^c0gjb1vZsl6nQO?Z{3x73|U&-C+KZQtDOUI4@`6?Q4GP zOeuQ+Q%wGR#Ko}bWkN^rWoZS0V-BXbDMe`BtJDGm%&ReQcBh$>OHX8+R7uHxlu(HC zbQ#4YPAY4NBG+a@K_T-@DYN4bkV($52DH7Im)4~y%keW$*?maco1=o5_%2r<xOCq05lq98>8(K#5pI#7cQ!&anYy8Q^?_paCHbynzL ztS8qt{9Aqk7%?qBA)_ik@n=(V&v#78yvaPs{ige$nLP?uc)>Dnrt-oKhXm-V(r~x{}I%z^IVY@vD98zng^tQ9t z>FvwvD_*|xqTkzn{kq#jxn-^1s5R@2e!U3@|4S*Q{CjBhq+9w36?Z!QZUsyVzE8%iF^tUxUVK4&-Qj0pz$#pM{ab;X619HJSxcqnQIWnwhAP zQlZz~BinnY^pVu)9_$$KC~3$^c+@oDQLUbgM_8V8ElRIvl$NlrnFs5d=EZGP8m$}{ zP-_`6V9rjPc{*L!JJnO_reNLFANsL_W4$2Ot>?hH^`cm}o)_z23P~gGAMPBg1IE(% zQH~cs>1N^^D=MMedLDFJ{}Sky(CNLPo8bhsey{>(Le@xM^j1O4YvsVaR#D6&6?)yH zZU?ZVUNe2>b)g;Fr%rd0CXz%2TLr0LOF^_&9z<&;;?T3=C)-?WJ_K8I7o1eVme=zW zkYJ-A5^Ur^f{mg`u#sn#lSZ1f${PiFrVVA4H}b6V#uwn3?$T#6&y<^KvR1@Oxn4LW zH&OV6Ov0+ZteXXKYcmIKZ5G9?%|Z-S(vXvoY_lLK+0-z-S=%udk}z>I4<>GYiGwYn z(|hNm7$P3{6z{+#hy(Qz2Xo=z7e%j}gl?hO6(@Aej+FNPT$Uiok}>Dg#2HMTVPwO; z=U=!3@w13*jM>_XrPTycbk+sYH3znuaL+O!-)~fto-l%c9G>(JI(zCESQQ1K6nrwv z?-qqin&?k=x;&d*pgR->$V3Sq;eq@BLJj-WF>!3WBOa-^T4XI>&Mq!)=>k4+y$Rv4 zwyYn2{PE>pXX$14_3lCY?RIC6K)BQ6zu|VYg!^>=s138J*J{mvZDajev;ND{$?i`3 z?CJW`!Pzr=^X#m?;jC}0+qI2BqtO`H_4B9oT7w{?J*Ku&OQf0p$#MIj+ub>?bhd%) zERlN?Z@AT`lJj4dj!*XYJH2=9z8zj!^`&jH{;v;vyWQh93Tn3rfG_;umX8wgY>6RE zyK>a8*Y=la`|X|GnMofpZ!K)Mn&1} zHo-E2KnO{d9!b*SKA{^cSh4Z~dm#T{pC`!YFoI)Q^8Y#X zKir5Ww+bjj|5LB6Z|eG=R(<2q{_`Q8yXb!+ZH|l*3sLBhGmOx_T$-FN7G)c;Y=I&M zcBFbv4e*Rml~MW*R!Uc1&>=)In&64=zj_5|e4T90JO!JipeUztjx@v&xNCZTFeaz& z@6>69&LQ@>vU}58zds<~~8g2!%k#j%WcO&9g$>0E|dOhfj z&797(-e|#0k-bF$fb^Q)d6#;<6VmUiUibgDe(rYvx%ci%EMvxGl9sWdPA;VwpKN#g zq}S^nY;D`0t2=H}Fz5E|C zfPMEBk-^4SM+YAg_zMKoS3=~nhWV9HxvXJ+88VlH7vn`Wx%IsXu3rZJmE)n?7Ly97 zx6*}>+&GDx_wpjf^;L@}?l}O(Isf;JsU@Q=;_$Y@hU8of<}LM8Jx$^e%I z>(JZz5Pa}Bo3LK3!OvF*ZHVD)?f4G-WJf+Nx~xu>4W@h#&Bx{KterV0S$I~;D|ku} z34{l|nXt$JG)kd8nLx65nnS|EcCCR3=jwd22h-85RYyr}N$u<*fOq0udH#*ZL;Bni zO&Zn4f&hBx5vWkw#iI?8d0H#ISKAxSR&BK`UN_n6#(HC86%3E~2Xfk5EAC)ip_`8U zT*cJKEU2n8tbPG_6J&igFP>@xm={l9eK8bGfsqcPvULC!LZdT#aJ3k49kj_xJ%=TG z4$q$xf`&=t$HO!jm%$$S93w;nv$RN%|LMUkgL;nLUbYTt#|C`+ZYW2APck|Ky=OLcO-2Vhhd=1{*I2dj?e0Ohd zZ05^&bF&@&*l@5HW>@6MB^2P&Dgc+p0|2IYA2}e?%!EwSkk0H;(ZtZ$UA|+b4ZCW@q$NPTQJ5%cRwf!4T{z=qP!$9PP#g5F+v)AS%dz~e z2Uz~%eWWaZp-!4Y<&^ZXSvNUPNiLJ=ixYE_OxU9v`Sz$(7>)=lN28>ismV-uZ9Krj z7w;pDaFUJR8A1jgOqB&)QpdJ%7C1DVZ=jmGgn~p+aAFnoT@C~^Z-?{FbBlVGut?x#j}X!qsG@w+Ad$MaV& z`|pmrTkxA&x<9Koz`v|TIR3Sxe*I0XDEuuIt>fijEqcLBV@%a5gwpj1v+!2#tD!g0 zOljccp!2q~yVrTSCpXd5n^;e7LaNr%t8L(ma`fVLJaudQ0^G5+gAJd29|%2`1PIlcy602>aAzIJzI0JQR@ zbOCpXbuqVMfVFVHf{S@ONf-IDR+HkErU5J#yA&Z-N-bGX5X|d82xJ$nU0}^7h4M6k zHgRls(kA4PRLlN5m14478s#rEO;(O$RT-6;m1dscSO z*F`pXk|Ot-!LvcY)9YNFV{oNS_~v6yY}>YNW0K6o$;7suiESq*wmGqFTN6!eC+BS5 z-G6P>ZdF&^PxY7XPhI_7_tn449Rczr4{PAwEfFcF*b-pAXKM~2gUOMBdJ_~E&kXZjC#mTAT#rA~;9_+gVzxe_XA5WBdBzDs`gdZTB3N#v6#WtYaUMMiAh@SZTIndVf9bmE}t6?SX5I1XMP( zcV-yL{WCz%AdUT?FJyhwJNbpnex&#R3CszSx8?83eQt?|$W}j}FIlAjHG4bHbu>TC zRcpTE1nypyn!-j^_{?l|xcLQEK1ahTt3a)Gb-`$X(1T}Jj9Eg+=`bAttz{CUkY4!T za?OFU_k?yMJdfU9>iTfH$d9|SWm4aN8qhbHKkpJfBF8tnLtm@cgeG!Qs)X3mJ~(Xk z6Y8lWt4<*?Iqh)h6c-wjYx%;HTJ-bMWlAL?#n(=LH-|7RG-3>r%J$hun{lwz=MVa> z1m!_qFb39>3is(t2w5c?Q0XgyKXg1KC+p=B>Zs_vQ2aHZ{~bI3FCrisWCjuC@m)=azGr#i-lDQB62F@f)iL~937 z)+XxonEFBN%aAG5b1L0mPY!$P(%DgFdxthKa|K%i>s?EgQL)29^|WD;KnYxJ|0OaQ z@~9|Vb%T}Pag`Odpw;2@rzybYI!5^W2^MkQL*K5SE*cV6%Nozpr!#?9;Or9vd4x_g{|iat(9jZ;&J_Udo`Axy&kGQPERHC z<;_EG6BA3%JUE{mL%te|$EHw2f2J?a#!Eh6`|0LneWu+K>zEUuZq9{I0A?qERKJ-> ze{0MZ4=IBi83A!asWz}Au2IPpMLUTlY5iN4J%Jtv&a|~$z}ZOVI#Iz3<|Y)Ed~dV} z{-Tbme_!AJ^%?qIR_gKWCZYb8rC>Ky6eOPORapE(i{me;SU~b{sWwuv(cy;+^LlPvKbRh5N0%+cLO7>yfd@Zz0t|;b|{;9K_^#^JZ`ecV=;V|b1M?K z{;h(%5~DD^$b%ax#U%9WpwT}l*6lc+km+XfQRCj;gGtptP#$ai7se5A^K!p@es-zO zIsCBUdq0YV@KxnM-xt?Ns^xL zV<-Cs`gcN+|5e8THX(zo`UAN4M6z!HWj6qe8-O>HKbAs6%-qV^1Ey#C!tK6)Is$7# z6iwPWA>q@mtB+UI)9%HS`xJC$Y*Nd>acrsFiv=TeP*@He!a|m2Wg~)G6#xSU;t^T- z$T z14S*KAid~U_5*A$Rmwpfl=Ur61pdunWt9lQ*=*^Xi~i@jdVqbo2+uM=Wf?HrYz&8w zX`CncGsq|`Jy7O$HG=teH4y8z?Xw(u>}D9aM9y)ii0VP-{rdydB7{yo=oFK=X^)$|b|iP~VIG78 zaD|m0jNK3Ka691rK~|7`=D2YQ|G|(&PqZAsoH}ntc8+}~fe3*mo}6jd>K@f%#Z~hK zIt|gPGLO}FkUq9wCeZpy8-{2lOt!py$ac_a`TR-`vY5q!A*-0Y@TZ5;@mo;3T3=o| z#mZPfk*UE~q`wXc+)bKK1S>HVfC)%pTr+Oqq+W;#mA9amc6OpLfow6hyE4`1l7ein z=RsuGu92!a%oXcxuhXHbUr5EN^-$ucZZR6i<{C%1yfJtH9=F6+!LzUK8-7!|KS1*U zs5@DV8DQXMy{B~7TDMRiitE?at?JzB!WBP6uCP>URB$moB31S5P=hG2CY{mf$_=o; zBIN00ij=jB3OM>XNb%644V71J6ntJLP)1sryK)Iuw6cO}o+Vx40o5ag(!j~?{BfMV zB=Ww+RMX*n8QvxDUS$(2>_CHVwTjiEy`up2e|8w$>m<=^EKypiHN3nX90{g%ZX;xT zmSImnDMYQ_Q+AvEvX-PTTx9E$QRZD%W2NpXN7>?D(`!5cdJFT+GzW}UB=6HXn$=Vuz z>1h4AiD!c8m8O*@XfiOxgDY9onrhC0zb|u^t-7;gpdJ|C1nLIf<}u#Yt5An7#Mz9g z>;Rzkuk+(H&?{tfLgwJ72m?^Jdp>FytfTEOkvlxujg&Ee0uqDFO=42y60<)5y^D6coKLXAejbIq!UVdGhRR1LT~ zg~-=p4B>>W973+0q*d48H;h*C3$wvIzE$DUHYpSgI0ykC&dfYIN0Xi%Z-vuP@3EPLOt!>ki2B;_t8 znNR;&0);Luz2}Fz^It#pxkse%`97Zg*$h|~*mC$m%D0MMif^kRCkg2Fm6>qhic=Xg z%+33oQmENP{T~you`2K~^Y0 zR;;EbqF&kQlZ-cTCn0g8&$s-#Yo|2CPy1P}Dl4E>XUDnn{#bT6Q-3>&{!=xVZ@Q`J z{{DWV3F*Yj%&)#d&FiW9sreVcPPl5507+avOd-Sbj*oG8rLh%za?Q3|%V&ee%xUbu zyhpY7oAUbrn(dN<_W&u$$Z5d$<2@Lu$yPSPOLcCzfyJiGTESLb!kWM<=GBx0k%qlP>gefQFy0`n*-i$+~oJmZ+2%FaEY%+nX?#X;qhoH`KiDd6$g zToKsT)2z7wY{CW?`BDhx8p^Ce>(kg zQXZ0%+Wyg!L#2O^s5pzNfS+&%-{O2=GOscg%Q$$uQl;WlXwPScG%Ti!-vZh?H35L zBm!YQ)~gL!1+ogIzI6}Weuk&d?H+6u5!n$txYlFL_UYlspPy%Y-^7GZdP6O1Yl`f0 ztt~ZimTT@UArFgS!9{T!5q|7t4_|vw6rcf5DX|N$)`AQfDeG)!u*P}Ego+n-fb>D8 z1gD^lPK5i|juIyA7Y9u)5nOZ~L+Gb@j%yDVa;z+Y#JUbE!6?B+^5eTQ*R%m;an|v8 zb*j2L7gPV2EKiPA;@Vx^n;_r(H1t*rrfGUr$BC>z z=Megh`$+@`6$i{48)liroUXl@4iif=G*=@;4&~ZiWbP&Lyq--!O2>h9xKJ3=+MiwF z4q8c!S>NDjm41}C1p6_kCew31BAnx4<(*;WnQ3^IY8svBiid_Na^RUM){xH7SA@gV z8C56xMp3#Z`qEPVkmaRFVrYIqAgD-2ZAvN4ZCF8aLq~!%GAqW2;)^Ow#rZ2**|ir? zPExH?^NS8WH_!q#KaL+h8Aot;2ap7i^*%@B7~M#jfH28Vt=zWh^^0QmTsTorF_ zmgi$%bAx-^Re{rzfx(m8&&S)6@`h?Y)DZ;Y)#%3yRyur0`ZyNAkWqw8H4@u{o0a2{ zFi5(23qfwKNz!ad6Y-qHe)SMP<`P1pToir?->0ff1M4BTuNn) z_4>_`@U_$TZD6*9B2Q@2;sH-o2xqL;q!L*!g^g4=ro2=0FR9|hLAYbmoRK+y+nXb} z4p;Nzgrw-WNUY_bTgEt4RTGR6$8{sYYMzXXnV;pYGc2HOq`}PZ4qVq_AYEODX4h8- zlcpz2&?~Bn@@rk!hs$|D5FXEhO{jv1hs+FJfnOP<^VqMkiUNYZ7cFHK!^}X&6^A9RS=x5Md_gVnYuQ!rL5Xs}0sDQZ% zI``NBjV`XgdTpHv;Noj@Gby8ELp^fn7=n$iRIkY2LdZBEzlR^*PvsDx&E(_NTCr8k^?y!@%YJX43LYEsYS)?}O!sTyDzmpVhm#==lDlRR%3wAvMsHoqq}uZ=x@aHW5n(AH!f zSd*-k&3tYS*4;3T3RAv}qgmV}H(8v0wq0UJK2`D4J1PZDwcFao2?r9}e`U;FgKpZS zLFWY1Z=hyuVco%doH9`8DoN%;CXZgmr(w;!PW&inbIbSZ!QW7_KM)b1j(@_^uldxT zM3u8YqMs2$QQt5}%hygzRgNIQ(l)tL%Ts=4T?i?XVW2bRxz^;qWy3?+{crf`a&ICt ziR0$AK`8jf`BZLzi9Ew%@i0D6diBTvF=)~LpRk% z-E^9V|nq>sRM?;5MSUPHX75o~wiiL50i`d(%3{Jyc=FVP}e` zWFM|gw$VcjjMA|32d^w;EDq;=4^*B_Nxb%DARe2y$_Rny=DNUXnlQC;$njx;Nl&vx zN8Txp7kI&cAtF0GU2kgDo9+ZB-YWkBEl$!6ZV1nb;%&216rY}lNma?dVNjK zqj`5gM|I6b@4X*iNsq`efd(M*e_-FIV-cS_ZlmjN7}R-eB5~}LXpJNJ#*f=VX|wUt zjnIPE!^UuSS!o*wlkaTYT}9>&z>2|9fkM3Ni?0(~ze|Hqzx|fQ=h-eE9P*T(r;fu9 zxro&Mh2|v=!=J@p;4Eo%#mU2mj>V$?w<@tU3ZXO-ZUnP!#;SOEp>yI7d-G27wtI%v zdfCEyqcA*CUM3iqzL2wc3gEyH+^B@f*Q1YImYpmcdW|>U@LP4SFNLGjD!mp+oV>K^ z<-RWsdF`Tv%x>GucxvjE$kD)DK~$MQAm)(k4TGgP2-0w>vJq>%r7`-rhx3rt#RzZA ziMtWeZsi;0=epDHeLwPl8(w?7rc56}(afF8a{y?_aMIz>?~ggBIv|gFc|#7e#wB`0 z`5fF)b*6Dhxa5ZNKg!g10+Qu1ZJ1^be#9(>Uf>uV2d>|0fNc+T6FXP#&+|65?@hV47lhbWSoH0J-Na@`;FSXvZ zNY2Ke*TQ%?x{B~Q1t5LH-H7#oHg1Axna^uT!S=^q#bE+nV4cjYED4APOc;wq}7CXvTBNErh z6f1_KFlL~BWU}O=788{<_!OqyhlWMHeG&1{SE-hm(9`7X$)l!+va*^$L#mpH`D3M6 zJ0r!flha!0lel_xza3`b$NHZt*Tz(;UiCaG3S zY;tcrv7cbLu)qhEV3$r}T)uS*(+PRh4e?Bo;olG(qvxNdn9-2W&@{m1qSOYpMAh73 zG`X!6Z3PJv>UlBO0ceuC5w(6ZxnSKe%(B{9Rf9aqj#P|e_-KR4B^^nPwwxtN#u#Z>o`pKj06nrSUJHfR(h8|qhCgXOjNsn1_@b{9}SlDSHi4$ zb0sh9xdzObD%kbpB@8F6n7Y`^WPYr353GStg=|{V0!$WiPg?x9;#L`k_Vd5|M&fqY z;(m?CP6;}`?NpfAb?N0yakt*mfN8sr_rv9&IHT3z)~R8-N2&!z2Q#~6Onv}&u-z>G zd0K6a-v)gjjjg&@s;@;Vsgd^27sm0iW2hwQwv}}th&Bv_JTae*^};y%8GM9Rp#|FO z7l}Rkpp~p@@Q%(J&b!vfEe%Q_cY!PGx7t%AOjru|LC<5&wWwaE_!K$7{ELasPLh*v z>>=N+wLxsLCNWbdY&QG60>ESa_ujd5A7b{_-NQ5Q!B7a=v*%8x15O0Ilz@qUPUZJ; zxXRPp`j~+~@_LFsN|XxcY^1T*`a!g)D-ky4=&Nr3~k# zqZPSrH1oGjm%$1;nQiiNBih(t*PLPK&hL0`%!hRUvTkDdn)1sLDK6leiyKNJ|K*oX z+SdZxzukW`igZ5v$|54@YSeOw7R`9UsSU|SeAIlnEZD{p3EK@Hg2q2+qannzxbY`+v zm(%{^d0OdzihME}-WFOc+q+O%(RpCHK~O@OmC*#lLeJN8pm%gh1et=}N##0I{_pq; zv7CEXMO_-w(Y-0`V;HX1sxjO@87dM)YbeQGqh`V&i?)f4yx`)H6#=BgC5>!|;OG(F z;~U;&5#QHU3xlXEA#-qV)3T|)aPqel8Ih`IbXlSlPRVE7f6ri`LmZ3|RhOA>&RxWu zUa5>DMV756??{V=ywC~_n`X2MN4_jVGxjXWVD|`V1iu!nZO6$eY6U-;ClVEB+?>1k zj4cgK&n@Lmm&|{Ih01Y>pO*+7QWUOewimR1)Gc0vU>x85Ga;^kf#y&M{ZqgaliTC{ z2W#D5iYtL7Vl+7f(6@U@doN(hbXjq&X?1+#+4_X_u!Qv@2A3~ zUs>Yv8l2r$1t#&jZ zLDq?07o@JvTjHmGq55svbCchaqBZABWCzfl7CBKw)!qt*&l)4Q^-&RS`VU9J*318Z zL8J`19!6!NOW_$Ar6tN246SYWC~mfdZ`7i|-ZJd(2u<9*Lvy19Ml!A8L+H^ zxOUr{rDh9vMjmw8trH%&^+yRz4+=Q)r2B}i|CW({XO32{&vNjE##2pj9C#^f470C7Q`A?!&8q0iQYFL-4?m^TdkT!s;DN5+^=sQ2;Ge&&o z?-y@6g3BtakrmE9mz7mdADy90ZrLzAi&FiTj6nNNg#tIJ8o&%2F%*WN?}_Yt+>n96 zMSQy-blbm$8hh6S2bDl5P!`mE8Si^-?^(YKQwrWfw@(Xe$eNVtMaV`6l_3ROT zpp#Q@`Jr{5N}~5?^>z&MK6(}Ch7j${b9>d7r>_U`D^~(D1rC34FD*vhzBYd<=Eu7& z(#7fCf{q&W-3o?<=Cx>y8U9ppfv++Rhw7gxhFOL~hG_P1nFQ&xhh^jI$E(fl->dS` zNma=!Q=fH!CpDc}#Z9SkzbKRXW5Lv@_I!P_r|oh6B);R`MWLOmOY*Mdpqm-^$(7je z9qx=itW@`S+d2gYo@sVsX~O4W-)8Q7>;57Wp`=OU#{8}Q#>Ae;7Cjc+F>~XT&1j!} z(Id=z;ilq&vg7{b?xFnbj?yvu{Kv%EcnW8OJIT56MA6r^p;^L)=bJ^`vaQCtjKN5L z-v?_ijLRy)3sl#ktoP9fIZFGEDa`r%LA=P7Jqe2ytAY3CB@v|P`7%Bz3?7&U)bw%_ zpP5S!_euQ`?jx{m;8XTq>C(6=QX+B$k=xy!JJacIC%2-xYV$ZP{K_)qOGRR2T)2w# z4O(GiZFl>Mh?u13>_@8yG6RcB+-@TF8_lTQi;Gp~;&qSk#ie`vrh?%3?)_E&(>^lU zj4sH-dF}d2BhI@M_H$;k`|AFZCwIrGtv7V#;u6xr`;wnW+2~SWCMBm^p_tOD8dR%q z_^X@KMB<-Y{L@YlJ_Wv=ok=%%k&y8E?)AgY5pkc$XXH6l-0DcG&c@C;AxT@jvcc7* zJ&PN^PwtMtPsOUh`gyCFQP0*kg+OHWP?f~d=T}=@?F3AWtb2QR``q22iiwcWUG{O+ zREXa%(5LM^N7>{2Kkr{F|7PgkPw#_2d4`S5`+(j-jY_Cp?4_q3BE9JWbaC3wORC_3 zg%QnH<002|>%Cg)o7c-;*rveh`@VgH`ns+AQ!@aesKk4kpF)bp_bvePSSW$|qBjTd`9&f+!CWb?<~ z2RPDkhL0Vfww3_TxqznUD?T#4ZQ^~$*0Wg^A-DdTsl{zlnS)EybFW?;!-)F1N zXEN2HFzp{qX~`m8m0CK4;o1jNsUw97IAv6s2CSX1!<*3^jW2Veh5>5dTQ>Sg#S{6a$v!Y>Bn(L<1RyQ(sEkjux4N&X}--` z6jeeat6MZ7+IU;3)DRHl6!bl)+Ou>amCda2|Hj-Zh7RDyM;G2C0zXIAieehrS2@*c z{BZH*dg?xs78A$-^b8%dkz|d}1#d{;ATT=wxV#d?wuPBC9hRt)w&EMV5Me9t}2l0nbE2?TJT{6LzFTVXCr zaOw3ek%aoO1;toW5J=F8(x9zNU~+<`eBd(*1;ppK zWPDER6j2#UK{i~P**2*T$3cCRm3j*woS7(mLckV0eyUFH$806f&y}R*Mje=1?&_E^ z+s3TFGxY~~fI%)}1RGiVk)pXa>8n(ZjuzYaYIG?q8%akhGr1i;ch(L+uTGR9w3{<~ z1!r-Wy1|ATmY_zRM;PM6#})5r#YMe^1j)Z87%_S*eonh?0z&t%3jB6US)bCs0ll7D z4qRh~ht^K6o{AIdIlRJj*Cc80G)I8WBVhJjmBH&D3=%57qUCkl3=4^gl4&X7Fvm{l zUkAGVF*o`aZHIrp<<9f*8g8yIG2?xP(3pBgzTcxW4hgk$trfSew7XzWUF@~e`>DUX zGUl{$H9(S5V-Q5Z=eKh`YE#_0JlJwCqnIJf1c#fj|C$d$@t;&}QI1I6QD62(B>D#N z{;b>HK%gGCghVsbC)7LKI`Qvb(5R`~y&8NyxD~Mmpmw_h7iTX(`Fvv8ssa;Y%tg__ z3P`!lz$_yp8@r>s%hTwITB85n^+UU1BKkh!j4B~?grjO6b3Epdos*34Fh*O`Oa(H4 zv#^>J@uymhvB|QPs${x3lkukV@%|p!$AWxy#N$AeQi*m&;(BwnbkVlAG%%eT%TDkn zfn%XZy%U%-Wp<2ZOwXjMnz@o6RNTI7;6POq)L}D@IUyY}BH8aW;HRjp#jYNYdYSOy z1^JClHb1-xJOTuFNsYsasYXFWgDb5ykC01e8?LkLefrIIfb-_qF&X}gMh4a{0sD+I ztln{?^%3anQ9I$fS_7e3k!oefD^U2IP}$40JvlBB)5%m?7U%prh$9y|&+RhVhse)@ zH^tuxA|PGqqkpET4C&8o3pYnQ>}Q}FEpS!QCM%|!R@R0T2>8=Y)|0a-)>5grPl^yK z^MV4n5f19Z7f%Ow2$ab=A%CRtJMINtcJ43=KIA-fk_c+w_3Cg?gQ}K!h>yO^b5l$p z4o1*cqsWdH+B$%|?LeHTICh$UD=O=89Qv#hJt7~$G0RSH zS8~^vcL08**H+fKvrbRa{F^qOlVSY68&H1s<3S{VF<;|N1umdwe&GPrd=`KE)mLv9 zdEL!bhHCbiv(rk!Gn==&qas7Ob}pc>^Sk}S+8i#!g! z?OGHaFfG~~KD4!_jC3lnmcgG)vdrklJ2oOJi3A{b363jQY$ccdsh>9>MNWoL`wKfr z!L}P5lIzazx17Ugpq?$8uaj6hs~dg<-2+~E$~vIqP2|VShZ2_e9rx&$Mq{~0r8~>^ zS{>Rh(faWPcyHiqmLZ=Oz0TK+a$Jt5h^@)1&xB@6V@{vck2OqrAQ}ix8?3JOQ$iMR z&o}cE-RZSCAIdfcGA$^1HhPELSQ={h$tVL2@<#ZNSgS0F^i+*yx(1w3ZLC$uVI+Ff!EFek&-p8HLX^aAeh2Qk4Lt+0MoiIJCDBn&)&khK0<)fy0Tic!fclCq>8zMHj_>i zr{nqT{U7d!pGV3aWQt~GR!OxCjM@M`nxp1{D8<)uvL;X8|B^d2VNO0K|@T`H4^Sz1z-g zTX1yG!V001+%aO_g-REPFt45;c^Bz9q@91&^C09An7_K8*%*}~Q~hfh=SCq4HWdcp zAW>$IKS^n4=QjYYk3YVG_t>7tc=bXw-RNBRvYFFdt0%RYGJkE#*PGA0icgQTyeMCe zvs?yR`FL!zT?YRvx?d#~c{98R%0H0;#Xfk+1-?vb{gF=cFlAFTu*|Ti#oFnk>{3BrzrQ_y?inAJRx$Od%Y6 zsS05;7BZER7X($SrD&|VA##Wjayp__zD8B{=*eAzo4Rt$Et9*d z=)wU=7&iQJLKV+f!O9D0HhesE0ZaIAgkxu6J zu`ptGdXZ6-JL;6ff3Q?)`GOG{G`PJC@C$xx|3H-&l;A|I9QL7=kgXJweFVOj&K9xh zpZd6W*izBmzTh)H}4B2c+i_W)ZYx^CXaewTfD=JcfmzygcLN+YIm* z+$Mz7Xa`W*KHKEYc^2C`dxX;JHi4bUT8=FSW5IFg){8fgaYZ9nXuHZz7fR@`@E1S+ zpQ1HS)}kKvP!9{EAaqCiD{C!i{uk^BN8UFm6V{w$R*e#!ydZ}wQ_>hVSD9BJ%Eug(+!7fFh$6SD2ikc|c2V>wb z{xzy_i&3YoN&?Mjpd-9oKe@UNI)SgRuyY9(Ccd+!ieR40T~BY>6%K7G6d^Xv{!uwD}v?pU|QF5l2q2wRa58@k3udZsp z8W{!c#xC;bX1?5Hu2>pG(HonDXJS5(9?Vav`X(XeH+(HghNEC?MxPsV%Z^UKm?*U)$;-L7<+P0IO0-jm z3L+vl7$hh71I2c585l*o!Pb7I?AJ=fI#NEKTFv1W&_0%&h);8t{F3$vCV1g=N4%=GOX<^LVHa;TmonslKN3)UzdESza36QR0V<%0w+Kzt|2BLS=IYrZTS&%8nND-bq^6Mybt1}S29h!dZgI6nx$u6 zo9R)qa%j*yGVNg9_PX@R`4y;lcL&Z0!|6BEB_M)PzS(Mla@oQxCQWm5y!tMB7pql6 zq#cS+JG4W#zWHgWL4OP1v-&uGkv!~DIDP1(XSI?tb0z#@$Wb4VVuS*{<uQ(&sx=iCa);`3E z+U!aB8}Xmov8I+>zvisX#jA{wE;msPxvIaclBiGFU{nd=^Ne7v=G1Fhr$#ne{VSY7 zHD)lni(gahYgy1L-@VM`m&w2tEcpxaLYi*ZkoE3=I1QI~k%pHFaOyMGTlqU`8t9?6 zmc@5PY$a@9rD4;ERE75O3cgR{C1gP;B6hfR?OdO9y3lS@-2k%H?K^ohmCGMo^W znt*H`3hTw$DZX9ipkZ99><5lcCwF0DNZy&D9R6vSF_U*`t(eW zmkQ>!8IF1h?6NK96sc1ve_Ut2$?(2vCM-b$(yslpP@5fHKgR)p0qzwNI>vCRro69D zod9P9hB_?EC;!rVWFp_q6YM%F@jKD!=ByOcm zVvly*S`?8r8*!~nY89E)I37NK$vGstU$KdQfz+HfxznX9nH=>N1r$6oqTl&FWQym} zYpD9zp_d5=2!dI=hKR@8&6fXgGedm;*lGqo@x1z89K2e7W*BwrY$ohKw~(}3y|MdC z9x>SQD#@yV?q5DM^X7yRb0%8id9|%H&X+gv>!J5I*nG8p6dK$vJ=vYhOaZmi^RR>j z+1X4V>4I;)FCN`$4tDk{+*#)YhkivpY7RgZoBbv(!KQ=@lLfQA_r+#s?cK-mPr1B- z^WILocsLg;SU>ej{`MTxXYiY~wVx_K0|vruxgQ@L5?4X@q@o|ub)a~WL@GUShelC`;2ym z!_FS*CRrx1uZ(`boArc7R?_W04S5OW^tnpzT#}Czt>|v$b|3L~)TK`kCNVYm_@E|@ znMbISL^0NVBj)9)%C=ekgP?%blEi|2fQ$Rd{3cf8nX@I$m=3k#J&;5e1Jin_~4_WBRjYr zhSHK$khg+~WU}$%qed!Vvb)7jOy(Dbk&}jN&pgAvCYfS|NDn;skU3r!J~@czRel`f znug!7W7lkqX-?K0u^#xebz6=x3o9ASxDNqu(X{o~`y;@#)o;rwf2pEPqOJ9pRZix4 zu`hG4utA0@M8+MJzfiG}S4#I%|5|6e5*`rHwmXDSU_r*87)^cQ-T;wrkJfE4(-{YAA7huuDivU`BimL_aau*#7V<{c0g3n+o zDE{2874gxj<06=S<4Vy;#zx_P-+uCce!-dd{_?9T2~)5~_nfNAG6b#*ys6?>NTAIS zZT!r$Te69r_OGdklOmA+1T}e-2t+X`c>AZj?tts-SNLcs9Y7Q4)levT55mOsBW6om zua5-Ub#3C#b+fRETm#;t?%|Td3On+-E4}$nVkVffhBFfr6Y&hCdC8mXwcBlc>P$U; z`6$P1vu1>{M54VCvlTvTJILXiKAVFEDTw~4rp=<9D>M$x;PRV6dl{rzj11{cXRM-m z&g8U5fC1fo{wXt&aK+k-7+*^-#Iq+6kG<3RoFXYK2qFH9*GWgF_`aba;#cvu55@BH zUH)8QrpZBJ7lm~|6l!%k3il_#hHJc@zEKD7hw7orJ^5m^8gIVS#k4Sl0(nRnCo5(_ zsgI@6IE?%+Of@srkh&GCZx!5Zd6mwzeP}VJ_>6-RP@!9Xc)_(Zp9c0Cd3bA+uBe9 z9-JHc?UVV386WXK{mVJ}i{=@9a3#nX$4v@-GzUxG#Q}ys%Oh5ZT&$ztSHu}WkQodJ zUU5F4M+Oh{ZMEt^L}EnNJbl!E;{5dmMLa)t#OtFhF70*zu=zOnNDO+tdYSc?@8Wv8 z{}DTOA>hY?-vsiX=kN;^@(bw&9fjAD!x{i>8OEBThZfB@ih`9DY+{4ZwL{FFCTtGv z`_D9K%F}lAm*LEpyVFNRvrC)%dEKs`KA+1RdNzBGT{LDG47HF%s=^G{HWE+n-(d4@ zpZOj;zJ})(NkOLtGzrGhcf4`I=7l_x-z+L1za<(9lQuD?1V(3)j&>Z$;j8g%?Txf& z%+U1Fz;qPzSK#OB*k_r_A&&XK@%vTCw{E}Mo;vZ}UW8PV*Gk8%f#6;pCNNkS~2$o9(3+G4qv)rBvA()+B1FAtj^6%NVJ1PJ&Z0CYn| zMhs&imd&7gHLFfJ%w!!)avWwPA(RUUGM(Y^^#z!cGT8i7L7b!xo|KvFFudmaon~== z{6nCAAk$C%@^Q;RxV&tt$J>@f!&9iPj_@{ofob$pYTe&6sYxC>>+}8SS^Y!g>cV#t z;YJ+D6NL5;^1}gjO#lcAJ{SL3wU@|t&<0m_FBLaw1%%kZ>VzH{Fpo!&^9li;2s2sb zNGUcR4)i@+UC29 zF}8tj>>RdgV!J?(RSuG6?*Z{Oejo20TLwP%#sCanY=PKr;Js`3jbCh6`>TNM{ne8T zFO=V92^ypA+UfmtH!2izZ8WKHgoAtz_1HzM`pARECFRzuWt{x!kGQI&G1^A@d!1C{ zqcb3kdmgEC`jHs9VFvHR5X#~7JQs2+5^NU z@In@+w~_@GKL=*{I!yNGIrAkxZYlS4WaN+vSeuoVt_zz>asogEgh9PTlbl=+m5rk% zowO`=EIIjw>7)Bhv6^Yrtr)z~>}$Hh*rI2!xHbdKPB-0Z7H}QlQs7y*A3Kt9tpWoO zA>!IFGFD+il;5^HjfzGAS%RbWxH9Ci(imjtnA2$aGpd9#;!n6)>-W#D8JeghIYC~P zV5;%Gxx!%Y$g4fyPnvReXkMY8j@6c492M)F)j7TyG{cZ){=WXkX5L%EzPbGl%%P6E zxJ%II0!fbjzL@UzLN%&HB){N+zABa|0 z+EJ?>Q<>?*ciZ33WS?l1>4k2B>*3;PS%i7}_g4db`H+AiO-Dp0AkQ3;LrNk~5&EIB zsI$;SL_ZC#9D&-~3U*hmpyVeAvCH}ap{(FDg zh%bn`R=ZCKV15SIqr57MA}J_e3cMTN`=s=g1zVhhkuH3Bs6^DLq3`#aj*j2<=Qs9= z$4?&|(G~doL?OMR{S+iva8JtY2rbPQ39s5no}*a{&ZB?Wx$3H$(+v6ZO;?^+r2tyW@Gq1PzBck@9; z8oK0(H6<6f4A@Zpm$G}52VdR8)MSjgNJh&yH)*?sYMmw8tJ1$?rQtg-7bR`yi%k6U zD)J?Ut^2{N)@JID>D+CB@L7Dm@N#`wAz!Bge2vGs4?uYt>w2z)AU`?=?Vo_29K8l>*e_L5R_XGqckLazF0N>!;Du4o3S(}cppIxoT zb#@0{pEU3}z6l`kRk;U!P(Z&Mi^OZ2KocP-=EHrtv*40+*4_QCMB|*ru;;^l_}Tsm z)v@b-Q`0bk>lG16Bd&V)117aVy1j)T401WUH5EF)AFR*ehN!;fEijMvz>HHLEdLbl zU!MoqUI3ypv?zUVpEDuYUq*TcG3RgqnB{f!Ks00TF2e@+x3#tg78w-&K%v*3x}lZ& zVDslg$+3Ch@P#4b3%x5gCc3W@FTSjxz9Ljdz0Siga#m8i#o@xiK)Gl*9H=Y3i;-UUEhCuAD0)wnWESuGTjzC#v1{|2jI`QD**Y<0YRtB+1k_}7TV#r` z_7y+>97yLTk|Zi)(&Xx|5E*L^k~#At5Lj((aBe?wpIavoZV<$)PUQY--&?9{!AhK! zSWxDkb|Wb`ruN@P8T(zG?~8CnTP3kO3#ExXoLl`zUF#d;=t3X=nDyMq^DqygOx0GH zL93M?tFBJep)48Aot_N5QjocZRasp>YEm|oeyFx`SKd@j)`Wq9aEif_lUsG0E^6cI z0j3{)`Br67(S+llEG6l|vE0@$Af~Rg^qhqG0i8V}1A3Bf2zy%&ZZO3g5lhnJ@)GX5 z-7cKTdh|Dn2m${bYWAc|m^=@^M%nkR5%$m;OxJ|!x*`sjm`8}Byk{ZVxcce9l;1Cs zS{iUPSx~968b&f=y++xW(wiyS>y(lli56?Mg0^V4zUJS`1M=e=t#?1L<$Q(>vWnjPG8rR<%XQA3tFW3%r$yF!r+oZah(-(KpHC^WOa12 z)w0;awrys(nm}?5J6^QnHRf)+mTkJzpw%A*)@-LC*9DRj_F$YPEr+(mZP@pF(zuAs z8fZzHswk#FPQbQb#2pN6b!PKY$!IW~=hvt@7}wpv?eJ-Y;IsZVVmdu4(L!%*>hm!3lmgvV= zIvrn&4WNxx5^eTp9h6m8>ov<6QcQHiAr?64rrsSU`aCCS+3p)TBg10>TxBgGhHHK9~8 z(r7S4AIlrP=1T8#xw|tI+uNxfGF!yM9%%LGY1_yr#3T$TtfeXK2vOt-q6Y}5$GC-s zGtZdFlIu8-T&LUUiEPVqQRs?J>1gHqqcyq-LS9g_xmsiS$S&eZIPq~d&!)}Qx(SSE zwv}VA$qh4`GgTY!&}zaagK{Xerg|-yM&UT476bZjnoB|7PGdo>w^e?nO8o}wWaijh z&l3*Ho=$e=2+L9PwFOz1`$nX#><>d&H%bSuXP4x*r?;?U7QhJ*72%b;OO~;q$ zMcAsN^AR*w5n#E|Zb2F0*v%G77-H1gSTJE8U=7clAOsbc0A&_iFZCv_0+N}gHCo&X z(_<;Y;#xZ!>R7{3P@)IeS*OV|wSw-iY|L@2O>aSfu0ZO(Px8~6foItutCu>9F>zob zeY1U?8GeAfHh?@4-a_qJfJu^v_%7j6uWlPgZE;WaG+E^$XGYPCM<&PFLx~UH`O<1Rc zktei=%cZfwGjEwMge4@WdvhC%MSj+w;%hA5cmt|ccSW-1UT2d$z!Qw$lG}}KV*M>4 zh9Dl>L7UhZtzK_E7HVj0b=8(l&pdTOmGEiJjI>~I(?+7Xy^*CL3b5Ev(ayjd0JRft zXX`GrV0#WiDIKlH`X=rm0lTEn2cE;%-K(>mizyfT^#Gml%iEye9eI+YlGqO0~H zIhR;s5g5$|$n_5G%v#r542{>864OC59;<8S+R<_nPt$nI)kKgh*S$fZCM_RbTw6_! ziXC+@qKbZRGAYT)3KoJ9*=+R;tm)yaqSX_M^^R?~=4vcafj}>O+$E@iG@SXqQQwJf zs?)-jkUJ3>HdBmlyHnQ%AxO%OQ z&04ql-ZhPFu6uxsvUC(p)mcc{+0<^PIW}%YeSW4xa$Q?CcAa=w*z4XTYeWUX0YVTr z188bBGa%Zn9oid-?R9[-$>-noe`DK)!CMFmtix*iX3PHEc{YYWLy{U(@(0CDF{ z;BU?0CKUj$l5Rxe&c-iXv5@DwSB9}|7B|$y$1V@R?)o=yL zwGhQ^Nwdv(a4pn0MDedpcUGWQ5_5u&b6z^BPpH1;$B31BRvn9Vc^>%-He3txvP;WT zO1?I@K-6bSi712VJVVn(?$^*sksJ7&ojW|+B}O6F=Zej4=Ole+S7>~H%8-aR(OnBf z^&SxVYD}SAsHiwXqO8DDfmwF8E_w3;!H_l4XqgtHWOY%DcRkCVi2~+YczY`wK%6AX z)NgOH-i)r#y;guNS2?|);k21z0XC+>9W+MDWjkIQo0i!Tm9dxedYz$2@PF;S*$V7R zmnL>^@H_kz4c(BYQu6>+1sYQGJWDr)=9HA0XMx|{(ypp*yKuX)4c#`J;GB!ScV?ud zSnC^BL?~4DpyentoAu*1_OiNpIcO@F`*un-;~+SKX8iV~RZXLIms?pI^={>&kpLLou;4JaM0fzUz+LF13 z3`HkA6d^&l$7=E(o}F2T!^=(zcpcH@Qn%UsdB5lm(sH~BZ)4xgGl~e*c(9CKR|gJc zttkVxz9|9-4bAswNi!&KU=WFl97rfQU+TTq^Xjkn2r|JjCX+V$3fkwsM<`>V1?9CE zU~8kR_u-CG;$X z$n?FlAa-`kTd7zdvaK7L(apfEPMtAYTTL~?>Tg522_zG*g<``2D84nZW7Wdk-JK@P zaA8jA{a6uhzQ0^`Es<AZ-`@ms_g&q9a^uW0nL~4JM3-K#k0+tXj9C2zI*>0cg?> zQRR=mtCsb3hubT$zp;NbvmNy+k^pu?JLNY9(h-K4H3jW$niS5vp}`-MmCr$;>)4Ev zPrQPCJH++-=O0f-srLxm9ucwHb$Gb_OJc5Oeg{U%!@lD_oGy4hxJ#!=LWa7xo?O6N z)MN|(#WfQj`C?db&jJJQ^AKb9{R{?)yb*7RZzgk2Jo+Ux&w9eaCFU2b0pz9|r^#;V zg+{kxoqPE6TD$?-iShvGF~iM6TVEK9Jz2quy?5FNbwC=$om%^=prH=w3j>GPhkyRW z*)Y8u#QW<@i9>VJ39+|;F8rl(d}AJuDZZgz&*>6)q_$=EitS`#K3o@o0oZ+k{%P`R z@T+yp(HxM*Umqs2&_cM_F&>IUs1vSnW^uP2lS1HOIZqq8fna?k520a5gws=wgujSh zT=azpieo1At|3P95(~eBeml9Ilej|>DhwvQ!F)9dDXtv87wMjegPbaN3F$`vPR%30j5aFy-l@10TZ zF+Rs!5B!4iqo_~hWfh7_L|hs1OMXBdpCgwqYs56O-|&E>;WlW?@e+>IvM)a!QsvE9 zdt3vT56VW*{3MZ{qlH zU*YjWYcyZ{DS2D`QA}vkWRIx$`MAu9%23P$n|Nmm7PZ#hHb@xt(7#!ZtNG#>sPvs< z=I_m4K0*~E?H9v3*jSKHcLx51R;^1Acmf3-24F>`Z>aGb6$+EvLB8ToUXOs(W~)|2 zF>it~rrQ~8agq7@s`0Pi$mUJ>x=KPd0wcKdJn*B5du&STYdern^5;h*R)JVp3ev-P z#`KL9-6!2oDn$+e6M)II$(XENr_=-EQui~>kDl7`6|BPqz8V+4yR)aI%o-FixVYUQ z1{zp1@$0mZncR+1ES7#51cwwB=`uIcQ^41?tE{?w$ZMoK-A&HAyif0I(YI$0&B{zA zZk3)A#)FXr)W=6Yw@AGSP@@H;T*~vywZXdIve+9E2ls}`kVvFf^2PLL*0RO$BHBC! zZy0%M0bPD%(~k4C6g2OAn7M^Dcp%ZU{gt5h%@6(@-1LToceI)nY9rS6qZon$PV_6Z zLOgvplcYV~HjjsT+`tfm+h?>aBN5Y83MGk^<}2d;q`a-eSF?Eh)5X80;jr%~I9Uh> zRL1Av`18%y9MtDl>r|@xzMr$Jy_ha3HzZIDywfhpI`xo(IbUT;`-&k-&GP_lAXF{U zLX~Ussh}=Fk5vu@e@*7ajCIWRuC;c?;gO30AwyH(D;7r_kz}YvO`>#_gBS!T& zSgPYk&JFHunA(*}BZWfV_5?0d6Hk(mTCM$wba-`4`gYMt#3(%*K?gYMg8jIjGfyOu zGR$fikfu#85&W1E#xKk<_rjfI0}1jpbh0M>vwi)gzN48Ige$_qpea<**Gk?cH+z9y z^5KewY$0&%y9*Gn_t#?ztEo!@;=YT(@+GrI#7C{!sHQJFA_i)A@gU(HVDOjEEwH1~ zyAwcfe~>AU4{htcp1R;jYoTRNSu=fim=>q_OUQfBd+!!DPVYKdUmHl0Y|^xh#&;g$ z21oH_^pmiUTWS!ze21)h9HR$*)^x)2jg(Q?dw(o2eyh#uXY|4Y$W`^q6iDdK%1}TZ z?dXkd1ck;TNN>Jm=Z%3YU>GJSgR1#qvXi3(@xlQtu@o)SjqWqaJ95HWx<&9%Q$&w6 zGzRvtKG?x6SP$3~8~3g5JypMFJ~l3{zL{038|0}$Rec!l<2=`*AW*{Kx5&rV$S!0> zF7jR?zIr0KPa&Gz2(`pTx+SoWs4A%)nEUpkL9@;oMV#Kfq5CN<1Tia5(#jC2mn}mD zme-8H!fmig4J6Sd3l~dtE##IiS^&u_MXt_wwaxsO&e492YjJps0(4yIZ9luL(&}MrS`d1&6PFl2_CN8Ib4Xt7{rk#{6qT z$YLgf0zYx~Niy}zYvt4~0Wd=ghWSm`$(uU>Z@*-^WRke7UjS{Fj)vv3TYeE(uhz9l zO;WWTiG?exdVXrTT5Je>y7P&6U3~-*NcjCk6Ntt^Hlgl@uddQi@15b&SGdi~4#W%b z%KPy>yrvf0EJ?jJ+d;}x+=Ztd*pDakKT#y;%#OR16>w*b+78v=-Qs%%R8op^7OvOF zOROY?qC>X+_Gc)4dHYw9VUxWzL3=?5buv7PWp{Vj>ff@HX}GlRoy(;gHGRWrH0&MG zNB0fRgfPpLqJi)0|omy z^$mlka?5pfF)}aqo2MDpCSsZx>#PE$(vDan82$J6W%FnikQBTw4t!Y;^$7II22Pv< zGa!EJIx1n_@JD@q34_8oiN>GFD;4f>j;f_LK9~Pi6cXR6ZihCgg<{>xQ<9PcGnw}% z$x5280vvMuqlj&ysd&z$_KVo=F;3ZEGHO%oplO}FyVpS-`h-`8{{Bjcq+_kQf-t#v-MB$VnTO0XWM1TjHmUO^om-iF5#npL{zI7P| z#UD!_EP|bBXoQoEf66k|XG)AtR>!0qor{B zd9<*A6MX}s>AQ$1*d4^}mNfsQ<9>WOtO7c6KaY&YyugAgnKwTguggF?gE`f3HTSIN zr|ZeP@4km|aK1e+KIF>@HTw_1ydI@=KWNvzeTmD4l!U&-FcAB{)21Y|XdB}VUV)hN zNQl}VO+PoAZ}DwMejrM-^(T_qw;r$O zixY2aMbA@zevf8^O@SEE7+-7k36##)&5j-&i%iyuaY*9Gta~JrBo?q7O*F4J54Sya6NgN(gAsw@vhGa~syLCEtG8DI4 zBs@Lb^dKQ|U0wpJin6O29f0Cf0LE!iv=pJ9tv`Zvil%7iYh-l(%m^H(Rl|nssS=y5 zTrqB(bsZ9OP0aO~?k^~x^%aUJoFZe+)A)~PI@(1zw;vo@*@^~?7t(@eO0BiW;_*uy z%-#2dxXTbW2EppG(@YqyS|^yA3+_FR+Ivzirey@+FLiwi2`$f*0KdM9e!uecEge8J zQu=&ckAarZP0b|X4OoDzw`=&F7h{U11tcQ=1}DutM*jt%5*{L>j7FhrKoR3V(hj#R zQYAbXxaEP$MGxNj#Vs?@(eMp+se54f;5q=`qM-m#^d>m2)yNOPe){7TFNU8~KK`?H zLW+^vOj4FXDAEWxiVYX_k8;Pl6khRIZ(-5Eye@9YC0yQz@>UT9y&N&Aay@H(ZT=uw zS`7&FX-)>YJs)!0dy1QHC<{JP$r-xyXcX{(k!YQx6E?Y^oEkGTR614E$zN06}jNiDk z`EsYlHeQmp!dyh}CS8jpwi#oS!L7CQjR1r0ZDPG>^r!WRx!tT7%x?~%&5S%? zkv_n~34e6^0Mw@=usLw4-I#Z|B+WdgKa=>Wv0FnifF@XrTQ7hNQ^LhkG@p7tSU8uY zpkRIK!v$zw-&gaHC$#vW@h(Fl_gTITFI z%kO!1wUOM91NknuWzoKhnBQb}_1nR23pcYg`7%UAd{3qbOwY61wG?A@K!d#49DQhn zknq;~+&8q!&wFbcworMRHV}TYa|vIf46~0-ELs^kO@lxNAgE3T2TL16`MWy+xG0$q zP;*0ED&d9&Yaz@_T%mlR|CO0m;(5Csd zpC>C=7Deb=btbp2ITL20x{khm;b#k~A2h27uX+O);w|E8t#<0Zgp5lbs{k$9UX;Hq zljZ~ldS9ab6piRRlYgT36$J_wRtpDyW54|9^%tTB;-$YCK^l0)G_*f^A;ExM#dOfN zstHx{R+l$u``%KxQcB!=;1g>Q&bj0DK;qaFcMBH$kzZrfNX-`XEn2|pF*do&Pd-rh zz0PhGs}~`}!YS{$WPfh%#_GXK*p&({s69qD?ngdDUe~N-97KxdO@8oD_Xx z?$Tn*#_qRy+{v9QK*os(Bh{JXH&f}&b}8!70GO$*Jov+F_$e*iI=hpom6xjeb&~`Z zj1Ink!tw0Tja(PSRbv1C=T7FG=}L-jqmuW!|{eqZTMU_7vRk^=e>C zNo4+A8L!(y5CxQhId?6$i-kt?W@m=;Z`jlW6zcxiSmk7(-x+aoM&rPQ-F$yo5-rrv z)peWiWqF`{@Z&=id)FI-Cr55)8V&v3t+r%GJSG?Aq%6Ox{IDw(aO9D4og|2OY5lH4 zF&e`gJhx9(JC0G`%|Jm*80-=O*$zNh(s363B?3o%5DyZG)Z2sz^~!~n3Z7KP?VnbCBA!npdarrrsD8<2yjY-b3Q0=$Ud{ zu}__G+NHLu6ZfiG$s<4Lv$-g#BZg2`+nnQXYeTpenB~u^bPiqo9>Xy1%IDI zoti|awma7Cs`%FBbln{u#()nxZkMH=j<@ftP6++-XknKOF-;29*H0K{_zgMv@%xnf zU=@z$;W?^sbRyqyulYhA;7++7BF?w8L4S^Kta+k@<>jb*O{arJbAS>C#x+dO342CxhcN<}FpiBsP21jAwi)8LhzJ2XDrI(cHe5D^-n4)i{Pu1|dxO5F4 zE@9SBStdwiLyYu#On=+eQ!6A?ItCnuyk+|mkllIjtx|Z>OK33-gXNCj%ngZxO-;W z@QWHRG}5@YzsNn&+R+ygkHIq(b&4g_oUr8SeEo~OAz5$RdYddpm1^C57F*3MIow%6 ztEgUJUbakG_p)3*jYb_?Et&_^^_Jha0D3%G;o%b=jjV__{g?6_%!P7Et6Is_SEn27 zfLrHzQaAbn2dUrS(48$(Cn-AlSzT(v2{ne$$KA#1>CPJ`2&(Zbi(;}^{*(T4mFP61Uis!LvA&E^sMHzpx~k^JIZUP{q8czQFdgVH^aMJR0R93D3@S{44LwDpaU`z~ zUZ?s?|8lqDZITkN)G?&`bXVNj`}^@$8)g`u$p|KN8>Aem!=$_;RgkfA z`Y;(w9X>qbTIPQg(ax$+yl}9&utf$sZ*W;IYFXd-rr(S?-U3=Xsh-^Y z0bhOFm$Db7J|kQ6{HXhk?OUGbL$6h)a7TE=5vsQWgj%aR0% zKigb4Qv|(Hj7zUCLgMlhcXc)Szt2pq*(QqQ)m6(Urw9C`jA;9KhUCr$8jXw&*T5OF zuD1Uw5zBPr_JPZLXYYjZYb+PIwcef;ZdnhrVnE8h5cCQrfgmG=5v6Ot*k5xoTtY(I z4=fT^NWFb@?gTOqG}vM8eD3pgvQ9+-`8IN3a4;C3-Vo$=A}Tfpi`)j1Z$00)$i=~kyXT}WRYM|O~JrUq{10oC#?_)|&egCKsls z2&0-U>F1meDTq%EIDk_={p#zRbhbOXogFt*KUE(8!=axk_y)LXQmR(w&@<=WeP?7E zp{ALIy_%Ymymk8;bW+zUFqJ4|9oUxty)`g#fa#}D$EBm5ItnwtaNwnr@4~ml_OUo|VBA35h4_mrIof4By}vy&<2ZQQIm3H7(WpC_jo0N1TKB5eL%hsyNay?z zU#^uns)>WeP#qy`xf8nQ|oG)@^)X;4Wp#o)JxxmOQ$2LtQ+76MzH(}P!G~D z{ni3y;aB+{AC>%NXX=z_sHYm>nJNeCmD;;1oWFZdV<4wQMJ%z~n-cu|7}j54Y5Pz& z20jzr3i=tcGWavObC|SKACV(mKeZ~8>?GVLjKkow;hnKHV62VFm;IKM)p3!^Y{|9y zQJKCwlkTA(HqID)vk&HpbHyX9h}3~W1o@aIlZ;9Nd^As>0E7SaJqv{x?okay<7T)( zOe|V}1G*dnRcJb&8^kwbk%H+XKl(@u_OmgCrEXG@$5Y@9tDMa z)-n{!r#*Z~JuqQ&H^>7esBW_OHqsJ}*0dMih_YCxkJ+OmGlj!xzaJye0lN8(Lf=mc z(J0pWQwzuqnMTJ<(NL{E({a;Gafog|Qsba9$C62}d`t;&%y|2G4>Rkkb5u(8u@O`t zUhWLAgYa$XGp(PZQK@GzzGqggQQXJLos_bu#LgYBqK0Dn)@vaInxrwayMjc7fkCyv zCMw+noB4nm%C}Md+66=#V*RK92oWLD!3y+nRdt$CLFaI=@4a?W&lNs9in{Y-%$b76 zJ<{vYl)3p>fcDon&Y8l?Fc_5tpkV>8-mXe*2>1E z(kQjhsZLY;+HaALLANT*o!iRl-zfrcCsdknvpKJ5$1OMghOKeNC$AEqqWPKYA@)Rv z6(YJdIB(5oWU{kP`pOD?_NcG-TTrkPGtCsb=OD$kyca1c9)bylb(;&8;;CKbv2?vW z;g4L@N{ybnSMCC-J^#Mp~h#I!0T{4+A!RF=x-6Wt(k76M}4xF`ib~_8+^PcX6>C^%IMIlLiF^l;NMUfehHW}tueXX z6F#%stzrVawtFO+E3XGI{9BW8w`xNE;9c!ims%6$ol~QERxkIc_1ik%h`0pog-suB zO$SssOPA94k15xz#K+_1Cs%n#u&Em%I6z0V=rR8+Gw{?NrHx)5j~LVhVQH)SWg^Et z$nF$JTou}>s+XEt;20xDLII7aY>h-bed;?zotclD3h3pX`h1`2;>zA3)_*_qIKFjk znp91#Vd|5Wh1quSGP>m_jl&C+ICW974%i2dKSJSKn{*67m=EvqFO^hWqbad3+Tj-5zU5JpyQ`e)55|M((3-})Y{Y`Ufo zFWi;PY7{W|BYEsxelyuMG_|FUr$nN5oy;sewNm2leZKNK^Hd|q+)v3VO_GcPWWi{^ z>3CDrZ2?KPp>6bPz~GsjxfPKGXWM64off8c@ABU)vIB?sskTIqZHMxFni6h15~?f4 zL~O&QLh9T~ds>?Cw|j!P>q|m{{qHGhjmv69wt{}qPe6Dfi4Q4Q%hVV_HGTioLp+xB zU6cBQgu32oD)X{mw`ZKxMx+Rk+zltG+i@?h^l^;H4sRw!wn=!)s_XsHHoJwE7gk4e zDCbV&g?0DN$DOD2fsAwi1FZI!uxsoYLY&+PimrC&~4TvTbAmE$yavwH5(R!mDpieAaS2T@UTNWMudGK`X&`I}HtN@Xbl z^4XrCHjAZRREH<(2eh}o2Y(nI1Nz0_bt*C?J1hiO+2)R~y8Gteqg%WY1a*(nR;MOL z=uJ>xI>Ot%X#s!<<=eaq!Q|(!#_?@jLaxsR*u!25)Z6W8) zyL(-~Blb1rY_kbhZ-ydGpwe~us6Z>D-!isuN-FGrG8%r{4Pe8oX0&2M|4Qi$s3-3F zH&Y=W;R~B#!Olesw?gydc_rgUn*;?r95!_a>bm(l$6FqQT5(I_TYbYy?QjGWFt2=> z;jwz;_UJpXr{H7CTbOXP93rN1czN#0zRDqSTwKS_-;G@Thc>R7rRo6^^3}biWquag zs0uHFcBakyjvq>0|7_R5yQjwhG+`HLegkUbIxle1f=@NV?#IuOu&EkVr(&a7a60Vm z4@u#oAA_I{)lRnQ)*~z^_!w5x&nUF zkV5mA+&w0B<3WJEu>;|{5uLKbGkf@2bMcuJ%)}MurZ4bI*jK0>lo8N|Ju1_%uHC5_ zv~8<+8=dALz2(I>q4idPY+9d#SyNoyNe3`}weZ_5$@UN=nkL=JK5mk8RRun+mM0?jfLv4VRgw2)FnSf5&Yx$KKyzkqYpdF;dQ&rYozb$p@c z@e$9>u63a_w9ecnEfLbOWR8$ZrVE(n#6IUj$gXAvCmn`EfZCngrhH#4G+`=wg9-4l zP?kWAJ2VcZ@SmYj>~KmtDnWC(TQjdaKML0fddiG$TVK{i`zQOX7J~_CQF_c-8s%zB zR#>K!kE+OJqmgw%?+5S0v!WtFW$SrVJW>~ za(B2{;Ywml)^J1ab5VO_X%NGf_rFM=Ccjwxb@adOT=otcyk|AE)llpPU_hyOKea(( zMP!8me}P`Ig0J%`oo^-Rdj9+TPBHos8_KYaw;X=MFL5{niJJVFbb8Ar%RF)sR<2ot z0Oii@!8N5$5J`CsUZn$(gCH{*PLBQ#tVnF6yNlo?Mk%j07bl1ZW7bY8U(nUB1&&7) zJoURHpnqWZu0r}otv6?3UgU#aTG?cqNfcX#ynds@AnN*xexD^wI=CGQJN{M!Hx>lX zCEJALYg?*6GoZP@8iPxa3>n%)mJitQl@tjHl2im&Sng7H#YjeSZ19U)fN&{c$| z1KkQ3SZE~{Oz5}iMb-AiU7_s5>$Xi4R(Qpy+5_h0(tKUWP7NYslvH@RNPgXf&~=~; zIU`e-Fjf{-vF=%QO9k1>oaz&?}aV4 zM^rGQtrz}U180HjhoVCa#ghl--j)s9QgO5^Z-;qPt!4|iylvnbo^uf_2Qv*x!Ee|y zK?{s4E9&I4fCNRx)%$^1Hf5~9Us3SPlAwum`C^6mGxlc9PjA|Th>j#(ocqz(xbKp> z{!?R^BfdPI)acw1w~%?d`%Gmid$-B?al4X+LI7)~K?z;RqB(>ORQFq{x71TBCgmG9 z&Yy8cLJhc|7EBH|yuKL8>wMYQ4fN?wrwZ&@L_Q@4C8}y(3=TqEAC4t0lg7T$%g{bV zIL<{SaSM9Ocpy$v0jqk0dsWc;Tk$dq9PO&;=3|ekS|{hXL&U`XyA&}y;l z+-&aT9O;ITROPTBz7mN`u>{N2W+4HxhNgLPjv<)=2+ zA58U_RV&Kl$x{9sQ|SuK&4jlyAuLY)p;A?C2m*{+;D<}WQR1sfpZhpo=X{ndZA{=; z3bsF9l%`w>&Kt9+H)MGWtpsqVE?Kvn+lZD4ozgof2fAgkgqXyggU|A1FoBv$3~OSbxhz8(07AYC(9PLUQwSw!l%QkX z(AYC%??yc5Sd~ku9cFn)zv6{Y{Ie-Jl7~`Zdql;^kLpqRKEQJ)V_VOJP;P6;$ydsU z+;h+j!q4|o``1mzW6Y4@9SSxd5=^hWDQ0I);+Do>dt}o7%=L`q7aTJj^ab~AW@E|f zKq*n@odF-j7m3Wj@jNcRmn$sj21nF&cvqlt&5dh>o|X5YCNlC-DCBxQwSX+^*QqpR zX;wE9Dwzoh3G>7}-ff#qN!hwX-$aaUHoB;U?K?c&G;x$0@FAYXF}22LZQfD9k#Y0X z@0jqd2sOS$Crm6`df0mx?)b9^8`vskpTw%%zo{@%xqPrg(x#=f$hBVBOJEs7S^qxy zN?j3_6v#u2zqk%wUA%peG7?WS<=Vrfj|`0r{zXgwrIzXzgC1c07)@Jrc z0--?*_ADV9z6yAz>yLR?>GP;xpXT$<&~Xp>#?{uBmoIL4KP0U5G-^5zJR}eaH3pl? zBfsVe>aCRE94BK3eK}(9x@t5U`JtY%d0%g*`o$l=f2ZXrQ`&7itt2Kc6;lJAy*%R( zLc`E6UH6O=!9fdvgVwf!pZO6s%$t`nj+;`FADi+8f}0i5ny$miAXcZLNXG=m{!oO` zI42W)U6`Y;Ab=d$Jt!1-v%>79=xy|RYR>(nVNcU%0MPWdXX@yM`h1$?q+YHL7mZ?l zyE{YhJKte+OgZJ7J??Q~iy7(QSxt!$_Y7M09TI{Nl5p_=u$bEFq?UT#Qz08jJVMh# zz0Z_hQ6bWujwO}+W==p`$)6LGgFmjCQ6L&o#f?eR4n zAKlQhP0bF_%^Z`y9@j%9wswxZQM`Ws#u}B65@NtE(WQ%nb(;%aLq_`z0Uq>+Ef-05TKm;u;Kld-sb2Gxhr@0PG#-s*FDyUKN6f-#b;TxAZkX+sWpSmW$gZ;iSly zuSvl9;f5{KqLuP?+YP(ePF$c0U8?vBQXlU~Y$&d5t$2#$#}3;IZWWcIa8U(qZjN}? zf*^-4vFYVpz0z~#b|)B?sD1luVyO_(Yq`fCm&WWjr7h%Zp+LO|=(#$%%94tqdQb7M zLhc1U-g_`Rc^yg2G$o749|nE1cT-_g_A+gKOWKmDQ=c~HkqvjW|{KkhgTuR$7z zBOfKoSbu9;*d@yrXyf8 zve!38q7$rMbEziA`o&NAgasTkZi2cA&NDf4wQF4eRJZkotLx>>#1@797>k@o-RuB8 z)x@-?@Bx0@(oN?vZEHRs>OFZ-7Ah4$oN%Fj1vp10^_j?T8&=WPMz~3YQc?}y0ud2M zGCFy*-f^k}CvNpsU#0rtaIR5p#3Q0seGoN@#Um;$Ne=VIILIvS@fAaWeo1IHE-NWy zGutNY*)NHxHG3ZO9ktI!DGK^+nfZ!RRy6qA`0iQ=?7nvH_3kBon#s@yTodP!+Ajw! zcl1<2C50OO{u#6h)PqMl!y#l!w_W?$3B{`b97nCyLr_CH)s#UPR}L@iyQkhAaazlC zwD;%DH!>v+xQ2rqo8Cg4$gn~BDZk~FA&Ak-K;}e>%!rWCGLgw-F#MQJtQrG(kmwag zUWsAyEv*fg_N)#}N&&xw+K30G3e@jsyHsnBCo|w*qLLP>f$z8%)tOMg%u?0c*kmjY zr)H0ibSQ2i9$lbDq$Kk8Ay0Mj{yqf$Uam_rb&aY-{zWv35`)^!tsn6o+!$-XFV8Lh z$z$COV_^WHA(74QOfKlsc#VuZ7yeqPXQdJOL%!4Cg|R~V7PFAw!z)X{;!RQW%DbKI+n$}4XO~p7U=}=g49z3REO{pu z>tuoJ-D$qT33c@be_Zu#m_xpUH+894TL?|F7dW}XDV#e8Jsf4*qtJB3b-aGc_q~l( zIX*diOwAiNS8=z5G~kiCQwM{LrxR|j@3y$jWDEEJe{MDiMx)#ua1#++Mw%jJ(vUiU z8_DAd>dF;bA2Az=QUDKKEpRE!j&9WyhR6#pYH+tKjbVe$@yv*L{^EW7y))@Eaimy^ zeWgbN=fU*-9v+VqQtqV>1SH#*-Zr~jvoY>(K|UpwM;iOk_Am$zYS;ShX$$ZUQoJ<< zU(9*7vX8`Bqyldo;}K!S&~~ksW$iO%S^paJWtrA->yK&FQE*rscvJV=##K7^W@bGS zx<+im6%M=SMJD)YIEyh~sqd_)l2Q|Rrrr-5kngh;5~QtUWI`Pf$4&Bde zsX(XL1#WO*U)uhMd;cViDOo?QgL75CyH4q32#{YTu}}FVU&D9}DtYuIAU#E+w1;$6 zRne&*CdfGGke6}K3+P1rGtSfPPh62WY>J&R!SDw|5o`XK=WT+A!)(*>czOKJ33(@p zeI363=DB_&BJfEQ@r88pfskV-Yaz}sJc4=k4zre^gXI!y(`abKtV$xD{1l+={Zp5b5!rBHm?Rq#hTvODT_k(ZeJP^1w$QIj? zP~MB<^Xy=-w2lTI)oZlwoMdj)kQOn9)=LQswZ82+G-)#q?ENXKwV?=$kB9JW;L67B zc)H1NUK^0iez)2)Pu3@SR2G})39wPXJ$2ze{Au~CoNk#l&KC#5FXa6lbzRALdumWY z2QtiO+#`%#HT>>~ke7^#Im(|mE=5a|0ejkHSiMA1VpQTQ9=l)OLW^^|LTyWvFgh!n zLd-N%3j_>_Tgws@ltop}s}?>}O^VNi@XG#dPxGYFabQYbYVlYmd(ph}-JHtm*5E|_ zzC5BW@~EfpoqqjZkUba#mCEYvfB?MAq%0SVMX_w?8kj#2FtZ>I{^F^}p@g?NKf2Ys zFqoN}6JLk@s3m&1fQB3Cf%D!V7Rc|isr}bLC)MYh%MSh!L*FgTok#KjZ4bFBu@gUI zMg*7LtYPLZvJZVslW72V0XnI-Vd1~GDP%2m1wc=+&QlCcyQE-YUcAgxEBhCn&rl?W zTs3oY+Tb_y-BvbH87QS;A>5MMF|v|7;O(ALHf4G%i-vd`q4ZQfV~r2U<$hf8Grf|Z z5J>d#gz>EhK^-DS%UZyZ)WE1i3wbqxATK~N%ak}IhA8}G->n+}+sjz1Mv=a} zihDP*ywd_j#^oxaeE?gZhOF=I9P=~z=fE2}pS}l2ac)0ds@&NpXpt{8{%KPlzBMfI zwVhh?t)ZOv`F-O3>fcvc${Bixqm>qNCqA?-aKR+lp@M<=&BdqZ-DHe`KL*#QW1T4Z zNJL?Am!n{y*K)OCZ^s^)|ExQJytWHWsIZDs0jTX4zyn}v`tku*)q4~FBl4af*-WX@n zyi&ppYFu1SKkrIaatA#>*N+AU@JjHVU#^9;9PR6SrhXG^$)-T%#&u#OPN|~&hm6nc zRuytX%yP<34H;)qfW81uc?!K;q`qY^GlsUbZs%=5oqk5_*L1*Vt|RozAY7*HL{l!H z1B!g<3qL4nE|8%(M=-tfcO-|x9BUTvh*^>WKbFHeYE73PEIf76eu+Ih3ct8SU(XJl zg1`Mx{u`2)m@L#w$(mQx1buOg!odw4rp-US4)9PaXx}GmFvVG=^I(jAQWdETqViw9R8ui`mX9 zB#e1gJdMNwG7ru0N_Jn0n6;81-j83LvYbnDpBCoSR^F6Kvc5HTU;PFTTz13kPN8Qq zdT$Pp&$$F6+CMzNK@RbRJH+4II?r^tB|&!aOi0%p3M?*BECVhpu@298;9g zzzX&gxP>?#9+!VP@FRKNg)Q7f+1ZdH65#^EO^vQ8<$`#dBi}IDZi679<0ciX8nW6i z-^M(qB!}`Oo?Zomv;DPKH5jV2XDHmV*7Kj&qnqY#C2{AB*Z+RH$Rom2R8O!>$oKE$ zokVHK$QW}@6j}4^fV_MueugvX#r|wCCa3XvU%=)%dOLmf>`YoDgM zpvttba_nID7C=UeSWSq8pXW_^8BiLu%6EH86!x%vegj8~yo-j2cmQLa#+}&6H2>Tz z?(vtAiNeOgx7(qFfnUxxMZ_rviu((p%i}VI8o)$-?x)Pvg|K+z)nzPE{kY~%7{i$= z$o^b7^-QGMOkd!6k@_ztvGlCjt+)+DRHY?+cVNE%(@hKdiZ!)!@JkAXJf=K?rS zrz}CYVX)oj)h>hcps^DSv8nnJpWZ5pFzfhQBOXEM5pe`%ux~We7!KzB(R**gi*H^_~Cdxc_;_M=L`_9XnnHZr|8?1%<`u(s%JJjI1v# zEB2v`KIvqn)IdVR4-zl+P*!V?+rpM2CRjS_+bLzrSx*_-^HW?RTWv^|s-b*x2h?wF z)@UBX8V`ohY_$HD+3K}Gy#{yVEJO%v&V3RCCe{W)22SvgdMP&xJVkQsJ9Oj_t;lUL zM?9+qw1kXnFxXl3-XY9wdHG(w==a(*DYQ;V{su)#lQ=Y@v9-e>@P_h8Nz#?^A4Ca= zF{Y5y$sGbg={t*V`4RoRK-)g>LayFKf8blkKbp9 zbylJ$D{!k$VQOf&K9YnsL@LS%hIS?_y#K!W6cv`hycsre7hcscYA}T9GO4$-eeTiK zeU^kKt0Bs?U9)G}%?z3!m`2yHh=&O_4P0C6KXcJPTfQbpYP0gVrD6b(7$r6xv8J?X z)>*u~6JJ##S=T%L4yvqHfJao&eHG*ov*Mdc4+jIt)G0#`RTft=65(_a2sBu%F{#At8m;dG$QALVX=iAdHiP{|N{rOBMdr620ALl^LNYE(Z= z|18ZlcbTUUi9v_!KSj$cK zn>V-X} z3{B&tZ?-c{6p%6clF5>N1xCqcwdM@@g+WCr?FUCM(K`ZoLx7*&mjl$6>Cx;)&L*LGWtxV>Xt zcfaZf%Q|~&;GV+dv+?mK$xg{9h6bUz?3noPej824MkU{mKlCRgO}2Y8n>HD4A+>ZG zv4Y|OGqlY&ukFi?D<^W{DPOX}MrzNxfU_>x7m<`9=W5pC3^QV5!mp`C+HjvDk(X-( zcF0jH{Z2JShj(>@wDPt>uU&d_m)tYDaq9#?ws;Ka7R`KnD*JAC=~?1H55(? z%>#-QM*&^%BTCc@N+<~n@9&C9e?Emn7fgvmX=q95VnjHRr+$YX0OnCilw|1{!TC?4 zgPgJIPLC-bA^<`|=QC_2yf|Lyh%Y{BGLbp=?`oV~#lfdQnXu+r)dl={m)47H+LHbq zLn7k#F-BC+`xh|0DtI{}n947S5`ud2e-VVW+W@KwEcfHsrk+`EL@8U}nqGg+b;*;F@h1IvA{QOGb7WC+W@e2e16-apd zz6WV-!d$;XnyFUKcK%r!bnC4SnJW47Z3?-#zyBg*?#Cshd!va*e4lI61)zmLOM{qU@~dqCR*-~SHMcNNh@rYQ6Hfpx z5B4>y+0eG>(6FmH>lR^ZZK-ye$&|KPxUPdMGT1Mh=RNE};|v4eeD}2lH?(r|9Oo#g z3=u;n8L-i>rCmjC1e-shJ1C3$ej`kJ-%eim;7BxM<-B|uu!ij7xb>-OC!BL@YkE8; zuD&S-S86tT%Po{HJ1&>aotE%T_l9Y#LOW__x82+@pEgh$EHMWExt&=KwT=BT7m!_@+asyrN>hq z7|+5_`4*6*oRELss5zt~AS$ep677JuFB@y%B9m);dzKUPOO5LzOhs_P%sAWj(|DLp z@_Y5vC`rE+DJ50j#lwLcZG1gf$D?n&$64@lDBReG4AbbTK{s<(H`w^SaJQ?eJ7NQT z;d1zgx;32$TVTL^$LPm*?IBSC`PXQqT=?W{+H}41?w}zly=dXNBigh?Ks}vqUfc5G zBH}k0aWkdkDe&nhy!;=8l7jZEPPbp-caI-^Nq=*Ra~&PyXFz{|0pSC=esD6)S7w6HttB+T2-LH= zlXr|~FJ2K%m8om>m_}IfbC+?XLECE8>77ZJ|6^li`nX@vOuFNRK}4iQA8;nSB3+J8 zn%09!>*S)OCmz+$&2EKe!r(~uc@9%JI_6M#Udi*i_G(=$b*^QTEfJ1KKHl~@3l%-O znFU8QNiJESDVmfgM$(W_&HiX(@3k+)d|8ppbRiqezRepKUi0G1Ry}XsK2son{MBkm zQw_(!JC(q3Pph@kcD5PhL+Jzy4ODO>?Q^rDY7smBTD;6!ZfWC?`HtU%itjTAbsRs- zea7nQCG_vm1*Xzfa=ZpeV)Ri2efFh*oS5uae&15@2k)}&9GjE3i+Fz}o0XbMe2(X* zqt4hAEUc`s_YidS#{xv26D|!zr>^}4r^8#TDbQzb#w)~6HUvB)OWw{F7Qpx0^S=dV z4O5T=Z8xdQ8qRvHH8{aGMa7zQ1xdS-oll0`5&I-*o6vpm_B(Em*m@D_hD14=Pmp2-qjG<)<)KeQ%R7Q7Bl>RxVk(bO7 zKl<*sGz#?6t?Q^cKe;`SVta5%Ag_S)q)n-Qa$S0yI#4Hl{MqR=eKxPBoeO& z&*Ez~aV3g=7WKQY#Xa|*;b!gG(TsrWq|%V1c(};D{#ErPutFIKTW{sG zV03D6+>123y%^N~)abYSv(6oP>&yE3Zf{P5OUFNVACaxmRG*ilxj7dNXj{b|m6Fy? z%yHnyz{m9&v0f?C$j6VGFrf@Sp#g6|H~%=fC~^-~QwK@BjY2 zluvyBinyH z|L>7oyK4FSqy7EA{Eh!s{$tR;&wmidVEC{6|L@TL;a`XO&+3a`Kh6L6ch4EmfAhZNb})2==Fd1^?&_{)-P3>|6hN`kEiFIi~p4m z{nP#bKm3WwwOaDOJxA64os|`=>tu@DEuv-GAz*^Pm3Z@X~*H|Chgc zI(h%+UjFGhxAh-x#98v`U%$M6@4x@u=<~NY%iJ#Q?<>nQ?gpiqoBG!Ow}1ZE3;U;k zgP{NNtACr7Q;G{8h=nzli5D{_{`$%T@i`W&Fdx<=j90g=2rUzuI5z zul85_tNqpfYJauA+F$Lj_E-C>{nh?zf3?5bU+u5 * - * Copyright (C) 2013, Kolab Systems AG + * Copyright (C) 2014, 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 @@ -22,19 +22,12 @@ */ use \Sabre\VObject; - -// load Sabre\VObject classes -if (!class_exists('\Sabre\VObject\Reader')) { - require_once __DIR__ . '/lib/Sabre/VObject/includes.php'; -} +use \Sabre\VObject\DateTimeParser; /** * Class to parse and build vCalendar (iCalendar) files * - * Uses the SabreTooth VObject library, version 2.1. - * - * Download from https://github.com/fruux/sabre-vobject/archive/2.1.0.zip - * and place the lib files in this plugin's lib directory + * Uses the Sabre VObject library, version 3.x. * */ class libvcalendar implements Iterator @@ -396,6 +389,8 @@ class libvcalendar implements Iterator if (!($prop instanceof VObject\Property)) continue; + $value = strval($prop); + switch ($prop->name) { case 'DTSTART': case 'DTEND': @@ -405,31 +400,30 @@ class libvcalendar implements Iterator break; case 'TRANSP': - $event['free_busy'] = $prop->value == 'TRANSPARENT' ? 'free' : 'busy'; + $event['free_busy'] = strval($prop) == 'TRANSPARENT' ? 'free' : 'busy'; break; case 'STATUS': - if ($prop->value == 'TENTATIVE') + if ($value == 'TENTATIVE') $event['free_busy'] = 'tentative'; - else if ($prop->value == 'CANCELLED') + else if ($value == 'CANCELLED') $event['cancelled'] = true; - else if ($prop->value == 'COMPLETED') + else if ($value == 'COMPLETED') $event['complete'] = 100; - $event['status'] = strval($prop->value); + $event['status'] = $value; break; case 'PRIORITY': - if (is_numeric($prop->value)) - $event['priority'] = $prop->value; + if (is_numeric($value)) + $event['priority'] = $value; break; case 'RRULE': $params = is_array($event['recurrence']) ? $event['recurrence'] : array(); // parse recurrence rule attributes - foreach (explode(';', $prop->value) as $par) { - list($k, $v) = explode('=', $par); - $params[$k] = $v; + foreach ($prop->getParts() as $k => $v) { + $params[strtoupper($k)] = $v; } if ($params['UNTIL']) $params['UNTIL'] = date_create($params['UNTIL']); @@ -440,13 +434,17 @@ class libvcalendar implements Iterator break; case 'EXDATE': - if (!empty($prop->value)) - $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], self::convert_datetime($prop, true)); + if (!empty($value)) { + $exdates = array_map(function($_) { return is_array($_) ? $_[0] : $_; }, self::convert_datetime($prop, true)); + $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], $exdates); + } break; case 'RDATE': - if (!empty($prop->value)) - $event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], self::convert_datetime($prop, true)); + if (!empty($value)) { + $rdates = array_map(function($_) { return is_array($_) ? $_[0] : $_; }, self::convert_datetime($prop, true)); + $event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], $rdates); + } break; case 'RECURRENCE-ID': @@ -455,16 +453,16 @@ class libvcalendar implements Iterator case 'RELATED-TO': if ($prop->offsetGet('RELTYPE') == 'PARENT') { - $event['parent_id'] = $prop->value; + $event['parent_id'] = $value; } break; case 'SEQUENCE': - $event['sequence'] = intval($prop->value); + $event['sequence'] = intval($value); break; case 'PERCENT-COMPLETE': - $event['complete'] = intval($prop->value); + $event['complete'] = intval($value); break; case 'LOCATION': @@ -481,27 +479,28 @@ class libvcalendar implements Iterator case 'CLASS': case 'X-CALENDARSERVER-ACCESS': - $event['sensitivity'] = strtolower($prop->value); + $event['sensitivity'] = strtolower($value); break; case 'X-MICROSOFT-CDO-BUSYSTATUS': - if ($prop->value == 'OOF') + if ($value == 'OOF') $event['free_busy'] = 'outofoffice'; - else if (in_array($prop->value, array('FREE', 'BUSY', 'TENTATIVE'))) - $event['free_busy'] = strtolower($prop->value); + else if (in_array($value, array('FREE', 'BUSY', 'TENTATIVE'))) + $event['free_busy'] = strtolower($value); break; case 'ATTENDEE': case 'ORGANIZER': $params = array(); - foreach ($prop->parameters as $param) { - switch ($param->name) { - case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break; - default: $params[$param->name] = $param->value; break; + foreach ($prop->parameters() as $pname => $pvalue) { + switch ($pname) { + case 'RSVP': $params[$pname] = strtolower($pvalue) == 'true'; break; + case 'CN': $params[$pname] = self::unescape($pvalue); break; + default: $params[$pname] = strval($pvalue); break; } } $attendee = self::map_keys($params, array_flip($this->attendee_keymap)); - $attendee['email'] = preg_replace('/^mailto:/i', '', $prop->value); + $attendee['email'] = preg_replace('/^mailto:/i', '', $value); if ($prop->name == 'ORGANIZER') { $attendee['role'] = 'ORGANIZER'; @@ -515,20 +514,20 @@ class libvcalendar implements Iterator case 'ATTACH': $params = self::parameters_array($prop); - if (substr($prop->value, 0, 4) == 'http' && !strpos($prop->value, ':attachment:')) { - $event['links'][] = $prop->value; + if (substr($value, 0, 4) == 'http' && !strpos($value, ':attachment:')) { + $event['links'][] = $value; } - else if (strlen($prop->value) && strtoupper($params['VALUE']) == 'BINARY') { + else if (strlen($value) && strtoupper($params['VALUE']) == 'BINARY') { $attachment = self::map_keys($params, array('FMTTYPE' => 'mimetype', 'X-LABEL' => 'name')); - $attachment['data'] = base64_decode($prop->value); - $attachment['size'] = strlen($attachment['data']); + $attachment['data'] = $value; + $attachment['size'] = strlen($value); $event['attachments'][] = $attachment; } break; default: if (substr($prop->name, 0, 2) == 'X-') - $event['x-custom'][] = array($prop->name, strval($prop->value)); + $event['x-custom'][] = array($prop->name, strval($value)); break; } } @@ -580,20 +579,22 @@ class libvcalendar implements Iterator $alarm = array(); foreach ($valarm->children as $prop) { + $value = strval($prop); + switch ($prop->name) { case 'TRIGGER': - foreach ($prop->parameters as $param) { - if ($param->name == 'VALUE' && $param->value == 'DATE-TIME') { + foreach ($prop->parameters() as $pname => $pvalue) { + if ($pname == 'VALUE' && $pvalue == 'DATE-TIME') { $trigger = '@' . $prop->getDateTime()->format('U'); $alarm['trigger'] = $prop->getDateTime(); } } - if (!$trigger && ($values = libcalendaring::parse_alaram_value($prop->value))) { + if (!$trigger && ($values = libcalendaring::parse_alaram_value($value))) { $trigger = $values[2]; } if (!$alarm['trigger']) { - $alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $prop->value), 'T'); + $alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $value), 'T'); // if all 0-values have been stripped, assume 'at time' if ($alarm['trigger'] == 'P') $alarm['trigger'] = 'PT0S'; @@ -601,7 +602,7 @@ class libvcalendar implements Iterator break; case 'ACTION': - $action = $alarm['action'] = strtoupper($prop->value); + $action = $alarm['action'] = strtoupper($value); break; case 'SUMMARY': @@ -611,18 +612,18 @@ class libvcalendar implements Iterator break; case 'REPEAT': - $alarm['repeat'] = intval($prop->value); + $alarm['repeat'] = intval($value); break; case 'ATTENDEE': - $alarm['attendees'][] = preg_replace('/^mailto:/i', '', $prop->value); + $alarm['attendees'][] = preg_replace('/^mailto:/i', '', $value); break; case 'ATTACH': $params = self::parameters_array($prop); - if (strlen($prop->value) && (preg_match('/^[a-z]+:/', $prop->value) || strtoupper($params['VALUE']) == 'URI')) { + if (strlen($value) && (preg_match('/^[a-z]+:/', $value) || strtoupper($params['VALUE']) == 'URI')) { // we only support URI-type of attachments here - $alarm['uri'] = $prop->value; + $alarm['uri'] = $value; } break; } @@ -674,6 +675,8 @@ class libvcalendar implements Iterator if (!($prop instanceof VObject\Property)) continue; + $value = strval($prop); + switch ($prop->name) { case 'CREATED': case 'LAST-MODIFIED': @@ -681,20 +684,20 @@ class libvcalendar implements Iterator case 'DTSTART': case 'DTEND': $propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed'); - $this->freebusy[$propmap[$prop->name]] = self::convert_datetime($prop); + $this->freebusy[$propmap[$prop->name]] = self::convert_datetime($prop); break; case 'ORGANIZER': - $this->freebusy['organizer'] = preg_replace('/^mailto:/i', '', $prop->value); + $this->freebusy['organizer'] = preg_replace('/^mailto:/i', '', $value); break; case 'FREEBUSY': // The freebusy component can hold more than 1 value, separated by commas. - $periods = explode(',', $prop->value); + $periods = explode(',', $value); $fbtype = strval($prop['FBTYPE']) ?: 'BUSY'; // skip dupes - if ($seen[$prop->value.':'.$fbtype]++) + if ($seen[$value.':'.$fbtype]++) continue; foreach ($periods as $period) { @@ -703,8 +706,8 @@ class libvcalendar implements Iterator // duration (relative) value. list($busyStart, $busyEnd) = explode('/', $period); - $busyStart = VObject\DateTimeParser::parse($busyStart); - $busyEnd = VObject\DateTimeParser::parse($busyEnd); + $busyStart = DateTimeParser::parse($busyStart); + $busyEnd = DateTimeParser::parse($busyEnd); if ($busyEnd instanceof \DateInterval) { $tmp = clone $busyStart; $tmp->add($busyEnd); @@ -717,7 +720,7 @@ class libvcalendar implements Iterator break; case 'COMMENT': - $this->freebusy['comment'] = $prop->value; + $this->freebusy['comment'] = $value; } } @@ -729,7 +732,15 @@ class libvcalendar implements Iterator */ public static function convert_string($prop) { - return str_replace('\,', ',', strval($prop->value)); + return strval($prop); + } + + /** + * + */ + public static function unescape($prop) + { + return str_replace('\,', ',', strval($prop)); } /** @@ -740,44 +751,47 @@ class libvcalendar implements Iterator if (empty($prop)) { return $as_array ? array() : null; } - else if ($prop instanceof VObject\Property\MultiDateTime) { + + else if ($prop instanceof VObject\Property\iCalendar\DateTime) { + if (count($prop->getDateTimes()) > 1) { + $dt = array(); + $dateonly = !$prop->hasTime(); + foreach ($prop->getDateTimes() as $item) { + $item->_dateonly = $dateonly; + $dt[] = $item; + } + } + else { + $dt = $prop->getDateTime(); + if (!$prop->hasTime()) { + $dt->_dateonly = true; + } + } + } + else if ($prop instanceof VObject\Property\iCalendar\Period) { $dt = array(); - $dateonly = ($prop->getDateType() & VObject\Property\DateTime::DATE); - foreach ($prop->getDateTimes() as $item) { - $item->_dateonly = $dateonly; - $dt[] = $item; - } - } - else if ($prop instanceof VObject\Property\DateTime) { - $dt = $prop->getDateTime(); - if ($prop->getDateType() & VObject\Property\DateTime::DATE) { - $dt->_dateonly = true; - } - } - else if ($prop instanceof VObject\Property && ($prop['VALUE'] == 'DATE' || $prop['VALUE'] == 'DATE-TIME')) { - try { - list($type, $dt) = VObject\Property\DateTime::parseData($prop->value, $prop); - $dt->_dateonly = ($type & VObject\Property\DateTime::DATE); - } - catch (Exception $e) { - // ignore date parse errors - } - } - else if ($prop instanceof VObject\Property && $prop['VALUE'] == 'PERIOD') { - $dt = array(); - foreach(explode(',', $prop->value) as $val) { + foreach ($prop->getParts() as $val) { try { list($start, $end) = explode('/', $val); - list($type, $item) = VObject\Property\DateTime::parseData($start, $prop); - $item->_dateonly = ($type & VObject\Property\DateTime::DATE); - $dt[] = $item; + $start = DateTimeParser::parseDateTime($start); + + // This is a duration value. + if ($end[0] === 'P') { + $dur = DateTimeParser::parseDuration($end); + $end = clone $start; + $end->add($dur); + } + else { + $end = DateTimeParser::parseDateTime($end); + } + $dt[] = array($start, $end); } catch (Exception $e) { // ignore single date parse errors } } } - else if ($prop instanceof DateTime) { + else if ($prop instanceof \DateTime) { $dt = $prop; } @@ -793,16 +807,28 @@ class libvcalendar implements Iterator /** * Create a Sabre\VObject\Property instance from a PHP DateTime object * - * @param string Property name - * @param object DateTime + * @param object VObject\Document parent node to create property for + * @param string Property name + * @param object DateTime + * @param boolean Set as UTC date + * @param boolean Set as VALUE=DATE property */ - public function datetime_prop($name, $dt, $utc = false, $dateonly = null) + public function datetime_prop($cal, $name, $dt, $utc = false, $dateonly = null) { - $is_utc = $utc || (($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z'))); + if ($utc) { + $dt->setTimeZone(new \DateTimeZone('UTC')); + $is_utc = true; + } + else { + $is_utc = ($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z')); + } $is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly; - $vdt = new VObject\Property\DateTime($name); - $vdt->setDateTime($dt, $is_dateonly ? VObject\Property\DateTime::DATE : - ($is_utc ? VObject\Property\DateTime::UTC : VObject\Property\DateTime::LOCALTZ)); + $vdt = $cal->createProperty($name); + $vdt->setValue($dt); + + if ($is_dateonly) { + $vdt['VALUE'] = 'DATE'; + } // register timezone for VTIMEZONE block if (!$is_utc && !$dateonly && $tz && ($tzname = $tz->getName())) { @@ -838,8 +864,8 @@ class libvcalendar implements Iterator private static function parameters_array($prop) { $params = array(); - foreach ($prop->parameters as $param) { - $params[strtoupper($param->name)] = $param->value; + foreach ($prop->parameters() as $name => $value) { + $params[strtoupper($name)] = $value; } return $params; } @@ -861,10 +887,10 @@ class libvcalendar implements Iterator $this->method = $method; // encapsulate in VCALENDAR container - $vcal = VObject\Component::create('VCALENDAR'); - $vcal->version = '2.0'; - $vcal->prodid = $this->prodid; - $vcal->calscale = 'GREGORIAN'; + $vcal = new VObject\Component\VCalendar(); + $vcal->VERSION = '2.0'; + $vcal->PRODID = $this->prodid; + $vcal->CALSCALE = 'GREGORIAN'; if (!empty($method)) { $vcal->METHOD = $method; @@ -882,7 +908,7 @@ class libvcalendar implements Iterator // include timezone information if ($with_timezones || !empty($method)) { foreach ($this->vtimezones as $tzid => $range) { - $vt = self::get_vtimezone($tzid, $range[0], $range[1]); + $vt = self::get_vtimezone($tzid, $range[0], $range[1], $vcal); if (empty($vt)) { continue; // no timezone information found } @@ -916,12 +942,14 @@ class libvcalendar implements Iterator private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null) { $type = $event['_type'] ?: 'event'; - $ve = VObject\Component::create($this->type_component_map[$type]); - $ve->add('UID', $event['uid']); + + $cal = $vcal ?: new VObject\Component\VCalendar(); + $ve = $cal->create($this->type_component_map[$type]); + $ve->UID = $event['uid']; // set DTSTAMP according to RFC 5545, 3.8.7.2. - $dtstamp = !empty($event['changed']) && !empty($this->method) ? $event['changed'] : new DateTime(); - $ve->add($this->datetime_prop('DTSTAMP', $dtstamp, true)); + $dtstamp = !empty($event['changed']) && !empty($this->method) ? $event['changed'] : new DateTime('now', new \DateTimeZone('UTC')); + $ve->DTSTAMP = $dtstamp; // all-day events end the next day if ($event['allday'] && !empty($event['end'])) { @@ -930,23 +958,24 @@ class libvcalendar implements Iterator $event['end']->_dateonly = true; } if (!empty($event['created'])) - $ve->add($this->datetime_prop('CREATED', $event['created'], true)); + $ve->add($this->datetime_prop($cal, 'CREATED', $event['created'], true)); if (!empty($event['changed'])) - $ve->add($this->datetime_prop('LAST-MODIFIED', $event['changed'], true)); + $ve->add($this->datetime_prop($cal, 'LAST-MODIFIED', $event['changed'], true)); if (!empty($event['start'])) - $ve->add($this->datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday'])); + $ve->add($this->datetime_prop($cal, 'DTSTART', $event['start'], false, (bool)$event['allday'])); if (!empty($event['end'])) - $ve->add($this->datetime_prop('DTEND', $event['end'], false, (bool)$event['allday'])); + $ve->add($this->datetime_prop($cal, 'DTEND', $event['end'], false, (bool)$event['allday'])); if (!empty($event['due'])) - $ve->add($this->datetime_prop('DUE', $event['due'], false)); + $ve->add($this->datetime_prop($cal, 'DUE', $event['due'], false)); - if ($recurrence_id) - $ve->add($recurrence_id); + if ($recurrence_id) { + $ve->add($this->datetime_prop($cal, 'RECURRENCE-ID', $recurrence_id, true)); + } $ve->add('SUMMARY', $event['title']); if ($event['location']) - $ve->add($this->is_apple() ? new vobject_location_property('LOCATION', $event['location']) : new VObject\Property('LOCATION', $event['location'])); + $ve->add($this->is_apple() ? new vobject_location_property($cal, 'LOCATION', $event['location']) : $cal->create('LOCATION', $event['location'])); if ($event['description']) $ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); // normalize line endings @@ -972,21 +1001,20 @@ class libvcalendar implements Iterator $exd = clone $event['start']; $exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j')); $exd->setTimeZone(new \DateTimeZone('UTC')); - $ve->add(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z'))); + $ve->add($this->datetime_prop($cal, 'EXDATE', $exd, true)); } } } // add RDATEs if (!empty($rdates)) { - $sample = $this->datetime_prop('RDATE', $rdates[0]); - $rdprop = new VObject\Property\MultiDateTime('RDATE', null); - $rdprop->setDateTimes($rdates, $sample->getDateType()); - $ve->add($rdprop); + foreach ((array)$rdates as $rdate) { + $ve->add($this->datetime_prop($cal, 'RDATE', $rdate)); + } } } if ($event['categories']) { - $cat = VObject\Property::create('CATEGORIES'); + $cat = $cal->create('CATEGORIES'); $cat->setParts((array)$event['categories']); $ve->add($cat); } @@ -1019,15 +1047,15 @@ class libvcalendar implements Iterator $ve->add('PERCENT-COMPLETE', intval($event['complete'])); // Apple iCal required the COMPLETED date to be set in order to consider a task complete if ($event['complete'] == 100) - $ve->add($this->datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true)); + $ve->add($this->datetime_prop($cal, 'COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true)); } if ($event['valarms']) { foreach ($event['valarms'] as $alarm) { - $va = VObject\Component::create('VALARM'); + $va = $cal->createComponent('VALARM'); $va->action = $alarm['action']; if ($alarm['trigger'] instanceof DateTime) { - $va->add($this->datetime_prop('TRIGGER', $alarm['trigger'], true)); + $va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true)); } else { $va->add('TRIGGER', $alarm['trigger']); @@ -1056,13 +1084,13 @@ class libvcalendar implements Iterator } // legacy support else if ($event['alarms']) { - $va = VObject\Component::create('VALARM'); + $va = $cal->createComponent('VALARM'); list($trigger, $va->action) = explode(':', $event['alarms']); $val = libcalendaring::parse_alaram_value($trigger); if ($val[3]) $va->add('TRIGGER', $val[3]); else if ($val[0] instanceof DateTime) - $va->add($this->datetime_prop('TRIGGER', $val[0])); + $va->add($this->datetime_prop($cal, 'TRIGGER', $val[0])); $ve->add($va); } @@ -1107,7 +1135,7 @@ class libvcalendar implements Iterator if (is_callable($get_attachment) && ($data = call_user_func($get_attachment, $attach['id'], $event))) { // embed attachments for iCal $ve->add('ATTACH', - base64_encode($data), + $data, array_filter(array('VALUE' => 'BINARY', 'ENCODING' => 'BASE64', 'FMTTYPE' => $attach['mimetype'], 'X-LABEL' => $attach['name']))); unset($data); // attempt to free memory } @@ -1146,10 +1174,9 @@ class libvcalendar implements Iterator foreach ($event['recurrence']['EXCEPTIONS'] as $ex) { $exdate = clone $event['start']; $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j')); - $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $exdate, true); // if ($ex['thisandfuture']) // not supported by any client :-( // $recurrence_id->add('RANGE', 'THISANDFUTURE'); - $this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id); + $this->_to_ical($ex, $vcal, $get_attachment, $exdate); } } } @@ -1165,10 +1192,11 @@ class libvcalendar implements Iterator * @return mixed A Sabre\VObject\Component object representing a VTIMEZONE definition * or false if no timezone information is available */ - public static function get_vtimezone($tzid, $from = 0, $to = 0) + public static function get_vtimezone($tzid, $from = 0, $to = 0, $cal = null) { if (!$from) $from = time(); if (!$to) $to = $from; + if (!$cal) $cal = new VObject\Component\VCalendar(); if (is_string($tzid)) { try { @@ -1189,7 +1217,7 @@ class libvcalendar implements Iterator $year = 86400 * 360; $transitions = $tz->getTransitions($from - $year, $to + $year); - $vt = new VObject\Component('VTIMEZONE'); + $vt = $cal->createComponent('VTIMEZONE'); $vt->TZID = $tz->getName(); $std = null; $dst = null; @@ -1203,12 +1231,12 @@ class libvcalendar implements Iterator if ($trans['isdst']) { $t_dst = $trans['ts']; - $dst = new VObject\Component('DAYLIGHT'); + $dst = $cal->createComponent('DAYLIGHT'); $cmp = $dst; } else { $t_std = $trans['ts']; - $std = new VObject\Component('STANDARD'); + $std = $cal->createComponent('STANDARD'); $cmp = $std; } @@ -1282,48 +1310,25 @@ class libvcalendar implements Iterator /** - * Override Sabre\VObject\Property that quotes commas in the location property + * Override Sabre\VObject\Property\Text that quotes commas in the location property * because Apple clients treat that property as list. */ -class vobject_location_property extends VObject\Property +class vobject_location_property extends VObject\Property\Text { /** - * Turns the object back into a serialized blob. + * List of properties that are considered 'structured'. * - * @return string + * @var array */ - public function serialize() - { - $str = $this->name; + protected $structuredValues = array( + // vCard + 'N', + 'ADR', + 'ORG', + 'GENDER', + 'LOCATION', + // iCalendar + 'REQUEST-STATUS', + ); - foreach ($this->parameters as $param) { - $str.=';' . $param->serialize(); - } - - $src = array( - '\\', - "\n", - ',', - ); - $out = array( - '\\\\', - '\n', - '\,', - ); - $str.=':' . str_replace($src, $out, $this->value); - - $out = ''; - while (strlen($str) > 0) { - if (strlen($str) > 75) { - $out.= mb_strcut($str, 0, 75, 'utf-8') . "\r\n"; - $str = ' ' . mb_strcut($str, 75, strlen($str), 'utf-8'); - } else { - $out.= $str . "\r\n"; - $str = ''; - break; - } - } - - return $out; - } } diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php index ce4edeab..c247defe 100644 --- a/plugins/libcalendaring/tests/libvcalendar.php +++ b/plugins/libcalendaring/tests/libvcalendar.php @@ -5,7 +5,7 @@ * * @author Thomas Bruederli * - * Copyright (C) 2013, Kolab Systems AG + * Copyright (C) 2014, 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 @@ -98,7 +98,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $event = $events[0]; $this->assertEquals(1, count($events), "Import event data"); - $this->assertFalse(array_key_exists('created', $event), "No created date field"); + $this->assertInstanceOf('DateTime', $event['created'], "Created date field"); $this->assertFalse(array_key_exists('changed', $event), "No changed date field"); } @@ -280,6 +280,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals(9, count($event['recurrence']['RDATE'])); $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][0]); + $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][1]); } /** @@ -294,7 +295,9 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertInstanceOf('DateTime', $freebusy['start'], "'start' property is DateTime object"); $this->assertInstanceOf('DateTime', $freebusy['end'], "'end' property is DateTime object"); $this->assertEquals(11, count($freebusy['periods']), "Number of freebusy periods defined"); - $this->assertEquals(9, count($ical->get_busy_periods()), "Number of busy periods found"); + $periods = $ical->get_busy_periods(); + $this->assertEquals(9, count($periods), "Number of busy periods found"); + $this->assertEquals('BUSY-TENTATIVE', $periods[8][2], "FBTYPE=BUSY-TENTATIVE"); } /** @@ -439,8 +442,8 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals($num, substr_count($ics, 'UID:'.$event['uid']), "Recurrence Exceptions with same UID"); $this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END"); - $this->assertContains('RECURRENCE-ID;VALUE=DATE-TIME:20130814', $ics, "Recurrence-ID (1) being the exception date"); - $this->assertContains('RECURRENCE-ID;VALUE=DATE-TIME:20131113', $ics, "Recurrence-ID (2) being the exception date"); + $this->assertContains('RECURRENCE-ID:20130814', $ics, "Recurrence-ID (1) being the exception date"); + $this->assertContains('RECURRENCE-ID:20131113', $ics, "Recurrence-ID (2) being the exception date"); $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title"); } @@ -453,7 +456,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8'); $ics = $ical->export($events, null, false); - $this->assertContains('RDATE;VALUE=DATE-TIME:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values"); + $this->assertContains('RDATE:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values"); } /** @@ -480,10 +483,11 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase function test_datetime() { $ical = new libvcalendar(); - $localtime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin'))); - $localdate = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true); - $utctime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC'))); - $asutctime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true); + $cal = new \Sabre\VObject\Component\VCalendar(); + $localtime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin'))); + $localdate = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true); + $utctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC'))); + $asutctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true); $this->assertContains('TZID=Europe/Berlin', $localtime->serialize()); $this->assertContains('VALUE=DATE', $localdate->serialize()); From 756121c7adf57c5df8d50a969dcae30d7908791e Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Mon, 10 Nov 2014 17:56:11 +0100 Subject: [PATCH 2/7] Add some more tests for ical parsing --- plugins/libcalendaring/tests/libvcalendar.php | 25 ++++++++++++++++--- .../libcalendaring/tests/resources/itip.ics | 4 ++- .../tests/resources/recurring.ics | 8 ++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php index c247defe..eff827c5 100644 --- a/plugins/libcalendaring/tests/libvcalendar.php +++ b/plugins/libcalendaring/tests/libvcalendar.php @@ -124,7 +124,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('REQUEST', $ical->method, "iTip method"); // attendees - $this->assertEquals(2, count($event['attendees']), "Attendees list (including organizer)"); + $this->assertEquals(3, count($event['attendees']), "Attendees list (including organizer)"); $organizer = $event['attendees'][0]; $this->assertEquals('ORGANIZER', $organizer['role'], 'Organizer ROLE'); $this->assertEquals('Rolf Test', $organizer['name'], 'Organizer name'); @@ -133,8 +133,17 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('REQ-PARTICIPANT', $attendee['role'], 'Attendee ROLE'); $this->assertEquals('NEEDS-ACTION', $attendee['status'], 'Attendee STATUS'); $this->assertEquals('rolf2@mykolab.com', $attendee['email'], 'Attendee mailto:'); + $this->assertEquals('carl@mykolab.com', $attendee['delegated-from'], 'Attendee delegated-from'); $this->assertTrue($attendee['rsvp'], 'Attendee RSVP'); + $delegator = $event['attendees'][2]; + $this->assertEquals('NON-PARTICIPANT', $delegator['role'], 'Delegator ROLE'); + $this->assertEquals('DELEGATED', $delegator['status'], 'Delegator STATUS'); + $this->assertEquals('INDIVIDUAL', $delegator['cutype'], 'Delegator CUTYPE'); + $this->assertEquals('carl@mykolab.com', $delegator['email'], 'Delegator mailto:'); + $this->assertEquals('rolf2@mykolab.com', $delegator['delegated-to'], 'Delegator delegated-to'); + $this->assertFalse($delegator['rsvp'], 'Delegator RSVP'); + // attachments $this->assertEquals(1, count($event['attachments']), "Embedded attachments"); $attachment = $event['attachments'][0]; @@ -156,6 +165,14 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals(2, count($rrule['EXDATE']), "Recurrence EXDATEs"); $this->assertInstanceOf('DateTime', $rrule['EXDATE'][0], "Recurrence EXDATE as DateTime"); + $this->assertTrue(is_array($rrule['EXCEPTIONS'])); + $this->assertEquals(1, count($rrule['EXCEPTIONS']), "Recurrence Exceptions"); + + $exception = $rrule['EXCEPTIONS'][0]; + $this->assertEquals($event['uid'], $event['uid'], "Exception UID"); + $this->assertEquals('Recurring Test (Exception)', $exception['title'], "Exception title"); + $this->assertInstanceOf('DateTime', $exception['start'], "Exception start"); + // categories, class $this->assertEquals('libcalendaring tests', join(',', (array)$event['categories']), "Event categories"); $this->assertEquals('confidential', $event['sensitivity'], "Class/sensitivity = confidential"); @@ -364,11 +381,11 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertContains('SEQUENCE:' . $event['sequence'], $ics, "Export Sequence number"); $this->assertContains('CLASS:CONFIDENTIAL', $ics, "Sensitivity => Class"); $this->assertContains('DESCRIPTION:*Exported by', $ics, "Export Description"); - $this->assertContains('ORGANIZER;CN=Rolf Test:mailto:rolf@', $ics, "Export organizer"); + $this->assertContains('ORGANIZER;CN=Rolf Test:mailto:rolf@', $ics, "Export organizer"); $this->assertRegExp('/ATTENDEE.*;ROLE=REQ-PARTICIPANT/', $ics, "Export Attendee ROLE"); $this->assertRegExp('/ATTENDEE.*;PARTSTAT=NEEDS-ACTION/', $ics, "Export Attendee Status"); $this->assertRegExp('/ATTENDEE.*;RSVP=TRUE/', $ics, "Export Attendee RSVP"); - $this->assertRegExp('/ATTENDEE.*:mailto:rolf2@/', $ics, "Export Attendee mailto:"); + $this->assertRegExp('/:mailto:rolf2@/', $ics, "Export Attendee mailto:"); $rrule = $event['recurrence']; $this->assertRegExp('/RRULE:.*FREQ='.$rrule['FREQ'].'/', $ics, "Export Recurrence Frequence"); @@ -420,6 +437,8 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase // add exceptions $event = $events[0]; + unset($event['recurrence']['EXCEPTIONS']); + $exception1 = $event; $exception1['start'] = clone $event['start']; $exception1['start']->setDate(2013, 8, 14); diff --git a/plugins/libcalendaring/tests/resources/itip.ics b/plugins/libcalendaring/tests/resources/itip.ics index af283e37..50eb4ee9 100644 --- a/plugins/libcalendaring/tests/resources/itip.ics +++ b/plugins/libcalendaring/tests/resources/itip.ics @@ -7,7 +7,9 @@ BEGIN:VEVENT ORGANIZER;CN="Rolf Test":MAILTO:rolf@mykolab.com DTSTAMP:20130628T190056Z ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT; - X-UID=208889384:mailto:rolf2@mykolab.com + DELEGATED-FROM=carl@mykolab.com;X-UID=208889384:mailto:rolf2@mykolab.com +ATTENDEE;RSVP=FALSE;PARTSTAT=DELEGATED;ROLE=NON-PARTICIPANT;CUTYPE=INDIVIDUAL; + DELEGATED-TO=rolf2@mykolab.com:mailto:carl@mykolab.com CREATED:20130628T190032Z UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0 LAST-MODIFIED:20130628T190032Z diff --git a/plugins/libcalendaring/tests/resources/recurring.ics b/plugins/libcalendaring/tests/resources/recurring.ics index 92db9e28..85860be2 100644 --- a/plugins/libcalendaring/tests/resources/recurring.ics +++ b/plugins/libcalendaring/tests/resources/recurring.ics @@ -40,4 +40,12 @@ TRIGGER:-PT12H ACTION:DISPLAY END:VALARM END:VEVENT +BEGIN:VEVENT +DTSTART;TZID="Europe/Zurich":20140521T100000 +DTEND;TZID="Europe/Zurich":20140521T150000 +RECURRENCE-ID:20140521T080000Z +UID:7e93e8e8eef16f28aa33b78cd73613eb +DTSTAMP:20130718T082032Z +SUMMARY:Recurring Test (Exception) +END:VEVENT END:VCALENDAR From 1e2898da5ac091607d006365388bc416de8b122a Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 12 Nov 2014 11:54:24 +0100 Subject: [PATCH 3/7] Fix TRIGGER properties with absilute date-time values (#3881) --- plugins/libcalendaring/libvcalendar.php | 26 +++++++++---------- plugins/libcalendaring/tests/libvcalendar.php | 4 ++- .../libcalendaring/tests/resources/alarms.ics | 5 ++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php index 3331b4e8..0965064e 100644 --- a/plugins/libcalendaring/libvcalendar.php +++ b/plugins/libcalendaring/libvcalendar.php @@ -500,7 +500,7 @@ class libvcalendar implements Iterator } } $attendee = self::map_keys($params, array_flip($this->attendee_keymap)); - $attendee['email'] = preg_replace('/^mailto:/i', '', $value); + $attendee['email'] = preg_replace('!^mailto:!i', '', $value); if ($prop->name == 'ORGANIZER') { $attendee['role'] = 'ORGANIZER'; @@ -583,11 +583,9 @@ class libvcalendar implements Iterator switch ($prop->name) { case 'TRIGGER': - foreach ($prop->parameters() as $pname => $pvalue) { - if ($pname == 'VALUE' && $pvalue == 'DATE-TIME') { - $trigger = '@' . $prop->getDateTime()->format('U'); - $alarm['trigger'] = $prop->getDateTime(); - } + if ($prop['VALUE'] == 'DATE-TIME') { + $trigger = '@' . $prop->getDateTime()->format('U'); + $alarm['trigger'] = $prop->getDateTime(); } if (!$trigger && ($values = libcalendaring::parse_alaram_value($value))) { $trigger = $values[2]; @@ -616,7 +614,7 @@ class libvcalendar implements Iterator break; case 'ATTENDEE': - $alarm['attendees'][] = preg_replace('/^mailto:/i', '', $value); + $alarm['attendees'][] = preg_replace('!^mailto:!i', '', $value); break; case 'ATTACH': @@ -688,7 +686,7 @@ class libvcalendar implements Iterator break; case 'ORGANIZER': - $this->freebusy['organizer'] = preg_replace('/^mailto:/i', '', $value); + $this->freebusy['organizer'] = preg_replace('!^mailto:!i', '', $value); break; case 'FREEBUSY': @@ -813,7 +811,7 @@ class libvcalendar implements Iterator * @param boolean Set as UTC date * @param boolean Set as VALUE=DATE property */ - public function datetime_prop($cal, $name, $dt, $utc = false, $dateonly = null) + public function datetime_prop($cal, $name, $dt, $utc = false, $dateonly = null, $set_type = false) { if ($utc) { $dt->setTimeZone(new \DateTimeZone('UTC')); @@ -823,12 +821,14 @@ class libvcalendar implements Iterator $is_utc = ($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z')); } $is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly; - $vdt = $cal->createProperty($name); - $vdt->setValue($dt); + $vdt = $cal->createProperty($name, $dt, null, $is_dateonly ? 'DATE' : 'DATE-TIME'); if ($is_dateonly) { $vdt['VALUE'] = 'DATE'; } + else if ($set_type) { + $vdt['VALUE'] = 'DATE-TIME'; + } // register timezone for VTIMEZONE block if (!$is_utc && !$dateonly && $tz && ($tzname = $tz->getName())) { @@ -1055,7 +1055,7 @@ class libvcalendar implements Iterator $va = $cal->createComponent('VALARM'); $va->action = $alarm['action']; if ($alarm['trigger'] instanceof DateTime) { - $va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true)); + $va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true, null, true)); } else { $va->add('TRIGGER', $alarm['trigger']); @@ -1090,7 +1090,7 @@ class libvcalendar implements Iterator if ($val[3]) $va->add('TRIGGER', $val[3]); else if ($val[0] instanceof DateTime) - $va->add($this->datetime_prop($cal, 'TRIGGER', $val[0])); + $va->add($this->datetime_prop($cal, 'TRIGGER', $val[0], true, null, true)); $ve->add($va); } diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php index eff827c5..80752c09 100644 --- a/plugins/libcalendaring/tests/libvcalendar.php +++ b/plugins/libcalendaring/tests/libvcalendar.php @@ -215,13 +215,14 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "First alarm action"); $this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text"); - $this->assertEquals(2, count($event['valarms']), "List all VALARM blocks"); + $this->assertEquals(3, count($event['valarms']), "List all VALARM blocks"); $valarm = $event['valarms'][1]; $this->assertEquals(1, count($valarm['attendees']), "Email alarm attendees"); $this->assertEquals('EMAIL', $valarm['action'], "Second alarm item (action)"); $this->assertEquals('-P1D', $valarm['trigger'], "Second alarm item (trigger)"); $this->assertEquals('This is the reminder message', $valarm['summary'], "Email alarm text"); + $this->assertInstanceOf('DateTime', $event['valarms'][2]['trigger'], "Absolute trigger date/time"); // test alarms export $ics = $ical->export(array($event)); @@ -230,6 +231,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertContains('DESCRIPTION:This is the first event reminder', $ics, "Alarm description"); $this->assertContains('SUMMARY:This is the reminder message', $ics, "Email alarm summary"); $this->assertContains('ATTENDEE:mailto:reminder-recipient@example.org', $ics, "Email alarm recipient"); + $this->assertContains('TRIGGER;VALUE=DATE-TIME:20130812', $ics, "Date-Time trigger"); } /** diff --git a/plugins/libcalendaring/tests/resources/alarms.ics b/plugins/libcalendaring/tests/resources/alarms.ics index d6f9d547..9d588543 100644 --- a/plugins/libcalendaring/tests/resources/alarms.ics +++ b/plugins/libcalendaring/tests/resources/alarms.ics @@ -46,6 +46,11 @@ ATTENDEE:mailto:reminder-recipient@example.org SUMMARY:This is the reminder message DESCRIPTION:This is the second event reminder END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:An absolute reminder +TRIGGER;VALUE=DATE-TIME:20130812T160000Z +END:VALARM END:VEVENT END:VCALENDAR From 998b25171d60485e2c73203423aa998af4ff6f78 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 12 Nov 2014 15:21:32 +0100 Subject: [PATCH 4/7] Accept events with METHOD:CANCEL and no end date --- plugins/libcalendaring/libvcalendar.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php index 0965064e..4d432488 100644 --- a/plugins/libcalendaring/libvcalendar.php +++ b/plugins/libcalendaring/libvcalendar.php @@ -653,6 +653,11 @@ class libvcalendar implements Iterator unset($event['end']); } + // some iTip CANCEL messages only contain the start date + if (!$event['end'] && $event['start'] && $this->method == 'CANCEL') { + $event['end'] = clone $event['start']; + } + // minimal validation if (empty($event['uid']) || ($event['_type'] == 'event' && empty($event['start']) != empty($event['end']))) { throw new VObject\ParseException('Object validation failed: missing mandatory object properties'); From 1976658f3e0d6a51ea06b35d7f5800aaa12cbd66 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 12 Nov 2014 18:53:50 +0100 Subject: [PATCH 5/7] Initialize class before using self::$config --- plugins/libkolab/lib/kolab_storage.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php index dfd18877..01bbe986 100644 --- a/plugins/libkolab/lib/kolab_storage.php +++ b/plugins/libkolab/lib/kolab_storage.php @@ -249,6 +249,8 @@ class kolab_storage */ public static function get_freebusy_server() { + self::setup(); + $url = 'https://' . $_SESSION['imap_host'] . '/freebusy'; $url = self::$config->get('kolab_freebusy_server', $url); $url = rcube_utils::resolve_url($url); From d055e770abcd5aeab26e5017362745803e711c03 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 12 Nov 2014 18:55:04 +0100 Subject: [PATCH 6/7] Set RSVP=TRUE only on rescheduling --- plugins/libkolab/lib/kolab_format_xcal.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php index ad545050..b9cce9ca 100644 --- a/plugins/libkolab/lib/kolab_format_xcal.php +++ b/plugins/libkolab/lib/kolab_format_xcal.php @@ -366,7 +366,7 @@ abstract class kolab_format_xcal extends kolab_format // set attendee RSVP if missing if (!isset($attendee['rsvp'])) { - $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = true; + $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = $reschedule; } $att = new Attendee; From 963cc3e1f5098490be8cf5f7afea6b8b84358e44 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 12 Nov 2014 19:03:06 +0100 Subject: [PATCH 7/7] Support SCHEDULE-STATUS attende/organizer parameters --- plugins/libcalendaring/libvcalendar.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php index 4d432488..7c049b3d 100644 --- a/plugins/libcalendaring/libvcalendar.php +++ b/plugins/libcalendaring/libvcalendar.php @@ -36,8 +36,16 @@ class libvcalendar implements Iterator private $attach_uri = null; private $prodid = '-//Roundcube//Roundcube libcalendaring//Sabre//Sabre VObject//EN'; private $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO'); - private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE', - 'cutype' => 'CUTYPE', 'rsvp' => 'RSVP', 'delegated-from' => 'DELEGATED-FROM', 'delegated-to' => 'DELEGATED-TO'); + private $attendee_keymap = array( + 'name' => 'CN', + 'status' => 'PARTSTAT', + 'role' => 'ROLE', + 'cutype' => 'CUTYPE', + 'rsvp' => 'RSVP', + 'delegated-from' => 'DELEGATED-FROM', + 'delegated-to' => 'DELEGATED-TO', + 'schedule-status' => 'SCHEDULE-STATUS', + ); private $iteratorkey = 0; private $charset; private $forward_exceptions; @@ -1107,12 +1115,14 @@ class libvcalendar implements Iterator else if (!empty($attendee['email'])) { if (isset($attendee['rsvp'])) $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : 'FALSE'; - $ve->add('ATTENDEE', 'mailto:' . $attendee['email'], array_filter(self::map_keys($attendee, $this->attendee_keymap))); + $ve->add('ATTENDEE', 'mailto:' . $attendee['email'], + array_filter(self::map_keys($attendee, $this->attendee_keymap))); } } if ($event['organizer']) { - $ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], self::map_keys($event['organizer'], array('name' => 'CN'))); + $ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], + array_filter(self::map_keys($event['organizer'], $this->attendee_keymap))); } foreach ((array)$event['url'] as $url) {