Fix debugging to error log.
[davical.git] / inc / DAVResource.php
blob6f3a5dbf3b746c0d8a268a2548e73622939fca19
1 <?php
2 /**
3 * An object representing a DAV 'resource'
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
12 require_once('AwlCache.php');
13 require_once('AwlQuery.php');
14 require_once('DAVPrincipal.php');
15 require_once('DAVTicket.php');
16 require_once('iCalendar.php');
19 /**
20 * A class for things to do with a DAV Resource
22 * @package davical
24 class DAVResource
26 /**
27 * @var The partial URL of the resource within our namespace, which this resource is being retrieved as
29 protected $dav_name;
31 /**
32 * @var Boolean: does the resource actually exist yet?
34 protected $exists;
36 /**
37 * @var The unique etag associated with the current version of the resource
39 protected $unique_tag;
41 /**
42 * @var The actual resource content, if it exists and is not a collection
44 protected $resource;
46 /**
47 * @var The parent of the resource, which will always be a collection
49 protected $parent;
51 /**
52 * @var The types of the resource, possibly multiple
54 protected $resourcetypes;
56 /**
57 * @var The type of the content
59 protected $contenttype;
61 /**
62 * @var The canonical name which this resource exists at
64 protected $bound_from;
66 /**
67 * @var An object which is the collection record for this resource, or for it's container
69 private $collection;
71 /**
72 * @var An object which is the principal for this resource, or would be if it existed.
74 private $principal;
76 /**
77 * @var A bit mask representing the current user's privileges towards this DAVResource
79 private $privileges;
81 /**
82 * @var True if this resource is a collection of any kind
84 private $_is_collection;
86 /**
87 * @var True if this resource is a principal-URL
89 private $_is_principal;
91 /**
92 * @var True if this resource is a calendar collection
94 private $_is_calendar;
96 /**
97 * @var True if this resource is a binding to another resource
99 private $_is_binding;
102 * @var True if this resource is a binding to an external resource
104 private $_is_external;
107 * @var True if this resource is an addressbook collection
109 private $_is_addressbook;
112 * @var True if this resource is, or is in, a proxy collection
114 private $_is_proxy_request;
117 * @var An array of the methods we support on this resource.
119 private $supported_methods;
122 * @var An array of the reports we support on this resource.
124 private $supported_reports;
127 * @var An array of the dead properties held for this resource
129 private $dead_properties;
132 * @var An array of the component types we support on this resource.
134 private $supported_components;
137 * @var An array of DAVTicket objects if any apply to this resource, such as via a bind.
139 private $tickets;
142 * Constructor
143 * @param mixed $parameters If null, an empty Resourced is created.
144 * If it is an object then it is expected to be a record that was
145 * read elsewhere.
147 function __construct( $parameters = null ) {
148 $this->exists = null;
149 $this->bound_from = null;
150 $this->dav_name = null;
151 $this->unique_tag = null;
152 $this->resource = null;
153 $this->collection = null;
154 $this->principal = null;
155 $this->parent = null;
156 $this->resourcetypes = null;
157 $this->contenttype = null;
158 $this->privileges = null;
159 $this->dead_properties = null;
160 $this->supported_methods = null;
161 $this->supported_reports = null;
163 $this->_is_collection = false;
164 $this->_is_principal = false;
165 $this->_is_calendar = false;
166 $this->_is_binding = false;
167 $this->_is_external = false;
168 $this->_is_addressbook = false;
169 $this->_is_proxy_request = false;
170 if ( isset($parameters) && is_object($parameters) ) {
171 $this->FromRow($parameters);
173 else if ( isset($parameters) && is_array($parameters) ) {
174 if ( isset($parameters['path']) ) {
175 $this->FromPath($parameters['path']);
178 else if ( isset($parameters) && is_string($parameters) ) {
179 $this->FromPath($parameters);
185 * Initialise from a database row
186 * @param object $row The row from the DB.
188 function FromRow($row) {
189 global $c;
191 if ( $row == null ) return;
193 $this->exists = true;
194 $this->dav_name = $row->dav_name;
195 $this->bound_from = (isset($row->bound_from)? $row->bound_from : $row->dav_name);
196 $this->_is_collection = preg_match( '{/$}', $this->dav_name );
198 if ( $this->_is_collection ) {
199 $this->contenttype = 'httpd/unix-directory';
200 $this->collection = (object) array();
201 $this->resource_id = $row->collection_id;
203 $this->_is_principal = preg_match( '{^/[^/]+/$}', $this->dav_name );
204 if ( preg_match( '#^(/principals/[^/]+/[^/]+)/?$#', $this->dav_name, $matches) ) {
205 $this->collection->dav_name = $matches[1].'/';
206 $this->collection->type = 'principal_link';
207 $this->_is_principal = true;
210 else {
211 $this->resource = (object) array();
212 if ( isset($row->dav_id) ) $this->resource_id = $row->dav_id;
215 dbg_error_log( 'DAVResource', ':FromRow: Named "%s" is%s a collection.', $this->dav_name, ($this->_is_collection?'':' not') );
217 foreach( $row AS $k => $v ) {
218 if ( $this->_is_collection )
219 $this->collection->{$k} = $v;
220 else
221 $this->resource->{$k} = $v;
222 switch ( $k ) {
223 case 'created':
224 case 'modified':
225 $this->{$k} = $v;
226 break;
228 case 'resourcetypes':
229 if ( $this->_is_collection ) $this->{$k} = $v;
230 break;
232 case 'dav_etag':
233 $this->unique_tag = '"'.$v.'"';
234 break;
239 if ( $this->_is_collection ) {
240 if ( !isset( $this->collection->type ) || $this->collection->type == 'collection' ) {
241 if ( $this->_is_principal )
242 $this->collection->type = 'principal';
243 else if ( $row->is_calendar == 't' ) {
244 $this->collection->type = 'calendar';
246 else if ( $row->is_addressbook == 't' ) {
247 $this->collection->type = 'addressbook';
249 else if ( isset($row->is_proxy) && $row->is_proxy == 't' ) {
250 $this->collection->type = 'proxy';
252 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) )
253 $this->collection->type = 'schedule-'. $matches[3]. 'box';
254 else if ( $this->dav_name == '/' )
255 $this->collection->type = 'root';
256 else
257 $this->collection->type = 'collection';
260 $this->_is_calendar = ($this->collection->is_calendar == 't');
261 $this->_is_addressbook = ($this->collection->is_addressbook == 't');
262 $this->_is_proxy_request = ($this->collection->type == 'proxy');
263 if ( $this->_is_principal && !isset($this->resourcetypes) ) {
264 $this->resourcetypes = '<DAV::collection/><DAV::principal/>';
266 else if ( $this->_is_proxy_request ) {
267 $this->resourcetypes = $this->collection->resourcetypes;
269 if ( isset($this->collection->dav_displayname) ) $this->collection->displayname = $this->collection->dav_displayname;
271 else {
272 $this->resourcetypes = '';
273 if ( isset($this->resource->caldav_data) ) {
274 if ( isset($this->resource->summary) )$this->resource->displayname = $this->resource->summary;
275 if ( strtoupper(substr($this->resource->caldav_data,0,15)) == 'BEGIN:VCALENDAR' ) {
276 $this->contenttype = 'text/calendar';
277 if ( !$this->HavePrivilegeTo('read') && $this->HavePrivilegeTo('read-free-busy') ) {
278 $vcal = new iCalComponent($this->resource->caldav_data);
279 $confidential = $vcal->CloneConfidential();
280 $this->resource->caldav_data = $confidential->Render();
281 $this->resource->displayname = $this->resource->summary = translate('Busy');
282 $this->resource->description = null;
283 $this->resource->location = null;
284 $this->resource->url = null;
286 else if ( isset($c->hide_alarm) && $c->hide_alarm && !$this->HavePrivilegeTo('write') ) {
287 $vcal1 = new iCalComponent($this->resource->caldav_data);
288 $comps = $vcal1->GetComponents();
289 $vcal2 = new iCalComponent();
290 $vcal2->VCalendar();
291 foreach( $comps AS $comp ) {
292 $comp->ClearComponents('VALARM');
293 $vcal2->AddComponent($comp);
295 $this->resource->displayname = $this->resource->summary = $vcal2->GetPValue('SUMMARY');
296 $this->resource->caldav_data = $vcal2->Render();
299 else if ( strtoupper(substr($this->resource->caldav_data,0,11)) == 'BEGIN:VCARD' ) {
300 $this->contenttype = 'text/vcard';
302 else if ( strtoupper(substr($this->resource->caldav_data,0,11)) == 'BEGIN:VLIST' ) {
303 $this->contenttype = 'text/x-vlist';
311 * Initialise from a path
312 * @param object $inpath The path to populate the resource data from
314 function FromPath($inpath) {
315 global $c;
317 $this->dav_name = DeconstructURL($inpath);
319 $this->FetchCollection();
320 if ( $this->_is_collection ) {
321 if ( $this->_is_principal || $this->collection->type == 'principal' ) $this->FetchPrincipal();
323 else {
324 $this->FetchResource();
326 dbg_error_log( 'DAVResource', ':FromPath: Path "%s" is%s a collection%s.',
327 $this->dav_name, ($this->_is_collection?' '.$this->resourcetypes:' not'), ($this->_is_principal?' and a principal':'') );
331 private function ReadCollectionFromDatabase() {
332 global $c, $session;
334 $this->collection = (object) array(
335 'collection_id' => -1,
336 'type' => 'nonexistent',
337 'is_calendar' => false, 'is_principal' => false, 'is_addressbook' => false
340 $base_sql = 'SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), ';
341 $base_sql .= 'p.principal_id, p.type_id AS principal_type_id, ';
342 $base_sql .= 'p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges, ';
343 $base_sql .= 'timezones.vtimezone ';
344 $base_sql .= 'FROM collection LEFT JOIN principal p USING (user_no) ';
345 $base_sql .= 'LEFT JOIN timezones ON (collection.timezone=timezones.tzid) ';
346 $base_sql .= 'WHERE ';
347 $sql = $base_sql .'collection.dav_name = :raw_path ';
348 $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth );
349 if ( !preg_match( '#/$#', $this->dav_name ) ) {
350 $sql .= ' OR collection.dav_name = :up_to_slash OR collection.dav_name = :plus_slash ';
351 $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name);
352 $params[':plus_slash'] = $this->dav_name.'/';
354 $sql .= 'ORDER BY LENGTH(collection.dav_name) DESC LIMIT 1';
355 $qry = new AwlQuery( $sql, $params );
356 if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
357 $this->collection = $row;
358 $this->collection->exists = true;
359 if ( $row->is_calendar == 't' )
360 $this->collection->type = 'calendar';
361 else if ( $row->is_addressbook == 't' )
362 $this->collection->type = 'addressbook';
363 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) )
364 $this->collection->type = 'schedule-'. $matches[3]. 'box';
365 else
366 $this->collection->type = 'collection';
368 else if ( preg_match( '{^( ( / ([^/]+) / ) \.(in|out)/ ) [^/]*$}x', $this->dav_name, $matches ) ) {
369 // The request is for a scheduling inbox or outbox (or something inside one) and we should auto-create it
370 $params = array( ':username' => $matches[3], ':parent_container' => $matches[2], ':dav_name' => $matches[1] );
371 $params[':boxname'] = ($matches[4] == 'in' ? ' Inbox' : ' Outbox');
372 $this->collection_type = 'schedule-'. $matches[4]. 'box';
373 $params[':resourcetypes'] = sprintf('<DAV::collection/><urn:ietf:params:xml:ns:caldav:%s/>', $this->collection_type );
374 $sql = <<<EOSQL
375 INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified, dav_etag, resourcetypes )
376 VALUES( (SELECT user_no FROM usr WHERE username = text(:username)),
377 :parent_container, :dav_name,
378 (SELECT fullname FROM usr WHERE username = text(:username)) || :boxname,
379 FALSE, current_timestamp, current_timestamp, '1', :resourcetypes )
380 EOSQL;
381 $qry = new AwlQuery( $sql, $params );
382 $qry->Exec('DAVResource');
383 dbg_error_log( 'DAVResource', 'Created new collection as "%s".', trim($params[':boxname']) );
385 $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth );
386 $qry = new AwlQuery( $base_sql . ' dav_name = :raw_path', $params );
387 if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
388 $this->collection = $row;
389 $this->collection->exists = true;
390 $this->collection->type = $this->collection_type;
393 else if ( preg_match( '#^(/([^/]+)/calendar-proxy-(read|write))/?[^/]*$#', $this->dav_name, $matches ) ) {
394 $this->collection->type = 'proxy';
395 $this->_is_proxy_request = true;
396 $this->proxy_type = $matches[3];
397 $this->collection->dav_name = $this->dav_name;
398 $this->collection->dav_displayname = sprintf( '%s proxy %s', $matches[2], $matches[3] );
399 $this->collection->exists = true;
400 $this->collection->parent_container = $matches[1] . '/';
402 else if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name, $matches)
403 || preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches) ) {
404 $this->_is_principal = true;
405 $this->FetchPrincipal();
406 $this->collection->is_principal = true;
407 $this->collection->type = 'principal';
409 else if ( $this->dav_name == '/' ) {
410 $this->collection->dav_name = '/';
411 $this->collection->type = 'root';
412 $this->collection->exists = true;
413 $this->collection->displayname = $c->system_name;
414 $this->collection->default_privileges = (1 | 16 | 32);
415 $this->collection->parent_container = '/';
417 else {
418 $sql = <<<EOSQL
419 SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), p.principal_id,
420 p.type_id AS principal_type_id, p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges,
421 timezones.vtimezone, dav_binding.access_ticket_id, dav_binding.parent_container AS bind_parent_container,
422 dav_binding.dav_displayname, owner.dav_name AS bind_owner_url, dav_binding.dav_name AS bound_to,
423 dav_binding.external_url AS external_url, dav_binding.type AS external_type, dav_binding.bind_id AS bind_id
424 FROM dav_binding
425 LEFT JOIN collection ON (collection.collection_id=bound_source_id)
426 LEFT JOIN principal p USING (user_no)
427 LEFT JOIN dav_principal owner ON (dav_binding.dav_owner_id=owner.principal_id)
428 LEFT JOIN timezones ON (collection.timezone=timezones.tzid)
429 WHERE dav_binding.dav_name = :raw_path
430 EOSQL;
431 $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth );
432 if ( !preg_match( '#/$#', $this->dav_name ) ) {
433 $sql .= ' OR dav_binding.dav_name = :up_to_slash OR collection.dav_name = :plus_slash OR dav_binding.dav_name = :plus_slash ';
434 $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name);
435 $params[':plus_slash'] = $this->dav_name.'/';
437 $sql .= ' ORDER BY LENGTH(dav_binding.dav_name) DESC LIMIT 1';
438 $qry = new AwlQuery( $sql, $params );
439 if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
440 $this->collection = $row;
441 $this->collection->exists = true;
442 $this->collection->parent_set = $row->parent_container;
443 $this->collection->parent_container = $row->bind_parent_container;
444 $this->collection->bound_from = $row->dav_name;
445 $this->collection->dav_name = $row->bound_to;
446 if ( $row->is_calendar == 't' )
447 $this->collection->type = 'calendar';
448 else if ( $row->is_addressbook == 't' )
449 $this->collection->type = 'addressbook';
450 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) )
451 $this->collection->type = 'schedule-'. $matches[3]. 'box';
452 else
453 $this->collection->type = 'collection';
454 if ( strlen($row->external_url) > 8 ) {
455 $this->_is_external = true;
456 if ( $row->external_type == 'calendar' )
457 $this->collection->type = 'calendar';
458 else if ( $row->external_type == 'addressbook' )
459 $this->collection->type = 'addressbook';
460 else
461 $this->collection->type = 'collection';
463 $this->_is_binding = true;
464 $this->bound_from = str_replace( $row->bound_to, $row->dav_name, $this->dav_name);
465 if ( isset($row->access_ticket_id) ) {
466 if ( !isset($this->tickets) ) $this->tickets = array();
467 $this->tickets[] = new DAVTicket($row->access_ticket_id);
470 else {
471 dbg_error_log( 'DAVResource', 'No collection for path "%s".', $this->dav_name );
472 $this->collection->exists = false;
473 $this->collection->dav_name = preg_replace('{/[^/]*$}', '/', $this->dav_name);
480 * Find the collection associated with this resource.
482 protected function FetchCollection() {
483 global $session;
486 * RFC4918, 8.3: Identifiers for collections SHOULD end in '/'
487 * - also discussed at more length in 5.2
489 * So we look for a collection which matches one of the following URLs:
490 * - The exact request.
491 * - If the exact request, doesn't end in '/', then the request URL with a '/' appended
492 * - The request URL truncated to the last '/'
493 * The collection URL for this request is therefore the longest row in the result, so we
494 * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1"
496 dbg_error_log( 'DAVResource', ':FetchCollection: Looking for collection for "%s".', $this->dav_name );
498 // Try and pull the answer out of a hat
499 $cache = getCacheInstance();
500 $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $this->dav_name);
501 $cache_key = 'dav_resource'.$session->user_no;
502 $this->collection = $cache->get( $cache_ns, $cache_key );
503 if ( $this->collection === false ) {
504 $this->ReadCollectionFromDatabase();
505 if ( $this->collection->type != 'principal' ) {
506 $cache_ns = 'collection-'.$this->collection->dav_name;
507 @dbg_error_log( 'Cache', ':FetchCollection: Setting cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type );
508 $cache->set( $cache_ns, $cache_key, $this->collection );
510 @dbg_error_log( 'DAVResource', ':FetchCollection: Found collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type );
512 else {
513 @dbg_error_log( 'Cache', ':FetchCollection: Got cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type );
514 if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name, $matches)
515 || preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches) ) {
516 $this->_is_principal = true;
517 $this->FetchPrincipal();
518 $this->collection->is_principal = true;
519 $this->collection->type = 'principal';
521 @dbg_error_log( 'DAVResource', ':FetchCollection: Read cached collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type );
524 if ( isset($this->collection->bound_from) ) {
525 $this->_is_binding = true;
526 $this->bound_from = str_replace( $this->collection->bound_to, $this->collection->bound_from, $this->dav_name);
527 if ( isset($this->collection->access_ticket_id) ) {
528 if ( !isset($this->tickets) ) $this->tickets = array();
529 $this->tickets[] = new DAVTicket($this->collection->access_ticket_id);
533 $this->_is_collection = ( $this->_is_principal || $this->collection->dav_name == $this->dav_name || $this->collection->dav_name == $this->dav_name.'/' );
534 if ( $this->_is_collection ) {
535 $this->dav_name = $this->collection->dav_name;
536 $this->resource_id = $this->collection->collection_id;
537 $this->_is_calendar = ($this->collection->type == 'calendar');
538 $this->_is_addressbook = ($this->collection->type == 'addressbook');
539 $this->contenttype = 'httpd/unix-directory';
540 if ( !isset($this->exists) && isset($this->collection->exists) ) {
541 // If this seems peculiar it's because we only set it to false above...
542 $this->exists = $this->collection->exists;
544 if ( $this->exists ) {
545 if ( isset($this->collection->dav_etag) ) $this->unique_tag = '"'.$this->collection->dav_etag.'"';
546 if ( isset($this->collection->created) ) $this->created = $this->collection->created;
547 if ( isset($this->collection->modified) ) $this->modified = $this->collection->modified;
548 if ( isset($this->collection->dav_displayname) ) $this->collection->displayname = $this->collection->dav_displayname;
550 else {
551 if ( !isset($this->parent) ) $this->GetParentContainer();
552 $this->user_no = $this->parent->GetProperty('user_no');
554 if ( isset($this->collection->resourcetypes) )
555 $this->resourcetypes = $this->collection->resourcetypes;
556 else {
557 $this->resourcetypes = '<DAV::collection/>';
558 if ( $this->_is_principal ) $this->resourcetypes .= '<DAV::principal/>';
559 if ( $this->_is_addressbook ) $this->resourcetypes .= '<urn:ietf:params:xml:ns:carddav:addressbook/>';
560 if ( $this->_is_calendar ) $this->resourcetypes .= '<urn:ietf:params:xml:ns:caldav:calendar/>';
567 * Find the principal associated with this resource.
569 protected function FetchPrincipal() {
570 if ( isset($this->principal) ) return;
571 $this->principal = new DAVPrincipal( array( "path" => $this->bound_from() ) );
572 if ( $this->_is_principal ) {
573 $this->exists = $this->principal->Exists();
574 $this->collection->dav_name = $this->dav_name();
575 $this->collection->type = 'principal';
576 if ( $this->exists ) {
577 $this->collection = $this->principal->AsCollection();
578 $this->displayname = $this->principal->GetProperty('displayname');
579 $this->user_no = $this->principal->user_no();
580 $this->resource_id = $this->principal->principal_id();
581 $this->created = $this->principal->created;
582 $this->modified = $this->principal->modified;
583 $this->resourcetypes = $this->principal->resourcetypes;
590 * Retrieve the actual resource.
592 protected function FetchResource() {
593 global $c, $session;
595 if ( isset($this->exists) ) return; // True or false, we've got what we can already
596 if ( $this->_is_collection ) return; // We have all we're going to read
598 $sql = <<<EOQRY
599 SELECT calendar_item.*, addressbook_resource.*, caldav_data.*
600 FROM caldav_data LEFT OUTER JOIN calendar_item USING (collection_id,dav_id)
601 LEFT OUTER JOIN addressbook_resource USING (dav_id)
602 WHERE caldav_data.dav_name = :dav_name
603 EOQRY;
604 $params = array( ':dav_name' => $this->bound_from() );
606 $qry = new AwlQuery( $sql, $params );
607 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) {
608 $this->exists = true;
609 $row = $qry->Fetch();
610 $this->FromRow($row);
612 else {
613 $this->exists = false;
619 * Fetch any dead properties for this URL
621 protected function FetchDeadProperties() {
622 if ( isset($this->dead_properties) ) return;
624 $this->dead_properties = array();
625 if ( !$this->exists || !$this->_is_collection ) return;
627 $qry = new AwlQuery('SELECT property_name, property_value FROM property WHERE dav_name= :dav_name', array(':dav_name' => $this->dav_name) );
628 if ( $qry->Exec('DAVResource') ) {
629 while ( $property = $qry->Fetch() ) {
630 $this->dead_properties[$property->property_name] = self::BuildDeadPropertyXML($property->property_name,$property->property_value);
635 public static function BuildDeadPropertyXML($property_name, $raw_string) {
636 if ( !preg_match('{^\s*<.*>\s*$}s', $raw_string) ) return $raw_string;
637 $xmlns = null;
638 if ( preg_match( '{^(.*):([^:]+)$}', $property_name, $matches) ) {
639 $xmlns = $matches[1];
640 $property_name = $matches[2];
642 $xml = sprintf('<%s%s>%s</%s>', $property_name, (isset($xmlns)?' xmlns="'.$xmlns.'"':''), $raw_string, $property_name);
643 $xml_parser = xml_parser_create_ns('UTF-8');
644 $xml_tags = array();
645 xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
646 xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
647 $rc = xml_parse_into_struct( $xml_parser, $xml, $xml_tags );
648 if ( $rc == false ) {
649 dbg_error_log( 'ERROR', 'XML parsing error: %s at line %d, column %d',
650 xml_error_string(xml_get_error_code($xml_parser)),
651 xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
652 dbg_error_log( 'ERROR', "Error occurred in:\n%s\n",$xml);
653 return $raw_string;
655 xml_parser_free($xml_parser);
656 $position = 0;
657 $xmltree = BuildXMLTree( $xml_tags, $position);
658 return $xmltree->GetContent();
662 * Build permissions for this URL
664 protected function FetchPrivileges() {
665 global $session, $request;
667 if ( $this->dav_name == '/' || $this->dav_name == '' || $this->_is_external ) {
668 $this->privileges = (1 | 16 | 32); // read + read-acl + read-current-user-privilege-set
669 dbg_error_log( 'DAVResource', ':FetchPrivileges: Read permissions for user accessing /' );
670 return;
673 if ( $session->AllowedTo('Admin') ) {
674 $this->privileges = privilege_to_bits('all');
675 dbg_error_log( 'DAVResource', ':FetchPrivileges: Full permissions for an administrator.' );
676 return;
679 if ( $this->IsPrincipal() ) {
680 if ( !isset($this->principal) ) $this->FetchPrincipal();
681 $this->privileges = $this->principal->Privileges();
682 dbg_error_log( 'DAVResource', ':FetchPrivileges: Privileges of "%s" for user accessing principal "%s"', $this->privileges, $this->principal->username() );
683 return;
686 if ( ! isset($this->collection) ) $this->FetchCollection();
687 $this->privileges = 0;
688 if ( !isset($this->collection->path_privs) ) {
689 if ( !isset($this->parent) ) $this->GetParentContainer();
691 $this->collection->path_privs = $this->parent->Privileges();
692 $this->collection->user_no = $this->parent->GetProperty('user_no');
693 $this->collection->principal_id = $this->parent->GetProperty('principal_id');
696 $this->privileges = $this->collection->path_privs;
697 if ( is_string($this->privileges) ) $this->privileges = bindec( $this->privileges );
699 dbg_error_log( 'DAVResource', ':FetchPrivileges: Privileges of "%s" for user "%s" accessing "%s"',
700 decbin($this->privileges), $session->username, $this->dav_name() );
702 if ( isset($request->ticket) && $request->ticket->MatchesPath($this->bound_from()) ) {
703 $this->privileges |= $request->ticket->privileges();
704 dbg_error_log( 'DAVResource', ':FetchPrivileges: Applying permissions for ticket "%s" now: %s', $request->ticket->id(), decbin($this->privileges) );
707 if ( isset($this->tickets) ) {
708 if ( !isset($this->resource_id) ) $this->FetchResource();
709 foreach( $this->tickets AS $k => $ticket ) {
710 if ( $ticket->MatchesResource($this->resource_id()) || $ticket->MatchesPath($this->bound_from()) ) {
711 $this->privileges |= $ticket->privileges();
712 dbg_error_log( 'DAVResource', ':FetchPrivileges: Applying permissions for ticket "%s" now: %s', $ticket->id(), decbin($this->privileges) );
720 * Get a DAVResource which is the parent to this resource.
722 function GetParentContainer() {
723 if ( $this->dav_name == '/' ) return null;
724 if ( !isset($this->parent) ) {
725 if ( $this->_is_collection ) {
726 dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() );
727 $this->parent = new DAVResource( $this->parent_path() );
729 else {
730 dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() );
731 $this->parent = new DAVResource($this->collection->dav_name);
734 return $this->parent;
739 * Fetch the parent to this resource. This is deprecated - use GetParentContainer() instead.
741 function FetchParentContainer() {
742 deprecated('DAVResource::FetchParentContainer');
743 return $this->GetParentContainer();
748 * Return the privileges bits for the current session user to this resource
750 function Privileges() {
751 if ( !isset($this->privileges) ) $this->FetchPrivileges();
752 return $this->privileges;
757 * Does the user have the privileges to do what is requested.
758 * @param $do_what mixed The request privilege name, or array of privilege names, to be checked.
759 * @param $any boolean Whether we accept any of the privileges. The default is true, unless the requested privilege is 'all', when it is false.
760 * @return boolean Whether they do have one of those privileges against this resource.
762 function HavePrivilegeTo( $do_what, $any = null ) {
763 if ( !isset($this->privileges) ) $this->FetchPrivileges();
764 if ( !isset($any) ) $any = ($do_what != 'all');
765 $test_bits = privilege_to_bits( $do_what );
766 dbg_error_log( 'DAVResource', 'Testing %s privileges of "%s" (%s) against allowed "%s" => "%s" (%s)', ($any?'any':'exactly'),
767 $do_what, decbin($test_bits), decbin($this->privileges), ($this->privileges & $test_bits), decbin($this->privileges & $test_bits) );
768 if ( $any ) {
769 return ($this->privileges & $test_bits) > 0;
771 else {
772 return ($this->privileges & $test_bits) == $test_bits;
778 * Check if we have the needed privilege or send an error response. If the user does not have the privileges then
779 * the call will not return, and an XML error document will be output.
781 * @param string $privilege The name of the needed privilege.
782 * @param boolean $any Whether we accept any of the privileges. The default is true, unless the requested privilege is 'all', when it is false.
784 function NeedPrivilege( $privilege, $any = null ) {
785 global $request;
787 // Do the test
788 if ( $this->HavePrivilegeTo($privilege, $any) ) return;
790 // They failed, so output the error
791 $request->NeedPrivilege( $privilege, $this->dav_name );
792 exit(0); // Unecessary, but might clarify things
797 * Returns the array of privilege names converted into XMLElements
799 function BuildPrivileges( $privilege_names=null, &$xmldoc=null ) {
800 if ( $privilege_names == null ) {
801 if ( !isset($this->privileges) ) $this->FetchPrivileges();
802 $privilege_names = bits_to_privilege($this->privileges, ($this->_is_collection ? $this->collection->type : null ) );
804 return privileges_to_XML( $privilege_names, $xmldoc);
809 * Returns the array of supported methods
811 function FetchSupportedMethods( ) {
812 if ( isset($this->supported_methods) ) return $this->supported_methods;
814 $this->supported_methods = array(
815 'OPTIONS' => '',
816 'PROPFIND' => '',
817 'REPORT' => '',
818 'DELETE' => '',
819 'LOCK' => '',
820 'UNLOCK' => '',
821 'MOVE' => ''
823 if ( $this->IsCollection() ) {
824 /* if ( $this->IsPrincipal() ) {
825 $this->supported_methods['MKCALENDAR'] = '';
826 $this->supported_methods['MKCOL'] = '';
827 } */
828 switch ( $this->collection->type ) {
829 case 'root':
830 case 'email':
831 // We just override the list completely here.
832 $this->supported_methods = array(
833 'OPTIONS' => '',
834 'PROPFIND' => '',
835 'REPORT' => ''
837 break;
839 case 'schedule-outbox':
840 $this->supported_methods = array_merge(
841 $this->supported_methods,
842 array(
843 'POST' => '', 'PROPPATCH' => '', 'MKTICKET' => '', 'DELTICKET' => ''
846 break;
847 case 'schedule-inbox':
848 case 'calendar':
849 $this->supported_methods['GET'] = '';
850 $this->supported_methods['PUT'] = '';
851 $this->supported_methods['HEAD'] = '';
852 $this->supported_methods['MKTICKET'] = '';
853 $this->supported_methods['DELTICKET'] = '';
854 $this->supported_methods['ACL'] = '';
855 break;
856 case 'collection':
857 $this->supported_methods['MKTICKET'] = '';
858 $this->supported_methods['DELTICKET'] = '';
859 $this->supported_methods['BIND'] = '';
860 $this->supported_methods['ACL'] = '';
861 case 'principal':
862 $this->supported_methods['GET'] = '';
863 $this->supported_methods['HEAD'] = '';
864 $this->supported_methods['MKCOL'] = '';
865 $this->supported_methods['MKCALENDAR'] = '';
866 $this->supported_methods['PROPPATCH'] = '';
867 $this->supported_methods['BIND'] = '';
868 $this->supported_methods['ACL'] = '';
869 break;
872 else {
873 $this->supported_methods = array_merge(
874 $this->supported_methods,
875 array(
876 'GET' => '', 'HEAD' => '', 'PUT' => '', 'MKTICKET' => '', 'DELTICKET' => ''
881 return $this->supported_methods;
886 * Returns the array of supported methods converted into XMLElements
888 function BuildSupportedMethods( ) {
889 if ( !isset($this->supported_methods) ) $this->FetchSupportedMethods();
890 $methods = array();
891 foreach( $this->supported_methods AS $k => $v ) {
892 // dbg_error_log( 'DAVResource', ':BuildSupportedMethods: Adding method "%s" which is "%s".', $k, $v );
893 $methods[] = new XMLElement( 'supported-method', null, array('name' => $k) );
895 return $methods;
900 * Returns the array of supported reports
902 function FetchSupportedReports( ) {
903 if ( isset($this->supported_reports) ) return $this->supported_reports;
905 $this->supported_reports = array(
906 'DAV::principal-property-search' => '',
907 'DAV::principal-search-property-set' => '',
908 'DAV::expand-property' => '',
909 'DAV::sync-collection' => ''
912 if ( !isset($this->collection) ) $this->FetchCollection();
914 if ( $this->collection->is_calendar ) {
915 $this->supported_reports = array_merge(
916 $this->supported_reports,
917 array(
918 'urn:ietf:params:xml:ns:caldav:calendar-query' => '',
919 'urn:ietf:params:xml:ns:caldav:calendar-multiget' => '',
920 'urn:ietf:params:xml:ns:caldav:free-busy-query' => ''
924 if ( $this->collection->is_addressbook ) {
925 $this->supported_reports = array_merge(
926 $this->supported_reports,
927 array(
928 'urn:ietf:params:xml:ns:carddav:addressbook-query' => '',
929 'urn:ietf:params:xml:ns:carddav:addressbook-multiget' => ''
933 return $this->supported_reports;
938 * Returns the array of supported reports converted into XMLElements
940 function BuildSupportedReports( &$reply ) {
941 if ( !isset($this->supported_reports) ) $this->FetchSupportedReports();
942 $reports = array();
943 foreach( $this->supported_reports AS $k => $v ) {
944 dbg_error_log( 'DAVResource', ':BuildSupportedReports: Adding supported report "%s" which is "%s".', $k, $v );
945 $report = new XMLElement('report');
946 $reply->NSElement($report, $k );
947 $reports[] = new XMLElement('supported-report', $report );
949 return $reports;
954 * Fetches an array of the access_ticket records applying to this path
956 function FetchTickets( ) {
957 global $c;
958 if ( isset($this->access_tickets) ) return;
959 $this->access_tickets = array();
961 $sql =
962 'SELECT access_ticket.*, COALESCE( resource.dav_name, collection.dav_name) AS target_dav_name,
963 (access_ticket.expires < current_timestamp) AS expired,
964 dav_principal.dav_name AS principal_dav_name,
965 EXTRACT( \'epoch\' FROM (access_ticket.expires - current_timestamp)) AS seconds,
966 path_privs(access_ticket.dav_owner_id,collection.dav_name,:scan_depth) AS grantor_collection_privileges
967 FROM access_ticket JOIN collection ON (target_collection_id = collection_id)
968 JOIN dav_principal ON (dav_owner_id = principal_id)
969 LEFT JOIN caldav_data resource ON (resource.dav_id = access_ticket.target_resource_id)
970 WHERE target_collection_id = :collection_id ';
971 $params = array(':collection_id' => $this->collection->collection_id, ':scan_depth' => $c->permission_scan_depth);
972 if ( $this->IsCollection() ) {
973 $sql .= 'AND target_resource_id IS NULL';
975 else {
976 if ( !isset($this->exists) ) $this->FetchResource();
977 $sql .= 'AND target_resource_id = :dav_id';
978 $params[':dav_id'] = $this->resource->dav_id;
980 if ( isset($this->exists) && !$this->exists ) return;
982 $qry = new AwlQuery( $sql, $params );
983 if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() ) {
984 while( $ticket = $qry->Fetch() ) {
985 $this->access_tickets[] = $ticket;
992 * Returns the array of tickets converted into XMLElements
994 * If the current user does not have DAV::read-acl privilege on this resource they
995 * will only get to see the tickets where they are the owner, or which they supplied
996 * along with the request.
998 * @param &XMLDocument $reply A reference to the XMLDocument used to construct the reply
999 * @return XMLTreeFragment A fragment of an XMLDocument to go in the reply
1001 function BuildTicketinfo( &$reply ) {
1002 global $session, $request;
1004 if ( !isset($this->access_tickets) ) $this->FetchTickets();
1005 $tickets = array();
1006 $show_all = $this->HavePrivilegeTo('DAV::read-acl');
1007 foreach( $this->access_tickets AS $meh => $trow ) {
1008 if ( !$show_all && ( $trow->dav_owner_id == $session->principal_id || $request->ticket->id() == $trow->ticket_id ) ) continue;
1009 dbg_error_log( 'DAVResource', ':BuildTicketinfo: Adding access_ticket "%s" which is "%s".', $trow->ticket_id, $trow->privileges );
1010 $ticket = new XMLElement( $reply->Tag( 'ticketinfo', 'http://www.xythos.com/namespaces/StorageServer', 'TKT' ) );
1011 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:id', $trow->ticket_id );
1012 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:owner', $reply->href( ConstructURL($trow->principal_dav_name)) );
1013 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:timeout', (isset($trow->seconds) ? sprintf( 'Seconds-%d', $trow->seconds) : 'infinity') );
1014 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:visits', 'infinity' );
1015 $privs = array();
1016 foreach( bits_to_privilege(bindec($trow->privileges) & bindec($trow->grantor_collection_privileges) ) AS $k => $v ) {
1017 $privs[] = $reply->NewXMLElement($v);
1019 $reply->NSElement($ticket, 'DAV::privilege', $privs );
1020 $tickets[] = $ticket;
1022 return $tickets;
1027 * Checks whether the resource is locked, returning any lock token, or false
1029 * @todo This logic does not catch all locking scenarios. For example an infinite
1030 * depth request should check the permissions for all collections and resources within
1031 * that. At present we only maintain permissions on a per-collection basis though.
1033 function IsLocked( $depth = 0 ) {
1034 if ( !isset($this->_locks_found) ) {
1035 $this->_locks_found = array();
1037 * Find the locks that might apply and load them into an array
1039 $sql = 'SELECT * FROM locks WHERE :this_path::text ~ (\'^\'||dav_name||:match_end)::text';
1040 $qry = new AwlQuery($sql, array( ':this_path' => $this->dav_name, ':match_end' => ($depth == DEPTH_INFINITY ? '' : '$') ) );
1041 if ( $qry->Exec('DAVResource',__LINE__,__FILE__) ) {
1042 while( $lock_row = $qry->Fetch() ) {
1043 $this->_locks_found[$lock_row->opaquelocktoken] = $lock_row;
1046 else {
1047 $this->DoResponse(500,i18n("Database Error"));
1048 // Does not return.
1052 foreach( $this->_locks_found AS $lock_token => $lock_row ) {
1053 if ( $lock_row->depth == DEPTH_INFINITY || $lock_row->dav_name == $this->dav_name ) {
1054 return $lock_token;
1058 return false; // Nothing matched
1063 * Checks whether this resource is a collection
1065 function IsCollection() {
1066 return $this->_is_collection;
1071 * Checks whether this resource is a principal
1073 function IsPrincipal() {
1074 return $this->_is_collection && $this->_is_principal;
1079 * Checks whether this resource is a calendar
1081 function IsCalendar() {
1082 return $this->_is_collection && $this->_is_calendar;
1087 * Checks whether this resource is a scheduling inbox/outbox collection
1088 * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any'
1090 function IsSchedulingCollection( $type = 'any' ) {
1091 if ( $this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) {
1092 return ($type == 'any' || $type == $matches[1]);
1094 return false;
1099 * Checks whether this resource is IN a scheduling inbox/outbox collection
1100 * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any'
1102 function IsInSchedulingCollection( $type = 'any' ) {
1103 if ( !$this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) {
1104 return ($type == 'any' || $type == $matches[1]);
1106 return false;
1111 * Checks whether this resource is an addressbook
1113 function IsAddressbook() {
1114 return $this->_is_collection && $this->_is_addressbook;
1119 * Checks whether this resource is a bind to another resource
1121 function IsBinding() {
1122 return $this->_is_binding;
1127 * Checks whether this resource is a bind to an external resource
1129 function IsExternal() {
1130 return $this->_is_external;
1135 * Checks whether this resource actually exists, in the virtual sense, within the hierarchy
1137 function Exists() {
1138 if ( ! isset($this->exists) ) {
1139 if ( $this->IsPrincipal() ) {
1140 if ( !isset($this->principal) ) $this->FetchPrincipal();
1141 $this->exists = $this->principal->Exists();
1143 else if ( ! $this->IsCollection() ) {
1144 if ( !isset($this->resource) ) $this->FetchResource();
1147 // dbg_error_log('DAVResource',' Checking whether "%s" exists. It would appear %s.', $this->dav_name, ($this->exists ? 'so' : 'not') );
1148 return $this->exists;
1153 * Checks whether the container for this resource actually exists, in the virtual sense, within the hierarchy
1155 function ContainerExists() {
1156 if ( $this->collection->dav_name != $this->dav_name ) {
1157 return $this->collection->exists;
1159 $parent = $this->GetParentContainer();
1160 return $parent->Exists();
1165 * Returns the URL of our resource
1167 function url() {
1168 if ( !isset($this->dav_name) ) {
1169 throw Exception("What! How can dav_name not be set?");
1171 return ConstructURL($this->dav_name);
1176 * Returns the dav_name of the resource in our internal namespace
1178 function dav_name() {
1179 if ( isset($this->dav_name) ) return $this->dav_name;
1180 return null;
1185 * Returns the dav_name of the resource we are bound to, within our internal namespace
1187 function bound_from() {
1188 if ( isset($this->bound_from) ) return $this->bound_from;
1189 return $this->dav_name();
1194 * Sets the dav_name of the resource we are bound as
1196 function set_bind_location( $new_dav_name ) {
1197 if ( !isset($this->bound_from) && isset($this->dav_name) ) {
1198 $this->bound_from = $this->dav_name;
1200 $this->dav_name = $new_dav_name;
1201 return $this->dav_name;
1206 * Returns the dav_name of the resource in our internal namespace
1208 function parent_path() {
1209 if ( $this->IsCollection() ) {
1210 if ( !isset($this->collection) ) $this->FetchCollection();
1211 if ( !isset($this->collection->parent_container) ) {
1212 $this->collection->parent_container = preg_replace( '{[^/]+/$}', '', $this->bound_from());
1214 return $this->collection->parent_container;
1216 return preg_replace( '{[^/]+$}', '', $this->bound_from());
1222 * Returns the principal-URL for this resource
1224 function principal_url() {
1225 if ( !isset($this->principal) ) $this->FetchPrincipal();
1226 return $this->principal->url();
1231 * Returns the internal user_no for the principal for this resource
1233 function user_no() {
1234 if ( !isset($this->principal) ) $this->FetchPrincipal();
1235 return $this->principal->user_no();
1240 * Returns the internal collection_id for this collection, or the collection containing this resource
1242 function collection_id() {
1243 if ( !isset($this->collection) ) $this->FetchCollection();
1244 return $this->collection->collection_id;
1249 * Returns the database row for this resource
1251 function resource() {
1252 if ( !isset($this->resource) ) $this->FetchResource();
1253 return $this->resource;
1258 * Returns the unique_tag (ETag or getctag) for this resource
1260 function unique_tag() {
1261 if ( isset($this->unique_tag) ) return $this->unique_tag;
1262 if ( $this->IsPrincipal() && !isset($this->principal) ) {
1263 $this->FetchPrincipal();
1264 $this->unique_tag = $this->principal->unique_tag();
1266 else if ( !$this->_is_collection && !isset($this->resource) ) $this->FetchResource();
1268 if ( $this->exists !== true || !isset($this->unique_tag) ) $this->unique_tag = '';
1270 return $this->unique_tag;
1275 * Returns the definitive resource_id for this resource - usually a dav_id
1277 function resource_id() {
1278 if ( isset($this->resource_id) ) return $this->resource_id;
1279 if ( $this->IsPrincipal() && !isset($this->principal) ) $this->FetchPrincipal();
1280 else if ( !$this->_is_collection && !isset($this->resource) ) $this->FetchResource();
1282 if ( $this->exists !== true || !isset($this->resource_id) ) $this->resource_id = null;
1284 return $this->resource_id;
1289 * Returns the current sync_token for this collection, or the containing collection
1291 function sync_token( $cachedOK = true ) {
1292 dbg_error_log('DAVResource', 'Request for a%scached sync-token', ($cachedOK ? ' ' : 'n un') );
1293 if ( $this->IsPrincipal() ) return null;
1294 if ( $this->collection_id() == 0 ) return null;
1295 if ( !isset($this->sync_token) || !$cachedOK ) {
1296 $sql = 'SELECT new_sync_token( 0, :collection_id) AS sync_token';
1297 $params = array( ':collection_id' => $this->collection_id());
1298 $qry = new AwlQuery($sql, $params );
1299 if ( !$qry->Exec() || !$row = $qry->Fetch() ) {
1300 if ( !$qry->QDo('SELECT new_sync_token( 0, :collection_id) AS sync_token', $params) ) throw new Exception('Problem with database query');
1301 $row = $qry->Fetch();
1303 $this->sync_token = 'data:,'.$row->sync_token;
1305 dbg_error_log('DAVResource', 'Returning sync token of "%s"', $this->sync_token );
1306 return $this->sync_token;
1310 * Checks whether the target collection is publicly_readable
1312 function IsPublic() {
1313 return ( isset($this->collection->publicly_readable) && $this->collection->publicly_readable == 't' );
1318 * Checks whether the target collection is for public events only
1320 function IsPublicOnly() {
1321 return ( isset($this->collection->publicly_events_only) && $this->collection->publicly_events_only == 't' );
1326 * Return the type of whatever contains this resource, or would if it existed.
1328 function ContainerType() {
1329 if ( $this->IsPrincipal() ) return 'root';
1330 if ( !$this->IsCollection() ) return $this->collection->type;
1332 if ( ! isset($this->collection->parent_container) ) return null;
1334 if ( isset($this->parent_container_type) ) return $this->parent_container_type;
1336 if ( preg_match('#/[^/]+/#', $this->collection->parent_container) ) {
1337 $this->parent_container_type = 'principal';
1339 else {
1340 $qry = new AwlQuery('SELECT * FROM collection WHERE dav_name = :parent_name',
1341 array( ':parent_name' => $this->collection->parent_container ) );
1342 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 && $parent = $qry->Fetch() ) {
1343 if ( $parent->is_calendar == 't' )
1344 $this->parent_container_type = 'calendar';
1345 else if ( $parent->is_addressbook == 't' )
1346 $this->parent_container_type = 'addressbook';
1347 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) )
1348 $this->parent_container_type = 'schedule-'. $matches[3]. 'box';
1349 else
1350 $this->parent_container_type = 'collection';
1352 else
1353 $this->parent_container_type = null;
1355 return $this->parent_container_type;
1360 * BuildACE - construct an XMLElement subtree for a DAV::ace
1362 function BuildACE( &$xmldoc, $privs, $principal ) {
1363 $privilege_names = bits_to_privilege($privs, ($this->_is_collection ? $this->collection->type : 'resource'));
1364 $privileges = array();
1365 foreach( $privilege_names AS $k ) {
1366 $privilege = new XMLElement('privilege');
1367 if ( isset($xmldoc) )
1368 $xmldoc->NSElement($privilege,$k);
1369 else
1370 $privilege->NewElement($k);
1371 $privileges[] = $privilege;
1373 $ace = new XMLElement('ace', array(
1374 new XMLElement('principal', $principal),
1375 new XMLElement('grant', $privileges ) )
1377 return $ace;
1381 * Return ACL settings
1383 function GetACL( &$xmldoc ) {
1384 global $c, $session;
1386 if ( !isset($this->principal) ) $this->FetchPrincipal();
1387 $default_privs = $this->principal->default_privileges;
1388 if ( isset($this->collection->default_privileges) ) $default_privs = $this->collection->default_privileges;
1390 $acl = array();
1391 $acl[] = $this->BuildACE($xmldoc, pow(2,25) - 1, new XMLElement('property', new XMLElement('owner')) );
1393 $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',
1394 array( ':collection_id' => $this->collection->collection_id,
1395 ':principal_id' => $this->principal->principal_id() ) );
1396 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) {
1397 $by_collection = null;
1398 while( $grant = $qry->Fetch() ) {
1399 if ( !isset($by_collection) ) $by_collection = isset($grant->by_collection);
1400 if ( $by_collection && !isset($grant->by_collection) ) break;
1401 $acl[] = $this->BuildACE($xmldoc, $grant->privileges, $xmldoc->href(ConstructURL($grant->dav_name)) );
1405 $acl[] = $this->BuildACE($xmldoc, $default_privs, new XMLElement('authenticated') );
1407 return $acl;
1413 * Return general server-related properties, in plain form
1415 function GetProperty( $name ) {
1416 global $c, $session;
1418 // dbg_error_log( 'DAVResource', ':GetProperty: Fetching "%s".', $name );
1419 $value = null;
1421 switch( $name ) {
1422 case 'collection_id':
1423 return $this->collection_id();
1424 break;
1426 case 'principal_id':
1427 if ( !isset($this->principal) ) $this->FetchPrincipal();
1428 return $this->principal->principal_id();
1429 break;
1431 case 'resourcetype':
1432 if ( isset($this->resourcetypes) ) {
1433 $this->resourcetypes = preg_replace('{^\s*<(.*)/>\s*$}', '$1', $this->resourcetypes);
1434 $type_list = preg_split('{(/>\s*<|\n)}', $this->resourcetypes);
1435 foreach( $type_list AS $k => $resourcetype ) {
1436 if ( preg_match( '{^([^:]+):([^:]+) \s+ xmlns:([^=]+)="([^"]+)" \s* $}x', $resourcetype, $matches ) ) {
1437 $type_list[$k] = $matches[4] .':' .$matches[2];
1439 else if ( preg_match( '{^([^:]+) \s+ xmlns="([^"]+)" \s* $}x', $resourcetype, $matches ) ) {
1440 $type_list[$k] = $matches[2] .':' .$matches[1];
1443 return $type_list;
1446 case 'resource':
1447 if ( !isset($this->resource) ) $this->FetchResource();
1448 return clone($this->resource);
1449 break;
1451 case 'dav-data':
1452 if ( !isset($this->resource) ) $this->FetchResource();
1453 trace_bug("Exists ".($this->exists?"true":"false"));
1454 return $this->resource->caldav_data;
1455 break;
1457 case 'principal':
1458 if ( !isset($this->principal) ) $this->FetchPrincipal();
1459 return clone($this->principal);
1460 break;
1462 default:
1463 if ( isset($this->{$name}) ) {
1464 if ( ! is_object($this->{$name}) ) return $this->{$name};
1465 return clone($this->{$name});
1467 if ( $this->_is_principal ) {
1468 if ( !isset($this->principal) ) $this->FetchPrincipal();
1469 if ( isset($this->principal->{$name}) ) return $this->principal->{$name};
1470 if ( isset($this->collection->{$name}) ) return $this->collection->{$name};
1472 else if ( $this->_is_collection ) {
1473 if ( isset($this->collection->{$name}) ) return $this->collection->{$name};
1474 if ( isset($this->principal->{$name}) ) return $this->principal->{$name};
1476 else {
1477 if ( !isset($this->resource) ) $this->FetchResource();
1478 if ( isset($this->resource->{$name}) ) return $this->resource->{$name};
1479 if ( !isset($this->principal) ) $this->FetchPrincipal();
1480 if ( isset($this->principal->{$name}) ) return $this->principal->{$name};
1481 if ( isset($this->collection->{$name}) ) return $this->collection->{$name};
1483 if ( isset($this->{$name}) ) {
1484 if ( ! is_object($this->{$name}) ) return $this->{$name};
1485 return clone($this->{$name});
1487 // dbg_error_log( 'DAVResource', ':GetProperty: Failed to find property "%s" on "%s".', $name, $this->dav_name );
1490 return $value;
1495 * Return an array which is an expansion of the DAV::allprop
1497 function DAV_AllProperties() {
1498 if ( isset($this->dead_properties) ) $this->FetchDeadProperties();
1499 $allprop = array_merge( (isset($this->dead_properties)?$this->dead_properties:array()),
1500 (isset($include_properties)?$include_properties:array()),
1501 array(
1502 'DAV::getcontenttype', 'DAV::resourcetype', 'DAV::getcontentlength', 'DAV::displayname', 'DAV::getlastmodified',
1503 'DAV::creationdate', 'DAV::getetag', 'DAV::getcontentlanguage', 'DAV::supportedlock', 'DAV::lockdiscovery',
1504 'DAV::owner', 'DAV::principal-URL', 'DAV::current-user-principal',
1505 'urn:ietf:params:xml:ns:carddav:max-resource-size', 'urn:ietf:params:xml:ns:carddav:supported-address-data',
1506 'urn:ietf:params:xml:ns:carddav:addressbook-description', 'urn:ietf:params:xml:ns:carddav:addressbook-home-set'
1507 ) );
1509 return $allprop;
1514 * Return general server-related properties for this URL
1516 function ResourceProperty( $tag, $prop, &$reply, &$denied ) {
1517 global $c, $session, $request;
1519 // dbg_error_log( 'DAVResource', 'Processing "%s" on "%s".', $tag, $this->dav_name );
1521 if ( $reply === null ) $reply = $GLOBALS['reply'];
1523 switch( $tag ) {
1524 case 'DAV::allprop':
1525 $property_list = $this->DAV_AllProperties();
1526 $discarded = array();
1527 foreach( $property_list AS $k => $v ) {
1528 $this->ResourceProperty($v, $prop, $reply, $discarded);
1530 break;
1532 case 'DAV::href':
1533 $prop->NewElement('href', ConstructURL($this->dav_name) );
1534 break;
1536 case 'DAV::resource-id':
1537 if ( $this->resource_id > 0 )
1538 $reply->DAVElement( $prop, 'resource-id', $reply->href(ConstructURL('/.resources/'.$this->resource_id) ) );
1539 else
1540 return false;
1541 break;
1543 case 'DAV::parent-set':
1544 $sql = <<<EOQRY
1545 SELECT b.parent_container FROM dav_binding b JOIN collection c ON (b.bound_source_id=c.collection_id)
1546 WHERE regexp_replace( b.dav_name, '^.*/', c.dav_name ) = :bound_from
1547 EOQRY;
1548 $qry = new AwlQuery($sql, array( ':bound_from' => $this->bound_from() ) );
1549 $parents = array();
1550 if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() > 0 ) {
1551 while( $row = $qry->Fetch() ) {
1552 $parents[$row->parent_container] = true;
1555 $parents[preg_replace( '{(?<=/)[^/]+/?$}','',$this->bound_from())] = true;
1556 $parents[preg_replace( '{(?<=/)[^/]+/?$}','',$this->dav_name())] = true;
1558 $parent_set = $reply->DAVElement( $prop, 'parent-set' );
1559 foreach( $parents AS $parent => $v ) {
1560 if ( preg_match( '{^(.*)?/([^/]+)/?$}', $parent, $matches ) ) {
1561 $reply->DAVElement($parent_set, 'parent', array(
1562 new XMLElement( 'href', ConstructURL($matches[1])),
1563 new XMLElement( 'segment', $matches[2])
1566 else if ( $parent == '/' ) {
1567 $reply->DAVElement($parent_set, 'parent', array(
1568 new XMLElement( 'href', '/'),
1569 new XMLElement( 'segment', ( ConstructURL('/') == '/caldav.php/' ? 'caldav.php' : ''))
1573 break;
1575 case 'DAV::getcontenttype':
1576 if ( !isset($this->contenttype) && !$this->_is_collection && !isset($this->resource) ) $this->FetchResource();
1577 $prop->NewElement('getcontenttype', $this->contenttype );
1578 break;
1580 case 'DAV::resourcetype':
1581 $resourcetypes = $prop->NewElement('resourcetype' );
1582 if ( $this->_is_collection ) {
1583 $type_list = $this->GetProperty('resourcetype');
1584 if ( !is_array($type_list) ) return true;
1585 // dbg_error_log( 'DAVResource', ':ResourceProperty: "%s" are "%s".', $tag, implode(', ',$type_list) );
1586 foreach( $type_list AS $k => $v ) {
1587 if ( $v == '' ) continue;
1588 $reply->NSElement( $resourcetypes, $v );
1590 if ( $this->_is_binding ) {
1591 $reply->NSElement( $resourcetypes, 'http://xmlns.davical.org/davical:webdav-binding' );
1594 break;
1596 case 'DAV::getlastmodified':
1597 /** getlastmodified is HTTP Date format: i.e. the Last-Modified header in response to a GET */
1598 $reply->NSElement($prop, $tag, ISODateToHTTPDate($this->GetProperty('modified')) );
1599 break;
1601 case 'DAV::creationdate':
1602 /** creationdate is ISO8601 format */
1603 $reply->NSElement($prop, $tag, DateToISODate($this->GetProperty('created'), true) );
1604 break;
1606 case 'DAV::getcontentlength':
1607 if ( $this->_is_collection ) return false;
1608 if ( !isset($this->resource) ) $this->FetchResource();
1609 if ( isset($this->resource) ) {
1610 $reply->NSElement($prop, $tag, strlen($this->resource->caldav_data) );
1612 break;
1614 case 'DAV::getcontentlanguage':
1615 $locale = (isset($c->current_locale) ? $c->current_locale : '');
1616 if ( isset($this->locale) && $this->locale != '' ) $locale = $this->locale;
1617 $reply->NSElement($prop, $tag, $locale );
1618 break;
1620 case 'DAV::acl-restrictions':
1621 $reply->NSElement($prop, $tag, array( new XMLElement('grant-only'), new XMLElement('no-invert') ) );
1622 break;
1624 case 'DAV::inherited-acl-set':
1625 $inherited_acls = array();
1626 if ( ! $this->_is_collection ) {
1627 $inherited_acls[] = $reply->href(ConstructURL($this->collection->dav_name));
1629 $reply->NSElement($prop, $tag, $inherited_acls );
1630 break;
1632 case 'DAV::owner':
1633 // The principal-URL of the owner
1634 if ( $this->IsExternal() ){
1635 $reply->DAVElement( $prop, 'owner', $reply->href( ConstructURL($this->collection->bound_from )) );
1637 else {
1638 $reply->DAVElement( $prop, 'owner', $reply->href( ConstructURL(DeconstructURL($this->principal_url())) ) );
1640 break;
1642 case 'DAV::add-member':
1643 if ( ! $this->_is_collection ) return false;
1644 if ( isset($c->post_add_member) && $c->post_add_member === false ) return false;
1645 $reply->DAVElement( $prop, 'add-member', $reply->href(ConstructURL(DeconstructURL($this->url())).'?add-member') );
1646 break;
1648 // Empty tag responses.
1649 case 'DAV::group':
1650 case 'DAV::alternate-URI-set':
1651 $reply->NSElement($prop, $tag );
1652 break;
1654 case 'DAV::getetag':
1655 if ( $this->_is_collection ) return false;
1656 $reply->NSElement($prop, $tag, $this->unique_tag() );
1657 break;
1659 case 'http://calendarserver.org/ns/:getctag':
1660 if ( ! $this->_is_collection ) return false;
1661 $reply->NSElement($prop, $tag, $this->unique_tag() );
1662 break;
1664 case 'DAV::sync-token':
1665 if ( ! $this->_is_collection ) return false;
1666 $sync_token = $this->sync_token();
1667 if ( empty($sync_token) ) return false;
1668 $reply->NSElement($prop, $tag, $sync_token );
1669 break;
1671 case 'http://calendarserver.org/ns/:calendar-proxy-read-for':
1672 $proxy_type = 'read';
1673 case 'http://calendarserver.org/ns/:calendar-proxy-write-for':
1674 if ( isset($c->disable_caldav_proxy) && $c->disable_caldav_proxy ) return false;
1675 if ( !isset($proxy_type) ) $proxy_type = 'write';
1676 // ProxyFor is an already constructed URL
1677 $reply->CalendarserverElement($prop, 'calendar-proxy-'.$proxy_type.'-for', $reply->href( $this->principal->ProxyFor($proxy_type) ) );
1678 break;
1680 case 'DAV::current-user-privilege-set':
1681 if ( $this->HavePrivilegeTo('DAV::read-current-user-privilege-set') ) {
1682 $reply->NSElement($prop, $tag, $this->BuildPrivileges() );
1684 else {
1685 $denied[] = $tag;
1687 break;
1689 case 'urn:ietf:params:xml:ns:caldav:supported-calendar-data':
1690 if ( ! $this->IsCalendar() && ! $this->IsSchedulingCollection() ) return false;
1691 $reply->NSElement($prop, $tag, 'text/calendar' );
1692 break;
1694 case 'urn:ietf:params:xml:ns:caldav:supported-calendar-component-set':
1695 if ( ! $this->_is_collection ) return false;
1696 if ( $this->IsCalendar() ) {
1697 if ( !isset($this->dead_properties) ) $this->FetchDeadProperties();
1698 if ( isset($this->dead_properties[$tag]) ) {
1699 $set_of_components = $this->dead_properties[$tag];
1700 foreach( $set_of_components AS $k => $v ) {
1701 if ( preg_match('{(VEVENT|VTODO|VJOURNAL|VTIMEZONE|VFREEBUSY|VPOLL|VAVAILABILITY)}', $v, $matches) ) {
1702 $set_of_components[$k] = $matches[1];
1704 else {
1705 unset( $set_of_components[$k] );
1709 else {
1710 $set_of_components = array( 'VEVENT', 'VTODO', 'VJOURNAL', 'VTIMEZONE', 'VFREEBUSY', 'VPOLL', 'VAVAILABILITY' );
1713 else if ( $this->IsSchedulingCollection() )
1714 $set_of_components = array( 'VEVENT', 'VTODO', 'VFREEBUSY' );
1715 else return false;
1716 $components = array();
1717 foreach( $set_of_components AS $v ) {
1718 $components[] = $reply->NewXMLElement( 'comp', '', array('name' => $v), 'urn:ietf:params:xml:ns:caldav');
1720 $reply->CalDAVElement($prop, 'supported-calendar-component-set', $components );
1721 break;
1723 case 'DAV::supported-method-set':
1724 $prop->NewElement('supported-method-set', $this->BuildSupportedMethods() );
1725 break;
1727 case 'DAV::supported-report-set':
1728 $prop->NewElement('supported-report-set', $this->BuildSupportedReports( $reply ) );
1729 break;
1731 case 'DAV::supportedlock':
1732 $prop->NewElement('supportedlock',
1733 new XMLElement( 'lockentry',
1734 array(
1735 new XMLElement('lockscope', new XMLElement('exclusive')),
1736 new XMLElement('locktype', new XMLElement('write')),
1740 break;
1742 case 'DAV::supported-privilege-set':
1743 $prop->NewElement('supported-privilege-set', $request->BuildSupportedPrivileges($reply) );
1744 break;
1746 case 'DAV::principal-collection-set':
1747 $prop->NewElement( 'principal-collection-set', $reply->href( ConstructURL('/') ) );
1748 break;
1750 case 'DAV::current-user-principal':
1751 $prop->NewElement('current-user-principal', $reply->href( ConstructURL(DeconstructURL($request->principal->url())) ) );
1752 break;
1754 case 'SOME-DENIED-PROPERTY': /** indicating the style for future expansion */
1755 $denied[] = $reply->Tag($tag);
1756 break;
1758 case 'urn:ietf:params:xml:ns:caldav:calendar-timezone':
1759 if ( ! $this->_is_collection ) return false;
1760 if ( !isset($this->collection->vtimezone) || $this->collection->vtimezone == '' ) return false;
1762 $cal = new iCalComponent();
1763 $cal->VCalendar();
1764 $cal->AddComponent( new iCalComponent($this->collection->vtimezone) );
1765 $reply->NSElement($prop, $tag, $cal->Render() );
1766 break;
1768 case 'urn:ietf:params:xml:ns:carddav:address-data':
1769 case 'urn:ietf:params:xml:ns:caldav:calendar-data':
1770 if ( $this->_is_collection ) return false;
1771 if ( !isset($c->sync_resource_data_ok) || $c->sync_resource_data_ok == false ) return false;
1772 if ( !isset($this->resource) ) $this->FetchResource();
1773 $reply->NSElement($prop, $tag, $this->resource->caldav_data );
1774 break;
1776 case 'urn:ietf:params:xml:ns:carddav:max-resource-size':
1777 if ( ! $this->_is_collection || !$this->_is_addressbook ) return false;
1778 $reply->NSElement($prop, $tag, 65500 );
1779 break;
1781 case 'urn:ietf:params:xml:ns:carddav:supported-address-data':
1782 if ( ! $this->_is_collection || !$this->_is_addressbook ) return false;
1783 $address_data = $reply->NewXMLElement( 'address-data', false,
1784 array( 'content-type' => 'text/vcard', 'version' => '3.0'), 'urn:ietf:params:xml:ns:carddav');
1785 $reply->NSElement($prop, $tag, $address_data );
1786 break;
1788 case 'DAV::acl':
1789 if ( $this->HavePrivilegeTo('DAV::read-acl') ) {
1790 $reply->NSElement($prop, $tag, $this->GetACL( $reply ) );
1792 else {
1793 $denied[] = $tag;
1795 break;
1797 case 'http://www.xythos.com/namespaces/StorageServer:ticketdiscovery':
1798 case 'DAV::ticketdiscovery':
1799 $reply->NSElement($prop,'http://www.xythos.com/namespaces/StorageServer:ticketdiscovery', $this->BuildTicketinfo($reply) );
1800 break;
1802 default:
1803 $property_value = $this->GetProperty(preg_replace('{^.*:}', '', $tag));
1804 if ( isset($property_value) ) {
1805 $reply->NSElement($prop, $tag, $property_value );
1807 else {
1808 if ( !isset($this->dead_properties) ) $this->FetchDeadProperties();
1809 if ( isset($this->dead_properties[$tag]) ) {
1810 $reply->NSElement($prop, $tag, $this->dead_properties[$tag] );
1812 else {
1813 // dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of path "%s".', $tag, $this->dav_name );
1814 return false;
1819 return true;
1824 * Construct XML propstat fragment for this resource
1826 * @param array of string $properties The requested properties for this resource
1828 * @return string An XML fragment with the requested properties for this resource
1830 function GetPropStat( $properties, &$reply, $props_only = false ) {
1831 global $request;
1833 dbg_error_log('DAVResource',':GetPropStat: propstat for href "%s"', $this->dav_name );
1835 $prop = new XMLElement('prop', null, null, 'DAV:');
1836 $denied = array();
1837 $not_found = array();
1838 foreach( $properties AS $k => $tag ) {
1839 if ( is_object($tag) ) {
1840 dbg_error_log( 'DAVResource', ':GetPropStat: "$properties" should be an array of text. Assuming this object is an XMLElement!.' );
1841 $tag = $tag->GetNSTag();
1843 $found = $this->ResourceProperty($tag, $prop, $reply, $denied );
1844 if ( !$found ) {
1845 if ( !isset($this->principal) ) $this->FetchPrincipal();
1846 $found = $this->principal->PrincipalProperty( $tag, $prop, $reply, $denied );
1848 if ( ! $found ) {
1849 // dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of resource "%s".', $tag, $this->dav_name );
1850 $not_found[] = $tag;
1853 if ( $props_only ) return $prop;
1855 $status = new XMLElement('status', 'HTTP/1.1 200 OK', null, 'DAV:' );
1857 $elements = array( new XMLElement( 'propstat', array($prop,$status), null, 'DAV:' ) );
1859 if ( count($denied) > 0 ) {
1860 $status = new XMLElement('status', 'HTTP/1.1 403 Forbidden', null, 'DAV:' );
1861 $noprop = new XMLElement('prop', null, null, 'DAV:');
1862 foreach( $denied AS $k => $v ) {
1863 $reply->NSElement($noprop, $v);
1865 $elements[] = new XMLElement( 'propstat', array( $noprop, $status), null, 'DAV:' );
1868 if ( !$request->PreferMinimal() && count($not_found) > 0 ) {
1869 $status = new XMLElement('status', 'HTTP/1.1 404 Not Found', null, 'DAV:' );
1870 $noprop = new XMLElement('prop', null, null, 'DAV:');
1871 foreach( $not_found AS $k => $v ) {
1872 $reply->NSElement($noprop,$v);
1874 $elements[] = new XMLElement( 'propstat', array( $noprop, $status), null, 'DAV:' );
1876 return $elements;
1881 * Render XML for this resource
1883 * @param array $properties The requested properties for this principal
1884 * @param reference $reply A reference to the XMLDocument being used for the reply
1886 * @return string An XML fragment with the requested properties for this principal
1888 function RenderAsXML( $properties, &$reply, $bound_parent_path = null ) {
1889 global $session, $c, $request;
1891 dbg_error_log('DAVResource',':RenderAsXML: Resource "%s" exists(%d)', $this->dav_name, $this->Exists() );
1893 if ( !$this->Exists() ) return null;
1895 $elements = $this->GetPropStat( $properties, $reply );
1896 if ( isset($bound_parent_path) ) {
1897 $dav_name = str_replace( $this->parent_path(), $bound_parent_path, $this->dav_name );
1899 else {
1900 $dav_name = $this->dav_name;
1903 array_unshift( $elements, $reply->href(ConstructURL($dav_name)));
1905 $response = new XMLElement( 'response', $elements, null, 'DAV:' );
1907 return $response;