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