diff --git a/action/ajax.php b/action/ajax.php --- a/action/ajax.php +++ b/action/ajax.php @@ -1,162 +1,163 @@ hlp =& plugin_load('helper','davcal'); } function register(Doku_Event_Handler $controller) { $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call_unknown'); } function handle_ajax_call_unknown(&$event, $param) { if($event->data != 'plugin_davcal') return; $event->preventDefault(); $event->stopPropagation(); global $INPUT; $action = trim($INPUT->post->str('action')); $id = trim($INPUT->post->str('id')); $page = trim($INPUT->post->str('page')); $params = $INPUT->post->arr('params'); if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) $user = $_SERVER['REMOTE_USER']; else $user = null; $write = false; $multi = false; $data = array(); $data['result'] = false; $data['html'] = $this->getLang('unknown_error'); // Check if we have access to the calendar ($id is given by parameters, // that's not necessarily the page we come from) $acl = auth_quickaclcheck($id); if($acl > AUTH_READ) { $write = true; } // Retrieve the calendar pages based on the meta data $calendarPages = $this->hlp->getCalendarPagesByMeta($page); if($calendarPages === false) { $calendarPages = array($page); } if(count($calendarPages) > 1) $multi = true; // Parse the requested action switch($action) { // Add a new Event case 'newEvent': if($write) { $data['result'] = true; $data['html'] = $this->getLang('event_added'); $this->hlp->addCalendarEntryToCalendarForPage($id, $user, $params); } else { $data['result'] = false; $data['html'] = $this->getLang('no_permission'); } break; // Retrieve existing Events case 'getEvents': $startDate = $INPUT->post->str('start'); $endDate = $INPUT->post->str('end'); + $timezone = $INPUT->post->str('timezone'); $data = array(); foreach($calendarPages as $calPage) { $data = array_merge($data, $this->hlp->getEventsWithinDateRange($calPage, - $user, $startDate, $endDate)); + $user, $startDate, $endDate, $timezone)); } break; // Edit an event case 'editEvent': if($write) { $data['result'] = true; $data['html'] = $this->getLang('event_edited'); $this->hlp->editCalendarEntryForPage($id, $user, $params); } else { $data['result'] = false; $data['html'] = $this->getLang('no_permission'); } break; // Delete an Event case 'deleteEvent': if($write) { $data['result'] = true; $data['html'] = $this->getLang('event_deleted'); $this->hlp->deleteCalendarEntryForPage($id, $params); } else { $data['result'] = false; $data['html'] = $this->getLang('no_permission'); } break; // Get personal settings case 'getSettings': $data['result'] = true; $data['settings'] = $this->hlp->getPersonalSettings($user); $data['settings']['multi'] = $multi; $data['settings']['calids'] = $this->hlp->getCalendarMapForIDs($calendarPages); $data['settings']['readonly'] = !$write; $data['settings']['syncurl'] = $this->hlp->getSyncUrlForPage($page, $user); $data['settings']['privateurl'] = $this->hlp->getPrivateURLForPage($page); $data['settings']['meta'] = $this->hlp->getCalendarMetaForPage($page); break; // Save personal settings case 'saveSettings': $settings = array(); $settings['weeknumbers'] = $params['weeknumbers']; $settings['timezone'] = $params['timezone']; $settings['workweek'] = $params['workweek']; $settings['monday'] = $params['monday']; if($this->hlp->savePersonalSettings($settings, $user)) { $data['result'] = true; $data['html'] = $this->getLang('settings_saved'); } else { $data['result'] = false; $data['html'] = $this->getLang('error_saving'); } break; } // If we are still here, JSON output is requested //json library of DokuWiki require_once DOKU_INC . 'inc/JSON.php'; $json = new JSON(); //set content type header('Content-Type: application/json'); echo $json->encode($data); } } \ No newline at end of file diff --git a/helper.php b/helper.php --- a/helper.php +++ b/helper.php @@ -1,1128 +1,1125 @@ sqlite =& plugin_load('helper', 'sqlite'); global $conf; if($conf['allowdebug']) dbglog('---- DAVCAL helper.php init'); if(!$this->sqlite) { if($conf['allowdebug']) dbglog('This plugin requires the sqlite plugin. Please install it.'); msg('This plugin requires the sqlite plugin. Please install it.'); return; } if(!$this->sqlite->init('davcal', DOKU_PLUGIN.'davcal/db/')) { if($conf['allowdebug']) dbglog('Error initialising the SQLite DB for DAVCal'); return; } } /** * Retrieve meta data for a given page * * @param string $id optional The page ID * @return array The metadata */ private function getMeta($id = null) { global $ID; global $INFO; if ($id === null) $id = $ID; if($ID === $id && $INFO['meta']) { $meta = $INFO['meta']; } else { $meta = p_get_metadata($id); } return $meta; } /** * Retrieve the meta data for a given page * * @param string $id optional The page ID * @return array with meta data */ public function getCalendarMetaForPage($id = null) { if(is_null($id)) { global $ID; $id = $ID; } $meta = $this->getMeta($id); if(isset($meta['plugin_davcal'])) return $meta['plugin_davcal']; else return array(); } /** * Get all calendar pages used by a given page * based on the stored metadata * * @param string $id optional The page id * @return mixed The pages as array or false */ public function getCalendarPagesByMeta($id = null) { if(is_null($id)) { global $ID; $id = $ID; } $meta = $this->getCalendarMetaForPage($id); if(isset($meta['id'])) - { return array_keys($meta['id']); - } return false; } /** * Get a list of calendar names/pages/ids/colors * for an array of page ids * * @param array $calendarPages The calendar pages to retrieve * @return array The list */ public function getCalendarMapForIDs($calendarPages) { $data = array(); foreach($calendarPages as $page) { $calid = $this->getCalendarIdForPage($page); if($calid !== false) { $settings = $this->getCalendarSettings($calid); $name = $settings['displayname']; $color = $settings['calendarcolor']; $data[] = array('name' => $name, 'page' => $page, 'calid' => $calid, 'color' => $color); } } return $data; } /** * Get the saved calendar color for a given page. * * @param string $id optional The page ID * @return mixed The color on success, otherwise false */ public function getCalendarColorForPage($id = null) { if(is_null($id)) { global $ID; $id = $ID; } $calid = $this->getCalendarIdForPage($id); if($calid === false) return false; return $this->getCalendarColorForCalendar($calid); } /** * Get the saved calendar color for a given calendar ID. * * @param string $id optional The calendar ID * @return mixed The color on success, otherwise false */ public function getCalendarColorForCalendar($calid) { if(isset($this->cachedValues['calendarcolor'][$calid])) return $this->cachedValues['calendarcolor'][$calid]; $row = $this->getCalendarSettings($calid); if(!isset($row['calendarcolor'])) return false; $color = $row['calendarcolor']; $this->cachedValues['calendarcolor'][$calid] = $color; return $color; } /** * Set the calendar color for a given page. * * @param string $color The color definition * @param string $id optional The page ID * @return boolean True on success, otherwise false */ public function setCalendarColorForPage($color, $id = null) { if(is_null($id)) { global $ID; $id = $ID; } $calid = $this->getCalendarIdForPage($id); if($calid === false) return false; $query = "UPDATE calendars SET calendarcolor=".$this->sqlite->quote_string($color). " WHERE id=".$this->sqlite->quote_string($calid); $res = $this->sqlite->query($query); if($res !== false) { $this->cachedValues['calendarcolor'][$calid] = $color; return true; } return false; } /** * Set the calendar name and description for a given page with a given * page id. * If the calendar doesn't exist, the calendar is created! * * @param string $name The name of the new calendar * @param string $description The description of the new calendar * @param string $id (optional) The ID of the page * @param string $userid The userid of the creating user * * @return boolean True on success, otherwise false. */ public function setCalendarNameForPage($name, $description, $id = null, $userid = null) { if(is_null($id)) { global $ID; $id = $ID; } if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { $userid = uniqid('davcal-'); } } $calid = $this->getCalendarIdForPage($id); if($calid === false) return $this->createCalendarForPage($name, $description, $id, $userid); $query = "UPDATE calendars SET displayname=".$this->sqlite->quote_string($name).", ". "description=".$this->sqlite->quote_string($description)." WHERE ". "id=".$this->sqlite->quote_string($calid); $res = $this->sqlite->query($query); if($res !== false) return true; return false; } /** * Save the personal settings to the SQLite database 'calendarsettings'. * * @param array $settings The settings array to store * @param string $userid (optional) The userid to store * * @param boolean True on success, otherwise false */ public function savePersonalSettings($settings, $userid = null) { if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { return false; } } $this->sqlite->query("BEGIN TRANSACTION"); $query = "DELETE FROM calendarsettings WHERE userid=".$this->sqlite->quote_string($userid); $this->sqlite->query($query); foreach($settings as $key => $value) { $query = "INSERT INTO calendarsettings (userid, key, value) VALUES (". $this->sqlite->quote_string($userid).", ". $this->sqlite->quote_string($key).", ". $this->sqlite->quote_string($value).")"; $res = $this->sqlite->query($query); if($res === false) return false; } $this->sqlite->query("COMMIT TRANSACTION"); $this->cachedValues['settings'][$userid] = $settings; return true; } /** * Retrieve the settings array for a given user id. * Some sane defaults are returned, currently: * * timezone => local * weeknumbers => 0 * workweek => 0 * * @param string $userid (optional) The user id to retrieve * * @return array The settings array */ public function getPersonalSettings($userid = null) { // Some sane default settings $settings = array( 'timezone' => $this->getConf('timezone'), 'weeknumbers' => $this->getConf('weeknumbers'), 'workweek' => $this->getConf('workweek'), 'monday' => $this->getConf('monday') ); if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { return $settings; } } if(isset($this->cachedValues['settings'][$userid])) return $this->cachedValues['settings'][$userid]; $query = "SELECT key, value FROM calendarsettings WHERE userid=".$this->sqlite->quote_string($userid); $res = $this->sqlite->query($query); $arr = $this->sqlite->res2arr($res); foreach($arr as $row) { $settings[$row['key']] = $row['value']; } $this->cachedValues['settings'][$userid] = $settings; return $settings; } /** * Retrieve the calendar ID based on a page ID from the SQLite table * 'pagetocalendarmapping'. * * @param string $id (optional) The page ID to retrieve the corresponding calendar * * @return mixed the ID on success, otherwise false */ public function getCalendarIdForPage($id = null) { if(is_null($id)) { global $ID; $id = $ID; } if(isset($this->cachedValues['calid'][$id])) return $this->cachedValues['calid'][$id]; $query = "SELECT calid FROM pagetocalendarmapping WHERE page=".$this->sqlite->quote_string($id); $res = $this->sqlite->query($query); $row = $this->sqlite->res2row($res); if(isset($row['calid'])) { $calid = $row['calid']; $this->cachedValues['calid'] = $calid; return $calid; } return false; } /** * Retrieve the complete calendar id to page mapping. * This is necessary to be able to retrieve a list of * calendars for a given user and check the access rights. * * @return array The mapping array */ public function getCalendarIdToPageMapping() { $query = "SELECT calid, page FROM pagetocalendarmapping"; $res = $this->sqlite->query($query); $arr = $this->sqlite->res2arr($res); return $arr; } /** * Retrieve all calendar IDs a given user has access to. * The user is specified by the principalUri, so the * user name is actually split from the URI component. * * Access rights are checked against DokuWiki's ACL * and applied accordingly. * * @param string $principalUri The principal URI to work on * * @return array An associative array of calendar IDs */ public function getCalendarIdsForUser($principalUri) { global $auth; $user = explode('/', $principalUri); $user = end($user); $mapping = $this->getCalendarIdToPageMapping(); $calids = array(); $ud = $auth->getUserData($user); $groups = $ud['grps']; foreach($mapping as $row) { $id = $row['calid']; $page = $row['page']; $acl = auth_aclcheck($page, $user, $groups); if($acl >= AUTH_READ) { $write = $acl > AUTH_READ; $calids[$id] = array('readonly' => !$write); } } return $calids; } /** * Create a new calendar for a given page ID and set name and description * accordingly. Also update the pagetocalendarmapping table on success. * * @param string $name The calendar's name * @param string $description The calendar's description * @param string $id (optional) The page ID to work on * @param string $userid (optional) The user ID that created the calendar * * @return boolean True on success, otherwise false */ public function createCalendarForPage($name, $description, $id = null, $userid = null) { if(is_null($id)) { global $ID; $id = $ID; } if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { $userid = uniqid('davcal-'); } } $values = array('principals/'.$userid, $name, str_replace(array('/', ' ', ':'), '_', $id), $description, 'VEVENT,VTODO', 0, 1); $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) VALUES (".$this->sqlite->quote_and_join($values, ',').");"; $res = $this->sqlite->query($query); if($res === false) return false; // Get the new calendar ID $query = "SELECT id FROM calendars WHERE principaluri=".$this->sqlite->quote_string($values[0])." AND ". "displayname=".$this->sqlite->quote_string($values[1])." AND ". "uri=".$this->sqlite->quote_string($values[2])." AND ". "description=".$this->sqlite->quote_string($values[3]); $res = $this->sqlite->query($query); $row = $this->sqlite->res2row($res); // Update the pagetocalendarmapping table with the new calendar ID if(isset($row['id'])) { $values = array($id, $row['id']); $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (".$this->sqlite->quote_and_join($values, ',').")"; $res = $this->sqlite->query($query); return ($res !== false); } return false; } /** * Add a new iCal entry for a given page, i.e. a given calendar. * * The parameter array needs to contain - * timezone => The timezone of the entries * detectedtz => The timezone as detected by the browser + * currenttz => The timezone in use by the calendar * eventfrom => The event's start date * eventfromtime => The event's start time * eventto => The event's end date * eventtotime => The event's end time * eventname => The event's name * eventdescription => The event's description * * @param string $id The page ID to work on * @param string $user The user who created the calendar * @param string $params A parameter array with values to create * * @return boolean True on success, otherwise false */ public function addCalendarEntryToCalendarForPage($id, $user, $params) { - $settings = $this->getPersonalSettings($user); - if($settings['timezone'] !== '' && $settings['timezone'] !== 'local') - $timezone = new \DateTimeZone($settings['timezone']); - elseif($settings['timezone'] === 'local') + if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') + $timezone = new \DateTimeZone($params['currenttz']); + elseif($params['currenttz'] === 'local') $timezone = new \DateTimeZone($params['detectedtz']); else $timezone = new \DateTimeZone('UTC'); // Retrieve dates from settings $startDate = explode('-', $params['eventfrom']); $startTime = explode(':', $params['eventfromtime']); $endDate = explode('-', $params['eventto']); $endTime = explode(':', $params['eventtotime']); // Load SabreDAV require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); $vcalendar = new \Sabre\VObject\Component\VCalendar(); // Add VCalendar, UID and Event Name $event = $vcalendar->add('VEVENT'); $uuid = \Sabre\VObject\UUIDUtil::getUUID(); $event->add('UID', $uuid); $event->summary = $params['eventname']; // Add a description if requested $description = $params['eventdescription']; if($description !== '') $event->add('DESCRIPTION', $description); // Add attachments $attachments = $params['attachments']; - foreach($attachments as $attachment) - $event->add('ATTACH', $attachment); + if(!is_null($attachments)) + foreach($attachments as $attachment) + $event->add('ATTACH', $attachment); // Create a timestamp for last modified, created and dtstamp values in UTC $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); $event->add('DTSTAMP', $dtStamp); $event->add('CREATED', $dtStamp); $event->add('LAST-MODIFIED', $dtStamp); // Adjust the start date, based on the given timezone information $dtStart = new \DateTime(); $dtStart->setTimezone($timezone); $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); // Only add the time values if it's not an allday event if($params['allday'] != '1') $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); // Adjust the end date, based on the given timezone information $dtEnd = new \DateTime(); $dtEnd->setTimezone($timezone); $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); // Only add the time values if it's not an allday event if($params['allday'] != '1') $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); // According to the VCal spec, we need to add a whole day here if($params['allday'] == '1') $dtEnd->add(new \DateInterval('P1D')); // Really add Start and End events $dtStartEv = $event->add('DTSTART', $dtStart); $dtEndEv = $event->add('DTEND', $dtEnd); // Adjust the DATE format for allday events if($params['allday'] == '1') { $dtStartEv['VALUE'] = 'DATE'; $dtEndEv['VALUE'] = 'DATE'; } // Actually add the values to the database $calid = $this->getCalendarIdForPage($id); $uri = uniqid('dokuwiki-').'.ics'; $now = new DateTime(); $eventStr = $vcalendar->serialize(); $values = array($calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT', $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(), strlen($eventStr), md5($eventStr), $uuid ); $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (".$this->sqlite->quote_and_join($values, ',').")"; $res = $this->sqlite->query($query); // If successfully, update the sync token database if($res !== false) { $this->updateSyncTokenLog($calid, $uri, 'added'); return true; } return false; } /** * Retrieve the calendar settings of a given calendar id * * @param string $calid The calendar ID * * @return array The calendar settings array */ public function getCalendarSettings($calid) { $query = "SELECT principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id=".$this->sqlite->quote_string($calid); $res = $this->sqlite->query($query); $row = $this->sqlite->res2row($res); return $row; } /** * Retrieve all events that are within a given date range, * based on the timezone setting. * * There is also support for retrieving recurring events, * using Sabre's VObject Iterator. Recurring events are represented * as individual calendar entries with the same UID. * * @param string $id The page ID to work with * @param string $user The user ID to work with * @param string $startDate The start date as a string * @param string $endDate The end date as a string * * @return array An array containing the calendar entries. */ - public function getEventsWithinDateRange($id, $user, $startDate, $endDate) + public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone) { - $settings = $this->getPersonalSettings($user); - if($settings['timezone'] !== '' && $settings['timezone'] !== 'local') - $timezone = new \DateTimeZone($settings['timezone']); + if($timezone !== '' && $timezone !== 'local') + $timezone = new \DateTimeZone($timezone); else $timezone = new \DateTimeZone('UTC'); $data = array(); // Load SabreDAV require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); $calid = $this->getCalendarIdForPage($id); $color = $this->getCalendarColorForCalendar($calid); $startTs = new \DateTime($startDate); $endTs = new \DateTime($endDate); // Retrieve matching calendar objects $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid=". $this->sqlite->quote_string($calid)." AND firstoccurence < ". $this->sqlite->quote_string($endTs->getTimestamp())." AND lastoccurence > ". $this->sqlite->quote_string($startTs->getTimestamp()); $res = $this->sqlite->query($query); $arr = $this->sqlite->res2arr($res); // Parse individual calendar entries foreach($arr as $row) { if(isset($row['calendardata'])) { $entry = array(); $vcal = \Sabre\VObject\Reader::read($row['calendardata']); $recurrence = $vcal->VEVENT->RRULE; // If it is a recurring event, pass it through Sabre's EventIterator if($recurrence != null) { $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT)); $rEvents->rewind(); $done = false; while($rEvents->valid() && !$done) { $event = $rEvents->getEventObject(); // If we are after the given time range, exit if(($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp()) && ($rEvents->getDtEnd()->getTimestamp() > $endTs->getTimestamp())) $done = true; // If we are before the given time range, continue if($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp()) { $rEvents->next(); continue; } // If we are within the given time range, parse the event $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true); $rEvents->next(); } } else $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color); } } return $data; } /** * Helper function that parses the iCal data of a VEVENT to a calendar entry. * * @param \Sabre\VObject\VEvent $event The event to parse * @param \DateTimeZone $timezone The timezone object * @param string $uid The entry's UID * @param boolean $recurring (optional) Set to true to define a recurring event * * @return array The parse calendar entry */ private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false) { $entry = array(); $start = $event->DTSTART; // Parse only if the start date/time is present if($start !== null) { $dtStart = $start->getDateTime(); $dtStart->setTimezone($timezone); $entry['start'] = $dtStart->format(\DateTime::ATOM); if($start['VALUE'] == 'DATE') $entry['allDay'] = true; else $entry['allDay'] = false; } $end = $event->DTEND; // Parse onlyl if the end date/time is present if($end !== null) { $dtEnd = $end->getDateTime(); $dtEnd->setTimezone($timezone); $entry['end'] = $dtEnd->format(\DateTime::ATOM); } $description = $event->DESCRIPTION; if($description !== null) $entry['description'] = (string)$description; else $entry['description'] = ''; $attachments = $event->ATTACH; if($attachments !== null) { $entry['attachments'] = array(); foreach($attachments as $attachment) $entry['attachments'][] = (string)$attachment; } $entry['title'] = (string)$event->summary; $entry['id'] = $uid; $entry['page'] = $page; $entry['color'] = $color; $entry['recurring'] = $recurring; return $entry; } /** * Retrieve an event by its UID * * @param string $uid The event's UID * * @return mixed The table row with the given event */ public function getEventWithUid($uid) { $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid=". $this->sqlite->quote_string($uid); $res = $this->sqlite->query($query); $row = $this->sqlite->res2row($res); return $row; } /** * Retrieve all calendar events for a given calendar ID * * @param string $calid The calendar's ID * * @return array An array containing all calendar data */ public function getAllCalendarEvents($calid) { $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid=". $this->sqlite->quote_string($calid); $res = $this->sqlite->query($query); $arr = $this->sqlite->res2arr($res); return $arr; } /** * Edit a calendar entry for a page, given by its parameters. * The params array has the same format as @see addCalendarEntryForPage * * @param string $id The page's ID to work on * @param string $user The user's ID to work on * @param array $params The parameter array for the edited calendar event * * @return boolean True on success, otherwise false */ public function editCalendarEntryForPage($id, $user, $params) { - $settings = $this->getPersonalSettings($user); - if($settings['timezone'] !== '' && $settings['timezone'] !== 'local') - $timezone = new \DateTimeZone($settings['timezone']); - elseif($settings['timezone'] === 'local') + if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') + $timezone = new \DateTimeZone($params['currenttz']); + elseif($params['currenttz'] === 'local') $timezone = new \DateTimeZone($params['detectedtz']); else $timezone = new \DateTimeZone('UTC'); // Parse dates $startDate = explode('-', $params['eventfrom']); $startTime = explode(':', $params['eventfromtime']); $endDate = explode('-', $params['eventto']); $endTime = explode(':', $params['eventtotime']); // Retrieve the existing event based on the UID $uid = $params['uid']; $event = $this->getEventWithUid($uid); // Load SabreDAV require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); if(!isset($event['calendardata'])) return false; $uri = $event['uri']; $calid = $event['calendarid']; // Parse the existing event $vcal = \Sabre\VObject\Reader::read($event['calendardata']); $vevent = $vcal->VEVENT; // Set the new event values $vevent->summary = $params['eventname']; $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); $description = $params['eventdescription']; // Remove existing timestamps to overwrite them $vevent->remove('DESCRIPTION'); $vevent->remove('DTSTAMP'); $vevent->remove('LAST-MODIFIED'); $vevent->remove('ATTACH'); // Add new time stamps and description $vevent->add('DTSTAMP', $dtStamp); $vevent->add('LAST-MODIFIED', $dtStamp); if($description !== '') $vevent->add('DESCRIPTION', $description); // Add attachments $attachments = $params['attachments']; - foreach($attachments as $attachment) - $vevent->add('ATTACH', $attachment); + if(!is_null($attachments)) + foreach($attachments as $attachment) + $vevent->add('ATTACH', $attachment); // Setup DTSTART $dtStart = new \DateTime(); $dtStart->setTimezone($timezone); $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); if($params['allday'] != '1') $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); // Setup DTEND $dtEnd = new \DateTime(); $dtEnd->setTimezone($timezone); $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); if($params['allday'] != '1') $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); // According to the VCal spec, we need to add a whole day here if($params['allday'] == '1') $dtEnd->add(new \DateInterval('P1D')); $vevent->remove('DTSTART'); $vevent->remove('DTEND'); $dtStartEv = $vevent->add('DTSTART', $dtStart); $dtEndEv = $vevent->add('DTEND', $dtEnd); // Remove the time for allday events if($params['allday'] == '1') { $dtStartEv['VALUE'] = 'DATE'; $dtEndEv['VALUE'] = 'DATE'; } $now = new DateTime(); $eventStr = $vcal->serialize(); // Actually write to the database $query = "UPDATE calendarobjects SET calendardata=".$this->sqlite->quote_string($eventStr). ", lastmodified=".$this->sqlite->quote_string($now->getTimestamp()). ", firstoccurence=".$this->sqlite->quote_string($dtStart->getTimestamp()). ", lastoccurence=".$this->sqlite->quote_string($dtEnd->getTimestamp()). ", size=".strlen($eventStr). ", etag=".$this->sqlite->quote_string(md5($eventStr)). " WHERE uid=".$this->sqlite->quote_string($uid); $res = $this->sqlite->query($query); if($res !== false) { $this->updateSyncTokenLog($calid, $uri, 'modified'); return true; } return false; } /** * Delete a calendar entry for a given page. Actually, the event is removed * based on the entry's UID, so that page ID is no used. * * @param string $id The page's ID (unused) * @param array $params The parameter array to work with * * @return boolean True */ public function deleteCalendarEntryForPage($id, $params) { $uid = $params['uid']; $event = $this->getEventWithUid($uid); $calid = $event['calendarid']; $uri = $event['uri']; $query = "DELETE FROM calendarobjects WHERE uid=".$this->sqlite->quote_string($uid); $res = $this->sqlite->query($query); if($res !== false) { $this->updateSyncTokenLog($calid, $uri, 'deleted'); } return true; } /** * Retrieve the current sync token for a calendar * * @param string $calid The calendar id * * @return mixed The synctoken or false */ public function getSyncTokenForCalendar($calid) { $row = $this->getCalendarSettings($calid); if(isset($row['synctoken'])) return $row['synctoken']; return false; } /** * Helper function to convert the operation name to * an operation code as stored in the database * * @param string $operationName The operation name * * @return mixed The operation code or false */ public function operationNameToOperation($operationName) { switch($operationName) { case 'added': return 1; break; case 'modified': return 2; break; case 'deleted': return 3; break; } return false; } /** * Update the sync token log based on the calendar id and the * operation that was performed. * * @param string $calid The calendar ID that was modified * @param string $uri The calendar URI that was modified * @param string $operation The operation that was performed * * @return boolean True on success, otherwise false */ private function updateSyncTokenLog($calid, $uri, $operation) { $currentToken = $this->getSyncTokenForCalendar($calid); $operationCode = $this->operationNameToOperation($operation); if(($operationCode === false) || ($currentToken === false)) return false; $values = array($uri, $currentToken, $calid, $operationCode ); $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(". $this->sqlite->quote_and_join($values, ',').")"; $res = $this->sqlite->query($query); if($res === false) return false; $currentToken++; $query = "UPDATE calendars SET synctoken=".$this->sqlite->quote_string($currentToken)." WHERE id=". $this->sqlite->quote_string($calid); $res = $this->sqlite->query($query); return ($res !== false); } /** * Return the sync URL for a given Page, i.e. a calendar * * @param string $id The page's ID * @param string $user (optional) The user's ID * * @return mixed The sync url or false */ public function getSyncUrlForPage($id, $user = null) { if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { return false; } } $calid = $this->getCalendarIdForPage($id); if($calid === false) return false; $calsettings = $this->getCalendarSettings($calid); if(!isset($calsettings['uri'])) return false; $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri']; return $syncurl; } /** * Return the private calendar's URL for a given page * * @param string $id the page ID * * @return mixed The private URL or false */ public function getPrivateURLForPage($id) { $calid = $this->getCalendarIdForPage($id); if($calid === false) return false; return $this->getPrivateURLForCalendar($calid); } /** * Return the private calendar's URL for a given calendar ID * * @param string $calid The calendar's ID * * @return mixed The private URL or false */ public function getPrivateURLForCalendar($calid) { if(isset($this->cachedValues['privateurl'][$calid])) return $this->cachedValues['privateurl'][$calid]; $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid=".$this->sqlite->quote_string($calid); $res = $this->sqlite->query($query); $row = $this->sqlite->res2row($res); if(!isset($row['url'])) { $url = uniqid("dokuwiki-").".ics"; $values = array( $url, $calid ); $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(". $this->sqlite->quote_and_join($values, ", ").")"; $res = $this->sqlite->query($query); if($res === false) return false; } else { $url = $row['url']; } $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url; $this->cachedValues['privateurl'][$calid] = $url; return $url; } /** * Retrieve the calendar ID for a given private calendar URL * * @param string $url The private URL * * @return mixed The calendar ID or false */ public function getCalendarForPrivateURL($url) { $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url=".$this->sqlite->quote_string($url); $res = $this->sqlite->query($query); $row = $this->sqlite->res2row($res); if(!isset($row['calid'])) return false; return $row['calid']; } /** * Return a given calendar as ICS feed, i.e. all events in one ICS file. * * @param string $caldi The calendar ID to retrieve * * @return mixed The calendar events as string or false */ public function getCalendarAsICSFeed($calid) { $calSettings = $this->getCalendarSettings($calid); if($calSettings === false) return false; $events = $this->getAllCalendarEvents($calid); if($events === false) return false; $out = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\nCALSCALE:GREGORIAN\nX-WR-CALNAME:"; $out .= $calSettings['displayname']."\n"; foreach($events as $event) { $out .= rtrim($event['calendardata']); $out .= "\n"; } $out .= "END:VCALENDAR\n"; return $out; } /** * Retrieve a configuration option for the plugin * * @param string $key The key to query * @return mixed The option set, null if not found */ public function getConfig($key) { return $this->getConf($key); } } diff --git a/lang/de-informal/lang.php b/lang/de-informal/lang.php --- a/lang/de-informal/lang.php +++ b/lang/de-informal/lang.php @@ -1,52 +1,54 @@ '); jQuery.post( DOKU_BASE + 'lib/exe/ajax.php', { call: 'plugin_davcal', id: dw_davcal__modals.page, page: dw_davcal__modals.page, action: 'saveSettings', params: postArray }, function(data) { var result = data['result']; var html = data['html']; jQuery('#dw_davcal__ajaxsettings').html(html); if(result === true) { location.reload(); } } ); }; } dialogButtons[LANG.plugins.davcal['cancel']] = function () { dw_davcal__modals.hideSettingsDialog(); }; var settingsHtml = '
'; if(JSINFO.plugin.davcal['disable_settings'] && JSINFO.plugin.davcal['disable_sync'] && JSINFO.plugin.davcal['disable_ics']) { settingsHtml += LANG.plugins.davcal['nothing_to_show']; } if(!JSINFO.plugin.davcal['disable_settings']) { settingsHtml += '' + '' + '' + ''; } if(!JSINFO.plugin.davcal['disable_sync']) { settingsHtml += ''; } if(!JSINFO.plugin.davcal['disable_ics']) { settingsHtml += ''; } settingsHtml += '
' + LANG.plugins.davcal['timezone'] + '
' + LANG.plugins.davcal['weeknumbers'] + '
' + LANG.plugins.davcal['only_workweek'] + '
' + LANG.plugins.davcal['start_monday'] + '
' + LANG.plugins.davcal['sync_url'] + '
' + LANG.plugins.davcal['private_url'] + '
' + '
' + '
'; dw_davcal__modals.$settingsDialog = jQuery(document.createElement('div')) .dialog({ autoOpen: false, draggable: true, title: LANG.plugins.davcal['settings'], resizable: true, buttons: dialogButtons, }) .html( settingsHtml ) .parent() .attr('id','dw_davcal__settings') .show() .appendTo('.dokuwiki:first'); jQuery('#dw_davcal__settings').position({ my: "center", at: "center", of: window }); // Initialize current settings if(!JSINFO.plugin.davcal['disable_settings']) { var $tzdropdown = jQuery('#dw_davcal__settings_timezone'); jQuery('#fullCalendarTimezoneList option').each(function() { jQuery(''); } if(edit || (dw_davcal__modals.settings['calids'].length < 1)) { $dropdown.prop('disabled', true); } // Set up existing/predefined values jQuery('#dw_davcal__tz_edit').val(dw_davcal__modals.detectedTz); + jQuery('#dw_davcal__currenttz_edit').val(dw_davcal__modals.currentTz); jQuery('#dw_davcal__uid_edit').val(calEvent.id); jQuery('#dw_davcal__eventname_edit').val(calEvent.title); jQuery('#dw_davcal__eventfrom_edit').val(calEvent.start.format('YYYY-MM-DD')); jQuery('#dw_davcal__eventfromtime_edit').val(calEvent.start.format('HH:mm')); jQuery('#dw_davcal__eventdescription_edit').val(calEvent.description); if(calEvent.attachments && (calEvent.attachments !== null)) { for(var i=0; i' + url + '' + LANG.plugins.davcal['delete'] + ''; jQuery('#dw_davcal__editevent_attachments > tbody:last').append(row); } } dw_davcal__modals.attachAttachmentDeleteHandlers(); jQuery('#dw_davcal__editevent_attach').on("click", function(e) { e.preventDefault(); var url = jQuery('#dw_davcal__editevent_attachment').val(); jQuery('#dw_davcal__editevent_attachment').val('http://'); var row = '' + url + '' + LANG.plugins.davcal['delete'] + ''; jQuery('#dw_davcal__editevent_attachments > tbody:last').append(row); dw_davcal__modals.attachAttachmentDeleteHandlers(); return false; }); if(calEvent.allDay && (calEvent.end === null)) { jQuery('#dw_davcal__eventto_edit').val(calEvent.start.format('YYYY-MM-DD')); jQuery('#dw_davcal__eventtotime_edit').val(calEvent.start.format('HH:mm')); } else if(calEvent.allDay) { endEvent = moment(calEvent.end); endEvent.subtract(1, 'days'); jQuery('#dw_davcal__eventto_edit').val(endEvent.format('YYYY-MM-DD')); jQuery('#dw_davcal__eventotime_edit').val(endEvent.format('HH:mm')); } else { jQuery('#dw_davcal__eventto_edit').val(calEvent.end.format('YYYY-MM-DD')); jQuery('#dw_davcal__eventtotime_edit').val(calEvent.end.format('HH:mm')); } jQuery('#dw_davcal__allday_edit').prop('checked', calEvent.allDay); // attach event handlers jQuery('#dw_davcal__edit .ui-dialog-titlebar-close').click(function(){ dw_davcal__modals.hideEditEventDialog(); }); jQuery('#dw_davcal__eventfrom_edit').datetimepicker({format:'YYYY-MM-DD', formatDate:'YYYY-MM-DD', datepicker: true, timepicker: false, }); jQuery('#dw_davcal__eventfromtime_edit').datetimepicker({format:'HH:mm', formatTime:'HH:mm', datepicker: false, timepicker: true, step: 15}); jQuery('#dw_davcal__eventto_edit').datetimepicker({format:'YYYY-MM-DD', formatDate:'YYYY-MM-DD', datepicker: true, timepicker: false, }); jQuery('#dw_davcal__eventtotime_edit').datetimepicker({format:'HH:mm', formatTime:'HH:mm', datepicker: false, timepicker: true, step:15}); jQuery('#dw_davcal__allday_edit').change(function() { if(jQuery(this).is(":checked")) { jQuery('#dw_davcal__eventfromtime_edit').prop('readonly', true); jQuery('#dw_davcal__eventtotime_edit').prop('readonly', true); } else { jQuery('#dw_davcal__eventfromtime_edit').prop('readonly', false); jQuery('#dw_davcal__eventtotime_edit').prop('readonly', false); } }); jQuery('#dw_davcal__allday_edit').change(); }, + /** + * Attach handles to delete the attachments to all 'delete' links + */ attachAttachmentDeleteHandlers: function() { jQuery("#dw_davcal__editevent_attachments .deleteLink").on("click", function(e) { e.preventDefault(); var tr = jQuery(this).closest('tr'); tr.css("background-color", "#FF3700"); tr.fadeOut(400, function() { tr.remove(); }); return false; }); }, /** * Show an info/confirmation dialog * @param {Object} confirm Whether a confirmation dialog (true) or an info dialog (false) is requested */ showDialog : function(confirm) { if(dw_davcal__modals.$confirmDialog) return; var dialogButtons = {}; var title = ''; if(confirm) { title = LANG.plugins.davcal['confirmation']; var pageid = dw_davcal__modals.page; if(dw_davcal__modals.settings['multi']) { pageid = jQuery("#dw_davcal__editevent_calendar option:selected").val(); } dialogButtons[LANG.plugins.davcal['yes']] = function() { jQuery.post( DOKU_BASE + 'lib/exe/ajax.php', { call: 'plugin_davcal', id: pageid, page: dw_davcal__modals.page, action: dw_davcal__modals.action, params: { uid: dw_davcal__modals.uid } }, function(data) { dw_davcal__modals.completeCb(data); } ); dw_davcal__modals.hideDialog(); }; dialogButtons[LANG.plugins.davcal['cancel']] = function() { dw_davcal__modals.hideDialog(); }; } else { title = LANG.plugins.davcal['info']; dialogButtons[LANG.plugins.davcal['ok']] = function() { dw_davcal__modals.hideDialog(); }; } dw_davcal__modals.$dialog = jQuery(document.createElement('div')) .dialog({ autoOpen: false, draggable: true, title: title, resizable: true, buttons: dialogButtons, }) .html( '
' + dw_davcal__modals.msg + '
' ) .parent() .attr('id','dw_davcal__confirm') .show() .appendTo('.dokuwiki:first'); jQuery('#dw_davcal__confirm').position({ my: "center", at: "center", of: window }); // attach event handlers jQuery('#dw_davcal__confirm .ui-dialog-titlebar-close').click(function(){ dw_davcal__modals.hideDialog(); }); }, /** * Hide the edit event dialog */ hideEditEventDialog : function() { dw_davcal__modals.$editEventDialog.empty(); dw_davcal__modals.$editEventDialog.remove(); dw_davcal__modals.$editEventDialog = null; }, /** * Hide the confirm/info dialog */ hideDialog: function() { dw_davcal__modals.$dialog.empty(); dw_davcal__modals.$dialog.remove(); dw_davcal__modals.$dialog = null; }, /** * Hide the settings dialog */ hideSettingsDialog: function() { dw_davcal__modals.$settingsDialog.empty(); dw_davcal__modals.$settingsDialog.remove(); dw_davcal__modals.$settingsDialog = null; } }; diff --git a/syntax/calendar.php b/syntax/calendar.php --- a/syntax/calendar.php +++ b/syntax/calendar.php @@ -1,145 +1,155 @@ */ // must be run within Dokuwiki if(!defined('DOKU_INC')) die(); if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); require_once(DOKU_PLUGIN.'syntax.php'); class syntax_plugin_davcal_calendar extends DokuWiki_Syntax_Plugin { protected $hlp = null; // Load the helper plugin public function syntax_plugin_davcal_calendar() { $this->hlp =& plugin_load('helper', 'davcal'); } /** * What kind of syntax are we? */ function getType(){ return 'substition'; } /** * What about paragraphs? */ function getPType(){ return 'normal'; } /** * Where to sort in? */ function getSort(){ return 165; } /** * Connect pattern to lexer */ function connectTo($mode) { $this->Lexer->addSpecialPattern('\{\{davcal>[^}]*\}\}',$mode,'plugin_davcal_calendar'); } /** * Handle the match */ function handle($match, $state, $pos, &$handler){ global $ID; $options = trim(substr($match,9,-2)); $options = explode(',', $options); $data = array('name' => $ID, 'description' => $this->getLang('created_by_davcal'), 'id' => array(), 'settings' => 'show', - 'view' => 'month' + 'view' => 'month', + 'forcetimezone' => 'no' ); $lastid = $ID; foreach($options as $option) { list($key, $val) = explode('=', $option); $key = strtolower(trim($key)); $val = trim($val); switch($key) { case 'id': $lastid = $val; if(!in_array($val, $data['id'])) $data['id'][$val] = '#3a87ad'; break; case 'color': $data['id'][$lastid] = $val; break; case 'view': if(in_array($val, array('month', 'basicDay', 'basicWeek', 'agendaWeek', 'agendaDay'))) $data['view'] = $val; else $data['view'] = 'month'; break; + case 'forcetimezone': + $tzlist = \DateTimeZone::listIdentifiers(DateTimeZone::ALL); + if(in_array($val, $tzlist) || $val === 'no') + $data['forcetimezone'] = $val; + else + msg($this->getLang('error_timezone_not_in_list'), -1); + break; default: $data[$key] = $val; } } // Handle the default case when the user didn't enter a different ID if(empty($data['id'])) { $data['id'] = array($ID => '#3a87ad'); } // Only update the calendar name/description if the ID matches the page ID. // Otherwise, the calendar is included in another page and we don't want // to interfere with its data. if(in_array($ID, array_keys($data['id']))) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) $username = $_SERVER['REMOTE_USER']; else $username = uniqid('davcal-'); $this->hlp->setCalendarNameForPage($data['name'], $data['description'], $ID, $username); $this->hlp->setCalendarColorForPage($data['id'][$ID], $ID); } p_set_metadata($ID, array('plugin_davcal' => $data)); return $data; } /** * Create output */ function render($format, &$R, $data) { if($format != 'xhtml') return false; global $ID; $tzlist = \DateTimeZone::listIdentifiers(DateTimeZone::ALL); // Render the Calendar. Timezone list is within a hidden div, // the calendar ID is in a data-calendarid tag. + if($data['forcetimezone'] !== 'no') + $R->doc .= '
'.sprintf($this->getLang('this_calendar_uses_timezone'), $data['forcetimezone']).'
'; $R->doc .= '
'; $R->doc .= ''; if(($this->getConf('hide_settings') !== 1) && ($data['settings'] !== 'hide')) { $R->doc .= '
'.$this->getLang('settings').'
'; } } } // vim:ts=4:sw=4:et:enc=utf-8: diff --git a/syntax/table.php b/syntax/table.php --- a/syntax/table.php +++ b/syntax/table.php @@ -1,196 +1,206 @@ */ // must be run within Dokuwiki if(!defined('DOKU_INC')) die(); if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); require_once(DOKU_PLUGIN.'syntax.php'); class syntax_plugin_davcal_table extends DokuWiki_Syntax_Plugin { protected $hlp = null; // Load the helper plugin public function syntax_plugin_davcal_table() { $this->hlp =& plugin_load('helper', 'davcal'); } /** * What kind of syntax are we? */ function getType(){ return 'substition'; } /** * What about paragraphs? */ function getPType(){ return 'normal'; } /** * Where to sort in? */ function getSort(){ return 165; } /** * Connect pattern to lexer */ function connectTo($mode) { $this->Lexer->addSpecialPattern('\{\{davcaltable>[^}]*\}\}',$mode,'plugin_davcal_table'); } /** * Handle the match */ function handle($match, $state, $pos, &$handler){ global $ID; $options = trim(substr($match,14,-2)); $options = explode(',', $options); $data = array('id' => array(), 'startdate' => 'today', 'numdays' => 30, 'dateformat' => 'Y-m-d H:i', 'onlystart' => false, - 'sort' => 'desc' + 'sort' => 'desc', + 'timezone' => 'local' ); $lastid = $ID; foreach($options as $option) { list($key, $val) = explode('=', $option); $key = strtolower(trim($key)); $val = trim($val); switch($key) { case 'id': $lastid = $val; if(!in_array($val, $data['id'])) $data['id'][$val] = '#3a87ad'; break; case 'onlystart': if(($val === 'on') || ($val === 'true')) $data['onlystart'] = true; break; + case 'timezone': + $tzlist = \DateTimeZone::listIdentifiers(DateTimeZone::ALL); + if(in_array($val, $tzlist) || $val === 'no') + $data['timezone'] = $val; + else + msg($this->getLang('error_timezone_not_in_list'), -1); + break; default: $data[$key] = $val; } } // Handle the default case when the user didn't enter a different ID if(empty($data['id'])) { $data['id'] = array($ID => '#3a87ad'); } return $data; } private static function sort_events_asc($a, $b) { $from1 = new \DateTime($a['start']); $from2 = new \DateTime($b['start']); return $from2 < $from1; } private static function sort_events_desc($a, $b) { $from1 = new \DateTime($a['start']); $from2 = new \DateTime($b['start']); return $from1 < $from2; } /** * Create output */ function render($format, &$R, $data) { if($format == 'metadata') { $R->meta['plugin_davcal']['table'] = true; return true; } if(($format != 'xhtml') && ($format != 'odt')) return false; global $ID; $events = array(); $from = $data['startdate']; if($from === 'today') $from = new \DateTime(); else $from = new \DateTime($from); $to = clone $from; $to->add(new \DateInterval('P'.$data['numdays'].'D')); + $timezone = $data['timezone']; foreach($data['id'] as $calPage => $color) { $events = array_merge($events, $this->hlp->getEventsWithinDateRange($calPage, - $user, $from->format('Y-m-d'), $to->format('Y-m-d'))); + $user, $from->format('Y-m-d'), $to->format('Y-m-d'), + $timezone)); } if($data['sort'] === 'desc') usort($events, array("syntax_plugin_davcal_table", "sort_events_desc")); else usort($events, array("syntax_plugin_davcal_table", "sort_events_asc")); $R->table_open(); $R->tablethead_open(); $R->tableheader_open(); $R->doc .= $data['onlystart'] ? $this->getLang('at') : $this->getLang('from'); $R->tableheader_close(); if(!$data['onlystart']) { $R->tableheader_open(); $R->doc .= $this->getLang('to'); $R->tableheader_close(); } $R->tableheader_open(); $R->doc .= $this->getLang('title'); $R->tableheader_close(); $R->tableheader_open(); $R->doc .= $this->getLang('description'); $R->tableheader_close(); $R->tablethead_close(); foreach($events as $event) { $R->tablerow_open(); $R->tablecell_open(); $from = new \DateTime($event['start']); $R->doc .= $from->format($data['dateformat']); $R->tablecell_close(); if(!$data['onlystart']) { $to = new \DateTime($event['end']); // Fixup all day events, which have one day in excess if($event['allDay'] === true) { $to->sub(new \DateInterval('P1D')); } $R->tablecell_open(); $R->doc .= $to->format($data['dateformat']); $R->tablecell_close(); } $R->tablecell_open(); $R->doc .= $event['title']; $R->tablecell_close(); $R->tablecell_open(); $R->doc .= $event['description']; $R->tablecell_close(); $R->tablerow_close(); } $R->table_close(); } } // vim:ts=4:sw=4:et:enc=utf-8: