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