xref: /plugin/davcal/helper.php (revision 7e0b85904b164030b40f2dd78d6fb56f296d2b07)
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   * Get all calendar pages used by a given page
84   * based on the stored metadata
85   *
86   * @param string $id optional The page id
87   * @return mixed The pages as array or false
88   */
89  public function getCalendarPagesByMeta($id = null)
90  {
91      if(is_null($id))
92      {
93          global $ID;
94          $id = $ID;
95      }
96
97      $meta = $this->getCalendarMetaForPage($id);
98      if(isset($meta['id']))
99          return array_keys($meta['id']);
100      return false;
101  }
102
103  /**
104   * Get a list of calendar names/pages/ids/colors
105   * for an array of page ids
106   *
107   * @param array $calendarPages The calendar pages to retrieve
108   * @return array The list
109   */
110  public function getCalendarMapForIDs($calendarPages)
111  {
112      $data = array();
113      foreach($calendarPages as $page)
114      {
115          $calid = $this->getCalendarIdForPage($page);
116          if($calid !== false)
117          {
118            $settings = $this->getCalendarSettings($calid);
119            $name = $settings['displayname'];
120            $color = $settings['calendarcolor'];
121            $data[] = array('name' => $name, 'page' => $page, 'calid' => $calid,
122                            'color' => $color);
123          }
124      }
125      return $data;
126  }
127
128  /**
129   * Get the saved calendar color for a given page.
130   *
131   * @param string $id optional The page ID
132   * @return mixed The color on success, otherwise false
133   */
134  public function getCalendarColorForPage($id = null)
135  {
136      if(is_null($id))
137      {
138          global $ID;
139          $id = $ID;
140      }
141
142      $calid = $this->getCalendarIdForPage($id);
143      if($calid === false)
144        return false;
145
146      return $this->getCalendarColorForCalendar($calid);
147  }
148
149  /**
150   * Get the saved calendar color for a given calendar ID.
151   *
152   * @param string $id optional The calendar ID
153   * @return mixed The color on success, otherwise false
154   */
155  public function getCalendarColorForCalendar($calid)
156  {
157      if(isset($this->cachedValues['calendarcolor'][$calid]))
158        return $this->cachedValues['calendarcolor'][$calid];
159
160      $row = $this->getCalendarSettings($calid);
161
162      if(!isset($row['calendarcolor']))
163        return false;
164
165      $color = $row['calendarcolor'];
166      $this->cachedValues['calendarcolor'][$calid] = $color;
167      return $color;
168  }
169
170  /**
171   * Get the user's principal URL for iOS sync
172   * @param string $user the user name
173   * @return the URL to the principal sync
174   */
175  public function getPrincipalUrlForUser($user)
176  {
177      if(is_null($user))
178        return false;
179      $url = DOKU_URL.'lib/plugins/davcal/calendarserver.php/principals/'.$user;
180      return $url;
181  }
182
183  /**
184   * Set the calendar color for a given page.
185   *
186   * @param string $color The color definition
187   * @param string $id optional The page ID
188   * @return boolean True on success, otherwise false
189   */
190  public function setCalendarColorForPage($color, $id = null)
191  {
192      if(is_null($id))
193      {
194          global $ID;
195          $id = $ID;
196      }
197      $calid = $this->getCalendarIdForPage($id);
198      if($calid === false)
199        return false;
200
201      $query = "UPDATE calendars SET calendarcolor = ? ".
202               " WHERE id = ?";
203      $res = $this->sqlite->query($query, $color, $calid);
204      if($res !== false)
205      {
206        $this->cachedValues['calendarcolor'][$calid] = $color;
207        return true;
208      }
209      return false;
210  }
211
212  /**
213   * Set the calendar name and description for a given page with a given
214   * page id.
215   * If the calendar doesn't exist, the calendar is created!
216   *
217   * @param string  $name The name of the new calendar
218   * @param string  $description The description of the new calendar
219   * @param string  $id (optional) The ID of the page
220   * @param string  $userid The userid of the creating user
221   *
222   * @return boolean True on success, otherwise false.
223   */
224  public function setCalendarNameForPage($name, $description, $id = null, $userid = null)
225  {
226      if(is_null($id))
227      {
228          global $ID;
229          $id = $ID;
230      }
231      if(is_null($userid))
232      {
233        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
234        {
235          $userid = $_SERVER['REMOTE_USER'];
236        }
237        else
238        {
239          $userid = uniqid('davcal-');
240        }
241      }
242      $calid = $this->getCalendarIdForPage($id);
243      if($calid === false)
244        return $this->createCalendarForPage($name, $description, $id, $userid);
245
246      $query = "UPDATE calendars SET displayname = ?, description = ? WHERE id = ?";
247      $res = $this->sqlite->query($query, $name, $description, $calid);
248      if($res !== false)
249        return true;
250      return false;
251  }
252
253  /**
254   * Save the personal settings to the SQLite database 'calendarsettings'.
255   *
256   * @param array  $settings The settings array to store
257   * @param string $userid (optional) The userid to store
258   *
259   * @param boolean True on success, otherwise false
260   */
261  public function savePersonalSettings($settings, $userid = null)
262  {
263      if(is_null($userid))
264      {
265          if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
266          {
267            $userid = $_SERVER['REMOTE_USER'];
268          }
269          else
270          {
271              return false;
272          }
273      }
274      $this->sqlite->query("BEGIN TRANSACTION");
275
276      $query = "DELETE FROM calendarsettings WHERE userid = ?";
277      $this->sqlite->query($query, $userid);
278
279      foreach($settings as $key => $value)
280      {
281          $query = "INSERT INTO calendarsettings (userid, key, value) VALUES (?, ?, ?)";
282          $res = $this->sqlite->query($query, $userid, $key, $value);
283          if($res === false)
284              return false;
285      }
286      $this->sqlite->query("COMMIT TRANSACTION");
287      $this->cachedValues['settings'][$userid] = $settings;
288      return true;
289  }
290
291  /**
292   * Retrieve the settings array for a given user id.
293   * Some sane defaults are returned, currently:
294   *
295   *    timezone    => local
296   *    weeknumbers => 0
297   *    workweek    => 0
298   *
299   * @param string $userid (optional) The user id to retrieve
300   *
301   * @return array The settings array
302   */
303  public function getPersonalSettings($userid = null)
304  {
305      // Some sane default settings
306      $settings = array(
307        'timezone' => $this->getConf('timezone'),
308        'weeknumbers' => $this->getConf('weeknumbers'),
309        'workweek' => $this->getConf('workweek'),
310        'monday' => $this->getConf('monday'),
311        'timeformat' => $this->getConf('timeformat')
312      );
313      if(is_null($userid))
314      {
315          if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
316          {
317            $userid = $_SERVER['REMOTE_USER'];
318          }
319          else
320          {
321            return $settings;
322          }
323      }
324
325      if(isset($this->cachedValues['settings'][$userid]))
326        return $this->cachedValues['settings'][$userid];
327      $query = "SELECT key, value FROM calendarsettings WHERE userid = ?";
328      $res = $this->sqlite->query($query, $userid);
329      $arr = $this->sqlite->res2arr($res);
330      foreach($arr as $row)
331      {
332          $settings[$row['key']] = $row['value'];
333      }
334      $this->cachedValues['settings'][$userid] = $settings;
335      return $settings;
336  }
337
338  /**
339   * Retrieve the calendar ID based on a page ID from the SQLite table
340   * 'pagetocalendarmapping'.
341   *
342   * @param string $id (optional) The page ID to retrieve the corresponding calendar
343   *
344   * @return mixed the ID on success, otherwise false
345   */
346  public function getCalendarIdForPage($id = null)
347  {
348      if(is_null($id))
349      {
350          global $ID;
351          $id = $ID;
352      }
353
354      if(isset($this->cachedValues['calid'][$id]))
355        return $this->cachedValues['calid'][$id];
356
357      $query = "SELECT calid FROM pagetocalendarmapping WHERE page = ?";
358      $res = $this->sqlite->query($query, $id);
359      $row = $this->sqlite->res2row($res);
360      if(isset($row['calid']))
361      {
362        $calid = $row['calid'];
363        $this->cachedValues['calid'] = $calid;
364        return $calid;
365      }
366      return false;
367  }
368
369  /**
370   * Retrieve the complete calendar id to page mapping.
371   * This is necessary to be able to retrieve a list of
372   * calendars for a given user and check the access rights.
373   *
374   * @return array The mapping array
375   */
376  public function getCalendarIdToPageMapping()
377  {
378      $query = "SELECT calid, page FROM pagetocalendarmapping";
379      $res = $this->sqlite->query($query);
380      $arr = $this->sqlite->res2arr($res);
381      return $arr;
382  }
383
384  /**
385   * Retrieve all calendar IDs a given user has access to.
386   * The user is specified by the principalUri, so the
387   * user name is actually split from the URI component.
388   *
389   * Access rights are checked against DokuWiki's ACL
390   * and applied accordingly.
391   *
392   * @param string $principalUri The principal URI to work on
393   *
394   * @return array An associative array of calendar IDs
395   */
396  public function getCalendarIdsForUser($principalUri)
397  {
398      global $auth;
399      $user = explode('/', $principalUri);
400      $user = end($user);
401      $mapping = $this->getCalendarIdToPageMapping();
402      $calids = array();
403      $ud = $auth->getUserData($user);
404      $groups = $ud['grps'];
405      foreach($mapping as $row)
406      {
407          $id = $row['calid'];
408          $page = $row['page'];
409          $acl = auth_aclcheck($page, $user, $groups);
410          if($acl >= AUTH_READ)
411          {
412              $write = $acl > AUTH_READ;
413              $calids[$id] = array('readonly' => !$write);
414          }
415      }
416      return $calids;
417  }
418
419  /**
420   * Create a new calendar for a given page ID and set name and description
421   * accordingly. Also update the pagetocalendarmapping table on success.
422   *
423   * @param string $name The calendar's name
424   * @param string $description The calendar's description
425   * @param string $id (optional) The page ID to work on
426   * @param string $userid (optional) The user ID that created the calendar
427   *
428   * @return boolean True on success, otherwise false
429   */
430  public function createCalendarForPage($name, $description, $id = null, $userid = null)
431  {
432      if(is_null($id))
433      {
434          global $ID;
435          $id = $ID;
436      }
437      if(is_null($userid))
438      {
439        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
440        {
441          $userid = $_SERVER['REMOTE_USER'];
442        }
443        else
444        {
445          $userid = uniqid('davcal-');
446        }
447      }
448      $values = array('principals/'.$userid,
449                      $name,
450                      str_replace(array('/', ' ', ':'), '_', $id),
451                      $description,
452                      'VEVENT,VTODO',
453                      0,
454                      1);
455      $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) ".
456               "VALUES (?, ?, ?, ?, ?, ?, ?)";
457      $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6]);
458      if($res === false)
459        return false;
460
461      // Get the new calendar ID
462      $query = "SELECT id FROM calendars WHERE principaluri = ? AND displayname = ? AND ".
463               "uri = ? AND description = ?";
464      $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3]);
465      $row = $this->sqlite->res2row($res);
466
467      // Update the pagetocalendarmapping table with the new calendar ID
468      if(isset($row['id']))
469      {
470          $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (?, ?)";
471          $res = $this->sqlite->query($query, $id, $row['id']);
472          return ($res !== false);
473      }
474
475      return false;
476  }
477
478  /**
479   * Add a new iCal entry for a given page, i.e. a given calendar.
480   *
481   * The parameter array needs to contain
482   *   detectedtz       => The timezone as detected by the browser
483   *   currenttz        => The timezone in use by the calendar
484   *   eventfrom        => The event's start date
485   *   eventfromtime    => The event's start time
486   *   eventto          => The event's end date
487   *   eventtotime      => The event's end time
488   *   eventname        => The event's name
489   *   eventdescription => The event's description
490   *
491   * @param string $id The page ID to work on
492   * @param string $user The user who created the calendar
493   * @param string $params A parameter array with values to create
494   *
495   * @return boolean True on success, otherwise false
496   */
497  public function addCalendarEntryToCalendarForPage($id, $user, $params)
498  {
499      if($params['currenttz'] !== '' && $params['currenttz'] !== 'local')
500          $timezone = new \DateTimeZone($params['currenttz']);
501      elseif($params['currenttz'] === 'local')
502          $timezone = new \DateTimeZone($params['detectedtz']);
503      else
504          $timezone = new \DateTimeZone('UTC');
505
506      // Retrieve dates from settings
507      $startDate = explode('-', $params['eventfrom']);
508      $startTime = explode(':', $params['eventfromtime']);
509      $endDate = explode('-', $params['eventto']);
510      $endTime = explode(':', $params['eventtotime']);
511
512      // Load SabreDAV
513      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
514      $vcalendar = new \Sabre\VObject\Component\VCalendar();
515
516      // Add VCalendar, UID and Event Name
517      $event = $vcalendar->add('VEVENT');
518      $uuid = \Sabre\VObject\UUIDUtil::getUUID();
519      $event->add('UID', $uuid);
520      $event->summary = $params['eventname'];
521
522      // Add a description if requested
523      $description = $params['eventdescription'];
524      if($description !== '')
525        $event->add('DESCRIPTION', $description);
526
527      // Add attachments
528      $attachments = $params['attachments'];
529      if(!is_null($attachments))
530        foreach($attachments as $attachment)
531          $event->add('ATTACH', $attachment);
532
533      // Create a timestamp for last modified, created and dtstamp values in UTC
534      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
535      $event->add('DTSTAMP', $dtStamp);
536      $event->add('CREATED', $dtStamp);
537      $event->add('LAST-MODIFIED', $dtStamp);
538
539      // Adjust the start date, based on the given timezone information
540      $dtStart = new \DateTime();
541      $dtStart->setTimezone($timezone);
542      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
543
544      // Only add the time values if it's not an allday event
545      if($params['allday'] != '1')
546        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
547
548      // Adjust the end date, based on the given timezone information
549      $dtEnd = new \DateTime();
550      $dtEnd->setTimezone($timezone);
551      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
552
553      // Only add the time values if it's not an allday event
554      if($params['allday'] != '1')
555        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
556
557      // According to the VCal spec, we need to add a whole day here
558      if($params['allday'] == '1')
559          $dtEnd->add(new \DateInterval('P1D'));
560
561      // Really add Start and End events
562      $dtStartEv = $event->add('DTSTART', $dtStart);
563      $dtEndEv = $event->add('DTEND', $dtEnd);
564
565      // Adjust the DATE format for allday events
566      if($params['allday'] == '1')
567      {
568          $dtStartEv['VALUE'] = 'DATE';
569          $dtEndEv['VALUE'] = 'DATE';
570      }
571
572      // Actually add the values to the database
573      $calid = $this->getCalendarIdForPage($id);
574      $uri = uniqid('dokuwiki-').'.ics';
575      $now = new DateTime();
576      $eventStr = $vcalendar->serialize();
577
578      $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
579      $res = $this->sqlite->query($query, $calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT',
580                                  $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(),
581                                  strlen($eventStr), md5($eventStr), $uuid);
582
583      // If successfully, update the sync token database
584      if($res !== false)
585      {
586          $this->updateSyncTokenLog($calid, $uri, 'added');
587          return true;
588      }
589      return false;
590  }
591
592  /**
593   * Retrieve the calendar settings of a given calendar id
594   *
595   * @param string $calid The calendar ID
596   *
597   * @return array The calendar settings array
598   */
599  public function getCalendarSettings($calid)
600  {
601      $query = "SELECT principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id= ? ";
602      $res = $this->sqlite->query($query, $calid);
603      $row = $this->sqlite->res2row($res);
604      return $row;
605  }
606
607  /**
608   * Retrieve all events that are within a given date range,
609   * based on the timezone setting.
610   *
611   * There is also support for retrieving recurring events,
612   * using Sabre's VObject Iterator. Recurring events are represented
613   * as individual calendar entries with the same UID.
614   *
615   * @param string $id The page ID to work with
616   * @param string $user The user ID to work with
617   * @param string $startDate The start date as a string
618   * @param string $endDate The end date as a string
619   *
620   * @return array An array containing the calendar entries.
621   */
622  public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone)
623  {
624      if($timezone !== '' && $timezone !== 'local')
625          $timezone = new \DateTimeZone($timezone);
626      else
627          $timezone = new \DateTimeZone('UTC');
628      $data = array();
629
630      // Load SabreDAV
631      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
632      $calid = $this->getCalendarIdForPage($id);
633      $color = $this->getCalendarColorForCalendar($calid);
634      $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid = ?";
635      $startTs = null;
636      $endTs = null;
637      if($startDate !== null)
638      {
639        $startTs = new \DateTime($startDate);
640        $query .= " AND lastoccurence > ".$this->sqlite->quote_string($startTs->getTimestamp());
641      }
642      if($endDate !== null)
643      {
644        $endTs = new \DateTime($endDate);
645        $query .= " AND firstoccurence < ".$this->sqlite->quote_string($endTs->getTimestamp());
646      }
647
648      // Retrieve matching calendar objects
649      $res = $this->sqlite->query($query, $calid);
650      $arr = $this->sqlite->res2arr($res);
651
652      // Parse individual calendar entries
653      foreach($arr as $row)
654      {
655          if(isset($row['calendardata']))
656          {
657              $entry = array();
658              $vcal = \Sabre\VObject\Reader::read($row['calendardata']);
659              $recurrence = $vcal->VEVENT->RRULE;
660              // If it is a recurring event, pass it through Sabre's EventIterator
661              if($recurrence != null)
662              {
663                  $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT));
664                  $rEvents->rewind();
665                  while($rEvents->valid())
666                  {
667                      $event = $rEvents->getEventObject();
668                      // If we are after the given time range, exit
669                      if(($endTs !== null) && ($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp()))
670                          break;
671
672                      // If we are before the given time range, continue
673                      if(($startTs != null) && ($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp()))
674                      {
675                          $rEvents->next();
676                          continue;
677                      }
678
679                      // If we are within the given time range, parse the event
680                      $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true);
681                      $rEvents->next();
682                  }
683              }
684              else
685                $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color);
686          }
687      }
688      return $data;
689  }
690
691  /**
692   * Helper function that parses the iCal data of a VEVENT to a calendar entry.
693   *
694   * @param \Sabre\VObject\VEvent $event The event to parse
695   * @param \DateTimeZone $timezone The timezone object
696   * @param string $uid The entry's UID
697   * @param boolean $recurring (optional) Set to true to define a recurring event
698   *
699   * @return array The parse calendar entry
700   */
701  private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false)
702  {
703      $entry = array();
704      $start = $event->DTSTART;
705      // Parse only if the start date/time is present
706      if($start !== null)
707      {
708        $dtStart = $start->getDateTime();
709        $dtStart->setTimezone($timezone);
710
711        // moment.js doesn't like times be given even if
712        // allDay is set to true
713        // This should fix T23
714        if($start['VALUE'] == 'DATE')
715        {
716          $entry['allDay'] = true;
717          $entry['start'] = $dtStart->format("Y-m-d");
718        }
719        else
720        {
721          $entry['allDay'] = false;
722          $entry['start'] = $dtStart->format(\DateTime::ATOM);
723        }
724      }
725      $end = $event->DTEND;
726      // Parse only if the end date/time is present
727      if($end !== null)
728      {
729        $dtEnd = $end->getDateTime();
730        $dtEnd->setTimezone($timezone);
731        if($end['VALUE'] == 'DATE')
732          $entry['end'] = $dtEnd->format("Y-m-d");
733        else
734          $entry['end'] = $dtEnd->format(\DateTime::ATOM);
735      }
736      $description = $event->DESCRIPTION;
737      if($description !== null)
738        $entry['description'] = (string)$description;
739      else
740        $entry['description'] = '';
741      $attachments = $event->ATTACH;
742      if($attachments !== null)
743      {
744        $entry['attachments'] = array();
745        foreach($attachments as $attachment)
746          $entry['attachments'][] = (string)$attachment;
747      }
748      $entry['title'] = (string)$event->summary;
749      $entry['id'] = $uid;
750      $entry['page'] = $page;
751      $entry['color'] = $color;
752      $entry['recurring'] = $recurring;
753
754      return $entry;
755  }
756
757  /**
758   * Retrieve an event by its UID
759   *
760   * @param string $uid The event's UID
761   *
762   * @return mixed The table row with the given event
763   */
764  public function getEventWithUid($uid)
765  {
766      $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?";
767      $res = $this->sqlite->query($query, $uid);
768      $row = $this->sqlite->res2row($res);
769      return $row;
770  }
771
772  /**
773   * Retrieve all calendar events for a given calendar ID
774   *
775   * @param string $calid The calendar's ID
776   *
777   * @return array An array containing all calendar data
778   */
779  public function getAllCalendarEvents($calid)
780  {
781      $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?";
782      $res = $this->sqlite->query($query, $calid);
783      $arr = $this->sqlite->res2arr($res);
784      return $arr;
785  }
786
787  /**
788   * Edit a calendar entry for a page, given by its parameters.
789   * The params array has the same format as @see addCalendarEntryForPage
790   *
791   * @param string $id The page's ID to work on
792   * @param string $user The user's ID to work on
793   * @param array $params The parameter array for the edited calendar event
794   *
795   * @return boolean True on success, otherwise false
796   */
797  public function editCalendarEntryForPage($id, $user, $params)
798  {
799      if($params['currenttz'] !== '' && $params['currenttz'] !== 'local')
800          $timezone = new \DateTimeZone($params['currenttz']);
801      elseif($params['currenttz'] === 'local')
802          $timezone = new \DateTimeZone($params['detectedtz']);
803      else
804          $timezone = new \DateTimeZone('UTC');
805
806      // Parse dates
807      $startDate = explode('-', $params['eventfrom']);
808      $startTime = explode(':', $params['eventfromtime']);
809      $endDate = explode('-', $params['eventto']);
810      $endTime = explode(':', $params['eventtotime']);
811
812      // Retrieve the existing event based on the UID
813      $uid = $params['uid'];
814      $event = $this->getEventWithUid($uid);
815
816      // Load SabreDAV
817      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
818      if(!isset($event['calendardata']))
819        return false;
820      $uri = $event['uri'];
821      $calid = $event['calendarid'];
822
823      // Parse the existing event
824      $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
825      $vevent = $vcal->VEVENT;
826
827      // Set the new event values
828      $vevent->summary = $params['eventname'];
829      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
830      $description = $params['eventdescription'];
831
832      // Remove existing timestamps to overwrite them
833      $vevent->remove('DESCRIPTION');
834      $vevent->remove('DTSTAMP');
835      $vevent->remove('LAST-MODIFIED');
836      $vevent->remove('ATTACH');
837
838      // Add new time stamps and description
839      $vevent->add('DTSTAMP', $dtStamp);
840      $vevent->add('LAST-MODIFIED', $dtStamp);
841      if($description !== '')
842        $vevent->add('DESCRIPTION', $description);
843
844      // Add attachments
845      $attachments = $params['attachments'];
846      if(!is_null($attachments))
847        foreach($attachments as $attachment)
848          $vevent->add('ATTACH', $attachment);
849
850      // Setup DTSTART
851      $dtStart = new \DateTime();
852      $dtStart->setTimezone($timezone);
853      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
854      if($params['allday'] != '1')
855        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
856
857      // Setup DTEND
858      $dtEnd = new \DateTime();
859      $dtEnd->setTimezone($timezone);
860      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
861      if($params['allday'] != '1')
862        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
863
864      // According to the VCal spec, we need to add a whole day here
865      if($params['allday'] == '1')
866          $dtEnd->add(new \DateInterval('P1D'));
867      $vevent->remove('DTSTART');
868      $vevent->remove('DTEND');
869      $dtStartEv = $vevent->add('DTSTART', $dtStart);
870      $dtEndEv = $vevent->add('DTEND', $dtEnd);
871
872      // Remove the time for allday events
873      if($params['allday'] == '1')
874      {
875          $dtStartEv['VALUE'] = 'DATE';
876          $dtEndEv['VALUE'] = 'DATE';
877      }
878      $now = new DateTime();
879      $eventStr = $vcal->serialize();
880      // Actually write to the database
881      $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ".
882               "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?";
883      $res = $this->sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(),
884                                  $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid);
885      if($res !== false)
886      {
887          $this->updateSyncTokenLog($calid, $uri, 'modified');
888          return true;
889      }
890      return false;
891  }
892
893  /**
894   * Delete a calendar entry for a given page. Actually, the event is removed
895   * based on the entry's UID, so that page ID is no used.
896   *
897   * @param string $id The page's ID (unused)
898   * @param array $params The parameter array to work with
899   *
900   * @return boolean True
901   */
902  public function deleteCalendarEntryForPage($id, $params)
903  {
904      $uid = $params['uid'];
905      $event = $this->getEventWithUid($uid);
906      $calid = $event['calendarid'];
907      $uri = $event['uri'];
908      $query = "DELETE FROM calendarobjects WHERE uid = ?";
909      $res = $this->sqlite->query($query, $uid);
910      if($res !== false)
911      {
912          $this->updateSyncTokenLog($calid, $uri, 'deleted');
913      }
914      return true;
915  }
916
917  /**
918   * Retrieve the current sync token for a calendar
919   *
920   * @param string $calid The calendar id
921   *
922   * @return mixed The synctoken or false
923   */
924  public function getSyncTokenForCalendar($calid)
925  {
926      $row = $this->getCalendarSettings($calid);
927      if(isset($row['synctoken']))
928          return $row['synctoken'];
929      return false;
930  }
931
932  /**
933   * Helper function to convert the operation name to
934   * an operation code as stored in the database
935   *
936   * @param string $operationName The operation name
937   *
938   * @return mixed The operation code or false
939   */
940  public function operationNameToOperation($operationName)
941  {
942      switch($operationName)
943      {
944          case 'added':
945              return 1;
946          break;
947          case 'modified':
948              return 2;
949          break;
950          case 'deleted':
951              return 3;
952          break;
953      }
954      return false;
955  }
956
957  /**
958   * Update the sync token log based on the calendar id and the
959   * operation that was performed.
960   *
961   * @param string $calid The calendar ID that was modified
962   * @param string $uri The calendar URI that was modified
963   * @param string $operation The operation that was performed
964   *
965   * @return boolean True on success, otherwise false
966   */
967  private function updateSyncTokenLog($calid, $uri, $operation)
968  {
969      $currentToken = $this->getSyncTokenForCalendar($calid);
970      $operationCode = $this->operationNameToOperation($operation);
971      if(($operationCode === false) || ($currentToken === false))
972          return false;
973      $values = array($uri,
974                      $currentToken,
975                      $calid,
976                      $operationCode
977      );
978      $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)";
979      $res = $this->sqlite->query($query, $uri, $currentToken, $calid, $operationCode);
980      if($res === false)
981        return false;
982      $currentToken++;
983      $query = "UPDATE calendars SET synctoken = ? WHERE id = ?";
984      $res = $this->sqlite->query($query, $currentToken, $calid);
985      return ($res !== false);
986  }
987
988  /**
989   * Return the sync URL for a given Page, i.e. a calendar
990   *
991   * @param string $id The page's ID
992   * @param string $user (optional) The user's ID
993   *
994   * @return mixed The sync url or false
995   */
996  public function getSyncUrlForPage($id, $user = null)
997  {
998      if(is_null($userid))
999      {
1000        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
1001        {
1002          $userid = $_SERVER['REMOTE_USER'];
1003        }
1004        else
1005        {
1006          return false;
1007        }
1008      }
1009
1010      $calid = $this->getCalendarIdForPage($id);
1011      if($calid === false)
1012        return false;
1013
1014      $calsettings = $this->getCalendarSettings($calid);
1015      if(!isset($calsettings['uri']))
1016        return false;
1017
1018      $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri'];
1019      return $syncurl;
1020  }
1021
1022  /**
1023   * Return the private calendar's URL for a given page
1024   *
1025   * @param string $id the page ID
1026   *
1027   * @return mixed The private URL or false
1028   */
1029  public function getPrivateURLForPage($id)
1030  {
1031      $calid = $this->getCalendarIdForPage($id);
1032      if($calid === false)
1033        return false;
1034
1035      return $this->getPrivateURLForCalendar($calid);
1036  }
1037
1038  /**
1039   * Return the private calendar's URL for a given calendar ID
1040   *
1041   * @param string $calid The calendar's ID
1042   *
1043   * @return mixed The private URL or false
1044   */
1045  public function getPrivateURLForCalendar($calid)
1046  {
1047      if(isset($this->cachedValues['privateurl'][$calid]))
1048        return $this->cachedValues['privateurl'][$calid];
1049      $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?";
1050      $res = $this->sqlite->query($query, $calid);
1051      $row = $this->sqlite->res2row($res);
1052      if(!isset($row['url']))
1053      {
1054          $url = uniqid("dokuwiki-").".ics";
1055          $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)";
1056          $res = $this->sqlite->query($query, $url, $calid);
1057          if($res === false)
1058            return false;
1059      }
1060      else
1061      {
1062          $url = $row['url'];
1063      }
1064
1065      $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url;
1066      $this->cachedValues['privateurl'][$calid] = $url;
1067      return $url;
1068  }
1069
1070  /**
1071   * Retrieve the calendar ID for a given private calendar URL
1072   *
1073   * @param string $url The private URL
1074   *
1075   * @return mixed The calendar ID or false
1076   */
1077  public function getCalendarForPrivateURL($url)
1078  {
1079      $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?";
1080      $res = $this->sqlite->query($query, $url);
1081      $row = $this->sqlite->res2row($res);
1082      if(!isset($row['calid']))
1083        return false;
1084      return $row['calid'];
1085  }
1086
1087  /**
1088   * Return a given calendar as ICS feed, i.e. all events in one ICS file.
1089   *
1090   * @param string $calid The calendar ID to retrieve
1091   *
1092   * @return mixed The calendar events as string or false
1093   */
1094  public function getCalendarAsICSFeed($calid)
1095  {
1096      $calSettings = $this->getCalendarSettings($calid);
1097      if($calSettings === false)
1098        return false;
1099      $events = $this->getAllCalendarEvents($calid);
1100      if($events === false)
1101        return false;
1102
1103      // Load SabreDAV
1104      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
1105      $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:";
1106      $out .= $calSettings['displayname']."\r\n";
1107      foreach($events as $event)
1108      {
1109          $vcal = \Sabre\VObject\Reader::read($event['calendardata']);
1110          $evt = $vcal->VEVENT;
1111          $out .= $evt->serialize();
1112      }
1113      $out .= "END:VCALENDAR\r\n";
1114      return $out;
1115  }
1116
1117  /**
1118   * Retrieve a configuration option for the plugin
1119   *
1120   * @param string $key The key to query
1121   * @return mixed The option set, null if not found
1122   */
1123  public function getConfig($key)
1124  {
1125      return $this->getConf($key);
1126  }
1127
1128}
1129