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 $enabled = $this->getCalendarStatus($calid); 949 if($enabled === false) 950 return $data; 951 952 // Retrieve matching calendar objects 953 $res = $this->sqlite->query($query, $calid); 954 $arr = $this->sqlite->res2arr($res); 955 } 956 957 // Parse individual calendar entries 958 foreach($arr as $row) 959 { 960 if(isset($row['calendardata'])) 961 { 962 $entry = array(); 963 $vcal = \Sabre\VObject\Reader::read($row['calendardata']); 964 $recurrence = $vcal->VEVENT->RRULE; 965 // If it is a recurring event, pass it through Sabre's EventIterator 966 if($recurrence != null) 967 { 968 $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT)); 969 $rEvents->rewind(); 970 while($rEvents->valid()) 971 { 972 $event = $rEvents->getEventObject(); 973 // If we are after the given time range, exit 974 if(($endTs !== null) && ($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp())) 975 break; 976 977 // If we are before the given time range, continue 978 if(($startTs != null) && ($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp())) 979 { 980 $rEvents->next(); 981 continue; 982 } 983 984 // If we are within the given time range, parse the event 985 $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true); 986 $rEvents->next(); 987 } 988 } 989 else 990 $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color); 991 } 992 } 993 return $data; 994 } 995 996 /** 997 * Helper function that parses the iCal data of a VEVENT to a calendar entry. 998 * 999 * @param \Sabre\VObject\VEvent $event The event to parse 1000 * @param \DateTimeZone $timezone The timezone object 1001 * @param string $uid The entry's UID 1002 * @param boolean $recurring (optional) Set to true to define a recurring event 1003 * 1004 * @return array The parse calendar entry 1005 */ 1006 private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false) 1007 { 1008 $entry = array(); 1009 $start = $event->DTSTART; 1010 // Parse only if the start date/time is present 1011 if($start !== null) 1012 { 1013 $dtStart = $start->getDateTime(); 1014 $dtStart->setTimezone($timezone); 1015 1016 // moment.js doesn't like times be given even if 1017 // allDay is set to true 1018 // This should fix T23 1019 if($start['VALUE'] == 'DATE') 1020 { 1021 $entry['allDay'] = true; 1022 $entry['start'] = $dtStart->format("Y-m-d"); 1023 } 1024 else 1025 { 1026 $entry['allDay'] = false; 1027 $entry['start'] = $dtStart->format(\DateTime::ATOM); 1028 } 1029 } 1030 $end = $event->DTEND; 1031 // Parse only if the end date/time is present 1032 if($end !== null) 1033 { 1034 $dtEnd = $end->getDateTime(); 1035 $dtEnd->setTimezone($timezone); 1036 if($end['VALUE'] == 'DATE') 1037 $entry['end'] = $dtEnd->format("Y-m-d"); 1038 else 1039 $entry['end'] = $dtEnd->format(\DateTime::ATOM); 1040 } 1041 $description = $event->DESCRIPTION; 1042 if($description !== null) 1043 $entry['description'] = (string)$description; 1044 else 1045 $entry['description'] = ''; 1046 $attachments = $event->ATTACH; 1047 if($attachments !== null) 1048 { 1049 $entry['attachments'] = array(); 1050 foreach($attachments as $attachment) 1051 $entry['attachments'][] = (string)$attachment; 1052 } 1053 $entry['title'] = (string)$event->summary; 1054 $entry['location'] = (string)$event->location; 1055 $entry['id'] = $uid; 1056 $entry['page'] = $page; 1057 $entry['color'] = $color; 1058 $entry['recurring'] = $recurring; 1059 1060 return $entry; 1061 } 1062 1063 /** 1064 * Retrieve an event by its UID 1065 * 1066 * @param string $uid The event's UID 1067 * 1068 * @return mixed The table row with the given event 1069 */ 1070 public function getEventWithUid($uid) 1071 { 1072 $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?"; 1073 $res = $this->sqlite->query($query, $uid); 1074 $row = $this->sqlite->res2row($res); 1075 return $row; 1076 } 1077 1078 /** 1079 * Retrieve information of a calendar's object, not including the actual 1080 * calendar data! This is mainly needed for the sync support. 1081 * 1082 * @param int $calid The calendar ID 1083 * 1084 * @return mixed The result 1085 */ 1086 public function getCalendarObjects($calid) 1087 { 1088 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM calendarobjects WHERE calendarid = ?"; 1089 $res = $this->sqlite->query($query, $calid); 1090 $arr = $this->sqlite->res2arr($res); 1091 return $arr; 1092 } 1093 1094 /** 1095 * Retrieve a single calendar object by calendar ID and URI 1096 * 1097 * @param int $calid The calendar's ID 1098 * @param string $uri The object's URI 1099 * 1100 * @return mixed The result 1101 */ 1102 public function getCalendarObjectByUri($calid, $uri) 1103 { 1104 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1105 $res = $this->sqlite->query($query, $calid, $uri); 1106 $row = $this->sqlite->res2row($res); 1107 return $row; 1108 } 1109 1110 /** 1111 * Retrieve several calendar objects by specifying an array of URIs. 1112 * This is mainly neede for sync. 1113 * 1114 * @param int $calid The calendar's ID 1115 * @param array $uris An array of URIs 1116 * 1117 * @return mixed The result 1118 */ 1119 public function getMultipleCalendarObjectsByUri($calid, $uris) 1120 { 1121 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri IN ("; 1122 // Inserting a whole bunch of question marks 1123 $query .= implode(',', array_fill(0, count($uris), '?')); 1124 $query .= ')'; 1125 $vals = array_merge(array($calid), $uris); 1126 1127 $res = $this->sqlite->query($query, $vals); 1128 $arr = $this->sqlite->res2arr($res); 1129 return $arr; 1130 } 1131 1132 /** 1133 * Retrieve all calendar events for a given calendar ID 1134 * 1135 * @param string $calid The calendar's ID 1136 * 1137 * @return array An array containing all calendar data 1138 */ 1139 public function getAllCalendarEvents($calid) 1140 { 1141 $enabled = $this->getCalendarStatus($calid); 1142 if($enabled === false) 1143 return false; 1144 $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?"; 1145 $res = $this->sqlite->query($query, $calid); 1146 $arr = $this->sqlite->res2arr($res); 1147 return $arr; 1148 } 1149 1150 /** 1151 * Edit a calendar entry for a page, given by its parameters. 1152 * The params array has the same format as @see addCalendarEntryForPage 1153 * 1154 * @param string $id The page's ID to work on 1155 * @param string $user The user's ID to work on 1156 * @param array $params The parameter array for the edited calendar event 1157 * 1158 * @return boolean True on success, otherwise false 1159 */ 1160 public function editCalendarEntryForPage($id, $user, $params) 1161 { 1162 if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') 1163 $timezone = new \DateTimeZone($params['currenttz']); 1164 elseif($params['currenttz'] === 'local') 1165 $timezone = new \DateTimeZone($params['detectedtz']); 1166 else 1167 $timezone = new \DateTimeZone('UTC'); 1168 1169 // Parse dates 1170 $startDate = explode('-', $params['eventfrom']); 1171 $startTime = explode(':', $params['eventfromtime']); 1172 $endDate = explode('-', $params['eventto']); 1173 $endTime = explode(':', $params['eventtotime']); 1174 1175 // Retrieve the existing event based on the UID 1176 $uid = $params['uid']; 1177 1178 if(strpos($id, 'webdav://') === 0) 1179 { 1180 $wdc =& plugin_load('helper', 'webdavclient'); 1181 if(is_null($wdc)) 1182 return false; 1183 $event = $wdc->getCalendarEntryByUid($uid); 1184 } 1185 else 1186 { 1187 $event = $this->getEventWithUid($uid); 1188 } 1189 1190 // Load SabreDAV 1191 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1192 if(!isset($event['calendardata'])) 1193 return false; 1194 $uri = $event['uri']; 1195 $calid = $event['calendarid']; 1196 1197 // Parse the existing event 1198 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1199 $vevent = $vcal->VEVENT; 1200 1201 // Set the new event values 1202 $vevent->summary = $params['eventname']; 1203 $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); 1204 $description = $params['eventdescription']; 1205 $location = $params['eventlocation']; 1206 1207 // Remove existing timestamps to overwrite them 1208 $vevent->remove('DESCRIPTION'); 1209 $vevent->remove('DTSTAMP'); 1210 $vevent->remove('LAST-MODIFIED'); 1211 $vevent->remove('ATTACH'); 1212 $vevent->remove('LOCATION'); 1213 1214 // Add new time stamps, description and location 1215 $vevent->add('DTSTAMP', $dtStamp); 1216 $vevent->add('LAST-MODIFIED', $dtStamp); 1217 if($description !== '') 1218 $vevent->add('DESCRIPTION', $description); 1219 if($location !== '') 1220 $vevent->add('LOCATION', $location); 1221 1222 // Add attachments 1223 $attachments = $params['attachments']; 1224 if(!is_null($attachments)) 1225 foreach($attachments as $attachment) 1226 $vevent->add('ATTACH', $attachment); 1227 1228 // Setup DTSTART 1229 $dtStart = new \DateTime(); 1230 $dtStart->setTimezone($timezone); 1231 $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); 1232 if($params['allday'] != '1') 1233 $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); 1234 1235 // Setup DTEND 1236 $dtEnd = new \DateTime(); 1237 $dtEnd->setTimezone($timezone); 1238 $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); 1239 if($params['allday'] != '1') 1240 $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); 1241 1242 // According to the VCal spec, we need to add a whole day here 1243 if($params['allday'] == '1') 1244 $dtEnd->add(new \DateInterval('P1D')); 1245 $vevent->remove('DTSTART'); 1246 $vevent->remove('DTEND'); 1247 $dtStartEv = $vevent->add('DTSTART', $dtStart); 1248 $dtEndEv = $vevent->add('DTEND', $dtEnd); 1249 1250 // Remove the time for allday events 1251 if($params['allday'] == '1') 1252 { 1253 $dtStartEv['VALUE'] = 'DATE'; 1254 $dtEndEv['VALUE'] = 'DATE'; 1255 } 1256 $eventStr = $vcal->serialize(); 1257 if(strpos($id, 'webdav://') === 0) 1258 { 1259 $connectionId = str_replace('webdav://', '', $id); 1260 return $wdc->editCalendarEntry($connectionId, $uid, $eventStr); 1261 } 1262 else 1263 { 1264 $now = new DateTime(); 1265 // Actually write to the database 1266 $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ". 1267 "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?"; 1268 $res = $this->sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(), 1269 $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid); 1270 if($res !== false) 1271 { 1272 $this->updateSyncTokenLog($calid, $uri, 'modified'); 1273 return true; 1274 } 1275 } 1276 return false; 1277 } 1278 1279 /** 1280 * Delete an event from a calendar by calendar ID and URI 1281 * 1282 * @param int $calid The calendar's ID 1283 * @param string $uri The object's URI 1284 * 1285 * @return true 1286 */ 1287 public function deleteCalendarEntryForCalendarByUri($calid, $uri) 1288 { 1289 $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1290 $res = $this->sqlite->query($query, $calid, $uri); 1291 if($res !== false) 1292 { 1293 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1294 } 1295 return true; 1296 } 1297 1298 /** 1299 * Delete a calendar entry for a given page. Actually, the event is removed 1300 * based on the entry's UID, so that page ID is no used. 1301 * 1302 * @param string $id The page's ID (unused) 1303 * @param array $params The parameter array to work with 1304 * 1305 * @return boolean True 1306 */ 1307 public function deleteCalendarEntryForPage($id, $params) 1308 { 1309 $uid = $params['uid']; 1310 if(strpos($id, 'webdav://') === 0) 1311 { 1312 $wdc =& plugin_load('helper', 'webdavclient'); 1313 if(is_null($wdc)) 1314 return false; 1315 $connectionId = str_replace('webdav://', '', $id); 1316 $result = $wdc->deleteCalendarEntry($connectionId, $uid); 1317 return $result; 1318 } 1319 $event = $this->getEventWithUid($uid); 1320 $calid = $event['calendarid']; 1321 $uri = $event['uri']; 1322 $query = "DELETE FROM calendarobjects WHERE uid = ?"; 1323 $res = $this->sqlite->query($query, $uid); 1324 if($res !== false) 1325 { 1326 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1327 } 1328 return true; 1329 } 1330 1331 /** 1332 * Retrieve the current sync token for a calendar 1333 * 1334 * @param string $calid The calendar id 1335 * 1336 * @return mixed The synctoken or false 1337 */ 1338 public function getSyncTokenForCalendar($calid) 1339 { 1340 $row = $this->getCalendarSettings($calid); 1341 if(isset($row['synctoken'])) 1342 return $row['synctoken']; 1343 return false; 1344 } 1345 1346 /** 1347 * Helper function to convert the operation name to 1348 * an operation code as stored in the database 1349 * 1350 * @param string $operationName The operation name 1351 * 1352 * @return mixed The operation code or false 1353 */ 1354 public function operationNameToOperation($operationName) 1355 { 1356 switch($operationName) 1357 { 1358 case 'added': 1359 return 1; 1360 break; 1361 case 'modified': 1362 return 2; 1363 break; 1364 case 'deleted': 1365 return 3; 1366 break; 1367 } 1368 return false; 1369 } 1370 1371 /** 1372 * Update the sync token log based on the calendar id and the 1373 * operation that was performed. 1374 * 1375 * @param string $calid The calendar ID that was modified 1376 * @param string $uri The calendar URI that was modified 1377 * @param string $operation The operation that was performed 1378 * 1379 * @return boolean True on success, otherwise false 1380 */ 1381 private function updateSyncTokenLog($calid, $uri, $operation) 1382 { 1383 $currentToken = $this->getSyncTokenForCalendar($calid); 1384 $operationCode = $this->operationNameToOperation($operation); 1385 if(($operationCode === false) || ($currentToken === false)) 1386 return false; 1387 $values = array($uri, 1388 $currentToken, 1389 $calid, 1390 $operationCode 1391 ); 1392 $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)"; 1393 $res = $this->sqlite->query($query, $uri, $currentToken, $calid, $operationCode); 1394 if($res === false) 1395 return false; 1396 $currentToken++; 1397 $query = "UPDATE calendars SET synctoken = ? WHERE id = ?"; 1398 $res = $this->sqlite->query($query, $currentToken, $calid); 1399 return ($res !== false); 1400 } 1401 1402 /** 1403 * Return the sync URL for a given Page, i.e. a calendar 1404 * 1405 * @param string $id The page's ID 1406 * @param string $user (optional) The user's ID 1407 * 1408 * @return mixed The sync url or false 1409 */ 1410 public function getSyncUrlForPage($id, $user = null) 1411 { 1412 if(is_null($userid)) 1413 { 1414 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 1415 { 1416 $userid = $_SERVER['REMOTE_USER']; 1417 } 1418 else 1419 { 1420 return false; 1421 } 1422 } 1423 1424 $calid = $this->getCalendarIdForPage($id); 1425 if($calid === false) 1426 return false; 1427 1428 $calsettings = $this->getCalendarSettings($calid); 1429 if(!isset($calsettings['uri'])) 1430 return false; 1431 1432 $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri']; 1433 return $syncurl; 1434 } 1435 1436 /** 1437 * Return the private calendar's URL for a given page 1438 * 1439 * @param string $id the page ID 1440 * 1441 * @return mixed The private URL or false 1442 */ 1443 public function getPrivateURLForPage($id) 1444 { 1445 $calid = $this->getCalendarIdForPage($id); 1446 if($calid === false) 1447 return false; 1448 1449 return $this->getPrivateURLForCalendar($calid); 1450 } 1451 1452 /** 1453 * Return the private calendar's URL for a given calendar ID 1454 * 1455 * @param string $calid The calendar's ID 1456 * 1457 * @return mixed The private URL or false 1458 */ 1459 public function getPrivateURLForCalendar($calid) 1460 { 1461 if(isset($this->cachedValues['privateurl'][$calid])) 1462 return $this->cachedValues['privateurl'][$calid]; 1463 $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?"; 1464 $res = $this->sqlite->query($query, $calid); 1465 $row = $this->sqlite->res2row($res); 1466 if(!isset($row['url'])) 1467 { 1468 $url = uniqid("dokuwiki-").".ics"; 1469 $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)"; 1470 $res = $this->sqlite->query($query, $url, $calid); 1471 if($res === false) 1472 return false; 1473 } 1474 else 1475 { 1476 $url = $row['url']; 1477 } 1478 1479 $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url; 1480 $this->cachedValues['privateurl'][$calid] = $url; 1481 return $url; 1482 } 1483 1484 /** 1485 * Retrieve the calendar ID for a given private calendar URL 1486 * 1487 * @param string $url The private URL 1488 * 1489 * @return mixed The calendar ID or false 1490 */ 1491 public function getCalendarForPrivateURL($url) 1492 { 1493 $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?"; 1494 $res = $this->sqlite->query($query, $url); 1495 $row = $this->sqlite->res2row($res); 1496 if(!isset($row['calid'])) 1497 return false; 1498 return $row['calid']; 1499 } 1500 1501 /** 1502 * Return a given calendar as ICS feed, i.e. all events in one ICS file. 1503 * 1504 * @param string $calid The calendar ID to retrieve 1505 * 1506 * @return mixed The calendar events as string or false 1507 */ 1508 public function getCalendarAsICSFeed($calid) 1509 { 1510 $calSettings = $this->getCalendarSettings($calid); 1511 if($calSettings === false) 1512 return false; 1513 $events = $this->getAllCalendarEvents($calid); 1514 if($events === false) 1515 return false; 1516 1517 // Load SabreDAV 1518 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1519 $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:"; 1520 $out .= $calSettings['displayname']."\r\n"; 1521 foreach($events as $event) 1522 { 1523 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1524 $evt = $vcal->VEVENT; 1525 $out .= $evt->serialize(); 1526 } 1527 $out .= "END:VCALENDAR\r\n"; 1528 return $out; 1529 } 1530 1531 /** 1532 * Retrieve a configuration option for the plugin 1533 * 1534 * @param string $key The key to query 1535 * @return mixed The option set, null if not found 1536 */ 1537 public function getConfig($key) 1538 { 1539 return $this->getConf($key); 1540 } 1541 1542 /** 1543 * Parses some information from calendar objects, used for optimized 1544 * calendar-queries. Taken nearly unmodified from Sabre's PDO backend 1545 * 1546 * Returns an array with the following keys: 1547 * * etag - An md5 checksum of the object without the quotes. 1548 * * size - Size of the object in bytes 1549 * * componentType - VEVENT, VTODO or VJOURNAL 1550 * * firstOccurence 1551 * * lastOccurence 1552 * * uid - value of the UID property 1553 * 1554 * @param string $calendarData 1555 * @return array 1556 */ 1557 protected function getDenormalizedData($calendarData) 1558 { 1559 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1560 1561 $vObject = \Sabre\VObject\Reader::read($calendarData); 1562 $componentType = null; 1563 $component = null; 1564 $firstOccurence = null; 1565 $lastOccurence = null; 1566 $uid = null; 1567 foreach ($vObject->getComponents() as $component) 1568 { 1569 if ($component->name !== 'VTIMEZONE') 1570 { 1571 $componentType = $component->name; 1572 $uid = (string)$component->UID; 1573 break; 1574 } 1575 } 1576 if (!$componentType) 1577 { 1578 return false; 1579 } 1580 if ($componentType === 'VEVENT') 1581 { 1582 $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); 1583 // Finding the last occurence is a bit harder 1584 if (!isset($component->RRULE)) 1585 { 1586 if (isset($component->DTEND)) 1587 { 1588 $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); 1589 } 1590 elseif (isset($component->DURATION)) 1591 { 1592 $endDate = clone $component->DTSTART->getDateTime(); 1593 $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue())); 1594 $lastOccurence = $endDate->getTimeStamp(); 1595 } 1596 elseif (!$component->DTSTART->hasTime()) 1597 { 1598 $endDate = clone $component->DTSTART->getDateTime(); 1599 $endDate->modify('+1 day'); 1600 $lastOccurence = $endDate->getTimeStamp(); 1601 } 1602 else 1603 { 1604 $lastOccurence = $firstOccurence; 1605 } 1606 } 1607 else 1608 { 1609 $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID); 1610 $maxDate = new \DateTime('2038-01-01'); 1611 if ($it->isInfinite()) 1612 { 1613 $lastOccurence = $maxDate->getTimeStamp(); 1614 } 1615 else 1616 { 1617 $end = $it->getDtEnd(); 1618 while ($it->valid() && $end < $maxDate) 1619 { 1620 $end = $it->getDtEnd(); 1621 $it->next(); 1622 } 1623 $lastOccurence = $end->getTimeStamp(); 1624 } 1625 } 1626 } 1627 1628 return array( 1629 'etag' => md5($calendarData), 1630 'size' => strlen($calendarData), 1631 'componentType' => $componentType, 1632 'firstOccurence' => $firstOccurence, 1633 'lastOccurence' => $lastOccurence, 1634 'uid' => $uid, 1635 ); 1636 1637 } 1638 1639 /** 1640 * Query a calendar by ID and taking several filters into account. 1641 * This is heavily based on Sabre's PDO backend. 1642 * 1643 * @param int $calendarId The calendar's ID 1644 * @param array $filters The filter array to apply 1645 * 1646 * @return mixed The result 1647 */ 1648 public function calendarQuery($calendarId, $filters) 1649 { 1650 dbglog('davcal::helper::calendarQuery'); 1651 $componentType = null; 1652 $requirePostFilter = true; 1653 $timeRange = null; 1654 1655 // if no filters were specified, we don't need to filter after a query 1656 if (!$filters['prop-filters'] && !$filters['comp-filters']) 1657 { 1658 $requirePostFilter = false; 1659 } 1660 1661 // Figuring out if there's a component filter 1662 if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) 1663 { 1664 $componentType = $filters['comp-filters'][0]['name']; 1665 1666 // Checking if we need post-filters 1667 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) 1668 { 1669 $requirePostFilter = false; 1670 } 1671 // There was a time-range filter 1672 if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) 1673 { 1674 $timeRange = $filters['comp-filters'][0]['time-range']; 1675 1676 // If start time OR the end time is not specified, we can do a 1677 // 100% accurate mysql query. 1678 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) 1679 { 1680 $requirePostFilter = false; 1681 } 1682 } 1683 1684 } 1685 1686 if ($requirePostFilter) 1687 { 1688 $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?"; 1689 } 1690 else 1691 { 1692 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1693 } 1694 1695 $values = array( 1696 $calendarId 1697 ); 1698 1699 if ($componentType) 1700 { 1701 $query .= " AND componenttype = ?"; 1702 $values[] = $componentType; 1703 } 1704 1705 if ($timeRange && $timeRange['start']) 1706 { 1707 $query .= " AND lastoccurence > ?"; 1708 $values[] = $timeRange['start']->getTimeStamp(); 1709 } 1710 if ($timeRange && $timeRange['end']) 1711 { 1712 $query .= " AND firstoccurence < ?"; 1713 $values[] = $timeRange['end']->getTimeStamp(); 1714 } 1715 1716 $res = $this->sqlite->query($query, $values); 1717 $arr = $this->sqlite->res2arr($res); 1718 1719 $result = array(); 1720 foreach($arr as $row) 1721 { 1722 if ($requirePostFilter) 1723 { 1724 if (!$this->validateFilterForObject($row, $filters)) 1725 { 1726 continue; 1727 } 1728 } 1729 $result[] = $row['uri']; 1730 1731 } 1732 1733 return $result; 1734 } 1735 1736 /** 1737 * This method validates if a filter (as passed to calendarQuery) matches 1738 * the given object. Taken from Sabre's PDO backend 1739 * 1740 * @param array $object 1741 * @param array $filters 1742 * @return bool 1743 */ 1744 protected function validateFilterForObject($object, $filters) 1745 { 1746 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1747 // Unfortunately, setting the 'calendardata' here is optional. If 1748 // it was excluded, we actually need another call to get this as 1749 // well. 1750 if (!isset($object['calendardata'])) 1751 { 1752 $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']); 1753 } 1754 1755 $vObject = \Sabre\VObject\Reader::read($object['calendardata']); 1756 $validator = new \Sabre\CalDAV\CalendarQueryValidator(); 1757 1758 $res = $validator->validate($vObject, $filters); 1759 return $res; 1760 1761 } 1762 1763 /** 1764 * Retrieve changes for a given calendar based on the given syncToken. 1765 * 1766 * @param int $calid The calendar's ID 1767 * @param int $syncToken The supplied sync token 1768 * @param int $syncLevel The sync level 1769 * @param int $limit The limit of changes 1770 * 1771 * @return array The result 1772 */ 1773 public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null) 1774 { 1775 // Current synctoken 1776 $currentToken = $this->getSyncTokenForCalendar($calid); 1777 1778 if ($currentToken === false) return null; 1779 1780 $result = array( 1781 'syncToken' => $currentToken, 1782 'added' => array(), 1783 'modified' => array(), 1784 'deleted' => array(), 1785 ); 1786 1787 if ($syncToken) 1788 { 1789 1790 $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; 1791 if ($limit > 0) $query .= " LIMIT " . (int)$limit; 1792 1793 // Fetching all changes 1794 $res = $this->sqlite->query($query, $syncToken, $currentToken, $calid); 1795 if($res === false) 1796 return null; 1797 1798 $arr = $this->sqlite->res2arr($res); 1799 $changes = array(); 1800 1801 // This loop ensures that any duplicates are overwritten, only the 1802 // last change on a node is relevant. 1803 foreach($arr as $row) 1804 { 1805 $changes[$row['uri']] = $row['operation']; 1806 } 1807 1808 foreach ($changes as $uri => $operation) 1809 { 1810 switch ($operation) 1811 { 1812 case 1 : 1813 $result['added'][] = $uri; 1814 break; 1815 case 2 : 1816 $result['modified'][] = $uri; 1817 break; 1818 case 3 : 1819 $result['deleted'][] = $uri; 1820 break; 1821 } 1822 1823 } 1824 } 1825 else 1826 { 1827 // No synctoken supplied, this is the initial sync. 1828 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1829 $res = $this->sqlite->query($query); 1830 $arr = $this->sqlite->res2arr($res); 1831 1832 $result['added'] = $arr; 1833 } 1834 return $result; 1835 } 1836 1837} 1838