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