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