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