xref: /plugin/davcal/helper.php (revision b0737c19dd48bf387561e3e92a999e03bf752a9b)
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          $page = $row['page'];
552          $acl = auth_aclcheck($page, $user, $groups);
553          if($acl >= AUTH_READ)
554          {
555              $write = $acl > AUTH_READ;
556              $calids[$id] = array('readonly' => !$write);
557          }
558      }
559      return $calids;
560  }
561
562  /**
563   * Create a new calendar for a given page ID and set name and description
564   * accordingly. Also update the pagetocalendarmapping table on success.
565   *
566   * @param string $name The calendar's name
567   * @param string $description The calendar's description
568   * @param string $id (optional) The page ID to work on
569   * @param string $userid (optional) The user ID that created the calendar
570   *
571   * @return boolean True on success, otherwise false
572   */
573  public function createCalendarForPage($name, $description, $id = null, $userid = null)
574  {
575      if(is_null($id))
576      {
577          global $ID;
578          $id = $ID;
579      }
580      if(is_null($userid))
581      {
582        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
583        {
584          $userid = $_SERVER['REMOTE_USER'];
585        }
586        else
587        {
588          $userid = uniqid('davcal-');
589        }
590      }
591      $values = array('principals/'.$userid,
592                      $name,
593                      str_replace(array('/', ' ', ':'), '_', $id),
594                      $description,
595                      'VEVENT,VTODO',
596                      0,
597                      1);
598      $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) ".
599               "VALUES (?, ?, ?, ?, ?, ?, ?)";
600      $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6]);
601      if($res === false)
602        return false;
603
604      // Get the new calendar ID
605      $query = "SELECT id FROM calendars WHERE principaluri = ? AND displayname = ? AND ".
606               "uri = ? AND description = ?";
607      $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3]);
608      $row = $this->sqlite->res2row($res);
609
610      // Update the pagetocalendarmapping table with the new calendar ID
611      if(isset($row['id']))
612      {
613          $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (?, ?)";
614          $res = $this->sqlite->query($query, $id, $row['id']);
615          return ($res !== false);
616      }
617
618      return false;
619  }
620
621  /**
622   * Add a new calendar entry to the given calendar. Calendar data is
623   * specified as ICS file, thus it needs to be parsed first.
624   *
625   * This is mainly needed for the sync support.
626   *
627   * @param int $calid The calendar's ID
628   * @param string $uri The new object URI
629   * @param string $ics The ICS file
630   *
631   * @return mixed The etag.
632   */
633  public function addCalendarEntryToCalendarByICS($calid, $uri, $ics)
634  {
635    $extraData = $this->getDenormalizedData($ics);
636
637    $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)";
638    $res = $this->sqlite->query($query,
639            $calid,
640            $uri,
641            $ics,
642            time(),
643            $extraData['etag'],
644            $extraData['size'],
645            $extraData['componentType'],
646            $extraData['firstOccurence'],
647            $extraData['lastOccurence'],
648            $extraData['uid']);
649            // If successfully, update the sync token database
650    if($res !== false)
651    {
652        $this->updateSyncTokenLog($calid, $uri, 'added');
653    }
654    return $extraData['etag'];
655  }
656
657  /**
658   * Edit a calendar entry by providing a new ICS file. This is mainly
659   * needed for the sync support.
660   *
661   * @param int $calid The calendar's IS
662   * @param string $uri The object's URI to modify
663   * @param string $ics The new object's ICS file
664   */
665  public function editCalendarEntryToCalendarByICS($calid, $uri, $ics)
666  {
667      $extraData = $this->getDenormalizedData($ics);
668
669      $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?";
670      $res = $this->sqlite->query($query,
671        $ics,
672        time(),
673        $extraData['etag'],
674        $extraData['size'],
675        $extraData['componentType'],
676        $extraData['firstOccurence'],
677        $extraData['lastOccurence'],
678        $extraData['uid'],
679        $calid,
680        $uri
681      );
682      if($res !== false)
683      {
684          $this->updateSyncTokenLog($calid, $uri, 'modified');
685      }
686      return $extraData['etag'];
687  }
688
689  /**
690   * Add a new iCal entry for a given page, i.e. a given calendar.
691   *
692   * The parameter array needs to contain
693   *   detectedtz       => The timezone as detected by the browser
694   *   currenttz        => The timezone in use by the calendar
695   *   eventfrom        => The event's start date
696   *   eventfromtime    => The event's start time
697   *   eventto          => The event's end date
698   *   eventtotime      => The event's end time
699   *   eventname        => The event's name
700   *   eventdescription => The event's description
701   *
702   * @param string $id The page ID to work on
703   * @param string $user The user who created the calendar
704   * @param string $params A parameter array with values to create
705   *
706   * @return boolean True on success, otherwise false
707   */
708  public function addCalendarEntryToCalendarForPage($id, $user, $params)
709  {
710      if($params['currenttz'] !== '' && $params['currenttz'] !== 'local')
711          $timezone = new \DateTimeZone($params['currenttz']);
712      elseif($params['currenttz'] === 'local')
713          $timezone = new \DateTimeZone($params['detectedtz']);
714      else
715          $timezone = new \DateTimeZone('UTC');
716
717      // Retrieve dates from settings
718      $startDate = explode('-', $params['eventfrom']);
719      $startTime = explode(':', $params['eventfromtime']);
720      $endDate = explode('-', $params['eventto']);
721      $endTime = explode(':', $params['eventtotime']);
722
723      // Load SabreDAV
724      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
725      $vcalendar = new \Sabre\VObject\Component\VCalendar();
726
727      // Add VCalendar, UID and Event Name
728      $event = $vcalendar->add('VEVENT');
729      $uuid = \Sabre\VObject\UUIDUtil::getUUID();
730      $event->add('UID', $uuid);
731      $event->summary = $params['eventname'];
732
733      // Add a description if requested
734      $description = $params['eventdescription'];
735      if($description !== '')
736        $event->add('DESCRIPTION', $description);
737
738      // Add attachments
739      $attachments = $params['attachments'];
740      if(!is_null($attachments))
741        foreach($attachments as $attachment)
742          $event->add('ATTACH', $attachment);
743
744      // Create a timestamp for last modified, created and dtstamp values in UTC
745      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
746      $event->add('DTSTAMP', $dtStamp);
747      $event->add('CREATED', $dtStamp);
748      $event->add('LAST-MODIFIED', $dtStamp);
749
750      // Adjust the start date, based on the given timezone information
751      $dtStart = new \DateTime();
752      $dtStart->setTimezone($timezone);
753      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
754
755      // Only add the time values if it's not an allday event
756      if($params['allday'] != '1')
757        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
758
759      // Adjust the end date, based on the given timezone information
760      $dtEnd = new \DateTime();
761      $dtEnd->setTimezone($timezone);
762      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
763
764      // Only add the time values if it's not an allday event
765      if($params['allday'] != '1')
766        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
767
768      // According to the VCal spec, we need to add a whole day here
769      if($params['allday'] == '1')
770          $dtEnd->add(new \DateInterval('P1D'));
771
772      // Really add Start and End events
773      $dtStartEv = $event->add('DTSTART', $dtStart);
774      $dtEndEv = $event->add('DTEND', $dtEnd);
775
776      // Adjust the DATE format for allday events
777      if($params['allday'] == '1')
778      {
779          $dtStartEv['VALUE'] = 'DATE';
780          $dtEndEv['VALUE'] = 'DATE';
781      }
782
783      $eventStr = $vcalendar->serialize();
784
785      if(strpos($id, 'webdav://') === 0)
786      {
787          $wdc =& plugin_load('helper', 'webdavclient');
788          if(is_null($wdc))
789            return false;
790          $connectionId = str_replace('webdav://', '', $id);
791          return $wdc->addCalendarEntry($connectionId, $eventStr);
792      }
793      else
794      {
795          // Actually add the values to the database
796          $calid = $this->getCalendarIdForPage($id);
797          $uri = uniqid('dokuwiki-').'.ics';
798          $now = new \DateTime();
799
800          $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
801          $res = $this->sqlite->query($query, $calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT',
802                                      $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(),
803                                      strlen($eventStr), md5($eventStr), $uuid);
804
805          // If successfully, update the sync token database
806          if($res !== false)
807          {
808              $this->updateSyncTokenLog($calid, $uri, 'added');
809              return true;
810          }
811      }
812      return false;
813  }
814
815  /**
816   * Retrieve the calendar settings of a given calendar id
817   *
818   * @param string $calid The calendar ID
819   *
820   * @return array The calendar settings array
821   */
822  public function getCalendarSettings($calid)
823  {
824      $query = "SELECT id, principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id= ? ";
825      $res = $this->sqlite->query($query, $calid);
826      $row = $this->sqlite->res2row($res);
827      return $row;
828  }
829
830  /**
831   * Retrieve all events that are within a given date range,
832   * based on the timezone setting.
833   *
834   * There is also support for retrieving recurring events,
835   * using Sabre's VObject Iterator. Recurring events are represented
836   * as individual calendar entries with the same UID.
837   *
838   * @param string $id The page ID to work with
839   * @param string $user The user ID to work with
840   * @param string $startDate The start date as a string
841   * @param string $endDate The end date as a string
842   * @param string $color (optional) The calendar's color
843   *
844   * @return array An array containing the calendar entries.
845   */
846  public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone, $color = null)
847  {
848      if($timezone !== '' && $timezone !== 'local')
849          $timezone = new \DateTimeZone($timezone);
850      else
851          $timezone = new \DateTimeZone('UTC');
852      $data = array();
853
854      $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid = ?";
855      $startTs = null;
856      $endTs = null;
857      if($startDate !== null)
858      {
859        $startTs = new \DateTime($startDate);
860        $query .= " AND lastoccurence > ".$this->sqlite->quote_string($startTs->getTimestamp());
861      }
862      if($endDate !== null)
863      {
864        $endTs = new \DateTime($endDate);
865        $query .= " AND firstoccurence < ".$this->sqlite->quote_string($endTs->getTimestamp());
866      }
867
868      // Load SabreDAV
869      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
870
871      if(strpos($id, 'webdav://') === 0)
872      {
873          $wdc =& plugin_load('helper', 'webdavclient');
874          if(is_null($wdc))
875            return $data;
876          $connectionId = str_replace('webdav://', '', $id);
877          $arr = $wdc->getCalendarEntries($connectionId, $startDate, $endDate);
878      }
879      else
880      {
881          $calid = $this->getCalendarIdForPage($id);
882          if(is_null($color))
883            $color = $this->getCalendarColorForCalendar($calid);
884
885          // Retrieve matching calendar objects
886          $res = $this->sqlite->query($query, $calid);
887          $arr = $this->sqlite->res2arr($res);
888      }
889
890      // Parse individual calendar entries
891      foreach($arr as $row)
892      {
893          if(isset($row['calendardata']))
894          {
895              $entry = array();
896              $vcal = \Sabre\VObject\Reader::read($row['calendardata']);
897              $recurrence = $vcal->VEVENT->RRULE;
898              // If it is a recurring event, pass it through Sabre's EventIterator
899              if($recurrence != null)
900              {
901                  $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT));
902                  $rEvents->rewind();
903                  while($rEvents->valid())
904                  {
905                      $event = $rEvents->getEventObject();
906                      // If we are after the given time range, exit
907                      if(($endTs !== null) && ($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp()))
908                          break;
909
910                      // If we are before the given time range, continue
911                      if(($startTs != null) && ($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp()))
912                      {
913                          $rEvents->next();
914                          continue;
915                      }
916
917                      // If we are within the given time range, parse the event
918                      $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true);
919                      $rEvents->next();
920                  }
921              }
922              else
923                $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color);
924          }
925      }
926      return $data;
927  }
928
929  /**
930   * Helper function that parses the iCal data of a VEVENT to a calendar entry.
931   *
932   * @param \Sabre\VObject\VEvent $event The event to parse
933   * @param \DateTimeZone $timezone The timezone object
934   * @param string $uid The entry's UID
935   * @param boolean $recurring (optional) Set to true to define a recurring event
936   *
937   * @return array The parse calendar entry
938   */
939  private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false)
940  {
941      $entry = array();
942      $start = $event->DTSTART;
943      // Parse only if the start date/time is present
944      if($start !== null)
945      {
946        $dtStart = $start->getDateTime();
947        $dtStart->setTimezone($timezone);
948
949        // moment.js doesn't like times be given even if
950        // allDay is set to true
951        // This should fix T23
952        if($start['VALUE'] == 'DATE')
953        {
954          $entry['allDay'] = true;
955          $entry['start'] = $dtStart->format("Y-m-d");
956        }
957        else
958        {
959          $entry['allDay'] = false;
960          $entry['start'] = $dtStart->format(\DateTime::ATOM);
961        }
962      }
963      $end = $event->DTEND;
964      // Parse only if the end date/time is present
965      if($end !== null)
966      {
967        $dtEnd = $end->getDateTime();
968        $dtEnd->setTimezone($timezone);
969        if($end['VALUE'] == 'DATE')
970          $entry['end'] = $dtEnd->format("Y-m-d");
971        else
972          $entry['end'] = $dtEnd->format(\DateTime::ATOM);
973      }
974      $description = $event->DESCRIPTION;
975      if($description !== null)
976        $entry['description'] = (string)$description;
977      else
978        $entry['description'] = '';
979      $attachments = $event->ATTACH;
980      if($attachments !== null)
981      {
982        $entry['attachments'] = array();
983        foreach($attachments as $attachment)
984          $entry['attachments'][] = (string)$attachment;
985      }
986      $entry['title'] = (string)$event->summary;
987      $entry['id'] = $uid;
988      $entry['page'] = $page;
989      $entry['color'] = $color;
990      $entry['recurring'] = $recurring;
991
992      return $entry;
993  }
994
995  /**
996   * Retrieve an event by its UID
997   *
998   * @param string $uid The event's UID
999   *
1000   * @return mixed The table row with the given event
1001   */
1002  public function getEventWithUid($uid)
1003  {
1004      $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?";
1005      $res = $this->sqlite->query($query, $uid);
1006      $row = $this->sqlite->res2row($res);
1007      return $row;
1008  }
1009
1010  /**
1011   * Retrieve information of a calendar's object, not including the actual
1012   * calendar data! This is mainly neede for the sync support.
1013   *
1014   * @param int $calid The calendar ID
1015   *
1016   * @return mixed The result
1017   */
1018  public function getCalendarObjects($calid)
1019  {
1020      $query = "SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM calendarobjects WHERE calendarid = ?";
1021      $res = $this->sqlite->query($query, $calid);
1022      $arr = $this->sqlite->res2arr($res);
1023      return $arr;
1024  }
1025
1026  /**
1027   * Retrieve a single calendar object by calendar ID and URI
1028   *
1029   * @param int $calid The calendar's ID
1030   * @param string $uri The object's URI
1031   *
1032   * @return mixed The result
1033   */
1034  public function getCalendarObjectByUri($calid, $uri)
1035  {
1036      $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri = ?";
1037      $res = $this->sqlite->query($query, $calid, $uri);
1038      $row = $this->sqlite->res2row($res);
1039      return $row;
1040  }
1041
1042  /**
1043   * Retrieve several calendar objects by specifying an array of URIs.
1044   * This is mainly neede for sync.
1045   *
1046   * @param int $calid The calendar's ID
1047   * @param array $uris An array of URIs
1048   *
1049   * @return mixed The result
1050   */
1051  public function getMultipleCalendarObjectsByUri($calid, $uris)
1052  {
1053        $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri IN (";
1054        // Inserting a whole bunch of question marks
1055        $query .= implode(',', array_fill(0, count($uris), '?'));
1056        $query .= ')';
1057        $vals = array_merge(array($calid), $uris);
1058
1059        $res = $this->sqlite->query($query, $vals);
1060        $arr = $this->sqlite->res2arr($res);
1061        return $arr;
1062  }
1063
1064  /**
1065   * Retrieve all calendar events for a given calendar ID
1066   *
1067   * @param string $calid The calendar's ID
1068   *
1069   * @return array An array containing all calendar data
1070   */
1071  public function getAllCalendarEvents($calid)
1072  {
1073      $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?";
1074      $res = $this->sqlite->query($query, $calid);
1075      $arr = $this->sqlite->res2arr($res);
1076      return $arr;
1077  }
1078
1079  /**
1080   * Edit a calendar entry for a page, given by its parameters.
1081   * The params array has the same format as @see addCalendarEntryForPage
1082   *
1083   * @param string $id The page's ID to work on
1084   * @param string $user The user's ID to work on
1085   * @param array $params The parameter array for the edited calendar event
1086   *
1087   * @return boolean True on success, otherwise false
1088   */
1089  public function editCalendarEntryForPage($id, $user, $params)
1090  {
1091      if($params['currenttz'] !== '' && $params['currenttz'] !== 'local')
1092          $timezone = new \DateTimeZone($params['currenttz']);
1093      elseif($params['currenttz'] === 'local')
1094          $timezone = new \DateTimeZone($params['detectedtz']);
1095      else
1096          $timezone = new \DateTimeZone('UTC');
1097
1098      // Parse dates
1099      $startDate = explode('-', $params['eventfrom']);
1100      $startTime = explode(':', $params['eventfromtime']);
1101      $endDate = explode('-', $params['eventto']);
1102      $endTime = explode(':', $params['eventtotime']);
1103
1104      // Retrieve the existing event based on the UID
1105      $uid = $params['uid'];
1106
1107      if(strpos($id, 'webdav://') === 0)
1108      {
1109        $wdc =& plugin_load('helper', 'webdavclient');
1110        if(is_null($wdc))
1111          return false;
1112        $event = $wdc->getCalendarEntryByUid($uid);
1113      }
1114      else
1115      {
1116        $event = $this->getEventWithUid($uid);
1117      }
1118
1119      // Load SabreDAV
1120      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1121      if(!isset($event['calendardata']))
1122        return false;
1123      $uri = $event['uri'];
1124      $calid = $event['calendarid'];
1125
1126      // Parse the existing event
1127      $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
1128      $vevent = $vcal->VEVENT;
1129
1130      // Set the new event values
1131      $vevent->summary = $params['eventname'];
1132      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
1133      $description = $params['eventdescription'];
1134
1135      // Remove existing timestamps to overwrite them
1136      $vevent->remove('DESCRIPTION');
1137      $vevent->remove('DTSTAMP');
1138      $vevent->remove('LAST-MODIFIED');
1139      $vevent->remove('ATTACH');
1140
1141      // Add new time stamps and description
1142      $vevent->add('DTSTAMP', $dtStamp);
1143      $vevent->add('LAST-MODIFIED', $dtStamp);
1144      if($description !== '')
1145        $vevent->add('DESCRIPTION', $description);
1146
1147      // Add attachments
1148      $attachments = $params['attachments'];
1149      if(!is_null($attachments))
1150        foreach($attachments as $attachment)
1151          $vevent->add('ATTACH', $attachment);
1152
1153      // Setup DTSTART
1154      $dtStart = new \DateTime();
1155      $dtStart->setTimezone($timezone);
1156      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
1157      if($params['allday'] != '1')
1158        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
1159
1160      // Setup DTEND
1161      $dtEnd = new \DateTime();
1162      $dtEnd->setTimezone($timezone);
1163      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
1164      if($params['allday'] != '1')
1165        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
1166
1167      // According to the VCal spec, we need to add a whole day here
1168      if($params['allday'] == '1')
1169          $dtEnd->add(new \DateInterval('P1D'));
1170      $vevent->remove('DTSTART');
1171      $vevent->remove('DTEND');
1172      $dtStartEv = $vevent->add('DTSTART', $dtStart);
1173      $dtEndEv = $vevent->add('DTEND', $dtEnd);
1174
1175      // Remove the time for allday events
1176      if($params['allday'] == '1')
1177      {
1178          $dtStartEv['VALUE'] = 'DATE';
1179          $dtEndEv['VALUE'] = 'DATE';
1180      }
1181      $eventStr = $vcal->serialize();
1182      if(strpos($id, 'webdav://') === 0)
1183      {
1184          $connectionId = str_replace('webdav://', '', $id);
1185          return $wdc->editCalendarEntry($connectionId, $uid, $eventStr);
1186      }
1187      else
1188      {
1189          $now = new DateTime();
1190          // Actually write to the database
1191          $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ".
1192                   "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?";
1193          $res = $this->sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(),
1194                                      $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid);
1195          if($res !== false)
1196          {
1197              $this->updateSyncTokenLog($calid, $uri, 'modified');
1198              return true;
1199          }
1200      }
1201      return false;
1202  }
1203
1204  /**
1205   * Delete an event from a calendar by calendar ID and URI
1206   *
1207   * @param int $calid The calendar's ID
1208   * @param string $uri The object's URI
1209   *
1210   * @return true
1211   */
1212  public function deleteCalendarEntryForCalendarByUri($calid, $uri)
1213  {
1214      $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?";
1215      $res = $this->sqlite->query($query, $calid, $uri);
1216      if($res !== false)
1217      {
1218          $this->updateSyncTokenLog($calid, $uri, 'deleted');
1219      }
1220      return true;
1221  }
1222
1223  /**
1224   * Delete a calendar entry for a given page. Actually, the event is removed
1225   * based on the entry's UID, so that page ID is no used.
1226   *
1227   * @param string $id The page's ID (unused)
1228   * @param array $params The parameter array to work with
1229   *
1230   * @return boolean True
1231   */
1232  public function deleteCalendarEntryForPage($id, $params)
1233  {
1234      $uid = $params['uid'];
1235      if(strpos($id, 'webdav://') === 0)
1236      {
1237        $wdc =& plugin_load('helper', 'webdavclient');
1238        if(is_null($wdc))
1239          return false;
1240        $connectionId = str_replace('webdav://', '', $id);
1241        $result = $wdc->deleteCalendarEntry($connectionId, $uid);
1242        return $result;
1243      }
1244      $event = $this->getEventWithUid($uid);
1245      $calid = $event['calendarid'];
1246      $uri = $event['uri'];
1247      $query = "DELETE FROM calendarobjects WHERE uid = ?";
1248      $res = $this->sqlite->query($query, $uid);
1249      if($res !== false)
1250      {
1251          $this->updateSyncTokenLog($calid, $uri, 'deleted');
1252      }
1253      return true;
1254  }
1255
1256  /**
1257   * Retrieve the current sync token for a calendar
1258   *
1259   * @param string $calid The calendar id
1260   *
1261   * @return mixed The synctoken or false
1262   */
1263  public function getSyncTokenForCalendar($calid)
1264  {
1265      $row = $this->getCalendarSettings($calid);
1266      if(isset($row['synctoken']))
1267          return $row['synctoken'];
1268      return false;
1269  }
1270
1271  /**
1272   * Helper function to convert the operation name to
1273   * an operation code as stored in the database
1274   *
1275   * @param string $operationName The operation name
1276   *
1277   * @return mixed The operation code or false
1278   */
1279  public function operationNameToOperation($operationName)
1280  {
1281      switch($operationName)
1282      {
1283          case 'added':
1284              return 1;
1285          break;
1286          case 'modified':
1287              return 2;
1288          break;
1289          case 'deleted':
1290              return 3;
1291          break;
1292      }
1293      return false;
1294  }
1295
1296  /**
1297   * Update the sync token log based on the calendar id and the
1298   * operation that was performed.
1299   *
1300   * @param string $calid The calendar ID that was modified
1301   * @param string $uri The calendar URI that was modified
1302   * @param string $operation The operation that was performed
1303   *
1304   * @return boolean True on success, otherwise false
1305   */
1306  private function updateSyncTokenLog($calid, $uri, $operation)
1307  {
1308      $currentToken = $this->getSyncTokenForCalendar($calid);
1309      $operationCode = $this->operationNameToOperation($operation);
1310      if(($operationCode === false) || ($currentToken === false))
1311          return false;
1312      $values = array($uri,
1313                      $currentToken,
1314                      $calid,
1315                      $operationCode
1316      );
1317      $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)";
1318      $res = $this->sqlite->query($query, $uri, $currentToken, $calid, $operationCode);
1319      if($res === false)
1320        return false;
1321      $currentToken++;
1322      $query = "UPDATE calendars SET synctoken = ? WHERE id = ?";
1323      $res = $this->sqlite->query($query, $currentToken, $calid);
1324      return ($res !== false);
1325  }
1326
1327  /**
1328   * Return the sync URL for a given Page, i.e. a calendar
1329   *
1330   * @param string $id The page's ID
1331   * @param string $user (optional) The user's ID
1332   *
1333   * @return mixed The sync url or false
1334   */
1335  public function getSyncUrlForPage($id, $user = null)
1336  {
1337      if(is_null($userid))
1338      {
1339        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
1340        {
1341          $userid = $_SERVER['REMOTE_USER'];
1342        }
1343        else
1344        {
1345          return false;
1346        }
1347      }
1348
1349      $calid = $this->getCalendarIdForPage($id);
1350      if($calid === false)
1351        return false;
1352
1353      $calsettings = $this->getCalendarSettings($calid);
1354      if(!isset($calsettings['uri']))
1355        return false;
1356
1357      $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri'];
1358      return $syncurl;
1359  }
1360
1361  /**
1362   * Return the private calendar's URL for a given page
1363   *
1364   * @param string $id the page ID
1365   *
1366   * @return mixed The private URL or false
1367   */
1368  public function getPrivateURLForPage($id)
1369  {
1370      $calid = $this->getCalendarIdForPage($id);
1371      if($calid === false)
1372        return false;
1373
1374      return $this->getPrivateURLForCalendar($calid);
1375  }
1376
1377  /**
1378   * Return the private calendar's URL for a given calendar ID
1379   *
1380   * @param string $calid The calendar's ID
1381   *
1382   * @return mixed The private URL or false
1383   */
1384  public function getPrivateURLForCalendar($calid)
1385  {
1386      if(isset($this->cachedValues['privateurl'][$calid]))
1387        return $this->cachedValues['privateurl'][$calid];
1388      $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?";
1389      $res = $this->sqlite->query($query, $calid);
1390      $row = $this->sqlite->res2row($res);
1391      if(!isset($row['url']))
1392      {
1393          $url = uniqid("dokuwiki-").".ics";
1394          $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)";
1395          $res = $this->sqlite->query($query, $url, $calid);
1396          if($res === false)
1397            return false;
1398      }
1399      else
1400      {
1401          $url = $row['url'];
1402      }
1403
1404      $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url;
1405      $this->cachedValues['privateurl'][$calid] = $url;
1406      return $url;
1407  }
1408
1409  /**
1410   * Retrieve the calendar ID for a given private calendar URL
1411   *
1412   * @param string $url The private URL
1413   *
1414   * @return mixed The calendar ID or false
1415   */
1416  public function getCalendarForPrivateURL($url)
1417  {
1418      $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?";
1419      $res = $this->sqlite->query($query, $url);
1420      $row = $this->sqlite->res2row($res);
1421      if(!isset($row['calid']))
1422        return false;
1423      return $row['calid'];
1424  }
1425
1426  /**
1427   * Return a given calendar as ICS feed, i.e. all events in one ICS file.
1428   *
1429   * @param string $calid The calendar ID to retrieve
1430   *
1431   * @return mixed The calendar events as string or false
1432   */
1433  public function getCalendarAsICSFeed($calid)
1434  {
1435      $calSettings = $this->getCalendarSettings($calid);
1436      if($calSettings === false)
1437        return false;
1438      $events = $this->getAllCalendarEvents($calid);
1439      if($events === false)
1440        return false;
1441
1442      // Load SabreDAV
1443      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1444      $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:";
1445      $out .= $calSettings['displayname']."\r\n";
1446      foreach($events as $event)
1447      {
1448          $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
1449          $evt = $vcal->VEVENT;
1450          $out .= $evt->serialize();
1451      }
1452      $out .= "END:VCALENDAR\r\n";
1453      return $out;
1454  }
1455
1456  /**
1457   * Retrieve a configuration option for the plugin
1458   *
1459   * @param string $key The key to query
1460   * @return mixed The option set, null if not found
1461   */
1462  public function getConfig($key)
1463  {
1464      return $this->getConf($key);
1465  }
1466
1467  /**
1468   * Parses some information from calendar objects, used for optimized
1469   * calendar-queries. Taken nearly unmodified from Sabre's PDO backend
1470   *
1471   * Returns an array with the following keys:
1472   *   * etag - An md5 checksum of the object without the quotes.
1473   *   * size - Size of the object in bytes
1474   *   * componentType - VEVENT, VTODO or VJOURNAL
1475   *   * firstOccurence
1476   *   * lastOccurence
1477   *   * uid - value of the UID property
1478   *
1479   * @param string $calendarData
1480   * @return array
1481   */
1482  protected function getDenormalizedData($calendarData)
1483  {
1484    require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1485
1486    $vObject = \Sabre\VObject\Reader::read($calendarData);
1487    $componentType = null;
1488    $component = null;
1489    $firstOccurence = null;
1490    $lastOccurence = null;
1491    $uid = null;
1492    foreach ($vObject->getComponents() as $component)
1493    {
1494        if ($component->name !== 'VTIMEZONE')
1495        {
1496            $componentType = $component->name;
1497            $uid = (string)$component->UID;
1498            break;
1499        }
1500    }
1501    if (!$componentType)
1502    {
1503        return false;
1504    }
1505    if ($componentType === 'VEVENT')
1506    {
1507        $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
1508        // Finding the last occurence is a bit harder
1509        if (!isset($component->RRULE))
1510        {
1511            if (isset($component->DTEND))
1512            {
1513                $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
1514            }
1515            elseif (isset($component->DURATION))
1516            {
1517                $endDate = clone $component->DTSTART->getDateTime();
1518                $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue()));
1519                $lastOccurence = $endDate->getTimeStamp();
1520            }
1521            elseif (!$component->DTSTART->hasTime())
1522            {
1523                $endDate = clone $component->DTSTART->getDateTime();
1524                $endDate->modify('+1 day');
1525                $lastOccurence = $endDate->getTimeStamp();
1526            }
1527            else
1528            {
1529                $lastOccurence = $firstOccurence;
1530            }
1531        }
1532        else
1533        {
1534            $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID);
1535            $maxDate = new \DateTime('2038-01-01');
1536            if ($it->isInfinite())
1537            {
1538                $lastOccurence = $maxDate->getTimeStamp();
1539            }
1540            else
1541            {
1542                $end = $it->getDtEnd();
1543                while ($it->valid() && $end < $maxDate)
1544                {
1545                    $end = $it->getDtEnd();
1546                    $it->next();
1547                }
1548                $lastOccurence = $end->getTimeStamp();
1549            }
1550        }
1551    }
1552
1553    return array(
1554        'etag'           => md5($calendarData),
1555        'size'           => strlen($calendarData),
1556        'componentType'  => $componentType,
1557        'firstOccurence' => $firstOccurence,
1558        'lastOccurence'  => $lastOccurence,
1559        'uid'            => $uid,
1560    );
1561
1562  }
1563
1564  /**
1565   * Query a calendar by ID and taking several filters into account.
1566   * This is heavily based on Sabre's PDO backend.
1567   *
1568   * @param int $calendarId The calendar's ID
1569   * @param array $filters The filter array to apply
1570   *
1571   * @return mixed The result
1572   */
1573  public function calendarQuery($calendarId, $filters)
1574  {
1575    dbglog('davcal::helper::calendarQuery');
1576    $componentType = null;
1577    $requirePostFilter = true;
1578    $timeRange = null;
1579
1580    // if no filters were specified, we don't need to filter after a query
1581    if (!$filters['prop-filters'] && !$filters['comp-filters'])
1582    {
1583        $requirePostFilter = false;
1584    }
1585
1586    // Figuring out if there's a component filter
1587    if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined'])
1588    {
1589        $componentType = $filters['comp-filters'][0]['name'];
1590
1591        // Checking if we need post-filters
1592        if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters'])
1593        {
1594            $requirePostFilter = false;
1595        }
1596        // There was a time-range filter
1597        if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range']))
1598        {
1599            $timeRange = $filters['comp-filters'][0]['time-range'];
1600
1601            // If start time OR the end time is not specified, we can do a
1602            // 100% accurate mysql query.
1603            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end']))
1604            {
1605                $requirePostFilter = false;
1606            }
1607        }
1608
1609    }
1610
1611    if ($requirePostFilter)
1612    {
1613        $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?";
1614    }
1615    else
1616    {
1617        $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?";
1618    }
1619
1620    $values = array(
1621        $calendarId
1622    );
1623
1624    if ($componentType)
1625    {
1626        $query .= " AND componenttype = ?";
1627        $values[] = $componentType;
1628    }
1629
1630    if ($timeRange && $timeRange['start'])
1631    {
1632        $query .= " AND lastoccurence > ?";
1633        $values[] = $timeRange['start']->getTimeStamp();
1634    }
1635    if ($timeRange && $timeRange['end'])
1636    {
1637        $query .= " AND firstoccurence < ?";
1638        $values[] = $timeRange['end']->getTimeStamp();
1639    }
1640
1641    dbglog($query);
1642    $res = $this->sqlite->query($query, $values);
1643    $arr = $this->sqlite->res2arr($res);
1644    dbglog($arr);
1645
1646    $result = array();
1647    foreach($arr as $row)
1648    {
1649        if ($requirePostFilter)
1650        {
1651            dbglog('requirePostFilter for');
1652            dbglog($row);
1653            dbglog($filters);
1654            if (!$this->validateFilterForObject($row, $filters))
1655            {
1656                continue;
1657            }
1658        }
1659        $result[] = $row['uri'];
1660
1661    }
1662
1663    return $result;
1664  }
1665
1666  /**
1667   * This method validates if a filter (as passed to calendarQuery) matches
1668   * the given object. Taken from Sabre's PDO backend
1669   *
1670   * @param array $object
1671   * @param array $filters
1672   * @return bool
1673   */
1674  protected function validateFilterForObject($object, $filters)
1675  {
1676      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1677      // Unfortunately, setting the 'calendardata' here is optional. If
1678      // it was excluded, we actually need another call to get this as
1679      // well.
1680      if (!isset($object['calendardata']))
1681      {
1682          dbglog('fetching object...');
1683          $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']);
1684      }
1685
1686      dbglog('object to validate: ');
1687      dbglog($object);
1688      $vObject = \Sabre\VObject\Reader::read($object['calendardata']);
1689      $validator = new \Sabre\CalDAV\CalendarQueryValidator();
1690
1691      $res = $validator->validate($vObject, $filters);
1692      dbglog($res);
1693      return $res;
1694
1695  }
1696
1697  /**
1698   * Retrieve changes for a given calendar based on the given syncToken.
1699   *
1700   * @param int $calid The calendar's ID
1701   * @param int $syncToken The supplied sync token
1702   * @param int $syncLevel The sync level
1703   * @param int $limit The limit of changes
1704   *
1705   * @return array The result
1706   */
1707  public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null)
1708  {
1709      // Current synctoken
1710      $currentToken = $this->getSyncTokenForCalendar($calid);
1711
1712      if ($currentToken === false) return null;
1713
1714      $result = array(
1715          'syncToken' => $currentToken,
1716          'added'     => array(),
1717          'modified'  => array(),
1718          'deleted'   => array(),
1719      );
1720
1721      if ($syncToken)
1722      {
1723
1724          $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken";
1725          if ($limit > 0) $query .= " LIMIT " . (int)$limit;
1726
1727          // Fetching all changes
1728          $res = $this->sqlite->query($query, $syncToken, $currentToken, $calid);
1729          if($res === false)
1730              return null;
1731
1732          $arr = $this->sqlite->res2arr($res);
1733          $changes = array();
1734
1735          // This loop ensures that any duplicates are overwritten, only the
1736          // last change on a node is relevant.
1737          foreach($arr as $row)
1738          {
1739              $changes[$row['uri']] = $row['operation'];
1740          }
1741
1742          foreach ($changes as $uri => $operation)
1743          {
1744              switch ($operation)
1745              {
1746                  case 1 :
1747                      $result['added'][] = $uri;
1748                      break;
1749                  case 2 :
1750                      $result['modified'][] = $uri;
1751                      break;
1752                  case 3 :
1753                      $result['deleted'][] = $uri;
1754                      break;
1755              }
1756
1757          }
1758      }
1759      else
1760      {
1761          // No synctoken supplied, this is the initial sync.
1762          $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?";
1763          $res = $this->sqlite->query($query);
1764          $arr = $this->sqlite->res2arr($res);
1765
1766          $result['added'] = $arr;
1767      }
1768      return $result;
1769  }
1770
1771}
1772