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