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