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