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