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