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