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