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