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