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