From 99f904adcc37d93c90defcd8ce898598e25be212 Mon Sep 17 00:00:00 2001 From: Hugues Hiegel Date: Wed, 11 Mar 2015 16:55:04 +0100 Subject: Lot of plugins --- libgpl/caldav/caldav-client.php | 468 +++++++++++++++++++++++++++++++++++++ libgpl/caldav/caldav_sync.php | 284 ++++++++++++++++++++++ libgpl/caldav/vobject_sanitize.php | 110 +++++++++ 3 files changed, 862 insertions(+) create mode 100644 libgpl/caldav/caldav-client.php create mode 100644 libgpl/caldav/caldav_sync.php create mode 100644 libgpl/caldav/vobject_sanitize.php (limited to 'libgpl/caldav') diff --git a/libgpl/caldav/caldav-client.php b/libgpl/caldav/caldav-client.php new file mode 100644 index 0000000..417c42b --- /dev/null +++ b/libgpl/caldav/caldav-client.php @@ -0,0 +1,468 @@ + + * + * Copyright (C) 2013, Awesome IT GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +require_once (dirname(__FILE__).'/../../libcalendaring/SabreDAV/vendor/autoload.php'); +require_once (dirname(__FILE__).'/vobject_sanitize.php'); + + +class caldav_client extends Sabre\DAV\Client +{ + const CLARK_GETCTAG = '{http://calendarserver.org/ns/}getctag'; + const CLARK_GETETAG = '{DAV:}getetag'; + const CLARK_CALDATA = '{urn:ietf:params:xml:ns:caldav}calendar-data'; + + public $base_uri; + public $path; + private $libvcal; + private $rc; + private $user_agent; + + /** + * Default constructor for CalDAV client. + * + * @param string Caldav URI to appropriate calendar. + * @param string Username for HTTP basic auth. + * @param string Password for HTTP basic auth. + * @param boolean Verify SSL cert. // Mod by Rosali (https://gitlab.awesome-it.de/kolab/roundcube-plugins/issues/1) + */ + public function __construct($uri, $user = null, $pass = null, $verifySSL = array(true, true)) // Mod by Rosali (https://gitlab.awesome-it.de/kolab/roundcube-plugins/issues/1) + { + $this->user_agent = 'MyRoundcube-SabreDAV/' . Sabre\DAV\Version::VERSION; + + // Include libvcalendar on demand ... + if(!class_exists("libvcalendar")) + require_once (dirname(__FILE__).'/../../libcalendaring/libvcalendar.php'); + + $this->libvcal = new libvcalendar(); + $this->rc = rcube::get_instance(); + + $tokens = parse_url($uri); + $this->base_uri = $tokens['scheme']."://".$tokens['host'].($tokens['port'] ? ":".$tokens['port'] : null); + $this->path = $tokens['path'].($tokens['query'] ? "?".$tokens['query'] : null); + $settings = array( + 'baseUri' => $this->base_uri, + 'authType' => Sabre\DAV\Client::AUTH_BASIC + ); + + if ($user) $settings['userName'] = $user; + if ($pass) $settings['password'] = $pass; + + parent::__construct($settings); + + $this->verifyPeer = $verifySSL[0]; + $this->verifyHost = $verifySSL[1]; + $this->authType = CURLAUTH_BASIC | CURLAUTH_ANY; + } + + /** + * Fetches calendar ctag. + * + * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Retrieving_calendar_information + * @return Calendar ctag or null on error. + */ + public function get_ctag() + { + try + { + $arr = $this->propFind($this->path, array(self::CLARK_GETCTAG)); + + if (isset($arr[self::CLARK_GETCTAG])) + return $arr[self::CLARK_GETCTAG]; + } + catch(Sabre\DAV\Exception $err) + { + rcube::raise_error(array( + 'code' => $err->getHTTPCode(), + 'type' => 'DAV', + 'file' => $err->getFile(), + 'line' => $err->getLine(), + 'message' => $err->getMessage() + ), true, false); + } + + return null; + } + + /** + * Fetches event etags and urls. + * + * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Finding_out_if_anything_changed + * + * @param array Optional list of relative event URL's to retrieve specific etags. If not specified, all etags of the current calendar are returned. + * @return array List of etag properties with keys: + * url: Event ical path relative to the calendar URL. + * etag: Current event etag. + */ + public function get_etags(array $event_urls = array()) + { + $etags = array(); + + try + { + $arr = $this->prop_report($this->path, array(self::CLARK_GETETAG), $event_urls); + foreach ($arr as $path => $data) + { + // Some caldav server return an empty calendar as event where etag is missing. Skip this! + if($data[self::CLARK_GETETAG]) + { + array_push($etags, array( + "url" => $path, + "etag" => str_replace('"', null, $data[self::CLARK_GETETAG]) + )); + } + } + } + catch(Sabre\DAV\Exception $err) + { + rcube::raise_error(array( + 'code' => $err->getHTTPCode(), + 'type' => 'DAV', + 'file' => $err->getFile(), + 'line' => $err->getLine(), + 'message' => $err->getMessage() + ), true, false); + } + + return $etags; + } + + /** + * Fetches calendar events. + * + * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Downloading_objects + * @param array $urls = array() Optional list of event URL's to fetch. If non is specified, all + * events from the appropriate calendar will be fetched. + * @return Array hash list that maps the events URL to the appropriate event properties. + */ + public function get_events($urls = array()) + { + $events = array(); + + try + { + $vcals = $this->prop_report($this->path, array( + self::CLARK_GETETAG, + self::CLARK_CALDATA + ), $urls); + + foreach ($vcals as $path => $response) + { + $vcal = $response[self::CLARK_CALDATA]; + $vobject_sanitize = new vobject_sanitize($vcal, array('CATEGORIES'), 'serialize'); + $vcal = $vobject_sanitize->vobject; + $vobject_sanitize = new vobject_sanitize($vcal, array('RDATE'), 'unserialize'); + $vcal = $vobject_sanitize->vobject; + foreach ($this->libvcal->import($vcal) as $event) { + $events[$path] = $event; + } + } + } + catch(Sabre\DAV\Exception $err) + { + rcube::raise_error(array( + 'code' => $err->getHTTPCode(), + 'type' => 'DAV', + 'file' => $err->getFile(), + 'line' => $err->getLine(), + 'message' => $err->getMessage() + ), true, false); + } + return $events; + } + + /** + * Does a REPORT request + * + * @param string $url + * @param array $properties List of requested properties must be specified as an array, in clark + * notation. + * @param array $event_urls If specified, a multiget report request will be initiated with the + * specified event urls. + * @param int $depth = 1 Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * @return array Hash with ics event path as key and a hash array with properties and appropriate values. + */ + public function prop_report($url, array $properties, array $event_urls = array(), $depth = 1) + { + $url = slashify($url); // iCloud + + $parent_tag = sizeof($event_urls) > 0 ? "c:calendar-multiget" : "d:propfind"; + $method = sizeof($event_urls) > 0 ? 'REPORT' : 'PROPFIND'; + + $body = ''."\n".'<'.$parent_tag.' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'."\n"; + + $body .= ' '."\n"; + foreach ($properties as $property) + { + + list($namespace, $elementName) = Sabre\DAV\XMLUtil::parseClarkNotation($property); + + if ($namespace === 'DAV:') + { + $body .= ' '."\n"; + } + else + { + $body .= ' '."\n"; + } + } + $body .= ' '."\n"; + + // http://tools.ietf.org/html/rfc4791#page-90 + // http://www.bedework.org/trac/bedework/wiki/Bedework/DevDocs/Filters + /* + if($start && $end) + { + $body.= ' '."\n". + ' '."\n". + ' '."\n". + ' '."\n". + ' '."\n". + ' '."\n". + ' ' . "\n"; + } + */ + + foreach ($event_urls as $event_url) + { + $body .= ''.$event_url.''."\n"; + } + + $body .= ''; + + $response = $this->request($method, $url, $body, array( + 'Depth' => $depth, + 'Content-Type' => 'application/xml', + 'User-Agent' => $this->user_agent + )); + + $result = $this->parseMultiStatus($response['body']); + + // If depth was 0, we only return the top item + if ($depth === 0) + { + reset($result); + $result = current($result); + return isset($result[200]) ? $result[200] : array(); + } + + $new_result = array(); + foreach ($result as $href => $status_list) + { + $new_result[$href] = isset($status_list[200]) ? $status_list[200] : array(); + } + + return $new_result; + } + + /** + * Updates or creates a calendar event. + * + * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Updating_a_calendar_object + * @param string Event ics path for the event. + * @param array Hash array with event properties. + * @param string Current event etag to match against server data. Pass null for new events. + * @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error. + */ + public function put_event($path, $event, $etag = null) + { + try + { + $headers = array( + 'Content-Type' => 'text/calendar; charset=utf-8', + 'User-Agent' => $this->user_agent + ); + if ($etag) $headers["If-Match"] = '"'.$etag.'"'; + + // Temporarily disable error reporting since libvcal seems not checking array key properly. + // TODO: Remove this todo if we could ensure that those errors come not from incomplete event properties. + //$err_rep = error_reporting(E_ERROR); + $vcal = $this->libvcal->export(array($event)); + if (is_array($vcal)) + $vcal = array_shift($vcal); + + //error_reporting($err_rep); + $response = $this->request('PUT', $path, $vcal, $headers); + + // Following http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Creating_a_calendar_object, the + // caldav server must not always return the new etag. + + return $response["statusCode"] == 201 || // 201 (created, successfully created) + $response["statusCode"] == 204; // 204 (no content, successfully updated) + } + catch(Sabre\DAV\Exception\PreconditionFailed $err) + { + // Event tag not up to date, must be updated first ... + return -1; + } + catch(Sabre\DAV\Exception $err) + { + rcube::raise_error(array( + 'code' => $err->getHTTPCode(), + 'type' => 'DAV', + 'file' => $err->getFile(), + 'line' => $err->getLine(), + 'message' => $err->getMessage() + ), true, false); + } + return false; + } + + /** + * Removes event of given URL. + * + * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Deleting_a_calendar_object + * @param string Event ics path for the event. + * @param string Current event etag to match against server data. Pass null to force removing the event. + * @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error. + **/ + public function remove_event($path, $etag = null) + { + return $this->delete_request($path, $etag); + } + + /** + * Fires a DELETE request to a given URL. + * + * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Deleting_a_calendar_object + * @param string Path. + * @param string Current etag to match against server data or null. + * @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error. + **/ + public function delete_request($path, $etag = null) + { + try + { + $headers = array( + 'Content-Type' => 'text/calendar; charset=utf-8', + 'User-Agent' => $this->user_agent + ); + if ($etag) $headers["If-Match"] = '"'.$etag.'"'; + + $response = $this->request('DELETE', $path, null, $headers); + return $response["statusCode"] == 204; // 204 (no content, successfully deleted); + } + catch(Sabre\DAV\Exception\PreconditionFailed $err) + { + // Event tag not up to date, must be updated first ... + return -1; + } + catch(Sabre\DAV\Exception $err) + { + rcube::raise_error(array( + 'code' => $err->getHTTPCode(), + 'type' => 'DAV', + 'file' => $err->getFile(), + 'line' => $err->getLine(), + 'message' => $err->getMessage() + ), true, false); + } + return false; + } + + /** + * Make a propFind query to caldav server + * @param string $path absolute or relative URL to Resource + * @param array $props list of properties to use for the query. Properties must have clark-notation. + * @param int $depth 0 means no recurse while 1 means recurse + * @param boolean $log log exception + * @return array + */ + public function prop_find($path, $props, $depth, $log = true) + { + try { + $response = $this->propFind($path, $props, $depth); + } + catch(Sabre\DAV\Exception $err) + { + rcube::raise_error(array( + 'code' => $err->getHTTPCode(), + 'type' => 'DAV', + 'file' => $err->getFile(), + 'line' => $err->getLine(), + 'message' => $err->getMessage() + ), $log, false); + } + return $response; + } + + /** + * Add a caldendar collection + * @param string collection URL + * @parma string displayname + * @param string resource type + * @param string namespace + * @return array response + */ + public function add_collection($url, $displayname, $resourcetype, $namespace) + { + $body = '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + ' ' . + ' ' . + ' ' . $displayname . '' . + ' ' . + ' ' . + ' '; + + try { + $response = $this->request('MKCOL', $url, $body, array( + 'Content-Type' => 'application/xml', + 'User-Agent' => $this->user_agent + )); + } + catch(Sabre\DAV\Exception $err) + { + return false; + } + return $response; + } + + /** + * Freebusy request for a given user + * @param string username + * @param path relative path to base uri + * @param integer unix timestamp + * @param integer unix timestamp + * @retrun array List of busy timeslots within the requested range + */ + public function freebusy($user, $path, $start, $end) + { + $body = '' . + '' . + '' . + ''; + return $this->request('REPORT', $path, $body, array( + 'Content-Type' => 'application/xml', + 'Depth' => 1, + 'User-Agent' => $this->user_agent + )); + } +}; +?> diff --git a/libgpl/caldav/caldav_sync.php b/libgpl/caldav/caldav_sync.php new file mode 100644 index 0000000..0266af4 --- /dev/null +++ b/libgpl/caldav/caldav_sync.php @@ -0,0 +1,284 @@ + + * + * Copyright (C) 2013, Awesome IT GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +require_once (dirname(__FILE__) . '/caldav-client.php'); + +class caldav_sync +{ + const ACTION_NONE = 1; + const ACTION_UPDATE = 2; + const ACTION_CREATE = 4; + + private $cal_id = null; + private $ctag = null; + private $user = null; + private $pass = null; + private $url = null; + private $env; + + public $sync = 0; + + /** + * Default constructor for calendar synchronization adapter. + * + * @param int Calendar id. + * @param array Hash array with caldav properties: + * url: Caldav calendar URL. + * user: Caldav http basic auth user. + * pass: Password für caldav user. + * ctag: Caldav ctag for calendar. + * @param boolean verify SSL Cert // Mod by Rosali (https://gitlab.awesome-it.de/kolab/roundcube-plugins/issues/1) + */ + public function __construct($cal_id, $props, $verifySSL, $env) + { + $this->env = $env; + $this->cal_id = $cal_id; + + $this->url = $props["url"]; + $this->ctag = isset($props["tag"]) ? $props["tag"] : null; + $this->user = isset($props["user"]) ? $props["user"] : null; + $this->pass = isset($props["pass"]) ? $props["pass"] : null; + $this->sync = isset($props["sync"]) ? $props["sync"] : 0; + + $this->caldav = new caldav_client($this->url, $this->user, $this->pass, $verifySSL); // Mod by Rosali (https://gitlab.awesome-it.de/kolab/roundcube-plugins/issues/1) + } + + /** + * Getter for current calendar ctag. + * @return string + */ + public function get_ctag() + { + return $this->ctag; + } + + /** + * Determines whether current calendar needs to be synced + * regarding the CalDAV ctag. + * + * @return True if the current calendar ctag differs from the CalDAV tag which + * indicates that there are changes that must be synched. Returns false + * if the calendar is up to date, no sync necesarry. + */ + public function is_synced($force = false) + { + $is_synced = $this->ctag == $this->caldav->get_ctag() && $this->ctag; + $env = $this->env; + $env::debug_log("Ctag indicates that calendar \"$this->cal_id\" ".($is_synced ? "is synced." : "needs update!")); + + return $is_synced; + } + + /** + * Synchronizes given events with caldav server and returns updates. + * + * @param array List of local events. + * @param array List of caldav properties for each event. + * @return array Tuple containing the following lists: + * + * Caldav properties for events to be created or to be updated with the keys: + * url: Event ical URL relative to calendar URL + * etag: Remote etag of the event + * local_event: The local event in case of an update. + * remote_event: The current event retrieved from caldav server. + * + * A list of event ids that are in sync. + */ + public function get_updates($events, $caldav_props) + { + $ctag = $this->caldav->get_ctag(); + + if($ctag) + { + $this->ctag = $ctag; + $etags = $this->caldav->get_etags(); + list($updates, $synced_event_ids) = $this->_get_event_updates($events, $caldav_props, $etags); + return array($this->_get_event_data($updates), $synced_event_ids); + } + else + { + $env = $this->env; + $env::debug_log("Unkown error while fetching calendar ctag for calendar \"$this->cal_id\"!"); + } + + return null; + } + + /** + * Determines sync status and requried updates for the given events using given list of etags. + * + * @param array List of local events. + * @param array List of caldav properties for each event. + * @param array List of current remote etags. + * @return array Tuple containing the following lists: + * + * Caldav properties for events to be created or to be updated with the keys: + * url: Event ical URL relative to calendar URL + * etag: Remote etag of the event + * local_event: The local event in case of an update. + * + * A list of event ids that are in sync. + */ + private function _get_event_updates($events, $caldav_props, $etags) + { + $updates = array(); + $in_sync = array(); + + foreach ($etags as $etag) + { + $url = $etag["url"]; + $etag = $etag["etag"]; + $event_found = false; + for($i = 0; $i < sizeof($events); $i ++) + { + if ($caldav_props[$i]["url"] == $url) + { + $event_found = true; + + if ($caldav_props[$i]["tag"] != $etag) + { + $env = $this->env; + $env::debug_log("Event ".$events[$i]["uid"]." needs update."); + + array_push($updates, array( + "local_event" => $events[$i], + "etag" => $etag, + "url" => $url + )); + } + else + { + array_push($in_sync, $events[$i]["id"]); + } + } + } + + if (!$event_found) + { + $env = $this->env; + $env::debug_log("Found new event ".$url); + + array_push($updates, array( + "url" => $url, + "etag" => $etag + )); + } + } + + return array($updates, $in_sync); + } + + /** + * Fetches event data and attaches it to the given update properties. + * + * @param $updates List of update properties. + * @return array List of update properties with additional key "remote_event" containing the current caldav event. + */ + private function _get_event_data($updates) + { + $urls = array(); + + foreach ($updates as $update) + { + array_push($urls, $update["url"]); + } + + $events = $this->caldav->get_events($urls); + + foreach($updates as &$update) + { + // Attach remote events to the appropriate updates. + // Note that this assumes unique event URL's! + $url = $update["url"]; + if($events[$url]) { + $update["remote_event"] = $events[$url]; + $update["remote_event"]["calendar"] = $this->cal_id; + } + } + + return $updates; + } + + /** + * Creates the given event on the caldav server. + * + * @param array Hash array with event properties. + * @return Caldav properties with created URL on success, false on error. + */ + public function create_event($event) + { + $props = array( + "url" => parse_url($this->url, PHP_URL_PATH)."/".$event["uid"].".ics", + "tag" => null + ); + + $env = $this->env; + $env::debug_log("Push new event to url ".$props["url"]); + $result = $this->caldav->put_event($props["url"], $event); + + if($result == false || $result < 0) return false; + return $props; + } + + /** + * Updates the given event on the caldav server. + * + * @param array Hash array with event properties to update. + * @param array Hash array with caldav properties "url" and "tag" for the event. + * @return True on success, false on error, -1 if the given event/etag is not up to date. + */ + public function update_event($event, $props) + { + $env = $this->env; + $env::debug_log("Updating event uid \"".$event["uid"]."\"."); + return $this->caldav->put_event($props["url"], $event, $props["tag"]); + } + + /** + * Removes the given event from the caldav server. + * + * @param array Hash array with caldav properties "url" and "tag" for the event. + * @return True on success, false on error. + */ + public function remove_event($props) + { + $env = $this->env; + $env::debug_log("Removing event url \"".$props["url"]."\"."); + return $this->caldav->remove_event($props["url"]); + } + + /** + * Freebusy request for a given user. + * + * @param string username + * @param string relative path to caldav base uri + * @param integer unix timestamp + * @param integer unix timestamp + * @retrun array List of busy timeslots within the requested range + */ + public function freebusy($user, $path, $start, $end) + { + return $this->caldav->freebusy($user, $path, $start, $end); + } +}; +?> \ No newline at end of file diff --git a/libgpl/caldav/vobject_sanitize.php b/libgpl/caldav/vobject_sanitize.php new file mode 100644 index 0000000..9db4b41 --- /dev/null +++ b/libgpl/caldav/vobject_sanitize.php @@ -0,0 +1,110 @@ +vobject = $vobject; + $this->properties = (array) $properties; + $this->_unfoald(); + $this->_eol(); + switch($method){ + case 'serialize': + $this->_serialize(); + break; + case 'unserialize': + $this->_unserialize(); + break; + } + } + + private function _unfoald() + { + $data = array(); + $content = explode("\n", $this->vobject); + for($i = 0; $i < count($content); $i++){ + $line = rtrim($content[$i]); + while(isset($content[$i + 1]) && strlen($content[$i + 1]) > 0 && ($content[$i+1]{0} == ' ' || $content[$i + 1]{0} == "\t" )){ + $line .= rtrim(substr($content[++$i], 1)); + } + $data[] = $line; + } + $this->vobject = implode(PHP_EOL, $data); + } + + private function _eol() + { + $this->vobject = preg_replace('/\s\s+/', PHP_EOL, $this->vobject); + } + + private function _serialize() + { + $tokens = array(); + foreach($this->components as $component){ + $regex = '#BEGIN:' . $component . '(?:(?!BEGIN:' . $component . ').)*?END:' . $component . '#si'; + preg_match_all($regex, $this->vobject, $matches); + foreach($matches as $part){ + foreach($part as $match){ + $token = md5($match); + $tokens[$token] = $match; + $this->vobject = str_replace($match, '***' . $token . '***', $this->vobject); + } + } + foreach($tokens as $token => $content){ + foreach($this->properties as $property){ + $content = preg_replace('#' . PHP_EOL . $property . ':#i', PHP_EOL . 'X-ICAL-SANITIZE-' . $property . ':', $content, 1); + $content = preg_replace('#' . PHP_EOL . $property . ':#i', ',', $content); + $content = str_replace(PHP_EOL . 'X-ICAL-SANITIZE-' . $property . ':', PHP_EOL . $property . ':', $content); + $this->vobject = str_replace('***' . $token . '***', $content, $this->vobject); + } + } + } + } + + private function _unserialize() + { + foreach($this->properties as $property){ + preg_match_all('#' . PHP_EOL . $property . '.*:.*,.*' . PHP_EOL . '#i', $this->vobject, $matches); + $content = $this->vobject; + if(is_array($matches)){ + foreach($matches[0] as $match){ + $temp = explode(':', $match, 2); + $field = $temp[0]; + $values = $temp[1]; + $properties = explode(';', $field); + $tz = false; + foreach($properties as $idx => $property){ + if(strtolower(substr($property, 0, 5)) == 'tzid='){ + $temp = explode('=', $property, 2); + $tz = $temp[1]; + unset($properties[$idx]); + } + if(strtolower(substr($property, 0, 6)) == 'value='){ + $temp = explode('=', $property, 2); + $daot = $temp[1]; + } + } + $field = implode(';', $properties); + $values = explode(',', $values); + $line = ''; + foreach($values as $value){ + if($tz){ + $datetime = new DateTime($value, new DateTimeZone($tz)); + if(strtolower($daot) == 'date-time'){ + $ts = $datetime->format('U'); + $value = gmdate('Ymd\THis\Z', $ts); + } + } + $line .= $field . ':' . $value . PHP_EOL; + } + $content = preg_replace('/\s\s+/', PHP_EOL, str_replace($match, $line, $content)); + } + } + $this->vobject = $content; + } + } +} +?> \ No newline at end of file -- cgit v1.2.3