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