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