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