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