1<?php 2/** 3* An object representing a DAV 'resource' 4* 5* @package davical 6* @subpackage Resource 7* @author Andrew McMillan <andrew@mcmillan.net.nz> 8* @copyright Morphoss Ltd 9* @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later 10*/ 11 12require_once('AwlCache.php'); 13require_once('AwlQuery.php'); 14require_once('DAVPrincipal.php'); 15require_once('DAVTicket.php'); 16require_once('iCalendar.php'); 17 18 19/** 20* A class for things to do with a DAV Resource 21* 22* @package davical 23*/ 24class DAVResource 25{ 26 /** 27 * @var string The partial URL of the resource within our namespace, which this resource is being retrieved as 28 */ 29 protected $dav_name; 30 31 /** 32 * @var bool Does the resource actually exist yet? 33 */ 34 protected $exists; 35 36 /** 37 * @var string The unique etag associated with the current version of the resource 38 */ 39 protected $unique_tag; 40 41 /** 42 * @var string The actual resource content, if it exists and is not a collection 43 */ 44 protected $resource; 45 46 /** 47 * @var DAVResource The parent of the resource, which will always be a collection 48 */ 49 protected $parent; 50 51 /** 52 * @var array The types of the resource, possibly multiple 53 */ 54 protected $resourcetypes; 55 56 /** 57 * @var string The type of the content 58 */ 59 protected $contenttype; 60 61 /** 62 * @var string The canonical name which this resource exists at 63 */ 64 protected $bound_from; 65 66 /** 67 * @var DAVResource An object which is the collection record for this resource, or for it's container 68 */ 69 private $collection; 70 71 /** 72 * @var DAVPrincipal An object which is the principal for this resource, or would be if it existed. 73 */ 74 private $principal; 75 76 /** 77 * @var integer A bit mask representing the current user's privileges towards this DAVResource 78 */ 79 private $privileges; 80 81 /** 82 * @var bool True if this resource is a collection of any kind 83 */ 84 private $_is_collection; 85 86 /** 87 * @var bool True if this resource is a principal-URL 88 */ 89 private $_is_principal; 90 91 /** 92 * @var bool True if this resource is a calendar collection 93 */ 94 private $_is_calendar; 95 96 /** 97 * @var bool True if this resource is a binding to another resource 98 */ 99 private $_is_binding; 100 101 /** 102 * @var bool True if this resource is a binding to an external resource 103 */ 104 private $_is_external; 105 106 /** 107 * @var bool True if this resource is an addressbook collection 108 */ 109 private $_is_addressbook; 110 111 /** 112 * @var bool True if this resource is, or is in, a proxy collection 113 */ 114 private $_is_proxy_resource; 115 116 /** 117 * @var The type of proxy collection this resource is or is in: read or write 118 */ 119 private $proxy_type; 120 121 /** 122 * @var array An array of the methods we support on this resource. 123 */ 124 private $supported_methods; 125 126 /** 127 * @var array An array of the reports we support on this resource. 128 */ 129 private $supported_reports; 130 131 /** 132 * @var array An array of the dead properties held for this resource 133 */ 134 private $dead_properties; 135 136 /** 137 * @var array An array of the component types we support on this resource. 138 */ 139 private $supported_components; 140 141 /** 142 * @var array An array of DAVTicket objects if any apply to this resource, such as via a bind. 143 */ 144 private $tickets; 145 146 /** 147 * Constructor 148 * @param mixed $parameters If null, an empty Resourced is created. 149 * If it is an object then it is expected to be a record that was 150 * read elsewhere. 151 * @param object $prefetched_collection If provided, the internal collection 152 * field of the resource is populated with the given data, so it does not need 153 * to be queried again later 154 */ 155 function __construct( $parameters = null, DAVResource $prefetched_collection = null ) { 156 $this->exists = null; 157 $this->bound_from = null; 158 $this->dav_name = null; 159 $this->unique_tag = null; 160 $this->resource = null; 161 $this->collection = null; 162 $this->principal = null; 163 $this->parent = null; 164 $this->resourcetypes = null; 165 $this->contenttype = null; 166 $this->privileges = null; 167 $this->dead_properties = null; 168 $this->supported_methods = null; 169 $this->supported_reports = null; 170 171 $this->_is_collection = false; 172 $this->_is_principal = false; 173 $this->_is_calendar = false; 174 $this->_is_binding = false; 175 $this->_is_external = false; 176 $this->_is_addressbook = false; 177 $this->_is_proxy_resource = false; 178 179 if ( isset($prefetched_collection) ) { 180 $this->collection = $prefetched_collection; 181 } 182 183 if ( isset($parameters) && is_object($parameters) ) { 184 $this->FromRow($parameters); 185 } 186 else if ( isset($parameters) && is_array($parameters) ) { 187 if ( isset($parameters['path']) ) { 188 $this->FromPath($parameters['path']); 189 } 190 } 191 else if ( isset($parameters) && is_string($parameters) ) { 192 $this->FromPath($parameters); 193 } 194 } 195 196 197 /** 198 * Initialise from a database row 199 * @param object $row The row from the DB. 200 */ 201 function FromRow($row) { 202 global $c, $session; 203 204 if ( $row == null ) return; 205 206 $this->exists = true; 207 $this->dav_name = $row->dav_name; 208 $this->bound_from = (isset($row->bound_from)? $row->bound_from : $row->dav_name); 209 $this->_is_collection = preg_match( '{/$}', $this->dav_name ); 210 211 if ( $this->_is_collection ) { 212 $this->contenttype = 'httpd/unix-directory'; 213 $this->collection = (object) array(); 214 $this->resource_id = $row->collection_id; 215 216 $this->_is_principal = preg_match( '{^/[^/]+/$}', $this->dav_name ); 217 if ( preg_match( '#^(/principals/[^/]+/[^/]+)/?$#', $this->dav_name, $matches) ) { 218 $this->collection->dav_name = $matches[1].'/'; 219 $this->collection->type = 'principal_link'; 220 $this->_is_principal = true; 221 } 222 } 223 else { 224 $this->resource = (object) array(); 225 if ( isset($row->dav_id) ) $this->resource_id = $row->dav_id; 226 } 227 228 dbg_error_log( 'DAVResource', ':FromRow: Named "%s" is%s a collection.', $this->dav_name, ($this->_is_collection?'':' not') ); 229 230 foreach( $row AS $k => $v ) { 231 if ( $this->_is_collection ) 232 $this->collection->{$k} = $v; 233 else 234 $this->resource->{$k} = $v; 235 switch ( $k ) { 236 case 'created': 237 case 'modified': 238 $this->{$k} = $v; 239 break; 240 241 case 'resourcetypes': 242 if ( $this->_is_collection ) $this->{$k} = $v; 243 break; 244 245 case 'dav_etag': 246 $this->unique_tag = '"'.$v.'"'; 247 break; 248 249 } 250 } 251 252 if ( $this->_is_collection ) { 253 if ( !isset( $this->collection->type ) || $this->collection->type == 'collection' ) { 254 if ( $this->_is_principal ) 255 $this->collection->type = 'principal'; 256 else if ( $row->is_calendar == 't' ) { 257 $this->collection->type = 'calendar'; 258 } 259 else if ( $row->is_addressbook == 't' ) { 260 $this->collection->type = 'addressbook'; 261 } 262 else if ( isset($row->is_proxy) && $row->is_proxy == 't' ) { 263 $this->collection->type = 'proxy'; 264 } 265 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) ) 266 $this->collection->type = 'schedule-'. $matches[3]. 'box'; 267 else if ( $this->dav_name == '/' ) 268 $this->collection->type = 'root'; 269 else 270 $this->collection->type = 'collection'; 271 } 272 273 $this->_is_calendar = ($this->collection->is_calendar == 't'); 274 $this->_is_addressbook = ($this->collection->is_addressbook == 't'); 275 $this->_is_proxy_resource = ($this->collection->type == 'proxy'); 276 if ( $this->_is_principal && !isset($this->resourcetypes) ) { 277 $this->resourcetypes = '<DAV::collection/><DAV::principal/>'; 278 } 279 else if ( $this->_is_proxy_resource ) { 280 $this->resourcetypes = $this->collection->resourcetypes; 281 preg_match( '#^/[^/]+/calendar-proxy-(read|write)/?[^/]*$#', $this->dav_name, $matches ); 282 $this->proxy_type = $matches[1]; 283 } 284 if ( isset($this->collection->dav_displayname) ) $this->collection->displayname = $this->collection->dav_displayname; 285 } 286 else { 287 $this->resourcetypes = ''; 288 if ( isset($this->resource->caldav_data) ) { 289 if ( isset($this->resource->summary) )$this->resource->displayname = $this->resource->summary; 290 if ( strtoupper(substr($this->resource->caldav_data,0,15)) == 'BEGIN:VCALENDAR' ) { 291 $this->contenttype = 'text/calendar'; 292 if ( isset($this->resource->caldav_type) ) $this->contenttype .= "; component=" . strtolower($this->resource->caldav_type); 293 if ( !$this->HavePrivilegeTo('read') && $this->HavePrivilegeTo('read-free-busy') ) { 294 $vcal = new iCalComponent($this->resource->caldav_data); 295 $confidential = $vcal->CloneConfidential(); 296 $this->resource->caldav_data = $confidential->Render(); 297 $this->resource->displayname = $this->resource->summary = translate('Busy'); 298 $this->resource->description = null; 299 $this->resource->location = null; 300 $this->resource->url = null; 301 } 302 else { 303 if ( isset($this->resource->class) && strtoupper($this->resource->class)=='CONFIDENTIAL' && !$this->HavePrivilegeTo('all') && $session->user_no != $this->resource->user_no ) { 304 $vcal = new iCalComponent($this->resource->caldav_data); 305 $confidential = $vcal->CloneConfidential(); 306 $this->resource->caldav_data = $confidential->Render(); 307 } 308 if ( isset($c->hide_alarm) && $c->hide_alarm && !$this->HavePrivilegeTo('write') ) { 309 $vcal1 = new iCalComponent($this->resource->caldav_data); 310 $comps = $vcal1->GetComponents(); 311 $vcal2 = new iCalComponent(); 312 $vcal2->VCalendar(); 313 foreach( $comps AS $comp ) { 314 $comp->ClearComponents('VALARM'); 315 $vcal2->AddComponent($comp); 316 } 317 $this->resource->displayname = $this->resource->summary = $vcal2->GetPValue('SUMMARY'); 318 $this->resource->caldav_data = $vcal2->Render(); 319 } 320 } 321 } 322 else if ( strtoupper(substr($this->resource->caldav_data,0,11)) == 'BEGIN:VCARD' ) { 323 $this->contenttype = 'text/vcard'; 324 } 325 else if ( strtoupper(substr($this->resource->caldav_data,0,11)) == 'BEGIN:VLIST' ) { 326 $this->contenttype = 'text/x-vlist'; 327 } 328 } 329 } 330 } 331 332 333 /** 334 * Initialise from a path 335 * @param object $inpath The path to populate the resource data from 336 */ 337 function FromPath($inpath) { 338 global $c; 339 340 $this->dav_name = DeconstructURL($inpath); 341 342 $this->FetchCollection(); 343 if ( $this->_is_collection ) { 344 if ( $this->_is_principal || $this->collection->type == 'principal' ) $this->FetchPrincipal(); 345 } 346 else { 347 $this->FetchResource(); 348 } 349 dbg_error_log( 'DAVResource', ':FromPath: Path "%s" is%s a collection%s.', 350 $this->dav_name, ($this->_is_collection?' '.$this->resourcetypes:' not'), ($this->_is_principal?' and a principal':'') ); 351 } 352 353 354 private function ReadCollectionFromDatabase() { 355 global $c, $session; 356 357 $this->collection = (object) array( 358 'collection_id' => -1, 359 'type' => 'nonexistent', 360 'is_calendar' => false, 'is_principal' => false, 'is_addressbook' => false 361 ); 362 363 $base_sql = 'SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), '; 364 $base_sql .= 'p.principal_id, p.type_id AS principal_type_id, '; 365 $base_sql .= 'p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges, '; 366 $base_sql .= 'timezones.vtimezone '; 367 $base_sql .= 'FROM collection LEFT JOIN principal p USING (user_no) '; 368 $base_sql .= 'LEFT JOIN timezones ON (collection.timezone=timezones.tzid) '; 369 $base_sql .= 'WHERE '; 370 $sql = $base_sql .'collection.dav_name = :raw_path '; 371 $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth ); 372 if ( !preg_match( '#/$#', $this->dav_name ) ) { 373 $sql .= ' OR collection.dav_name = :up_to_slash OR collection.dav_name = :plus_slash '; 374 $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name); 375 $params[':plus_slash'] = $this->dav_name.'/'; 376 } 377 $sql .= 'ORDER BY LENGTH(collection.dav_name) DESC LIMIT 1'; 378 $qry = new AwlQuery( $sql, $params ); 379 if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) { 380 $this->collection = $row; 381 $this->collection->exists = true; 382 if ( $row->is_calendar == 't' ) 383 $this->collection->type = 'calendar'; 384 else if ( $row->is_addressbook == 't' ) 385 $this->collection->type = 'addressbook'; 386 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) ) 387 $this->collection->type = 'schedule-'. $matches[3]. 'box'; 388 else 389 $this->collection->type = 'collection'; 390 } 391 else if ( preg_match( '{^( ( / ([^/]+) / ) \.(in|out)/ ) [^/]*$}x', $this->dav_name, $matches ) ) { 392 // The request is for a scheduling inbox or outbox (or something inside one) and we should auto-create it 393 $params = array( ':username' => $matches[3], ':parent_container' => $matches[2], ':dav_name' => $matches[1] ); 394 $params[':boxname'] = ($matches[4] == 'in' ? ' Inbox' : ' Outbox'); 395 $this->collection_type = 'schedule-'. $matches[4]. 'box'; 396 $params[':resourcetypes'] = sprintf('<DAV::collection/><urn:ietf:params:xml:ns:caldav:%s/>', $this->collection_type ); 397 $sql = <<<EOSQL 398INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified, dav_etag, resourcetypes ) 399 VALUES( (SELECT user_no FROM usr WHERE username = text(:username)), 400 :parent_container, :dav_name, 401 (SELECT fullname FROM usr WHERE username = text(:username)) || :boxname, 402 FALSE, current_timestamp, current_timestamp, '1', :resourcetypes ) 403EOSQL; 404 $qry = new AwlQuery( $sql, $params ); 405 $qry->Exec('DAVResource'); 406 dbg_error_log( 'DAVResource', 'Created new collection as "%s".', trim($params[':boxname']) ); 407 408 $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth ); 409 $qry = new AwlQuery( $base_sql . ' dav_name = :raw_path', $params ); 410 if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) { 411 $this->collection = $row; 412 $this->collection->exists = true; 413 $this->collection->type = $this->collection_type; 414 } 415 } 416 else if ( preg_match( '#^(/([^/]+)/calendar-proxy-(read|write))/?[^/]*$#', $this->dav_name, $matches ) ) { 417 $this->collection->type = 'proxy'; 418 $this->_is_proxy_resource = true; 419 $this->proxy_type = $matches[3]; 420 $this->collection->dav_name = $this->dav_name; 421 $this->collection->dav_displayname = sprintf( '%s proxy %s', $matches[2], $matches[3] ); 422 $this->collection->exists = true; 423 $this->collection->parent_container = '/' . $matches[2] . '/'; 424 } 425 else if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name, $matches) 426 || preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches) ) { 427 $this->_is_principal = true; 428 $this->FetchPrincipal(); 429 $this->collection->is_principal = true; 430 $this->collection->type = 'principal'; 431 } 432 else if ( $this->dav_name == '/' ) { 433 $this->collection->dav_name = '/'; 434 $this->collection->type = 'root'; 435 $this->collection->exists = true; 436 $this->collection->displayname = $c->system_name; 437 $this->collection->default_privileges = (1 | 16 | 32); 438 $this->collection->parent_container = '/'; 439 } 440 else { 441 $sql = <<<EOSQL 442SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), p.principal_id, 443 p.type_id AS principal_type_id, p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges, 444 timezones.vtimezone, dav_binding.access_ticket_id, dav_binding.parent_container AS bind_parent_container, 445 dav_binding.dav_displayname, owner.dav_name AS bind_owner_url, dav_binding.dav_name AS bound_to, 446 dav_binding.external_url AS external_url, dav_binding.type AS external_type, dav_binding.bind_id AS bind_id 447FROM dav_binding 448 LEFT JOIN collection ON (collection.collection_id=bound_source_id) 449 LEFT JOIN principal p USING (user_no) 450 LEFT JOIN dav_principal owner ON (dav_binding.dav_owner_id=owner.principal_id) 451 LEFT JOIN timezones ON (collection.timezone=timezones.tzid) 452 WHERE dav_binding.dav_name = :raw_path 453EOSQL; 454 $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth ); 455 if ( !preg_match( '#/$#', $this->dav_name ) ) { 456 $sql .= ' OR dav_binding.dav_name = :up_to_slash OR collection.dav_name = :plus_slash OR dav_binding.dav_name = :plus_slash '; 457 $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name); 458 $params[':plus_slash'] = $this->dav_name.'/'; 459 } 460 $sql .= ' ORDER BY LENGTH(dav_binding.dav_name) DESC LIMIT 1'; 461 $qry = new AwlQuery( $sql, $params ); 462 if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) { 463 $this->collection = $row; 464 $this->collection->exists = true; 465 $this->collection->parent_set = $row->parent_container; 466 $this->collection->parent_container = $row->bind_parent_container; 467 $this->collection->bound_from = $row->dav_name; 468 $this->collection->dav_name = $row->bound_to; 469 if ( $row->is_calendar == 't' ) 470 $this->collection->type = 'calendar'; 471 else if ( $row->is_addressbook == 't' ) 472 $this->collection->type = 'addressbook'; 473 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) ) 474 $this->collection->type = 'schedule-'. $matches[3]. 'box'; 475 else 476 $this->collection->type = 'collection'; 477 if ( strlen($row->external_url) > 8 ) { 478 $this->_is_external = true; 479 if ( $row->external_type == 'calendar' ) 480 $this->collection->type = 'calendar'; 481 else if ( $row->external_type == 'addressbook' ) 482 $this->collection->type = 'addressbook'; 483 else 484 $this->collection->type = 'collection'; 485 } 486 $this->_is_binding = true; 487 $this->bound_from = str_replace( $row->bound_to, $row->dav_name, $this->dav_name); 488 if ( isset($row->access_ticket_id) ) { 489 if ( !isset($this->tickets) ) $this->tickets = array(); 490 $this->tickets[] = new DAVTicket($row->access_ticket_id); 491 } 492 } 493 else { 494 dbg_error_log( 'DAVResource', 'No collection for path "%s".', $this->dav_name ); 495 $this->collection->exists = false; 496 $this->collection->dav_name = preg_replace('{/[^/]*$}', '/', $this->dav_name); 497 } 498 } 499 500 } 501 502 /** 503 * Find the collection associated with this resource. 504 */ 505 protected function FetchCollection() { 506 global $session; 507 508 /** 509 * RFC4918, 8.3: Identifiers for collections SHOULD end in '/' 510 * - also discussed at more length in 5.2 511 * 512 * So we look for a collection which matches one of the following URLs: 513 * - The exact request. 514 * - If the exact request, doesn't end in '/', then the request URL with a '/' appended 515 * - The request URL truncated to the last '/' 516 * The collection URL for this request is therefore the longest row in the result, so we 517 * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1" 518 */ 519 dbg_error_log( 'DAVResource', ':FetchCollection: Looking for collection for "%s".', $this->dav_name ); 520 521 // Try and pull the answer out of a hat 522 $cache = getCacheInstance(); 523 $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $this->dav_name); 524 $cache_key = 'dav_resource'.$session->user_no; 525 $this->collection = $cache->get( $cache_ns, $cache_key ); 526 if ( $this->collection === false ) { 527 $this->ReadCollectionFromDatabase(); 528 if ( $this->collection->type != 'principal' ) { 529 $cache_ns = 'collection-'.$this->collection->dav_name; 530 @dbg_error_log( 'Cache', ':FetchCollection: Setting cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type ); 531 $cache->set( $cache_ns, $cache_key, $this->collection ); 532 } 533 @dbg_error_log( 'DAVResource', ':FetchCollection: Found collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type ); 534 } 535 else { 536 @dbg_error_log( 'Cache', ':FetchCollection: Got cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type ); 537 if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name, $matches) 538 || preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches) ) { 539 $this->_is_principal = true; 540 $this->FetchPrincipal(); 541 $this->collection->is_principal = true; 542 $this->collection->type = 'principal'; 543 } 544 @dbg_error_log( 'DAVResource', ':FetchCollection: Read cached collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type ); 545 } 546 547 if ( isset($this->collection->bound_from) ) { 548 $this->_is_binding = true; 549 $this->bound_from = str_replace( $this->collection->bound_to, $this->collection->bound_from, $this->dav_name); 550 if ( isset($this->collection->access_ticket_id) ) { 551 if ( !isset($this->tickets) ) $this->tickets = array(); 552 $this->tickets[] = new DAVTicket($this->collection->access_ticket_id); 553 } 554 } 555 556 $this->_is_collection = ( $this->_is_principal || $this->collection->dav_name == $this->dav_name || $this->collection->dav_name == $this->dav_name.'/' ); 557 if ( $this->_is_collection ) { 558 $this->dav_name = $this->collection->dav_name; 559 $this->resource_id = $this->collection->collection_id; 560 $this->_is_calendar = ($this->collection->type == 'calendar'); 561 $this->_is_addressbook = ($this->collection->type == 'addressbook'); 562 $this->contenttype = 'httpd/unix-directory'; 563 if ( !isset($this->exists) && isset($this->collection->exists) ) { 564 // If this seems peculiar it's because we only set it to false above... 565 $this->exists = $this->collection->exists; 566 } 567 if ( $this->exists ) { 568 if ( isset($this->collection->dav_etag) ) $this->unique_tag = '"'.$this->collection->dav_etag.'"'; 569 if ( isset($this->collection->created) ) $this->created = $this->collection->created; 570 if ( isset($this->collection->modified) ) $this->modified = $this->collection->modified; 571 if ( isset($this->collection->dav_displayname) ) $this->collection->displayname = $this->collection->dav_displayname; 572 } 573 else { 574 if ( !isset($this->parent) ) $this->GetParentContainer(); 575 $this->user_no = $this->parent->GetProperty('user_no'); 576 } 577 if ( isset($this->collection->resourcetypes) ) 578 $this->resourcetypes = $this->collection->resourcetypes; 579 else { 580 $this->resourcetypes = '<DAV::collection/>'; 581 if ( $this->_is_principal ) $this->resourcetypes .= '<DAV::principal/>'; 582 if ( $this->_is_addressbook ) $this->resourcetypes .= '<urn:ietf:params:xml:ns:carddav:addressbook/>'; 583 if ( $this->_is_calendar ) $this->resourcetypes .= '<urn:ietf:params:xml:ns:caldav:calendar/>'; 584 } 585 } 586 } 587 588 589 /** 590 * Find the principal associated with this resource. 591 */ 592 protected function FetchPrincipal() { 593 if ( isset($this->principal) ) return; 594 $this->principal = new DAVPrincipal( array( "path" => $this->bound_from() ) ); 595 if ( $this->_is_principal ) { 596 $this->exists = $this->principal->Exists(); 597 $this->collection->dav_name = $this->dav_name(); 598 $this->collection->type = 'principal'; 599 if ( $this->exists ) { 600 $this->collection = $this->principal->AsCollection(); 601 $this->displayname = $this->principal->GetProperty('displayname'); 602 $this->user_no = $this->principal->user_no(); 603 $this->resource_id = $this->principal->principal_id(); 604 $this->created = $this->principal->created; 605 $this->modified = $this->principal->modified; 606 $this->resourcetypes = $this->principal->resourcetypes; 607 } 608 } 609 } 610 611 612 /** 613 * Retrieve the actual resource. 614 */ 615 protected function FetchResource() { 616 if ( isset($this->exists) ) return; // True or false, we've got what we can already 617 if ( $this->_is_collection ) return; // We have all we're going to read 618 619 $sql = <<<EOQRY 620SELECT calendar_item.*, addressbook_resource.*, caldav_data.* 621 FROM caldav_data LEFT OUTER JOIN calendar_item USING (collection_id,dav_id) 622 LEFT OUTER JOIN addressbook_resource USING (dav_id) 623 WHERE caldav_data.dav_name = :dav_name 624EOQRY; 625 $params = array( ':dav_name' => $this->bound_from() ); 626 627 $qry = new AwlQuery( $sql, $params ); 628 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) { 629 $this->exists = true; 630 $row = $qry->Fetch(); 631 $this->FromRow($row); 632 } 633 else { 634 $this->exists = false; 635 } 636 } 637 638 639 /** 640 * Fetch any dead properties for this URL 641 */ 642 protected function FetchDeadProperties() { 643 if ( isset($this->dead_properties) ) return; 644 645 $this->dead_properties = array(); 646 if ( !$this->exists || !$this->_is_collection ) return; 647 648 $qry = new AwlQuery('SELECT property_name, property_value FROM property WHERE dav_name= :dav_name', array(':dav_name' => $this->dav_name) ); 649 if ( $qry->Exec('DAVResource') ) { 650 while ( $property = $qry->Fetch() ) { 651 $this->dead_properties[$property->property_name] = self::BuildDeadPropertyXML($property->property_name,$property->property_value); 652 } 653 } 654 } 655 656 /** 657 * FIXME: does this function return a string or an array, or either? 658 * It used to be string only, but b4fd9e2e changed successfully parsed 659 * values to array. However values not in angle brackets are passed 660 * through, and those seem to be the majority in my database?! 661 */ 662 public static function BuildDeadPropertyXML($property_name, $raw_string) { 663 if ( !preg_match('{^\s*<.*>\s*$}s', $raw_string) ) return $raw_string; 664 $xmlns = null; 665 if ( preg_match( '{^(.*):([^:]+)$}', $property_name, $matches) ) { 666 $xmlns = $matches[1]; 667 $property_name = $matches[2]; 668 } 669 $xml = sprintf('<%s%s>%s</%s>', $property_name, (isset($xmlns)?' xmlns="'.$xmlns.'"':''), $raw_string, $property_name); 670 $xml_parser = xml_parser_create_ns('UTF-8'); 671 $xml_tags = array(); 672 xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 ); 673 xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 ); 674 $rc = xml_parse_into_struct( $xml_parser, $xml, $xml_tags ); 675 if ( $rc == false ) { 676 $errno = xml_get_error_code($xml_parser); 677 dbg_error_log( 'ERROR', 'XML parsing error: %s (%d) at line %d, column %d', 678 xml_error_string($errno), $errno, 679 xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) ); 680 dbg_error_log( 'ERROR', "Error occurred in:\n%s\n",$xml); 681 if ($errno >= 200 && $errno < 300 && count($xml_tags) >= 3) { 682 // XML namespace error, but parsing was probably fine: continue and return tags (cf. #9) 683 dbg_error_log( 'ERROR', 'XML namespace error but tags extracted, trying to continue'); 684 } else { 685 return $raw_string; 686 } 687 } 688 xml_parser_free($xml_parser); 689 $position = 0; 690 $xmltree = BuildXMLTree( $xml_tags, $position); 691 return $xmltree->GetContent(); 692 } 693 694 /** 695 * Build permissions for this URL 696 */ 697 protected function FetchPrivileges() { 698 global $session, $request; 699 700 if ( $this->dav_name == '/' || $this->dav_name == '' || $this->_is_external ) { 701 $this->privileges = (1 | 16 | 32); // read + read-acl + read-current-user-privilege-set 702 dbg_error_log( 'DAVResource', ':FetchPrivileges: Read permissions for user accessing /' ); 703 return; 704 } 705 706 if ( $session->AllowedTo('Admin') ) { 707 $this->privileges = privilege_to_bits('all'); 708 dbg_error_log( 'DAVResource', ':FetchPrivileges: Full permissions for an administrator.' ); 709 return; 710 } 711 712 if ( $this->IsPrincipal() ) { 713 if ( !isset($this->principal) ) $this->FetchPrincipal(); 714 $this->privileges = $this->principal->Privileges(); 715 dbg_error_log( 'DAVResource', ':FetchPrivileges: Privileges of "%s" for user accessing principal "%s"', $this->privileges, $this->principal->username() ); 716 return; 717 } 718 719 if ( ! isset($this->collection) ) $this->FetchCollection(); 720 $this->privileges = 0; 721 if ( !isset($this->collection->path_privs) ) { 722 if ( !isset($this->parent) ) $this->GetParentContainer(); 723 724 $this->collection->path_privs = $this->parent->Privileges(); 725 $this->collection->user_no = $this->parent->GetProperty('user_no'); 726 $this->collection->principal_id = $this->parent->GetProperty('principal_id'); 727 } 728 729 $this->privileges = $this->collection->path_privs; 730 if ( is_string($this->privileges) ) $this->privileges = bindec( $this->privileges ); 731 732 dbg_error_log( 'DAVResource', ':FetchPrivileges: Privileges of "%s" for user "%s" accessing "%s"', 733 decbin($this->privileges), $session->username, $this->dav_name() ); 734 735 if ( isset($request->ticket) && $request->ticket->MatchesPath($this->bound_from()) ) { 736 $this->privileges |= $request->ticket->privileges(); 737 dbg_error_log( 'DAVResource', ':FetchPrivileges: Applying permissions for ticket "%s" now: %s', $request->ticket->id(), decbin($this->privileges) ); 738 } 739 740 if ( isset($this->tickets) ) { 741 if ( !isset($this->resource_id) ) $this->FetchResource(); 742 foreach( $this->tickets AS $k => $ticket ) { 743 if ( $ticket->MatchesResource($this->resource_id()) || $ticket->MatchesPath($this->bound_from()) ) { 744 $this->privileges |= $ticket->privileges(); 745 dbg_error_log( 'DAVResource', ':FetchPrivileges: Applying permissions for ticket "%s" now: %s', $ticket->id(), decbin($this->privileges) ); 746 } 747 } 748 } 749 } 750 751 752 /** 753 * Get a DAVResource which is the parent to this resource. 754 */ 755 function GetParentContainer() { 756 if ( $this->dav_name == '/' ) return null; 757 if ( !isset($this->parent) ) { 758 if ( $this->_is_collection ) { 759 dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() ); 760 $this->parent = new DAVResource( $this->parent_path() ); 761 } 762 else { 763 dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() ); 764 $this->parent = new DAVResource($this->collection->dav_name); 765 } 766 } 767 return $this->parent; 768 } 769 770 771 /** 772 * Fetch the parent to this resource. This is deprecated - use GetParentContainer() instead. 773 * @deprecated 774 */ 775 function FetchParentContainer() { 776 deprecated('DAVResource::FetchParentContainer'); 777 return $this->GetParentContainer(); 778 } 779 780 781 /** 782 * Return the privileges bits for the current session user to this resource 783 */ 784 function Privileges() { 785 if ( !isset($this->privileges) ) $this->FetchPrivileges(); 786 return $this->privileges; 787 } 788 789 790 /** 791 * Does the user have the privileges to do what is requested. 792 * @param $do_what mixed The request privilege name, or array of privilege names, to be checked. 793 * @param $any boolean Whether we accept any of the privileges. The default is true, unless the requested privilege is 'all', when it is false. 794 * @return boolean Whether they do have one of those privileges against this resource. 795 */ 796 function HavePrivilegeTo( $do_what, $any = null ) { 797 if ( !isset($this->privileges) ) $this->FetchPrivileges(); 798 if ( !isset($any) ) $any = ($do_what != 'all'); 799 $test_bits = privilege_to_bits( $do_what ); 800 dbg_error_log( 'DAVResource', 'Testing %s privileges of "%s" (%s) against allowed "%s" => "%s" (%s)', ($any?'any':'exactly'), 801 $do_what, decbin($test_bits), decbin($this->privileges), ($this->privileges & $test_bits), decbin($this->privileges & $test_bits) ); 802 if ( $any ) { 803 return ($this->privileges & $test_bits) > 0; 804 } 805 else { 806 return ($this->privileges & $test_bits) == $test_bits; 807 } 808 } 809 810 811 /** 812 * Check if we have the needed privilege or send an error response. If the user does not have the privileges then 813 * the call will not return, and an XML error document will be output. 814 * 815 * @param string $privilege The name of the needed privilege. 816 * @param boolean $any Whether we accept any of the privileges. The default is true, unless the requested privilege is 'all', when it is false. 817 */ 818 function NeedPrivilege( $privilege, $any = null ) { 819 global $request; 820 821 // Do the test 822 if ( $this->HavePrivilegeTo($privilege, $any) ) return; 823 824 // They failed, so output the error 825 $request->NeedPrivilege( $privilege, $this->dav_name ); 826 exit(0); // Unecessary, but might clarify things 827 } 828 829 830 /** 831 * Returns the array of privilege names converted into XMLElements 832 */ 833 function BuildPrivileges( $privilege_names=null, &$xmldoc=null ) { 834 if ( $privilege_names == null ) { 835 if ( !isset($this->privileges) ) $this->FetchPrivileges(); 836 $privilege_names = bits_to_privilege($this->privileges, ($this->_is_collection ? $this->collection->type : null ) ); 837 } 838 return privileges_to_XML( $privilege_names, $xmldoc); 839 } 840 841 842 /** 843 * Returns the array of supported methods 844 */ 845 function FetchSupportedMethods( ) { 846 if ( isset($this->supported_methods) ) return $this->supported_methods; 847 848 $this->supported_methods = array( 849 'OPTIONS' => '', 850 'PROPFIND' => '', 851 'REPORT' => '', 852 'DELETE' => '', 853 'LOCK' => '', 854 'UNLOCK' => '', 855 'MOVE' => '' 856 ); 857 if ( $this->IsCollection() ) { 858/* if ( $this->IsPrincipal() ) { 859 $this->supported_methods['MKCALENDAR'] = ''; 860 $this->supported_methods['MKCOL'] = ''; 861 } */ 862 switch ( $this->collection->type ) { 863 case 'root': 864 case 'email': 865 // We just override the list completely here. 866 $this->supported_methods = array( 867 'OPTIONS' => '', 868 'PROPFIND' => '', 869 'REPORT' => '' 870 ); 871 break; 872 873 case 'schedule-outbox': 874 $this->supported_methods = array_merge( 875 $this->supported_methods, 876 array( 877 'POST' => '', 'PROPPATCH' => '', 'MKTICKET' => '', 'DELTICKET' => '' 878 ) 879 ); 880 break; 881 case 'schedule-inbox': 882 case 'calendar': 883 $this->supported_methods['GET'] = ''; 884 $this->supported_methods['PUT'] = ''; 885 $this->supported_methods['HEAD'] = ''; 886 $this->supported_methods['MKTICKET'] = ''; 887 $this->supported_methods['DELTICKET'] = ''; 888 $this->supported_methods['ACL'] = ''; 889 break; 890 case 'collection': 891 $this->supported_methods['MKTICKET'] = ''; 892 $this->supported_methods['DELTICKET'] = ''; 893 $this->supported_methods['BIND'] = ''; 894 $this->supported_methods['ACL'] = ''; 895 case 'principal': 896 $this->supported_methods['GET'] = ''; 897 $this->supported_methods['HEAD'] = ''; 898 $this->supported_methods['MKCOL'] = ''; 899 $this->supported_methods['MKCALENDAR'] = ''; 900 $this->supported_methods['PROPPATCH'] = ''; 901 $this->supported_methods['BIND'] = ''; 902 $this->supported_methods['ACL'] = ''; 903 break; 904 } 905 } 906 else { 907 $this->supported_methods = array_merge( 908 $this->supported_methods, 909 array( 910 'GET' => '', 'HEAD' => '', 'PUT' => '', 'MKTICKET' => '', 'DELTICKET' => '' 911 ) 912 ); 913 } 914 915 return $this->supported_methods; 916 } 917 918 919 /** 920 * Returns the array of supported methods converted into XMLElements 921 */ 922 function BuildSupportedMethods( ) { 923 if ( !isset($this->supported_methods) ) $this->FetchSupportedMethods(); 924 $methods = array(); 925 foreach( $this->supported_methods AS $k => $v ) { 926// dbg_error_log( 'DAVResource', ':BuildSupportedMethods: Adding method "%s" which is "%s".', $k, $v ); 927 $methods[] = new XMLElement( 'supported-method', null, array('name' => $k) ); 928 } 929 return $methods; 930 } 931 932 933 /** 934 * Returns the array of supported reports 935 */ 936 function FetchSupportedReports( ) { 937 if ( isset($this->supported_reports) ) return $this->supported_reports; 938 939 $this->supported_reports = array( 940 'DAV::principal-property-search' => '', 941 'DAV::principal-search-property-set' => '', 942 'DAV::expand-property' => '', 943 'DAV::principal-match' => '', 944 'DAV::sync-collection' => '' 945 ); 946 947 if ( !isset($this->collection) ) $this->FetchCollection(); 948 949 if ( $this->collection->is_calendar ) { 950 $this->supported_reports = array_merge( 951 $this->supported_reports, 952 array( 953 'urn:ietf:params:xml:ns:caldav:calendar-query' => '', 954 'urn:ietf:params:xml:ns:caldav:calendar-multiget' => '', 955 'urn:ietf:params:xml:ns:caldav:free-busy-query' => '' 956 ) 957 ); 958 } 959 if ( $this->collection->is_addressbook ) { 960 $this->supported_reports = array_merge( 961 $this->supported_reports, 962 array( 963 'urn:ietf:params:xml:ns:carddav:addressbook-query' => '', 964 'urn:ietf:params:xml:ns:carddav:addressbook-multiget' => '' 965 ) 966 ); 967 } 968 return $this->supported_reports; 969 } 970 971 972 /** 973 * Returns the array of supported reports converted into XMLElements 974 */ 975 function BuildSupportedReports( &$reply ) { 976 if ( !isset($this->supported_reports) ) $this->FetchSupportedReports(); 977 $reports = array(); 978 foreach( $this->supported_reports AS $k => $v ) { 979 dbg_error_log( 'DAVResource', ':BuildSupportedReports: Adding supported report "%s" which is "%s".', $k, $v ); 980 $report = new XMLElement('report'); 981 $reply->NSElement($report, $k ); 982 $reports[] = new XMLElement('supported-report', $report ); 983 } 984 return $reports; 985 } 986 987 988 /** 989 * Fetches an array of the access_ticket records applying to this path 990 */ 991 function FetchTickets( ) { 992 global $c; 993 if ( isset($this->access_tickets) ) return; 994 $this->access_tickets = array(); 995 996 $sql = 997'SELECT access_ticket.*, COALESCE( resource.dav_name, collection.dav_name) AS target_dav_name, 998 (access_ticket.expires < current_timestamp) AS expired, 999 dav_principal.dav_name AS principal_dav_name, 1000 EXTRACT( \'epoch\' FROM (access_ticket.expires - current_timestamp)) AS seconds, 1001 path_privs(access_ticket.dav_owner_id,collection.dav_name,:scan_depth) AS grantor_collection_privileges 1002 FROM access_ticket JOIN collection ON (target_collection_id = collection_id) 1003 JOIN dav_principal ON (dav_owner_id = principal_id) 1004 LEFT JOIN caldav_data resource ON (resource.dav_id = access_ticket.target_resource_id) 1005 WHERE target_collection_id = :collection_id '; 1006 $params = array(':collection_id' => $this->collection->collection_id, ':scan_depth' => $c->permission_scan_depth); 1007 if ( $this->IsCollection() ) { 1008 $sql .= 'AND target_resource_id IS NULL'; 1009 } 1010 else { 1011 if ( !isset($this->exists) ) $this->FetchResource(); 1012 $sql .= 'AND target_resource_id = :dav_id'; 1013 $params[':dav_id'] = $this->resource->dav_id; 1014 } 1015 if ( isset($this->exists) && !$this->exists ) return; 1016 1017 $qry = new AwlQuery( $sql, $params ); 1018 if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() ) { 1019 while( $ticket = $qry->Fetch() ) { 1020 $this->access_tickets[] = $ticket; 1021 } 1022 } 1023 } 1024 1025 1026 /** 1027 * Returns the array of tickets converted into XMLElements 1028 * 1029 * If the current user does not have DAV::read-acl privilege on this resource they 1030 * will only get to see the tickets where they are the owner, or which they supplied 1031 * along with the request. 1032 * 1033 * @param &XMLDocument $reply A reference to the XMLDocument used to construct the reply 1034 * @return XMLTreeFragment A fragment of an XMLDocument to go in the reply 1035 */ 1036 function BuildTicketinfo( &$reply ) { 1037 global $session, $request; 1038 1039 if ( !isset($this->access_tickets) ) $this->FetchTickets(); 1040 $tickets = array(); 1041 $show_all = $this->HavePrivilegeTo('DAV::read-acl'); 1042 foreach( $this->access_tickets AS $meh => $trow ) { 1043 if ( !$show_all && ( $trow->dav_owner_id == $session->principal_id || $request->ticket->id() == $trow->ticket_id ) ) continue; 1044 dbg_error_log( 'DAVResource', ':BuildTicketinfo: Adding access_ticket "%s" which is "%s".', $trow->ticket_id, $trow->privileges ); 1045 $ticket = new XMLElement( $reply->Tag( 'ticketinfo', 'http://www.xythos.com/namespaces/StorageServer', 'TKT' ) ); 1046 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:id', $trow->ticket_id ); 1047 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:owner', $reply->href( ConstructURL($trow->principal_dav_name)) ); 1048 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:timeout', (isset($trow->seconds) ? sprintf( 'Seconds-%d', $trow->seconds) : 'infinity') ); 1049 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:visits', 'infinity' ); 1050 $privs = array(); 1051 foreach( bits_to_privilege(bindec($trow->privileges) & bindec($trow->grantor_collection_privileges) ) AS $k => $v ) { 1052 $privs[] = $reply->NewXMLElement($v); 1053 } 1054 $reply->NSElement($ticket, 'DAV::privilege', $privs ); 1055 $tickets[] = $ticket; 1056 } 1057 return $tickets; 1058 } 1059 1060 1061 /** 1062 * Checks whether the resource is locked, returning any lock token, or false 1063 * 1064 * @todo This logic does not catch all locking scenarios. For example an infinite 1065 * depth request should check the permissions for all collections and resources within 1066 * that. At present we only maintain permissions on a per-collection basis though. 1067 */ 1068 function IsLocked( $depth = 0 ) { 1069 if ( !isset($this->_locks_found) ) { 1070 $this->_locks_found = array(); 1071 /** 1072 * Find the locks that might apply and load them into an array 1073 */ 1074 $sql = 'SELECT * FROM locks WHERE :this_path::text ~ (\'^\'||dav_name||:match_end)::text'; 1075 $qry = new AwlQuery($sql, array( ':this_path' => $this->dav_name, ':match_end' => ($depth == DEPTH_INFINITY ? '' : '$') ) ); 1076 if ( $qry->Exec('DAVResource',__LINE__,__FILE__) ) { 1077 while( $lock_row = $qry->Fetch() ) { 1078 $this->_locks_found[$lock_row->opaquelocktoken] = $lock_row; 1079 } 1080 } 1081 else { 1082 $this->DoResponse(500,i18n("Database Error")); 1083 // Does not return. 1084 } 1085 } 1086 1087 foreach( $this->_locks_found AS $lock_token => $lock_row ) { 1088 if ( $lock_row->depth == DEPTH_INFINITY || $lock_row->dav_name == $this->dav_name ) { 1089 return $lock_token; 1090 } 1091 } 1092 1093 return false; // Nothing matched 1094 } 1095 1096 1097 /** 1098 * Checks whether this resource is a collection 1099 */ 1100 function IsCollection() { 1101 return $this->_is_collection; 1102 } 1103 1104 1105 /** 1106 * Checks whether this resource is a principal 1107 */ 1108 function IsPrincipal() { 1109 return $this->_is_collection && $this->_is_principal; 1110 } 1111 1112 1113 /** 1114 * Checks whether this resource is a calendar 1115 */ 1116 function IsCalendar() { 1117 return $this->_is_collection && $this->_is_calendar; 1118 } 1119 1120 1121 /** 1122 * Checks whether this resource is a proxy collection 1123 * @param string $type The type of proxy collection, 'read', 'write' or 'any' 1124 */ 1125 function IsProxyCollection( $type = 'any' ) { 1126 if ( $this->_is_proxy_resource ) { 1127 return ($type == 'any' || $type == $this->proxy_type); 1128 } 1129 return false; 1130 } 1131 1132 1133 /** 1134 * Checks whether this resource is a scheduling inbox/outbox collection 1135 * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any' 1136 */ 1137 function IsSchedulingCollection( $type = 'any' ) { 1138 if ( $this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) { 1139 return ($type == 'any' || $type == $matches[1]); 1140 } 1141 return false; 1142 } 1143 1144 1145 /** 1146 * Checks whether this resource is IN a scheduling inbox/outbox collection 1147 * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any' 1148 */ 1149 function IsInSchedulingCollection( $type = 'any' ) { 1150 if ( !$this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) { 1151 return ($type == 'any' || $type == $matches[1]); 1152 } 1153 return false; 1154 } 1155 1156 1157 /** 1158 * Checks whether this resource is an addressbook 1159 */ 1160 function IsAddressbook() { 1161 return $this->_is_collection && $this->_is_addressbook; 1162 } 1163 1164 1165 /** 1166 * Checks whether this resource is a bind to another resource 1167 */ 1168 function IsBinding() { 1169 return $this->_is_binding; 1170 } 1171 1172 1173 /** 1174 * Checks whether this resource is a bind to an external resource 1175 */ 1176 function IsExternal() { 1177 return $this->_is_external; 1178 } 1179 1180 1181 /** 1182 * Checks whether this resource actually exists, in the virtual sense, within the hierarchy 1183 */ 1184 function Exists() { 1185 if ( ! isset($this->exists) ) { 1186 if ( $this->IsPrincipal() ) { 1187 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1188 $this->exists = $this->principal->Exists(); 1189 } 1190 else if ( ! $this->IsCollection() ) { 1191 if ( !isset($this->resource) ) $this->FetchResource(); 1192 } 1193 } 1194// dbg_error_log('DAVResource',' Checking whether "%s" exists. It would appear %s.', $this->dav_name, ($this->exists ? 'so' : 'not') ); 1195 return $this->exists; 1196 } 1197 1198 1199 /** 1200 * Checks whether the container for this resource actually exists, in the virtual sense, within the hierarchy 1201 */ 1202 function ContainerExists() { 1203 if ( $this->collection->dav_name != $this->dav_name ) { 1204 return $this->collection->exists; 1205 } 1206 $parent = $this->GetParentContainer(); 1207 return $parent->Exists(); 1208 } 1209 1210 1211 /** 1212 * Returns the URL of our resource 1213 * @return string 1214 */ 1215 function url() { 1216 if ( !isset($this->dav_name) ) { 1217 throw Exception("What! How can dav_name not be set?"); 1218 } 1219 return ConstructURL($this->dav_name); 1220 } 1221 1222 1223 /** 1224 * Returns the dav_name of the resource in our internal namespace 1225 * @return string 1226 */ 1227 function dav_name() { 1228 if ( isset($this->dav_name) ) return $this->dav_name; 1229 return null; 1230 } 1231 1232 1233 /** 1234 * Returns the dav_name of the resource we are bound to, within our internal namespace 1235 * @return string 1236 */ 1237 function bound_from() { 1238 if ( isset($this->bound_from) ) return $this->bound_from; 1239 return $this->dav_name(); 1240 } 1241 1242 1243 /** 1244 * Sets the dav_name of the resource we are bound as 1245 */ 1246 function set_bind_location( $new_dav_name ) { 1247 if ( !isset($this->bound_from) && isset($this->dav_name) ) { 1248 $this->bound_from = $this->dav_name; 1249 } 1250 $this->dav_name = $new_dav_name; 1251 return $this->dav_name; 1252 } 1253 1254 1255 /** 1256 * Returns the dav_name of the resource in our internal namespace 1257 */ 1258 function parent_path() { 1259 if ( $this->IsCollection() ) { 1260 if ( !isset($this->collection) ) $this->FetchCollection(); 1261 if ( !isset($this->collection->parent_container) ) { 1262 $this->collection->parent_container = preg_replace( '{[^/]+/$}', '', $this->bound_from()); 1263 } 1264 return $this->collection->parent_container; 1265 } 1266 return preg_replace( '{[^/]+$}', '', $this->bound_from()); 1267 } 1268 1269 1270 1271 /** 1272 * Returns the principal-URL for this resource 1273 */ 1274 function principal_url() { 1275 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1276 return $this->principal->url(); 1277 } 1278 1279 1280 /** 1281 * Returns the internal user_no for the principal for this resource 1282 */ 1283 function user_no() { 1284 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1285 return $this->principal->user_no(); 1286 } 1287 1288 1289 /** 1290 * Returns the internal collection_id for this collection, or the collection containing this resource 1291 */ 1292 function collection_id() { 1293 if ( !isset($this->collection) ) $this->FetchCollection(); 1294 return $this->collection->collection_id; 1295 } 1296 1297 1298 /** 1299 * Returns the name of the timezone for this collection, or the collection containing this resource 1300 */ 1301 function timezone_name() { 1302 if ( !isset($this->collection) ) $this->FetchCollection(); 1303 return $this->collection->timezone; 1304 } 1305 1306 1307 /** 1308 * Returns the database row for this resource 1309 */ 1310 function resource() { 1311 if ( !isset($this->resource) ) $this->FetchResource(); 1312 return $this->resource; 1313 } 1314 1315 1316 /** 1317 * Returns the unique_tag (ETag or getctag) for this resource 1318 */ 1319 function unique_tag() { 1320 if ( isset($this->unique_tag) ) return $this->unique_tag; 1321 if ( $this->IsPrincipal() && !isset($this->principal) ) { 1322 $this->FetchPrincipal(); 1323 $this->unique_tag = $this->principal->unique_tag(); 1324 } 1325 else if ( !$this->_is_collection && !isset($this->resource) ) $this->FetchResource(); 1326 1327 if ( $this->exists !== true || !isset($this->unique_tag) ) $this->unique_tag = ''; 1328 1329 return $this->unique_tag; 1330 } 1331 1332 1333 /** 1334 * Returns the definitive resource_id for this resource - usually a dav_id 1335 */ 1336 function resource_id() { 1337 if ( isset($this->resource_id) ) return $this->resource_id; 1338 if ( $this->IsPrincipal() && !isset($this->principal) ) $this->FetchPrincipal(); 1339 else if ( !$this->_is_collection && !isset($this->resource) ) $this->FetchResource(); 1340 1341 if ( $this->exists !== true || !isset($this->resource_id) ) $this->resource_id = null; 1342 1343 return $this->resource_id; 1344 } 1345 1346 1347 /** 1348 * Returns the current sync_token for this collection, or the containing collection 1349 */ 1350 function sync_token( $cachedOK = true ) { 1351 dbg_error_log('DAVResource', 'Request for a%scached sync-token', ($cachedOK ? ' ' : 'n un') ); 1352 if ( $this->IsPrincipal() ) return null; 1353 if ( $this->collection_id() == 0 ) return null; 1354 if ( !isset($this->sync_token) || !$cachedOK ) { 1355 $sql = 'SELECT new_sync_token( 0, :collection_id) AS sync_token'; 1356 $params = array( ':collection_id' => $this->collection_id()); 1357 $qry = new AwlQuery($sql, $params ); 1358 if ( !$qry->Exec() || !$row = $qry->Fetch() ) { 1359 if ( !$qry->QDo('SELECT new_sync_token( 0, :collection_id) AS sync_token', $params) ) throw new Exception('Problem with database query'); 1360 $row = $qry->Fetch(); 1361 } 1362 $this->sync_token = 'data:,'.$row->sync_token; 1363 } 1364 dbg_error_log('DAVResource', 'Returning sync token of "%s"', $this->sync_token ); 1365 return $this->sync_token; 1366 } 1367 1368 /** 1369 * Checks whether the target collection is publicly_readable 1370 */ 1371 function IsPublic() { 1372 return ( isset($this->collection->publicly_readable) && $this->collection->publicly_readable == 't' ); 1373 } 1374 1375 1376 /** 1377 * Checks whether the target collection is for public events only 1378 */ 1379 function IsPublicOnly() { 1380 return ( isset($this->collection->publicly_events_only) && $this->collection->publicly_events_only == 't' ); 1381 } 1382 1383 1384 /** 1385 * Return the type of whatever contains this resource, or would if it existed. 1386 */ 1387 function ContainerType() { 1388 if ( $this->IsPrincipal() ) return 'root'; 1389 if ( !$this->IsCollection() ) return $this->collection->type; 1390 1391 if ( ! isset($this->collection->parent_container) ) return null; 1392 1393 if ( isset($this->parent_container_type) ) return $this->parent_container_type; 1394 1395 if ( preg_match('#/[^/]+/#', $this->collection->parent_container) ) { 1396 $this->parent_container_type = 'principal'; 1397 } 1398 else { 1399 $qry = new AwlQuery('SELECT * FROM collection WHERE dav_name = :parent_name', 1400 array( ':parent_name' => $this->collection->parent_container ) ); 1401 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 && $parent = $qry->Fetch() ) { 1402 if ( $parent->is_calendar == 't' ) 1403 $this->parent_container_type = 'calendar'; 1404 else if ( $parent->is_addressbook == 't' ) 1405 $this->parent_container_type = 'addressbook'; 1406 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) ) 1407 $this->parent_container_type = 'schedule-'. $matches[3]. 'box'; 1408 else 1409 $this->parent_container_type = 'collection'; 1410 } 1411 else 1412 $this->parent_container_type = null; 1413 } 1414 return $this->parent_container_type; 1415 } 1416 1417 1418 /** 1419 * BuildACE - construct an XMLElement subtree for a DAV::ace 1420 */ 1421 function BuildACE( &$xmldoc, $privs, $principal ) { 1422 $privilege_names = bits_to_privilege($privs, ($this->_is_collection ? $this->collection->type : 'resource')); 1423 $privileges = array(); 1424 foreach( $privilege_names AS $k ) { 1425 $privilege = new XMLElement('privilege'); 1426 if ( isset($xmldoc) ) 1427 $xmldoc->NSElement($privilege,$k); 1428 else 1429 $privilege->NewElement($k); 1430 $privileges[] = $privilege; 1431 } 1432 $ace = new XMLElement('ace', array( 1433 new XMLElement('principal', $principal), 1434 new XMLElement('grant', $privileges ) ) 1435 ); 1436 return $ace; 1437 } 1438 1439 /** 1440 * Return ACL settings 1441 */ 1442 function GetACL( &$xmldoc ) { 1443 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1444 $default_privs = $this->principal->default_privileges; 1445 if ( isset($this->collection->default_privileges) ) $default_privs = $this->collection->default_privileges; 1446 1447 $acl = array(); 1448 $acl[] = $this->BuildACE($xmldoc, pow(2,25) - 1, new XMLElement('property', new XMLElement('owner')) ); 1449 1450 $qry = new AwlQuery('SELECT dav_principal.dav_name, grants.* FROM grants JOIN dav_principal ON (to_principal=principal_id) WHERE by_collection = :collection_id OR by_principal = :principal_id ORDER BY by_collection', 1451 array( ':collection_id' => $this->collection->collection_id, 1452 ':principal_id' => $this->principal->principal_id() ) ); 1453 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) { 1454 $by_collection = null; 1455 while( $grant = $qry->Fetch() ) { 1456 if ( !isset($by_collection) ) $by_collection = isset($grant->by_collection); 1457 if ( $by_collection && !isset($grant->by_collection) ) break; 1458 $acl[] = $this->BuildACE($xmldoc, $grant->privileges, $xmldoc->href(ConstructURL($grant->dav_name)) ); 1459 } 1460 } 1461 1462 $acl[] = $this->BuildACE($xmldoc, $default_privs, new XMLElement('authenticated') ); 1463 1464 return $acl; 1465 1466 } 1467 1468 1469 /** 1470 * Return general server-related properties, in plain form 1471 */ 1472 function GetProperty( $name ) { 1473// dbg_error_log( 'DAVResource', ':GetProperty: Fetching "%s".', $name ); 1474 $value = null; 1475 1476 switch( $name ) { 1477 case 'collection_id': 1478 return $this->collection_id(); 1479 break; 1480 1481 case 'principal_id': 1482 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1483 return $this->principal->principal_id(); 1484 break; 1485 1486 case 'resourcetype': 1487 if ( isset($this->resourcetypes) ) { 1488 $this->resourcetypes = preg_replace('{^\s*<(.*)/>\s*$}', '$1', $this->resourcetypes); 1489 $type_list = preg_split('{(/>\s*<|\n)}', $this->resourcetypes); 1490 foreach( $type_list AS $k => $resourcetype ) { 1491 if ( preg_match( '{^([^:]+):([^:]+) \s+ xmlns:([^=]+)="([^"]+)" \s* $}x', $resourcetype, $matches ) ) { 1492 $type_list[$k] = $matches[4] .':' .$matches[2]; 1493 } 1494 else if ( preg_match( '{^([^:]+) \s+ xmlns="([^"]+)" \s* $}x', $resourcetype, $matches ) ) { 1495 $type_list[$k] = $matches[2] .':' .$matches[1]; 1496 } 1497 } 1498 return $type_list; 1499 } 1500 1501 case 'resource': 1502 if ( !isset($this->resource) ) $this->FetchResource(); 1503 return clone($this->resource); 1504 break; 1505 1506 case 'dav-data': 1507 if ( !isset($this->resource) ) $this->FetchResource(); 1508 dbg_error_log( 'DAVResource', ':GetProperty: dav-data: fetched resource does%s exist.', ($this->exists?'':' not') ); 1509 return $this->resource->caldav_data; 1510 break; 1511 1512 case 'principal': 1513 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1514 return clone($this->principal); 1515 break; 1516 1517 default: 1518 if ( isset($this->{$name}) ) { 1519 if ( ! is_object($this->{$name}) ) return $this->{$name}; 1520 return clone($this->{$name}); 1521 } 1522 if ( $this->_is_principal ) { 1523 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1524 if ( isset($this->principal->{$name}) ) return $this->principal->{$name}; 1525 if ( isset($this->collection->{$name}) ) return $this->collection->{$name}; 1526 } 1527 else if ( $this->_is_collection ) { 1528 if ( isset($this->collection->{$name}) ) return $this->collection->{$name}; 1529 if ( isset($this->principal->{$name}) ) return $this->principal->{$name}; 1530 } 1531 else { 1532 if ( !isset($this->resource) ) $this->FetchResource(); 1533 if ( isset($this->resource->{$name}) ) return $this->resource->{$name}; 1534 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1535 if ( isset($this->principal->{$name}) ) return $this->principal->{$name}; 1536 if ( isset($this->collection->{$name}) ) return $this->collection->{$name}; 1537 } 1538 if ( isset($this->{$name}) ) { 1539 if ( ! is_object($this->{$name}) ) return $this->{$name}; 1540 return clone($this->{$name}); 1541 } 1542 // dbg_error_log( 'DAVResource', ':GetProperty: Failed to find property "%s" on "%s".', $name, $this->dav_name ); 1543 } 1544 1545 return $value; 1546 } 1547 1548 1549 /** 1550 * Return an array which is an expansion of the DAV::allprop 1551 */ 1552 function DAV_AllProperties() { 1553 if ( !isset($this->dead_properties) ) $this->FetchDeadProperties(); 1554 $allprop = array_merge( (isset($this->dead_properties)?array_keys($this->dead_properties):array()), 1555 (isset($include_properties)?$include_properties:array()), 1556 array( 1557 'DAV::getcontenttype', 'DAV::resourcetype', 'DAV::getcontentlength', 'DAV::displayname', 'DAV::getlastmodified', 1558 'DAV::creationdate', 'DAV::getetag', 'DAV::getcontentlanguage', 'DAV::supportedlock', 'DAV::lockdiscovery', 1559 'DAV::owner', 'DAV::principal-URL', 'DAV::current-user-principal', 1560 'urn:ietf:params:xml:ns:carddav:max-resource-size', 'urn:ietf:params:xml:ns:carddav:supported-address-data', 1561 'urn:ietf:params:xml:ns:carddav:addressbook-description', 'urn:ietf:params:xml:ns:carddav:addressbook-home-set' 1562 ) ); 1563 1564 return $allprop; 1565 } 1566 1567 1568 /** 1569 * Return general server-related properties for this URL 1570 */ 1571 function ResourceProperty( $tag, $prop, &$reply, &$denied ) { 1572 global $c, $session, $request; 1573 1574// dbg_error_log( 'DAVResource', 'Processing "%s" on "%s".', $tag, $this->dav_name ); 1575 1576 if ( $reply === null ) $reply = $GLOBALS['reply']; 1577 1578 switch( $tag ) { 1579 case 'DAV::allprop': 1580 $property_list = $this->DAV_AllProperties(); 1581 $discarded = array(); 1582 foreach( $property_list AS $k => $v ) { 1583 $this->ResourceProperty($v, $prop, $reply, $discarded); 1584 } 1585 break; 1586 1587 case 'DAV::href': 1588 $prop->NewElement('href', ConstructURL($this->dav_name) ); 1589 break; 1590 1591 case 'DAV::resource-id': 1592 if ( $this->resource_id > 0 ) 1593 $reply->DAVElement( $prop, 'resource-id', $reply->href(ConstructURL('/.resources/'.$this->resource_id) ) ); 1594 else 1595 return false; 1596 break; 1597 1598 case 'DAV::parent-set': 1599 $sql = <<<EOQRY 1600SELECT b.parent_container FROM dav_binding b JOIN collection c ON (b.bound_source_id=c.collection_id) 1601 WHERE regexp_replace( b.dav_name, '^.*/', c.dav_name ) = :bound_from 1602EOQRY; 1603 $qry = new AwlQuery($sql, array( ':bound_from' => $this->bound_from() ) ); 1604 $parents = array(); 1605 if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() > 0 ) { 1606 while( $row = $qry->Fetch() ) { 1607 $parents[$row->parent_container] = true; 1608 } 1609 } 1610 $parents[preg_replace( '{(?<=/)[^/]+/?$}','',$this->bound_from())] = true; 1611 $parents[preg_replace( '{(?<=/)[^/]+/?$}','',$this->dav_name())] = true; 1612 1613 $parent_set = $reply->DAVElement( $prop, 'parent-set' ); 1614 foreach( $parents AS $parent => $v ) { 1615 if ( preg_match( '{^(.*)?/([^/]+)/?$}', $parent, $matches ) ) { 1616 $reply->DAVElement($parent_set, 'parent', array( 1617 new XMLElement( 'href', ConstructURL($matches[1])), 1618 new XMLElement( 'segment', $matches[2]) 1619 )); 1620 } 1621 else if ( $parent == '/' ) { 1622 $reply->DAVElement($parent_set, 'parent', array( 1623 new XMLElement( 'href', '/'), 1624 new XMLElement( 'segment', ( ConstructURL('/') == '/caldav.php/' ? 'caldav.php' : '')) 1625 )); 1626 } 1627 } 1628 break; 1629 1630 case 'DAV::getcontenttype': 1631 if ( !isset($this->contenttype) && !$this->_is_collection && !isset($this->resource) ) $this->FetchResource(); 1632 $prop->NewElement('getcontenttype', $this->contenttype ); 1633 break; 1634 1635 case 'DAV::resourcetype': 1636 $resourcetypes = $prop->NewElement('resourcetype' ); 1637 if ( $this->_is_collection ) { 1638 $type_list = $this->GetProperty('resourcetype'); 1639 if ( !is_array($type_list) ) return true; 1640 // dbg_error_log( 'DAVResource', ':ResourceProperty: "%s" are "%s".', $tag, implode(', ',$type_list) ); 1641 foreach( $type_list AS $k => $v ) { 1642 if ( $v == '' ) continue; 1643 $reply->NSElement( $resourcetypes, $v ); 1644 } 1645 if ( $this->_is_binding ) { 1646 $reply->NSElement( $resourcetypes, 'http://xmlns.davical.org/davical:webdav-binding' ); 1647 } 1648 } 1649 break; 1650 1651 case 'DAV::getlastmodified': 1652 /** getlastmodified is HTTP Date format: i.e. the Last-Modified header in response to a GET */ 1653 $reply->NSElement($prop, $tag, ISODateToHTTPDate($this->GetProperty('modified')) ); 1654 break; 1655 1656 case 'DAV::creationdate': 1657 /** creationdate is ISO8601 format */ 1658 $reply->NSElement($prop, $tag, DateToISODate($this->GetProperty('created'), true) ); 1659 break; 1660 1661 case 'DAV::getcontentlength': 1662 if ( $this->_is_collection ) return false; 1663 if ( !isset($this->resource) ) $this->FetchResource(); 1664 if ( isset($this->resource) ) { 1665 $reply->NSElement($prop, $tag, strlen($this->resource->caldav_data) ); 1666 } 1667 break; 1668 1669 case 'DAV::getcontentlanguage': 1670 $locale = (isset($c->current_locale) ? $c->current_locale : ''); 1671 if ( isset($this->locale) && $this->locale != '' ) $locale = $this->locale; 1672 $reply->NSElement($prop, $tag, $locale ); 1673 break; 1674 1675 case 'DAV::acl-restrictions': 1676 $reply->NSElement($prop, $tag, array( new XMLElement('grant-only'), new XMLElement('no-invert') ) ); 1677 break; 1678 1679 case 'DAV::inherited-acl-set': 1680 $inherited_acls = array(); 1681 if ( ! $this->_is_collection ) { 1682 $inherited_acls[] = $reply->href(ConstructURL($this->collection->dav_name)); 1683 } 1684 $reply->NSElement($prop, $tag, $inherited_acls ); 1685 break; 1686 1687 case 'DAV::owner': 1688 // The principal-URL of the owner 1689 if ( $this->IsExternal() ){ 1690 $reply->DAVElement( $prop, 'owner', $reply->href( ConstructURL($this->collection->bound_from )) ); 1691 } 1692 else { 1693 $reply->DAVElement( $prop, 'owner', $reply->href( ConstructURL(DeconstructURL($this->principal_url())) ) ); 1694 } 1695 break; 1696 1697 case 'DAV::add-member': 1698 if ( ! $this->_is_collection ) return false; 1699 if ( $this->_is_principal ) return false; 1700 if ( isset($c->post_add_member) && $c->post_add_member === false ) return false; 1701 $reply->DAVElement( $prop, 'add-member', $reply->href(ConstructURL(DeconstructURL($this->url())).'?add_member') ); 1702 break; 1703 1704 // Empty tag responses. 1705 case 'DAV::group': 1706 case 'DAV::alternate-URI-set': 1707 $reply->NSElement($prop, $tag ); 1708 break; 1709 1710 case 'DAV::getetag': 1711 if ( $this->_is_collection ) return false; 1712 $reply->NSElement($prop, $tag, $this->unique_tag() ); 1713 break; 1714 1715 case 'http://calendarserver.org/ns/:getctag': 1716 if ( ! $this->_is_collection ) return false; 1717 $reply->NSElement($prop, $tag, $this->unique_tag() ); 1718 break; 1719 1720 case 'DAV::sync-token': 1721 if ( ! $this->_is_collection ) return false; 1722 $sync_token = $this->sync_token(); 1723 if ( empty($sync_token) ) return false; 1724 $reply->NSElement($prop, $tag, $sync_token ); 1725 break; 1726 1727 case 'http://calendarserver.org/ns/:calendar-proxy-read-for': 1728 $proxy_type = 'read'; 1729 case 'http://calendarserver.org/ns/:calendar-proxy-write-for': 1730 if ( isset($c->disable_caldav_proxy) && $c->disable_caldav_proxy ) return false; 1731 if ( !isset($proxy_type) ) $proxy_type = 'write'; 1732 // ProxyFor is an already constructed URL 1733 $this->FetchPrincipal(); 1734 $reply->CalendarserverElement($prop, 'calendar-proxy-'.$proxy_type.'-for', $reply->href( $this->principal->ProxyFor($proxy_type) ) ); 1735 break; 1736 1737 case 'http://calendarserver.org/ns/:group-member-set': 1738 case 'DAV::group-member-set': 1739 if ( $this->_is_proxy_resource ) { 1740 $this->FetchPrincipal(); 1741 if ( $this->proxy_type == 'read' ) { 1742 $reply->DAVElement( $prop, 'group-member-set', $reply->href( $this->principal->ReadProxyGroup() ) ); 1743 } else { 1744 $reply->DAVElement( $prop, 'group-member-set', $reply->href( $this->principal->WriteProxyGroup() ) ); 1745 } 1746 } else { 1747 return false; // leave this to DAVPrincipal 1748 } 1749 break; 1750 1751 case 'http://calendarserver.org/ns/:group-membership': 1752 case 'DAV::group-membership': 1753 if ( $this->_is_proxy_resource ) { 1754 /* the calendar-proxy-{read,write} pseudo-principal should not be a member of any group */ 1755 $reply->NSElement($prop, $tag ); 1756 } else { 1757 return false; // leave this to DAVPrincipal 1758 } 1759 break; 1760 1761 case 'DAV::current-user-privilege-set': 1762 if ( $this->HavePrivilegeTo('DAV::read-current-user-privilege-set') ) { 1763 $reply->NSElement($prop, $tag, $this->BuildPrivileges() ); 1764 } 1765 else { 1766 $denied[] = $tag; 1767 } 1768 break; 1769 1770 case 'urn:ietf:params:xml:ns:caldav:supported-calendar-data': 1771 if ( ! $this->IsCalendar() && ! $this->IsSchedulingCollection() ) return false; 1772 $reply->NSElement($prop, $tag, 'text/calendar' ); 1773 break; 1774 1775 case 'urn:ietf:params:xml:ns:caldav:supported-calendar-component-set': 1776 if ( ! $this->_is_collection ) return false; 1777 if ( $this->IsCalendar() ) { 1778 if ( !isset($this->dead_properties) ) $this->FetchDeadProperties(); 1779 if ( isset($this->dead_properties[$tag]) ) { 1780 $set_of_components = $this->dead_properties[$tag]; 1781 foreach( $set_of_components AS $k => $v ) { 1782 if ( preg_match('{(VEVENT|VTODO|VJOURNAL|VTIMEZONE|VFREEBUSY|VPOLL|VAVAILABILITY)}', $v, $matches) ) { 1783 $set_of_components[$k] = $matches[1]; 1784 } 1785 else { 1786 unset( $set_of_components[$k] ); 1787 } 1788 } 1789 } 1790 else if ( isset($c->default_calendar_components) && is_array($c->default_calendar_components) ) { 1791 $set_of_components = $c->default_calendar_components; 1792 } 1793 else { 1794 $set_of_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' ); 1795 } 1796 } 1797 else if ( $this->IsSchedulingCollection() ) 1798 $set_of_components = array( 'VEVENT', 'VTODO', 'VFREEBUSY' ); 1799 else return false; 1800 $components = array(); 1801 foreach( $set_of_components AS $v ) { 1802 $components[] = $reply->NewXMLElement( 'comp', '', array('name' => $v), 'urn:ietf:params:xml:ns:caldav'); 1803 } 1804 $reply->CalDAVElement($prop, 'supported-calendar-component-set', $components ); 1805 break; 1806 1807 case 'DAV::supported-method-set': 1808 $prop->NewElement('supported-method-set', $this->BuildSupportedMethods() ); 1809 break; 1810 1811 case 'DAV::supported-report-set': 1812 $prop->NewElement('supported-report-set', $this->BuildSupportedReports( $reply ) ); 1813 break; 1814 1815 case 'DAV::supportedlock': 1816 $prop->NewElement('supportedlock', 1817 new XMLElement( 'lockentry', 1818 array( 1819 new XMLElement('lockscope', new XMLElement('exclusive')), 1820 new XMLElement('locktype', new XMLElement('write')), 1821 ) 1822 ) 1823 ); 1824 break; 1825 1826 case 'DAV::supported-privilege-set': 1827 $prop->NewElement('supported-privilege-set', $request->BuildSupportedPrivileges($reply) ); 1828 break; 1829 1830 case 'DAV::principal-collection-set': 1831 $prop->NewElement( 'principal-collection-set', $reply->href( ConstructURL('/') ) ); 1832 break; 1833 1834 case 'DAV::current-user-principal': 1835 $prop->NewElement('current-user-principal', $reply->href( ConstructURL(DeconstructURL($session->principal->url())) ) ); 1836 break; 1837 1838 case 'SOME-DENIED-PROPERTY': /** indicating the style for future expansion */ 1839 $denied[] = $reply->Tag($tag); 1840 break; 1841 1842 case 'urn:ietf:params:xml:ns:caldav:calendar-timezone': 1843 if ( ! $this->_is_collection ) return false; 1844 if ( !isset($this->collection->vtimezone) || $this->collection->vtimezone == '' ) return false; 1845 1846 $cal = new iCalComponent(); 1847 $cal->VCalendar(); 1848 $cal->AddComponent( new iCalComponent($this->collection->vtimezone) ); 1849 $reply->NSElement($prop, $tag, $cal->Render() ); 1850 break; 1851 1852 case 'urn:ietf:params:xml:ns:carddav:address-data': 1853 case 'urn:ietf:params:xml:ns:caldav:calendar-data': 1854 if ( $this->_is_collection ) return false; 1855 if ( !isset($c->sync_resource_data_ok) || $c->sync_resource_data_ok == false ) return false; 1856 if ( !isset($this->resource) ) $this->FetchResource(); 1857 $reply->NSElement($prop, $tag, $this->resource->caldav_data ); 1858 break; 1859 1860 case 'urn:ietf:params:xml:ns:carddav:max-resource-size': 1861 if ( ! $this->_is_collection || !$this->_is_addressbook ) return false; 1862 $reply->NSElement($prop, $tag, $c->carddav_max_resource_size ); 1863 break; 1864 1865 case 'urn:ietf:params:xml:ns:carddav:supported-address-data': 1866 if ( ! $this->_is_collection || !$this->_is_addressbook ) return false; 1867 $address_data = $reply->NewXMLElement( 'address-data', false, 1868 array( 'content-type' => 'text/vcard', 'version' => '3.0'), 'urn:ietf:params:xml:ns:carddav'); 1869 $reply->NSElement($prop, $tag, $address_data ); 1870 break; 1871 1872 case 'DAV::acl': 1873 if ( $this->HavePrivilegeTo('DAV::read-acl') ) { 1874 $reply->NSElement($prop, $tag, $this->GetACL( $reply ) ); 1875 } 1876 else { 1877 $denied[] = $tag; 1878 } 1879 break; 1880 1881 case 'http://www.xythos.com/namespaces/StorageServer:ticketdiscovery': 1882 case 'DAV::ticketdiscovery': 1883 $reply->NSElement($prop,'http://www.xythos.com/namespaces/StorageServer:ticketdiscovery', $this->BuildTicketinfo($reply) ); 1884 break; 1885 1886 default: 1887 $property_value = $this->GetProperty(preg_replace('{^(DAV:|urn:ietf:params:xml:ns:ca(rd|l)dav):}', '', $tag)); 1888 if ( isset($property_value) ) { 1889 $reply->NSElement($prop, $tag, $property_value ); 1890 } 1891 else { 1892 if ( !isset($this->dead_properties) ) $this->FetchDeadProperties(); 1893 if ( isset($this->dead_properties[$tag]) ) { 1894 $reply->NSElement($prop, $tag, $this->dead_properties[$tag] ); 1895 } 1896 else { 1897// dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of path "%s".', $tag, $this->dav_name ); 1898 return false; 1899 } 1900 } 1901 } 1902 1903 return true; 1904 } 1905 1906 1907 /** 1908 * Construct XML propstat fragment for this resource 1909 * 1910 * @param array of string $properties The requested properties for this resource 1911 * 1912 * @return string An XML fragment with the requested properties for this resource 1913 */ 1914 function GetPropStat( $properties, &$reply, $props_only = false ) { 1915 global $request; 1916 1917 dbg_error_log('DAVResource',':GetPropStat: propstat for href "%s"', $this->dav_name ); 1918 1919 $prop = new XMLElement('prop', null, null, 'DAV:'); 1920 $denied = array(); 1921 $not_found = array(); 1922 foreach( $properties AS $k => $tag ) { 1923 if ( is_object($tag) ) { 1924 dbg_error_log( 'DAVResource', ':GetPropStat: "$properties" should be an array of text. Assuming this object is an XMLElement!.' ); 1925 $tag = $tag->GetNSTag(); 1926 } 1927 $found = $this->ResourceProperty($tag, $prop, $reply, $denied ); 1928 if ( !$found ) { 1929 if ( !isset($this->principal) ) $this->FetchPrincipal(); 1930 $found = $this->principal->PrincipalProperty( $tag, $prop, $reply, $denied ); 1931 } 1932 if ( ! $found ) { 1933// dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of resource "%s".', $tag, $this->dav_name ); 1934 $not_found[] = $tag; 1935 } 1936 } 1937 if ( $props_only ) return $prop; 1938 1939 $status = new XMLElement('status', 'HTTP/1.1 200 OK', null, 'DAV:' ); 1940 1941 $elements = array( new XMLElement( 'propstat', array($prop,$status), null, 'DAV:' ) ); 1942 1943 if ( count($denied) > 0 ) { 1944 $status = new XMLElement('status', 'HTTP/1.1 403 Forbidden', null, 'DAV:' ); 1945 $noprop = new XMLElement('prop', null, null, 'DAV:'); 1946 foreach( $denied AS $k => $v ) { 1947 $reply->NSElement($noprop, $v); 1948 } 1949 $elements[] = new XMLElement( 'propstat', array( $noprop, $status), null, 'DAV:' ); 1950 } 1951 1952 if ( !$request->PreferMinimal() && count($not_found) > 0 ) { 1953 $status = new XMLElement('status', 'HTTP/1.1 404 Not Found', null, 'DAV:' ); 1954 $noprop = new XMLElement('prop', null, null, 'DAV:'); 1955 foreach( $not_found AS $k => $v ) { 1956 $reply->NSElement($noprop,$v); 1957 } 1958 $elements[] = new XMLElement( 'propstat', array( $noprop, $status), null, 'DAV:' ); 1959 } 1960 return $elements; 1961 } 1962 1963 1964 /** 1965 * Render XML for this resource 1966 * 1967 * @param array $properties The requested properties for this principal 1968 * @param reference $reply A reference to the XMLDocument being used for the reply 1969 * 1970 * @return string An XML fragment with the requested properties for this principal 1971 */ 1972 function RenderAsXML( $properties, &$reply, $bound_parent_path = null ) { 1973 dbg_error_log('DAVResource',':RenderAsXML: Resource "%s" exists(%d)', $this->dav_name, $this->Exists() ); 1974 1975 if ( !$this->Exists() ) return null; 1976 1977 $elements = $this->GetPropStat( $properties, $reply ); 1978 if ( isset($bound_parent_path) ) { 1979 $dav_name = str_replace( $this->parent_path(), $bound_parent_path, $this->dav_name ); 1980 } 1981 else { 1982 $dav_name = $this->dav_name; 1983 } 1984 1985 array_unshift( $elements, $reply->href(ConstructURL($dav_name))); 1986 1987 $response = new XMLElement( 'response', $elements, null, 'DAV:' ); 1988 1989 return $response; 1990 } 1991 1992} 1993