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