3 * An object representing a DAV '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');
20 * A class for things to do with a DAV Resource
27 * @var The partial URL of the resource within our namespace, which this resource is being retrieved as
32 * @var Boolean: does the resource actually exist yet?
37 * @var The unique etag associated with the current version of the resource
39 protected $unique_tag;
42 * @var The actual resource content, if it exists and is not a collection
47 * @var The parent of the resource, which will always be a collection
52 * @var The types of the resource, possibly multiple
54 protected $resourcetypes;
57 * @var The type of the content
59 protected $contenttype;
62 * @var The canonical name which this resource exists at
64 protected $bound_from;
67 * @var An object which is the collection record for this resource, or for it's container
72 * @var An object which is the principal for this resource, or would be if it existed.
77 * @var A bit mask representing the current user's privileges towards this DAVResource
82 * @var True if this resource is a collection of any kind
84 private $_is_collection;
87 * @var True if this resource is a principal-URL
89 private $_is_principal;
92 * @var True if this resource is a calendar collection
94 private $_is_calendar;
97 * @var True if this resource is a binding to another resource
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.
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
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) {
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;
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;
221 $this->resource->{$k} = $v;
228 case 'resourcetypes':
229 if ( $this->_is_collection
) $this->{$k} = $v;
233 $this->unique_tag
= '"'.$v.'"';
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';
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
;
272 $this->resourcetypes
= '';
273 if ( isset($this->resource->caldav_data
) ) {
274 $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();
291 foreach( $comps AS $comp ) {
292 $comp->ClearComponents('VALARM');
293 $vcal2->AddComponent($comp);
295 $this->resource->caldav_data
= $vcal2->Render();
298 else if ( strtoupper(substr($this->resource->caldav_data
,0,11)) == 'BEGIN:VCARD' ) {
299 $this->contenttype
= 'text/vcard';
301 else if ( strtoupper(substr($this->resource->caldav_data
,0,11)) == 'BEGIN:VLIST' ) {
302 $this->contenttype
= 'text/x-vlist';
310 * Initialise from a path
311 * @param object $inpath The path to populate the resource data from
313 function FromPath($inpath) {
316 $this->dav_name
= DeconstructURL($inpath);
318 $this->FetchCollection();
319 if ( $this->_is_collection
) {
320 if ( $this->_is_principal ||
$this->collection
->type
== 'principal' ) $this->FetchPrincipal();
323 $this->FetchResource();
325 dbg_error_log( 'DAVResource', ':FromPath: Path "%s" is%s a collection%s.',
326 $this->dav_name
, ($this->_is_collection?
' '.$this->resourcetypes
:' not'), ($this->_is_principal?
' and a principal':'') );
330 private function ReadCollectionFromDatabase() {
333 $this->collection
= (object) array(
334 'collection_id' => -1,
335 'type' => 'nonexistent',
336 'is_calendar' => false, 'is_principal' => false, 'is_addressbook' => false
339 $base_sql = 'SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), ';
340 $base_sql .= 'p.principal_id, p.type_id AS principal_type_id, ';
341 $base_sql .= 'p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges, ';
342 $base_sql .= 'timezones.vtimezone ';
343 $base_sql .= 'FROM collection LEFT JOIN principal p USING (user_no) ';
344 $base_sql .= 'LEFT JOIN timezones ON (collection.timezone=timezones.tzid) ';
345 $base_sql .= 'WHERE ';
346 $sql = $base_sql .'collection.dav_name = :raw_path ';
347 $params = array( ':raw_path' => $this->dav_name
, ':session_principal' => $session->principal_id
, ':scan_depth' => $c->permission_scan_depth
);
348 if ( !preg_match( '#/$#', $this->dav_name
) ) {
349 $sql .= ' OR collection.dav_name = :up_to_slash OR collection.dav_name = :plus_slash ';
350 $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name
);
351 $params[':plus_slash'] = $this->dav_name
.'/';
353 $sql .= 'ORDER BY LENGTH(collection.dav_name) DESC LIMIT 1';
354 $qry = new AwlQuery( $sql, $params );
355 if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
356 $this->collection
= $row;
357 $this->collection
->exists
= true;
358 if ( $row->is_calendar
== 't' )
359 $this->collection
->type
= 'calendar';
360 else if ( $row->is_addressbook
== 't' )
361 $this->collection
->type
= 'addressbook';
362 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name
, $matches ) )
363 $this->collection
->type
= 'schedule-'. $matches[3]. 'box';
365 $this->collection
->type
= 'collection';
367 else if ( preg_match( '{^( ( / ([^/]+) / ) \.(in|out)/ ) [^/]*$}x', $this->dav_name
, $matches ) ) {
368 // The request is for a scheduling inbox or outbox (or something inside one) and we should auto-create it
369 $params = array( ':username' => $matches[3], ':parent_container' => $matches[2], ':dav_name' => $matches[1] );
370 $params[':boxname'] = ($matches[4] == 'in' ?
' Inbox' : ' Outbox');
371 $this->collection_type
= 'schedule-'. $matches[4]. 'box';
372 $params[':resourcetypes'] = sprintf('<DAV::collection/><urn:ietf:params:xml:ns:caldav:%s/>', $this->collection_type
);
374 INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified, dav_etag, resourcetypes )
375 VALUES( (SELECT user_no FROM usr WHERE username = text(:username)),
376 :parent_container, :dav_name,
377 (SELECT fullname FROM usr WHERE username = text(:username)) || :boxname,
378 FALSE, current_timestamp, current_timestamp, '1', :resourcetypes )
380 $qry = new AwlQuery( $sql, $params );
381 $qry->Exec('DAVResource');
382 dbg_error_log( 'DAVResource', 'Created new collection as "%s".', trim($params[':boxname']) );
384 $params = array( ':raw_path' => $this->dav_name
, ':session_principal' => $session->principal_id
, ':scan_depth' => $c->permission_scan_depth
);
385 $qry = new AwlQuery( $base_sql . ' dav_name = :raw_path', $params );
386 if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
387 $this->collection
= $row;
388 $this->collection
->exists
= true;
389 $this->collection
->type
= $this->collection_type
;
392 else if ( preg_match( '#^(/([^/]+)/calendar-proxy-(read|write))/?[^/]*$#', $this->dav_name
, $matches ) ) {
393 $this->collection
->type
= 'proxy';
394 $this->_is_proxy_request
= true;
395 $this->proxy_type
= $matches[3];
396 $this->collection
->dav_name
= $this->dav_name
;
397 $this->collection
->dav_displayname
= sprintf( '%s proxy %s', $matches[2], $matches[3] );
398 $this->collection
->exists
= true;
399 $this->collection
->parent_container
= $matches[1] . '/';
401 else if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name
, $matches)
402 ||
preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name
, $matches) ) {
403 $this->_is_principal
= true;
404 $this->FetchPrincipal();
405 $this->collection
->is_principal
= true;
406 $this->collection
->type
= 'principal';
408 else if ( $this->dav_name
== '/' ) {
409 $this->collection
->dav_name
= '/';
410 $this->collection
->type
= 'root';
411 $this->collection
->exists
= true;
412 $this->collection
->displayname
= $c->system_name
;
413 $this->collection
->default_privileges
= (1 |
16 |
32);
414 $this->collection
->parent_container
= '/';
418 SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), p.principal_id,
419 p.type_id AS principal_type_id, p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges,
420 timezones.vtimezone, dav_binding.access_ticket_id, dav_binding.parent_container AS bind_parent_container,
421 dav_binding.dav_displayname, owner.dav_name AS bind_owner_url, dav_binding.dav_name AS bound_to,
422 dav_binding.external_url AS external_url, dav_binding.type AS external_type, dav_binding.bind_id AS bind_id
424 LEFT JOIN collection ON (collection.collection_id=bound_source_id)
425 LEFT JOIN principal p USING (user_no)
426 LEFT JOIN dav_principal owner ON (dav_binding.dav_owner_id=owner.principal_id)
427 LEFT JOIN timezones ON (collection.timezone=timezones.tzid)
428 WHERE dav_binding.dav_name = :raw_path
430 $params = array( ':raw_path' => $this->dav_name
, ':session_principal' => $session->principal_id
, ':scan_depth' => $c->permission_scan_depth
);
431 if ( !preg_match( '#/$#', $this->dav_name
) ) {
432 $sql .= ' OR dav_binding.dav_name = :up_to_slash OR collection.dav_name = :plus_slash ';
433 $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name
);
434 $params[':plus_slash'] = $this->dav_name
.'/';
436 $sql .= ' ORDER BY LENGTH(dav_binding.dav_name) DESC LIMIT 1';
437 $qry = new AwlQuery( $sql, $params );
438 if ( $qry->Exec('DAVResource',__LINE__
,__FILE__
) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
439 $this->collection
= $row;
440 $this->collection
->exists
= true;
441 $this->collection
->parent_set
= $row->parent_container
;
442 $this->collection
->parent_container
= $row->bind_parent_container
;
443 $this->collection
->bound_from
= $row->dav_name
;
444 $this->collection
->dav_name
= $row->bound_to
;
445 if ( $row->is_calendar
== 't' )
446 $this->collection
->type
= 'calendar';
447 else if ( $row->is_addressbook
== 't' )
448 $this->collection
->type
= 'addressbook';
449 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name
, $matches ) )
450 $this->collection
->type
= 'schedule-'. $matches[3]. 'box';
452 $this->collection
->type
= 'collection';
453 if ( strlen($row->external_url
) > 8 ) {
454 $this->_is_external
= true;
455 if ( $row->external_type
== 'calendar' )
456 $this->collection
->type
= 'calendar';
457 else if ( $row->external_type
== 'addressbook' )
458 $this->collection
->type
= 'addressbook';
460 $this->collection
->type
= 'collection';
462 $this->_is_binding
= true;
463 $this->bound_from
= str_replace( $row->bound_to
, $row->dav_name
, $this->dav_name
);
464 if ( isset($row->access_ticket_id
) ) {
465 if ( !isset($this->tickets
) ) $this->tickets
= array();
466 $this->tickets
[] = new DAVTicket($row->access_ticket_id
);
470 dbg_error_log( 'DAVResource', 'No collection for path "%s".', $this->dav_name
);
471 $this->collection
->exists
= false;
472 $this->collection
->dav_name
= preg_replace('{/[^/]*$}', '/', $this->dav_name
);
479 * Find the collection associated with this resource.
481 protected function FetchCollection() {
485 * RFC4918, 8.3: Identifiers for collections SHOULD end in '/'
486 * - also discussed at more length in 5.2
488 * So we look for a collection which matches one of the following URLs:
489 * - The exact request.
490 * - If the exact request, doesn't end in '/', then the request URL with a '/' appended
491 * - The request URL truncated to the last '/'
492 * The collection URL for this request is therefore the longest row in the result, so we
493 * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1"
495 dbg_error_log( 'DAVResource', ':FetchCollection: Looking for collection for "%s".', $this->dav_name
);
497 // Try and pull the answer out of a hat
498 $cache = getCacheInstance();
499 $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $this->dav_name
);
500 $cache_key = 'dav_resource'.$session->user_no
;
501 $this->collection
= $cache->get( $cache_ns, $cache_key );
502 if ( $this->collection
=== false ) {
503 $this->ReadCollectionFromDatabase();
504 if ( $this->collection
->type
!= 'principal' ) {
505 $cache_ns = 'collection-'.$this->collection
->dav_name
;
506 @dbg_error_log
( 'Cache', ':FetchCollection: Setting cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection
->type
);
507 $cache->set( $cache_ns, $cache_key, $this->collection
);
509 @dbg_error_log
( 'DAVResource', ':FetchCollection: Found collection named "%s" of type "%s".', $this->collection
->dav_name
, $this->collection
->type
);
512 @dbg_error_log
( 'Cache', ':FetchCollection: Got cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection
->type
);
513 if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name
, $matches)
514 ||
preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name
, $matches) ) {
515 $this->_is_principal
= true;
516 $this->FetchPrincipal();
517 $this->collection
->is_principal
= true;
518 $this->collection
->type
= 'principal';
520 @dbg_error_log
( 'DAVResource', ':FetchCollection: Read cached collection named "%s" of type "%s".', $this->collection
->dav_name
, $this->collection
->type
);
523 if ( isset($this->collection
->bound_from
) ) {
524 $this->_is_binding
= true;
525 $this->bound_from
= str_replace( $this->collection
->bound_to
, $this->collection
->bound_from
, $this->dav_name
);
526 if ( isset($this->collection
->access_ticket_id
) ) {
527 if ( !isset($this->tickets
) ) $this->tickets
= array();
528 $this->tickets
[] = new DAVTicket($this->collection
->access_ticket_id
);
532 $this->_is_collection
= ( $this->_is_principal ||
$this->collection
->dav_name
== $this->dav_name ||
$this->collection
->dav_name
== $this->dav_name
.'/' );
533 if ( $this->_is_collection
) {
534 $this->dav_name
= $this->collection
->dav_name
;
535 $this->resource_id
= $this->collection
->collection_id
;
536 $this->_is_calendar
= ($this->collection
->type
== 'calendar');
537 $this->_is_addressbook
= ($this->collection
->type
== 'addressbook');
538 $this->contenttype
= 'httpd/unix-directory';
539 if ( !isset($this->exists
) && isset($this->collection
->exists
) ) {
540 // If this seems peculiar it's because we only set it to false above...
541 $this->exists
= $this->collection
->exists
;
543 if ( $this->exists
) {
544 if ( isset($this->collection
->dav_etag
) ) $this->unique_tag
= '"'.$this->collection
->dav_etag
.'"';
545 if ( isset($this->collection
->created
) ) $this->created
= $this->collection
->created
;
546 if ( isset($this->collection
->modified
) ) $this->modified
= $this->collection
->modified
;
547 if ( isset($this->collection
->dav_displayname
) ) $this->collection
->displayname
= $this->collection
->dav_displayname
;
550 if ( !isset($this->parent
) ) $this->GetParentContainer();
551 $this->user_no
= $this->parent
->GetProperty('user_no');
553 if ( isset($this->collection
->resourcetypes
) )
554 $this->resourcetypes
= $this->collection
->resourcetypes
;
556 $this->resourcetypes
= '<DAV::collection/>';
557 if ( $this->_is_principal
) $this->resourcetypes
.= '<DAV::principal/>';
558 if ( $this->_is_addressbook
) $this->resourcetypes
.= '<urn:ietf:params:xml:ns:carddav:addressbook/>';
559 if ( $this->_is_calendar
) $this->resourcetypes
.= '<urn:ietf:params:xml:ns:caldav:calendar/>';
566 * Find the principal associated with this resource.
568 protected function FetchPrincipal() {
569 if ( isset($this->principal
) ) return;
570 $this->principal
= new DAVPrincipal( array( "path" => $this->bound_from() ) );
571 if ( $this->_is_principal
) {
572 $this->exists
= $this->principal
->Exists();
573 $this->collection
->dav_name
= $this->dav_name();
574 $this->collection
->type
= 'principal';
575 if ( $this->exists
) {
576 $this->collection
= $this->principal
->AsCollection();
577 $this->displayname
= $this->principal
->GetProperty('displayname');
578 $this->user_no
= $this->principal
->user_no();
579 $this->resource_id
= $this->principal
->principal_id();
580 $this->created
= $this->principal
->created
;
581 $this->modified
= $this->principal
->modified
;
582 $this->resourcetypes
= $this->principal
->resourcetypes
;
589 * Retrieve the actual resource.
591 protected function FetchResource() {
594 if ( isset($this->exists
) ) return; // True or false, we've got what we can already
595 if ( $this->_is_collection
) return; // We have all we're going to read
598 SELECT calendar_item.*, addressbook_resource.*, caldav_data.*
599 FROM caldav_data LEFT OUTER JOIN calendar_item USING (collection_id,dav_id)
600 LEFT OUTER JOIN addressbook_resource USING (dav_id)
601 WHERE caldav_data.dav_name = :dav_name
603 $params = array( ':dav_name' => $this->bound_from() );
605 $qry = new AwlQuery( $sql, $params );
606 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) {
607 $this->exists
= true;
608 $row = $qry->Fetch();
609 $this->FromRow($row);
612 $this->exists
= false;
618 * Fetch any dead properties for this URL
620 protected function FetchDeadProperties() {
621 if ( isset($this->dead_properties
) ) return;
623 $this->dead_properties
= array();
624 if ( !$this->exists ||
!$this->_is_collection
) return;
626 $qry = new AwlQuery('SELECT property_name, property_value FROM property WHERE dav_name= :dav_name', array(':dav_name' => $this->dav_name
) );
627 if ( $qry->Exec('DAVResource') ) {
628 while ( $property = $qry->Fetch() ) {
629 $this->dead_properties
[$property->property_name
] = $property->property_value
;
636 * Build permissions for this URL
638 protected function FetchPrivileges() {
639 global $session, $request;
641 if ( $this->dav_name
== '/' ||
$this->dav_name
== '' ||
$this->_is_external
) {
642 $this->privileges
= (1 |
16 |
32); // read + read-acl + read-current-user-privilege-set
643 dbg_error_log( 'DAVResource', ':FetchPrivileges: Read permissions for user accessing /' );
647 if ( $session->AllowedTo('Admin') ) {
648 $this->privileges
= privilege_to_bits('all');
649 dbg_error_log( 'DAVResource', ':FetchPrivileges: Full permissions for an administrator.' );
653 if ( $this->IsPrincipal() ) {
654 if ( !isset($this->principal
) ) $this->FetchPrincipal();
655 $this->privileges
= $this->principal
->Privileges();
656 dbg_error_log( 'DAVResource', ':FetchPrivileges: Privileges of "%s" for user accessing principal "%s"', $this->privileges
, $this->principal
->username() );
660 if ( ! isset($this->collection
) ) $this->FetchCollection();
661 $this->privileges
= 0;
662 if ( !isset($this->collection
->path_privs
) ) {
663 if ( !isset($this->parent
) ) $this->GetParentContainer();
665 $this->collection
->path_privs
= $this->parent
->Privileges();
666 $this->collection
->user_no
= $this->parent
->GetProperty('user_no');
667 $this->collection
->principal_id
= $this->parent
->GetProperty('principal_id');
670 $this->privileges
= $this->collection
->path_privs
;
671 if ( is_string($this->privileges
) ) $this->privileges
= bindec( $this->privileges
);
673 dbg_error_log( 'DAVResource', ':FetchPrivileges: Privileges of "%s" for user "%s" accessing "%s"',
674 decbin($this->privileges
), $session->username
, $this->dav_name() );
676 if ( isset($request->ticket
) && $request->ticket
->MatchesPath($this->bound_from()) ) {
677 $this->privileges |
= $request->ticket
->privileges();
678 dbg_error_log( 'DAVResource', ':FetchPrivileges: Applying permissions for ticket "%s" now: %s', $request->ticket
->id(), decbin($this->privileges
) );
681 if ( isset($this->tickets
) ) {
682 if ( !isset($this->resource_id
) ) $this->FetchResource();
683 foreach( $this->tickets
AS $k => $ticket ) {
684 if ( $ticket->MatchesResource($this->resource_id()) ||
$ticket->MatchesPath($this->bound_from()) ) {
685 $this->privileges |
= $ticket->privileges();
686 dbg_error_log( 'DAVResource', ':FetchPrivileges: Applying permissions for ticket "%s" now: %s', $ticket->id(), decbin($this->privileges
) );
694 * Get a DAVResource which is the parent to this resource.
696 function GetParentContainer() {
697 if ( $this->dav_name
== '/' ) return null;
698 if ( !isset($this->parent
) ) {
699 if ( $this->_is_collection
) {
700 dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection
->dav_name
, $this->dav_name() );
701 $this->parent
= new DAVResource( $this->parent_path() );
704 dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection
->dav_name
, $this->dav_name() );
705 $this->parent
= new DAVResource($this->collection
->dav_name
);
708 return $this->parent
;
713 * Fetch the parent to this resource. This is deprecated - use GetParentContainer() instead.
715 function FetchParentContainer() {
716 deprecated('DAVResource::FetchParentContainer');
717 return $this->GetParentContainer();
722 * Return the privileges bits for the current session user to this resource
724 function Privileges() {
725 if ( !isset($this->privileges
) ) $this->FetchPrivileges();
726 return $this->privileges
;
731 * Is the user has the privileges to do what is requested.
732 * @param $do_what mixed The request privilege name, or array of privilege names, to be checked.
733 * @param $any boolean Whether we accept any of the privileges. The default is true, unless the requested privilege is 'all', when it is false.
734 * @return boolean Whether they do have one of those privileges against this resource.
736 function HavePrivilegeTo( $do_what, $any = null ) {
737 if ( !isset($this->privileges
) ) $this->FetchPrivileges();
738 if ( !isset($any) ) $any = ($do_what != 'all');
739 $test_bits = privilege_to_bits( $do_what );
740 dbg_error_log( 'DAVResource', 'Testing %s privileges of "%s" (%s) against allowed "%s" => "%s" (%s)', ($any?
'any':'exactly'),
741 $do_what, decbin($test_bits), decbin($this->privileges
), ($this->privileges
& $test_bits), decbin($this->privileges
& $test_bits) );
743 return ($this->privileges
& $test_bits) > 0;
746 return ($this->privileges
& $test_bits) == $test_bits;
752 * Check if we have the needed privilege or send an error response. If the user does not have the privileges then
753 * the call will not return, and an XML error document will be output.
755 * @param string $privilege The name of the needed privilege.
756 * @param boolean $any Whether we accept any of the privileges. The default is true, unless the requested privilege is 'all', when it is false.
758 function NeedPrivilege( $privilege, $any = null ) {
762 if ( $this->HavePrivilegeTo($privilege, $any) ) return;
764 // They failed, so output the error
765 $request->NeedPrivilege( $privilege, $this->dav_name
);
766 exit(0); // Unecessary, but might clarify things
771 * Returns the array of privilege names converted into XMLElements
773 function BuildPrivileges( $privilege_names=null, &$xmldoc=null ) {
774 if ( $privilege_names == null ) {
775 if ( !isset($this->privileges
) ) $this->FetchPrivileges();
776 $privilege_names = bits_to_privilege($this->privileges
, ($this->_is_collection ?
$this->collection
->type
: null ) );
778 return privileges_to_XML( $privilege_names, $xmldoc);
783 * Returns the array of supported methods
785 function FetchSupportedMethods( ) {
786 if ( isset($this->supported_methods
) ) return $this->supported_methods
;
788 $this->supported_methods
= array(
797 if ( $this->IsCollection() ) {
798 /* if ( $this->IsPrincipal() ) {
799 $this->supported_methods['MKCALENDAR'] = '';
800 $this->supported_methods['MKCOL'] = '';
802 switch ( $this->collection
->type
) {
805 // We just override the list completely here.
806 $this->supported_methods
= array(
813 case 'schedule-outbox':
814 $this->supported_methods
= array_merge(
815 $this->supported_methods
,
817 'POST' => '', 'PROPPATCH' => '', 'MKTICKET' => '', 'DELTICKET' => ''
821 case 'schedule-inbox':
823 $this->supported_methods
['GET'] = '';
824 $this->supported_methods
['PUT'] = '';
825 $this->supported_methods
['HEAD'] = '';
826 $this->supported_methods
['MKTICKET'] = '';
827 $this->supported_methods
['DELTICKET'] = '';
828 $this->supported_methods
['ACL'] = '';
831 $this->supported_methods
['MKTICKET'] = '';
832 $this->supported_methods
['DELTICKET'] = '';
833 $this->supported_methods
['BIND'] = '';
834 $this->supported_methods
['ACL'] = '';
836 $this->supported_methods
['GET'] = '';
837 $this->supported_methods
['HEAD'] = '';
838 $this->supported_methods
['MKCOL'] = '';
839 $this->supported_methods
['MKCALENDAR'] = '';
840 $this->supported_methods
['PROPPATCH'] = '';
841 $this->supported_methods
['BIND'] = '';
842 $this->supported_methods
['ACL'] = '';
847 $this->supported_methods
= array_merge(
848 $this->supported_methods
,
850 'GET' => '', 'HEAD' => '', 'PUT' => '', 'MKTICKET' => '', 'DELTICKET' => ''
855 return $this->supported_methods
;
860 * Returns the array of supported methods converted into XMLElements
862 function BuildSupportedMethods( ) {
863 if ( !isset($this->supported_methods
) ) $this->FetchSupportedMethods();
865 foreach( $this->supported_methods
AS $k => $v ) {
866 // dbg_error_log( 'DAVResource', ':BuildSupportedMethods: Adding method "%s" which is "%s".', $k, $v );
867 $methods[] = new XMLElement( 'supported-method', null, array('name' => $k) );
874 * Returns the array of supported reports
876 function FetchSupportedReports( ) {
877 if ( isset($this->supported_reports
) ) return $this->supported_reports
;
879 $this->supported_reports
= array(
880 'DAV::principal-property-search' => '',
881 'DAV::principal-search-property-set' => '',
882 'DAV::expand-property' => '',
883 'DAV::sync-collection' => ''
886 if ( !isset($this->collection
) ) $this->FetchCollection();
888 if ( $this->collection
->is_calendar
) {
889 $this->supported_reports
= array_merge(
890 $this->supported_reports
,
892 'urn:ietf:params:xml:ns:caldav:calendar-query' => '',
893 'urn:ietf:params:xml:ns:caldav:calendar-multiget' => '',
894 'urn:ietf:params:xml:ns:caldav:free-busy-query' => ''
898 if ( $this->collection
->is_addressbook
) {
899 $this->supported_reports
= array_merge(
900 $this->supported_reports
,
902 'urn:ietf:params:xml:ns:carddav:addressbook-query' => '',
903 'urn:ietf:params:xml:ns:carddav:addressbook-multiget' => ''
907 return $this->supported_reports
;
912 * Returns the array of supported reports converted into XMLElements
914 function BuildSupportedReports( &$reply ) {
915 if ( !isset($this->supported_reports
) ) $this->FetchSupportedReports();
917 foreach( $this->supported_reports
AS $k => $v ) {
918 dbg_error_log( 'DAVResource', ':BuildSupportedReports: Adding supported report "%s" which is "%s".', $k, $v );
919 $report = new XMLElement('report');
920 $reply->NSElement($report, $k );
921 $reports[] = new XMLElement('supported-report', $report );
928 * Fetches an array of the access_ticket records applying to this path
930 function FetchTickets( ) {
932 if ( isset($this->access_tickets
) ) return;
933 $this->access_tickets
= array();
936 'SELECT access_ticket.*, COALESCE( resource.dav_name, collection.dav_name) AS target_dav_name,
937 (access_ticket.expires < current_timestamp) AS expired,
938 dav_principal.dav_name AS principal_dav_name,
939 EXTRACT( \'epoch\' FROM (access_ticket.expires - current_timestamp)) AS seconds,
940 path_privs(access_ticket.dav_owner_id,collection.dav_name,:scan_depth) AS grantor_collection_privileges
941 FROM access_ticket JOIN collection ON (target_collection_id = collection_id)
942 JOIN dav_principal ON (dav_owner_id = principal_id)
943 LEFT JOIN caldav_data resource ON (resource.dav_id = access_ticket.target_resource_id)
944 WHERE target_collection_id = :collection_id ';
945 $params = array(':collection_id' => $this->collection
->collection_id
, ':scan_depth' => $c->permission_scan_depth
);
946 if ( $this->IsCollection() ) {
947 $sql .= 'AND target_resource_id IS NULL';
950 if ( !isset($this->exists
) ) $this->FetchResource();
951 $sql .= 'AND target_resource_id = :dav_id';
952 $params[':dav_id'] = $this->resource->dav_id
;
954 if ( isset($this->exists
) && !$this->exists
) return;
956 $qry = new AwlQuery( $sql, $params );
957 if ( $qry->Exec('DAVResource',__LINE__
,__FILE__
) && $qry->rows() ) {
958 while( $ticket = $qry->Fetch() ) {
959 $this->access_tickets
[] = $ticket;
966 * Returns the array of tickets converted into XMLElements
968 * If the current user does not have DAV::read-acl privilege on this resource they
969 * will only get to see the tickets where they are the owner, or which they supplied
970 * along with the request.
972 * @param &XMLDocument $reply A reference to the XMLDocument used to construct the reply
973 * @return XMLTreeFragment A fragment of an XMLDocument to go in the reply
975 function BuildTicketinfo( &$reply ) {
976 global $session, $request;
978 if ( !isset($this->access_tickets
) ) $this->FetchTickets();
980 $show_all = $this->HavePrivilegeTo('DAV::read-acl');
981 foreach( $this->access_tickets
AS $meh => $trow ) {
982 if ( !$show_all && ( $trow->dav_owner_id
== $session->principal_id ||
$request->ticket
->id() == $trow->ticket_id
) ) continue;
983 dbg_error_log( 'DAVResource', ':BuildTicketinfo: Adding access_ticket "%s" which is "%s".', $trow->ticket_id
, $trow->privileges
);
984 $ticket = new XMLElement( $reply->Tag( 'ticketinfo', 'http://www.xythos.com/namespaces/StorageServer', 'TKT' ) );
985 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:id', $trow->ticket_id
);
986 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:owner', $reply->href( ConstructURL($trow->principal_dav_name
)) );
987 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:timeout', (isset($trow->seconds
) ?
sprintf( 'Seconds-%d', $trow->seconds
) : 'infinity') );
988 $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:visits', 'infinity' );
990 foreach( bits_to_privilege(bindec($trow->privileges
) & bindec($trow->grantor_collection_privileges
) ) AS $k => $v ) {
991 $privs[] = $reply->NewXMLElement($v);
993 $reply->NSElement($ticket, 'DAV::privilege', $privs );
994 $tickets[] = $ticket;
1001 * Checks whether the resource is locked, returning any lock token, or false
1003 * @todo This logic does not catch all locking scenarios. For example an infinite
1004 * depth request should check the permissions for all collections and resources within
1005 * that. At present we only maintain permissions on a per-collection basis though.
1007 function IsLocked( $depth = 0 ) {
1008 if ( !isset($this->_locks_found
) ) {
1009 $this->_locks_found
= array();
1011 * Find the locks that might apply and load them into an array
1013 $sql = 'SELECT * FROM locks WHERE :this_path::text ~ (\'^\'||dav_name||:match_end)::text';
1014 $qry = new AwlQuery($sql, array( ':this_path' => $this->dav_name
, ':match_end' => ($depth == DEPTH_INFINITY ?
'' : '$') ) );
1015 if ( $qry->Exec('DAVResource',__LINE__
,__FILE__
) ) {
1016 while( $lock_row = $qry->Fetch() ) {
1017 $this->_locks_found
[$lock_row->opaquelocktoken
] = $lock_row;
1021 $this->DoResponse(500,i18n("Database Error"));
1026 foreach( $this->_locks_found
AS $lock_token => $lock_row ) {
1027 if ( $lock_row->depth
== DEPTH_INFINITY ||
$lock_row->dav_name
== $this->dav_name
) {
1032 return false; // Nothing matched
1037 * Checks whether this resource is a collection
1039 function IsCollection() {
1040 return $this->_is_collection
;
1045 * Checks whether this resource is a principal
1047 function IsPrincipal() {
1048 return $this->_is_collection
&& $this->_is_principal
;
1053 * Checks whether this resource is a calendar
1055 function IsCalendar() {
1056 return $this->_is_collection
&& $this->_is_calendar
;
1061 * Checks whether this resource is a scheduling inbox/outbox collection
1062 * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any'
1064 function IsSchedulingCollection( $type = 'any' ) {
1065 if ( $this->_is_collection
&& preg_match( '{schedule-(inbox|outbox)}', $this->collection
->type
, $matches ) ) {
1066 return ($type == 'any' ||
$type == $matches[1]);
1073 * Checks whether this resource is IN a scheduling inbox/outbox collection
1074 * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any'
1076 function IsInSchedulingCollection( $type = 'any' ) {
1077 if ( !$this->_is_collection
&& preg_match( '{schedule-(inbox|outbox)}', $this->collection
->type
, $matches ) ) {
1078 return ($type == 'any' ||
$type == $matches[1]);
1085 * Checks whether this resource is an addressbook
1087 function IsAddressbook() {
1088 return $this->_is_collection
&& $this->_is_addressbook
;
1093 * Checks whether this resource is a bind to another resource
1095 function IsBinding() {
1096 return $this->_is_binding
;
1101 * Checks whether this resource is a bind to an external resource
1103 function IsExternal() {
1104 return $this->_is_external
;
1109 * Checks whether this resource actually exists, in the virtual sense, within the hierarchy
1112 if ( ! isset($this->exists
) ) {
1113 if ( $this->IsPrincipal() ) {
1114 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1115 $this->exists
= $this->principal
->Exists();
1117 else if ( ! $this->IsCollection() ) {
1118 if ( !isset($this->resource) ) $this->FetchResource();
1121 // dbg_error_log('DAVResource',' Checking whether "%s" exists. It would appear %s.', $this->dav_name, ($this->exists ? 'so' : 'not') );
1122 return $this->exists
;
1127 * Checks whether the container for this resource actually exists, in the virtual sense, within the hierarchy
1129 function ContainerExists() {
1130 if ( $this->collection
->dav_name
!= $this->dav_name
) {
1131 return $this->collection
->exists
;
1133 $parent = $this->GetParentContainer();
1134 return $parent->Exists();
1139 * Returns the URL of our resource
1142 if ( !isset($this->dav_name
) ) {
1143 throw Exception("What! How can dav_name not be set?");
1145 return ConstructURL($this->dav_name
);
1150 * Returns the dav_name of the resource in our internal namespace
1152 function dav_name() {
1153 if ( isset($this->dav_name
) ) return $this->dav_name
;
1159 * Returns the dav_name of the resource we are bound to, within our internal namespace
1161 function bound_from() {
1162 if ( isset($this->bound_from
) ) return $this->bound_from
;
1163 return $this->dav_name();
1168 * Sets the dav_name of the resource we are bound as
1170 function set_bind_location( $new_dav_name ) {
1171 if ( !isset($this->bound_from
) && isset($this->dav_name
) ) {
1172 $this->bound_from
= $this->dav_name
;
1174 $this->dav_name
= $new_dav_name;
1175 return $this->dav_name
;
1180 * Returns the dav_name of the resource in our internal namespace
1182 function parent_path() {
1183 if ( $this->IsCollection() ) {
1184 if ( !isset($this->collection
) ) $this->FetchCollection();
1185 if ( !isset($this->collection
->parent_container
) ) {
1186 $this->collection
->parent_container
= preg_replace( '{[^/]+/$}', '', $this->bound_from());
1188 return $this->collection
->parent_container
;
1190 return preg_replace( '{[^/]+$}', '', $this->bound_from());
1196 * Returns the principal-URL for this resource
1198 function principal_url() {
1199 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1200 return $this->principal
->url();
1205 * Returns the internal user_no for the principal for this resource
1207 function user_no() {
1208 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1209 return $this->principal
->user_no();
1214 * Returns the internal collection_id for this collection, or the collection containing this resource
1216 function collection_id() {
1217 if ( !isset($this->collection
) ) $this->FetchCollection();
1218 return $this->collection
->collection_id
;
1223 * Returns the database row for this resource
1225 function resource() {
1226 if ( !isset($this->resource) ) $this->FetchResource();
1227 return $this->resource;
1232 * Returns the unique_tag (ETag or getctag) for this resource
1234 function unique_tag() {
1235 if ( isset($this->unique_tag
) ) return $this->unique_tag
;
1236 if ( $this->IsPrincipal() && !isset($this->principal
) ) {
1237 $this->FetchPrincipal();
1238 $this->unique_tag
= $this->principal
->unique_tag();
1240 else if ( !$this->_is_collection
&& !isset($this->resource) ) $this->FetchResource();
1242 if ( $this->exists
!== true ||
!isset($this->unique_tag
) ) $this->unique_tag
= '';
1244 return $this->unique_tag
;
1249 * Returns the definitive resource_id for this resource - usually a dav_id
1251 function resource_id() {
1252 if ( isset($this->resource_id
) ) return $this->resource_id
;
1253 if ( $this->IsPrincipal() && !isset($this->principal
) ) $this->FetchPrincipal();
1254 else if ( !$this->_is_collection
&& !isset($this->resource) ) $this->FetchResource();
1256 if ( $this->exists
!== true ||
!isset($this->resource_id
) ) $this->resource_id
= null;
1258 return $this->resource_id
;
1263 * Returns the current sync_token for this collection, or the containing collection
1265 function sync_token() {
1266 if ( $this->IsPrincipal() ) return null;
1267 if ( $this->collection_id() == 0 ) return null;
1268 if ( !isset($this->sync_token
) ) {
1269 $sql = 'SELECT sync_token FROM sync_tokens WHERE collection_id = :collection_id ORDER BY sync_token DESC LIMIT 1';
1270 $params = array( ':collection_id' => $this->collection_id());
1271 $qry = new AwlQuery($sql, $params );
1272 if ( !$qry->Exec() ||
!$row = $qry->Fetch() ) {
1273 if ( !$qry->QDo('SELECT new_sync_token( 0, :collection_id) AS sync_token', $params) ) throw new Exception('Problem with database query');
1275 $this->sync_token
= 'data:,'.$row->sync_token
;
1277 return $this->sync_token
;
1281 * Checks whether the target collection is publicly_readable
1283 function IsPublic() {
1284 return ( isset($this->collection
->publicly_readable
) && $this->collection
->publicly_readable
== 't' );
1289 * Checks whether the target collection is for public events only
1291 function IsPublicOnly() {
1292 return ( isset($this->collection
->publicly_events_only
) && $this->collection
->publicly_events_only
== 't' );
1297 * Return the type of whatever contains this resource, or would if it existed.
1299 function ContainerType() {
1300 if ( $this->IsPrincipal() ) return 'root';
1301 if ( !$this->IsCollection() ) return $this->collection
->type
;
1303 if ( ! isset($this->collection
->parent_container
) ) return null;
1305 if ( isset($this->parent_container_type
) ) return $this->parent_container_type
;
1307 if ( preg_match('#/[^/]+/#', $this->collection
->parent_container
) ) {
1308 $this->parent_container_type
= 'principal';
1311 $qry = new AwlQuery('SELECT * FROM collection WHERE dav_name = :parent_name',
1312 array( ':parent_name' => $this->collection
->parent_container
) );
1313 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 && $parent = $qry->Fetch() ) {
1314 if ( $parent->is_calendar
== 't' )
1315 $this->parent_container_type
= 'calendar';
1316 else if ( $parent->is_addressbook
== 't' )
1317 $this->parent_container_type
= 'addressbook';
1318 else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name
, $matches ) )
1319 $this->parent_container_type
= 'schedule-'. $matches[3]. 'box';
1321 $this->parent_container_type
= 'collection';
1324 $this->parent_container_type
= null;
1326 return $this->parent_container_type
;
1331 * BuildACE - construct an XMLElement subtree for a DAV::ace
1333 function BuildACE( &$xmldoc, $privs, $principal ) {
1334 $privilege_names = bits_to_privilege($privs, ($this->_is_collection ?
$this->collection
->type
: 'resource'));
1335 $privileges = array();
1336 foreach( $privilege_names AS $k ) {
1337 $privilege = new XMLElement('privilege');
1338 if ( isset($xmldoc) )
1339 $xmldoc->NSElement($privilege,$k);
1341 $privilege->NewElement($k);
1342 $privileges[] = $privilege;
1344 $ace = new XMLElement('ace', array(
1345 new XMLElement('principal', $principal),
1346 new XMLElement('grant', $privileges ) )
1352 * Return ACL settings
1354 function GetACL( &$xmldoc ) {
1355 global $c, $session;
1357 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1358 $default_privs = $this->principal
->default_privileges
;
1359 if ( isset($this->collection
->default_privileges
) ) $default_privs = $this->collection
->default_privileges
;
1362 $acl[] = $this->BuildACE($xmldoc, pow(2,25) - 1, new XMLElement('property', new XMLElement('owner')) );
1364 $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',
1365 array( ':collection_id' => $this->collection
->collection_id
,
1366 ':principal_id' => $this->principal
->principal_id() ) );
1367 if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) {
1368 $by_collection = null;
1369 while( $grant = $qry->Fetch() ) {
1370 if ( !isset($by_collection) ) $by_collection = isset($grant->by_collection
);
1371 if ( $by_collection && !isset($grant->by_collection
) ) break;
1372 $acl[] = $this->BuildACE($xmldoc, $grant->privileges
, $xmldoc->href(ConstructURL($grant->dav_name
)) );
1376 $acl[] = $this->BuildACE($xmldoc, $default_privs, new XMLElement('authenticated') );
1384 * Return general server-related properties, in plain form
1386 function GetProperty( $name ) {
1387 global $c, $session;
1389 // dbg_error_log( 'DAVResource', ':GetProperty: Fetching "%s".', $name );
1393 case 'collection_id':
1394 return $this->collection
->collection_id
;
1397 case 'principal_id':
1398 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1399 return $this->principal
->principal_id();
1402 case 'resourcetype':
1403 if ( isset($this->resourcetypes
) ) {
1404 $this->resourcetypes
= preg_replace('{^\s*<(.*)/>\s*$}', '$1', $this->resourcetypes
);
1405 $type_list = preg_split('{(/>\s*<|\n)}', $this->resourcetypes
);
1406 foreach( $type_list AS $k => $resourcetype ) {
1407 if ( preg_match( '{^([^:]+):([^:]+) \s+ xmlns:([^=]+)="([^"]+)" \s* $}x', $resourcetype, $matches ) ) {
1408 $type_list[$k] = $matches[4] .':' .$matches[2];
1410 else if ( preg_match( '{^([^:]+) \s+ xmlns="([^"]+)" \s* $}x', $resourcetype, $matches ) ) {
1411 $type_list[$k] = $matches[2] .':' .$matches[1];
1418 if ( !isset($this->resource) ) $this->FetchResource();
1419 return clone($this->resource);
1423 if ( !isset($this->resource) ) $this->FetchResource();
1424 return $this->resource->caldav_data
;
1428 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1429 return clone($this->principal
);
1433 if ( isset($this->{$name}) ) {
1434 if ( ! is_object($this->{$name}) ) return $this->{$name};
1435 return clone($this->{$name});
1437 if ( $this->_is_principal
) {
1438 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1439 if ( isset($this->principal
->{$name}) ) return $this->principal
->{$name};
1440 if ( isset($this->collection
->{$name}) ) return $this->collection
->{$name};
1442 else if ( $this->_is_collection
) {
1443 if ( isset($this->collection
->{$name}) ) return $this->collection
->{$name};
1444 if ( isset($this->principal
->{$name}) ) return $this->principal
->{$name};
1447 if ( !isset($this->resource) ) $this->FetchResource();
1448 if ( isset($this->resource->{$name}) ) return $this->resource->{$name};
1449 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1450 if ( isset($this->principal
->{$name}) ) return $this->principal
->{$name};
1451 if ( isset($this->collection
->{$name}) ) return $this->collection
->{$name};
1453 if ( isset($this->{$name}) ) {
1454 if ( ! is_object($this->{$name}) ) return $this->{$name};
1455 return clone($this->{$name});
1457 // dbg_error_log( 'DAVResource', ':GetProperty: Failed to find property "%s" on "%s".', $name, $this->dav_name );
1465 * Return an array which is an expansion of the DAV::allprop
1467 function DAV_AllProperties() {
1468 if ( isset($this->dead_properties
) ) $this->FetchDeadProperties();
1469 $allprop = array_merge( (isset($this->dead_properties
)?
$this->dead_properties
:array()),
1470 (isset($include_properties)?
$include_properties:array()),
1472 'DAV::getcontenttype', 'DAV::resourcetype', 'DAV::getcontentlength', 'DAV::displayname', 'DAV::getlastmodified',
1473 'DAV::creationdate', 'DAV::getetag', 'DAV::getcontentlanguage', 'DAV::supportedlock', 'DAV::lockdiscovery',
1474 'DAV::owner', 'DAV::principal-URL', 'DAV::current-user-principal',
1475 'urn:ietf:params:xml:ns:carddav:max-resource-size', 'urn:ietf:params:xml:ns:carddav:supported-address-data',
1476 'urn:ietf:params:xml:ns:carddav:addressbook-description', 'urn:ietf:params:xml:ns:carddav:addressbook-home-set'
1484 * Return general server-related properties for this URL
1486 function ResourceProperty( $tag, $prop, &$reply, &$denied ) {
1487 global $c, $session, $request;
1489 // dbg_error_log( 'DAVResource', 'Processing "%s" on "%s".', $tag, $this->dav_name );
1491 if ( $reply === null ) $reply = $GLOBALS['reply'];
1494 case 'DAV::allprop':
1495 $property_list = $this->DAV_AllProperties();
1496 $discarded = array();
1497 foreach( $property_list AS $k => $v ) {
1498 $this->ResourceProperty($v, $prop, $reply, $discarded);
1503 $prop->NewElement('href', ConstructURL($this->dav_name
) );
1506 case 'DAV::resource-id':
1507 if ( $this->resource_id
> 0 )
1508 $reply->DAVElement( $prop, 'resource-id', $reply->href(ConstructURL('/.resources/'.$this->resource_id
) ) );
1513 case 'DAV::parent-set':
1515 SELECT b.parent_container FROM dav_binding b JOIN collection c ON (b.bound_source_id=c.collection_id)
1516 WHERE regexp_replace( b.dav_name, '^.*/', c.dav_name ) = :bound_from
1518 $qry = new AwlQuery($sql, array( ':bound_from' => $this->bound_from() ) );
1520 if ( $qry->Exec('DAVResource',__LINE__
,__FILE__
) && $qry->rows() > 0 ) {
1521 while( $row = $qry->Fetch() ) {
1522 $parents[$row->parent_container
] = true;
1525 $parents[preg_replace( '{(?<=/)[^/]+/?$}','',$this->bound_from())] = true;
1526 $parents[preg_replace( '{(?<=/)[^/]+/?$}','',$this->dav_name())] = true;
1528 $parent_set = $reply->DAVElement( $prop, 'parent-set' );
1529 foreach( $parents AS $parent => $v ) {
1530 if ( preg_match( '{^(.*)?/([^/]+)/?$}', $parent, $matches ) ) {
1531 $reply->DAVElement($parent_set, 'parent', array(
1532 new XMLElement( 'href', ConstructURL($matches[1])),
1533 new XMLElement( 'segment', $matches[2])
1536 else if ( $parent == '/' ) {
1537 $reply->DAVElement($parent_set, 'parent', array(
1538 new XMLElement( 'href', '/'),
1539 new XMLElement( 'segment', ( ConstructURL('/') == '/caldav.php/' ?
'caldav.php' : ''))
1545 case 'DAV::getcontenttype':
1546 if ( !isset($this->contenttype
) && !$this->_is_collection
&& !isset($this->resource) ) $this->FetchResource();
1547 $prop->NewElement('getcontenttype', $this->contenttype
);
1550 case 'DAV::resourcetype':
1551 $resourcetypes = $prop->NewElement('resourcetype' );
1552 if ( $this->_is_collection
) {
1553 $type_list = $this->GetProperty('resourcetype');
1554 if ( !is_array($type_list) ) return true;
1555 // dbg_error_log( 'DAVResource', ':ResourceProperty: "%s" are "%s".', $tag, implode(', ',$type_list) );
1556 foreach( $type_list AS $k => $v ) {
1557 if ( $v == '' ) continue;
1558 $reply->NSElement( $resourcetypes, $v );
1560 if ( $this->_is_binding
) {
1561 $reply->NSElement( $resourcetypes, 'http://xmlns.davical.org/davical:webdav-binding' );
1566 case 'DAV::getlastmodified':
1567 /** getlastmodified is HTTP Date format: i.e. the Last-Modified header in response to a GET */
1568 $reply->NSElement($prop, $tag, ISODateToHTTPDate($this->GetProperty('modified')) );
1571 case 'DAV::creationdate':
1572 /** creationdate is ISO8601 format */
1573 $reply->NSElement($prop, $tag, DateToISODate($this->GetProperty('created'), true) );
1576 case 'DAV::getcontentlength':
1577 if ( $this->_is_collection
) return false;
1578 if ( !isset($this->resource) ) $this->FetchResource();
1579 if ( isset($this->resource) ) {
1580 $reply->NSElement($prop, $tag, strlen($this->resource->caldav_data
) );
1584 case 'DAV::getcontentlanguage':
1585 $locale = (isset($c->current_locale
) ?
$c->current_locale
: '');
1586 if ( isset($this->locale
) && $this->locale
!= '' ) $locale = $this->locale
;
1587 $reply->NSElement($prop, $tag, $locale );
1590 case 'DAV::acl-restrictions':
1591 $reply->NSElement($prop, $tag, array( new XMLElement('grant-only'), new XMLElement('no-invert') ) );
1594 case 'DAV::inherited-acl-set':
1595 $inherited_acls = array();
1596 if ( ! $this->_is_collection
) {
1597 $inherited_acls[] = $reply->href(ConstructURL($this->collection
->dav_name
));
1599 $reply->NSElement($prop, $tag, $inherited_acls );
1603 // The principal-URL of the owner
1604 if ( $this->IsExternal() ){
1605 $reply->DAVElement( $prop, 'owner', $reply->href( $this->collection
->bound_from
) );
1608 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1609 $reply->DAVElement( $prop, 'owner', $reply->href( $this->principal
->url() ) );
1613 case 'DAV::add-member':
1614 if ( ! $this->_is_collection
) return false;
1615 $reply->DAVElement( $prop, 'add-member', $reply->href($this->collection
->dav_name
.'?add-member') );
1618 // Empty tag responses.
1620 case 'DAV::alternate-URI-set':
1621 $reply->NSElement($prop, $tag );
1624 case 'DAV::getetag':
1625 if ( $this->_is_collection
) return false;
1626 $reply->NSElement($prop, $tag, $this->unique_tag() );
1629 case 'http://calendarserver.org/ns/:getctag':
1630 if ( ! $this->_is_collection
) return false;
1631 $reply->NSElement($prop, $tag, $this->unique_tag() );
1634 case 'DAV::sync-token':
1635 if ( ! $this->_is_collection ||
$this->_is_principal
) return false;
1636 $reply->NSElement($prop, $tag, $this->sync_token() );
1639 case 'http://calendarserver.org/ns/:calendar-proxy-read-for':
1640 $proxy_type = 'read';
1641 case 'http://calendarserver.org/ns/:calendar-proxy-write-for':
1642 if ( !isset($proxy_type) ) $proxy_type = 'write';
1643 $reply->CalendarserverElement($prop, 'calendar-proxy-'.$proxy_type.'-for', $reply->href( $this->principal
->ProxyFor($proxy_type) ) );
1646 case 'DAV::current-user-privilege-set':
1647 if ( $this->HavePrivilegeTo('DAV::read-current-user-privilege-set') ) {
1648 $reply->NSElement($prop, $tag, $this->BuildPrivileges() );
1655 case 'urn:ietf:params:xml:ns:caldav:supported-calendar-data':
1656 if ( ! $this->IsCalendar() && ! $this->IsSchedulingCollection() ) return false;
1657 $reply->NSElement($prop, $tag, 'text/calendar' );
1660 case 'urn:ietf:params:xml:ns:caldav:supported-calendar-component-set':
1661 if ( ! $this->_is_collection
) return false;
1662 if ( $this->IsCalendar() ) {
1663 if ( !isset($this->dead_properties
) ) $this->FetchDeadProperties();
1664 if ( isset($this->dead_properties
[$tag]) ) {
1665 $set_of_components = explode('"', $this->dead_properties
[$tag]);
1666 foreach( $set_of_components AS $k => $v ) {
1667 if ( !preg_match('{(VEVENT|VTODO|VJOURNAL|VTIMEZONE|VFREEBUSY|VPOLL|VAVAILABILITY)}', $v) ) {
1668 unset( $set_of_components[$k] );
1673 $set_of_components = array( 'VEVENT', 'VTODO', 'VJOURNAL', 'VTIMEZONE', 'VFREEBUSY', 'VPOLL', 'VAVAILABILITY' );
1676 else if ( $this->IsSchedulingCollection() )
1677 $set_of_components = array( 'VEVENT', 'VTODO', 'VFREEBUSY' );
1679 $components = array();
1680 foreach( $set_of_components AS $v ) {
1681 $components[] = $reply->NewXMLElement( 'comp', '', array('name' => $v), 'urn:ietf:params:xml:ns:caldav');
1683 $reply->CalDAVElement($prop, 'supported-calendar-component-set', $components );
1686 case 'DAV::supported-method-set':
1687 $prop->NewElement('supported-method-set', $this->BuildSupportedMethods() );
1690 case 'DAV::supported-report-set':
1691 $prop->NewElement('supported-report-set', $this->BuildSupportedReports( $reply ) );
1694 case 'DAV::supportedlock':
1695 $prop->NewElement('supportedlock',
1696 new XMLElement( 'lockentry',
1698 new XMLElement('lockscope', new XMLElement('exclusive')),
1699 new XMLElement('locktype', new XMLElement('write')),
1705 case 'DAV::supported-privilege-set':
1706 $prop->NewElement('supported-privilege-set', $request->BuildSupportedPrivileges($reply) );
1709 case 'DAV::principal-collection-set':
1710 $prop->NewElement( 'principal-collection-set', $reply->href( ConstructURL('/') ) );
1713 case 'DAV::current-user-principal':
1714 $prop->NewElement('current-user-principal', $reply->href( $request->principal
->url() ) );
1717 case 'SOME-DENIED-PROPERTY': /** indicating the style for future expansion */
1718 $denied[] = $reply->Tag($tag);
1721 case 'urn:ietf:params:xml:ns:caldav:calendar-timezone':
1722 if ( ! $this->_is_collection
) return false;
1723 if ( !isset($this->collection
->vtimezone
) ||
$this->collection
->vtimezone
== '' ) return false;
1725 $cal = new iCalComponent();
1727 $cal->AddComponent( new iCalComponent($this->collection
->vtimezone
) );
1728 $reply->NSElement($prop, $tag, $cal->Render() );
1731 case 'urn:ietf:params:xml:ns:carddav:address-data':
1732 case 'urn:ietf:params:xml:ns:caldav:calendar-data':
1733 if ( $this->_is_collection
) return false;
1734 if ( !isset($c->sync_resource_data_ok
) ||
$c->sync_resource_data_ok
== false ) return false;
1735 if ( !isset($this->resource) ) $this->FetchResource();
1736 $reply->NSElement($prop, $tag, $this->resource->caldav_data
);
1739 case 'urn:ietf:params:xml:ns:carddav:max-resource-size':
1740 if ( ! $this->_is_collection ||
!$this->_is_addressbook
) return false;
1741 $reply->NSElement($prop, $tag, 65500 );
1744 case 'urn:ietf:params:xml:ns:carddav:supported-address-data':
1745 if ( ! $this->_is_collection ||
!$this->_is_addressbook
) return false;
1746 $address_data = $reply->NewXMLElement( 'address-data', false,
1747 array( 'content-type' => 'text/vcard', 'version' => '3.0'), 'urn:ietf:params:xml:ns:carddav');
1748 $reply->NSElement($prop, $tag, $address_data );
1752 if ( $this->HavePrivilegeTo('DAV::read-acl') ) {
1753 $reply->NSElement($prop, $tag, $this->GetACL( $reply ) );
1760 case 'http://www.xythos.com/namespaces/StorageServer:ticketdiscovery':
1761 case 'DAV::ticketdiscovery':
1762 $reply->NSElement($prop,'http://www.xythos.com/namespaces/StorageServer:ticketdiscovery', $this->BuildTicketinfo($reply) );
1766 $property_value = $this->GetProperty(preg_replace('{^.*:}', '', $tag));
1767 if ( isset($property_value) ) {
1768 $reply->NSElement($prop, $tag, $property_value );
1771 if ( !isset($this->dead_properties
) ) $this->FetchDeadProperties();
1772 if ( isset($this->dead_properties
[$tag]) ) {
1773 $reply->NSElement($prop, $tag, $this->dead_properties
[$tag] );
1776 // dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of path "%s".', $tag, $this->dav_name );
1787 * Construct XML propstat fragment for this resource
1789 * @param array of string $properties The requested properties for this resource
1791 * @return string An XML fragment with the requested properties for this resource
1793 function GetPropStat( $properties, &$reply, $props_only = false ) {
1796 dbg_error_log('DAVResource',':GetPropStat: propstat for href "%s"', $this->dav_name
);
1798 $prop = new XMLElement('prop');
1800 $not_found = array();
1801 foreach( $properties AS $k => $tag ) {
1802 if ( is_object($tag) ) {
1803 dbg_error_log( 'DAVResource', ':GetPropStat: "$properties" should be an array of text. Assuming this object is an XMLElement!.' );
1804 $tag = $tag->GetTag();
1806 $found = $this->ResourceProperty($tag, $prop, $reply, $denied );
1808 if ( !isset($this->principal
) ) $this->FetchPrincipal();
1809 $found = $this->principal
->PrincipalProperty( $tag, $prop, $reply, $denied );
1812 // dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of resource "%s".', $tag, $this->dav_name );
1813 $not_found[] = $reply->Tag($tag);
1816 if ( $props_only ) return $prop;
1818 $status = new XMLElement('status', 'HTTP/1.1 200 OK' );
1820 $elements = array( new XMLElement( 'propstat', array($prop,$status) ) );
1822 if ( count($denied) > 0 ) {
1823 $status = new XMLElement('status', 'HTTP/1.1 403 Forbidden' );
1824 $noprop = new XMLElement('prop');
1825 foreach( $denied AS $k => $v ) {
1826 $reply->NSElement($noprop, $v);
1828 $elements[] = new XMLElement( 'propstat', array( $noprop, $status) );
1831 if ( !(isset($request->brief_response
) && $request->brief_response
) && count($not_found) > 0 ) {
1832 $status = new XMLElement('status', 'HTTP/1.1 404 Not Found' );
1833 $noprop = new XMLElement('prop');
1834 foreach( $not_found AS $k => $v ) {
1835 $noprop->NewElement($v);
1837 $elements[] = new XMLElement( 'propstat', array( $noprop, $status) );
1844 * Render XML for this resource
1846 * @param array $properties The requested properties for this principal
1847 * @param reference $reply A reference to the XMLDocument being used for the reply
1849 * @return string An XML fragment with the requested properties for this principal
1851 function RenderAsXML( $properties, &$reply, $bound_parent_path = null ) {
1852 global $session, $c, $request;
1854 dbg_error_log('DAVResource',':RenderAsXML: Resource "%s" exists(%d)', $this->dav_name
, $this->Exists() );
1856 if ( !$this->Exists() ) return null;
1858 $elements = $this->GetPropStat( $properties, $reply );
1859 if ( isset($bound_parent_path) ) {
1860 $dav_name = str_replace( $this->parent_path(), $bound_parent_path, $this->dav_name
);
1863 $dav_name = $this->dav_name
;
1866 array_unshift( $elements, $reply->href(ConstructURL($dav_name)));
1868 $response = new XMLElement( 'response', $elements );