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