1<?php 2/** 3 * Helper Class for the webdavclient plugin 4 * This helper does the actual work. 5 * 6 * Configurable in DokuWiki's configuration 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12class helper_plugin_webdavclient extends DokuWiki_Plugin { 13 14 protected $sqlite = null; 15 protected $client = null; 16 protected $client_headers = array(); 17 protected $lastErr = ''; 18 protected $syncChangeLogFile; 19 20 /** 21 * Constructor to load the configuration 22 */ 23 public function helper_plugin_webdavclient() { 24 global $conf; 25 26 $this->syncChangeLogFile = $conf['metadir'].'/.webdavclient/synclog'; 27 28 $this->client = new DokuHTTPClient(); 29 $client_headers = $this->client->headers; 30 } 31 32 /** Establish and initialize the database if not already done 33 * @return sqlite interface or false 34 */ 35 private function getDB() 36 { 37 if($this->sqlite === null) 38 { 39 $this->sqlite = plugin_load('helper', 'sqlite'); 40 if(!$this->sqlite) 41 { 42 dbglog('This plugin requires the sqlite plugin. Please install it.'); 43 msg('This plugin requires the sqlite plugin. Please install it.', -1); 44 return false; 45 } 46 if(!$this->sqlite->init('webdavclient', DOKU_PLUGIN.'webdavclient/db/')) 47 { 48 $this->sqlite = null; 49 dbglog('Error initialising the SQLite DB for webdavclient'); 50 return false; 51 } 52 } 53 return $this->sqlite; 54 } 55 56 /** 57 * Get the last error message, if any 58 * 59 * @return string The last error message 60 */ 61 public function getLastError() 62 { 63 return $this->lastErr; 64 } 65 66 /** 67 * Add a new calendar entry to a given connection ID 68 * 69 * @param int $connectionId The connection ID to work with 70 * @param string $data The new calendar entry (ICS file) 71 * @param string $dwuser (Optional) The DokuWiki user 72 * 73 * @return True on success, otherwise false 74 */ 75 public function addCalendarEntry($connectionId, $data, $dwuser = null) 76 { 77 $conn = $this->getConnection($connectionId); 78 if($conn === false) 79 return false; 80 $conn = array_merge($conn, $this->getCredentials($connectionId)); 81 $this->setupClient($conn, strlen($data), null, 'text/calendar; charset=utf-8'); 82 $path = $conn['uri'].'/'.uniqid('dokuwiki-').'.ics'; 83 $resp = $this->client->sendRequest($path, $data, 'PUT'); 84 if($this->client->status == 201) 85 { 86 $this->syncConnection($conn['id'], true); 87 return true; 88 } 89 $this->lastErr = 'Error adding calendar entry, server reported status '.$this->client->status; 90 return false; 91 } 92 93 /** 94 * Edit a calendar entry for a given connection ID 95 * 96 * @param int $connectionId The connection ID to work with 97 * @param string $uid The event's UID as stored internally 98 * @param string $dwuser (Optional) The DokuWiki user 99 * 100 * @return True on success, otherwise false 101 */ 102 public function editCalendarEntry($connectionId, $uid, $data, $dwuser = null) 103 { 104 $conn = $this->getConnection($connectionId); 105 if($conn === false) 106 return false; 107 $conn = array_merge($conn, $this->getCredentials($connectionId)); 108 $entry = $this->getCalendarEntryByUid($uid); 109 $etag = '"'.$entry['etag'].'"'; 110 $this->setupClient($conn, strlen($data), null, 'text/calendar; charset=utf-8', array('If-Match' => $etag)); 111 $path = $conn['uri'].'/'.$entry['uri']; 112 $resp = $this->client->sendRequest($path, $data, 'PUT'); 113 if($this->client->status == 204) 114 { 115 $this->syncConnection($conn['id'], true); 116 return true; 117 } 118 $this->lastErr = 'Error editing calendar entry, server reported status '.$this->client->status; 119 return false; 120 } 121 122 /** 123 * Delete a calendar entry for a given connection ID 124 * 125 * @param int $connectionId The connection ID to work with 126 * @param string $uid The event's UID as stored internally 127 * @param string $dwuser (Optional) The DokuWiki user name 128 * 129 * @return True on success, otherwise false 130 */ 131 public function deleteCalendarEntry($connectionId, $uid, $dwuser = null) 132 { 133 $conn = $this->getConnection($connectionId); 134 if($conn === false) 135 return false; 136 $conn = array_merge($conn, $this->getCredentials($connectionId)); 137 $entry = $this->getCalendarEntryByUid($uid); 138 $etag = '"'.$entry['etag'].'"'; 139 $this->setupClient($conn, strlen($data), null, 'text/calendar; charset=utf-8', array('If-Match' => $etag)); 140 $path = $conn['uri'].'/'.$entry['uri']; 141 $resp = $this->client->sendRequest($path, '', 'DELETE'); 142 if($this->client->status == 204) 143 { 144 $this->syncConnection($conn['id'], true); 145 return true; 146 } 147 $this->lastErr = 'Error deleting calendar entry, server reported status '.$this->client->status; 148 return false; 149 } 150 151 /** 152 * Retrieve a calendar entry based on UID 153 * 154 * @param string $uid The event's UID 155 * 156 * @return mixed The result 157 */ 158 public function getCalendarEntryByUid($uid) 159 { 160 $sqlite = $this->getDB(); 161 if(!$sqlite) 162 return false; 163 $query = "SELECT calendardata, calendarid, componenttype, etag, uri FROM calendarobjects WHERE uid = ?"; 164 $res = $sqlite->query($query, $uid); 165 return $sqlite->res2row($res); 166 } 167 168 /** 169 * Retrieve a calendar entry based on connection ID and URI 170 * 171 * @param int $connectionId The connection ID 172 * @param string $uri The object's URI 173 * 174 * @return mixed The result 175 */ 176 public function getCalendarEntryByUri($connectionId, $uri) 177 { 178 $sqlite = $this->getDB(); 179 if(!$sqlite) 180 return false; 181 $query = "SELECT calendardata, calendarid, componenttype, etag, uri, uid FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 182 $res = $sqlite->query($query, $connectionId, $uri); 183 return $sqlite->res2row($res); 184 } 185 186 /** 187 * Retrieve an addressbook entry based on connection ID and URI 188 * 189 * @param int $connectionId The connection ID 190 * @param string $uri The object's URI 191 * 192 * @return mixed The result 193 */ 194 public function getAddressbookEntryByUri($connectionId, $uri) 195 { 196 $sqlite = $this->getDB(); 197 if(!$sqlite) 198 return false; 199 $query = "SELECT contactdata, addressbookid, etag, uri, formattedname, structuredname FROM addressbookobjects WHERE addressbookid = ? AND uri = ?"; 200 $res = $sqlite->query($query, $connectionId, $uri); 201 return $sqlite->res2row($res); 202 } 203 204 /** 205 * Delete a connection, including all associated objects 206 * 207 * @param int $connectionId The connection ID to delete 208 * 209 * @return boolean True on success, otherwise false 210 */ 211 public function deleteConnection($connectionId) 212 { 213 $sqlite = $this->getDB(); 214 if(!$sqlite) 215 return false; 216 $conn = $this->getConnection($connectionId); 217 if($conn === false) 218 return false; 219 if($conn['type'] === 'calendar') 220 { 221 $query = "DELETE FROM calendarobjects WHERE calendarid = ?"; 222 $sqlite->query($query, $connectionId); 223 } 224 elseif($conn['type'] === 'contacts') 225 { 226 $query = "DELETE FROM addressbookobjects WHERE addressbookid = ?"; 227 $sqlite->query($query, $connectionId); 228 } 229 $query = "DELETE FROM connections WHERE id = ?"; 230 $res = $sqlite->query($query, $connectionId); 231 if($res !== false) 232 return true; 233 $this->lastErr = "Error deleting connection."; 234 return false; 235 } 236 237 /** 238 * Retreive all calendar events for a given connection ID. 239 * A sync is NOT performed during this stage, only locally cached data 240 * are available. 241 * 242 * @param int $connectionId The connection ID to retrieve 243 * @param string $startDate The start date as a string 244 * @param string $endDate The end date as a string 245 * @param string $dwuser Unused 246 * 247 * @return An array with events 248 */ 249 public function getCalendarEntries($connectionId, $startDate = null, $endDate = null, $dwuser = null) 250 { 251 $sqlite = $this->getDB(); 252 if(!$sqlite) 253 return false; 254 $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid = ?"; 255 $startTs = null; 256 $endTs = null; 257 if($startDate !== null) 258 { 259 $startTs = new \DateTime($startDate); 260 $query .= " AND lastoccurence > ".$sqlite->quote_string($startTs->getTimestamp()); 261 } 262 if($endDate !== null) 263 { 264 $endTs = new \DateTime($endDate); 265 $query .= " AND firstoccurence < ".$sqlite->quote_string($endTs->getTimestamp()); 266 } 267 $res = $sqlite->query($query, $connectionId); 268 return $sqlite->res2arr($res); 269 } 270 271 /** 272 * Add a new address book entry to a given connection ID 273 * 274 * @param int $connectionId The connection ID to work with 275 * @param string $data The new VCF entry 276 * @param string $dwuser (optional) The DokuWiki user (unused) 277 * 278 * @return boolean True on success, otherwise false 279 */ 280 public function addAddressbookEntry($connectionId, $data, $dwuser = null) 281 { 282 $conn = $this->getConnection($connectionId); 283 if($conn === false) 284 return false; 285 $conn = array_merge($conn, $this->getCredentials($connectionId)); 286 $this->setupClient($conn, strlen($data), null, 'text/vcard; charset=utf-8'); 287 $path = $conn['uri'].'/'.uniqid('dokuwiki-').'.vcf'; 288 $resp = $this->client->sendRequest($path, $data, 'PUT'); 289 if($this->client->status == 201) 290 { 291 $this->syncConnection($conn['id'], true); 292 return true; 293 } 294 $this->lastErr = 'Error adding addressbook entry, server reported status '.$this->client->status; 295 return false; 296 } 297 298 /** 299 * Edit an address book entry for a given connection ID 300 * 301 * @param int $connectionID The connection ID to work with 302 * @param string $uri The object's URI to modify 303 * @param string $data The edited entry 304 * @param string $dwuser (Optional) The DokuWiki user 305 * 306 * @return boolean True on success, otherwise false 307 */ 308 public function editAddressbookEntry($connectionId, $uri, $data, $dwuser = null) 309 { 310 $conn = $this->getConnection($connectionId); 311 if($conn === false) 312 return false; 313 $conn = array_merge($conn, $this->getCredentials($connectionId)); 314 $entry = $this->getAddressbookEntryByUri($connectionId, $uri); 315 $etag = '"'.$entry['etag'].'"'; 316 $this->setupClient($conn, strlen($data), null, 'text/vcard; charset=utf-8', array('If-Match' => $etag)); 317 $path = $conn['uri'].'/'.$entry['uri']; 318 $resp = $this->client->sendRequest($path, $data, 'PUT'); 319 if($this->client->status == 204) 320 { 321 $this->syncConnection($conn['id'], true); 322 return true; 323 } 324 $this->lastErr = 'Error editing addressbook entry, server reported status '.$this->client->status; 325 return false; 326 } 327 328 /** 329 * Delete an address book entry from a given connection ID 330 * 331 * @param int $connectionId The connection ID to work with 332 * @param string $uri The object's URI to delete 333 * @param string $dwuser (Optional) The DokuWiki user 334 * 335 * @return boolean True on success, otherwise false 336 */ 337 public function deleteAddressbookEntry($connectionId, $uri, $dwuser = null) 338 { 339 $conn = $this->getConnection($connectionId); 340 if($conn === false) 341 return false; 342 $conn = array_merge($conn, $this->getCredentials($connectionId)); 343 $entry = $this->getAddressbookEntryByUri($connectionId, $uri); 344 $etag = '"'.$entry['etag'].'"'; 345 $this->setupClient($conn, strlen($data), null, 'text/vcard; charset=utf-8', array('If-Match' => $etag)); 346 $path = $conn['uri'].'/'.$entry['uri']; 347 $resp = $this->client->sendRequest($path, '', 'DELETE'); 348 if($this->client->status == 204) 349 { 350 $this->syncConnection($conn['id'], true); 351 return true; 352 } 353 $this->lastErr = 'Error deleting addressbook entry, server reported status '.$this->client->status; 354 return false; 355 } 356 357 /** 358 * Retrieve all address book entries for a given connection ID 359 * 360 * @param int $connectionId The connection ID to work with 361 * @param string $dwuser (optional) currently unused 362 * 363 * @param An array with the address book entries 364 */ 365 public function getAddressbookEntries($connectionId, $dwuser = null) 366 { 367 $sqlite = $this->getDB(); 368 if(!$sqlite) 369 return false; 370 $query = "SELECT contactdata, uri, formattedname, structuredname FROM addressbookobjects WHERE addressbookid = ?"; 371 $res = $sqlite->query($query, $connectionId); 372 return $sqlite->res2arr($res); 373 } 374 375 /** Delete all entries from a WebDAV resource - be careful! 376 * 377 * @param int $connectionId The connection ID to work with 378 */ 379 public function deleteAllEntries($connectionId) 380 { 381 $conn = $this->getConnection($connectionId); 382 switch($conn['type']) 383 { 384 case 'contacts': 385 $entries = $this->getAddressbookEntries($connectionId); 386 foreach($entries as $entry) 387 { 388 $this->deleteAddressbookEntry($connectionId, $entry['uri']); 389 } 390 break; 391 case 'calendar': 392 $entries = $this->getCalendarEntries($connectionId); 393 foreach($entries as $entry) 394 { 395 $this->deleteCalendarEntry($connectionId, $entry['uid']); 396 } 397 break; 398 } 399 return true; 400 } 401 402 /** 403 * Add a new WebDAV connection to the backend 404 * 405 * @param string $uri The URI of the new ressource 406 * @param string $username The username for logging in 407 * @param string $password The password for logging in 408 * @param string $displayname The displayname of the ressource 409 * @param string $description The description of the ressource 410 * @param string $type The connection type, can be 'contacts' or 'calendar' 411 * @param int $syncinterval The sync interval in seconds 412 * @param boolean $active (optional) If the connection is active, defaults to true 413 * @param string $dwuser (optional) currently unused 414 * 415 * @return true on success, otherwise false 416 */ 417 public function addConnection($uri, $username, $password, $displayname, $description, $type, $syncinterval = '3600', $write = false, $active = true, $dwuser = null) 418 { 419 $sqlite = $this->getDB(); 420 if(!$sqlite) 421 return false; 422 $query = "INSERT INTO connections (uri, displayname, description, username, password, dwuser, type, syncinterval, lastsynced, active, write) ". 423 "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; 424 $res = $sqlite->query($query, $uri, $displayname, $description, $username, $password, $dwuser, $type, $syncinterval, '0', $active ? '1' : '0', $write ? '1' : '0'); 425 if($res === false) 426 { 427 $this->lastErr = "Error inserting values into SQLite DB"; 428 return false; 429 } 430 431 // Retrieve the connection ID 432 $query = "SELECT id FROM connections WHERE uri = ? AND displayname = ? AND description = ? AND username = ? AND password = ? AND dwuser = ? AND type = ? and syncinterval = ? and lastsynced = 0 AND active = ? AND write = ?"; 433 $res = $sqlite->query($query, $uri, $displayname, $description, $username, $password, $dwuser, $type, $syncinterval, $active, $write); 434 $row = $sqlite->res2row($res); 435 436 if(isset($row['id'])) 437 return $row['id']; 438 439 $this->lastErr = "Error retrieving new connection ID from SQLite DB"; 440 return false; 441 } 442 443 /** 444 * Modify an existing connection, overwriting previously defined values 445 * 446 * @param int $connId The connection ID to modify 447 * @param string $permission The page to take the permissions from 448 * @param string $displayname The new Display Name 449 * @param string $syncinterval The sync interval 450 * @param string $write If it should be writable 451 * @param string $active If it is active 452 * 453 * @return boolean True on success, otherwise false 454 */ 455 public function modifyConnection($connId, $permission, $displayname, $syncinterval, $write, $active) 456 { 457 $sqlite = $this->getDB(); 458 if(!$sqlite) 459 return false; 460 $query = "UPDATE connections SET permission = ?, displayname = ?, syncinterval = ?, write = ?, active = ? WHERE id = ?"; 461 $res = $sqlite->query($query, $permission, $displayname, $syncinterval, $write, $active, $connId); 462 if($res !== false) 463 return true; 464 465 $this->lastErr = "Error modifying connection information"; 466 return false; 467 } 468 469 /** 470 * Retrieve information about all configured connections 471 * 472 * @return An array containing the connection information 473 */ 474 public function getConnections() 475 { 476 $sqlite = $this->getDB(); 477 if(!$sqlite) 478 return false; 479 $query = "SELECT id, uri, displayname, description, synctoken, dwuser, type, syncinterval, lastsynced, ctag, active, write, permission FROM connections"; 480 $res = $sqlite->query($query); 481 return $sqlite->res2arr($res); 482 } 483 484 /** 485 * Retrieve information about a specific connection 486 * 487 * @param int $connectionId The connection ID to retrieve 488 * 489 * @return An array containing the connection information 490 */ 491 public function getConnection($connectionId) 492 { 493 $sqlite = $this->getDB(); 494 if(!$sqlite) 495 return false; 496 $query = "SELECT id, uri, displayname, description, synctoken, dwuser, type, syncinterval, lastsynced, ctag, active, write, permission FROM connections WHERE id = ?"; 497 $res = $sqlite->query($query, $connectionId); 498 return $sqlite->res2row($res); 499 } 500 501 /** 502 * Retrieve the credentials for a given connection by its ID. 503 * 504 * @param int $connectionId The connection ID to retrieve 505 * 506 * @return An array containing the username and password 507 */ 508 public function getCredentials($connectionId) 509 { 510 $sqlite = $this->getDB(); 511 if(!$sqlite) 512 return false; 513 $query = "SELECT username, password FROM connections WHERE id = ?"; 514 $res = $sqlite->query($query, $connectionId); 515 return $sqlite->res2row($res); 516 } 517 518 /** 519 * Query a Server and do WebDAV auto discovery. 520 * Currently, we do the follwing: 521 * 1) Do a PROPFIND on / and try to follow .well-known URLs 522 * 2) Prefer HTTPS wherever possible 523 * 3) Uniquify the results 524 * 4) Do a PROPFIND on the resulting URLs for the calendars/addressbook homes 525 * 5) Do a PROPFIND on the resulting home sets for calendars/addressbooks 526 * 6) Filter for calendards/addressbooks we support and 527 * 7) Return the results 528 * 529 * @param string $uri The URI of the server 530 * @param string $username The username for login 531 * @param string $password The password for login 532 * 533 * @return An array containing calendards and addressbooks that were found 534 */ 535 public function queryServer($uri, $username, $password) 536 { 537 global $conf; 538 dbglog('queryServer: '.$uri); 539 540 $webdavobjects = array(); 541 $webdavobjects['calendars'] = array(); 542 $webdavobjects['addressbooks'] = array(); 543 544 // Remove the scheme, if given 545 $pos = strpos($uri, '//'); 546 if($pos !== false) 547 $uri = substr($uri, $pos+2); 548 549 // We try the given URL, https first 550 // as well as the .well-known URLs 551 $urilist = array('https://'.$uri, 'http://'.$uri, 552 'https://'.$uri.'/.well-known/caldav', 553 'http://'.$uri.'/.well-known/caldav', 554 'https://'.$uri.'/.well-known/carddav', 555 'http://'.$uri.'/.well-known/carddav'); 556 557 $discoveredUris = array(); 558 $max_redir = 3; 559 $redirects = 0; 560 $data = $this->buildPropfind(array('D:current-user-principal')); 561 $conn = array(); 562 $conn['uri'] = $urilist[0]; 563 $conn['username'] = $username; 564 $conn['password'] = $password; 565 $this->setupClient($conn, strlen($data), ' 0', 566 'application/xml; charset=utf-8', array(), 567 0); // Don't follow redirects here 568 569 // Try all URLs, following up to 3 redirects and sending 570 // a PROPFIND each time 571 while(count($urilist) > 0) 572 { 573 $uri = array_shift($urilist); 574 $this->client->sendRequest($uri, $data, 'PROPFIND'); 575 switch($this->client->status) 576 { 577 case 301: 578 case 302: 579 case 303: 580 case 307: 581 case 308: 582 // We follow the redirect with a PROPFIND, even if this is INVALID as per RFC 583 dbglog('Redirect, following...'); 584 if($redirects < $max_redir) 585 { 586 array_unshift($urilist, $this->client->resp_headers['location']); 587 $redirects++; 588 } 589 break; 590 case 207: 591 // This is a success 592 $redirects = 0; 593 dbglog('Found!'); 594 $response = $this->parseResponse(); 595 foreach($response as $href => $params) 596 { 597 $components = parse_url($uri); 598 if(isset($params['current-user-principal']['href'])) 599 $discoveredUris[] = $components['scheme'].'://'. 600 $components['host']. 601 $params['current-user-principal']['href']; 602 } 603 break; 604 default: 605 $redirects = 0; 606 dbglog('Probably an error, continuing...'); 607 break; 608 } 609 } 610 // Remove Duplicates 611 $discoveredUris = $this->postprocessUris($discoveredUris); 612 $calendarhomes = array(); 613 $addressbookhomes = array(); 614 615 // Go through all discovered URLs and do a PROPFIND for calendar-home-set 616 // and for addressbook-home-set 617 foreach($discoveredUris as $uri) 618 { 619 $data = $this->buildPropfind(array('C:calendar-home-set'), 620 array('C' => 'urn:ietf:params:xml:ns:caldav')); 621 $this->setupClient($conn, strlen($data), ' 0'); 622 $this->client->sendRequest($uri, $data, 'PROPFIND'); 623 if($this->client->status == 207) 624 { 625 $response = $this->parseResponse(); 626 foreach($response as $href => $params) 627 { 628 if(isset($params['calendar-home-set']['href'])) 629 { 630 $components = parse_url($uri); 631 $calendarhomes[] = $components['scheme'].'://'. 632 $components['host']. 633 $params['calendar-home-set']['href']; 634 } 635 } 636 } 637 638 $data = $this->buildPropfind(array('C:addressbook-home-set'), 639 array('C' => 'urn:ietf:params:xml:ns:carddav')); 640 $this->setupClient($conn, strlen($data), ' 0'); 641 $this->client->sendRequest($uri, $data, 'PROPFIND'); 642 if($this->client->status == 207) 643 { 644 $response = $this->parseResponse(); 645 foreach($response as $href => $params) 646 { 647 if(isset($params['addressbook-home-set']['href'])) 648 { 649 $components = parse_url($uri); 650 $addressbookhomes[] = $components['scheme'].'://'. 651 $components['host']. 652 $params['addressbook-home-set']['href']; 653 } 654 } 655 } 656 } 657 658 // Now we need to query the addressbook list for address books 659 // and the calendar list for calendars 660 661 foreach($calendarhomes as $uri) 662 { 663 $data = $this->buildPropfind(array('D:resourcetype', 'D:displayname', 664 'CS:getctag', 'C:supported-calendar-component-set'), 665 array('C' => 'urn:ietf:params:xml:ns:caldav', 666 'CS' => 'http://calendarserver.org/ns/')); 667 $this->setupClient($conn, strlen($data), '1'); 668 $this->client->sendRequest($uri, $data, 'PROPFIND'); 669 $response = $this->parseResponse(); 670 $webdavobjects['calendars'] = $this->getSupportedCalendarsFromDavResponse($uri, $response); 671 } 672 673 foreach($addressbookhomes as $uri) 674 { 675 $data = $this->buildPropfind(array('D:resourcetype', 'D:displayname', 'CS:getctag'), 676 array('CS' => 'http://calendarserver.org/ns/')); 677 $this->setupClient($conn, strlen($data), '1'); 678 $this->client->sendRequest($uri, $data, 'PROPFIND'); 679 $response = $this->parseResponse(); 680 $webdavobjects['addressbooks'] = $this->getSupportedAddressbooksFromDavResponse($uri, $response); 681 } 682 683 return $webdavobjects; 684 685 } 686 687 /** 688 * Filter the DAV response by calendars we support 689 * 690 * @param string $uri The request URI where the PROPFIND was done 691 * @param array $response The response from the PROPFIND 692 * 693 * @return array An array containing URL => Displayname 694 */ 695 private function getSupportedCalendarsFromDavResponse($uri, $response) 696 { 697 $calendars = array(); 698 foreach($response as $href => $data) 699 { 700 if(!isset($data['resourcetype']['calendar'])) 701 continue; 702 if(!isset($data['supported-calendar-component-set']['comp']['name'])) 703 continue; 704 if((is_array($data['supported-calendar-component-set']['comp']['name']) && 705 !in_array('VEVENT', $data['supported-calendar-component-set']['comp']['name'])) || 706 (!is_array($data['supported-calendar-component-set']['comp']['name']) && 707 $data['supported-calendar-component-set']['comp']['name'] != 'VEVENT')) 708 continue; 709 710 $components = parse_url($uri); 711 $href = $components['scheme'].'://'.$components['host'].$href; 712 $calendars[$href] = $data['displayname']; 713 } 714 return $calendars; 715 } 716 717 /** 718 * Filter the DAV response by addressbooks we support 719 * 720 * @param string $uri The request URI where the PROPFIND was done 721 * @param array $response The response from the PROPFIND 722 * 723 * @return array An array containing URL => Displayname 724 */ 725 private function getSupportedAddressbooksFromDavResponse($uri, $response) 726 { 727 $addressbooks = array(); 728 foreach($response as $href => $data) 729 { 730 if(!isset($data['resourcetype']['addressbook'])) 731 continue; 732 $components = parse_url($uri); 733 $href = $components['scheme'].'://'.$components['host'].$href; 734 $addressbooks[$href] = $data['displayname']; 735 } 736 return $addressbooks; 737 } 738 739 /** 740 * Remove duplicate URLs from the list and prefere HTTPS if both are given 741 * 742 * @param array $urilist The list of URLs to process 743 * 744 * @return array The processed URI list 745 */ 746 private function postprocessUris($urilist) 747 { 748 $discoveredUris = array(); 749 foreach($urilist as $uri) 750 { 751 $href = str_replace(array('http', 'https'), '', $uri); 752 if(in_array('http'.$href, $urilist) && in_array('https'.$href, $urilist)) 753 { 754 if(!in_array('https'.$href, $discoveredUris)) 755 $discoveredUris[] = 'https'.$href; 756 } 757 else 758 { 759 if(!in_array($uri, $discoveredUris)) 760 $discoveredUris[] = $uri; 761 } 762 } 763 return $discoveredUris; 764 } 765 766 /** 767 * Sync a single connection if required. 768 * Sync requirement is checked based on 769 * 1) Time 770 * 2) CTag 771 * 3) ETag 772 * 773 * @param int $connectionId The connection ID to work with 774 * @param boolean $force Force sync, even if the interval hasn't passed 775 * @param boolean $overrideActive Force sync, even if the connection is inactive 776 * @param boolean $deleteBeforeSync Force sync AND delete local data beforehand 777 * 778 * @return true on success, otherwise false 779 */ 780 public function syncConnection($connectionId, $force = false, $overrideActive = false, 781 $deleteBeforeSync = false) 782 { 783 global $conf; 784 dbglog('syncConnection: '.$connectionId); 785 $conn = $this->getConnection($connectionId); 786 if($conn === false) 787 { 788 $this->lastErr = "Error retrieving connection information from SQLite DB"; 789 return false; 790 } 791 $conn = array_merge($conn, $this->getCredentials($connectionId)); 792 793 dbglog('Got connection information for connectionId: '.$connectionId); 794 795 // Sync required? 796 if((time() < ($conn['lastsynced'] + $conn['syncinterval'])) && !$force) 797 { 798 dbglog('Sync not required (time)'); 799 $this->lastErr = "Sync not required (time)"; 800 return false; 801 } 802 803 // Active? 804 if(($conn['active'] !== '1') && !$overrideActive) 805 { 806 dbglog('Connection not active.'); 807 $this->lastErr = "Connection not active"; 808 return false; 809 } 810 811 dbglog('Sync required for ConnectionID: '.$connectionId); 812 813 if(($conn['type'] !== 'contacts') && ($conn['type'] !== 'calendar') 814 && ($conn['type'] !== 'icsfeed')) 815 { 816 $this->lastErr = "Unsupported connection type found: ".$conn['type']; 817 return false; 818 } 819 820 // Check if we are dealing with an ICS feed 821 822 if($conn['type'] === 'icsfeed') 823 { 824 return $this->syncConnectionFeed($conn, $force); 825 } 826 else 827 { 828 // Perform the sync 829 return $this->syncConnectionDAV($conn, $force, $deleteBeforeSync); 830 } 831 } 832 833 /** 834 * Sync connection as ICS Feed 835 * @param $conn array the connection parameters 836 * @param $force bool force syncing even if it's not required 837 * @return true on success, otherwise false 838 */ 839 private function syncConnectionFeed($conn, $force = false) 840 { 841 dbglog('Sync ICS Feed'); 842 $this->setupClient($conn, null, null, null); 843 $resp = $this->client->sendRequest($conn['uri']); 844 if(($this->client->status >= 400) || ($this->client->status < 200)) 845 { 846 dbglog('Error: Status reported was ' . $this->client->status); 847 $this->lastErr = "Error: Server reported status ".$this->client->status; 848 return false; 849 } 850 $caldata = $this->client->resp_body; 851 dbglog($caldata); 852 require_once(DOKU_PLUGIN.'webdavclient/vendor/autoload.php'); 853 854 $vObject = \Sabre\VObject\Reader::read($caldata); 855 856 $sqlite = $this->getDB(); 857 if(!$sqlite) 858 return false; 859 860 $sqlite->query("BEGIN TRANSACTION"); 861 // Delete local entries 862 $query = "DELETE FROM calendarobjects WHERE calendarid = ?"; 863 $sqlite->query($query, $conn['id']); 864 865 foreach ($vObject->getComponents() as $component) 866 { 867 $componentType = $component->name; 868 if($componentType === 'VEVENT') 869 { 870 $calendarData = array(); 871 $calendarObject = new \Sabre\VObject\Component\VCalendar(); 872 $calendarObject->add($component); 873 $calendarData['calendar-data'] = $calendarObject->serialize(); 874 $calendarData['href'] = $conn['uri']; 875 $calendarData['getetag'] = md5($calendarData['calendar-data']); 876 $this->object2calendar($conn['id'], $calendarData); 877 } 878 } 879 $sqlite->query("COMMIT TRANSACTION"); 880 881 $this->updateConnection($conn['id'], time()); 882 return true; 883 } 884 885 /** 886 * Sync connection via DAV 887 * @param $conn array the connection parameters 888 * @param $force bool force syncing even if it's not required 889 * @param $deleteBeforeSync bool delete calendar entries before sync 890 * @return true on success, otherwise false 891 */ 892 private function syncConnectionDAV($conn, $force = false, 893 $deleteBeforeSync = false) 894 { 895 $syncResponse = $this->getCollectionStatusForConnection($conn); 896 897 // If the server supports getctag, we can check using the ctag if something has changed 898 899 if(isset($syncResponse['getctag'])) 900 { 901 if(($conn['ctag'] === $syncResponse['getctag']) && !$force) 902 { 903 dbglog('CTags match, no need to sync'); 904 $this->updateConnection($conn['id'], time(), $conn['ctag']); 905 $this->lastErr = "CTags match, there is no need to sync"; 906 return false; 907 } 908 } 909 910 // Get etags and check if the etags match our existing etags 911 // This also works if the ctag is not supported 912 913 $remoteEtags = $this->getRemoteETagsForConnection($conn); 914 dbglog($remoteEtags); 915 if($remoteEtags === false) 916 { 917 $this->lastErr = "Fetching ETags from remote server failed."; 918 return false; 919 } 920 921 $sqlite = $this->getDB(); 922 if(!$sqlite) 923 return false; 924 925 // Delete all local entries if requested to do so 926 if($deleteBeforeSync === true) 927 { 928 if($conn['type'] === 'calendar') 929 { 930 $query = "DELETE FROM calendarobjects WHERE calendarid = ?"; 931 $sqlite->query($query, $conn['id']); 932 } 933 elseif($conn['type'] === 'contacts') 934 { 935 $query = "DELETE FROM addressbookobjects WHERE addressbookid = ?"; 936 $sqlite->query($query, $conn['id']); 937 } 938 } 939 940 $localEtags = $this->getLocalETagsForConnection($conn); 941 dbglog($localEtags); 942 if($localEtags === false) 943 { 944 $this->lastErr = "Fetching ETags from local database failed."; 945 return false; 946 } 947 948 $worklist = $this->compareETags($remoteEtags, $localEtags); 949 dbglog($worklist); 950 951 $sqlite->query("BEGIN TRANSACTION"); 952 953 // Fetch the etags that need to be fetched 954 if(!empty($worklist['fetch'])) 955 { 956 $objects = $this->getRemoteObjectsByEtag($conn, $worklist['fetch']); 957 if($objects === false) 958 { 959 $this->lastErr = "Fetching remote objects by ETag failed."; 960 $sqlite->query("ROLLBACK TRANSACTION"); 961 return false; 962 } 963 dbglog($objects); 964 $this->insertObjects($conn, $objects); 965 } 966 967 // Delete the etags that need to be deleted 968 if(!empty($worklist['del'])) 969 { 970 $this->deleteEntriesByETag($conn, $worklist['del']); 971 } 972 973 $sqlite->query("COMMIT TRANSACTION"); 974 975 $this->updateConnection($conn['id'], time(), $syncResponse['getctag']); 976 977 return true; 978 } 979 980 /** 981 * Insert a DAV object into the local cache database 982 * 983 * @param array $conn The connection to work with 984 * @param array $objects A list of objects to insert 985 * 986 * @return True 987 */ 988 private function insertObjects($conn, $objects) 989 { 990 foreach($objects as $href => $data) 991 { 992 $data['href'] = basename($href); 993 if($conn['type'] === 'calendar') 994 { 995 $this->object2calendar($conn['id'], $data); 996 } 997 elseif($conn['type'] === 'contacts') 998 { 999 $this->object2addressbook($conn['id'], $data); 1000 } 1001 else 1002 { 1003 $this->lastErr = "Unsupported type."; 1004 return false; 1005 } 1006 } 1007 return true; 1008 } 1009 1010 /** 1011 * Delete entries from the local cache DB by ETag 1012 * 1013 * @param array $conn The connection to work with 1014 * @param array $worklist An array of etags 1015 * 1016 * @return True on success, otherwise false 1017 */ 1018 private function deleteEntriesByETag($conn, $worklist) 1019 { 1020 if($conn['type'] === 'calendar') 1021 { 1022 $table = 'calendarobjects'; 1023 $filter = 'calendarid'; 1024 } 1025 elseif($conn['type'] === 'contacts') 1026 { 1027 $table = 'addressbookobjects'; 1028 $filter = 'addressbookid'; 1029 } 1030 else 1031 { 1032 $this->lastErr = "Unsupported type."; 1033 return false; 1034 } 1035 $sqlite = $this->getDB(); 1036 if(!$sqlite) 1037 return false; 1038 foreach($worklist as $etag => $href) 1039 { 1040 $query = "DELETE FROM " . $table . " WHERE etag = ? AND " . $filter . " = ?"; 1041 $sqlite->query($query, $etag, $conn['id']); 1042 } 1043 return true; 1044 } 1045 1046 /** 1047 * Fetch remote DAV objects by ETag 1048 * 1049 * @param array $conn The connection to work with 1050 * @param array $etags An array of etags to retrieve 1051 * 1052 * @return array The parsed response as array 1053 */ 1054 private function getRemoteObjectsByEtag($conn, $etags) 1055 { 1056 if($conn['type'] === 'contacts') 1057 { 1058 $data = $this->buildReport('C:addressbook-multiget', array('C' => 'urn:ietf:params:xml:ns:carddav'), 1059 array('D:getetag', 1060 'C:address-data'), array(), 1061 array_values($etags)); 1062 } 1063 elseif($conn['type'] === 'calendar') 1064 { 1065 $data = $this->buildReport('C:calendar-multiget', array('C' => 'urn:ietf:params:xml:ns:caldav'), 1066 array('D:getetag', 1067 'C:calendar-data'), 1068 array(), 1069 array_values($etags)); 1070 } 1071 $this->setupClient($conn, strlen($data), '1'); 1072 $resp = $this->client->sendRequest($conn['uri'], $data, 'REPORT'); 1073 $response = $this->parseResponse(); 1074 return $response; 1075 } 1076 1077 /** 1078 * Compare a local and a remote list of etags and return delete and fetch lists 1079 * 1080 * @param array $remoteEtags Array with the remot ETags 1081 * @param array $localEtags Array with the local ETags 1082 * 1083 * @return array An Array containing a 'del' and a 'fetch' list 1084 */ 1085 private function compareETags($remoteEtags, $localEtags) 1086 { 1087 $lEtags = array(); 1088 $rEtags = array(); 1089 $data = array(); 1090 $data['del'] = array(); 1091 $data['fetch'] = array(); 1092 1093 foreach($localEtags as $localEtag) 1094 $lEtags[$localEtag['etag']] = $localEtag['uri']; 1095 1096 foreach($remoteEtags as $href => $etag) 1097 { 1098 $rEtags[$etag['getetag']] = $href; 1099 } 1100 1101 $data['del'] = array_diff_key($lEtags, $rEtags); 1102 $data['fetch'] = array_diff_key($rEtags, $lEtags); 1103 1104 return $data; 1105 } 1106 1107 /** 1108 * Internal function to set up the DokuHTTPClient for data retrieval 1109 * 1110 * @param array $conn The connection information 1111 * @param string $cl (Optional) The Content-Length parameter 1112 * @param string $depth (Optional) The Depth parameter 1113 * @param string $ct (Optional) The Content-Type 1114 * @param array $headers (Optional) Additional headers 1115 * @param int $redirect (Optional) Number of redirects to follow 1116 */ 1117 private function setupClient($conn, $cl = null, $depth = null, 1118 $ct = 'application/xml; charset=utf-8', $headers = array(), 1119 $redirect = 3) 1120 { 1121 $this->client->user = $conn['username']; 1122 $this->client->pass = $conn['password']; 1123 $this->client->http = '1.1'; 1124 $this->client->max_redirect = $redirect; 1125 // For big request, having keep alive enabled doesn't seem to work correctly 1126 $this->client->keep_alive = false; 1127 // Restore the Client's default headers, otherwise we might keep 1128 // old headers for later requests 1129 $this->client->headers = $this->client_headers; 1130 foreach($headers as $header => $content) 1131 $this->client->headers[$header] = $content; 1132 if(!is_null($ct)) 1133 $this->client->headers['Content-Type'] = $ct; 1134 if(!is_null($depth)) 1135 $this->client->headers['Depth'] = $depth; 1136 if(!is_null($cl)) 1137 $this->client->headers['Content-Length'] = $cl; 1138 1139 } 1140 1141 /** 1142 * Retrieve the local ETags for a given connection 1143 * 1144 * @param array $conn The local connection 1145 * 1146 * @return array An array containing the ETags 1147 */ 1148 private function getLocalETagsForConnection($conn) 1149 { 1150 if($conn['type'] === 'calendar') 1151 { 1152 $table = 'calendarobjects'; 1153 $id = 'calendarid'; 1154 } 1155 elseif($conn['type'] === 'contacts') 1156 { 1157 $table = 'addressbookobjects'; 1158 $id = 'addressbookid'; 1159 } 1160 else 1161 { 1162 $this->lastErr = "Unsupported type."; 1163 return false; 1164 } 1165 $sqlite = $this->getDB(); 1166 if(!$sqlite) 1167 return false; 1168 $query = "SELECT uri, etag FROM " . $table . " WHERE " . $id . " = ?"; 1169 $res = $sqlite->query($query, $conn['id']); 1170 $data = $sqlite->res2arr($res); 1171 return $data; 1172 } 1173 1174 /** 1175 * Retrieve the remote ETags for a given connection 1176 * 1177 * @param array $conn The connection to work with 1178 * 1179 * @return array An array of remote ETags 1180 */ 1181 private function getRemoteETagsForConnection($conn) 1182 { 1183 global $conf; 1184 if($conn['type'] === 'contacts') 1185 { 1186 $data = $this->buildReport('C:addressbook-query', array('C' => 'urn:ietf:params:xml:ns:carddav'), 1187 array('D:getetag')); 1188 } 1189 elseif($conn['type'] === 'calendar') 1190 { 1191 $data = $this->buildReport('C:calendar-query', array('C' => 'urn:ietf:params:xml:ns:caldav'), 1192 array('D:getetag'), 1193 array('C:comp-filter' => array('VCALENDAR' => 'VEVENT'))); 1194 } 1195 else 1196 { 1197 $this->lastErr = "Unsupported type."; 1198 return false; 1199 } 1200 $this->setupClient($conn, strlen($data), '1'); 1201 $resp = $this->client->sendRequest($conn['uri'], $data, 'REPORT'); 1202 $etags = $this->parseResponse(); 1203 return $etags; 1204 } 1205 1206 /** 1207 * Parse a remote response and check the status of the response objects 1208 * 1209 * @return mixed An array with the response objects or false 1210 */ 1211 private function parseResponse() 1212 { 1213 global $conf; 1214 if(($this->client->status >= 400) || ($this->client->status < 200)) 1215 { 1216 dbglog('Error: Status reported was ' . $this->client->status); 1217 $this->lastErr = "Error: Server reported status ".$this->client->status; 1218 return false; 1219 } 1220 1221 dbglog($this->client->status); 1222 dbglog($this->client->error); 1223 1224 $response = $this->clean_response($this->client->resp_body); 1225 try 1226 { 1227 $xml = simplexml_load_string($response); 1228 } 1229 catch(Exception $e) 1230 { 1231 dbglog('Exception occured: '.$e->getMessage()); 1232 $this->lastErr = "Exception occured while parsing response: ".$e->getMessage(); 1233 return false; 1234 } 1235 1236 $data = array(); 1237 1238 if(!empty($xml->response)) 1239 { 1240 foreach($xml->response as $response) 1241 { 1242 $href = (string)$response->href; 1243 $status = $this->parseHttpStatus((string)$response->propstat->status); 1244 $data[$href]['status'] = $status; 1245 // We parse here all props that succeeded and ignore the failed ones 1246 if($status === '200') 1247 { 1248 $data[$href] = $this->recursiveXmlToArray($response->propstat->prop->children()); 1249 1250 } 1251 } 1252 } 1253 return $data; 1254 } 1255 1256 /** 1257 * Recursively convert XML Tags and attributes to an array 1258 * 1259 * @param mixed $objects SimpleXML Objects to recurse 1260 * 1261 * @return array An array containing the processed data 1262 */ 1263 private function recursiveXmlToArray($objects) 1264 { 1265 $ret = array(); 1266 foreach($objects as $object) 1267 { 1268 // If our object contains child objects, call ourselves again 1269 if($object->count() > 0) 1270 { 1271 $ret[$object->getName()] = $this->recursiveXmlToArray($object->children()); 1272 } 1273 // If our object has attributes, extract the attributes 1274 elseif(!is_null($object->attributes()) && (count($object->attributes()) > 0)) 1275 { 1276 // This is the hardest part: sometimes, attributes 1277 // have the same name. Parse them as arrays 1278 if(!is_array($ret[$object->getName()])) 1279 $ret[$object->getName()] = array(); 1280 foreach($object->attributes() as $key => $val) 1281 { 1282 // If our existing value is not yet an array, 1283 // convert it to an array and add the new value 1284 if(isset($ret[$object->getName()][(string)$key]) && 1285 !is_array($ret[$object->getName()][(string)$key])) 1286 { 1287 $ret[$object->getName()][(string)$key] = 1288 array($ret[$object->getName()][(string)$key], trim((string)$val, '"')); 1289 } 1290 // If it is already an array, simply append to an array 1291 elseif(isset($ret[$object->getName()][(string)$key]) && 1292 is_array($ret[$object->getName()][(string)$key])) 1293 { 1294 $ret[$object->getName()][(string)$key][] = trim((string)$val, '"'); 1295 } 1296 // If the key doesn't exist, add it to output as string 1297 // as we don't know yet if there are further values 1298 else 1299 { 1300 $ret[$object->getName()][(string)$key] = trim((string)$val, '"'); 1301 } 1302 } 1303 } 1304 // Simply add the object's value to the output 1305 else 1306 { 1307 $ret[$object->getName()] = trim((string)$object, '"'); 1308 } 1309 } 1310 return $ret; 1311 } 1312 1313 /** 1314 * Retrieve the status of a collection 1315 * 1316 * @param array $conn An array containing connection information 1317 * @return an Array containing the status 1318 */ 1319 private function getCollectionStatusForConnection($conn) 1320 { 1321 global $conf; 1322 $data = $this->buildPropfind(array('D:displayname', 'CS:getctag', 'D:sync-token'), array('CS' => 'http://calendarserver.org/ns/')); 1323 $this->setupClient($conn, strlen($data), ' 0'); 1324 1325 $resp = $this->client->sendRequest($conn['uri'], $data, 'PROPFIND'); 1326 1327 $response = $this->parseResponse(); 1328 if((count($response) != 1) || ($response === false)) 1329 { 1330 $this->lastErr = "Error: Unexpected response from server"; 1331 return array(); 1332 } 1333 1334 $syncResponse = array_values($response); 1335 $syncResponse = $syncResponse[0]; 1336 return $syncResponse; 1337 } 1338 1339 /** 1340 * Update the status of a connection 1341 * 1342 * @param int $connectionId The connection ID to work with 1343 * @param int $lastSynced The last time the connection was synnchronised 1344 * @param string $ctag (optional) The CTag of the current sync run 1345 * 1346 * @return true on success, otherwise false 1347 */ 1348 private function updateConnection($connectionId, $lastSynced, $ctag = null) 1349 { 1350 if(is_null($ctag)) 1351 $ctag = ''; 1352 1353 io_saveFile($this->syncChangeLogFile.$connectionId, serialize($lastSynced)); 1354 1355 $sqlite = $this->getDB(); 1356 if(!$sqlite) 1357 return false; 1358 $query = "UPDATE connections SET lastsynced = ?, ctag = ? WHERE id = ?"; 1359 $res = $sqlite->query($query, $lastSynced, $ctag, $connectionId); 1360 if($res !== false) 1361 return true; 1362 $this->lastErr = "Error updating connection"; 1363 return false; 1364 } 1365 1366 /** 1367 * Convert a given calendar object (including ICS data) and save 1368 * it in the local database 1369 * 1370 * @param int $connectionId The connection ID to work with 1371 * @param array $calendarobject The calendar object to convert 1372 * 1373 * @return true on success, otherwise false 1374 */ 1375 private function object2calendar($connectionId, $calendarobject) 1376 { 1377 $extradata = $this->getDenormalizedCalendarData($calendarobject['calendar-data']); 1378 1379 if($extradata === false) 1380 { 1381 $this->lastErr = "Couldn't parse calendar data"; 1382 return false; 1383 } 1384 1385 $sqlite = $this->getDB(); 1386 if(!$sqlite) 1387 return false; 1388 $query = "INSERT INTO calendarobjects (calendardata, uri, calendarid, lastmodified, etag, size, componenttype, uid, firstoccurence, lastoccurence) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; 1389 $lastmod = new \DateTime($calendarobject['getlastmodified']); 1390 $res = $sqlite->query($query, $calendarobject['calendar-data'], $calendarobject['href'], $connectionId, $lastmod->getTimestamp(), $calendarobject['getetag'], $extradata['size'], $extradata['componentType'], $extradata['uid'], $extradata['firstOccurence'], $extradata['lastOccurence']); 1391 if($res !== false) 1392 return true; 1393 $this->lastErr = "Error inserting object"; 1394 return false; 1395 } 1396 1397 /** 1398 * Convert a given address book object (including VCF data) and save it in 1399 * the local database 1400 * 1401 * @param int $connectionId The ID of the connection to work with 1402 * @param array $addressobject The address object data 1403 * 1404 * @return true on success, otherwise false 1405 */ 1406 private function object2addressbook($connectionId, $addressobject) 1407 { 1408 $extradata = $this->getDenormalizedContactData($addressobject['address-data']); 1409 1410 if($extradata === false) 1411 { 1412 $this->lastErr = "Couldn't parse contact data"; 1413 return false; 1414 } 1415 1416 $sqlite = $this->getDB(); 1417 if(!$sqlite) 1418 return false; 1419 $query = "INSERT INTO addressbookobjects (contactdata, uri, addressbookid, " 1420 ."lastmodified, etag, size, formattedname, structuredname) " 1421 ."VALUES(?, ?, ?, ?, ?, ?, ?, ?)"; 1422 $lastmod = new \DateTime($addressobject['getlastmodified']); 1423 $res = $sqlite->query($query, 1424 $addressobject['address-data'], 1425 $addressobject['href'], 1426 $connectionId, 1427 $lastmod->getTimestamp(), 1428 $addressobject['getetag'], 1429 $extradata['size'], 1430 $extradata['formattedname'], 1431 $extradata['structuredname'] 1432 ); 1433 if($res !== false) 1434 return true; 1435 $this->lastErr = "Error inserting object"; 1436 return false; 1437 } 1438 1439 /** 1440 * Helper function to parse a HTTP status response into a status code only 1441 * 1442 * @param string $statusString The HTTP status string 1443 * 1444 * @return The status as a string 1445 */ 1446 private function parseHttpStatus($statusString) 1447 { 1448 $status = explode(' ', $statusString, 3); 1449 $status = $status[1]; 1450 return $status; 1451 } 1452 1453 /** 1454 * Helper function to remove all namespace prefixes from XML tags 1455 * 1456 * @param string $response The response to clean 1457 * 1458 * @return String containing the cleaned response 1459 */ 1460 private function clean_response($response) 1461 { 1462 dbglog($response); 1463 // Gets rid of all namespace definitions 1464 $response = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $response); 1465 1466 // Strip the namespace prefixes on all XML tags 1467 $response = preg_replace('/(<\/*)[^>:]+:/', '$1', $response); 1468 dbglog($response); 1469 return $response; 1470 } 1471 1472 /** 1473 * Helper function to generate a PROPFIND request 1474 * 1475 * @param array $props The properties to retrieve 1476 * @param array $ns Any custom namespaces used 1477 * 1478 * @return String containing the XML 1479 */ 1480 private function buildPropfind($props, $ns = array()) 1481 { 1482 $xml = new XMLWriter(); 1483 $xml->openMemory(); 1484 $xml->setIndent(4); 1485 $xml->startDocument('1.0', 'utf-8'); 1486 $xml->startElement('D:propfind'); 1487 $xml->writeAttribute('xmlns:D', 'DAV:'); 1488 foreach($ns as $key => $val) 1489 $xml->writeAttribute('xmlns:'.$key, $val); 1490 $xml->startElement('D:prop'); 1491 foreach($props as $prop) 1492 $xml->writeElement($prop); 1493 $xml->endElement(); 1494 $xml->endElement(); 1495 $xml->endDocument(); 1496 return $xml->outputMemory()."\r\n"; 1497 } 1498 1499 /** 1500 * Helper function to generate a REPORT 1501 * 1502 * @param string $ns The namespace 1503 * @param string $op The report operation 1504 * @param array $props (optional) The properties to retrieve 1505 * @param array $filters (optional) The filters to apply 1506 * 1507 * @return String containing the XML 1508 */ 1509 private function buildReport($op, $ns = array(), $props = array(), $filters = array(), $hrefs = array()) 1510 { 1511 $xml = new XMLWriter(); 1512 $xml->openMemory(); 1513 $xml->setIndent(4); 1514 $xml->startDocument('1.0', 'utf-8'); 1515 $xml->startElement($op); 1516 $xml->writeAttribute('xmlns:D', 'DAV:'); 1517 foreach($ns as $key => $val) 1518 $xml->writeAttribute('xmlns:'.$key, $val); 1519 $xml->startElement('D:prop'); 1520 foreach($props as $prop) 1521 { 1522 $xml->writeElement($prop); 1523 } 1524 $xml->endElement(); 1525 if(!empty($filters)) 1526 { 1527 $xml->startElement('C:filter'); 1528 foreach($filters as $filter => $params) 1529 { 1530 foreach($params as $key => $val) 1531 { 1532 $xml->startElement($filter); 1533 $xml->writeAttribute('name', $key); 1534 if($val !== '') 1535 { 1536 $xml->startElement($filter); 1537 $xml->writeAttribute('name', $val); 1538 $xml->endElement(); 1539 } 1540 $xml->endElement(); 1541 } 1542 } 1543 $xml->endElement(); 1544 } 1545 foreach($hrefs as $href) 1546 { 1547 $xml->writeElement('D:href', $href); 1548 } 1549 $xml->endElement(); 1550 $xml->endDocument(); 1551 return $xml->outputMemory()."\r\n"; 1552 } 1553 1554 /** 1555 * Synchronise all configured connections 1556 */ 1557 public function syncAllConnections() 1558 { 1559 $connections = $this->getConnections(); 1560 foreach($connections as $connection) 1561 { 1562 $this->syncConnection($connection['id']); 1563 } 1564 } 1565 1566 /** 1567 * Synchronise all configured connections when running with the indexer 1568 * This takes care that only *one* connection is synchronised. 1569 * 1570 * @return true if something was synchronised, otherwise false 1571 */ 1572 public function indexerSyncAllConnections() 1573 { 1574 global $conf; 1575 dbglog('IndexerSyncAllConnections'); 1576 1577 $connections = $this->getConnections(); 1578 foreach($connections as $connection) 1579 { 1580 if($this->syncConnection($connection['id']) === true) 1581 return true; 1582 } 1583 return false; 1584 } 1585 1586 /** 1587 * Retrieve a configuration option for the plugin 1588 * 1589 * @param string $key The key to query 1590 * 1591 * @return mixed The option set, null if not found 1592 */ 1593 public function getConfig($key) 1594 { 1595 return $this->getConf($key); 1596 } 1597 1598 /** 1599 * Parses some information from contact objects, used 1600 * for optimized addressbook-queries. 1601 * 1602 * @param string $contactData 1603 * 1604 * @return array 1605 */ 1606 protected function getDenormalizedContactData($contactData) 1607 { 1608 require_once(DOKU_PLUGIN.'webdavclient/vendor/autoload.php'); 1609 1610 $vObject = \Sabre\VObject\Reader::read($contactData); 1611 $formattedname = ''; 1612 $structuredname = ''; 1613 1614 if(isset($vObject->FN)) 1615 $formattedname = (string)$vObject->FN; 1616 1617 if(isset($vObject->N)) 1618 $structuredname = join(';', $vObject->N->getParts()); 1619 1620 return array( 1621 'formattedname' => $formattedname, 1622 'structuredname' => $structuredname, 1623 'size' => strlen($contactData) 1624 ); 1625 } 1626 1627 /** 1628 * Parses some information from calendar objects, used for optimized 1629 * calendar-queries. 1630 * 1631 * Returns an array with the following keys: 1632 * * etag - An md5 checksum of the object without the quotes. 1633 * * size - Size of the object in bytes 1634 * * componentType - VEVENT, VTODO or VJOURNAL 1635 * * firstOccurence 1636 * * lastOccurence 1637 * * uid - value of the UID property 1638 * 1639 * @param string $calendarData 1640 * @return array 1641 */ 1642 protected function getDenormalizedCalendarData($calendarData) 1643 { 1644 require_once(DOKU_PLUGIN.'webdavclient/vendor/autoload.php'); 1645 1646 $vObject = \Sabre\VObject\Reader::read($calendarData); 1647 $componentType = null; 1648 $component = null; 1649 $firstOccurence = null; 1650 $lastOccurence = null; 1651 $uid = null; 1652 foreach ($vObject->getComponents() as $component) 1653 { 1654 if ($component->name !== 'VTIMEZONE') 1655 { 1656 $componentType = $component->name; 1657 $uid = (string)$component->UID; 1658 break; 1659 } 1660 } 1661 if (!$componentType) 1662 { 1663 return false; 1664 } 1665 if ($componentType === 'VEVENT') 1666 { 1667 $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); 1668 // Finding the last occurence is a bit harder 1669 if (!isset($component->RRULE)) 1670 { 1671 if (isset($component->DTEND)) 1672 { 1673 $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); 1674 } 1675 elseif (isset($component->DURATION)) 1676 { 1677 $endDate = clone $component->DTSTART->getDateTime(); 1678 $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue())); 1679 $lastOccurence = $endDate->getTimeStamp(); 1680 } 1681 elseif (!$component->DTSTART->hasTime()) 1682 { 1683 $endDate = clone $component->DTSTART->getDateTime(); 1684 $endDate->modify('+1 day'); 1685 $lastOccurence = $endDate->getTimeStamp(); 1686 } 1687 else 1688 { 1689 $lastOccurence = $firstOccurence; 1690 } 1691 } 1692 else 1693 { 1694 $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID); 1695 $maxDate = new \DateTime('2038-01-01'); 1696 if ($it->isInfinite()) 1697 { 1698 $lastOccurence = $maxDate->getTimeStamp(); 1699 } 1700 else 1701 { 1702 $end = $it->getDtEnd(); 1703 while ($it->valid() && $end < $maxDate) 1704 { 1705 $end = $it->getDtEnd(); 1706 $it->next(); 1707 } 1708 $lastOccurence = $end->getTimeStamp(); 1709 } 1710 } 1711 } 1712 1713 return array( 1714 'etag' => md5($calendarData), 1715 'size' => strlen($calendarData), 1716 'componentType' => $componentType, 1717 'firstOccurence' => $firstOccurence, 1718 'lastOccurence' => $lastOccurence, 1719 'uid' => $uid, 1720 ); 1721 1722 } 1723 1724/** 1725 * Retrieve the path to the sync change file for a given connection ID. 1726 * You can use this file to easily monitor if a sync has changed anything. 1727 * 1728 * @param int $connectionID The connection ID to work with 1729 * @return string The path to the sync change file 1730 */ 1731 public function getLastSyncChangeFileForConnection($connectionId) 1732 { 1733 return $this->syncChangeLogFile.$connectionId; 1734 } 1735 1736} 1737