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