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