xref: /plugin/davcal/helper.php (revision 9f1dab8c22c579ae1903b00ba4d7de46f7cbdea4)
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 = $settings['write'];
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   * Update a calendar's displayname
306   *
307   * @param int $calid The calendar's ID
308   * @param string $name The new calendar name
309   *
310   * @return boolean True on success, otherwise false
311   */
312  public function updateCalendarName($calid, $name)
313  {
314      $query = "UPDATE calendars SET displayname = ? WHERE id = ?";
315      $res = $this->sqlite->query($query, $calid, $name);
316      if($res !== false)
317      {
318        $this->updateSyncTokenLog($calid, '', 'modified');
319        return true;
320      }
321      return false;
322  }
323
324  /**
325   * Update the calendar description
326   *
327   * @param int $calid The calendar's ID
328   * @param string $description The new calendar's description
329   *
330   * @return boolean True on success, otherwise false
331   */
332  public function updateCalendarDescription($calid, $description)
333  {
334      $query = "UPDATE calendars SET description = ? WHERE id = ?";
335      $res = $this->sqlite->query($query, $calid, $description);
336      if($res !== false)
337      {
338        $this->updateSyncTokenLog($calid, '', 'modified');
339        return true;
340      }
341      return false;
342  }
343
344  /**
345   * Update a calendar's timezone information
346   *
347   * @param int $calid The calendar's ID
348   * @param string $timezone The new timezone to set
349   *
350   * @return boolean True on success, otherwise false
351   */
352  public function updateCalendarTimezone($calid, $timezone)
353  {
354      $query = "UPDATE calendars SET timezone = ? WHERE id = ?";
355      $res = $this->sqlite->query($query, $calid, $timezone);
356      if($res !== false)
357      {
358        $this->updateSyncTokenLog($calid, '', 'modified');
359        return true;
360      }
361      return false;
362  }
363
364  /**
365   * Save the personal settings to the SQLite database 'calendarsettings'.
366   *
367   * @param array  $settings The settings array to store
368   * @param string $userid (optional) The userid to store
369   *
370   * @param boolean True on success, otherwise false
371   */
372  public function savePersonalSettings($settings, $userid = null)
373  {
374      if(is_null($userid))
375      {
376          if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
377          {
378            $userid = $_SERVER['REMOTE_USER'];
379          }
380          else
381          {
382              return false;
383          }
384      }
385      $this->sqlite->query("BEGIN TRANSACTION");
386
387      $query = "DELETE FROM calendarsettings WHERE userid = ?";
388      $this->sqlite->query($query, $userid);
389
390      foreach($settings as $key => $value)
391      {
392          $query = "INSERT INTO calendarsettings (userid, key, value) VALUES (?, ?, ?)";
393          $res = $this->sqlite->query($query, $userid, $key, $value);
394          if($res === false)
395              return false;
396      }
397      $this->sqlite->query("COMMIT TRANSACTION");
398      $this->cachedValues['settings'][$userid] = $settings;
399      return true;
400  }
401
402  /**
403   * Retrieve the settings array for a given user id.
404   * Some sane defaults are returned, currently:
405   *
406   *    timezone    => local
407   *    weeknumbers => 0
408   *    workweek    => 0
409   *
410   * @param string $userid (optional) The user id to retrieve
411   *
412   * @return array The settings array
413   */
414  public function getPersonalSettings($userid = null)
415  {
416      // Some sane default settings
417      $settings = array(
418        'timezone' => $this->getConf('timezone'),
419        'weeknumbers' => $this->getConf('weeknumbers'),
420        'workweek' => $this->getConf('workweek'),
421        'monday' => $this->getConf('monday'),
422        'timeformat' => $this->getConf('timeformat')
423      );
424      if(is_null($userid))
425      {
426          if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
427          {
428            $userid = $_SERVER['REMOTE_USER'];
429          }
430          else
431          {
432            return $settings;
433          }
434      }
435
436      if(isset($this->cachedValues['settings'][$userid]))
437        return $this->cachedValues['settings'][$userid];
438      $query = "SELECT key, value FROM calendarsettings WHERE userid = ?";
439      $res = $this->sqlite->query($query, $userid);
440      $arr = $this->sqlite->res2arr($res);
441      foreach($arr as $row)
442      {
443          $settings[$row['key']] = $row['value'];
444      }
445      $this->cachedValues['settings'][$userid] = $settings;
446      return $settings;
447  }
448
449  /**
450   * Retrieve the calendar ID based on a page ID from the SQLite table
451   * 'pagetocalendarmapping'.
452   *
453   * @param string $id (optional) The page ID to retrieve the corresponding calendar
454   *
455   * @return mixed the ID on success, otherwise false
456   */
457  public function getCalendarIdForPage($id = null)
458  {
459      if(is_null($id))
460      {
461          global $ID;
462          $id = $ID;
463      }
464
465      if(isset($this->cachedValues['calid'][$id]))
466        return $this->cachedValues['calid'][$id];
467
468      $query = "SELECT calid FROM pagetocalendarmapping WHERE page = ?";
469      $res = $this->sqlite->query($query, $id);
470      $row = $this->sqlite->res2row($res);
471      if(isset($row['calid']))
472      {
473        $calid = $row['calid'];
474        $this->cachedValues['calid'] = $calid;
475        return $calid;
476      }
477      return false;
478  }
479
480  /**
481   * Retrieve the complete calendar id to page mapping.
482   * This is necessary to be able to retrieve a list of
483   * calendars for a given user and check the access rights.
484   *
485   * @return array The mapping array
486   */
487  public function getCalendarIdToPageMapping()
488  {
489      $query = "SELECT calid, page FROM pagetocalendarmapping";
490      $res = $this->sqlite->query($query);
491      $arr = $this->sqlite->res2arr($res);
492      return $arr;
493  }
494
495  /**
496   * Retrieve all calendar IDs a given user has access to.
497   * The user is specified by the principalUri, so the
498   * user name is actually split from the URI component.
499   *
500   * Access rights are checked against DokuWiki's ACL
501   * and applied accordingly.
502   *
503   * @param string $principalUri The principal URI to work on
504   *
505   * @return array An associative array of calendar IDs
506   */
507  public function getCalendarIdsForUser($principalUri)
508  {
509      global $auth;
510      $user = explode('/', $principalUri);
511      $user = end($user);
512      $mapping = $this->getCalendarIdToPageMapping();
513      $calids = array();
514      $ud = $auth->getUserData($user);
515      $groups = $ud['grps'];
516      foreach($mapping as $row)
517      {
518          $id = $row['calid'];
519          $page = $row['page'];
520          $acl = auth_aclcheck($page, $user, $groups);
521          if($acl >= AUTH_READ)
522          {
523              $write = $acl > AUTH_READ;
524              $calids[$id] = array('readonly' => !$write);
525          }
526      }
527      return $calids;
528  }
529
530  /**
531   * Create a new calendar for a given page ID and set name and description
532   * accordingly. Also update the pagetocalendarmapping table on success.
533   *
534   * @param string $name The calendar's name
535   * @param string $description The calendar's description
536   * @param string $id (optional) The page ID to work on
537   * @param string $userid (optional) The user ID that created the calendar
538   *
539   * @return boolean True on success, otherwise false
540   */
541  public function createCalendarForPage($name, $description, $id = null, $userid = null)
542  {
543      if(is_null($id))
544      {
545          global $ID;
546          $id = $ID;
547      }
548      if(is_null($userid))
549      {
550        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
551        {
552          $userid = $_SERVER['REMOTE_USER'];
553        }
554        else
555        {
556          $userid = uniqid('davcal-');
557        }
558      }
559      $values = array('principals/'.$userid,
560                      $name,
561                      str_replace(array('/', ' ', ':'), '_', $id),
562                      $description,
563                      'VEVENT,VTODO',
564                      0,
565                      1);
566      $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) ".
567               "VALUES (?, ?, ?, ?, ?, ?, ?)";
568      $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6]);
569      if($res === false)
570        return false;
571
572      // Get the new calendar ID
573      $query = "SELECT id FROM calendars WHERE principaluri = ? AND displayname = ? AND ".
574               "uri = ? AND description = ?";
575      $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3]);
576      $row = $this->sqlite->res2row($res);
577
578      // Update the pagetocalendarmapping table with the new calendar ID
579      if(isset($row['id']))
580      {
581          $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (?, ?)";
582          $res = $this->sqlite->query($query, $id, $row['id']);
583          return ($res !== false);
584      }
585
586      return false;
587  }
588
589  /**
590   * Add a new calendar entry to the given calendar. Calendar data is
591   * specified as ICS file, thus it needs to be parsed first.
592   *
593   * This is mainly needed for the sync support.
594   *
595   * @param int $calid The calendar's ID
596   * @param string $uri The new object URI
597   * @param string $ics The ICS file
598   *
599   * @return mixed The etag.
600   */
601  public function addCalendarEntryToCalendarByICS($calid, $uri, $ics)
602  {
603    $extraData = $this->getDenormalizedData($ics);
604
605    $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)";
606    $res = $this->sqlite->query($query,
607            $calid,
608            $uri,
609            $ics,
610            time(),
611            $extraData['etag'],
612            $extraData['size'],
613            $extraData['componentType'],
614            $extraData['firstOccurence'],
615            $extraData['lastOccurence'],
616            $extraData['uid']);
617            // If successfully, update the sync token database
618    if($res !== false)
619    {
620        $this->updateSyncTokenLog($calid, $uri, 'added');
621    }
622    return $extraData['etag'];
623  }
624
625  /**
626   * Edit a calendar entry by providing a new ICS file. This is mainly
627   * needed for the sync support.
628   *
629   * @param int $calid The calendar's IS
630   * @param string $uri The object's URI to modify
631   * @param string $ics The new object's ICS file
632   */
633  public function editCalendarEntryToCalendarByICS($calid, $uri, $ics)
634  {
635      $extraData = $this->getDenormalizedData($ics);
636
637      $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?";
638      $res = $this->sqlite->query($query,
639        $ics,
640        time(),
641        $extraData['etag'],
642        $extraData['size'],
643        $extraData['componentType'],
644        $extraData['firstOccurence'],
645        $extraData['lastOccurence'],
646        $extraData['uid'],
647        $calid,
648        $uri
649      );
650      if($res !== false)
651      {
652          $this->updateSyncTokenLog($calid, $uri, 'modified');
653      }
654      return $extraData['etag'];
655  }
656
657  /**
658   * Add a new iCal entry for a given page, i.e. a given calendar.
659   *
660   * The parameter array needs to contain
661   *   detectedtz       => The timezone as detected by the browser
662   *   currenttz        => The timezone in use by the calendar
663   *   eventfrom        => The event's start date
664   *   eventfromtime    => The event's start time
665   *   eventto          => The event's end date
666   *   eventtotime      => The event's end time
667   *   eventname        => The event's name
668   *   eventdescription => The event's description
669   *
670   * @param string $id The page ID to work on
671   * @param string $user The user who created the calendar
672   * @param string $params A parameter array with values to create
673   *
674   * @return boolean True on success, otherwise false
675   */
676  public function addCalendarEntryToCalendarForPage($id, $user, $params)
677  {
678      if($params['currenttz'] !== '' && $params['currenttz'] !== 'local')
679          $timezone = new \DateTimeZone($params['currenttz']);
680      elseif($params['currenttz'] === 'local')
681          $timezone = new \DateTimeZone($params['detectedtz']);
682      else
683          $timezone = new \DateTimeZone('UTC');
684
685      // Retrieve dates from settings
686      $startDate = explode('-', $params['eventfrom']);
687      $startTime = explode(':', $params['eventfromtime']);
688      $endDate = explode('-', $params['eventto']);
689      $endTime = explode(':', $params['eventtotime']);
690
691      // Load SabreDAV
692      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
693      $vcalendar = new \Sabre\VObject\Component\VCalendar();
694
695      // Add VCalendar, UID and Event Name
696      $event = $vcalendar->add('VEVENT');
697      $uuid = \Sabre\VObject\UUIDUtil::getUUID();
698      $event->add('UID', $uuid);
699      $event->summary = $params['eventname'];
700
701      // Add a description if requested
702      $description = $params['eventdescription'];
703      if($description !== '')
704        $event->add('DESCRIPTION', $description);
705
706      // Add attachments
707      $attachments = $params['attachments'];
708      if(!is_null($attachments))
709        foreach($attachments as $attachment)
710          $event->add('ATTACH', $attachment);
711
712      // Create a timestamp for last modified, created and dtstamp values in UTC
713      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
714      $event->add('DTSTAMP', $dtStamp);
715      $event->add('CREATED', $dtStamp);
716      $event->add('LAST-MODIFIED', $dtStamp);
717
718      // Adjust the start date, based on the given timezone information
719      $dtStart = new \DateTime();
720      $dtStart->setTimezone($timezone);
721      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
722
723      // Only add the time values if it's not an allday event
724      if($params['allday'] != '1')
725        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
726
727      // Adjust the end date, based on the given timezone information
728      $dtEnd = new \DateTime();
729      $dtEnd->setTimezone($timezone);
730      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
731
732      // Only add the time values if it's not an allday event
733      if($params['allday'] != '1')
734        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
735
736      // According to the VCal spec, we need to add a whole day here
737      if($params['allday'] == '1')
738          $dtEnd->add(new \DateInterval('P1D'));
739
740      // Really add Start and End events
741      $dtStartEv = $event->add('DTSTART', $dtStart);
742      $dtEndEv = $event->add('DTEND', $dtEnd);
743
744      // Adjust the DATE format for allday events
745      if($params['allday'] == '1')
746      {
747          $dtStartEv['VALUE'] = 'DATE';
748          $dtEndEv['VALUE'] = 'DATE';
749      }
750
751      $eventStr = $vcalendar->serialize();
752
753      if(strpos($id, 'webdav://') === 0)
754      {
755          $wdc =& plugin_load('helper', 'webdavclient');
756          if(is_null($wdc))
757            return false;
758          $connectionId = str_replace('webdav://', '', $id);
759          return $wdc->addCalendarEntry($connectionId, $eventStr);
760      }
761      else
762      {
763          // Actually add the values to the database
764          $calid = $this->getCalendarIdForPage($id);
765          $uri = uniqid('dokuwiki-').'.ics';
766          $now = new DateTime();
767
768          $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
769          $res = $this->sqlite->query($query, $calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT',
770                                      $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(),
771                                      strlen($eventStr), md5($eventStr), $uuid);
772
773          // If successfully, update the sync token database
774          if($res !== false)
775          {
776              $this->updateSyncTokenLog($calid, $uri, 'added');
777              return true;
778          }
779      }
780      return false;
781  }
782
783  /**
784   * Retrieve the calendar settings of a given calendar id
785   *
786   * @param string $calid The calendar ID
787   *
788   * @return array The calendar settings array
789   */
790  public function getCalendarSettings($calid)
791  {
792      $query = "SELECT id, principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id= ? ";
793      $res = $this->sqlite->query($query, $calid);
794      $row = $this->sqlite->res2row($res);
795      return $row;
796  }
797
798  /**
799   * Retrieve all events that are within a given date range,
800   * based on the timezone setting.
801   *
802   * There is also support for retrieving recurring events,
803   * using Sabre's VObject Iterator. Recurring events are represented
804   * as individual calendar entries with the same UID.
805   *
806   * @param string $id The page ID to work with
807   * @param string $user The user ID to work with
808   * @param string $startDate The start date as a string
809   * @param string $endDate The end date as a string
810   * @param string $color (optional) The calendar's color
811   *
812   * @return array An array containing the calendar entries.
813   */
814  public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone, $color = null)
815  {
816      if($timezone !== '' && $timezone !== 'local')
817          $timezone = new \DateTimeZone($timezone);
818      else
819          $timezone = new \DateTimeZone('UTC');
820      $data = array();
821
822      $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid = ?";
823      $startTs = null;
824      $endTs = null;
825      if($startDate !== null)
826      {
827        $startTs = new \DateTime($startDate);
828        $query .= " AND lastoccurence > ".$this->sqlite->quote_string($startTs->getTimestamp());
829      }
830      if($endDate !== null)
831      {
832        $endTs = new \DateTime($endDate);
833        $query .= " AND firstoccurence < ".$this->sqlite->quote_string($endTs->getTimestamp());
834      }
835
836      // Load SabreDAV
837      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
838
839      if(strpos($id, 'webdav://') === 0)
840      {
841          $wdc =& plugin_load('helper', 'webdavclient');
842          if(is_null($wdc))
843            return $data;
844          $connectionId = str_replace('webdav://', '', $id);
845          $arr = $wdc->getCalendarEntries($connectionId, $startDate, $endDate);
846      }
847      else
848      {
849          $calid = $this->getCalendarIdForPage($id);
850          if(is_null($color))
851            $color = $this->getCalendarColorForCalendar($calid);
852
853          // Retrieve matching calendar objects
854          $res = $this->sqlite->query($query, $calid);
855          $arr = $this->sqlite->res2arr($res);
856      }
857
858      // Parse individual calendar entries
859      foreach($arr as $row)
860      {
861          if(isset($row['calendardata']))
862          {
863              $entry = array();
864              $vcal = \Sabre\VObject\Reader::read($row['calendardata']);
865              $recurrence = $vcal->VEVENT->RRULE;
866              // If it is a recurring event, pass it through Sabre's EventIterator
867              if($recurrence != null)
868              {
869                  $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT));
870                  $rEvents->rewind();
871                  while($rEvents->valid())
872                  {
873                      $event = $rEvents->getEventObject();
874                      // If we are after the given time range, exit
875                      if(($endTs !== null) && ($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp()))
876                          break;
877
878                      // If we are before the given time range, continue
879                      if(($startTs != null) && ($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp()))
880                      {
881                          $rEvents->next();
882                          continue;
883                      }
884
885                      // If we are within the given time range, parse the event
886                      $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true);
887                      $rEvents->next();
888                  }
889              }
890              else
891                $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color);
892          }
893      }
894      return $data;
895  }
896
897  /**
898   * Helper function that parses the iCal data of a VEVENT to a calendar entry.
899   *
900   * @param \Sabre\VObject\VEvent $event The event to parse
901   * @param \DateTimeZone $timezone The timezone object
902   * @param string $uid The entry's UID
903   * @param boolean $recurring (optional) Set to true to define a recurring event
904   *
905   * @return array The parse calendar entry
906   */
907  private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false)
908  {
909      $entry = array();
910      $start = $event->DTSTART;
911      // Parse only if the start date/time is present
912      if($start !== null)
913      {
914        $dtStart = $start->getDateTime();
915        $dtStart->setTimezone($timezone);
916
917        // moment.js doesn't like times be given even if
918        // allDay is set to true
919        // This should fix T23
920        if($start['VALUE'] == 'DATE')
921        {
922          $entry['allDay'] = true;
923          $entry['start'] = $dtStart->format("Y-m-d");
924        }
925        else
926        {
927          $entry['allDay'] = false;
928          $entry['start'] = $dtStart->format(\DateTime::ATOM);
929        }
930      }
931      $end = $event->DTEND;
932      // Parse only if the end date/time is present
933      if($end !== null)
934      {
935        $dtEnd = $end->getDateTime();
936        $dtEnd->setTimezone($timezone);
937        if($end['VALUE'] == 'DATE')
938          $entry['end'] = $dtEnd->format("Y-m-d");
939        else
940          $entry['end'] = $dtEnd->format(\DateTime::ATOM);
941      }
942      $description = $event->DESCRIPTION;
943      if($description !== null)
944        $entry['description'] = (string)$description;
945      else
946        $entry['description'] = '';
947      $attachments = $event->ATTACH;
948      if($attachments !== null)
949      {
950        $entry['attachments'] = array();
951        foreach($attachments as $attachment)
952          $entry['attachments'][] = (string)$attachment;
953      }
954      $entry['title'] = (string)$event->summary;
955      $entry['id'] = $uid;
956      $entry['page'] = $page;
957      $entry['color'] = $color;
958      $entry['recurring'] = $recurring;
959
960      return $entry;
961  }
962
963  /**
964   * Retrieve an event by its UID
965   *
966   * @param string $uid The event's UID
967   *
968   * @return mixed The table row with the given event
969   */
970  public function getEventWithUid($uid)
971  {
972      $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?";
973      $res = $this->sqlite->query($query, $uid);
974      $row = $this->sqlite->res2row($res);
975      return $row;
976  }
977
978  /**
979   * Retrieve information of a calendar's object, not including the actual
980   * calendar data! This is mainly neede for the sync support.
981   *
982   * @param int $calid The calendar ID
983   *
984   * @return mixed The result
985   */
986  public function getCalendarObjects($calid)
987  {
988      $query = "SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM calendarobjects WHERE calendarid = ?";
989      $res = $this->sqlite->query($query, $calid);
990      $arr = $this->sqlite->res2arr($res);
991      return $arr;
992  }
993
994  /**
995   * Retrieve a single calendar object by calendar ID and URI
996   *
997   * @param int $calid The calendar's ID
998   * @param string $uri The object's URI
999   *
1000   * @return mixed The result
1001   */
1002  public function getCalendarObjectByUri($calid, $uri)
1003  {
1004      $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri = ?";
1005      $res = $this->sqlite->query($query, $calid, $uri);
1006      $row = $this->sqlite->res2row($res);
1007      return $row;
1008  }
1009
1010  /**
1011   * Retrieve several calendar objects by specifying an array of URIs.
1012   * This is mainly neede for sync.
1013   *
1014   * @param int $calid The calendar's ID
1015   * @param array $uris An array of URIs
1016   *
1017   * @return mixed The result
1018   */
1019  public function getMultipleCalendarObjectsByUri($calid, $uris)
1020  {
1021        $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri IN (";
1022        // Inserting a whole bunch of question marks
1023        $query .= implode(',', array_fill(0, count($uris), '?'));
1024        $query .= ')';
1025        $vals = array_merge(array($calid), $uris);
1026
1027        $res = $this->sqlite->query($query, $vals);
1028        $arr = $this->sqlite->res2arr($res);
1029        return $arr;
1030  }
1031
1032  /**
1033   * Retrieve all calendar events for a given calendar ID
1034   *
1035   * @param string $calid The calendar's ID
1036   *
1037   * @return array An array containing all calendar data
1038   */
1039  public function getAllCalendarEvents($calid)
1040  {
1041      $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?";
1042      $res = $this->sqlite->query($query, $calid);
1043      $arr = $this->sqlite->res2arr($res);
1044      return $arr;
1045  }
1046
1047  /**
1048   * Edit a calendar entry for a page, given by its parameters.
1049   * The params array has the same format as @see addCalendarEntryForPage
1050   *
1051   * @param string $id The page's ID to work on
1052   * @param string $user The user's ID to work on
1053   * @param array $params The parameter array for the edited calendar event
1054   *
1055   * @return boolean True on success, otherwise false
1056   */
1057  public function editCalendarEntryForPage($id, $user, $params)
1058  {
1059      if($params['currenttz'] !== '' && $params['currenttz'] !== 'local')
1060          $timezone = new \DateTimeZone($params['currenttz']);
1061      elseif($params['currenttz'] === 'local')
1062          $timezone = new \DateTimeZone($params['detectedtz']);
1063      else
1064          $timezone = new \DateTimeZone('UTC');
1065
1066      // Parse dates
1067      $startDate = explode('-', $params['eventfrom']);
1068      $startTime = explode(':', $params['eventfromtime']);
1069      $endDate = explode('-', $params['eventto']);
1070      $endTime = explode(':', $params['eventtotime']);
1071
1072      // Retrieve the existing event based on the UID
1073      $uid = $params['uid'];
1074
1075      if(strpos($id, 'webdav://') === 0)
1076      {
1077        $wdc =& plugin_load('helper', 'webdavclient');
1078        if(is_null($wdc))
1079          return false;
1080        $event = $wdc->getCalendarEntryByUid($uid);
1081      }
1082      else
1083      {
1084        $event = $this->getEventWithUid($uid);
1085      }
1086
1087      // Load SabreDAV
1088      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1089      if(!isset($event['calendardata']))
1090        return false;
1091      $uri = $event['uri'];
1092      $calid = $event['calendarid'];
1093
1094      // Parse the existing event
1095      $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
1096      $vevent = $vcal->VEVENT;
1097
1098      // Set the new event values
1099      $vevent->summary = $params['eventname'];
1100      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
1101      $description = $params['eventdescription'];
1102
1103      // Remove existing timestamps to overwrite them
1104      $vevent->remove('DESCRIPTION');
1105      $vevent->remove('DTSTAMP');
1106      $vevent->remove('LAST-MODIFIED');
1107      $vevent->remove('ATTACH');
1108
1109      // Add new time stamps and description
1110      $vevent->add('DTSTAMP', $dtStamp);
1111      $vevent->add('LAST-MODIFIED', $dtStamp);
1112      if($description !== '')
1113        $vevent->add('DESCRIPTION', $description);
1114
1115      // Add attachments
1116      $attachments = $params['attachments'];
1117      if(!is_null($attachments))
1118        foreach($attachments as $attachment)
1119          $vevent->add('ATTACH', $attachment);
1120
1121      // Setup DTSTART
1122      $dtStart = new \DateTime();
1123      $dtStart->setTimezone($timezone);
1124      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
1125      if($params['allday'] != '1')
1126        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
1127
1128      // Setup DTEND
1129      $dtEnd = new \DateTime();
1130      $dtEnd->setTimezone($timezone);
1131      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
1132      if($params['allday'] != '1')
1133        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
1134
1135      // According to the VCal spec, we need to add a whole day here
1136      if($params['allday'] == '1')
1137          $dtEnd->add(new \DateInterval('P1D'));
1138      $vevent->remove('DTSTART');
1139      $vevent->remove('DTEND');
1140      $dtStartEv = $vevent->add('DTSTART', $dtStart);
1141      $dtEndEv = $vevent->add('DTEND', $dtEnd);
1142
1143      // Remove the time for allday events
1144      if($params['allday'] == '1')
1145      {
1146          $dtStartEv['VALUE'] = 'DATE';
1147          $dtEndEv['VALUE'] = 'DATE';
1148      }
1149      $eventStr = $vcal->serialize();
1150      if(strpos($id, 'webdav://') === 0)
1151      {
1152          $connectionId = str_replace('webdav://', '', $id);
1153          return $wdc->editCalendarEntry($connectionId, $uid, $eventStr);
1154      }
1155      else
1156      {
1157          $now = new DateTime();
1158          // Actually write to the database
1159          $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ".
1160                   "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?";
1161          $res = $this->sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(),
1162                                      $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid);
1163          if($res !== false)
1164          {
1165              $this->updateSyncTokenLog($calid, $uri, 'modified');
1166              return true;
1167          }
1168      }
1169      return false;
1170  }
1171
1172  /**
1173   * Delete an event from a calendar by calendar ID and URI
1174   *
1175   * @param int $calid The calendar's ID
1176   * @param string $uri The object's URI
1177   *
1178   * @return true
1179   */
1180  public function deleteCalendarEntryForCalendarByUri($calid, $uri)
1181  {
1182      $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?";
1183      $res = $this->sqlite->query($query, $calid, $uri);
1184      if($res !== false)
1185      {
1186          $this->updateSyncTokenLog($calid, $uri, 'deleted');
1187      }
1188      return true;
1189  }
1190
1191  /**
1192   * Delete a calendar entry for a given page. Actually, the event is removed
1193   * based on the entry's UID, so that page ID is no used.
1194   *
1195   * @param string $id The page's ID (unused)
1196   * @param array $params The parameter array to work with
1197   *
1198   * @return boolean True
1199   */
1200  public function deleteCalendarEntryForPage($id, $params)
1201  {
1202      $uid = $params['uid'];
1203      if(strpos($id, 'webdav://') === 0)
1204      {
1205        $wdc =& plugin_load('helper', 'webdavclient');
1206        if(is_null($wdc))
1207          return false;
1208        $connectionId = str_replace('webdav://', '', $id);
1209        $result = $wdc->deleteCalendarEntry($connectionId, $uid);
1210        return $result;
1211      }
1212      $event = $this->getEventWithUid($uid);
1213      $calid = $event['calendarid'];
1214      $uri = $event['uri'];
1215      $query = "DELETE FROM calendarobjects WHERE uid = ?";
1216      $res = $this->sqlite->query($query, $uid);
1217      if($res !== false)
1218      {
1219          $this->updateSyncTokenLog($calid, $uri, 'deleted');
1220      }
1221      return true;
1222  }
1223
1224  /**
1225   * Retrieve the current sync token for a calendar
1226   *
1227   * @param string $calid The calendar id
1228   *
1229   * @return mixed The synctoken or false
1230   */
1231  public function getSyncTokenForCalendar($calid)
1232  {
1233      $row = $this->getCalendarSettings($calid);
1234      if(isset($row['synctoken']))
1235          return $row['synctoken'];
1236      return false;
1237  }
1238
1239  /**
1240   * Helper function to convert the operation name to
1241   * an operation code as stored in the database
1242   *
1243   * @param string $operationName The operation name
1244   *
1245   * @return mixed The operation code or false
1246   */
1247  public function operationNameToOperation($operationName)
1248  {
1249      switch($operationName)
1250      {
1251          case 'added':
1252              return 1;
1253          break;
1254          case 'modified':
1255              return 2;
1256          break;
1257          case 'deleted':
1258              return 3;
1259          break;
1260      }
1261      return false;
1262  }
1263
1264  /**
1265   * Update the sync token log based on the calendar id and the
1266   * operation that was performed.
1267   *
1268   * @param string $calid The calendar ID that was modified
1269   * @param string $uri The calendar URI that was modified
1270   * @param string $operation The operation that was performed
1271   *
1272   * @return boolean True on success, otherwise false
1273   */
1274  private function updateSyncTokenLog($calid, $uri, $operation)
1275  {
1276      $currentToken = $this->getSyncTokenForCalendar($calid);
1277      $operationCode = $this->operationNameToOperation($operation);
1278      if(($operationCode === false) || ($currentToken === false))
1279          return false;
1280      $values = array($uri,
1281                      $currentToken,
1282                      $calid,
1283                      $operationCode
1284      );
1285      $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)";
1286      $res = $this->sqlite->query($query, $uri, $currentToken, $calid, $operationCode);
1287      if($res === false)
1288        return false;
1289      $currentToken++;
1290      $query = "UPDATE calendars SET synctoken = ? WHERE id = ?";
1291      $res = $this->sqlite->query($query, $currentToken, $calid);
1292      return ($res !== false);
1293  }
1294
1295  /**
1296   * Return the sync URL for a given Page, i.e. a calendar
1297   *
1298   * @param string $id The page's ID
1299   * @param string $user (optional) The user's ID
1300   *
1301   * @return mixed The sync url or false
1302   */
1303  public function getSyncUrlForPage($id, $user = null)
1304  {
1305      if(is_null($userid))
1306      {
1307        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
1308        {
1309          $userid = $_SERVER['REMOTE_USER'];
1310        }
1311        else
1312        {
1313          return false;
1314        }
1315      }
1316
1317      $calid = $this->getCalendarIdForPage($id);
1318      if($calid === false)
1319        return false;
1320
1321      $calsettings = $this->getCalendarSettings($calid);
1322      if(!isset($calsettings['uri']))
1323        return false;
1324
1325      $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri'];
1326      return $syncurl;
1327  }
1328
1329  /**
1330   * Return the private calendar's URL for a given page
1331   *
1332   * @param string $id the page ID
1333   *
1334   * @return mixed The private URL or false
1335   */
1336  public function getPrivateURLForPage($id)
1337  {
1338      $calid = $this->getCalendarIdForPage($id);
1339      if($calid === false)
1340        return false;
1341
1342      return $this->getPrivateURLForCalendar($calid);
1343  }
1344
1345  /**
1346   * Return the private calendar's URL for a given calendar ID
1347   *
1348   * @param string $calid The calendar's ID
1349   *
1350   * @return mixed The private URL or false
1351   */
1352  public function getPrivateURLForCalendar($calid)
1353  {
1354      if(isset($this->cachedValues['privateurl'][$calid]))
1355        return $this->cachedValues['privateurl'][$calid];
1356      $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?";
1357      $res = $this->sqlite->query($query, $calid);
1358      $row = $this->sqlite->res2row($res);
1359      if(!isset($row['url']))
1360      {
1361          $url = uniqid("dokuwiki-").".ics";
1362          $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)";
1363          $res = $this->sqlite->query($query, $url, $calid);
1364          if($res === false)
1365            return false;
1366      }
1367      else
1368      {
1369          $url = $row['url'];
1370      }
1371
1372      $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url;
1373      $this->cachedValues['privateurl'][$calid] = $url;
1374      return $url;
1375  }
1376
1377  /**
1378   * Retrieve the calendar ID for a given private calendar URL
1379   *
1380   * @param string $url The private URL
1381   *
1382   * @return mixed The calendar ID or false
1383   */
1384  public function getCalendarForPrivateURL($url)
1385  {
1386      $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?";
1387      $res = $this->sqlite->query($query, $url);
1388      $row = $this->sqlite->res2row($res);
1389      if(!isset($row['calid']))
1390        return false;
1391      return $row['calid'];
1392  }
1393
1394  /**
1395   * Return a given calendar as ICS feed, i.e. all events in one ICS file.
1396   *
1397   * @param string $calid The calendar ID to retrieve
1398   *
1399   * @return mixed The calendar events as string or false
1400   */
1401  public function getCalendarAsICSFeed($calid)
1402  {
1403      $calSettings = $this->getCalendarSettings($calid);
1404      if($calSettings === false)
1405        return false;
1406      $events = $this->getAllCalendarEvents($calid);
1407      if($events === false)
1408        return false;
1409
1410      // Load SabreDAV
1411      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1412      $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:";
1413      $out .= $calSettings['displayname']."\r\n";
1414      foreach($events as $event)
1415      {
1416          $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
1417          $evt = $vcal->VEVENT;
1418          $out .= $evt->serialize();
1419      }
1420      $out .= "END:VCALENDAR\r\n";
1421      return $out;
1422  }
1423
1424  /**
1425   * Retrieve a configuration option for the plugin
1426   *
1427   * @param string $key The key to query
1428   * @return mixed The option set, null if not found
1429   */
1430  public function getConfig($key)
1431  {
1432      return $this->getConf($key);
1433  }
1434
1435  /**
1436   * Parses some information from calendar objects, used for optimized
1437   * calendar-queries. Taken nearly unmodified from Sabre's PDO backend
1438   *
1439   * Returns an array with the following keys:
1440   *   * etag - An md5 checksum of the object without the quotes.
1441   *   * size - Size of the object in bytes
1442   *   * componentType - VEVENT, VTODO or VJOURNAL
1443   *   * firstOccurence
1444   *   * lastOccurence
1445   *   * uid - value of the UID property
1446   *
1447   * @param string $calendarData
1448   * @return array
1449   */
1450  protected function getDenormalizedData($calendarData)
1451  {
1452    require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1453
1454    $vObject = \Sabre\VObject\Reader::read($calendarData);
1455    $componentType = null;
1456    $component = null;
1457    $firstOccurence = null;
1458    $lastOccurence = null;
1459    $uid = null;
1460    foreach ($vObject->getComponents() as $component)
1461    {
1462        if ($component->name !== 'VTIMEZONE')
1463        {
1464            $componentType = $component->name;
1465            $uid = (string)$component->UID;
1466            break;
1467        }
1468    }
1469    if (!$componentType)
1470    {
1471        return false;
1472    }
1473    if ($componentType === 'VEVENT')
1474    {
1475        $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
1476        // Finding the last occurence is a bit harder
1477        if (!isset($component->RRULE))
1478        {
1479            if (isset($component->DTEND))
1480            {
1481                $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
1482            }
1483            elseif (isset($component->DURATION))
1484            {
1485                $endDate = clone $component->DTSTART->getDateTime();
1486                $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue()));
1487                $lastOccurence = $endDate->getTimeStamp();
1488            }
1489            elseif (!$component->DTSTART->hasTime())
1490            {
1491                $endDate = clone $component->DTSTART->getDateTime();
1492                $endDate->modify('+1 day');
1493                $lastOccurence = $endDate->getTimeStamp();
1494            }
1495            else
1496            {
1497                $lastOccurence = $firstOccurence;
1498            }
1499        }
1500        else
1501        {
1502            $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID);
1503            $maxDate = new \DateTime('2038-01-01');
1504            if ($it->isInfinite())
1505            {
1506                $lastOccurence = $maxDate->getTimeStamp();
1507            }
1508            else
1509            {
1510                $end = $it->getDtEnd();
1511                while ($it->valid() && $end < $maxDate)
1512                {
1513                    $end = $it->getDtEnd();
1514                    $it->next();
1515                }
1516                $lastOccurence = $end->getTimeStamp();
1517            }
1518        }
1519    }
1520
1521    return array(
1522        'etag'           => md5($calendarData),
1523        'size'           => strlen($calendarData),
1524        'componentType'  => $componentType,
1525        'firstOccurence' => $firstOccurence,
1526        'lastOccurence'  => $lastOccurence,
1527        'uid'            => $uid,
1528    );
1529
1530  }
1531
1532  /**
1533   * Query a calendar by ID and taking several filters into account.
1534   * This is heavily based on Sabre's PDO backend.
1535   *
1536   * @param int $calendarId The calendar's ID
1537   * @param array $filters The filter array to apply
1538   *
1539   * @return mixed The result
1540   */
1541  public function calendarQuery($calendarId, $filters)
1542  {
1543    $componentType = null;
1544    $requirePostFilter = true;
1545    $timeRange = null;
1546
1547    // if no filters were specified, we don't need to filter after a query
1548    if (!$filters['prop-filters'] && !$filters['comp-filters'])
1549    {
1550        $requirePostFilter = false;
1551    }
1552
1553    // Figuring out if there's a component filter
1554    if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined'])
1555    {
1556        $componentType = $filters['comp-filters'][0]['name'];
1557
1558        // Checking if we need post-filters
1559        if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters'])
1560        {
1561            $requirePostFilter = false;
1562        }
1563        // There was a time-range filter
1564        if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range']))
1565        {
1566            $timeRange = $filters['comp-filters'][0]['time-range'];
1567
1568            // If start time OR the end time is not specified, we can do a
1569            // 100% accurate mysql query.
1570            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end']))
1571            {
1572                $requirePostFilter = false;
1573            }
1574        }
1575
1576    }
1577
1578    if ($requirePostFilter)
1579    {
1580        $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?";
1581    }
1582    else
1583    {
1584        $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?";
1585    }
1586
1587    $values = array(
1588        $calendarId
1589    );
1590
1591    if ($componentType)
1592    {
1593        $query .= " AND componenttype = ?";
1594        $values[] = $componentType;
1595    }
1596
1597    if ($timeRange && $timeRange['start'])
1598    {
1599        $query .= " AND lastoccurence > ?";
1600        $values[] = $timeRange['start']->getTimeStamp();
1601    }
1602    if ($timeRange && $timeRange['end'])
1603    {
1604        $query .= " AND firstoccurence < ?";
1605        $values[] = $timeRange['end']->getTimeStamp();
1606    }
1607
1608    $res = $this->sqlite->query($query, $values);
1609    $arr = $this->sqlite->res2arr($res);
1610
1611    $result = array();
1612    foreach($arr as $row)
1613    {
1614        if ($requirePostFilter)
1615        {
1616            if (!$this->validateFilterForObject($row, $filters))
1617            {
1618                continue;
1619            }
1620        }
1621        $result[] = $row['uri'];
1622
1623    }
1624
1625    return $result;
1626  }
1627
1628  /**
1629   * This method validates if a filter (as passed to calendarQuery) matches
1630   * the given object. Taken from Sabre's PDO backend
1631   *
1632   * @param array $object
1633   * @param array $filters
1634   * @return bool
1635   */
1636  protected function validateFilterForObject($object, $filters)
1637  {
1638      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1639      // Unfortunately, setting the 'calendardata' here is optional. If
1640      // it was excluded, we actually need another call to get this as
1641      // well.
1642      if (!isset($object['calendardata']))
1643      {
1644          $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']);
1645      }
1646
1647      $vObject = \Sabre\VObject\Reader::read($object['calendardata']);
1648      $validator = new \Sabre\CalDAV\CalendarQueryValidator();
1649
1650      return $validator->validate($vObject, $filters);
1651
1652  }
1653
1654  /**
1655   * Retrieve changes for a given calendar based on the given syncToken.
1656   *
1657   * @param int $calid The calendar's ID
1658   * @param int $syncToken The supplied sync token
1659   * @param int $syncLevel The sync level
1660   * @param int $limit The limit of changes
1661   *
1662   * @return array The result
1663   */
1664  public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null)
1665  {
1666      // Current synctoken
1667      $currentToken = $this->getSyncTokenForCalendar($calid);
1668
1669      if ($currentToken === false) return null;
1670
1671      $result = array(
1672          'syncToken' => $currentToken,
1673          'added'     => array(),
1674          'modified'  => array(),
1675          'deleted'   => array(),
1676      );
1677
1678      if ($syncToken)
1679      {
1680
1681          $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken";
1682          if ($limit > 0) $query .= " LIMIT " . (int)$limit;
1683
1684          // Fetching all changes
1685          $res = $this->sqlite->query($query, $syncToken, $currentToken, $calid);
1686          if($res === false)
1687              return null;
1688
1689          $arr = $this->sqlite->res2arr($res);
1690          $changes = array();
1691
1692          // This loop ensures that any duplicates are overwritten, only the
1693          // last change on a node is relevant.
1694          foreach($arr as $row)
1695          {
1696              $changes[$row['uri']] = $row['operation'];
1697          }
1698
1699          foreach ($changes as $uri => $operation)
1700          {
1701              switch ($operation)
1702              {
1703                  case 1 :
1704                      $result['added'][] = $uri;
1705                      break;
1706                  case 2 :
1707                      $result['modified'][] = $uri;
1708                      break;
1709                  case 3 :
1710                      $result['deleted'][] = $uri;
1711                      break;
1712              }
1713
1714          }
1715      }
1716      else
1717      {
1718          // No synctoken supplied, this is the initial sync.
1719          $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?";
1720          $res = $this->sqlite->query($query);
1721          $arr = $this->sqlite->res2arr($res);
1722
1723          $result['added'] = $arr;
1724      }
1725      return $result;
1726  }
1727
1728}
1729