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