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 $enabled = $this->getCalendarStatus($id); 552 if($enabled == false) 553 continue; 554 $page = $row['page']; 555 $acl = auth_aclcheck($page, $user, $groups); 556 if($acl >= AUTH_READ) 557 { 558 $write = $acl > AUTH_READ; 559 $calids[$id] = array('readonly' => !$write); 560 } 561 } 562 return $calids; 563 } 564 565 /** 566 * Create a new calendar for a given page ID and set name and description 567 * accordingly. Also update the pagetocalendarmapping table on success. 568 * 569 * @param string $name The calendar's name 570 * @param string $description The calendar's description 571 * @param string $id (optional) The page ID to work on 572 * @param string $userid (optional) The user ID that created the calendar 573 * 574 * @return boolean True on success, otherwise false 575 */ 576 public function createCalendarForPage($name, $description, $id = null, $userid = null) 577 { 578 if(is_null($id)) 579 { 580 global $ID; 581 $id = $ID; 582 } 583 if(is_null($userid)) 584 { 585 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 586 { 587 $userid = $_SERVER['REMOTE_USER']; 588 } 589 else 590 { 591 $userid = uniqid('davcal-'); 592 } 593 } 594 $values = array('principals/'.$userid, 595 $name, 596 str_replace(array('/', ' ', ':'), '_', $id), 597 $description, 598 'VEVENT,VTODO', 599 0, 600 1); 601 $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) ". 602 "VALUES (?, ?, ?, ?, ?, ?, ?)"; 603 $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6]); 604 if($res === false) 605 return false; 606 607 // Get the new calendar ID 608 $query = "SELECT id FROM calendars WHERE principaluri = ? AND displayname = ? AND ". 609 "uri = ? AND description = ?"; 610 $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3]); 611 $row = $this->sqlite->res2row($res); 612 613 // Update the pagetocalendarmapping table with the new calendar ID 614 if(isset($row['id'])) 615 { 616 $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (?, ?)"; 617 $res = $this->sqlite->query($query, $id, $row['id']); 618 return ($res !== false); 619 } 620 621 return false; 622 } 623 624 /** 625 * Add a new calendar entry to the given calendar. Calendar data is 626 * specified as ICS file, thus it needs to be parsed first. 627 * 628 * This is mainly needed for the sync support. 629 * 630 * @param int $calid The calendar's ID 631 * @param string $uri The new object URI 632 * @param string $ics The ICS file 633 * 634 * @return mixed The etag. 635 */ 636 public function addCalendarEntryToCalendarByICS($calid, $uri, $ics) 637 { 638 $extraData = $this->getDenormalizedData($ics); 639 640 $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)"; 641 $res = $this->sqlite->query($query, 642 $calid, 643 $uri, 644 $ics, 645 time(), 646 $extraData['etag'], 647 $extraData['size'], 648 $extraData['componentType'], 649 $extraData['firstOccurence'], 650 $extraData['lastOccurence'], 651 $extraData['uid']); 652 // If successfully, update the sync token database 653 if($res !== false) 654 { 655 $this->updateSyncTokenLog($calid, $uri, 'added'); 656 } 657 return $extraData['etag']; 658 } 659 660 /** 661 * Edit a calendar entry by providing a new ICS file. This is mainly 662 * needed for the sync support. 663 * 664 * @param int $calid The calendar's IS 665 * @param string $uri The object's URI to modify 666 * @param string $ics The new object's ICS file 667 */ 668 public function editCalendarEntryToCalendarByICS($calid, $uri, $ics) 669 { 670 $extraData = $this->getDenormalizedData($ics); 671 672 $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?"; 673 $res = $this->sqlite->query($query, 674 $ics, 675 time(), 676 $extraData['etag'], 677 $extraData['size'], 678 $extraData['componentType'], 679 $extraData['firstOccurence'], 680 $extraData['lastOccurence'], 681 $extraData['uid'], 682 $calid, 683 $uri 684 ); 685 if($res !== false) 686 { 687 $this->updateSyncTokenLog($calid, $uri, 'modified'); 688 } 689 return $extraData['etag']; 690 } 691 692 /** 693 * Add a new iCal entry for a given page, i.e. a given calendar. 694 * 695 * The parameter array needs to contain 696 * detectedtz => The timezone as detected by the browser 697 * currenttz => The timezone in use by the calendar 698 * eventfrom => The event's start date 699 * eventfromtime => The event's start time 700 * eventto => The event's end date 701 * eventtotime => The event's end time 702 * eventname => The event's name 703 * eventdescription => The event's description 704 * 705 * @param string $id The page ID to work on 706 * @param string $user The user who created the calendar 707 * @param string $params A parameter array with values to create 708 * 709 * @return boolean True on success, otherwise false 710 */ 711 public function addCalendarEntryToCalendarForPage($id, $user, $params) 712 { 713 if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') 714 $timezone = new \DateTimeZone($params['currenttz']); 715 elseif($params['currenttz'] === 'local') 716 $timezone = new \DateTimeZone($params['detectedtz']); 717 else 718 $timezone = new \DateTimeZone('UTC'); 719 720 // Retrieve dates from settings 721 $startDate = explode('-', $params['eventfrom']); 722 $startTime = explode(':', $params['eventfromtime']); 723 $endDate = explode('-', $params['eventto']); 724 $endTime = explode(':', $params['eventtotime']); 725 726 // Load SabreDAV 727 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 728 $vcalendar = new \Sabre\VObject\Component\VCalendar(); 729 730 // Add VCalendar, UID and Event Name 731 $event = $vcalendar->add('VEVENT'); 732 $uuid = \Sabre\VObject\UUIDUtil::getUUID(); 733 $event->add('UID', $uuid); 734 $event->summary = $params['eventname']; 735 736 // Add a description if requested 737 $description = $params['eventdescription']; 738 if($description !== '') 739 $event->add('DESCRIPTION', $description); 740 741 // Add a location if requested 742 $location = $params['eventlocation']; 743 if($location !== '') 744 $event->add('LOCATION', $location); 745 746 // Add attachments 747 $attachments = $params['attachments']; 748 if(!is_null($attachments)) 749 foreach($attachments as $attachment) 750 $event->add('ATTACH', $attachment); 751 752 // Create a timestamp for last modified, created and dtstamp values in UTC 753 $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); 754 $event->add('DTSTAMP', $dtStamp); 755 $event->add('CREATED', $dtStamp); 756 $event->add('LAST-MODIFIED', $dtStamp); 757 758 // Adjust the start date, based on the given timezone information 759 $dtStart = new \DateTime(); 760 $dtStart->setTimezone($timezone); 761 $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); 762 763 // Only add the time values if it's not an allday event 764 if($params['allday'] != '1') 765 $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); 766 767 // Adjust the end date, based on the given timezone information 768 $dtEnd = new \DateTime(); 769 $dtEnd->setTimezone($timezone); 770 $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); 771 772 // Only add the time values if it's not an allday event 773 if($params['allday'] != '1') 774 $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); 775 776 // According to the VCal spec, we need to add a whole day here 777 if($params['allday'] == '1') 778 $dtEnd->add(new \DateInterval('P1D')); 779 780 // Really add Start and End events 781 $dtStartEv = $event->add('DTSTART', $dtStart); 782 $dtEndEv = $event->add('DTEND', $dtEnd); 783 784 // Adjust the DATE format for allday events 785 if($params['allday'] == '1') 786 { 787 $dtStartEv['VALUE'] = 'DATE'; 788 $dtEndEv['VALUE'] = 'DATE'; 789 } 790 791 $eventStr = $vcalendar->serialize(); 792 793 if(strpos($id, 'webdav://') === 0) 794 { 795 $wdc =& plugin_load('helper', 'webdavclient'); 796 if(is_null($wdc)) 797 return false; 798 $connectionId = str_replace('webdav://', '', $id); 799 return $wdc->addCalendarEntry($connectionId, $eventStr); 800 } 801 else 802 { 803 // Actually add the values to the database 804 $calid = $this->getCalendarIdForPage($id); 805 $uri = uniqid('dokuwiki-').'.ics'; 806 $now = new \DateTime(); 807 808 $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; 809 $res = $this->sqlite->query($query, $calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT', 810 $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(), 811 strlen($eventStr), md5($eventStr), $uuid); 812 813 // If successfully, update the sync token database 814 if($res !== false) 815 { 816 $this->updateSyncTokenLog($calid, $uri, 'added'); 817 return true; 818 } 819 } 820 return false; 821 } 822 823 /** 824 * Retrieve the calendar settings of a given calendar id 825 * 826 * @param string $calid The calendar ID 827 * 828 * @return array The calendar settings array 829 */ 830 public function getCalendarSettings($calid) 831 { 832 $query = "SELECT id, principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken, disabled FROM calendars WHERE id= ? "; 833 $res = $this->sqlite->query($query, $calid); 834 $row = $this->sqlite->res2row($res); 835 return $row; 836 } 837 838 /** 839 * Retrieve the calendar status of a given calendar id 840 * 841 * @param string $calid The calendar ID 842 * @return boolean True if calendar is enabled, otherwise false 843 */ 844 public function getCalendarStatus($calid) 845 { 846 $query = "SELECT disabled FROM calendars WHERE id = ?"; 847 $res = $this->sqlite->query($query, $calid); 848 $row = $this->sqlite->res2row($res); 849 if($row['disabled'] == 1) 850 return false; 851 else 852 return true; 853 } 854 855 /** 856 * Disable a calendar for a given page 857 * 858 * @param string $id The page ID 859 * 860 * @return boolean true on success, otherwise false 861 */ 862 public function disableCalendarForPage($id) 863 { 864 $calid = $this->getCalendarIdForPage($id); 865 if($calid === false) 866 return false; 867 $query = "UPDATE calendars SET disabled = 1 WHERE id = ?"; 868 $res = $this->sqlite->query($query, $calid); 869 if($res !== false) 870 return true; 871 return false; 872 } 873 874 /** 875 * Enable a calendar for a given page 876 * 877 * @param string $id The page ID 878 * 879 * @return boolean true on success, otherwise false 880 */ 881 public function enableCalendarForPage($id) 882 { 883 $calid = $this->getCalendarIdForPage($id); 884 if($calid === false) 885 return false; 886 $query = "UPDATE calendars SET disabled = 0 WHERE id = ?"; 887 $res = $this->sqlite->query($query, $calid); 888 if($res !== false) 889 return true; 890 return false; 891 } 892 893 /** 894 * Retrieve all events that are within a given date range, 895 * based on the timezone setting. 896 * 897 * There is also support for retrieving recurring events, 898 * using Sabre's VObject Iterator. Recurring events are represented 899 * as individual calendar entries with the same UID. 900 * 901 * @param string $id The page ID to work with 902 * @param string $user The user ID to work with 903 * @param string $startDate The start date as a string 904 * @param string $endDate The end date as a string 905 * @param string $color (optional) The calendar's color 906 * 907 * @return array An array containing the calendar entries. 908 */ 909 public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone, $color = null) 910 { 911 if($timezone !== '' && $timezone !== 'local') 912 $timezone = new \DateTimeZone($timezone); 913 else 914 $timezone = new \DateTimeZone('UTC'); 915 $data = array(); 916 917 $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid = ?"; 918 $startTs = null; 919 $endTs = null; 920 if($startDate !== null) 921 { 922 $startTs = new \DateTime($startDate); 923 $query .= " AND lastoccurence > ".$this->sqlite->quote_string($startTs->getTimestamp()); 924 } 925 if($endDate !== null) 926 { 927 $endTs = new \DateTime($endDate); 928 $query .= " AND firstoccurence < ".$this->sqlite->quote_string($endTs->getTimestamp()); 929 } 930 931 // Load SabreDAV 932 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 933 934 if(strpos($id, 'webdav://') === 0) 935 { 936 $wdc =& plugin_load('helper', 'webdavclient'); 937 if(is_null($wdc)) 938 return $data; 939 $connectionId = str_replace('webdav://', '', $id); 940 $arr = $wdc->getCalendarEntries($connectionId, $startDate, $endDate); 941 } 942 else 943 { 944 $calid = $this->getCalendarIdForPage($id); 945 if(is_null($color)) 946 $color = $this->getCalendarColorForCalendar($calid); 947 948 // Retrieve matching calendar objects 949 $res = $this->sqlite->query($query, $calid); 950 $arr = $this->sqlite->res2arr($res); 951 } 952 953 // Parse individual calendar entries 954 foreach($arr as $row) 955 { 956 if(isset($row['calendardata'])) 957 { 958 $entry = array(); 959 $vcal = \Sabre\VObject\Reader::read($row['calendardata']); 960 $recurrence = $vcal->VEVENT->RRULE; 961 // If it is a recurring event, pass it through Sabre's EventIterator 962 if($recurrence != null) 963 { 964 $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT)); 965 $rEvents->rewind(); 966 while($rEvents->valid()) 967 { 968 $event = $rEvents->getEventObject(); 969 // If we are after the given time range, exit 970 if(($endTs !== null) && ($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp())) 971 break; 972 973 // If we are before the given time range, continue 974 if(($startTs != null) && ($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp())) 975 { 976 $rEvents->next(); 977 continue; 978 } 979 980 // If we are within the given time range, parse the event 981 $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true); 982 $rEvents->next(); 983 } 984 } 985 else 986 $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color); 987 } 988 } 989 return $data; 990 } 991 992 /** 993 * Helper function that parses the iCal data of a VEVENT to a calendar entry. 994 * 995 * @param \Sabre\VObject\VEvent $event The event to parse 996 * @param \DateTimeZone $timezone The timezone object 997 * @param string $uid The entry's UID 998 * @param boolean $recurring (optional) Set to true to define a recurring event 999 * 1000 * @return array The parse calendar entry 1001 */ 1002 private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false) 1003 { 1004 $entry = array(); 1005 $start = $event->DTSTART; 1006 // Parse only if the start date/time is present 1007 if($start !== null) 1008 { 1009 $dtStart = $start->getDateTime(); 1010 $dtStart->setTimezone($timezone); 1011 1012 // moment.js doesn't like times be given even if 1013 // allDay is set to true 1014 // This should fix T23 1015 if($start['VALUE'] == 'DATE') 1016 { 1017 $entry['allDay'] = true; 1018 $entry['start'] = $dtStart->format("Y-m-d"); 1019 } 1020 else 1021 { 1022 $entry['allDay'] = false; 1023 $entry['start'] = $dtStart->format(\DateTime::ATOM); 1024 } 1025 } 1026 $end = $event->DTEND; 1027 // Parse only if the end date/time is present 1028 if($end !== null) 1029 { 1030 $dtEnd = $end->getDateTime(); 1031 $dtEnd->setTimezone($timezone); 1032 if($end['VALUE'] == 'DATE') 1033 $entry['end'] = $dtEnd->format("Y-m-d"); 1034 else 1035 $entry['end'] = $dtEnd->format(\DateTime::ATOM); 1036 } 1037 $description = $event->DESCRIPTION; 1038 if($description !== null) 1039 $entry['description'] = (string)$description; 1040 else 1041 $entry['description'] = ''; 1042 $attachments = $event->ATTACH; 1043 if($attachments !== null) 1044 { 1045 $entry['attachments'] = array(); 1046 foreach($attachments as $attachment) 1047 $entry['attachments'][] = (string)$attachment; 1048 } 1049 $entry['title'] = (string)$event->summary; 1050 $entry['location'] = (string)$event->location; 1051 $entry['id'] = $uid; 1052 $entry['page'] = $page; 1053 $entry['color'] = $color; 1054 $entry['recurring'] = $recurring; 1055 1056 return $entry; 1057 } 1058 1059 /** 1060 * Retrieve an event by its UID 1061 * 1062 * @param string $uid The event's UID 1063 * 1064 * @return mixed The table row with the given event 1065 */ 1066 public function getEventWithUid($uid) 1067 { 1068 $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?"; 1069 $res = $this->sqlite->query($query, $uid); 1070 $row = $this->sqlite->res2row($res); 1071 return $row; 1072 } 1073 1074 /** 1075 * Retrieve information of a calendar's object, not including the actual 1076 * calendar data! This is mainly neede for the sync support. 1077 * 1078 * @param int $calid The calendar ID 1079 * 1080 * @return mixed The result 1081 */ 1082 public function getCalendarObjects($calid) 1083 { 1084 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM calendarobjects WHERE calendarid = ?"; 1085 $res = $this->sqlite->query($query, $calid); 1086 $arr = $this->sqlite->res2arr($res); 1087 return $arr; 1088 } 1089 1090 /** 1091 * Retrieve a single calendar object by calendar ID and URI 1092 * 1093 * @param int $calid The calendar's ID 1094 * @param string $uri The object's URI 1095 * 1096 * @return mixed The result 1097 */ 1098 public function getCalendarObjectByUri($calid, $uri) 1099 { 1100 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1101 $res = $this->sqlite->query($query, $calid, $uri); 1102 $row = $this->sqlite->res2row($res); 1103 return $row; 1104 } 1105 1106 /** 1107 * Retrieve several calendar objects by specifying an array of URIs. 1108 * This is mainly neede for sync. 1109 * 1110 * @param int $calid The calendar's ID 1111 * @param array $uris An array of URIs 1112 * 1113 * @return mixed The result 1114 */ 1115 public function getMultipleCalendarObjectsByUri($calid, $uris) 1116 { 1117 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri IN ("; 1118 // Inserting a whole bunch of question marks 1119 $query .= implode(',', array_fill(0, count($uris), '?')); 1120 $query .= ')'; 1121 $vals = array_merge(array($calid), $uris); 1122 1123 $res = $this->sqlite->query($query, $vals); 1124 $arr = $this->sqlite->res2arr($res); 1125 return $arr; 1126 } 1127 1128 /** 1129 * Retrieve all calendar events for a given calendar ID 1130 * 1131 * @param string $calid The calendar's ID 1132 * 1133 * @return array An array containing all calendar data 1134 */ 1135 public function getAllCalendarEvents($calid) 1136 { 1137 $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?"; 1138 $res = $this->sqlite->query($query, $calid); 1139 $arr = $this->sqlite->res2arr($res); 1140 return $arr; 1141 } 1142 1143 /** 1144 * Edit a calendar entry for a page, given by its parameters. 1145 * The params array has the same format as @see addCalendarEntryForPage 1146 * 1147 * @param string $id The page's ID to work on 1148 * @param string $user The user's ID to work on 1149 * @param array $params The parameter array for the edited calendar event 1150 * 1151 * @return boolean True on success, otherwise false 1152 */ 1153 public function editCalendarEntryForPage($id, $user, $params) 1154 { 1155 if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') 1156 $timezone = new \DateTimeZone($params['currenttz']); 1157 elseif($params['currenttz'] === 'local') 1158 $timezone = new \DateTimeZone($params['detectedtz']); 1159 else 1160 $timezone = new \DateTimeZone('UTC'); 1161 1162 // Parse dates 1163 $startDate = explode('-', $params['eventfrom']); 1164 $startTime = explode(':', $params['eventfromtime']); 1165 $endDate = explode('-', $params['eventto']); 1166 $endTime = explode(':', $params['eventtotime']); 1167 1168 // Retrieve the existing event based on the UID 1169 $uid = $params['uid']; 1170 1171 if(strpos($id, 'webdav://') === 0) 1172 { 1173 $wdc =& plugin_load('helper', 'webdavclient'); 1174 if(is_null($wdc)) 1175 return false; 1176 $event = $wdc->getCalendarEntryByUid($uid); 1177 } 1178 else 1179 { 1180 $event = $this->getEventWithUid($uid); 1181 } 1182 1183 // Load SabreDAV 1184 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1185 if(!isset($event['calendardata'])) 1186 return false; 1187 $uri = $event['uri']; 1188 $calid = $event['calendarid']; 1189 1190 // Parse the existing event 1191 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1192 $vevent = $vcal->VEVENT; 1193 1194 // Set the new event values 1195 $vevent->summary = $params['eventname']; 1196 $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); 1197 $description = $params['eventdescription']; 1198 $location = $params['eventlocation']; 1199 1200 // Remove existing timestamps to overwrite them 1201 $vevent->remove('DESCRIPTION'); 1202 $vevent->remove('DTSTAMP'); 1203 $vevent->remove('LAST-MODIFIED'); 1204 $vevent->remove('ATTACH'); 1205 $vevent->remove('LOCATION'); 1206 1207 // Add new time stamps, description and location 1208 $vevent->add('DTSTAMP', $dtStamp); 1209 $vevent->add('LAST-MODIFIED', $dtStamp); 1210 if($description !== '') 1211 $vevent->add('DESCRIPTION', $description); 1212 if($location !== '') 1213 $vevent->add('LOCATION', $location); 1214 1215 // Add attachments 1216 $attachments = $params['attachments']; 1217 if(!is_null($attachments)) 1218 foreach($attachments as $attachment) 1219 $vevent->add('ATTACH', $attachment); 1220 1221 // Setup DTSTART 1222 $dtStart = new \DateTime(); 1223 $dtStart->setTimezone($timezone); 1224 $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); 1225 if($params['allday'] != '1') 1226 $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); 1227 1228 // Setup DTEND 1229 $dtEnd = new \DateTime(); 1230 $dtEnd->setTimezone($timezone); 1231 $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); 1232 if($params['allday'] != '1') 1233 $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); 1234 1235 // According to the VCal spec, we need to add a whole day here 1236 if($params['allday'] == '1') 1237 $dtEnd->add(new \DateInterval('P1D')); 1238 $vevent->remove('DTSTART'); 1239 $vevent->remove('DTEND'); 1240 $dtStartEv = $vevent->add('DTSTART', $dtStart); 1241 $dtEndEv = $vevent->add('DTEND', $dtEnd); 1242 1243 // Remove the time for allday events 1244 if($params['allday'] == '1') 1245 { 1246 $dtStartEv['VALUE'] = 'DATE'; 1247 $dtEndEv['VALUE'] = 'DATE'; 1248 } 1249 $eventStr = $vcal->serialize(); 1250 if(strpos($id, 'webdav://') === 0) 1251 { 1252 $connectionId = str_replace('webdav://', '', $id); 1253 return $wdc->editCalendarEntry($connectionId, $uid, $eventStr); 1254 } 1255 else 1256 { 1257 $now = new DateTime(); 1258 // Actually write to the database 1259 $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ". 1260 "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?"; 1261 $res = $this->sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(), 1262 $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid); 1263 if($res !== false) 1264 { 1265 $this->updateSyncTokenLog($calid, $uri, 'modified'); 1266 return true; 1267 } 1268 } 1269 return false; 1270 } 1271 1272 /** 1273 * Delete an event from a calendar by calendar ID and URI 1274 * 1275 * @param int $calid The calendar's ID 1276 * @param string $uri The object's URI 1277 * 1278 * @return true 1279 */ 1280 public function deleteCalendarEntryForCalendarByUri($calid, $uri) 1281 { 1282 $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1283 $res = $this->sqlite->query($query, $calid, $uri); 1284 if($res !== false) 1285 { 1286 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1287 } 1288 return true; 1289 } 1290 1291 /** 1292 * Delete a calendar entry for a given page. Actually, the event is removed 1293 * based on the entry's UID, so that page ID is no used. 1294 * 1295 * @param string $id The page's ID (unused) 1296 * @param array $params The parameter array to work with 1297 * 1298 * @return boolean True 1299 */ 1300 public function deleteCalendarEntryForPage($id, $params) 1301 { 1302 $uid = $params['uid']; 1303 if(strpos($id, 'webdav://') === 0) 1304 { 1305 $wdc =& plugin_load('helper', 'webdavclient'); 1306 if(is_null($wdc)) 1307 return false; 1308 $connectionId = str_replace('webdav://', '', $id); 1309 $result = $wdc->deleteCalendarEntry($connectionId, $uid); 1310 return $result; 1311 } 1312 $event = $this->getEventWithUid($uid); 1313 $calid = $event['calendarid']; 1314 $uri = $event['uri']; 1315 $query = "DELETE FROM calendarobjects WHERE uid = ?"; 1316 $res = $this->sqlite->query($query, $uid); 1317 if($res !== false) 1318 { 1319 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1320 } 1321 return true; 1322 } 1323 1324 /** 1325 * Retrieve the current sync token for a calendar 1326 * 1327 * @param string $calid The calendar id 1328 * 1329 * @return mixed The synctoken or false 1330 */ 1331 public function getSyncTokenForCalendar($calid) 1332 { 1333 $row = $this->getCalendarSettings($calid); 1334 if(isset($row['synctoken'])) 1335 return $row['synctoken']; 1336 return false; 1337 } 1338 1339 /** 1340 * Helper function to convert the operation name to 1341 * an operation code as stored in the database 1342 * 1343 * @param string $operationName The operation name 1344 * 1345 * @return mixed The operation code or false 1346 */ 1347 public function operationNameToOperation($operationName) 1348 { 1349 switch($operationName) 1350 { 1351 case 'added': 1352 return 1; 1353 break; 1354 case 'modified': 1355 return 2; 1356 break; 1357 case 'deleted': 1358 return 3; 1359 break; 1360 } 1361 return false; 1362 } 1363 1364 /** 1365 * Update the sync token log based on the calendar id and the 1366 * operation that was performed. 1367 * 1368 * @param string $calid The calendar ID that was modified 1369 * @param string $uri The calendar URI that was modified 1370 * @param string $operation The operation that was performed 1371 * 1372 * @return boolean True on success, otherwise false 1373 */ 1374 private function updateSyncTokenLog($calid, $uri, $operation) 1375 { 1376 $currentToken = $this->getSyncTokenForCalendar($calid); 1377 $operationCode = $this->operationNameToOperation($operation); 1378 if(($operationCode === false) || ($currentToken === false)) 1379 return false; 1380 $values = array($uri, 1381 $currentToken, 1382 $calid, 1383 $operationCode 1384 ); 1385 $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)"; 1386 $res = $this->sqlite->query($query, $uri, $currentToken, $calid, $operationCode); 1387 if($res === false) 1388 return false; 1389 $currentToken++; 1390 $query = "UPDATE calendars SET synctoken = ? WHERE id = ?"; 1391 $res = $this->sqlite->query($query, $currentToken, $calid); 1392 return ($res !== false); 1393 } 1394 1395 /** 1396 * Return the sync URL for a given Page, i.e. a calendar 1397 * 1398 * @param string $id The page's ID 1399 * @param string $user (optional) The user's ID 1400 * 1401 * @return mixed The sync url or false 1402 */ 1403 public function getSyncUrlForPage($id, $user = null) 1404 { 1405 if(is_null($userid)) 1406 { 1407 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 1408 { 1409 $userid = $_SERVER['REMOTE_USER']; 1410 } 1411 else 1412 { 1413 return false; 1414 } 1415 } 1416 1417 $calid = $this->getCalendarIdForPage($id); 1418 if($calid === false) 1419 return false; 1420 1421 $calsettings = $this->getCalendarSettings($calid); 1422 if(!isset($calsettings['uri'])) 1423 return false; 1424 1425 $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri']; 1426 return $syncurl; 1427 } 1428 1429 /** 1430 * Return the private calendar's URL for a given page 1431 * 1432 * @param string $id the page ID 1433 * 1434 * @return mixed The private URL or false 1435 */ 1436 public function getPrivateURLForPage($id) 1437 { 1438 $calid = $this->getCalendarIdForPage($id); 1439 if($calid === false) 1440 return false; 1441 1442 return $this->getPrivateURLForCalendar($calid); 1443 } 1444 1445 /** 1446 * Return the private calendar's URL for a given calendar ID 1447 * 1448 * @param string $calid The calendar's ID 1449 * 1450 * @return mixed The private URL or false 1451 */ 1452 public function getPrivateURLForCalendar($calid) 1453 { 1454 if(isset($this->cachedValues['privateurl'][$calid])) 1455 return $this->cachedValues['privateurl'][$calid]; 1456 $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?"; 1457 $res = $this->sqlite->query($query, $calid); 1458 $row = $this->sqlite->res2row($res); 1459 if(!isset($row['url'])) 1460 { 1461 $url = uniqid("dokuwiki-").".ics"; 1462 $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)"; 1463 $res = $this->sqlite->query($query, $url, $calid); 1464 if($res === false) 1465 return false; 1466 } 1467 else 1468 { 1469 $url = $row['url']; 1470 } 1471 1472 $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url; 1473 $this->cachedValues['privateurl'][$calid] = $url; 1474 return $url; 1475 } 1476 1477 /** 1478 * Retrieve the calendar ID for a given private calendar URL 1479 * 1480 * @param string $url The private URL 1481 * 1482 * @return mixed The calendar ID or false 1483 */ 1484 public function getCalendarForPrivateURL($url) 1485 { 1486 $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?"; 1487 $res = $this->sqlite->query($query, $url); 1488 $row = $this->sqlite->res2row($res); 1489 if(!isset($row['calid'])) 1490 return false; 1491 return $row['calid']; 1492 } 1493 1494 /** 1495 * Return a given calendar as ICS feed, i.e. all events in one ICS file. 1496 * 1497 * @param string $calid The calendar ID to retrieve 1498 * 1499 * @return mixed The calendar events as string or false 1500 */ 1501 public function getCalendarAsICSFeed($calid) 1502 { 1503 $calSettings = $this->getCalendarSettings($calid); 1504 if($calSettings === false) 1505 return false; 1506 $events = $this->getAllCalendarEvents($calid); 1507 if($events === false) 1508 return false; 1509 1510 // Load SabreDAV 1511 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1512 $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:"; 1513 $out .= $calSettings['displayname']."\r\n"; 1514 foreach($events as $event) 1515 { 1516 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1517 $evt = $vcal->VEVENT; 1518 $out .= $evt->serialize(); 1519 } 1520 $out .= "END:VCALENDAR\r\n"; 1521 return $out; 1522 } 1523 1524 /** 1525 * Retrieve a configuration option for the plugin 1526 * 1527 * @param string $key The key to query 1528 * @return mixed The option set, null if not found 1529 */ 1530 public function getConfig($key) 1531 { 1532 return $this->getConf($key); 1533 } 1534 1535 /** 1536 * Parses some information from calendar objects, used for optimized 1537 * calendar-queries. Taken nearly unmodified from Sabre's PDO backend 1538 * 1539 * Returns an array with the following keys: 1540 * * etag - An md5 checksum of the object without the quotes. 1541 * * size - Size of the object in bytes 1542 * * componentType - VEVENT, VTODO or VJOURNAL 1543 * * firstOccurence 1544 * * lastOccurence 1545 * * uid - value of the UID property 1546 * 1547 * @param string $calendarData 1548 * @return array 1549 */ 1550 protected function getDenormalizedData($calendarData) 1551 { 1552 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1553 1554 $vObject = \Sabre\VObject\Reader::read($calendarData); 1555 $componentType = null; 1556 $component = null; 1557 $firstOccurence = null; 1558 $lastOccurence = null; 1559 $uid = null; 1560 foreach ($vObject->getComponents() as $component) 1561 { 1562 if ($component->name !== 'VTIMEZONE') 1563 { 1564 $componentType = $component->name; 1565 $uid = (string)$component->UID; 1566 break; 1567 } 1568 } 1569 if (!$componentType) 1570 { 1571 return false; 1572 } 1573 if ($componentType === 'VEVENT') 1574 { 1575 $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); 1576 // Finding the last occurence is a bit harder 1577 if (!isset($component->RRULE)) 1578 { 1579 if (isset($component->DTEND)) 1580 { 1581 $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); 1582 } 1583 elseif (isset($component->DURATION)) 1584 { 1585 $endDate = clone $component->DTSTART->getDateTime(); 1586 $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue())); 1587 $lastOccurence = $endDate->getTimeStamp(); 1588 } 1589 elseif (!$component->DTSTART->hasTime()) 1590 { 1591 $endDate = clone $component->DTSTART->getDateTime(); 1592 $endDate->modify('+1 day'); 1593 $lastOccurence = $endDate->getTimeStamp(); 1594 } 1595 else 1596 { 1597 $lastOccurence = $firstOccurence; 1598 } 1599 } 1600 else 1601 { 1602 $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID); 1603 $maxDate = new \DateTime('2038-01-01'); 1604 if ($it->isInfinite()) 1605 { 1606 $lastOccurence = $maxDate->getTimeStamp(); 1607 } 1608 else 1609 { 1610 $end = $it->getDtEnd(); 1611 while ($it->valid() && $end < $maxDate) 1612 { 1613 $end = $it->getDtEnd(); 1614 $it->next(); 1615 } 1616 $lastOccurence = $end->getTimeStamp(); 1617 } 1618 } 1619 } 1620 1621 return array( 1622 'etag' => md5($calendarData), 1623 'size' => strlen($calendarData), 1624 'componentType' => $componentType, 1625 'firstOccurence' => $firstOccurence, 1626 'lastOccurence' => $lastOccurence, 1627 'uid' => $uid, 1628 ); 1629 1630 } 1631 1632 /** 1633 * Query a calendar by ID and taking several filters into account. 1634 * This is heavily based on Sabre's PDO backend. 1635 * 1636 * @param int $calendarId The calendar's ID 1637 * @param array $filters The filter array to apply 1638 * 1639 * @return mixed The result 1640 */ 1641 public function calendarQuery($calendarId, $filters) 1642 { 1643 dbglog('davcal::helper::calendarQuery'); 1644 $componentType = null; 1645 $requirePostFilter = true; 1646 $timeRange = null; 1647 1648 // if no filters were specified, we don't need to filter after a query 1649 if (!$filters['prop-filters'] && !$filters['comp-filters']) 1650 { 1651 $requirePostFilter = false; 1652 } 1653 1654 // Figuring out if there's a component filter 1655 if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) 1656 { 1657 $componentType = $filters['comp-filters'][0]['name']; 1658 1659 // Checking if we need post-filters 1660 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) 1661 { 1662 $requirePostFilter = false; 1663 } 1664 // There was a time-range filter 1665 if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) 1666 { 1667 $timeRange = $filters['comp-filters'][0]['time-range']; 1668 1669 // If start time OR the end time is not specified, we can do a 1670 // 100% accurate mysql query. 1671 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) 1672 { 1673 $requirePostFilter = false; 1674 } 1675 } 1676 1677 } 1678 1679 if ($requirePostFilter) 1680 { 1681 $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?"; 1682 } 1683 else 1684 { 1685 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1686 } 1687 1688 $values = array( 1689 $calendarId 1690 ); 1691 1692 if ($componentType) 1693 { 1694 $query .= " AND componenttype = ?"; 1695 $values[] = $componentType; 1696 } 1697 1698 if ($timeRange && $timeRange['start']) 1699 { 1700 $query .= " AND lastoccurence > ?"; 1701 $values[] = $timeRange['start']->getTimeStamp(); 1702 } 1703 if ($timeRange && $timeRange['end']) 1704 { 1705 $query .= " AND firstoccurence < ?"; 1706 $values[] = $timeRange['end']->getTimeStamp(); 1707 } 1708 1709 $res = $this->sqlite->query($query, $values); 1710 $arr = $this->sqlite->res2arr($res); 1711 1712 $result = array(); 1713 foreach($arr as $row) 1714 { 1715 if ($requirePostFilter) 1716 { 1717 if (!$this->validateFilterForObject($row, $filters)) 1718 { 1719 continue; 1720 } 1721 } 1722 $result[] = $row['uri']; 1723 1724 } 1725 1726 return $result; 1727 } 1728 1729 /** 1730 * This method validates if a filter (as passed to calendarQuery) matches 1731 * the given object. Taken from Sabre's PDO backend 1732 * 1733 * @param array $object 1734 * @param array $filters 1735 * @return bool 1736 */ 1737 protected function validateFilterForObject($object, $filters) 1738 { 1739 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1740 // Unfortunately, setting the 'calendardata' here is optional. If 1741 // it was excluded, we actually need another call to get this as 1742 // well. 1743 if (!isset($object['calendardata'])) 1744 { 1745 $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']); 1746 } 1747 1748 $vObject = \Sabre\VObject\Reader::read($object['calendardata']); 1749 $validator = new \Sabre\CalDAV\CalendarQueryValidator(); 1750 1751 $res = $validator->validate($vObject, $filters); 1752 return $res; 1753 1754 } 1755 1756 /** 1757 * Retrieve changes for a given calendar based on the given syncToken. 1758 * 1759 * @param int $calid The calendar's ID 1760 * @param int $syncToken The supplied sync token 1761 * @param int $syncLevel The sync level 1762 * @param int $limit The limit of changes 1763 * 1764 * @return array The result 1765 */ 1766 public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null) 1767 { 1768 // Current synctoken 1769 $currentToken = $this->getSyncTokenForCalendar($calid); 1770 1771 if ($currentToken === false) return null; 1772 1773 $result = array( 1774 'syncToken' => $currentToken, 1775 'added' => array(), 1776 'modified' => array(), 1777 'deleted' => array(), 1778 ); 1779 1780 if ($syncToken) 1781 { 1782 1783 $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; 1784 if ($limit > 0) $query .= " LIMIT " . (int)$limit; 1785 1786 // Fetching all changes 1787 $res = $this->sqlite->query($query, $syncToken, $currentToken, $calid); 1788 if($res === false) 1789 return null; 1790 1791 $arr = $this->sqlite->res2arr($res); 1792 $changes = array(); 1793 1794 // This loop ensures that any duplicates are overwritten, only the 1795 // last change on a node is relevant. 1796 foreach($arr as $row) 1797 { 1798 $changes[$row['uri']] = $row['operation']; 1799 } 1800 1801 foreach ($changes as $uri => $operation) 1802 { 1803 switch ($operation) 1804 { 1805 case 1 : 1806 $result['added'][] = $uri; 1807 break; 1808 case 2 : 1809 $result['modified'][] = $uri; 1810 break; 1811 case 3 : 1812 $result['deleted'][] = $uri; 1813 break; 1814 } 1815 1816 } 1817 } 1818 else 1819 { 1820 // No synctoken supplied, this is the initial sync. 1821 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1822 $res = $this->sqlite->query($query); 1823 $arr = $this->sqlite->res2arr($res); 1824 1825 $result['added'] = $arr; 1826 } 1827 return $result; 1828 } 1829 1830} 1831