xref: /plugin/davcal/helper.php (revision fb813b30f9da5689c01ae36ed3a46e4dcfe84ab5)
1<?php
2/**
3  * Helper Class for the DAVCal plugin
4  * This helper does the actual work.
5  *
6  */
7
8// must be run within Dokuwiki
9if(!defined('DOKU_INC')) die();
10
11class helper_plugin_davcal extends DokuWiki_Plugin {
12
13  protected $sqlite = null;
14  protected $cachedValues = array();
15
16  /**
17    * Constructor to load the configuration and the SQLite plugin
18    */
19  public function helper_plugin_davcal() {
20    $this->sqlite =& plugin_load('helper', 'sqlite');
21    if(!$this->sqlite)
22    {
23        msg('This plugin requires the sqlite plugin. Please install it.');
24        return;
25    }
26
27    if(!$this->sqlite->init('davcal', DOKU_PLUGIN.'davcal/db/'))
28    {
29        return;
30    }
31  }
32
33  /**
34   * Retrieve meta data for a given page
35   *
36   * @param string $id optional The page ID
37   * @return array The metadata
38   */
39  private function getMeta($id = null) {
40    global $ID;
41    global $INFO;
42
43    if ($id === null) $id = $ID;
44
45    if($ID === $id && $INFO['meta']) {
46        $meta = $INFO['meta'];
47    } else {
48        $meta = p_get_metadata($id);
49    }
50
51    return $meta;
52  }
53
54  /**
55   * Retrieve the meta data for a given page
56   *
57   * @param string $id optional The page ID
58   * @return array with meta data
59   */
60  public function getCalendarMetaForPage($id = null)
61  {
62      if(is_null($id))
63      {
64          global $ID;
65          $id = $ID;
66      }
67
68      $meta = $this->getMeta($id);
69      if(isset($meta['plugin_davcal']))
70        return $meta['plugin_davcal'];
71      else
72        return array();
73  }
74
75  /**
76   * Get all calendar pages used by a given page
77   * based on the stored metadata
78   *
79   * @param string $id optional The page id
80   * @return mixed The pages as array or false
81   */
82  public function getCalendarPagesByMeta($id = null)
83  {
84      if(is_null($id))
85      {
86          global $ID;
87          $id = $ID;
88      }
89
90      $meta = $this->getCalendarMetaForPage($id);
91      if(isset($meta['id']))
92      {
93          return array_keys($meta['id']);
94      }
95      return false;
96  }
97
98  /**
99   * Get a list of calendar names/pages/ids/colors
100   * for an array of page ids
101   *
102   * @param array $calendarPages The calendar pages to retrieve
103   * @return array The list
104   */
105  public function getCalendarMapForIDs($calendarPages)
106  {
107      $data = array();
108      foreach($calendarPages as $page)
109      {
110          $calid = $this->getCalendarIdForPage($page);
111          if($calid !== false)
112          {
113            $settings = $this->getCalendarSettings($calid);
114            $name = $settings['displayname'];
115            $color = $settings['calendarcolor'];
116            $data[] = array('name' => $name, 'page' => $page, 'calid' => $calid,
117                            'color' => $color);
118          }
119      }
120      return $data;
121  }
122
123  /**
124   * Get the saved calendar color for a given page.
125   *
126   * @param string $id optional The page ID
127   * @return mixed The color on success, otherwise false
128   */
129  public function getCalendarColorForPage($id = null)
130  {
131      if(is_null($id))
132      {
133          global $ID;
134          $id = $ID;
135      }
136
137      $calid = $this->getCalendarIdForPage($id);
138      if($calid === false)
139        return false;
140
141      return $this->getCalendarColorForCalendar($calid);
142  }
143
144  /**
145   * Get the saved calendar color for a given calendar ID.
146   *
147   * @param string $id optional The calendar ID
148   * @return mixed The color on success, otherwise false
149   */
150  public function getCalendarColorForCalendar($calid)
151  {
152      if(isset($this->cachedValues['calendarcolor'][$calid]))
153        return $this->cachedValues['calendarcolor'][$calid];
154
155      $row = $this->getCalendarSettings($calid);
156
157      if(!isset($row['calendarcolor']))
158        return false;
159
160      $color = $row['calendarcolor'];
161      $this->cachedValues['calendarcolor'][$calid] = $color;
162      return $color;
163  }
164
165  /**
166   * Set the calendar color for a given page.
167   *
168   * @param string $color The color definition
169   * @param string $id optional The page ID
170   * @return boolean True on success, otherwise false
171   */
172  public function setCalendarColorForPage($color, $id = null)
173  {
174      if(is_null($id))
175      {
176          global $ID;
177          $id = $ID;
178      }
179      $calid = $this->getCalendarIdForPage($id);
180      if($calid === false)
181        return false;
182
183      $query = "UPDATE calendars SET calendarcolor=".$this->sqlite->quote_string($color).
184               " WHERE id=".$this->sqlite->quote_string($calid);
185      $res = $this->sqlite->query($query);
186      if($res !== false)
187      {
188        $this->cachedValues['calendarcolor'][$calid] = $color;
189        return true;
190      }
191      return false;
192  }
193
194  /**
195   * Set the calendar name and description for a given page with a given
196   * page id.
197   * If the calendar doesn't exist, the calendar is created!
198   *
199   * @param string  $name The name of the new calendar
200   * @param string  $description The description of the new calendar
201   * @param string  $id (optional) The ID of the page
202   * @param string  $userid The userid of the creating user
203   *
204   * @return boolean True on success, otherwise false.
205   */
206  public function setCalendarNameForPage($name, $description, $id = null, $userid = null)
207  {
208      if(is_null($id))
209      {
210          global $ID;
211          $id = $ID;
212      }
213      if(is_null($userid))
214        $userid = $_SERVER['REMOTE_USER'];
215      $calid = $this->getCalendarIdForPage($id);
216      if($calid === false)
217        return $this->createCalendarForPage($name, $description, $id, $userid);
218
219      $query = "UPDATE calendars SET displayname=".$this->sqlite->quote_string($name).", ".
220               "description=".$this->sqlite->quote_string($description)." WHERE ".
221               "id=".$this->sqlite->quote_string($calid);
222      $res = $this->sqlite->query($query);
223      if($res !== false)
224        return true;
225      return false;
226  }
227
228  /**
229   * Save the personal settings to the SQLite database 'calendarsettings'.
230   *
231   * @param array  $settings The settings array to store
232   * @param string $userid (optional) The userid to store
233   *
234   * @param boolean True on success, otherwise false
235   */
236  public function savePersonalSettings($settings, $userid = null)
237  {
238      if(is_null($userid))
239          $userid = $_SERVER['REMOTE_USER'];
240      $this->sqlite->query("BEGIN TRANSACTION");
241
242      $query = "DELETE FROM calendarsettings WHERE userid=".$this->sqlite->quote_string($userid);
243      $this->sqlite->query($query);
244
245      foreach($settings as $key => $value)
246      {
247          $query = "INSERT INTO calendarsettings (userid, key, value) VALUES (".
248                   $this->sqlite->quote_string($userid).", ".
249                   $this->sqlite->quote_string($key).", ".
250                   $this->sqlite->quote_string($value).")";
251          $res = $this->sqlite->query($query);
252          if($res === false)
253              return false;
254      }
255      $this->sqlite->query("COMMIT TRANSACTION");
256      $this->cachedValues['settings'][$userid] = $settings;
257      return true;
258  }
259
260  /**
261   * Retrieve the settings array for a given user id.
262   * Some sane defaults are returned, currently:
263   *
264   *    timezone    => local
265   *    weeknumbers => 0
266   *    workweek    => 0
267   *
268   * @param string $userid (optional) The user id to retrieve
269   *
270   * @return array The settings array
271   */
272  public function getPersonalSettings($userid = null)
273  {
274      if(is_null($userid))
275        $userid = $_SERVER['REMOTE_USER'];
276
277      if(isset($this->cachedValues['settings'][$userid]))
278        return $this->cachedValues['settings'][$userid];
279      // Some sane default settings
280      $settings = array(
281        'timezone' => $this->getConf('timezone'),
282        'weeknumbers' => $this->getConf('weeknumbers'),
283        'workweek' => $this->getConf('workweek'),
284        'monday' => $this->getConf('monday')
285      );
286      $query = "SELECT key, value FROM calendarsettings WHERE userid=".$this->sqlite->quote_string($userid);
287      $res = $this->sqlite->query($query);
288      $arr = $this->sqlite->res2arr($res);
289      foreach($arr as $row)
290      {
291          $settings[$row['key']] = $row['value'];
292      }
293      $this->cachedValues['settings'][$userid] = $settings;
294      return $settings;
295  }
296
297  /**
298   * Retrieve the calendar ID based on a page ID from the SQLite table
299   * 'pagetocalendarmapping'.
300   *
301   * @param string $id (optional) The page ID to retrieve the corresponding calendar
302   *
303   * @return mixed the ID on success, otherwise false
304   */
305  public function getCalendarIdForPage($id = null)
306  {
307      if(is_null($id))
308      {
309          global $ID;
310          $id = $ID;
311      }
312
313      if(isset($this->cachedValues['calid'][$id]))
314        return $this->cachedValues['calid'][$id];
315
316      $query = "SELECT calid FROM pagetocalendarmapping WHERE page=".$this->sqlite->quote_string($id);
317      $res = $this->sqlite->query($query);
318      $row = $this->sqlite->res2row($res);
319      if(isset($row['calid']))
320      {
321        $calid = $row['calid'];
322        $this->cachedValues['calid'] = $calid;
323        return $calid;
324      }
325      return false;
326  }
327
328  /**
329   * Retrieve the complete calendar id to page mapping.
330   * This is necessary to be able to retrieve a list of
331   * calendars for a given user and check the access rights.
332   *
333   * @return array The mapping array
334   */
335  public function getCalendarIdToPageMapping()
336  {
337      $query = "SELECT calid, page FROM pagetocalendarmapping";
338      $res = $this->sqlite->query($query);
339      $arr = $this->sqlite->res2arr($res);
340      return $arr;
341  }
342
343  /**
344   * Retrieve all calendar IDs a given user has access to.
345   * The user is specified by the principalUri, so the
346   * user name is actually split from the URI component.
347   *
348   * Access rights are checked against DokuWiki's ACL
349   * and applied accordingly.
350   *
351   * @param string $principalUri The principal URI to work on
352   *
353   * @return array An associative array of calendar IDs
354   */
355  public function getCalendarIdsForUser($principalUri)
356  {
357      $user = explode('/', $principalUri);
358      $user = end($user);
359      $mapping = $this->getCalendarIdToPageMapping();
360      $calids = array();
361      foreach($mapping as $row)
362      {
363          $id = $row['calid'];
364          $page = $row['page'];
365          $acl = auth_quickaclcheck($page);
366          if($acl >= AUTH_READ)
367          {
368              $write = $acl > AUTH_READ;
369              $calids[$id] = array('readonly' => !$write);
370          }
371      }
372      return $calids;
373  }
374
375  /**
376   * Create a new calendar for a given page ID and set name and description
377   * accordingly. Also update the pagetocalendarmapping table on success.
378   *
379   * @param string $name The calendar's name
380   * @param string $description The calendar's description
381   * @param string $id (optional) The page ID to work on
382   * @param string $userid (optional) The user ID that created the calendar
383   *
384   * @return boolean True on success, otherwise false
385   */
386  public function createCalendarForPage($name, $description, $id = null, $userid = null)
387  {
388      if(is_null($id))
389      {
390          global $ID;
391          $id = $ID;
392      }
393      if(is_null($userid))
394          $userid = $_SERVER['REMOTE_USER'];
395      $values = array('principals/'.$userid,
396                      $name,
397                      str_replace(array('/', ' ', ':'), '_', $id),
398                      $description,
399                      'VEVENT,VTODO',
400                      0,
401                      1);
402      $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) VALUES (".$this->sqlite->quote_and_join($values, ',').");";
403      $res = $this->sqlite->query($query);
404      if($res === false)
405        return false;
406
407      // Get the new calendar ID
408      $query = "SELECT id FROM calendars WHERE principaluri=".$this->sqlite->quote_string($values[0])." AND ".
409               "displayname=".$this->sqlite->quote_string($values[1])." AND ".
410               "uri=".$this->sqlite->quote_string($values[2])." AND ".
411               "description=".$this->sqlite->quote_string($values[3]);
412      $res = $this->sqlite->query($query);
413      $row = $this->sqlite->res2row($res);
414
415      // Update the pagetocalendarmapping table with the new calendar ID
416      if(isset($row['id']))
417      {
418          $values = array($id, $row['id']);
419          $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (".$this->sqlite->quote_and_join($values, ',').")";
420          $res = $this->sqlite->query($query);
421          return ($res !== false);
422      }
423
424      return false;
425  }
426
427  /**
428   * Add a new iCal entry for a given page, i.e. a given calendar.
429   *
430   * The parameter array needs to contain
431   *   timezone         => The timezone of the entries
432   *   detectedtz       => The timezone as detected by the browser
433   *   eventfrom        => The event's start date
434   *   eventfromtime    => The event's start time
435   *   eventto          => The event's end date
436   *   eventtotime      => The event's end time
437   *   eventname        => The event's name
438   *   eventdescription => The event's description
439   *
440   * @param string $id The page ID to work on
441   * @param string $user The user who created the calendar
442   * @param string $params A parameter array with values to create
443   *
444   * @return boolean True on success, otherwise false
445   */
446  public function addCalendarEntryToCalendarForPage($id, $user, $params)
447  {
448      $settings = $this->getPersonalSettings($user);
449      if($settings['timezone'] !== '' && $settings['timezone'] !== 'local')
450          $timezone = new \DateTimeZone($settings['timezone']);
451      elseif($settings['timezone'] === 'local')
452          $timezone = new \DateTimeZone($params['detectedtz']);
453      else
454          $timezone = new \DateTimeZone('UTC');
455
456      // Retrieve dates from settings
457      $startDate = explode('-', $params['eventfrom']);
458      $startTime = explode(':', $params['eventfromtime']);
459      $endDate = explode('-', $params['eventto']);
460      $endTime = explode(':', $params['eventtotime']);
461
462      // Load SabreDAV
463      require_once('vendor/autoload.php');
464      $vcalendar = new \Sabre\VObject\Component\VCalendar();
465
466      // Add VCalendar, UID and Event Name
467      $event = $vcalendar->add('VEVENT');
468      $uuid = \Sabre\VObject\UUIDUtil::getUUID();
469      $event->add('UID', $uuid);
470      $event->summary = $params['eventname'];
471
472      // Add a description if requested
473      $description = $params['eventdescription'];
474      if($description !== '')
475        $event->add('DESCRIPTION', $description);
476
477      // Create a timestamp for last modified, created and dtstamp values in UTC
478      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
479      $event->add('DTSTAMP', $dtStamp);
480      $event->add('CREATED', $dtStamp);
481      $event->add('LAST-MODIFIED', $dtStamp);
482
483      // Adjust the start date, based on the given timezone information
484      $dtStart = new \DateTime();
485      $dtStart->setTimezone($timezone);
486      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
487
488      // Only add the time values if it's not an allday event
489      if($params['allday'] != '1')
490        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
491
492      // Adjust the end date, based on the given timezone information
493      $dtEnd = new \DateTime();
494      $dtEnd->setTimezone($timezone);
495      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
496
497      // Only add the time values if it's not an allday event
498      if($params['allday'] != '1')
499        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
500
501      // According to the VCal spec, we need to add a whole day here
502      if($params['allday'] == '1')
503          $dtEnd->add(new \DateInterval('P1D'));
504
505      // Really add Start and End events
506      $dtStartEv = $event->add('DTSTART', $dtStart);
507      $dtEndEv = $event->add('DTEND', $dtEnd);
508
509      // Adjust the DATE format for allday events
510      if($params['allday'] == '1')
511      {
512          $dtStartEv['VALUE'] = 'DATE';
513          $dtEndEv['VALUE'] = 'DATE';
514      }
515
516      // Actually add the values to the database
517      $calid = $this->getCalendarIdForPage($id);
518      $uri = uniqid('dokuwiki-').'.ics';
519      $now = new DateTime();
520      $eventStr = $vcalendar->serialize();
521
522      $values = array($calid,
523                      $uri,
524                      $eventStr,
525                      $now->getTimestamp(),
526                      'VEVENT',
527                      $event->DTSTART->getDateTime()->getTimeStamp(),
528                      $event->DTEND->getDateTime()->getTimeStamp(),
529                      strlen($eventStr),
530                      md5($eventStr),
531                      $uuid
532      );
533
534      $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (".$this->sqlite->quote_and_join($values, ',').")";
535      $res = $this->sqlite->query($query);
536
537      // If successfully, update the sync token database
538      if($res !== false)
539      {
540          $this->updateSyncTokenLog($calid, $uri, 'added');
541          return true;
542      }
543      return false;
544  }
545
546  /**
547   * Retrieve the calendar settings of a given calendar id
548   *
549   * @param string $calid The calendar ID
550   *
551   * @return array The calendar settings array
552   */
553  public function getCalendarSettings($calid)
554  {
555      $query = "SELECT principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id=".$this->sqlite->quote_string($calid);
556      $res = $this->sqlite->query($query);
557      $row = $this->sqlite->res2row($res);
558      return $row;
559  }
560
561  /**
562   * Retrieve all events that are within a given date range,
563   * based on the timezone setting.
564   *
565   * There is also support for retrieving recurring events,
566   * using Sabre's VObject Iterator. Recurring events are represented
567   * as individual calendar entries with the same UID.
568   *
569   * @param string $id The page ID to work with
570   * @param string $user The user ID to work with
571   * @param string $startDate The start date as a string
572   * @param string $endDate The end date as a string
573   *
574   * @return array An array containing the calendar entries.
575   */
576  public function getEventsWithinDateRange($id, $user, $startDate, $endDate)
577  {
578      $settings = $this->getPersonalSettings($user);
579      if($settings['timezone'] !== '' && $settings['timezone'] !== 'local')
580          $timezone = new \DateTimeZone($settings['timezone']);
581      else
582          $timezone = new \DateTimeZone('UTC');
583      $data = array();
584
585      // Load SabreDAV
586      require_once('vendor/autoload.php');
587      $calid = $this->getCalendarIdForPage($id);
588      $color = $this->getCalendarColorForCalendar($calid);
589      $startTs = new \DateTime($startDate);
590      $endTs = new \DateTime($endDate);
591
592      // Retrieve matching calendar objects
593      $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid=".
594                $this->sqlite->quote_string($calid)." AND firstoccurence < ".
595                $this->sqlite->quote_string($endTs->getTimestamp())." AND lastoccurence > ".
596                $this->sqlite->quote_string($startTs->getTimestamp());
597      $res = $this->sqlite->query($query);
598      $arr = $this->sqlite->res2arr($res);
599
600      // Parse individual calendar entries
601      foreach($arr as $row)
602      {
603          if(isset($row['calendardata']))
604          {
605              $entry = array();
606              $vcal = \Sabre\VObject\Reader::read($row['calendardata']);
607              $recurrence = $vcal->VEVENT->RRULE;
608              // If it is a recurring event, pass it through Sabre's EventIterator
609              if($recurrence != null)
610              {
611                  $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT));
612                  $rEvents->rewind();
613                  $done = false;
614                  while($rEvents->valid() && !$done)
615                  {
616                      $event = $rEvents->getEventObject();
617                      // If we are after the given time range, exit
618                      if(($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp()) &&
619                         ($rEvents->getDtEnd()->getTimestamp() > $endTs->getTimestamp()))
620                        $done = true;
621
622                      // If we are before the given time range, continue
623                      if($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp())
624                      {
625                          $rEvents->next();
626                          continue;
627                      }
628
629                      // If we are within the given time range, parse the event
630                      $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true);
631                      $rEvents->next();
632                  }
633              }
634              else
635                $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color);
636          }
637      }
638      return $data;
639  }
640
641  /**
642   * Helper function that parses the iCal data of a VEVENT to a calendar entry.
643   *
644   * @param \Sabre\VObject\VEvent $event The event to parse
645   * @param \DateTimeZone $timezone The timezone object
646   * @param string $uid The entry's UID
647   * @param boolean $recurring (optional) Set to true to define a recurring event
648   *
649   * @return array The parse calendar entry
650   */
651  private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false)
652  {
653      $entry = array();
654      $start = $event->DTSTART;
655      // Parse only if the start date/time is present
656      if($start !== null)
657      {
658        $dtStart = $start->getDateTime();
659        $dtStart->setTimezone($timezone);
660        $entry['start'] = $dtStart->format(\DateTime::ATOM);
661        if($start['VALUE'] == 'DATE')
662          $entry['allDay'] = true;
663        else
664          $entry['allDay'] = false;
665      }
666      $end = $event->DTEND;
667      // Parse onlyl if the end date/time is present
668      if($end !== null)
669      {
670        $dtEnd = $end->getDateTime();
671        $dtEnd->setTimezone($timezone);
672        $entry['end'] = $dtEnd->format(\DateTime::ATOM);
673      }
674      $description = $event->DESCRIPTION;
675      if($description !== null)
676        $entry['description'] = (string)$description;
677      else
678        $entry['description'] = '';
679      $entry['title'] = (string)$event->summary;
680      $entry['id'] = $uid;
681      $entry['page'] = $page;
682      $entry['color'] = $color;
683      $entry['recurring'] = $recurring;
684
685      return $entry;
686  }
687
688  /**
689   * Retrieve an event by its UID
690   *
691   * @param string $uid The event's UID
692   *
693   * @return mixed The table row with the given event
694   */
695  public function getEventWithUid($uid)
696  {
697      $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid=".
698                $this->sqlite->quote_string($uid);
699      $res = $this->sqlite->query($query);
700      $row = $this->sqlite->res2row($res);
701      return $row;
702  }
703
704  /**
705   * Retrieve all calendar events for a given calendar ID
706   *
707   * @param string $calid The calendar's ID
708   *
709   * @return array An array containing all calendar data
710   */
711  public function getAllCalendarEvents($calid)
712  {
713      $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid=".
714               $this->sqlite->quote_string($calid);
715      $res = $this->sqlite->query($query);
716      $arr = $this->sqlite->res2arr($res);
717      return $arr;
718  }
719
720  /**
721   * Edit a calendar entry for a page, given by its parameters.
722   * The params array has the same format as @see addCalendarEntryForPage
723   *
724   * @param string $id The page's ID to work on
725   * @param string $user The user's ID to work on
726   * @param array $params The parameter array for the edited calendar event
727   *
728   * @return boolean True on success, otherwise false
729   */
730  public function editCalendarEntryForPage($id, $user, $params)
731  {
732      $settings = $this->getPersonalSettings($user);
733      if($settings['timezone'] !== '' && $settings['timezone'] !== 'local')
734          $timezone = new \DateTimeZone($settings['timezone']);
735      elseif($settings['timezone'] === 'local')
736          $timezone = new \DateTimeZone($params['detectedtz']);
737      else
738          $timezone = new \DateTimeZone('UTC');
739
740      // Parse dates
741      $startDate = explode('-', $params['eventfrom']);
742      $startTime = explode(':', $params['eventfromtime']);
743      $endDate = explode('-', $params['eventto']);
744      $endTime = explode(':', $params['eventtotime']);
745
746      // Retrieve the existing event based on the UID
747      $uid = $params['uid'];
748      $event = $this->getEventWithUid($uid);
749
750      // Load SabreDAV
751      require_once('vendor/autoload.php');
752      if(!isset($event['calendardata']))
753        return false;
754      $uri = $event['uri'];
755      $calid = $event['calendarid'];
756
757      // Parse the existing event
758      $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
759      $vevent = $vcal->VEVENT;
760
761      // Set the new event values
762      $vevent->summary = $params['eventname'];
763      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
764      $description = $params['eventdescription'];
765
766      // Remove existing timestamps to overwrite them
767      $vevent->remove('DESCRIPTION');
768      $vevent->remove('DTSTAMP');
769      $vevent->remove('LAST-MODIFIED');
770
771      // Add new time stamps and description
772      $vevent->add('DTSTAMP', $dtStamp);
773      $vevent->add('LAST-MODIFIED', $dtStamp);
774      if($description !== '')
775        $vevent->add('DESCRIPTION', $description);
776
777      // Setup DTSTART
778      $dtStart = new \DateTime();
779      $dtStart->setTimezone($timezone);
780      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
781      if($params['allday'] != '1')
782        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
783
784      // Setup DETEND
785      $dtEnd = new \DateTime();
786      $dtEnd->setTimezone($timezone);
787      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
788      if($params['allday'] != '1')
789        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
790
791      // According to the VCal spec, we need to add a whole day here
792      if($params['allday'] == '1')
793          $dtEnd->add(new \DateInterval('P1D'));
794      $vevent->remove('DTSTART');
795      $vevent->remove('DTEND');
796      $dtStartEv = $vevent->add('DTSTART', $dtStart);
797      $dtEndEv = $vevent->add('DTEND', $dtEnd);
798
799      // Remove the time for allday events
800      if($params['allday'] == '1')
801      {
802          $dtStartEv['VALUE'] = 'DATE';
803          $dtEndEv['VALUE'] = 'DATE';
804      }
805      $now = new DateTime();
806      $eventStr = $vcal->serialize();
807
808      // Actually write to the database
809      $query = "UPDATE calendarobjects SET calendardata=".$this->sqlite->quote_string($eventStr).
810               ", lastmodified=".$this->sqlite->quote_string($now->getTimestamp()).
811               ", firstoccurence=".$this->sqlite->quote_string($dtStart->getTimestamp()).
812               ", lastoccurence=".$this->sqlite->quote_string($dtEnd->getTimestamp()).
813               ", size=".strlen($eventStr).
814               ", etag=".$this->sqlite->quote_string(md5($eventStr)).
815               " WHERE uid=".$this->sqlite->quote_string($uid);
816      $res = $this->sqlite->query($query);
817      if($res !== false)
818      {
819          $this->updateSyncTokenLog($calid, $uri, 'modified');
820          return true;
821      }
822      return false;
823  }
824
825  /**
826   * Delete a calendar entry for a given page. Actually, the event is removed
827   * based on the entry's UID, so that page ID is no used.
828   *
829   * @param string $id The page's ID (unused)
830   * @param array $params The parameter array to work with
831   *
832   * @return boolean True
833   */
834  public function deleteCalendarEntryForPage($id, $params)
835  {
836      $uid = $params['uid'];
837      $event = $this->getEventWithUid($uid);
838      $calid = $event['calendarid'];
839      $uri = $event['uri'];
840      $query = "DELETE FROM calendarobjects WHERE uid=".$this->sqlite->quote_string($uid);
841      $res = $this->sqlite->query($query);
842      if($res !== false)
843      {
844          $this->updateSyncTokenLog($calid, $uri, 'deleted');
845      }
846      return true;
847  }
848
849  /**
850   * Retrieve the current sync token for a calendar
851   *
852   * @param string $calid The calendar id
853   *
854   * @return mixed The synctoken or false
855   */
856  public function getSyncTokenForCalendar($calid)
857  {
858      $row = $this->getCalendarSettings($calid);
859      if(isset($row['synctoken']))
860          return $row['synctoken'];
861      return false;
862  }
863
864  /**
865   * Helper function to convert the operation name to
866   * an operation code as stored in the database
867   *
868   * @param string $operationName The operation name
869   *
870   * @return mixed The operation code or false
871   */
872  public function operationNameToOperation($operationName)
873  {
874      switch($operationName)
875      {
876          case 'added':
877              return 1;
878          break;
879          case 'modified':
880              return 2;
881          break;
882          case 'deleted':
883              return 3;
884          break;
885      }
886      return false;
887  }
888
889  /**
890   * Update the sync token log based on the calendar id and the
891   * operation that was performed.
892   *
893   * @param string $calid The calendar ID that was modified
894   * @param string $uri The calendar URI that was modified
895   * @param string $operation The operation that was performed
896   *
897   * @return boolean True on success, otherwise false
898   */
899  private function updateSyncTokenLog($calid, $uri, $operation)
900  {
901      $currentToken = $this->getSyncTokenForCalendar($calid);
902      $operationCode = $this->operationNameToOperation($operation);
903      if(($operationCode === false) || ($currentToken === false))
904          return false;
905      $values = array($uri,
906                      $currentToken,
907                      $calid,
908                      $operationCode
909      );
910      $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(".
911               $this->sqlite->quote_and_join($values, ',').")";
912      $res = $this->sqlite->query($query);
913      if($res === false)
914        return false;
915      $currentToken++;
916      $query = "UPDATE calendars SET synctoken=".$this->sqlite->quote_string($currentToken)." WHERE id=".
917               $this->sqlite->quote_string($calid);
918      $res = $this->sqlite->query($query);
919      return ($res !== false);
920  }
921
922  /**
923   * Return the sync URL for a given Page, i.e. a calendar
924   *
925   * @param string $id The page's ID
926   * @param string $user (optional) The user's ID
927   *
928   * @return mixed The sync url or false
929   */
930  public function getSyncUrlForPage($id, $user = null)
931  {
932      if(is_null($user))
933        $user = $_SERVER['REMOTE_USER'];
934
935      $calid = $this->getCalendarIdForPage($id);
936      if($calid === false)
937        return false;
938
939      $calsettings = $this->getCalendarSettings($calid);
940      if(!isset($calsettings['uri']))
941        return false;
942
943      $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri'];
944      return $syncurl;
945  }
946
947  /**
948   * Return the private calendar's URL for a given page
949   *
950   * @param string $id the page ID
951   *
952   * @return mixed The private URL or false
953   */
954  public function getPrivateURLForPage($id)
955  {
956      $calid = $this->getCalendarIdForPage($id);
957      if($calid === false)
958        return false;
959
960      return $this->getPrivateURLForCalendar($calid);
961  }
962
963  /**
964   * Return the private calendar's URL for a given calendar ID
965   *
966   * @param string $calid The calendar's ID
967   *
968   * @return mixed The private URL or false
969   */
970  public function getPrivateURLForCalendar($calid)
971  {
972      if(isset($this->cachedValues['privateurl'][$calid]))
973        return $this->cachedValues['privateurl'][$calid];
974      $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid=".$this->sqlite->quote_string($calid);
975      $res = $this->sqlite->query($query);
976      $row = $this->sqlite->res2row($res);
977      if(!isset($row['url']))
978      {
979          $url = uniqid("dokuwiki-").".ics";
980          $values = array(
981                $url,
982                $calid
983          );
984          $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(".
985                $this->sqlite->quote_and_join($values, ", ").")";
986          $res = $this->sqlite->query($query);
987          if($res === false)
988            return false;
989      }
990      else
991      {
992          $url = $row['url'];
993      }
994
995      $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url;
996      $this->cachedValues['privateurl'][$calid] = $url;
997      return $url;
998  }
999
1000  /**
1001   * Retrieve the calendar ID for a given private calendar URL
1002   *
1003   * @param string $url The private URL
1004   *
1005   * @return mixed The calendar ID or false
1006   */
1007  public function getCalendarForPrivateURL($url)
1008  {
1009      $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url=".$this->sqlite->quote_string($url);
1010      $res = $this->sqlite->query($query);
1011      $row = $this->sqlite->res2row($res);
1012      if(!isset($row['calid']))
1013        return false;
1014      return $row['calid'];
1015  }
1016
1017  /**
1018   * Return a given calendar as ICS feed, i.e. all events in one ICS file.
1019   *
1020   * @param string $caldi The calendar ID to retrieve
1021   *
1022   * @return mixed The calendar events as string or false
1023   */
1024  public function getCalendarAsICSFeed($calid)
1025  {
1026      $calSettings = $this->getCalendarSettings($calid);
1027      if($calSettings === false)
1028        return false;
1029      $events = $this->getAllCalendarEvents($calid);
1030      if($events === false)
1031        return false;
1032
1033      $out = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\nCALSCALE:GREGORIAN\nX-WR-CALNAME:";
1034      $out .= $calSettings['displayname']."\n";
1035      foreach($events as $event)
1036      {
1037          $out .= rtrim($event['calendardata']);
1038          $out .= "\n";
1039      }
1040      $out .= "END:VCALENDAR\n";
1041      return $out;
1042  }
1043
1044}
1045