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