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