diff --git a/action/ajax.php b/action/ajax.php --- a/action/ajax.php +++ b/action/ajax.php @@ -1,129 +1,139 @@ 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')); $params = $INPUT->post->arr('params'); $user = $_SERVER['REMOTE_USER']; $write = 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; } + // 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'); $data = $this->hlp->getEventsWithinDateRange($id, $user, $startDate, $endDate); 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']['syncurl'] = $this->hlp->getSyncUrlForPage($id, $user); $data['settings']['privateurl'] = $this->hlp->getPrivateURLForPage($id); break; + // Save personal settings case 'saveSettings': $settings = array(); $settings['weeknumbers'] = $params['weeknumbers']; $settings['timezone'] = $params['timezone']; $settings['workweek'] = $params['workweek']; 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/action/jsinfo.php b/action/jsinfo.php --- a/action/jsinfo.php +++ b/action/jsinfo.php @@ -1,28 +1,35 @@ register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'add_jsinfo_information'); } + /** + * Add the language variable to the JSINFO variable + */ function add_jsinfo_information(&$event, $param) { global $conf; global $JSINFO; $lang = $conf['lang']; if(strpos($lang, "de") === 0) { $lc = 'de'; } else { $lc = 'en'; } $JSINFO['plugin']['davcal']['language'] = $lc; } } diff --git a/authBackendDokuwiki.php b/authBackendDokuwiki.php --- a/authBackendDokuwiki.php +++ b/authBackendDokuwiki.php @@ -1,10 +1,16 @@ checkPass($username, $password); } } diff --git a/calendarserver.php b/calendarserver.php --- a/calendarserver.php +++ b/calendarserver.php @@ -1,96 +1,86 @@ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //Mapping PHP errors to exceptions function exception_error_handler($errno, $errstr, $errfile, $errline) { throw new ErrorException($errstr, 0, $errno, $errfile, $errline); } //set_error_handler("exception_error_handler"); // Files we need require_once 'vendor/autoload.php'; require_once('authBackendDokuwiki.php'); require_once('principalBackendDokuwiki.php'); require_once('calendarBackendDokuwiki.php'); -// Backends +// Backends - our DokuWiki backends $authBackend = new DokuWikiSabreAuthBackend(); -$calendarBackend = new DokuWikiSabreCalendarBackend($pdo); //Sabre\CalDAV\Backend\PDO($pdo); +$calendarBackend = new DokuWikiSabreCalendarBackend($pdo); $principalBackend = new DokuWikiSabrePrincipalBackend(); // Directory structure $tree = [ new Sabre\CalDAV\Principal\Collection($principalBackend), new Sabre\CalDAV\CalendarRoot($principalBackend, $calendarBackend), ]; $server = new Sabre\DAV\Server($tree); if (isset($baseUri)) $server->setBaseUri($baseUri); /* Server Plugins */ $authPlugin = new Sabre\DAV\Auth\Plugin($authBackend); $server->addPlugin($authPlugin); $aclPlugin = new Sabre\DAVACL\Plugin(); $server->addPlugin($aclPlugin); /* CalDAV support */ $caldavPlugin = new Sabre\CalDAV\Plugin(); $server->addPlugin($caldavPlugin); /* Calendar subscription support */ //$server->addPlugin( // new Sabre\CalDAV\Subscriptions\Plugin() //); /* Calendar scheduling support */ //$server->addPlugin( // new Sabre\CalDAV\Schedule\Plugin() //); /* WebDAV-Sync plugin */ $server->addPlugin(new Sabre\DAV\Sync\Plugin()); // Support for html frontend $browser = new Sabre\DAV\Browser\Plugin(); $server->addPlugin($browser); // And off we go! $server->exec(); diff --git a/helper.php b/helper.php --- a/helper.php +++ b/helper.php @@ -1,579 +1,859 @@ sqlite =& plugin_load('helper', 'sqlite'); if(!$this->sqlite) { msg('This plugin requires the sqlite plugin. Please install it.'); return; } if(!$this->sqlite->init('davcal', DOKU_PLUGIN.'davcal/db/')) { return; } } + /** + * 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)) $userid = $_SERVER['REMOTE_USER']; $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)) $userid = $_SERVER['REMOTE_USER']; $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"); 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) { if(is_null($userid)) $userid = $_SERVER['REMOTE_USER']; // Some sane default settings $settings = array( 'timezone' => 'local', 'weeknumbers' => '0', 'workweek' => '0' ); $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']; } 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; } $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'])) return $row['calid']; else 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) { $user = explode('/', $principalUri); $user = end($user); $mapping = $this->getCalendarIdToPageMapping(); $calids = array(); foreach($mapping as $row) { $id = $row['calid']; $page = $row['page']; $acl = auth_quickaclcheck($page); 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)) $userid = $_SERVER['REMOTE_USER']; $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 + * 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') $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('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); + + // 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), - uniqid() + $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, 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) { $settings = $this->getPersonalSettings($user); if($settings['timezone'] !== '' && $settings['timezone'] !== 'local') $timezone = new \DateTimeZone($settings['timezone']); else $timezone = new \DateTimeZone('UTC'); $data = array(); + + // Load SabreDAV require_once('vendor/autoload.php'); $calid = $this->getCalendarIdForPage($id); $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, $timezone, $row['uid']); $rEvents->next(); } } else $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $timezone, $row['uid']); } } 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 + * + * @return array The parse calendar entry + */ private function convertIcalDataToEntry($event, $timezone, $uid) { $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'] = ''; $entry['title'] = (string)$event->summary; $entry['id'] = $uid; 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') $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('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'); + + // Add new time stamps and description $vevent->add('DTSTAMP', $dtStamp); $vevent->add('LAST-MODIFIED', $dtStamp); if($description !== '') - $vevent->add('DESCRIPTION', $description); + $vevent->add('DESCRIPTION', $description); + + // 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 DETEND $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($user)) $user = $_SERVER['REMOTE_USER']; $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) { $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']; } return DOKU_URL.'lib/plugins/davcal/ics.php/'.$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; } } diff --git a/ics.php b/ics.php --- a/ics.php +++ b/ics.php @@ -1,22 +1,29 @@ getCalendarForPrivateURL($icsFile); if($calid === false) die("No calendar with this name known."); +// Retrieve calendar contents and serve $stream = $hlp->getCalendarAsICSFeed($calid); header("Content-Type: text/calendar"); header("Content-Transfer-Encoding: Binary"); header("Content-disposition: attachment; filename=\"calendar.ics\""); echo $stream; \ No newline at end of file diff --git a/principalBackendDokuwiki.php b/principalBackendDokuwiki.php --- a/principalBackendDokuwiki.php +++ b/principalBackendDokuwiki.php @@ -1,61 +1,66 @@ retrieveUsers(); $principals = array(); foreach($users as $user => $info) { $principal = 'principals/'.$user; if(strpos($principal, $prefixPath) === 0) $data = $this->getPrincipalByPath($user); if(!empty($data)) $principals[] = $data; } return $principals; } public function getPrincipalByPath($path) { global $auth; $user = str_replace('principals/', '', $path); $userData = $auth->getUserData($user); if($userData === false) return array(); return array('uri' => 'principals/'.$user, 'email' => $userData['mail'], 'displayname' => $userData['name'], 'id' => 0); } public function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) { } public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { } public function getGroupMemberSet($principal) { return array(); } public function getGroupMemberShip($principal) { return array(); } public function setGroupMemberSet($principal, array $members) { throw new Exception\NotImplemented('Not Implemented'); } } diff --git a/script.js b/script.js --- a/script.js +++ b/script.js @@ -1,553 +1,596 @@ /* DOKUWIKI:include_once fullcalendar-2.4.0/moment.js */ /* DOKUWIKI:include_once fullcalendar-2.4.0/fullcalendar.js */ /* DOKUWIKI:include_once fullcalendar-2.4.0/lang/de.js */ /* DOKUWIKI:include_once fullcalendar-2.4.0/lang/en.js */ /* DOKUWIKI:include_once datetimepicker-2.4.5/jquery.datetimepicker.js */ /* DOKUWIKI:include_once jstz.js */ +/** + * Initialize the DAVCal script, attaching some event handlers and triggering + * the initial load of the fullcalendar JS + */ jQuery(function() { // Redefine functions for using moment.js with datetimepicker Date.parseDate = function( input, format ){ return moment(input,format).toDate(); }; Date.prototype.dateFormat = function( format ){ return moment(this).format(format); }; // Attach to event links var calendarid = jQuery('#fullCalendar').data('calendarid'); dw_davcal__modals.calid = calendarid; jQuery('div.fullCalendarSettings a').each(function() { var $link = jQuery(this); var href = $link.attr('href'); if (!href) return; $link.click( function(e) { dw_davcal__modals.showSettingsDialog(); e.preventDefault(); return ''; } ); } ); + // First, retrieve the current settings. + // Upon success, initialize fullcalendar. var postArray = { }; jQuery.post( DOKU_BASE + 'lib/exe/ajax.php', { call: 'plugin_davcal', id: dw_davcal__modals.calid, action: 'getSettings', params: postArray }, function(data) { var result = data['result']; if(result === true) { dw_davcal__modals.settings = data['settings']; var wknum = false; var tz = false; var we = true; var detectedTz = jstz.determine().name(); dw_davcal__modals.detectedTz = detectedTz; if(data['settings']['weeknumbers'] == 1) wknum = true; if(data['settings']['timezone'] !== '') tz = data['settings']['timezone']; if(data['settings']['workweek'] == 1) we = false; // Initialize the davcal popup var res = jQuery('#fullCalendar').fullCalendar({ dayClick: function(date, jsEvent, view) { dw_davcal__modals.showEditEventDialog(date, false); }, eventClick: function(calEvent, jsEvent, view) { dw_davcal__modals.showEditEventDialog(calEvent, true); }, events: { url: DOKU_BASE + 'lib/exe/ajax.php', type: 'POST', data: { call: 'plugin_davcal', action: 'getEvents', id: dw_davcal__modals.calid }, error: function() { dw_davcal__modals.msg = LANG.plugins.davcal['error_retrieving_data']; dw_davcal__modals.showDialog(false); } }, header: { left: 'title', center: 'today prev,next', right: 'month,agendaWeek,agendaDay' }, lang: JSINFO.plugin.davcal['language'], weekNumbers: wknum, timezone: tz, weekends: we, }); } } ); }); +/** + * This holds all modal windows that DAVCal uses. + */ var dw_davcal__modals = { $editEventDialog: null, $dialog: null, $settingsDialog: null, msg: null, completeCb: null, action: null, uid: null, settings: null, calid: null, detectedTz: null, + /** + * Show the settings dialog + */ showSettingsDialog : function() { if(dw_davcal__modals.$settingsDialog) return; + // Dialog buttons are language-dependent and defined here. + // Attach event handlers for save and cancel. var dialogButtons = {}; dialogButtons[LANG.plugins.davcal['save']] = function() { var postArray = { }; jQuery("input[class=dw_davcal__settings], select[class=dw_davcal__settings]").each(function() { if(jQuery(this).attr('type') == 'checkbox') { postArray[jQuery(this).prop('name')] = jQuery(this).prop('checked') ? 1 : 0; } else { postArray[jQuery(this).prop('name')] = jQuery(this).val(); } }); jQuery('#dw_davcal__ajaxsettings').html(''); jQuery.post( DOKU_BASE + 'lib/exe/ajax.php', { call: 'plugin_davcal', id: dw_davcal__modals.calid, 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(); }; dw_davcal__modals.$settingsDialog = jQuery(document.createElement('div')) .dialog({ autoOpen: false, draggable: true, title: LANG.plugins.davcal['settings'], resizable: true, buttons: dialogButtons, }) .html( '
' + //'' + '' + '' + '' + '' + '' + '
' + LANG.plugins.davcal['use_lang_tz'] + '
' + LANG.plugins.davcal['timezone'] + '
' + LANG.plugins.davcal['weeknumbers'] + '
' + LANG.plugins.davcal['only_workweek'] + '
' + LANG.plugins.davcal['sync_url'] + '
' + LANG.plugins.davcal['private_url'] + '
' + '
' + '
' ) .parent() .attr('id','dw_davcal__settings') .show() .appendTo('.dokuwiki:first'); jQuery('#dw_davcal__settings').position({ my: "center", at: "center", of: window }); + // Initialize current settings jQuery('#dw_davcal__settings_syncurl').on('click', function() { jQuery(this).select(); }); jQuery('#dw_davcal__settings_privateurl').on('click', function() { jQuery(this).select(); }); - - // attach event handlers - jQuery('#dw_davcal__settings .ui-dialog-titlebar-close').click(function(){ - dw_davcal__modals.hideSettingsDialog(); - }); var $tzdropdown = jQuery('#dw_davcal__settings_timezone'); jQuery('#fullCalendarTimezoneList option').each(function() { jQuery('