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