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 __construct() {
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      $duration = $event->DURATION;
1122      // Parse duration only if start is set, but end is missing
1123      if($start !== null && $end == null && $duration !== null)
1124      {
1125          $interval = $duration->getDateInterval();
1126          $dtStart = $start->getDateTime();
1127          $dtStart->setTimezone($timezone);
1128          $dtEnd = $dtStart->add($interval);
1129          $dtEnd->setTimezone($timezone);
1130          $entry['end'] = $dtEnd->format(\DateTime::ATOM);
1131      }
1132      $description = $event->DESCRIPTION;
1133      if($description !== null)
1134        $entry['description'] = (string)$description;
1135      else
1136        $entry['description'] = '';
1137      $attachments = $event->ATTACH;
1138      if($attachments !== null)
1139      {
1140        $entry['attachments'] = array();
1141        foreach($attachments as $attachment)
1142          $entry['attachments'][] = (string)$attachment;
1143      }
1144      $entry['title'] = (string)$event->summary;
1145      $entry['location'] = (string)$event->location;
1146      $entry['id'] = $uid;
1147      $entry['page'] = $page;
1148      $entry['color'] = $color;
1149      $entry['recurring'] = $recurring;
1150
1151      return $entry;
1152  }
1153
1154  /**
1155   * Retrieve an event by its UID
1156   *
1157   * @param string $uid The event's UID
1158   *
1159   * @return mixed The table row with the given event
1160   */
1161  public function getEventWithUid($uid)
1162  {
1163      $sqlite = $this->getDB();
1164      if(!$sqlite)
1165        return false;
1166      $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?";
1167      $res = $sqlite->query($query, $uid);
1168      $row = $sqlite->res2row($res);
1169      return $row;
1170  }
1171
1172  /**
1173   * Retrieve information of a calendar's object, not including the actual
1174   * calendar data! This is mainly needed for the sync support.
1175   *
1176   * @param int $calid The calendar ID
1177   *
1178   * @return mixed The result
1179   */
1180  public function getCalendarObjects($calid)
1181  {
1182      $sqlite = $this->getDB();
1183      if(!$sqlite)
1184        return false;
1185      $query = "SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM calendarobjects WHERE calendarid = ?";
1186      $res = $sqlite->query($query, $calid);
1187      $arr = $sqlite->res2arr($res);
1188      return $arr;
1189  }
1190
1191  /**
1192   * Retrieve a single calendar object by calendar ID and URI
1193   *
1194   * @param int $calid The calendar's ID
1195   * @param string $uri The object's URI
1196   *
1197   * @return mixed The result
1198   */
1199  public function getCalendarObjectByUri($calid, $uri)
1200  {
1201      $sqlite = $this->getDB();
1202      if(!$sqlite)
1203        return false;
1204      $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri = ?";
1205      $res = $sqlite->query($query, $calid, $uri);
1206      $row = $sqlite->res2row($res);
1207      return $row;
1208  }
1209
1210  /**
1211   * Retrieve several calendar objects by specifying an array of URIs.
1212   * This is mainly neede for sync.
1213   *
1214   * @param int $calid The calendar's ID
1215   * @param array $uris An array of URIs
1216   *
1217   * @return mixed The result
1218   */
1219  public function getMultipleCalendarObjectsByUri($calid, $uris)
1220  {
1221      $sqlite = $this->getDB();
1222      if(!$sqlite)
1223        return false;
1224      $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri IN (";
1225      // Inserting a whole bunch of question marks
1226      $query .= implode(',', array_fill(0, count($uris), '?'));
1227      $query .= ')';
1228      $vals = array_merge(array($calid), $uris);
1229
1230      $res = $sqlite->query($query, $vals);
1231      $arr = $sqlite->res2arr($res);
1232      return $arr;
1233  }
1234
1235  /**
1236   * Retrieve all calendar events for a given calendar ID
1237   *
1238   * @param string $calid The calendar's ID
1239   *
1240   * @return array An array containing all calendar data
1241   */
1242  public function getAllCalendarEvents($calid)
1243  {
1244      $enabled = $this->getCalendarStatus($calid);
1245      if($enabled === false)
1246        return false;
1247
1248      $sqlite = $this->getDB();
1249      if(!$sqlite)
1250        return false;
1251      $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?";
1252      $res = $sqlite->query($query, $calid);
1253      $arr = $sqlite->res2arr($res);
1254      return $arr;
1255  }
1256
1257  /**
1258   * Edit a calendar entry for a page, given by its parameters.
1259   * The params array has the same format as @see addCalendarEntryForPage
1260   *
1261   * @param string $id The page's ID to work on
1262   * @param string $user The user's ID to work on
1263   * @param array $params The parameter array for the edited calendar event
1264   *
1265   * @return boolean True on success, otherwise false
1266   */
1267  public function editCalendarEntryForPage($id, $user, $params)
1268  {
1269      if($params['currenttz'] !== '' && $params['currenttz'] !== 'local')
1270          $timezone = new \DateTimeZone($params['currenttz']);
1271      elseif($params['currenttz'] === 'local')
1272          $timezone = new \DateTimeZone($params['detectedtz']);
1273      else
1274          $timezone = new \DateTimeZone('UTC');
1275
1276      // Parse dates
1277      $startDate = explode('-', $params['eventfrom']);
1278      $startTime = explode(':', $params['eventfromtime']);
1279      $endDate = explode('-', $params['eventto']);
1280      $endTime = explode(':', $params['eventtotime']);
1281
1282      // Retrieve the existing event based on the UID
1283      $uid = $params['uid'];
1284
1285      if(strpos($id, 'webdav://') === 0)
1286      {
1287        $wdc =& plugin_load('helper', 'webdavclient');
1288        if(is_null($wdc))
1289          return false;
1290        $event = $wdc->getCalendarEntryByUid($uid);
1291      }
1292      else
1293      {
1294        $event = $this->getEventWithUid($uid);
1295      }
1296
1297      // Load SabreDAV
1298      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1299      if(!isset($event['calendardata']))
1300        return false;
1301      $uri = $event['uri'];
1302      $calid = $event['calendarid'];
1303
1304      // Parse the existing event
1305      $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
1306      $vevent = $vcal->VEVENT;
1307
1308      // Set the new event values
1309      $vevent->summary = $params['eventname'];
1310      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
1311      $description = $params['eventdescription'];
1312      $location = $params['eventlocation'];
1313
1314      // Remove existing timestamps to overwrite them
1315      $vevent->remove('DESCRIPTION');
1316      $vevent->remove('DTSTAMP');
1317      $vevent->remove('LAST-MODIFIED');
1318      $vevent->remove('ATTACH');
1319      $vevent->remove('LOCATION');
1320
1321      // Add new time stamps, description and location
1322      $vevent->add('DTSTAMP', $dtStamp);
1323      $vevent->add('LAST-MODIFIED', $dtStamp);
1324      if($description !== '')
1325        $vevent->add('DESCRIPTION', $description);
1326      if($location !== '')
1327        $vevent->add('LOCATION', $location);
1328
1329      // Add attachments
1330      $attachments = $params['attachments'];
1331      if(!is_null($attachments))
1332        foreach($attachments as $attachment)
1333          $vevent->add('ATTACH', $attachment);
1334
1335      // Setup DTSTART
1336      $dtStart = new \DateTime();
1337      $dtStart->setTimezone($timezone);
1338      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
1339      if($params['allday'] != '1')
1340        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
1341
1342      // Setup DTEND
1343      $dtEnd = new \DateTime();
1344      $dtEnd->setTimezone($timezone);
1345      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
1346      if($params['allday'] != '1')
1347        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
1348
1349      // According to the VCal spec, we need to add a whole day here
1350      if($params['allday'] == '1')
1351          $dtEnd->add(new \DateInterval('P1D'));
1352      $vevent->remove('DTSTART');
1353      $vevent->remove('DTEND');
1354      $dtStartEv = $vevent->add('DTSTART', $dtStart);
1355      $dtEndEv = $vevent->add('DTEND', $dtEnd);
1356
1357      // Remove the time for allday events
1358      if($params['allday'] == '1')
1359      {
1360          $dtStartEv['VALUE'] = 'DATE';
1361          $dtEndEv['VALUE'] = 'DATE';
1362      }
1363      $eventStr = $vcal->serialize();
1364      if(strpos($id, 'webdav://') === 0)
1365      {
1366          $connectionId = str_replace('webdav://', '', $id);
1367          return $wdc->editCalendarEntry($connectionId, $uid, $eventStr);
1368      }
1369      else
1370      {
1371          $sqlite = $this->getDB();
1372          if(!$sqlite)
1373            return false;
1374          $now = new DateTime();
1375          // Actually write to the database
1376          $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ".
1377                   "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?";
1378          $res = $sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(),
1379                                      $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid);
1380          if($res !== false)
1381          {
1382              $this->updateSyncTokenLog($calid, $uri, 'modified');
1383              return true;
1384          }
1385      }
1386      return false;
1387  }
1388
1389  /**
1390   * Delete an event from a calendar by calendar ID and URI
1391   *
1392   * @param int $calid The calendar's ID
1393   * @param string $uri The object's URI
1394   *
1395   * @return true
1396   */
1397  public function deleteCalendarEntryForCalendarByUri($calid, $uri)
1398  {
1399      $sqlite = $this->getDB();
1400      if(!$sqlite)
1401        return false;
1402      $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?";
1403      $res = $sqlite->query($query, $calid, $uri);
1404      if($res !== false)
1405      {
1406          $this->updateSyncTokenLog($calid, $uri, 'deleted');
1407      }
1408      return true;
1409  }
1410
1411  /**
1412   * Delete a calendar entry for a given page. Actually, the event is removed
1413   * based on the entry's UID, so that page ID is no used.
1414   *
1415   * @param string $id The page's ID (unused)
1416   * @param array $params The parameter array to work with
1417   *
1418   * @return boolean True
1419   */
1420  public function deleteCalendarEntryForPage($id, $params)
1421  {
1422      $uid = $params['uid'];
1423      if(strpos($id, 'webdav://') === 0)
1424      {
1425        $wdc =& plugin_load('helper', 'webdavclient');
1426        if(is_null($wdc))
1427          return false;
1428        $connectionId = str_replace('webdav://', '', $id);
1429        $result = $wdc->deleteCalendarEntry($connectionId, $uid);
1430        return $result;
1431      }
1432      $sqlite = $this->getDB();
1433      if(!$sqlite)
1434        return false;
1435      $event = $this->getEventWithUid($uid);
1436      $calid = $event['calendarid'];
1437      $uri = $event['uri'];
1438      $query = "DELETE FROM calendarobjects WHERE uid = ?";
1439      $res = $sqlite->query($query, $uid);
1440      if($res !== false)
1441      {
1442          $this->updateSyncTokenLog($calid, $uri, 'deleted');
1443      }
1444      return true;
1445  }
1446
1447  /**
1448   * Retrieve the current sync token for a calendar
1449   *
1450   * @param string $calid The calendar id
1451   *
1452   * @return mixed The synctoken or false
1453   */
1454  public function getSyncTokenForCalendar($calid)
1455  {
1456      $row = $this->getCalendarSettings($calid);
1457      if(isset($row['synctoken']))
1458          return $row['synctoken'];
1459      return false;
1460  }
1461
1462  /**
1463   * Helper function to convert the operation name to
1464   * an operation code as stored in the database
1465   *
1466   * @param string $operationName The operation name
1467   *
1468   * @return mixed The operation code or false
1469   */
1470  public function operationNameToOperation($operationName)
1471  {
1472      switch($operationName)
1473      {
1474          case 'added':
1475              return 1;
1476          break;
1477          case 'modified':
1478              return 2;
1479          break;
1480          case 'deleted':
1481              return 3;
1482          break;
1483      }
1484      return false;
1485  }
1486
1487  /**
1488   * Update the sync token log based on the calendar id and the
1489   * operation that was performed.
1490   *
1491   * @param string $calid The calendar ID that was modified
1492   * @param string $uri The calendar URI that was modified
1493   * @param string $operation The operation that was performed
1494   *
1495   * @return boolean True on success, otherwise false
1496   */
1497  private function updateSyncTokenLog($calid, $uri, $operation)
1498  {
1499      $currentToken = $this->getSyncTokenForCalendar($calid);
1500      $operationCode = $this->operationNameToOperation($operation);
1501      if(($operationCode === false) || ($currentToken === false))
1502          return false;
1503      $values = array($uri,
1504                      $currentToken,
1505                      $calid,
1506                      $operationCode
1507      );
1508      $sqlite = $this->getDB();
1509      if(!$sqlite)
1510        return false;
1511      $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)";
1512      $res = $sqlite->query($query, $uri, $currentToken, $calid, $operationCode);
1513      if($res === false)
1514        return false;
1515      $currentToken++;
1516      $query = "UPDATE calendars SET synctoken = ? WHERE id = ?";
1517      $res = $sqlite->query($query, $currentToken, $calid);
1518      return ($res !== false);
1519  }
1520
1521  /**
1522   * Return the sync URL for a given Page, i.e. a calendar
1523   *
1524   * @param string $id The page's ID
1525   * @param string $user (optional) The user's ID
1526   *
1527   * @return mixed The sync url or false
1528   */
1529  public function getSyncUrlForPage($id, $user = null)
1530  {
1531      if(is_null($userid))
1532      {
1533        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
1534        {
1535          $userid = $_SERVER['REMOTE_USER'];
1536        }
1537        else
1538        {
1539          return false;
1540        }
1541      }
1542
1543      $calid = $this->getCalendarIdForPage($id);
1544      if($calid === false)
1545        return false;
1546
1547      $calsettings = $this->getCalendarSettings($calid);
1548      if(!isset($calsettings['uri']))
1549        return false;
1550
1551      $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri'];
1552      return $syncurl;
1553  }
1554
1555  /**
1556   * Return the private calendar's URL for a given page
1557   *
1558   * @param string $id the page ID
1559   *
1560   * @return mixed The private URL or false
1561   */
1562  public function getPrivateURLForPage($id)
1563  {
1564      $calid = $this->getCalendarIdForPage($id);
1565      if($calid === false)
1566        return false;
1567
1568      return $this->getPrivateURLForCalendar($calid);
1569  }
1570
1571  /**
1572   * Return the private calendar's URL for a given calendar ID
1573   *
1574   * @param string $calid The calendar's ID
1575   *
1576   * @return mixed The private URL or false
1577   */
1578  public function getPrivateURLForCalendar($calid)
1579  {
1580      if(isset($this->cachedValues['privateurl'][$calid]))
1581        return $this->cachedValues['privateurl'][$calid];
1582      $sqlite = $this->getDB();
1583      if(!$sqlite)
1584        return false;
1585      $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?";
1586      $res = $sqlite->query($query, $calid);
1587      $row = $sqlite->res2row($res);
1588      if(!isset($row['url']))
1589      {
1590          $url = 'dokuwiki-' . bin2hex(random_bytes(16)) . '.ics';
1591          $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)";
1592          $res = $sqlite->query($query, $url, $calid);
1593          if($res === false)
1594            return false;
1595      }
1596      else
1597      {
1598          $url = $row['url'];
1599      }
1600
1601      $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url;
1602      $this->cachedValues['privateurl'][$calid] = $url;
1603      return $url;
1604  }
1605
1606  /**
1607   * Retrieve the calendar ID for a given private calendar URL
1608   *
1609   * @param string $url The private URL
1610   *
1611   * @return mixed The calendar ID or false
1612   */
1613  public function getCalendarForPrivateURL($url)
1614  {
1615      $sqlite = $this->getDB();
1616      if(!$sqlite)
1617        return false;
1618      $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?";
1619      $res = $sqlite->query($query, $url);
1620      $row = $sqlite->res2row($res);
1621      if(!isset($row['calid']))
1622        return false;
1623      return $row['calid'];
1624  }
1625
1626  /**
1627   * Return a given calendar as ICS feed, i.e. all events in one ICS file.
1628   *
1629   * @param string $calid The calendar ID to retrieve
1630   *
1631   * @return mixed The calendar events as string or false
1632   */
1633  public function getCalendarAsICSFeed($calid)
1634  {
1635      $calSettings = $this->getCalendarSettings($calid);
1636      if($calSettings === false)
1637        return false;
1638      $events = $this->getAllCalendarEvents($calid);
1639      if($events === false)
1640        return false;
1641
1642      // Load SabreDAV
1643      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1644      $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:";
1645      $out .= $calSettings['displayname']."\r\n";
1646      foreach($events as $event)
1647      {
1648          $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
1649          $evt = $vcal->VEVENT;
1650          $out .= $evt->serialize();
1651      }
1652      $out .= "END:VCALENDAR\r\n";
1653      return $out;
1654  }
1655
1656  /**
1657   * Retrieve a configuration option for the plugin
1658   *
1659   * @param string $key The key to query
1660   * @return mixed The option set, null if not found
1661   */
1662  public function getConfig($key)
1663  {
1664      return $this->getConf($key);
1665  }
1666
1667  /**
1668   * Parses some information from calendar objects, used for optimized
1669   * calendar-queries. Taken nearly unmodified from Sabre's PDO backend
1670   *
1671   * Returns an array with the following keys:
1672   *   * etag - An md5 checksum of the object without the quotes.
1673   *   * size - Size of the object in bytes
1674   *   * componentType - VEVENT, VTODO or VJOURNAL
1675   *   * firstOccurence
1676   *   * lastOccurence
1677   *   * uid - value of the UID property
1678   *
1679   * @param string $calendarData
1680   * @return array
1681   */
1682  protected function getDenormalizedData($calendarData)
1683  {
1684    require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1685
1686    $vObject = \Sabre\VObject\Reader::read($calendarData);
1687    $componentType = null;
1688    $component = null;
1689    $firstOccurence = null;
1690    $lastOccurence = null;
1691    $uid = null;
1692    foreach ($vObject->getComponents() as $component)
1693    {
1694        if ($component->name !== 'VTIMEZONE')
1695        {
1696            $componentType = $component->name;
1697            $uid = (string)$component->UID;
1698            break;
1699        }
1700    }
1701    if (!$componentType)
1702    {
1703        return false;
1704    }
1705    if ($componentType === 'VEVENT')
1706    {
1707        $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
1708        // Finding the last occurence is a bit harder
1709        if (!isset($component->RRULE))
1710        {
1711            if (isset($component->DTEND))
1712            {
1713                $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
1714            }
1715            elseif (isset($component->DURATION))
1716            {
1717                $endDate = clone $component->DTSTART->getDateTime();
1718                $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue()));
1719                $lastOccurence = $endDate->getTimeStamp();
1720            }
1721            elseif (!$component->DTSTART->hasTime())
1722            {
1723                $endDate = clone $component->DTSTART->getDateTime();
1724                $endDate->modify('+1 day');
1725                $lastOccurence = $endDate->getTimeStamp();
1726            }
1727            else
1728            {
1729                $lastOccurence = $firstOccurence;
1730            }
1731        }
1732        else
1733        {
1734            $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID);
1735            $maxDate = new \DateTime('2038-01-01');
1736            if ($it->isInfinite())
1737            {
1738                $lastOccurence = $maxDate->getTimeStamp();
1739            }
1740            else
1741            {
1742                $end = $it->getDtEnd();
1743                while ($it->valid() && $end < $maxDate)
1744                {
1745                    $end = $it->getDtEnd();
1746                    $it->next();
1747                }
1748                $lastOccurence = $end->getTimeStamp();
1749            }
1750        }
1751    }
1752
1753    return array(
1754        'etag'           => md5($calendarData),
1755        'size'           => strlen($calendarData),
1756        'componentType'  => $componentType,
1757        'firstOccurence' => $firstOccurence,
1758        'lastOccurence'  => $lastOccurence,
1759        'uid'            => $uid,
1760    );
1761
1762  }
1763
1764  /**
1765   * Query a calendar by ID and taking several filters into account.
1766   * This is heavily based on Sabre's PDO backend.
1767   *
1768   * @param int $calendarId The calendar's ID
1769   * @param array $filters The filter array to apply
1770   *
1771   * @return mixed The result
1772   */
1773  public function calendarQuery($calendarId, $filters)
1774  {
1775    dbglog('davcal::helper::calendarQuery');
1776    $componentType = null;
1777    $requirePostFilter = true;
1778    $timeRange = null;
1779    $sqlite = $this->getDB();
1780    if(!$sqlite)
1781      return false;
1782
1783    // if no filters were specified, we don't need to filter after a query
1784    if (!$filters['prop-filters'] && !$filters['comp-filters'])
1785    {
1786        $requirePostFilter = false;
1787    }
1788
1789    // Figuring out if there's a component filter
1790    if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined'])
1791    {
1792        $componentType = $filters['comp-filters'][0]['name'];
1793
1794        // Checking if we need post-filters
1795        if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters'])
1796        {
1797            $requirePostFilter = false;
1798        }
1799        // There was a time-range filter
1800        if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range']))
1801        {
1802            $timeRange = $filters['comp-filters'][0]['time-range'];
1803
1804            // If start time OR the end time is not specified, we can do a
1805            // 100% accurate mysql query.
1806            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end']))
1807            {
1808                $requirePostFilter = false;
1809            }
1810        }
1811
1812    }
1813
1814    if ($requirePostFilter)
1815    {
1816        $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?";
1817    }
1818    else
1819    {
1820        $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?";
1821    }
1822
1823    $values = array(
1824        $calendarId
1825    );
1826
1827    if ($componentType)
1828    {
1829        $query .= " AND componenttype = ?";
1830        $values[] = $componentType;
1831    }
1832
1833    if ($timeRange && $timeRange['start'])
1834    {
1835        $query .= " AND lastoccurence > ?";
1836        $values[] = $timeRange['start']->getTimeStamp();
1837    }
1838    if ($timeRange && $timeRange['end'])
1839    {
1840        $query .= " AND firstoccurence < ?";
1841        $values[] = $timeRange['end']->getTimeStamp();
1842    }
1843
1844    $res = $sqlite->query($query, $values);
1845    $arr = $sqlite->res2arr($res);
1846
1847    $result = array();
1848    foreach($arr as $row)
1849    {
1850        if ($requirePostFilter)
1851        {
1852            if (!$this->validateFilterForObject($row, $filters))
1853            {
1854                continue;
1855            }
1856        }
1857        $result[] = $row['uri'];
1858
1859    }
1860
1861    return $result;
1862  }
1863
1864  /**
1865   * This method validates if a filter (as passed to calendarQuery) matches
1866   * the given object. Taken from Sabre's PDO backend
1867   *
1868   * @param array $object
1869   * @param array $filters
1870   * @return bool
1871   */
1872  protected function validateFilterForObject($object, $filters)
1873  {
1874      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1875      // Unfortunately, setting the 'calendardata' here is optional. If
1876      // it was excluded, we actually need another call to get this as
1877      // well.
1878      if (!isset($object['calendardata']))
1879      {
1880          $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']);
1881      }
1882
1883      $vObject = \Sabre\VObject\Reader::read($object['calendardata']);
1884      $validator = new \Sabre\CalDAV\CalendarQueryValidator();
1885
1886      $res = $validator->validate($vObject, $filters);
1887      return $res;
1888
1889  }
1890
1891  /**
1892   * Retrieve changes for a given calendar based on the given syncToken.
1893   *
1894   * @param int $calid The calendar's ID
1895   * @param int $syncToken The supplied sync token
1896   * @param int $syncLevel The sync level
1897   * @param int $limit The limit of changes
1898   *
1899   * @return array The result
1900   */
1901  public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null)
1902  {
1903      // Current synctoken
1904      $currentToken = $this->getSyncTokenForCalendar($calid);
1905
1906      if ($currentToken === false) return null;
1907
1908      $result = array(
1909          'syncToken' => $currentToken,
1910          'added'     => array(),
1911          'modified'  => array(),
1912          'deleted'   => array(),
1913      );
1914      $sqlite = $this->getDB();
1915      if(!$sqlite)
1916        return false;
1917
1918      if ($syncToken)
1919      {
1920
1921          $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken";
1922          if ($limit > 0) $query .= " LIMIT " . (int)$limit;
1923
1924          // Fetching all changes
1925          $res = $sqlite->query($query, $syncToken, $currentToken, $calid);
1926          if($res === false)
1927              return null;
1928
1929          $arr = $sqlite->res2arr($res);
1930          $changes = array();
1931
1932          // This loop ensures that any duplicates are overwritten, only the
1933          // last change on a node is relevant.
1934          foreach($arr as $row)
1935          {
1936              $changes[$row['uri']] = $row['operation'];
1937          }
1938
1939          foreach ($changes as $uri => $operation)
1940          {
1941              switch ($operation)
1942              {
1943                  case 1 :
1944                      $result['added'][] = $uri;
1945                      break;
1946                  case 2 :
1947                      $result['modified'][] = $uri;
1948                      break;
1949                  case 3 :
1950                      $result['deleted'][] = $uri;
1951                      break;
1952              }
1953
1954          }
1955      }
1956      else
1957      {
1958          // No synctoken supplied, this is the initial sync.
1959          $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?";
1960          $res = $sqlite->query($query);
1961          $arr = $sqlite->res2arr($res);
1962
1963          $result['added'] = $arr;
1964      }
1965      return $result;
1966  }
1967
1968}
1969