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 dbglog('---- DAVCAL helper.php init'); 21 } 22 23 /** Establish and initialize the database if not already done 24 * @return sqlite interface or false 25 */ 26 private function getDB() 27 { 28 if($this->sqlite === null) 29 { 30 $this->sqlite = plugin_load('helper', 'sqlite'); 31 if(!$this->sqlite) 32 { 33 dbglog('This plugin requires the sqlite plugin. Please install it.'); 34 msg('This plugin requires the sqlite plugin. Please install it.', -1); 35 return false; 36 } 37 if(!$this->sqlite->init('davcal', DOKU_PLUGIN.'davcal/db/')) 38 { 39 $this->sqlite = null; 40 dbglog('Error initialising the SQLite DB for DAVCal'); 41 return false; 42 } 43 } 44 return $this->sqlite; 45 } 46 47 /** 48 * Retrieve meta data for a given page 49 * 50 * @param string $id optional The page ID 51 * @return array The metadata 52 */ 53 private function getMeta($id = null) { 54 global $ID; 55 global $INFO; 56 57 if ($id === null) $id = $ID; 58 59 if($ID === $id && $INFO['meta']) { 60 $meta = $INFO['meta']; 61 } else { 62 $meta = p_get_metadata($id); 63 } 64 65 return $meta; 66 } 67 68 /** 69 * Retrieve the meta data for a given page 70 * 71 * @param string $id optional The page ID 72 * @return array with meta data 73 */ 74 public function getCalendarMetaForPage($id = null) 75 { 76 if(is_null($id)) 77 { 78 global $ID; 79 $id = $ID; 80 } 81 82 $meta = $this->getMeta($id); 83 if(isset($meta['plugin_davcal'])) 84 return $meta['plugin_davcal']; 85 else 86 return array(); 87 } 88 89 /** 90 * Check the permission of a user for a given calendar ID 91 * 92 * @param string $id The calendar ID to check 93 * @return int AUTH_* constants 94 */ 95 public function checkCalendarPermission($id) 96 { 97 if(strpos($id, 'webdav://') === 0) 98 { 99 $wdc =& plugin_load('helper', 'webdavclient'); 100 if(is_null($wdc)) 101 return AUTH_NONE; 102 $connectionId = str_replace('webdav://', '', $id); 103 $settings = $wdc->getConnection($connectionId); 104 if($settings === false) 105 return AUTH_NONE; 106 if($settings['write'] === '1') 107 return AUTH_CREATE; 108 return AUTH_READ; 109 } 110 else 111 { 112 $calid = $this->getCalendarIdForPage($id); 113 // We return AUTH_READ if the calendar does not exist. This makes 114 // davcal happy when there are just included calendars 115 if($calid === false) 116 return AUTH_READ; 117 return auth_quickaclcheck($id); 118 } 119 } 120 121 /** 122 * Filter calendar pages and return only those where the current 123 * user has at least read permission. 124 * 125 * @param array $calendarPages Array with calendar pages to check 126 * @return array with filtered calendar pages 127 */ 128 public function filterCalendarPagesByUserPermission($calendarPages) 129 { 130 $retList = array(); 131 foreach($calendarPages as $page => $data) 132 { 133 // WebDAV Connections are always readable 134 if(strpos($page, 'webdav://') === 0) 135 { 136 $retList[$page] = $data; 137 } 138 elseif(auth_quickaclcheck($page) >= AUTH_READ) 139 { 140 $retList[$page] = $data; 141 } 142 } 143 return $retList; 144 } 145 146 /** 147 * Get all calendar pages used by a given page 148 * based on the stored metadata 149 * 150 * @param string $id optional The page id 151 * @return mixed The pages as array or false 152 */ 153 public function getCalendarPagesByMeta($id = null) 154 { 155 if(is_null($id)) 156 { 157 global $ID; 158 $id = $ID; 159 } 160 161 $meta = $this->getCalendarMetaForPage($id); 162 163 if(isset($meta['id'])) 164 { 165 // Filter the list of pages by permission 166 $pages = $this->filterCalendarPagesByUserPermission($meta['id']); 167 if(empty($pages)) 168 return false; 169 return $pages; 170 } 171 return false; 172 } 173 174 /** 175 * Get a list of calendar names/pages/ids/colors 176 * for an array of page ids 177 * 178 * @param array $calendarPages The calendar pages to retrieve 179 * @return array The list 180 */ 181 public function getCalendarMapForIDs($calendarPages) 182 { 183 $data = array(); 184 foreach($calendarPages as $page => $color) 185 { 186 if(strpos($page, 'webdav://') === 0) 187 { 188 $wdc =& plugin_load('helper', 'webdavclient'); 189 if(is_null($wdc)) 190 continue; 191 $connectionId = str_replace('webdav://', '', $page); 192 $settings = $wdc->getConnection($connectionId); 193 if($settings === false) 194 continue; 195 $name = $settings['displayname']; 196 $write = ($settings['write'] === '1'); 197 $calid = $connectionId; 198 $color = '#3a87ad'; 199 } 200 else 201 { 202 $calid = $this->getCalendarIdForPage($page); 203 if($calid !== false) 204 { 205 $settings = $this->getCalendarSettings($calid); 206 $name = $settings['displayname']; 207 $color = $settings['calendarcolor']; 208 $write = (auth_quickaclcheck($page) > AUTH_READ); 209 } 210 else 211 { 212 continue; 213 } 214 } 215 $data[] = array('name' => $name, 'page' => $page, 'calid' => $calid, 216 'color' => $color, 'write' => $write); 217 } 218 return $data; 219 } 220 221 /** 222 * Get the saved calendar color for a given page. 223 * 224 * @param string $id optional The page ID 225 * @return mixed The color on success, otherwise false 226 */ 227 public function getCalendarColorForPage($id = null) 228 { 229 if(is_null($id)) 230 { 231 global $ID; 232 $id = $ID; 233 } 234 235 $calid = $this->getCalendarIdForPage($id); 236 if($calid === false) 237 return false; 238 239 return $this->getCalendarColorForCalendar($calid); 240 } 241 242 /** 243 * Get the saved calendar color for a given calendar ID. 244 * 245 * @param string $id optional The calendar ID 246 * @return mixed The color on success, otherwise false 247 */ 248 public function getCalendarColorForCalendar($calid) 249 { 250 if(isset($this->cachedValues['calendarcolor'][$calid])) 251 return $this->cachedValues['calendarcolor'][$calid]; 252 253 $row = $this->getCalendarSettings($calid); 254 255 if(!isset($row['calendarcolor'])) 256 return false; 257 258 $color = $row['calendarcolor']; 259 $this->cachedValues['calendarcolor'][$calid] = $color; 260 return $color; 261 } 262 263 /** 264 * Get the user's principal URL for iOS sync 265 * @param string $user the user name 266 * @return the URL to the principal sync 267 */ 268 public function getPrincipalUrlForUser($user) 269 { 270 if(is_null($user)) 271 return false; 272 $url = DOKU_URL.'lib/plugins/davcal/calendarserver.php/principals/'.$user; 273 return $url; 274 } 275 276 /** 277 * Set the calendar color for a given page. 278 * 279 * @param string $color The color definition 280 * @param string $id optional The page ID 281 * @return boolean True on success, otherwise false 282 */ 283 public function setCalendarColorForPage($color, $id = null) 284 { 285 if(is_null($id)) 286 { 287 global $ID; 288 $id = $ID; 289 } 290 $calid = $this->getCalendarIdForPage($id); 291 if($calid === false) 292 return false; 293 294 $sqlite = $this->getDB(); 295 if(!$sqlite) 296 return false; 297 $query = "UPDATE calendars SET calendarcolor = ? ". 298 " WHERE id = ?"; 299 $res = $sqlite->query($query, $color, $calid); 300 if($res !== false) 301 { 302 $this->cachedValues['calendarcolor'][$calid] = $color; 303 return true; 304 } 305 return false; 306 } 307 308 /** 309 * Set the calendar name and description for a given page with a given 310 * page id. 311 * If the calendar doesn't exist, the calendar is created! 312 * 313 * @param string $name The name of the new calendar 314 * @param string $description The description of the new calendar 315 * @param string $id (optional) The ID of the page 316 * @param string $userid The userid of the creating user 317 * 318 * @return boolean True on success, otherwise false. 319 */ 320 public function setCalendarNameForPage($name, $description, $id = null, $userid = null) 321 { 322 if(is_null($id)) 323 { 324 global $ID; 325 $id = $ID; 326 } 327 if(is_null($userid)) 328 { 329 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 330 { 331 $userid = $_SERVER['REMOTE_USER']; 332 } 333 else 334 { 335 $userid = uniqid('davcal-'); 336 } 337 } 338 $calid = $this->getCalendarIdForPage($id); 339 if($calid === false) 340 return $this->createCalendarForPage($name, $description, $id, $userid); 341 342 $sqlite = $this->getDB(); 343 if(!$sqlite) 344 return false; 345 $query = "UPDATE calendars SET displayname = ?, description = ? WHERE id = ?"; 346 $res = $sqlite->query($query, $name, $description, $calid); 347 if($res !== false) 348 return true; 349 return false; 350 } 351 352 /** 353 * Update a calendar's displayname 354 * 355 * @param int $calid The calendar's ID 356 * @param string $name The new calendar name 357 * 358 * @return boolean True on success, otherwise false 359 */ 360 public function updateCalendarName($calid, $name) 361 { 362 $sqlite = $this->getDB(); 363 if(!$sqlite) 364 return false; 365 $query = "UPDATE calendars SET displayname = ? WHERE id = ?"; 366 $res = $sqlite->query($query, $calid, $name); 367 if($res !== false) 368 { 369 $this->updateSyncTokenLog($calid, '', 'modified'); 370 return true; 371 } 372 return false; 373 } 374 375 /** 376 * Update the calendar description 377 * 378 * @param int $calid The calendar's ID 379 * @param string $description The new calendar's description 380 * 381 * @return boolean True on success, otherwise false 382 */ 383 public function updateCalendarDescription($calid, $description) 384 { 385 $sqlite = $this->getDB(); 386 if(!$sqlite) 387 return false; 388 $query = "UPDATE calendars SET description = ? WHERE id = ?"; 389 $res = $sqlite->query($query, $calid, $description); 390 if($res !== false) 391 { 392 $this->updateSyncTokenLog($calid, '', 'modified'); 393 return true; 394 } 395 return false; 396 } 397 398 /** 399 * Update a calendar's timezone information 400 * 401 * @param int $calid The calendar's ID 402 * @param string $timezone The new timezone to set 403 * 404 * @return boolean True on success, otherwise false 405 */ 406 public function updateCalendarTimezone($calid, $timezone) 407 { 408 $sqlite = $this->getDB(); 409 if(!$sqlite) 410 return false; 411 $query = "UPDATE calendars SET timezone = ? WHERE id = ?"; 412 $res = $sqlite->query($query, $calid, $timezone); 413 if($res !== false) 414 { 415 $this->updateSyncTokenLog($calid, '', 'modified'); 416 return true; 417 } 418 return false; 419 } 420 421 /** 422 * Save the personal settings to the SQLite database 'calendarsettings'. 423 * 424 * @param array $settings The settings array to store 425 * @param string $userid (optional) The userid to store 426 * 427 * @param boolean True on success, otherwise false 428 */ 429 public function savePersonalSettings($settings, $userid = null) 430 { 431 if(is_null($userid)) 432 { 433 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 434 { 435 $userid = $_SERVER['REMOTE_USER']; 436 } 437 else 438 { 439 return false; 440 } 441 } 442 $sqlite = $this->getDB(); 443 if(!$sqlite) 444 return false; 445 $sqlite->query("BEGIN TRANSACTION"); 446 447 $query = "DELETE FROM calendarsettings WHERE userid = ?"; 448 $sqlite->query($query, $userid); 449 450 foreach($settings as $key => $value) 451 { 452 $query = "INSERT INTO calendarsettings (userid, key, value) VALUES (?, ?, ?)"; 453 $res = $sqlite->query($query, $userid, $key, $value); 454 if($res === false) 455 return false; 456 } 457 $sqlite->query("COMMIT TRANSACTION"); 458 $this->cachedValues['settings'][$userid] = $settings; 459 return true; 460 } 461 462 /** 463 * Retrieve the settings array for a given user id. 464 * Some sane defaults are returned, currently: 465 * 466 * timezone => local 467 * weeknumbers => 0 468 * workweek => 0 469 * 470 * @param string $userid (optional) The user id to retrieve 471 * 472 * @return array The settings array 473 */ 474 public function getPersonalSettings($userid = null) 475 { 476 // Some sane default settings 477 $settings = array( 478 'timezone' => $this->getConf('timezone'), 479 'weeknumbers' => $this->getConf('weeknumbers'), 480 'workweek' => $this->getConf('workweek'), 481 'monday' => $this->getConf('monday'), 482 'timeformat' => $this->getConf('timeformat') 483 ); 484 if(is_null($userid)) 485 { 486 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 487 { 488 $userid = $_SERVER['REMOTE_USER']; 489 } 490 else 491 { 492 return $settings; 493 } 494 } 495 496 $sqlite = $this->getDB(); 497 if(!$sqlite) 498 return false; 499 if(isset($this->cachedValues['settings'][$userid])) 500 return $this->cachedValues['settings'][$userid]; 501 $query = "SELECT key, value FROM calendarsettings WHERE userid = ?"; 502 $res = $sqlite->query($query, $userid); 503 $arr = $sqlite->res2arr($res); 504 foreach($arr as $row) 505 { 506 $settings[$row['key']] = $row['value']; 507 } 508 $this->cachedValues['settings'][$userid] = $settings; 509 return $settings; 510 } 511 512 /** 513 * Retrieve the calendar ID based on a page ID from the SQLite table 514 * 'pagetocalendarmapping'. 515 * 516 * @param string $id (optional) The page ID to retrieve the corresponding calendar 517 * 518 * @return mixed the ID on success, otherwise false 519 */ 520 public function getCalendarIdForPage($id = null) 521 { 522 if(is_null($id)) 523 { 524 global $ID; 525 $id = $ID; 526 } 527 528 if(isset($this->cachedValues['calid'][$id])) 529 return $this->cachedValues['calid'][$id]; 530 531 $sqlite = $this->getDB(); 532 if(!$sqlite) 533 return false; 534 $query = "SELECT calid FROM pagetocalendarmapping WHERE page = ?"; 535 $res = $sqlite->query($query, $id); 536 $row = $sqlite->res2row($res); 537 if(isset($row['calid'])) 538 { 539 $calid = $row['calid']; 540 $this->cachedValues['calid'] = $calid; 541 return $calid; 542 } 543 return false; 544 } 545 546 /** 547 * Retrieve the complete calendar id to page mapping. 548 * This is necessary to be able to retrieve a list of 549 * calendars for a given user and check the access rights. 550 * 551 * @return array The mapping array 552 */ 553 public function getCalendarIdToPageMapping() 554 { 555 $sqlite = $this->getDB(); 556 if(!$sqlite) 557 return false; 558 $query = "SELECT calid, page FROM pagetocalendarmapping"; 559 $res = $sqlite->query($query); 560 $arr = $sqlite->res2arr($res); 561 return $arr; 562 } 563 564 /** 565 * Retrieve all calendar IDs a given user has access to. 566 * The user is specified by the principalUri, so the 567 * user name is actually split from the URI component. 568 * 569 * Access rights are checked against DokuWiki's ACL 570 * and applied accordingly. 571 * 572 * @param string $principalUri The principal URI to work on 573 * 574 * @return array An associative array of calendar IDs 575 */ 576 public function getCalendarIdsForUser($principalUri) 577 { 578 global $auth; 579 $user = explode('/', $principalUri); 580 $user = end($user); 581 $mapping = $this->getCalendarIdToPageMapping(); 582 $calids = array(); 583 $ud = $auth->getUserData($user); 584 $groups = $ud['grps']; 585 foreach($mapping as $row) 586 { 587 $id = $row['calid']; 588 $enabled = $this->getCalendarStatus($id); 589 if($enabled == false) 590 continue; 591 $page = $row['page']; 592 $acl = auth_aclcheck($page, $user, $groups); 593 if($acl >= AUTH_READ) 594 { 595 $write = $acl > AUTH_READ; 596 $calids[$id] = array('readonly' => !$write); 597 } 598 } 599 return $calids; 600 } 601 602 /** 603 * Create a new calendar for a given page ID and set name and description 604 * accordingly. Also update the pagetocalendarmapping table on success. 605 * 606 * @param string $name The calendar's name 607 * @param string $description The calendar's description 608 * @param string $id (optional) The page ID to work on 609 * @param string $userid (optional) The user ID that created the calendar 610 * 611 * @return boolean True on success, otherwise false 612 */ 613 public function createCalendarForPage($name, $description, $id = null, $userid = null) 614 { 615 if(is_null($id)) 616 { 617 global $ID; 618 $id = $ID; 619 } 620 if(is_null($userid)) 621 { 622 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 623 { 624 $userid = $_SERVER['REMOTE_USER']; 625 } 626 else 627 { 628 $userid = uniqid('davcal-'); 629 } 630 } 631 $values = array('principals/'.$userid, 632 $name, 633 str_replace(array('/', ' ', ':'), '_', $id), 634 $description, 635 'VEVENT,VTODO', 636 0, 637 1); 638 639 $sqlite = $this->getDB(); 640 if(!$sqlite) 641 return false; 642 $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) ". 643 "VALUES (?, ?, ?, ?, ?, ?, ?)"; 644 $res = $sqlite->query($query, $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6]); 645 if($res === false) 646 return false; 647 648 // Get the new calendar ID 649 $query = "SELECT id FROM calendars WHERE principaluri = ? AND displayname = ? AND ". 650 "uri = ? AND description = ?"; 651 $res = $sqlite->query($query, $values[0], $values[1], $values[2], $values[3]); 652 $row = $sqlite->res2row($res); 653 654 // Update the pagetocalendarmapping table with the new calendar ID 655 if(isset($row['id'])) 656 { 657 $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (?, ?)"; 658 $res = $sqlite->query($query, $id, $row['id']); 659 return ($res !== false); 660 } 661 662 return false; 663 } 664 665 /** 666 * Add a new calendar entry to the given calendar. Calendar data is 667 * specified as ICS file, thus it needs to be parsed first. 668 * 669 * This is mainly needed for the sync support. 670 * 671 * @param int $calid The calendar's ID 672 * @param string $uri The new object URI 673 * @param string $ics The ICS file 674 * 675 * @return mixed The etag. 676 */ 677 public function addCalendarEntryToCalendarByICS($calid, $uri, $ics) 678 { 679 $extraData = $this->getDenormalizedData($ics); 680 681 $sqlite = $this->getDB(); 682 if(!$sqlite) 683 return false; 684 $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)"; 685 $res = $sqlite->query($query, 686 $calid, 687 $uri, 688 $ics, 689 time(), 690 $extraData['etag'], 691 $extraData['size'], 692 $extraData['componentType'], 693 $extraData['firstOccurence'], 694 $extraData['lastOccurence'], 695 $extraData['uid']); 696 // If successfully, update the sync token database 697 if($res !== false) 698 { 699 $this->updateSyncTokenLog($calid, $uri, 'added'); 700 } 701 return $extraData['etag']; 702 } 703 704 /** 705 * Edit a calendar entry by providing a new ICS file. This is mainly 706 * needed for the sync support. 707 * 708 * @param int $calid The calendar's IS 709 * @param string $uri The object's URI to modify 710 * @param string $ics The new object's ICS file 711 */ 712 public function editCalendarEntryToCalendarByICS($calid, $uri, $ics) 713 { 714 $extraData = $this->getDenormalizedData($ics); 715 716 $sqlite = $this->getDB(); 717 if(!$sqlite) 718 return false; 719 $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?"; 720 $res = $sqlite->query($query, 721 $ics, 722 time(), 723 $extraData['etag'], 724 $extraData['size'], 725 $extraData['componentType'], 726 $extraData['firstOccurence'], 727 $extraData['lastOccurence'], 728 $extraData['uid'], 729 $calid, 730 $uri 731 ); 732 if($res !== false) 733 { 734 $this->updateSyncTokenLog($calid, $uri, 'modified'); 735 } 736 return $extraData['etag']; 737 } 738 739 /** 740 * Add a new iCal entry for a given page, i.e. a given calendar. 741 * 742 * The parameter array needs to contain 743 * detectedtz => The timezone as detected by the browser 744 * currenttz => The timezone in use by the calendar 745 * eventfrom => The event's start date 746 * eventfromtime => The event's start time 747 * eventto => The event's end date 748 * eventtotime => The event's end time 749 * eventname => The event's name 750 * eventdescription => The event's description 751 * 752 * @param string $id The page ID to work on 753 * @param string $user The user who created the calendar 754 * @param string $params A parameter array with values to create 755 * 756 * @return boolean True on success, otherwise false 757 */ 758 public function addCalendarEntryToCalendarForPage($id, $user, $params) 759 { 760 if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') 761 $timezone = new \DateTimeZone($params['currenttz']); 762 elseif($params['currenttz'] === 'local') 763 $timezone = new \DateTimeZone($params['detectedtz']); 764 else 765 $timezone = new \DateTimeZone('UTC'); 766 767 // Retrieve dates from settings 768 $startDate = explode('-', $params['eventfrom']); 769 $startTime = explode(':', $params['eventfromtime']); 770 $endDate = explode('-', $params['eventto']); 771 $endTime = explode(':', $params['eventtotime']); 772 773 // Load SabreDAV 774 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 775 $vcalendar = new \Sabre\VObject\Component\VCalendar(); 776 777 // Add VCalendar, UID and Event Name 778 $event = $vcalendar->add('VEVENT'); 779 $uuid = \Sabre\VObject\UUIDUtil::getUUID(); 780 $event->add('UID', $uuid); 781 $event->summary = $params['eventname']; 782 783 // Add a description if requested 784 $description = $params['eventdescription']; 785 if($description !== '') 786 $event->add('DESCRIPTION', $description); 787 788 // Add a location if requested 789 $location = $params['eventlocation']; 790 if($location !== '') 791 $event->add('LOCATION', $location); 792 793 // Add attachments 794 $attachments = $params['attachments']; 795 if(!is_null($attachments)) 796 foreach($attachments as $attachment) 797 $event->add('ATTACH', $attachment); 798 799 // Create a timestamp for last modified, created and dtstamp values in UTC 800 $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); 801 $event->add('DTSTAMP', $dtStamp); 802 $event->add('CREATED', $dtStamp); 803 $event->add('LAST-MODIFIED', $dtStamp); 804 805 // Adjust the start date, based on the given timezone information 806 $dtStart = new \DateTime(); 807 $dtStart->setTimezone($timezone); 808 $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); 809 810 // Only add the time values if it's not an allday event 811 if($params['allday'] != '1') 812 $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); 813 814 // Adjust the end date, based on the given timezone information 815 $dtEnd = new \DateTime(); 816 $dtEnd->setTimezone($timezone); 817 $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); 818 819 // Only add the time values if it's not an allday event 820 if($params['allday'] != '1') 821 $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); 822 823 // According to the VCal spec, we need to add a whole day here 824 if($params['allday'] == '1') 825 $dtEnd->add(new \DateInterval('P1D')); 826 827 // Really add Start and End events 828 $dtStartEv = $event->add('DTSTART', $dtStart); 829 $dtEndEv = $event->add('DTEND', $dtEnd); 830 831 // Adjust the DATE format for allday events 832 if($params['allday'] == '1') 833 { 834 $dtStartEv['VALUE'] = 'DATE'; 835 $dtEndEv['VALUE'] = 'DATE'; 836 } 837 838 $eventStr = $vcalendar->serialize(); 839 840 if(strpos($id, 'webdav://') === 0) 841 { 842 $wdc =& plugin_load('helper', 'webdavclient'); 843 if(is_null($wdc)) 844 return false; 845 $connectionId = str_replace('webdav://', '', $id); 846 return $wdc->addCalendarEntry($connectionId, $eventStr); 847 } 848 else 849 { 850 // Actually add the values to the database 851 $calid = $this->getCalendarIdForPage($id); 852 $uri = uniqid('dokuwiki-').'.ics'; 853 $now = new \DateTime(); 854 855 $sqlite = $this->getDB(); 856 if(!$sqlite) 857 return false; 858 $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; 859 $res = $sqlite->query($query, $calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT', 860 $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(), 861 strlen($eventStr), md5($eventStr), $uuid); 862 863 // If successfully, update the sync token database 864 if($res !== false) 865 { 866 $this->updateSyncTokenLog($calid, $uri, 'added'); 867 return true; 868 } 869 } 870 return false; 871 } 872 873 /** 874 * Retrieve the calendar settings of a given calendar id 875 * 876 * @param string $calid The calendar ID 877 * 878 * @return array The calendar settings array 879 */ 880 public function getCalendarSettings($calid) 881 { 882 $sqlite = $this->getDB(); 883 if(!$sqlite) 884 return false; 885 $query = "SELECT id, principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken, disabled FROM calendars WHERE id= ? "; 886 $res = $sqlite->query($query, $calid); 887 $row = $sqlite->res2row($res); 888 return $row; 889 } 890 891 /** 892 * Retrieve the calendar status of a given calendar id 893 * 894 * @param string $calid The calendar ID 895 * @return boolean True if calendar is enabled, otherwise false 896 */ 897 public function getCalendarStatus($calid) 898 { 899 $sqlite = $this->getDB(); 900 if(!$sqlite) 901 return false; 902 $query = "SELECT disabled FROM calendars WHERE id = ?"; 903 $res = $sqlite->query($query, $calid); 904 $row = $sqlite->res2row($res); 905 if($row['disabled'] == 1) 906 return false; 907 else 908 return true; 909 } 910 911 /** 912 * Disable a calendar for a given page 913 * 914 * @param string $id The page ID 915 * 916 * @return boolean true on success, otherwise false 917 */ 918 public function disableCalendarForPage($id) 919 { 920 $calid = $this->getCalendarIdForPage($id); 921 if($calid === false) 922 return false; 923 924 $sqlite = $this->getDB(); 925 if(!$sqlite) 926 return false; 927 $query = "UPDATE calendars SET disabled = 1 WHERE id = ?"; 928 $res = $sqlite->query($query, $calid); 929 if($res !== false) 930 return true; 931 return false; 932 } 933 934 /** 935 * Enable a calendar for a given page 936 * 937 * @param string $id The page ID 938 * 939 * @return boolean true on success, otherwise false 940 */ 941 public function enableCalendarForPage($id) 942 { 943 $calid = $this->getCalendarIdForPage($id); 944 if($calid === false) 945 return false; 946 $sqlite = $this->getDB(); 947 if(!$sqlite) 948 return false; 949 $query = "UPDATE calendars SET disabled = 0 WHERE id = ?"; 950 $res = $sqlite->query($query, $calid); 951 if($res !== false) 952 return true; 953 return false; 954 } 955 956 /** 957 * Retrieve all events that are within a given date range, 958 * based on the timezone setting. 959 * 960 * There is also support for retrieving recurring events, 961 * using Sabre's VObject Iterator. Recurring events are represented 962 * as individual calendar entries with the same UID. 963 * 964 * @param string $id The page ID to work with 965 * @param string $user The user ID to work with 966 * @param string $startDate The start date as a string 967 * @param string $endDate The end date as a string 968 * @param string $color (optional) The calendar's color 969 * 970 * @return array An array containing the calendar entries. 971 */ 972 public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone, $color = null) 973 { 974 if($timezone !== '' && $timezone !== 'local') 975 $timezone = new \DateTimeZone($timezone); 976 else 977 $timezone = new \DateTimeZone('UTC'); 978 $data = array(); 979 980 $sqlite = $this->getDB(); 981 if(!$sqlite) 982 return false; 983 984 $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid = ?"; 985 $startTs = null; 986 $endTs = null; 987 if($startDate !== null) 988 { 989 $startTs = new \DateTime($startDate); 990 $query .= " AND lastoccurence > ".$sqlite->quote_string($startTs->getTimestamp()); 991 } 992 if($endDate !== null) 993 { 994 $endTs = new \DateTime($endDate); 995 $query .= " AND firstoccurence < ".$sqlite->quote_string($endTs->getTimestamp()); 996 } 997 998 // Load SabreDAV 999 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1000 1001 if(strpos($id, 'webdav://') === 0) 1002 { 1003 $wdc =& plugin_load('helper', 'webdavclient'); 1004 if(is_null($wdc)) 1005 return $data; 1006 $connectionId = str_replace('webdav://', '', $id); 1007 $arr = $wdc->getCalendarEntries($connectionId, $startDate, $endDate); 1008 } 1009 else 1010 { 1011 $calid = $this->getCalendarIdForPage($id); 1012 if(is_null($color)) 1013 $color = $this->getCalendarColorForCalendar($calid); 1014 1015 $enabled = $this->getCalendarStatus($calid); 1016 if($enabled === false) 1017 return $data; 1018 1019 // Retrieve matching calendar objects 1020 $res = $sqlite->query($query, $calid); 1021 $arr = $sqlite->res2arr($res); 1022 } 1023 1024 // Parse individual calendar entries 1025 foreach($arr as $row) 1026 { 1027 if(isset($row['calendardata'])) 1028 { 1029 $entry = array(); 1030 $vcal = \Sabre\VObject\Reader::read($row['calendardata']); 1031 $recurrence = $vcal->VEVENT->RRULE; 1032 // If it is a recurring event, pass it through Sabre's EventIterator 1033 if($recurrence != null) 1034 { 1035 $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT)); 1036 $rEvents->rewind(); 1037 while($rEvents->valid()) 1038 { 1039 $event = $rEvents->getEventObject(); 1040 // If we are after the given time range, exit 1041 if(($endTs !== null) && ($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp())) 1042 break; 1043 1044 // If we are before the given time range, continue 1045 if(($startTs != null) && ($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp())) 1046 { 1047 $rEvents->next(); 1048 continue; 1049 } 1050 1051 // If we are within the given time range, parse the event 1052 $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true); 1053 $rEvents->next(); 1054 } 1055 } 1056 else 1057 $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color); 1058 } 1059 } 1060 return $data; 1061 } 1062 1063 /** 1064 * Helper function that parses the iCal data of a VEVENT to a calendar entry. 1065 * 1066 * @param \Sabre\VObject\VEvent $event The event to parse 1067 * @param \DateTimeZone $timezone The timezone object 1068 * @param string $uid The entry's UID 1069 * @param boolean $recurring (optional) Set to true to define a recurring event 1070 * 1071 * @return array The parse calendar entry 1072 */ 1073 private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false) 1074 { 1075 $entry = array(); 1076 $start = $event->DTSTART; 1077 // Parse only if the start date/time is present 1078 if($start !== null) 1079 { 1080 $dtStart = $start->getDateTime(); 1081 $dtStart->setTimezone($timezone); 1082 1083 // moment.js doesn't like times be given even if 1084 // allDay is set to true 1085 // This should fix T23 1086 if($start['VALUE'] == 'DATE') 1087 { 1088 $entry['allDay'] = true; 1089 $entry['start'] = $dtStart->format("Y-m-d"); 1090 } 1091 else 1092 { 1093 $entry['allDay'] = false; 1094 $entry['start'] = $dtStart->format(\DateTime::ATOM); 1095 } 1096 } 1097 $end = $event->DTEND; 1098 // Parse only if the end date/time is present 1099 if($end !== null) 1100 { 1101 $dtEnd = $end->getDateTime(); 1102 $dtEnd->setTimezone($timezone); 1103 if($end['VALUE'] == 'DATE') 1104 $entry['end'] = $dtEnd->format("Y-m-d"); 1105 else 1106 $entry['end'] = $dtEnd->format(\DateTime::ATOM); 1107 } 1108 $description = $event->DESCRIPTION; 1109 if($description !== null) 1110 $entry['description'] = (string)$description; 1111 else 1112 $entry['description'] = ''; 1113 $attachments = $event->ATTACH; 1114 if($attachments !== null) 1115 { 1116 $entry['attachments'] = array(); 1117 foreach($attachments as $attachment) 1118 $entry['attachments'][] = (string)$attachment; 1119 } 1120 $entry['title'] = (string)$event->summary; 1121 $entry['location'] = (string)$event->location; 1122 $entry['id'] = $uid; 1123 $entry['page'] = $page; 1124 $entry['color'] = $color; 1125 $entry['recurring'] = $recurring; 1126 1127 return $entry; 1128 } 1129 1130 /** 1131 * Retrieve an event by its UID 1132 * 1133 * @param string $uid The event's UID 1134 * 1135 * @return mixed The table row with the given event 1136 */ 1137 public function getEventWithUid($uid) 1138 { 1139 $sqlite = $this->getDB(); 1140 if(!$sqlite) 1141 return false; 1142 $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?"; 1143 $res = $sqlite->query($query, $uid); 1144 $row = $sqlite->res2row($res); 1145 return $row; 1146 } 1147 1148 /** 1149 * Retrieve information of a calendar's object, not including the actual 1150 * calendar data! This is mainly needed for the sync support. 1151 * 1152 * @param int $calid The calendar ID 1153 * 1154 * @return mixed The result 1155 */ 1156 public function getCalendarObjects($calid) 1157 { 1158 $sqlite = $this->getDB(); 1159 if(!$sqlite) 1160 return false; 1161 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM calendarobjects WHERE calendarid = ?"; 1162 $res = $sqlite->query($query, $calid); 1163 $arr = $sqlite->res2arr($res); 1164 return $arr; 1165 } 1166 1167 /** 1168 * Retrieve a single calendar object by calendar ID and URI 1169 * 1170 * @param int $calid The calendar's ID 1171 * @param string $uri The object's URI 1172 * 1173 * @return mixed The result 1174 */ 1175 public function getCalendarObjectByUri($calid, $uri) 1176 { 1177 $sqlite = $this->getDB(); 1178 if(!$sqlite) 1179 return false; 1180 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1181 $res = $sqlite->query($query, $calid, $uri); 1182 $row = $sqlite->res2row($res); 1183 return $row; 1184 } 1185 1186 /** 1187 * Retrieve several calendar objects by specifying an array of URIs. 1188 * This is mainly neede for sync. 1189 * 1190 * @param int $calid The calendar's ID 1191 * @param array $uris An array of URIs 1192 * 1193 * @return mixed The result 1194 */ 1195 public function getMultipleCalendarObjectsByUri($calid, $uris) 1196 { 1197 $sqlite = $this->getDB(); 1198 if(!$sqlite) 1199 return false; 1200 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri IN ("; 1201 // Inserting a whole bunch of question marks 1202 $query .= implode(',', array_fill(0, count($uris), '?')); 1203 $query .= ')'; 1204 $vals = array_merge(array($calid), $uris); 1205 1206 $res = $sqlite->query($query, $vals); 1207 $arr = $sqlite->res2arr($res); 1208 return $arr; 1209 } 1210 1211 /** 1212 * Retrieve all calendar events for a given calendar ID 1213 * 1214 * @param string $calid The calendar's ID 1215 * 1216 * @return array An array containing all calendar data 1217 */ 1218 public function getAllCalendarEvents($calid) 1219 { 1220 $enabled = $this->getCalendarStatus($calid); 1221 if($enabled === false) 1222 return false; 1223 1224 $sqlite = $this->getDB(); 1225 if(!$sqlite) 1226 return false; 1227 $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?"; 1228 $res = $sqlite->query($query, $calid); 1229 $arr = $sqlite->res2arr($res); 1230 return $arr; 1231 } 1232 1233 /** 1234 * Edit a calendar entry for a page, given by its parameters. 1235 * The params array has the same format as @see addCalendarEntryForPage 1236 * 1237 * @param string $id The page's ID to work on 1238 * @param string $user The user's ID to work on 1239 * @param array $params The parameter array for the edited calendar event 1240 * 1241 * @return boolean True on success, otherwise false 1242 */ 1243 public function editCalendarEntryForPage($id, $user, $params) 1244 { 1245 if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') 1246 $timezone = new \DateTimeZone($params['currenttz']); 1247 elseif($params['currenttz'] === 'local') 1248 $timezone = new \DateTimeZone($params['detectedtz']); 1249 else 1250 $timezone = new \DateTimeZone('UTC'); 1251 1252 // Parse dates 1253 $startDate = explode('-', $params['eventfrom']); 1254 $startTime = explode(':', $params['eventfromtime']); 1255 $endDate = explode('-', $params['eventto']); 1256 $endTime = explode(':', $params['eventtotime']); 1257 1258 // Retrieve the existing event based on the UID 1259 $uid = $params['uid']; 1260 1261 if(strpos($id, 'webdav://') === 0) 1262 { 1263 $wdc =& plugin_load('helper', 'webdavclient'); 1264 if(is_null($wdc)) 1265 return false; 1266 $event = $wdc->getCalendarEntryByUid($uid); 1267 } 1268 else 1269 { 1270 $event = $this->getEventWithUid($uid); 1271 } 1272 1273 // Load SabreDAV 1274 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1275 if(!isset($event['calendardata'])) 1276 return false; 1277 $uri = $event['uri']; 1278 $calid = $event['calendarid']; 1279 1280 // Parse the existing event 1281 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1282 $vevent = $vcal->VEVENT; 1283 1284 // Set the new event values 1285 $vevent->summary = $params['eventname']; 1286 $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); 1287 $description = $params['eventdescription']; 1288 $location = $params['eventlocation']; 1289 1290 // Remove existing timestamps to overwrite them 1291 $vevent->remove('DESCRIPTION'); 1292 $vevent->remove('DTSTAMP'); 1293 $vevent->remove('LAST-MODIFIED'); 1294 $vevent->remove('ATTACH'); 1295 $vevent->remove('LOCATION'); 1296 1297 // Add new time stamps, description and location 1298 $vevent->add('DTSTAMP', $dtStamp); 1299 $vevent->add('LAST-MODIFIED', $dtStamp); 1300 if($description !== '') 1301 $vevent->add('DESCRIPTION', $description); 1302 if($location !== '') 1303 $vevent->add('LOCATION', $location); 1304 1305 // Add attachments 1306 $attachments = $params['attachments']; 1307 if(!is_null($attachments)) 1308 foreach($attachments as $attachment) 1309 $vevent->add('ATTACH', $attachment); 1310 1311 // Setup DTSTART 1312 $dtStart = new \DateTime(); 1313 $dtStart->setTimezone($timezone); 1314 $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); 1315 if($params['allday'] != '1') 1316 $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); 1317 1318 // Setup DTEND 1319 $dtEnd = new \DateTime(); 1320 $dtEnd->setTimezone($timezone); 1321 $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); 1322 if($params['allday'] != '1') 1323 $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); 1324 1325 // According to the VCal spec, we need to add a whole day here 1326 if($params['allday'] == '1') 1327 $dtEnd->add(new \DateInterval('P1D')); 1328 $vevent->remove('DTSTART'); 1329 $vevent->remove('DTEND'); 1330 $dtStartEv = $vevent->add('DTSTART', $dtStart); 1331 $dtEndEv = $vevent->add('DTEND', $dtEnd); 1332 1333 // Remove the time for allday events 1334 if($params['allday'] == '1') 1335 { 1336 $dtStartEv['VALUE'] = 'DATE'; 1337 $dtEndEv['VALUE'] = 'DATE'; 1338 } 1339 $eventStr = $vcal->serialize(); 1340 if(strpos($id, 'webdav://') === 0) 1341 { 1342 $connectionId = str_replace('webdav://', '', $id); 1343 return $wdc->editCalendarEntry($connectionId, $uid, $eventStr); 1344 } 1345 else 1346 { 1347 $sqlite = $this->getDB(); 1348 if(!$sqlite) 1349 return false; 1350 $now = new DateTime(); 1351 // Actually write to the database 1352 $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ". 1353 "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?"; 1354 $res = $sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(), 1355 $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid); 1356 if($res !== false) 1357 { 1358 $this->updateSyncTokenLog($calid, $uri, 'modified'); 1359 return true; 1360 } 1361 } 1362 return false; 1363 } 1364 1365 /** 1366 * Delete an event from a calendar by calendar ID and URI 1367 * 1368 * @param int $calid The calendar's ID 1369 * @param string $uri The object's URI 1370 * 1371 * @return true 1372 */ 1373 public function deleteCalendarEntryForCalendarByUri($calid, $uri) 1374 { 1375 $sqlite = $this->getDB(); 1376 if(!$sqlite) 1377 return false; 1378 $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1379 $res = $sqlite->query($query, $calid, $uri); 1380 if($res !== false) 1381 { 1382 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1383 } 1384 return true; 1385 } 1386 1387 /** 1388 * Delete a calendar entry for a given page. Actually, the event is removed 1389 * based on the entry's UID, so that page ID is no used. 1390 * 1391 * @param string $id The page's ID (unused) 1392 * @param array $params The parameter array to work with 1393 * 1394 * @return boolean True 1395 */ 1396 public function deleteCalendarEntryForPage($id, $params) 1397 { 1398 $uid = $params['uid']; 1399 if(strpos($id, 'webdav://') === 0) 1400 { 1401 $wdc =& plugin_load('helper', 'webdavclient'); 1402 if(is_null($wdc)) 1403 return false; 1404 $connectionId = str_replace('webdav://', '', $id); 1405 $result = $wdc->deleteCalendarEntry($connectionId, $uid); 1406 return $result; 1407 } 1408 $sqlite = $this->getDB(); 1409 if(!$sqlite) 1410 return false; 1411 $event = $this->getEventWithUid($uid); 1412 $calid = $event['calendarid']; 1413 $uri = $event['uri']; 1414 $query = "DELETE FROM calendarobjects WHERE uid = ?"; 1415 $res = $sqlite->query($query, $uid); 1416 if($res !== false) 1417 { 1418 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1419 } 1420 return true; 1421 } 1422 1423 /** 1424 * Retrieve the current sync token for a calendar 1425 * 1426 * @param string $calid The calendar id 1427 * 1428 * @return mixed The synctoken or false 1429 */ 1430 public function getSyncTokenForCalendar($calid) 1431 { 1432 $row = $this->getCalendarSettings($calid); 1433 if(isset($row['synctoken'])) 1434 return $row['synctoken']; 1435 return false; 1436 } 1437 1438 /** 1439 * Helper function to convert the operation name to 1440 * an operation code as stored in the database 1441 * 1442 * @param string $operationName The operation name 1443 * 1444 * @return mixed The operation code or false 1445 */ 1446 public function operationNameToOperation($operationName) 1447 { 1448 switch($operationName) 1449 { 1450 case 'added': 1451 return 1; 1452 break; 1453 case 'modified': 1454 return 2; 1455 break; 1456 case 'deleted': 1457 return 3; 1458 break; 1459 } 1460 return false; 1461 } 1462 1463 /** 1464 * Update the sync token log based on the calendar id and the 1465 * operation that was performed. 1466 * 1467 * @param string $calid The calendar ID that was modified 1468 * @param string $uri The calendar URI that was modified 1469 * @param string $operation The operation that was performed 1470 * 1471 * @return boolean True on success, otherwise false 1472 */ 1473 private function updateSyncTokenLog($calid, $uri, $operation) 1474 { 1475 $currentToken = $this->getSyncTokenForCalendar($calid); 1476 $operationCode = $this->operationNameToOperation($operation); 1477 if(($operationCode === false) || ($currentToken === false)) 1478 return false; 1479 $values = array($uri, 1480 $currentToken, 1481 $calid, 1482 $operationCode 1483 ); 1484 $sqlite = $this->getDB(); 1485 if(!$sqlite) 1486 return false; 1487 $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)"; 1488 $res = $sqlite->query($query, $uri, $currentToken, $calid, $operationCode); 1489 if($res === false) 1490 return false; 1491 $currentToken++; 1492 $query = "UPDATE calendars SET synctoken = ? WHERE id = ?"; 1493 $res = $sqlite->query($query, $currentToken, $calid); 1494 return ($res !== false); 1495 } 1496 1497 /** 1498 * Return the sync URL for a given Page, i.e. a calendar 1499 * 1500 * @param string $id The page's ID 1501 * @param string $user (optional) The user's ID 1502 * 1503 * @return mixed The sync url or false 1504 */ 1505 public function getSyncUrlForPage($id, $user = null) 1506 { 1507 if(is_null($userid)) 1508 { 1509 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 1510 { 1511 $userid = $_SERVER['REMOTE_USER']; 1512 } 1513 else 1514 { 1515 return false; 1516 } 1517 } 1518 1519 $calid = $this->getCalendarIdForPage($id); 1520 if($calid === false) 1521 return false; 1522 1523 $calsettings = $this->getCalendarSettings($calid); 1524 if(!isset($calsettings['uri'])) 1525 return false; 1526 1527 $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri']; 1528 return $syncurl; 1529 } 1530 1531 /** 1532 * Return the private calendar's URL for a given page 1533 * 1534 * @param string $id the page ID 1535 * 1536 * @return mixed The private URL or false 1537 */ 1538 public function getPrivateURLForPage($id) 1539 { 1540 $calid = $this->getCalendarIdForPage($id); 1541 if($calid === false) 1542 return false; 1543 1544 return $this->getPrivateURLForCalendar($calid); 1545 } 1546 1547 /** 1548 * Return the private calendar's URL for a given calendar ID 1549 * 1550 * @param string $calid The calendar's ID 1551 * 1552 * @return mixed The private URL or false 1553 */ 1554 public function getPrivateURLForCalendar($calid) 1555 { 1556 if(isset($this->cachedValues['privateurl'][$calid])) 1557 return $this->cachedValues['privateurl'][$calid]; 1558 $sqlite = $this->getDB(); 1559 if(!$sqlite) 1560 return false; 1561 $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?"; 1562 $res = $sqlite->query($query, $calid); 1563 $row = $sqlite->res2row($res); 1564 if(!isset($row['url'])) 1565 { 1566 $url = uniqid("dokuwiki-").".ics"; 1567 $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)"; 1568 $res = $sqlite->query($query, $url, $calid); 1569 if($res === false) 1570 return false; 1571 } 1572 else 1573 { 1574 $url = $row['url']; 1575 } 1576 1577 $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url; 1578 $this->cachedValues['privateurl'][$calid] = $url; 1579 return $url; 1580 } 1581 1582 /** 1583 * Retrieve the calendar ID for a given private calendar URL 1584 * 1585 * @param string $url The private URL 1586 * 1587 * @return mixed The calendar ID or false 1588 */ 1589 public function getCalendarForPrivateURL($url) 1590 { 1591 $sqlite = $this->getDB(); 1592 if(!$sqlite) 1593 return false; 1594 $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?"; 1595 $res = $sqlite->query($query, $url); 1596 $row = $sqlite->res2row($res); 1597 if(!isset($row['calid'])) 1598 return false; 1599 return $row['calid']; 1600 } 1601 1602 /** 1603 * Return a given calendar as ICS feed, i.e. all events in one ICS file. 1604 * 1605 * @param string $calid The calendar ID to retrieve 1606 * 1607 * @return mixed The calendar events as string or false 1608 */ 1609 public function getCalendarAsICSFeed($calid) 1610 { 1611 $calSettings = $this->getCalendarSettings($calid); 1612 if($calSettings === false) 1613 return false; 1614 $events = $this->getAllCalendarEvents($calid); 1615 if($events === false) 1616 return false; 1617 1618 // Load SabreDAV 1619 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1620 $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:"; 1621 $out .= $calSettings['displayname']."\r\n"; 1622 foreach($events as $event) 1623 { 1624 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1625 $evt = $vcal->VEVENT; 1626 $out .= $evt->serialize(); 1627 } 1628 $out .= "END:VCALENDAR\r\n"; 1629 return $out; 1630 } 1631 1632 /** 1633 * Retrieve a configuration option for the plugin 1634 * 1635 * @param string $key The key to query 1636 * @return mixed The option set, null if not found 1637 */ 1638 public function getConfig($key) 1639 { 1640 return $this->getConf($key); 1641 } 1642 1643 /** 1644 * Parses some information from calendar objects, used for optimized 1645 * calendar-queries. Taken nearly unmodified from Sabre's PDO backend 1646 * 1647 * Returns an array with the following keys: 1648 * * etag - An md5 checksum of the object without the quotes. 1649 * * size - Size of the object in bytes 1650 * * componentType - VEVENT, VTODO or VJOURNAL 1651 * * firstOccurence 1652 * * lastOccurence 1653 * * uid - value of the UID property 1654 * 1655 * @param string $calendarData 1656 * @return array 1657 */ 1658 protected function getDenormalizedData($calendarData) 1659 { 1660 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1661 1662 $vObject = \Sabre\VObject\Reader::read($calendarData); 1663 $componentType = null; 1664 $component = null; 1665 $firstOccurence = null; 1666 $lastOccurence = null; 1667 $uid = null; 1668 foreach ($vObject->getComponents() as $component) 1669 { 1670 if ($component->name !== 'VTIMEZONE') 1671 { 1672 $componentType = $component->name; 1673 $uid = (string)$component->UID; 1674 break; 1675 } 1676 } 1677 if (!$componentType) 1678 { 1679 return false; 1680 } 1681 if ($componentType === 'VEVENT') 1682 { 1683 $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); 1684 // Finding the last occurence is a bit harder 1685 if (!isset($component->RRULE)) 1686 { 1687 if (isset($component->DTEND)) 1688 { 1689 $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); 1690 } 1691 elseif (isset($component->DURATION)) 1692 { 1693 $endDate = clone $component->DTSTART->getDateTime(); 1694 $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue())); 1695 $lastOccurence = $endDate->getTimeStamp(); 1696 } 1697 elseif (!$component->DTSTART->hasTime()) 1698 { 1699 $endDate = clone $component->DTSTART->getDateTime(); 1700 $endDate->modify('+1 day'); 1701 $lastOccurence = $endDate->getTimeStamp(); 1702 } 1703 else 1704 { 1705 $lastOccurence = $firstOccurence; 1706 } 1707 } 1708 else 1709 { 1710 $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID); 1711 $maxDate = new \DateTime('2038-01-01'); 1712 if ($it->isInfinite()) 1713 { 1714 $lastOccurence = $maxDate->getTimeStamp(); 1715 } 1716 else 1717 { 1718 $end = $it->getDtEnd(); 1719 while ($it->valid() && $end < $maxDate) 1720 { 1721 $end = $it->getDtEnd(); 1722 $it->next(); 1723 } 1724 $lastOccurence = $end->getTimeStamp(); 1725 } 1726 } 1727 } 1728 1729 return array( 1730 'etag' => md5($calendarData), 1731 'size' => strlen($calendarData), 1732 'componentType' => $componentType, 1733 'firstOccurence' => $firstOccurence, 1734 'lastOccurence' => $lastOccurence, 1735 'uid' => $uid, 1736 ); 1737 1738 } 1739 1740 /** 1741 * Query a calendar by ID and taking several filters into account. 1742 * This is heavily based on Sabre's PDO backend. 1743 * 1744 * @param int $calendarId The calendar's ID 1745 * @param array $filters The filter array to apply 1746 * 1747 * @return mixed The result 1748 */ 1749 public function calendarQuery($calendarId, $filters) 1750 { 1751 dbglog('davcal::helper::calendarQuery'); 1752 $componentType = null; 1753 $requirePostFilter = true; 1754 $timeRange = null; 1755 $sqlite = $this->getDB(); 1756 if(!$sqlite) 1757 return false; 1758 1759 // if no filters were specified, we don't need to filter after a query 1760 if (!$filters['prop-filters'] && !$filters['comp-filters']) 1761 { 1762 $requirePostFilter = false; 1763 } 1764 1765 // Figuring out if there's a component filter 1766 if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) 1767 { 1768 $componentType = $filters['comp-filters'][0]['name']; 1769 1770 // Checking if we need post-filters 1771 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) 1772 { 1773 $requirePostFilter = false; 1774 } 1775 // There was a time-range filter 1776 if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) 1777 { 1778 $timeRange = $filters['comp-filters'][0]['time-range']; 1779 1780 // If start time OR the end time is not specified, we can do a 1781 // 100% accurate mysql query. 1782 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) 1783 { 1784 $requirePostFilter = false; 1785 } 1786 } 1787 1788 } 1789 1790 if ($requirePostFilter) 1791 { 1792 $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?"; 1793 } 1794 else 1795 { 1796 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1797 } 1798 1799 $values = array( 1800 $calendarId 1801 ); 1802 1803 if ($componentType) 1804 { 1805 $query .= " AND componenttype = ?"; 1806 $values[] = $componentType; 1807 } 1808 1809 if ($timeRange && $timeRange['start']) 1810 { 1811 $query .= " AND lastoccurence > ?"; 1812 $values[] = $timeRange['start']->getTimeStamp(); 1813 } 1814 if ($timeRange && $timeRange['end']) 1815 { 1816 $query .= " AND firstoccurence < ?"; 1817 $values[] = $timeRange['end']->getTimeStamp(); 1818 } 1819 1820 $res = $sqlite->query($query, $values); 1821 $arr = $sqlite->res2arr($res); 1822 1823 $result = array(); 1824 foreach($arr as $row) 1825 { 1826 if ($requirePostFilter) 1827 { 1828 if (!$this->validateFilterForObject($row, $filters)) 1829 { 1830 continue; 1831 } 1832 } 1833 $result[] = $row['uri']; 1834 1835 } 1836 1837 return $result; 1838 } 1839 1840 /** 1841 * This method validates if a filter (as passed to calendarQuery) matches 1842 * the given object. Taken from Sabre's PDO backend 1843 * 1844 * @param array $object 1845 * @param array $filters 1846 * @return bool 1847 */ 1848 protected function validateFilterForObject($object, $filters) 1849 { 1850 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1851 // Unfortunately, setting the 'calendardata' here is optional. If 1852 // it was excluded, we actually need another call to get this as 1853 // well. 1854 if (!isset($object['calendardata'])) 1855 { 1856 $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']); 1857 } 1858 1859 $vObject = \Sabre\VObject\Reader::read($object['calendardata']); 1860 $validator = new \Sabre\CalDAV\CalendarQueryValidator(); 1861 1862 $res = $validator->validate($vObject, $filters); 1863 return $res; 1864 1865 } 1866 1867 /** 1868 * Retrieve changes for a given calendar based on the given syncToken. 1869 * 1870 * @param int $calid The calendar's ID 1871 * @param int $syncToken The supplied sync token 1872 * @param int $syncLevel The sync level 1873 * @param int $limit The limit of changes 1874 * 1875 * @return array The result 1876 */ 1877 public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null) 1878 { 1879 // Current synctoken 1880 $currentToken = $this->getSyncTokenForCalendar($calid); 1881 1882 if ($currentToken === false) return null; 1883 1884 $result = array( 1885 'syncToken' => $currentToken, 1886 'added' => array(), 1887 'modified' => array(), 1888 'deleted' => array(), 1889 ); 1890 $sqlite = $this->getDB(); 1891 if(!$sqlite) 1892 return false; 1893 1894 if ($syncToken) 1895 { 1896 1897 $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; 1898 if ($limit > 0) $query .= " LIMIT " . (int)$limit; 1899 1900 // Fetching all changes 1901 $res = $sqlite->query($query, $syncToken, $currentToken, $calid); 1902 if($res === false) 1903 return null; 1904 1905 $arr = $sqlite->res2arr($res); 1906 $changes = array(); 1907 1908 // This loop ensures that any duplicates are overwritten, only the 1909 // last change on a node is relevant. 1910 foreach($arr as $row) 1911 { 1912 $changes[$row['uri']] = $row['operation']; 1913 } 1914 1915 foreach ($changes as $uri => $operation) 1916 { 1917 switch ($operation) 1918 { 1919 case 1 : 1920 $result['added'][] = $uri; 1921 break; 1922 case 2 : 1923 $result['modified'][] = $uri; 1924 break; 1925 case 3 : 1926 $result['deleted'][] = $uri; 1927 break; 1928 } 1929 1930 } 1931 } 1932 else 1933 { 1934 // No synctoken supplied, this is the initial sync. 1935 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1936 $res = $sqlite->query($query); 1937 $arr = $sqlite->res2arr($res); 1938 1939 $result['added'] = $arr; 1940 } 1941 return $result; 1942 } 1943 1944} 1945