xref: /plugin/davcal/helper.php (revision 4a2bf5ee69b3cf7fe1cf3acf315a3b9591975fa2)
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          if(empty($pages))
123            return false;
124          return $pages;
125      }
126      return false;
127  }
128
129  /**
130   * Get a list of calendar names/pages/ids/colors
131   * for an array of page ids
132   *
133   * @param array $calendarPages The calendar pages to retrieve
134   * @return array The list
135   */
136  public function getCalendarMapForIDs($calendarPages)
137  {
138      $data = array();
139      foreach($calendarPages as $page => $color)
140      {
141          $calid = $this->getCalendarIdForPage($page);
142          if($calid !== false)
143          {
144            $settings = $this->getCalendarSettings($calid);
145            $name = $settings['displayname'];
146            $write = (auth_quickaclcheck($page) > AUTH_READ);
147            $data[] = array('name' => $name, 'page' => $page, 'calid' => $calid,
148                            'color' => $color, 'write' => $write);
149          }
150      }
151      return $data;
152  }
153
154  /**
155   * Get the saved calendar color for a given page.
156   *
157   * @param string $id optional The page ID
158   * @return mixed The color on success, otherwise false
159   */
160  public function getCalendarColorForPage($id = null)
161  {
162      if(is_null($id))
163      {
164          global $ID;
165          $id = $ID;
166      }
167
168      $calid = $this->getCalendarIdForPage($id);
169      if($calid === false)
170        return false;
171
172      return $this->getCalendarColorForCalendar($calid);
173  }
174
175  /**
176   * Get the saved calendar color for a given calendar ID.
177   *
178   * @param string $id optional The calendar ID
179   * @return mixed The color on success, otherwise false
180   */
181  public function getCalendarColorForCalendar($calid)
182  {
183      if(isset($this->cachedValues['calendarcolor'][$calid]))
184        return $this->cachedValues['calendarcolor'][$calid];
185
186      $row = $this->getCalendarSettings($calid);
187
188      if(!isset($row['calendarcolor']))
189        return false;
190
191      $color = $row['calendarcolor'];
192      $this->cachedValues['calendarcolor'][$calid] = $color;
193      return $color;
194  }
195
196  /**
197   * Get the user's principal URL for iOS sync
198   * @param string $user the user name
199   * @return the URL to the principal sync
200   */
201  public function getPrincipalUrlForUser($user)
202  {
203      if(is_null($user))
204        return false;
205      $url = DOKU_URL.'lib/plugins/davcal/calendarserver.php/principals/'.$user;
206      return $url;
207  }
208
209  /**
210   * Set the calendar color for a given page.
211   *
212   * @param string $color The color definition
213   * @param string $id optional The page ID
214   * @return boolean True on success, otherwise false
215   */
216  public function setCalendarColorForPage($color, $id = null)
217  {
218      if(is_null($id))
219      {
220          global $ID;
221          $id = $ID;
222      }
223      $calid = $this->getCalendarIdForPage($id);
224      if($calid === false)
225        return false;
226
227      $query = "UPDATE calendars SET calendarcolor = ? ".
228               " WHERE id = ?";
229      $res = $this->sqlite->query($query, $color, $calid);
230      if($res !== false)
231      {
232        $this->cachedValues['calendarcolor'][$calid] = $color;
233        return true;
234      }
235      return false;
236  }
237
238  /**
239   * Set the calendar name and description for a given page with a given
240   * page id.
241   * If the calendar doesn't exist, the calendar is created!
242   *
243   * @param string  $name The name of the new calendar
244   * @param string  $description The description of the new calendar
245   * @param string  $id (optional) The ID of the page
246   * @param string  $userid The userid of the creating user
247   *
248   * @return boolean True on success, otherwise false.
249   */
250  public function setCalendarNameForPage($name, $description, $id = null, $userid = null)
251  {
252      if(is_null($id))
253      {
254          global $ID;
255          $id = $ID;
256      }
257      if(is_null($userid))
258      {
259        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
260        {
261          $userid = $_SERVER['REMOTE_USER'];
262        }
263        else
264        {
265          $userid = uniqid('davcal-');
266        }
267      }
268      $calid = $this->getCalendarIdForPage($id);
269      if($calid === false)
270        return $this->createCalendarForPage($name, $description, $id, $userid);
271
272      $query = "UPDATE calendars SET displayname = ?, description = ? WHERE id = ?";
273      $res = $this->sqlite->query($query, $name, $description, $calid);
274      if($res !== false)
275        return true;
276      return false;
277  }
278
279  /**
280   * Save the personal settings to the SQLite database 'calendarsettings'.
281   *
282   * @param array  $settings The settings array to store
283   * @param string $userid (optional) The userid to store
284   *
285   * @param boolean True on success, otherwise false
286   */
287  public function savePersonalSettings($settings, $userid = null)
288  {
289      if(is_null($userid))
290      {
291          if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
292          {
293            $userid = $_SERVER['REMOTE_USER'];
294          }
295          else
296          {
297              return false;
298          }
299      }
300      $this->sqlite->query("BEGIN TRANSACTION");
301
302      $query = "DELETE FROM calendarsettings WHERE userid = ?";
303      $this->sqlite->query($query, $userid);
304
305      foreach($settings as $key => $value)
306      {
307          $query = "INSERT INTO calendarsettings (userid, key, value) VALUES (?, ?, ?)";
308          $res = $this->sqlite->query($query, $userid, $key, $value);
309          if($res === false)
310              return false;
311      }
312      $this->sqlite->query("COMMIT TRANSACTION");
313      $this->cachedValues['settings'][$userid] = $settings;
314      return true;
315  }
316
317  /**
318   * Retrieve the settings array for a given user id.
319   * Some sane defaults are returned, currently:
320   *
321   *    timezone    => local
322   *    weeknumbers => 0
323   *    workweek    => 0
324   *
325   * @param string $userid (optional) The user id to retrieve
326   *
327   * @return array The settings array
328   */
329  public function getPersonalSettings($userid = null)
330  {
331      // Some sane default settings
332      $settings = array(
333        'timezone' => $this->getConf('timezone'),
334        'weeknumbers' => $this->getConf('weeknumbers'),
335        'workweek' => $this->getConf('workweek'),
336        'monday' => $this->getConf('monday'),
337        'timeformat' => $this->getConf('timeformat')
338      );
339      if(is_null($userid))
340      {
341          if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
342          {
343            $userid = $_SERVER['REMOTE_USER'];
344          }
345          else
346          {
347            return $settings;
348          }
349      }
350
351      if(isset($this->cachedValues['settings'][$userid]))
352        return $this->cachedValues['settings'][$userid];
353      $query = "SELECT key, value FROM calendarsettings WHERE userid = ?";
354      $res = $this->sqlite->query($query, $userid);
355      $arr = $this->sqlite->res2arr($res);
356      foreach($arr as $row)
357      {
358          $settings[$row['key']] = $row['value'];
359      }
360      $this->cachedValues['settings'][$userid] = $settings;
361      return $settings;
362  }
363
364  /**
365   * Retrieve the calendar ID based on a page ID from the SQLite table
366   * 'pagetocalendarmapping'.
367   *
368   * @param string $id (optional) The page ID to retrieve the corresponding calendar
369   *
370   * @return mixed the ID on success, otherwise false
371   */
372  public function getCalendarIdForPage($id = null)
373  {
374      if(is_null($id))
375      {
376          global $ID;
377          $id = $ID;
378      }
379
380      if(isset($this->cachedValues['calid'][$id]))
381        return $this->cachedValues['calid'][$id];
382
383      $query = "SELECT calid FROM pagetocalendarmapping WHERE page = ?";
384      $res = $this->sqlite->query($query, $id);
385      $row = $this->sqlite->res2row($res);
386      if(isset($row['calid']))
387      {
388        $calid = $row['calid'];
389        $this->cachedValues['calid'] = $calid;
390        return $calid;
391      }
392      return false;
393  }
394
395  /**
396   * Retrieve the complete calendar id to page mapping.
397   * This is necessary to be able to retrieve a list of
398   * calendars for a given user and check the access rights.
399   *
400   * @return array The mapping array
401   */
402  public function getCalendarIdToPageMapping()
403  {
404      $query = "SELECT calid, page FROM pagetocalendarmapping";
405      $res = $this->sqlite->query($query);
406      $arr = $this->sqlite->res2arr($res);
407      return $arr;
408  }
409
410  /**
411   * Retrieve all calendar IDs a given user has access to.
412   * The user is specified by the principalUri, so the
413   * user name is actually split from the URI component.
414   *
415   * Access rights are checked against DokuWiki's ACL
416   * and applied accordingly.
417   *
418   * @param string $principalUri The principal URI to work on
419   *
420   * @return array An associative array of calendar IDs
421   */
422  public function getCalendarIdsForUser($principalUri)
423  {
424      global $auth;
425      $user = explode('/', $principalUri);
426      $user = end($user);
427      $mapping = $this->getCalendarIdToPageMapping();
428      $calids = array();
429      $ud = $auth->getUserData($user);
430      $groups = $ud['grps'];
431      foreach($mapping as $row)
432      {
433          $id = $row['calid'];
434          $page = $row['page'];
435          $acl = auth_aclcheck($page, $user, $groups);
436          if($acl >= AUTH_READ)
437          {
438              $write = $acl > AUTH_READ;
439              $calids[$id] = array('readonly' => !$write);
440          }
441      }
442      return $calids;
443  }
444
445  /**
446   * Create a new calendar for a given page ID and set name and description
447   * accordingly. Also update the pagetocalendarmapping table on success.
448   *
449   * @param string $name The calendar's name
450   * @param string $description The calendar's description
451   * @param string $id (optional) The page ID to work on
452   * @param string $userid (optional) The user ID that created the calendar
453   *
454   * @return boolean True on success, otherwise false
455   */
456  public function createCalendarForPage($name, $description, $id = null, $userid = null)
457  {
458      if(is_null($id))
459      {
460          global $ID;
461          $id = $ID;
462      }
463      if(is_null($userid))
464      {
465        if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER']))
466        {
467          $userid = $_SERVER['REMOTE_USER'];
468        }
469        else
470        {
471          $userid = uniqid('davcal-');
472        }
473      }
474      $values = array('principals/'.$userid,
475                      $name,
476                      str_replace(array('/', ' ', ':'), '_', $id),
477                      $description,
478                      'VEVENT,VTODO',
479                      0,
480                      1);
481      $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) ".
482               "VALUES (?, ?, ?, ?, ?, ?, ?)";
483      $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6]);
484      if($res === false)
485        return false;
486
487      // Get the new calendar ID
488      $query = "SELECT id FROM calendars WHERE principaluri = ? AND displayname = ? AND ".
489               "uri = ? AND description = ?";
490      $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3]);
491      $row = $this->sqlite->res2row($res);
492
493      // Update the pagetocalendarmapping table with the new calendar ID
494      if(isset($row['id']))
495      {
496          $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (?, ?)";
497          $res = $this->sqlite->query($query, $id, $row['id']);
498          return ($res !== false);
499      }
500
501      return false;
502  }
503
504  /**
505   * Add a new iCal entry for a given page, i.e. a given calendar.
506   *
507   * The parameter array needs to contain
508   *   detectedtz       => The timezone as detected by the browser
509   *   currenttz        => The timezone in use by the calendar
510   *   eventfrom        => The event's start date
511   *   eventfromtime    => The event's start time
512   *   eventto          => The event's end date
513   *   eventtotime      => The event's end time
514   *   eventname        => The event's name
515   *   eventdescription => The event's description
516   *
517   * @param string $id The page ID to work on
518   * @param string $user The user who created the calendar
519   * @param string $params A parameter array with values to create
520   *
521   * @return boolean True on success, otherwise false
522   */
523  public function addCalendarEntryToCalendarForPage($id, $user, $params)
524  {
525      if($params['currenttz'] !== '' && $params['currenttz'] !== 'local')
526          $timezone = new \DateTimeZone($params['currenttz']);
527      elseif($params['currenttz'] === 'local')
528          $timezone = new \DateTimeZone($params['detectedtz']);
529      else
530          $timezone = new \DateTimeZone('UTC');
531
532      // Retrieve dates from settings
533      $startDate = explode('-', $params['eventfrom']);
534      $startTime = explode(':', $params['eventfromtime']);
535      $endDate = explode('-', $params['eventto']);
536      $endTime = explode(':', $params['eventtotime']);
537
538      // Load SabreDAV
539      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
540      $vcalendar = new \Sabre\VObject\Component\VCalendar();
541
542      // Add VCalendar, UID and Event Name
543      $event = $vcalendar->add('VEVENT');
544      $uuid = \Sabre\VObject\UUIDUtil::getUUID();
545      $event->add('UID', $uuid);
546      $event->summary = $params['eventname'];
547
548      // Add a description if requested
549      $description = $params['eventdescription'];
550      if($description !== '')
551        $event->add('DESCRIPTION', $description);
552
553      // Add attachments
554      $attachments = $params['attachments'];
555      if(!is_null($attachments))
556        foreach($attachments as $attachment)
557          $event->add('ATTACH', $attachment);
558
559      // Create a timestamp for last modified, created and dtstamp values in UTC
560      $dtStamp = new \DateTime(null, new \DateTimeZone('UTC'));
561      $event->add('DTSTAMP', $dtStamp);
562      $event->add('CREATED', $dtStamp);
563      $event->add('LAST-MODIFIED', $dtStamp);
564
565      // Adjust the start date, based on the given timezone information
566      $dtStart = new \DateTime();
567      $dtStart->setTimezone($timezone);
568      $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2]));
569
570      // Only add the time values if it's not an allday event
571      if($params['allday'] != '1')
572        $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0);
573
574      // Adjust the end date, based on the given timezone information
575      $dtEnd = new \DateTime();
576      $dtEnd->setTimezone($timezone);
577      $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2]));
578
579      // Only add the time values if it's not an allday event
580      if($params['allday'] != '1')
581        $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0);
582
583      // According to the VCal spec, we need to add a whole day here
584      if($params['allday'] == '1')
585          $dtEnd->add(new \DateInterval('P1D'));
586
587      // Really add Start and End events
588      $dtStartEv = $event->add('DTSTART', $dtStart);
589      $dtEndEv = $event->add('DTEND', $dtEnd);
590
591      // Adjust the DATE format for allday events
592      if($params['allday'] == '1')
593      {
594          $dtStartEv['VALUE'] = 'DATE';
595          $dtEndEv['VALUE'] = 'DATE';
596      }
597
598      // Actually add the values to the database
599      $calid = $this->getCalendarIdForPage($id);
600      $uri = uniqid('dokuwiki-').'.ics';
601      $now = new DateTime();
602      $eventStr = $vcalendar->serialize();
603
604      $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
605      $res = $this->sqlite->query($query, $calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT',
606                                  $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(),
607                                  strlen($eventStr), md5($eventStr), $uuid);
608
609      // If successfully, update the sync token database
610      if($res !== false)
611      {
612          $this->updateSyncTokenLog($calid, $uri, 'added');
613          return true;
614      }
615      return false;
616  }
617
618  /**
619   * Retrieve the calendar settings of a given calendar id
620   *
621   * @param string $calid The calendar ID
622   *
623   * @return array The calendar settings array
624   */
625  public function getCalendarSettings($calid)
626  {
627      $query = "SELECT principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id= ? ";
628      $res = $this->sqlite->query($query, $calid);
629      $row = $this->sqlite->res2row($res);
630      return $row;
631  }
632
633  /**
634   * Retrieve all events that are within a given date range,
635   * based on the timezone setting.
636   *
637   * There is also support for retrieving recurring events,
638   * using Sabre's VObject Iterator. Recurring events are represented
639   * as individual calendar entries with the same UID.
640   *
641   * @param string $id The page ID to work with
642   * @param string $user The user ID to work with
643   * @param string $startDate The start date as a string
644   * @param string $endDate The end date as a string
645   * @param string $color (optional) The calendar's color
646   *
647   * @return array An array containing the calendar entries.
648   */
649  public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone, $color = null)
650  {
651      if($timezone !== '' && $timezone !== 'local')
652          $timezone = new \DateTimeZone($timezone);
653      else
654          $timezone = new \DateTimeZone('UTC');
655      $data = array();
656
657      // Load SabreDAV
658      require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php');
659      $calid = $this->getCalendarIdForPage($id);
660      if(is_null($color))
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