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