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