source: contrib/davical/inc/CalDAVRequest.php @ 3733

Revision 3733, 51.7 KB checked in by gabriel.malheiros, 13 years ago (diff)

Ticket #1541 - <Davical customizado para o Expresso.Utiliza Caldav e CardDav?>

Line 
1<?php
2/**
3* Functions that are needed for all CalDAV Requests
4*
5*  - Ascertaining the paths
6*  - Ascertaining the current user's permission to those paths.
7*  - Utility functions which we can use to decide whether this
8*    is a permitted activity for this user.
9*
10* @package   davical
11* @subpackage   Request
12* @author    Andrew McMillan <andrew@mcmillan.net.nz>
13* @copyright Catalyst .Net Ltd, Morphoss Ltd
14* @license   http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
15*/
16
17require_once("XMLDocument.php");
18require_once("CalDAVPrincipal.php");
19include("DAVTicket.php");
20include_once("drivers_ldap.php");
21
22define('DEPTH_INFINITY', 9999);
23
24/**
25* A class for collecting things to do with this request.
26*
27* @package   davical
28*/
29class CalDAVRequest
30{
31  var $options;
32
33  /**
34  * The raw data sent along with the request
35  */
36  var $raw_post;
37
38  /**
39  * The HTTP request method: PROPFIND, LOCK, REPORT, OPTIONS, etc...
40  */
41  var $method;
42
43  /**
44  * The depth parameter from the request headers, coerced into a valid integer: 0, 1
45  * or DEPTH_INFINITY which is defined above.  The default is set per various RFCs.
46  */
47  var $depth;
48
49  /**
50  * The 'principal' (user/resource/...) which this request seeks to access
51  * @var CalDAVPrincipal
52  */
53  var $principal;
54
55  /**
56  * The 'current_user_principal_xml' the DAV:current-user-principal answer. An
57  * XMLElement object with an <href> or <unauthenticated> fragment.
58  */
59  var $current_user_principal_xml;
60
61  /**
62  * The user agent making the request.
63  */
64  var $user_agent;
65
66  /**
67  * The ID of the collection containing this path, or of this path if it is a collection
68  */
69  var $collection_id;
70
71  /**
72  * The path corresponding to the collection_id
73  */
74  var $collection_path;
75
76  /**
77  * The type of collection being requested:
78  *  calendar, schedule-inbox, schedule-outbox
79  */
80  var $collection_type;
81
82  /**
83  * The type of collection being requested:
84  *  calendar, schedule-inbox, schedule-outbox
85  */
86  protected $exists;
87
88  /**
89  * The decimal privileges allowed by this user to the identified resource.
90  */
91  protected $privileges;
92
93  /**
94  * A static structure of supported privileges.
95  */
96  var $supported_privileges;
97
98  /**
99  * A DAVTicket object, if there is a ?ticket=id or Ticket: id with this request
100  */
101  public $ticket;
102
103  /**
104  * Create a new CalDAVRequest object.
105  */
106  function __construct( $options = array() ) {
107    global $session, $c, $debugging;
108
109    $this->supported_privileges = array(
110      'all' => array(
111        'read' => translate('Read the content of a resource or collection'),
112        'write' => array(
113          'bind' => translate('Create a resource or collection'),
114          'unbind' => translate('Delete a resource or collection'),
115          'write-content' => translate('Write content'),
116          'write-properties' => translate('Write properties')
117        ),
118        'urn:ietf:params:xml:ns:caldav:read-free-busy' => translate('Read the free/busy information for a calendar collection'),
119        'read-acl' => translate('Read ACLs for a resource or collection'),
120        'read-current-user-privilege-set' => translate('Read the details of the current user\'s access control to this resource.'),
121        'write-acl' => translate('Write ACLs for a resource or collection'),
122        'unlock' => translate('Remove a lock'),
123
124        'urn:ietf:params:xml:ns:caldav:schedule-deliver' => array(
125          'urn:ietf:params:xml:ns:caldav:schedule-deliver-invite'=> translate('Deliver scheduling invitations from an organiser to this scheduling inbox'),
126          'urn:ietf:params:xml:ns:caldav:schedule-deliver-reply' => translate('Deliver scheduling replies from an attendee to this scheduling inbox'),
127          'urn:ietf:params:xml:ns:caldav:schedule-query-freebusy' => translate('Allow free/busy enquiries targeted at the owner of this scheduling inbox')
128        ),
129
130        'urn:ietf:params:xml:ns:caldav:schedule-send' => array(
131          'urn:ietf:params:xml:ns:caldav:schedule-send-invite' => translate('Send scheduling invitations as an organiser from the owner of this scheduling outbox.'),
132          'urn:ietf:params:xml:ns:caldav:schedule-send-reply' => translate('Send scheduling replies as an attendee from the owner of this scheduling outbox.'),
133          'urn:ietf:params:xml:ns:caldav:schedule-send-freebusy' => translate('Send free/busy enquiries')
134        )
135      )
136    );
137
138    $this->options = $options;
139    if ( !isset($this->options['allow_by_email']) ) $this->options['allow_by_email'] = false;
140    $this->principal = (object) array( 'username' => $session->username, 'user_no' => $session->user_no );
141
142    $this->raw_post = file_get_contents ( 'php://input');
143
144    if ( (isset($c->dbg['ALL']) && $c->dbg['ALL']) || (isset($c->dbg['request']) && $c->dbg['request']) ) {
145      /** Log the request headers */
146      $lines = apache_request_headers();
147      dbg_error_log( "LOG ", "***************** Request Header ****************" );
148      dbg_error_log( "LOG ", "%s %s", $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'] );
149      foreach( $lines AS $k => $v ) {
150        dbg_error_log( "LOG headers", "-->%s: %s", $k, $v );
151      }
152      dbg_error_log( "LOG ", "******************** Request ********************" );
153      // Log the request in all it's gory detail.
154      $lines = preg_split( '#[\r\n]+#', $this->raw_post);
155      foreach( $lines AS $v ) {
156        dbg_error_log( "LOG request", "-->%s", $v );
157      }
158    }
159
160    if ( isset($debugging) && isset($_GET['method']) ) {
161      $_SERVER['REQUEST_METHOD'] = $_GET['method'];
162    }
163    else if ( $_SERVER['REQUEST_METHOD'] == 'POST' && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) ){
164      $_SERVER['REQUEST_METHOD'] = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
165    }
166    $this->method = $_SERVER['REQUEST_METHOD'];
167    if ( isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > 7 ) {
168      $this->content_type = (isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null);
169      if ( preg_match( '{^(\S+/\S+)\s*(;.*)?$}', $this->content_type, $matches ) ) {
170        $this->content_type = $matches[1];
171      }
172      if ( $this->method == 'PROPFIND' || $this->method == 'REPORT' ) {
173        if ( !preg_match( '{^(text|application)/xml$}', $this->content_type ) ) {
174          dbg_error_log( "LOG request", 'Request is "%s" but client set content-type to "%s". Assuming they meant XML!',
175                                                 $request->method, $this->content_type );
176          $this->content_type = 'text/xml';
177        }
178      }
179    }
180    $this->user_agent = ((isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : "Probably Mulberry"));
181
182    /**
183    * A variety of requests may set the "Depth" header to control recursion
184    */
185    if ( isset($_SERVER['HTTP_DEPTH']) ) {
186      $this->depth = $_SERVER['HTTP_DEPTH'];
187    }
188    else {
189      /**
190      * Per rfc2518, section 9.2, 'Depth' might not always be present, and if it
191      * is not present then a reasonable request-type-dependent default should be
192      * chosen.
193      */
194      switch( $this->method ) {
195        case 'PROPFIND':
196        case 'DELETE':
197        case 'MOVE':
198        case 'COPY':
199        case 'LOCK':
200          $this->depth = 'infinity';
201          break;
202
203        case 'REPORT':
204        default:
205          $this->depth = 0;
206      }
207    }
208    if ( $this->depth == 'infinity' ) $this->depth = DEPTH_INFINITY;
209    $this->depth = intval($this->depth);
210
211    /**
212    * MOVE/COPY use a "Destination" header and (optionally) an "Overwrite" one.
213    */
214    if ( isset($_SERVER['HTTP_DESTINATION']) ) $this->destination = $_SERVER['HTTP_DESTINATION'];
215    $this->overwrite = ( isset($_SERVER['HTTP_OVERWRITE']) && ($_SERVER['HTTP_OVERWRITE'] == 'F') ? false : true ); // RFC4918, 9.8.4 says default True.
216
217    /**
218    * LOCK things use an "If" header to hold the lock in some cases, and "Lock-token" in others
219    */
220    if ( isset($_SERVER['HTTP_IF']) ) $this->if_clause = $_SERVER['HTTP_IF'];
221    if ( isset($_SERVER['HTTP_LOCK_TOKEN']) && preg_match( '#[<]opaquelocktoken:(.*)[>]#', $_SERVER['HTTP_LOCK_TOKEN'], $matches ) ) {
222      $this->lock_token = $matches[1];
223    }
224
225    /**
226    * Check for an access ticket.
227    */
228    if ( isset($_GET['ticket']) ) {
229      $this->ticket = new DAVTicket($_GET['ticket']);
230    }
231    else if ( isset($_SERVER['HTTP_TICKET']) ) {
232      $this->ticket = new DAVTicket($_SERVER['HTTP_TICKET']);
233    }
234
235    /**
236    * LOCK things use a "Timeout" header to set a series of reducing alternative values
237    */
238    if ( isset($_SERVER['HTTP_TIMEOUT']) ) {
239      $timeouts = explode( ',', $_SERVER['HTTP_TIMEOUT'] );
240      foreach( $timeouts AS $k => $v ) {
241        if ( strtolower($v) == 'infinite' ) {
242          $this->timeout = (isset($c->maximum_lock_timeout) ? $c->maximum_lock_timeout : 86400 * 100);
243          break;
244        }
245        elseif ( strtolower(substr($v,0,7)) == 'second-' ) {
246          $this->timeout = min( intval(substr($v,7)), (isset($c->maximum_lock_timeout) ? $c->maximum_lock_timeout : 86400 * 100) );
247          break;
248        }
249      }
250      if ( ! isset($this->timeout) || $this->timeout == 0 ) $this->timeout = (isset($c->default_lock_timeout) ? $c->default_lock_timeout : 900);
251    }
252
253    /**
254    * Our path is /<script name>/<user name>/<user controlled> if it ends in
255    * a trailing '/' then it is referring to a DAV 'collection' but otherwise
256    * it is referring to a DAV data item.
257    *
258    * Permissions are controlled as follows:
259    *  1. if there is no <user name> component, the request has read privileges
260    *  2. if the requester is an admin, the request has read/write priviliges
261    *  3. if there is a <user name> component which matches the logged on user
262    *     then the request has read/write privileges
263    *  4. otherwise we query the defined relationships between users and use
264    *     the minimum privileges returned from that analysis.
265    */
266    $this->path = (isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : "/");
267    $this->path = rawurldecode($this->path);
268
269    /** Allow a request for .../calendar.ics to translate into the calendar URL */
270    if ( preg_match( '#^(/[^/]+/[^/]+).ics$#', $this->path, $matches ) ) {
271      $this->path = $matches[1]. '/';
272    }
273
274    // dbg_error_log( "caldav", "Sanitising path '%s'", $this->path );
275    $bad_chars_regex = '/[\\^\\[\\(\\\\]/';
276    if ( preg_match( $bad_chars_regex, $this->path ) ) {
277      $this->DoResponse( 400, translate("The calendar path contains illegal characters.") );
278    }
279    if ( strstr($this->path,'//') ) $this->path = preg_replace( '#//+#', '/', $this->path);
280
281    $this->user_no = $session->user_no;
282    $this->username = $session->username;
283    if ( $session->user_no > 0 ) {
284      $this->current_user_principal_url = new XMLElement('href', ConstructURL('/'.$session->username.'/') );
285    }
286    else {
287      $this->current_user_principal_url = new XMLElement('unauthenticated' );
288    }
289
290    /**
291    * RFC2518, 5.2: URL pointing to a collection SHOULD end in '/', and if it does not then
292    * we SHOULD return a Content-location header with the correction...
293    *
294    * We therefore look for a collection which matches one of the following URLs:
295    *  - The exact request.
296    *  - If the exact request, doesn't end in '/', then the request URL with a '/' appended
297    *  - The request URL truncated to the last '/'
298    * The collection URL for this request is therefore the longest row in the result, so we
299    * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1"
300    */
301    $sql = "SELECT * FROM collection WHERE dav_name = :exact_name";
302    $params = array( ':exact_name' => $this->path );
303    if ( !preg_match( '#/$#', $this->path ) ) {
304      $sql .= " OR dav_name = :truncated_name OR dav_name = :trailing_slash_name";
305      $params[':truncated_name'] = preg_replace( '#[^/]*$#', '', $this->path);
306      $params[':trailing_slash_name'] = $this->path."/";
307    }
308    $sql .= " ORDER BY LENGTH(dav_name) DESC LIMIT 1";
309    $qry = new AwlQuery( $sql, $params );
310    if ( $qry->Exec('caldav',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
311      if ( $row->dav_name == $this->path."/" ) {
312        $this->path = $row->dav_name;
313        dbg_error_log( "caldav", "Path is actually a collection - sending Content-Location header." );
314        header( "Content-Location: ".ConstructURL($this->path) );
315      }
316
317      $this->collection_id = $row->collection_id;
318      $this->collection_path = $row->dav_name;
319      $this->collection_type = ($row->is_calendar == 't' ? 'calendar' : 'collection');
320      $this->collection_type = ($row->is_addressbook == 't' ? 'addressbook' : 'collection');
321      $this->collection = $row;
322      if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->path, $matches ) ) {
323        $this->collection_type = 'schedule-'. $matches[3]. 'box';
324      }
325      $this->collection->type = $this->collection_type;
326    }
327    else if ( preg_match( '{^( ( / ([^/]+) / ) \.(in|out)/ ) [^/]*$}x', $this->path, $matches ) ) {
328      // The request is for a scheduling inbox or outbox (or something inside one) and we should auto-create it
329      $params = array( ':username' => $matches[3], ':parent_container' => $matches[2], ':dav_name' => $matches[1] );
330      $params[':boxname'] = ($matches[4] == 'in' ? ' Inbox' : ' Outbox');
331      $this->collection_type = 'schedule-'. $matches[4]. 'box';
332      $params[':resourcetypes'] = sprintf('<DAV::collection/><urn:ietf:params:xml:ns:caldav:%s/>', $this->collection_type );
333      $sql = <<<EOSQL
334INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified, dav_etag, resourcetypes )
335    VALUES( (SELECT user_no FROM usr WHERE username = :username),
336            :parent_container, :dav_name,
337            (SELECT fullname FROM usr WHERE username = :username) || :boxname,
338             FALSE, current_timestamp, current_timestamp, '1', :resourcetypes )
339EOSQL;
340
341      $qry = new AwlQuery( $sql, $params );
342      $qry->Exec('caldav',__LINE__,__FILE__);
343      dbg_error_log( 'caldav', 'Created new collection as "%s".', trim($params[':boxname']) );
344
345      $qry = new AwlQuery( "SELECT * FROM collection WHERE dav_name = :dav_name", array( ':dav_name' => $matches[1] ) );
346      if ( $qry->Exec('caldav',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
347        $this->collection_id = $row->collection_id;
348        $this->collection_path = $matches[1];
349        $this->collection = $row;
350        $this->collection->type = $this->collection_type;
351      }
352    }
353    else if ( preg_match( '{^( ( / ([^/]+) / ) addressbook/ ) [^/]*$}x', $this->path, $matches )  ) {
354      // The request is for a scheduling adressbook and we should auto-create it
355     
356      $dav_etag = md5("/".$matches[3]."/addressbook/");
357      $params = array( ':username' => $matches[3], ':parent_container' => "/".$matches[3]."/", ':dav_name' => "/".$matches[3]."/addressbook/", ':etag' =>  $dav_etag );
358      $params[':boxname'] = $matches[3];
359      $this->collection_type = 'addressbook';
360      $params[':resourcetypes'] = sprintf('<DAV::collection/><urn:ietf:params:xml:ns:carddav:%s/>',$this->collection_type);
361      $sql = "INSERT INTO collection ( user_no, parent_container, dav_name, dav_etag, dav_displayname, is_calendar, created, modified, is_addressbook, resourcetypes ) VALUES ( (SELECT user_no FROM usr WHERE username = :username),:parent_container, :dav_name, :etag, (SELECT fullname FROM usr WHERE username = :username) || :boxname, FALSE, current_timestamp, current_timestamp, TRUE , :resourcetypes )";
362      $qry = new AwlQuery( $sql, $params );
363      $qry->Exec('carddav',__LINE__,__FILE__);
364      dbg_error_log( 'carddav', 'Created new collection as "%s".', trim($params[':boxname']) );
365
366      $qry = new AwlQuery( "SELECT * FROM collection WHERE dav_name = :dav_name", array( ':dav_name' => $matches[3].$matches[4] ) );
367      if ( $qry->Exec('caldav',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
368        $this->collection_id = $row->collection_id;
369        $this->collection_path = $row->dav_name;
370        $this->collection = $row;
371        $this->collection->type = $this->collection_type;
372      }
373    }
374 
375    else if ( preg_match( '#^((/[^/]+/)calendar-proxy-(read|write))/?[^/]*$#', $this->path, $matches ) ) {
376      $this->collection_type = 'proxy';
377      $this->_is_proxy_request = true;
378      $this->proxy_type = $matches[3];
379      $this->collection_path = $matches[1].'/';  // Enforce trailling '/'
380      if ( $this->collection_path == $this->path."/" ) {
381        $this->path .= '/';
382        dbg_error_log( "caldav", "Path is actually a (proxy) collection - sending Content-Location header." );
383        header( "Content-Location: ".ConstructURL($this->path) );
384      }
385    }
386    else if ( $this->options['allow_by_email'] && preg_match( '#^/(\S+@\S+[.]\S+)/?$#', $this->path) ) {
387      /** @TODO: we should deprecate this now that Evolution 2.27 can do scheduling extensions */
388      $this->collection_id = -1;
389      $this->collection_type = 'email';
390      $this->collection_path = $this->path;
391      $this->_is_principal = true;
392    }
393    else if ( preg_match( '#^(/[^/]+)/?$#', $this->path, $matches) || preg_match( '#^(/principals/[^/]+/[^/]+)/?$#', $this->path, $matches) ) {
394      $this->collection_id = -1;
395      $this->collection_path = $matches[1].'/';  // Enforce trailling '/'
396      $this->collection_type = 'principal';
397      $this->_is_principal = true;
398      if ( $this->collection_path == $this->path."/" ) {
399        $this->path .= '/';
400        dbg_error_log( "caldav", "Path is actually a collection - sending Content-Location header." );
401        header( "Content-Location: ".ConstructURL($this->path) );
402      }
403      if ( preg_match( '#^(/principals/[^/]+/[^/]+)/?$#', $this->path, $matches) ) {
404        // Force a depth of 0 on these, which are at the wrong URL.
405        $this->depth = 0;
406      }
407    }
408    else if ( $this->path == '/' ) {
409      $this->collection_id = -1;
410      $this->collection_path = '/';
411      $this->collection_type = 'root';
412    }
413
414    if ( $this->collection_path == $this->path ) $this->_is_collection = true;
415    dbg_error_log( "caldav", " Collection '%s' is %d, type %s", $this->collection_path, $this->collection_id, $this->collection_type );
416
417    /**
418    * Extract the user whom we are accessing
419    */
420    $this->principal = new CalDAVPrincipal( array( "path" => $this->path, "options" => $this->options ) );
421    if ( isset($this->principal->user_no) ) $this->user_no  = $this->principal->user_no;
422    if ( isset($this->principal->username)) $this->username = $this->principal->username;
423    if ( isset($this->principal->by_email) && $this->principal->by_email) $this->by_email = true;
424    if ( isset($this->principal->principal_id)) $this->principal_id = $this->principal->principal_id;
425
426    if ( $this->collection_type == 'principal' || $this->collection_type == 'email' || $this->collection_type == 'proxy' ) {
427      $this->collection = $this->principal->AsCollection();
428      if( $this->collection_type == 'proxy' ) {
429        $this->collection = $this->principal->AsCollection();
430        $this->collection->is_proxy = 't';
431        $this->collection->type = 'proxy';
432        $this->collection->proxy_type = $this->proxy_type;
433        $this->collection->dav_displayname = sprintf('Proxy %s for %s', $this->proxy_type, $this->principal->username() );
434      }
435    }
436    elseif( $this->collection_type == 'root' ) {
437      $this->collection = (object) array(
438                            'collection_id' => 0,
439                            'dav_name' => '/',
440                            'dav_etag' => md5($c->system_name),
441                            'is_calendar' => 'f',
442                            'is_addressbook' => 'f',
443                            'is_principal' => 'f',
444                            'user_no' => 0,
445                            'dav_displayname' => $c->system_name,
446                            'type' => 'root',
447                            'created' => date('Ymd\THis')
448                          );
449    }
450
451    /**
452    * Evaluate our permissions for accessing the target
453    */
454    $this->setPermissions();
455
456    $this->supported_methods = array(
457      'OPTIONS' => '',
458      'PROPFIND' => '',
459      'REPORT' => '',
460      'DELETE' => '',
461      'LOCK' => '',
462      'UNLOCK' => '',
463      'MOVE' => '',
464      'ACL' => ''
465    );
466    if ( $this->IsCollection() ) {
467      switch ( $this->collection_type ) {
468        case 'root':
469        case 'email':
470          // We just override the list completely here.
471          $this->supported_methods = array(
472            'OPTIONS' => '',
473            'PROPFIND' => '',
474            'REPORT' => ''
475          );
476          break;
477        case 'schedule-inbox':
478        case 'schedule-outbox':
479          $this->supported_methods = array_merge(
480            $this->supported_methods,
481            array(
482              'POST' => '', 'GET' => '', 'PUT' => '', 'HEAD' => '', 'PROPPATCH' => ''
483            )
484          );
485          break;
486        case 'calendar':
487          $this->supported_methods['GET'] = '';
488          $this->supported_methods['PUT'] = '';
489          $this->supported_methods['HEAD'] = '';
490          break;
491        case 'collection':
492        case 'principal':
493          $this->supported_methods['GET'] = '';
494          $this->supported_methods['PUT'] = '';
495          $this->supported_methods['HEAD'] = '';
496          $this->supported_methods['MKCOL'] = '';
497          $this->supported_methods['MKCALENDAR'] = '';
498          $this->supported_methods['PROPPATCH'] = '';
499          $this->supported_methods['BIND'] = '';
500          break;
501      }
502    }
503    else {
504      $this->supported_methods = array_merge(
505        $this->supported_methods,
506        array(
507          'GET' => '',
508          'HEAD' => '',
509          'PUT' => ''
510        )
511      );
512    }
513    /**** comentado por motivos de incmpatibilidade do sync-collection
514    $this->supported_reports = array(
515      'DAV::principal-property-search' => '',
516      'DAV::expand-property' => '',
517      'DAV::sync-collection' => ''
518    );
519    */ 
520    $this->supported_reports = array(
521      'DAV::principal-property-search' => '',
522      'DAV::expand-property' => ''
523    );
524    if ( isset($this->collection) && $this->collection->is_calendar ) {
525      $this->supported_reports = array_merge(
526        $this->supported_reports,
527        array(
528          'urn:ietf:params:xml:ns:caldav:calendar-query' => '',
529          'urn:ietf:params:xml:ns:caldav:calendar-multiget' => '',
530          'urn:ietf:params:xml:ns:caldav:free-busy-query' => ''
531        )
532      );
533    }
534    if ( isset($this->collection) && $this->collection->is_addressbook ) {
535      $this->supported_reports = array_merge(
536        $this->supported_reports,
537        array(
538          'urn:ietf:params:xml:ns:carddav:addressbook-query' => '',
539          'urn:ietf:params:xml:ns:carddav:addressbook-multiget' => ''
540        )
541      );
542    }
543
544
545    /**
546    * If the content we are receiving is XML then we parse it here.  RFC2518 says we
547    * should reasonably expect to see either text/xml or application/xml
548    */
549    if ( isset($this->content_type) && preg_match( '#(application|text)/xml#', $this->content_type ) ) {
550      $xml_parser = xml_parser_create_ns('UTF-8');
551      $this->xml_tags = array();
552      xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
553      xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
554      $rc = xml_parse_into_struct( $xml_parser, $this->raw_post, $this->xml_tags );
555      if ( $rc == false ) {
556        dbg_error_log( 'ERROR', 'XML parsing error: %s at line %d, column %d',
557                    xml_error_string(xml_get_error_code($xml_parser)),
558                    xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
559        $this->XMLResponse( 400, new XMLElement( 'error', new XMLElement('invalid-xml'), array( 'xmlns' => 'DAV:') ) );
560      }
561      xml_parser_free($xml_parser);
562      if ( count($this->xml_tags) ) {
563        dbg_error_log( "caldav", " Parsed incoming XML request body." );
564      }
565      else {
566        $this->xml_tags = null;
567        dbg_error_log( "ERROR", "Incoming request sent content-type XML with no XML request body." );
568      }
569    }
570
571    /**
572    * Look out for If-None-Match or If-Match headers
573    */
574    if ( isset($_SERVER["HTTP_IF_NONE_MATCH"]) ) {
575      $this->etag_none_match = str_replace('"','',$_SERVER["HTTP_IF_NONE_MATCH"]);
576      if ( $this->etag_none_match == '' ) unset($this->etag_none_match);
577    }
578    if ( isset($_SERVER["HTTP_IF_MATCH"]) ) {
579      $this->etag_if_match = str_replace('"','',$_SERVER["HTTP_IF_MATCH"]);
580      if ( $this->etag_if_match == '' ) unset($this->etag_if_match);
581    }
582   dbg_error_log( "caldav", " FIM do contntrutor." );
583  }
584
585
586  /**
587  * Work out the user whose calendar we are accessing, based on elements of the path.
588  */
589  function UserFromPath() {
590    global $session;
591
592    $this->user_no = $session->user_no;
593    $this->username = $session->username;
594    $this->principal_id = $session->principal_id;
595
596    @dbg_error_log( "WARN", "Call to deprecated CalDAVRequest::UserFromPath()" );
597
598    if ( $this->path == '/' || $this->path == '' ) {
599      dbg_error_log( "caldav", "No useful path split possible" );
600      return false;
601    }
602
603    $path_split = explode('/', $this->path );
604    $this->username = $path_split[1];
605    if ( $this->username == 'principals' ) $this->username = $path_split[3];
606    @dbg_error_log( "caldav", "Path split into at least /// %s /// %s /// %s", $path_split[1], $path_split[2], $path_split[3] );
607    if ( isset($this->options['allow_by_email']) && preg_match( '#/(\S+@\S+[.]\S+)/?$#', $this->path, $matches) ) {
608      $this->by_email = $matches[1];
609      $qry = new AwlQuery("SELECT user_no, principal_id, username FROM usr JOIN principal USING (user_no) WHERE email = :email",
610                          array(':email' => $this->by_email ) );
611      if ( $qry->Exec('caldav',__LINE__,__FILE__) && $user = $qry->Fetch() ) {
612        $this->user_no = $user->user_no;
613        $this->username = $user->username;
614        $this->principal_id = $user->principal_id;
615      }
616    }
617    elseif( $user = getUserByName($this->username,'caldav',__LINE__,__FILE__)) {
618      $this->principal = $user;
619      $this->user_no = $user->user_no;
620      $this->principal_id = $user->principal_id;
621    }
622  }
623
624
625  /**
626  * Permissions are controlled as follows:
627  *  1. if the path is '/', the request has read privileges
628  *  2. if the requester is an admin, the request has read/write priviliges
629  *  3. if there is a <user name> component which matches the logged on user
630  *     then the request has read/write privileges
631  *  4. otherwise we query the defined relationships between users and use
632  *     the minimum privileges returned from that analysis.
633  *
634  * @param int $user_no The current user number
635  *
636  */
637  function setPermissions() {
638    global $c, $session;
639     
640    $path = preg_replace("/\/|addressbook/","",$this->collection_path);
641    dbg_error_log( "permissionn", "pasta  %s pasta de verdade %s",$path,$this->path );
642    if ( preg_match('{^/.well-known/carddav$}',$this->path))
643        {
644            $this->privileges = privilege_to_bits('all');
645             dbg_error_log( "caldav", "Full permissions for %s for path %s", ( $session->user_no == $this->user_no ? "user accessing their own hierarchy" : "a systems administrator"),$this->path );
646       }   
647    else if ( $this->path == '/' || $this->path == '' ) {
648      $this->privileges = privilege_to_bits( array('read','read-free-busy','read-acl'));
649      dbg_error_log( "caldav", "Full read permissions for user accessing /" );
650    }
651    else if ( $session->AllowedTo("Admin") || $session->user_no == $this->user_no && $this->username == $path ) {
652    //else if ( $session->AllowedTo("Admin") || $session->user_no == $this->user_no ) {
653      $this->privileges = privilege_to_bits('all');
654      dbg_error_log( "caldav", "Full permissions for %s for path %s", ( $session->user_no == $this->user_no ? "user accessing their own hierarchy" : "a systems administrator"),$this->path );
655    }
656    else if ( $this->username != $path && $this->collection_type == 'calendar' ) {
657         $this->privileges = 0;
658         $filtro = "uid=".$path;
659         $atributos = array("uidNumber");
660         $newpathh = ldapDrivers::requestAtributo($filtro, $atributos);           
661         $newpath = $newpathh['uidNumber'];
662         $filtro = "uid=".$this->username;
663         $atributos = array("uidNumber");
664         $accountt = ldapDrivers::requestAtributo($filtro, $atributos);
665         $account = $accountt['uidNumber'];
666         $qry = new AwlQuery("select acl_rights from phpgw_acl where acl_location = :account and acl_account = :path" ,array(':account' => $account ,':path' => $newpath));
667         if ( $qry->Exec('Request',__LINE__,__FILE__) &&  $qry->rows() == 1 && $permission_result = $qry->Fetch()){
668               switch( $permission_result->acl_rights ) {
669                     case '1' :   $this->privileges = privilege_to_bits( array('read','read-free-busy','read-acl'));
670                                  dbg_error_log( "caldav", "Basic read permissions for user accessing " );
671                                  break;
672                     case '3' :   $this->privileges = privilege_to_bits( array('read','read-free-busy','read-acl','write'));
673                                  dbg_error_log( "caldav", "Basic read/write permissions for user accessing " );
674                                  break;
675                     case '7' :   $this->privileges = privilege_to_bits( array('read','read-free-busy','read-acl','write'));
676                                  dbg_error_log( "caldav", "Basic read/write/edit permissions for user accessing " );
677                                  break;
678                     case '15' :  $this->privileges = privilege_to_bits( array('read','read-free-busy','read-acl','write','schedule-send'));
679                                  dbg_error_log( "caldav", "Basic read/write/edit/delete  permissions for user accessing  " );
680                                  break;
681                     case '31' :  $this->privileges = privilege_to_bits('all');     
682                                  dbg_error_log( "caldav", "Full permissions for %s for path %s", ( $session->user_no == $this->user_no ? "user accessing their own hierarchy" : "a systems administrator"),$this->path );
683                                  break;
684             }
685           
686         }         
687      }
688    else if ( $this->username == $path  && $this->collection_type == 'addressbook' ) {
689           $this->privileges = privilege_to_bits('all'); 
690      }
691    else {
692      $this->privileges = 0;
693      if ( $this->IsPublic() ) {
694        $this->privileges = privilege_to_bits(array('read','read-free-busy'));
695        dbg_error_log( "caldav", "Basic read permissions for user accessing a public collection" );
696      }
697      else if ( isset($c->public_freebusy_url) && $c->public_freebusy_url ) {
698        $this->privileges = privilege_to_bits('read-free-busy');
699        dbg_error_log( "caldav", "Basic free/busy permissions for user accessing a public free/busy URL" );
700      }
701
702      /**
703      * In other cases we need to query the database for permissions
704      */
705      $params = array( ':session_principal_id' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth );
706      if ( isset($this->by_email) && $this->by_email ) {
707        $sql ='SELECT pprivs( :session_principal_id::int8, :request_principal_id::int8, :scan_depth::int ) AS perm';
708        $params[':request_principal_id'] = $this->principal_id;
709      }
710      else {
711        $sql = 'SELECT path_privs( :session_principal_id::int8, :request_path::text, :scan_depth::int ) AS perm';
712        $params[':request_path'] = $this->path;
713      }
714      $qry = new AwlQuery( $sql, $params );
715      if ( $qry->Exec('caldav',__LINE__,__FILE__) && $permission_result = $qry->Fetch() )
716        $this->privileges |= bindec($permission_result->perm);
717
718      dbg_error_log( 'caldav', 'Restricted permissions for user accessing someone elses hierarchy: %s', decbin($this->privileges) );
719      if ( isset($this->ticket) && $this->ticket->MatchesPath($this->path) ) {
720        $this->privileges |= $this->ticket->privileges();
721        dbg_error_log( 'caldav', 'Applying permissions for ticket "%s" now: %s', $this->ticket->id(), decbin($this->privileges) );
722      }
723    }
724
725    /** convert privileges into older style permissions */
726    $this->permissions = array();
727    $privs = bits_to_privilege($this->privileges);
728    foreach( $privs AS $k => $v ) {
729      switch( $v ) {
730        case 'DAV::all':    $type = 'abstract';   break;
731        case 'DAV::write':  $type = 'aggregate';  break;
732        default: $type = 'real';
733      }
734      $v = str_replace('DAV::', '', $v);
735      $this->permissions[$v] = $type;
736    }
737
738  }
739
740
741  /**
742  * Checks whether the resource is locked, returning any lock token, or false
743  *
744  * @todo This logic does not catch all locking scenarios.  For example an infinite
745  * depth request should check the permissions for all collections and resources within
746  * that.  At present we only maintain permissions on a per-collection basis though.
747  */
748  function IsLocked() {
749    if ( !isset($this->_locks_found) ) {
750      $this->_locks_found = array();
751
752      $sql = 'DELETE FROM locks WHERE (start + timeout) < current_timestamp';
753      $qry = new AwlQuery($sql);
754      $qry->Exec('caldav',__LINE__,__FILE__);
755
756      /**
757      * Find the locks that might apply and load them into an array
758      */
759      $sql = 'SELECT * FROM locks WHERE :dav_name::text ~ (\'^\'||dav_name||:pattern_end_match)::text';
760      $qry = new AwlQuery($sql, array( ':dav_name' => $this->path, ':pattern_end_match' => ($this->IsInfiniteDepth() ? '' : '$') ) );
761      if ( $qry->Exec('caldav',__LINE__,__FILE__) ) {
762        while( $lock_row = $qry->Fetch() ) {
763          $this->_locks_found[$lock_row->opaquelocktoken] = $lock_row;
764        }
765      }
766      else {
767        $this->DoResponse(500,translate("Database Error"));
768        // Does not return.
769      }
770    }
771
772    foreach( $this->_locks_found AS $lock_token => $lock_row ) {
773      if ( $lock_row->depth == DEPTH_INFINITY || $lock_row->dav_name == $this->path ) {
774        return $lock_token;
775      }
776    }
777
778    return false;  // Nothing matched
779  }
780
781
782  /**
783  * Checks whether the collection is public
784  */
785  function IsPublic() {
786    if ( isset($this->collection) && isset($this->collection->publicly_readable) && $this->collection->publicly_readable == 't' ) {
787      return true;
788    }
789    return false;
790  }
791
792
793  /**
794  * Returns the dav_name of the resource in our internal namespace
795  */
796  function dav_name() {
797    if ( isset($this->path) ) return $this->path;
798    return null;
799  }
800
801
802  /**
803  * Returns the name for this depth: 0, 1, infinity
804  */
805  function GetDepthName( ) {
806    if ( $this->IsInfiniteDepth() ) return 'infinity';
807    return $this->depth;
808  }
809
810  /**
811  * Returns the tail of a Regex appropriate for this Depth, when appended to
812  *
813  */
814  function DepthRegexTail() {
815    if ( $this->IsInfiniteDepth() ) return '';
816    if ( $this->depth == 0 ) return '$';
817    return '[^/]*/?$';
818  }
819
820  /**
821  * Returns the locked row, either from the cache or from the database
822  *
823  * @param string $dav_name The resource which we want to know the lock status for
824  */
825  function GetLockRow( $lock_token ) {
826    if ( isset($this->_locks_found) && isset($this->_locks_found[$lock_token]) ) {
827      return $this->_locks_found[$lock_token];
828    }
829
830    $qry = new AwlQuery('SELECT * FROM locks WHERE opaquelocktoken = :lock_token', array( ':lock_token' => $lock_token ) );
831    if ( $qry->Exec('caldav',__LINE__,__FILE__) ) {
832      $lock_row = $qry->Fetch();
833      $this->_locks_found = array( $lock_token => $lock_row );
834      return $this->_locks_found[$lock_token];
835    }
836    else {
837      $this->DoResponse( 500, translate("Database Error") );
838    }
839
840    return false;  // Nothing matched
841  }
842
843
844  /**
845  * Checks to see whether the lock token given matches one of the ones handed in
846  * with the request.
847  *
848  * @param string $lock_token The opaquelocktoken which we are looking for
849  */
850  function ValidateLockToken( $lock_token ) {
851    if ( isset($this->lock_token) && $this->lock_token == $lock_token ) {
852      dbg_error_log( "caldav", "They supplied a valid lock token.  Great!" );
853      return true;
854    }
855    if ( isset($this->if_clause) ) {
856      dbg_error_log( "caldav", "Checking lock token '%s' against '%s'", $lock_token, $this->if_clause );
857      $tokens = preg_split( '/[<>]/', $this->if_clause );
858      foreach( $tokens AS $k => $v ) {
859        dbg_error_log( "caldav", "Checking lock token '%s' against '%s'", $lock_token, $v );
860        if ( 'opaquelocktoken:' == substr( $v, 0, 16 ) ) {
861          if ( substr( $v, 16 ) == $lock_token ) {
862            dbg_error_log( "caldav", "Lock token '%s' validated OK against '%s'", $lock_token, $v );
863            return true;
864          }
865        }
866      }
867    }
868    else {
869      @dbg_error_log( "caldav", "Invalid lock token '%s' - not in Lock-token (%s) or If headers (%s) ", $lock_token, $this->lock_token, $this->if_clause );
870    }
871
872    return false;
873  }
874
875
876  /**
877  * Returns the DB object associated with a lock token, or false.
878  *
879  * @param string $lock_token The opaquelocktoken which we are looking for
880  */
881  function GetLockDetails( $lock_token ) {
882    if ( !isset($this->_locks_found) && false === $this->IsLocked() ) return false;
883    if ( isset($this->_locks_found[$lock_token]) ) return $this->_locks_found[$lock_token];
884    return false;
885  }
886
887
888  /**
889  * This will either (a) return false if no locks apply, or (b) return the lock_token
890  * which the request successfully included to open the lock, or:
891  * (c) respond directly to the client with the failure.
892  *
893  * @return mixed false (no lock) or opaquelocktoken (opened lock)
894  */
895  function FailIfLocked() {
896    if ( $existing_lock = $this->IsLocked() ) { // NOTE Assignment in if() is expected here.
897      dbg_error_log( "caldav", "There is a lock on '%s'", $this->path);
898      if ( ! $this->ValidateLockToken($existing_lock) ) {
899        $lock_row = $this->GetLockRow($existing_lock);
900        /**
901        * Already locked - deny it
902        */
903        $response[] = new XMLElement( 'response', array(
904            new XMLElement( 'href',   $lock_row->dav_name ),
905            new XMLElement( 'status', 'HTTP/1.1 423 Resource Locked')
906        ));
907        if ( $lock_row->dav_name != $this->path ) {
908          $response[] = new XMLElement( 'response', array(
909              new XMLElement( 'href',   $this->path ),
910              new XMLElement( 'propstat', array(
911                new XMLElement( 'prop', new XMLElement( 'lockdiscovery' ) ),
912                new XMLElement( 'status', 'HTTP/1.1 424 Failed Dependency')
913              ))
914          ));
915        }
916        $response = new XMLElement( "multistatus", $response, array('xmlns'=>'DAV:') );
917        $xmldoc = $response->Render(0,'<?xml version="1.0" encoding="utf-8" ?>');
918        $this->DoResponse( 207, $xmldoc, 'text/xml; charset="utf-8"' );
919        // Which we won't come back from
920      }
921      return $existing_lock;
922    }
923    return false;
924  }
925
926
927  /**
928  * Coerces the Content-type of the request into something valid/appropriate
929  */
930  function CoerceContentType() {
931    if ( isset($this->content_type) ) {
932      $type = explode( '/', $this->content_type, 2);
933      /** @todo: Perhaps we should look at the target collection type, also. */
934      if ( $type[0] == 'text' ) return;
935    }
936
937    /** Null (or peculiar) content-type supplied so we have to try and work it out... */
938    $first_word = trim(substr( $this->raw_post, 0, 30));
939    $first_word = strtoupper( preg_replace( '/\s.*/s', '', $first_word ) );
940    switch( $first_word ) {
941      case '<?XML':
942        dbg_error_log( 'LOG WARNING', 'Application sent content-type of "%s" instead of "text/xml"',
943                                        (isset($this->content_type)?$this->content_type:'(null)') );
944        $this->content_type = 'text/xml';
945        break;
946      case 'BEGIN:VCALENDAR':
947        dbg_error_log( 'LOG WARNING', 'Application sent content-type of "%s" instead of "text/calendar"',
948                                        (isset($this->content_type)?$this->content_type:'(null)') );
949        $this->content_type = 'text/calendar';
950        break;
951      case 'BEGIN:VCARD':
952        dbg_error_log( 'LOG WARNING', 'Application sent content-type of "%s" instead of "text/vcard"',
953                                        (isset($this->content_type)?$this->content_type:'(null)') );
954        $this->content_type = 'text/vcard';
955        break;
956      default:
957        dbg_error_log( 'LOG NOTICE', 'Unusual content-type of "%s" and first word of content is "%s"',
958                                        (isset($this->content_type)?$this->content_type:'(null)'), $first_word );
959    }
960  }
961
962
963  /**
964  * Returns true if the URL referenced by this request points at a collection.
965  */
966  function IsCollection( ) {
967    if ( !isset($this->_is_collection) ) {
968      $this->_is_collection = preg_match( '#/$#', $this->path );
969    }
970    return $this->_is_collection;
971  }
972
973
974  /**
975  * Returns true if the URL referenced by this request points at a calendar collection.
976  */
977  function IsCalendar( ) {
978    if ( !$this->IsCollection() || !isset($this->collection) ) return false;
979    return $this->collection->is_calendar == 't';
980  }
981
982
983  /**
984  * Returns true if the URL referenced by this request points at an addressbook collection.
985  */
986  function IsAddressBook( ) {
987    //if ( !$this->IsCollection() || !isset($this->collection) ) return false;
988    if ( $this->collection_type == 'addressbook' ) 
989         return true;
990    else
991       return false;
992  }
993
994
995  /**
996  * Returns true if the URL referenced by this request points at a principal.
997  */
998  function IsPrincipal( ) {
999    if ( !isset($this->_is_principal) ) {
1000      $this->_is_principal = preg_match( '#^/[^/]+/$#', $this->path );
1001    }
1002    return $this->_is_principal;
1003  }
1004
1005
1006  /**
1007  * Returns true if the URL referenced by this request is within a proxy URL
1008  */
1009  function IsProxyRequest( ) {
1010    if ( !isset($this->_is_proxy_request) ) {
1011      $this->_is_proxy_request = preg_match( '#^/[^/]+/calendar-proxy-(read|write)/?[^/]*$#', $this->path );
1012    }
1013    return $this->_is_proxy_request;
1014  }
1015
1016
1017  /**
1018  * Returns true if the request asked for infinite depth
1019  */
1020  function IsInfiniteDepth( ) {
1021    return ($this->depth == DEPTH_INFINITY);
1022  }
1023
1024
1025  /**
1026  * Returns the ID of the collection of, or containing this request
1027  */
1028  function CollectionId( ) {
1029    return $this->collection_id;
1030  }
1031
1032
1033  /**
1034  * Returns the array of supported privileges converted into XMLElements
1035  */
1036  function BuildSupportedPrivileges( &$reply, $privs = null ) {
1037    $privileges = array();
1038    if ( $privs === null ) $privs = $this->supported_privileges;
1039    foreach( $privs AS $k => $v ) {
1040      dbg_error_log( 'caldav', 'Adding privilege "%s" which is "%s".', $k, $v );
1041      $privilege = new XMLElement('privilege');
1042      $reply->NSElement($privilege,$k);
1043      $privset = array($privilege);
1044      if ( is_array($v) ) {
1045        dbg_error_log( 'caldav', '"%s" is a container of sub-privileges.', $k );
1046        $privset = array_merge($privset, $this->BuildSupportedPrivileges($reply,$v));
1047      }
1048      else if ( $v == 'abstract' ) {
1049        dbg_error_log( 'caldav', '"%s" is an abstract privilege.', $v );
1050        $privset[] = new XMLElement('abstract');
1051      }
1052      else if ( strlen($v) > 1 ) {
1053        $privset[] = new XMLElement('description', $v);
1054      }
1055      $privileges[] = new XMLElement('supported-privilege',$privset);
1056    }
1057    return $privileges;
1058  }
1059
1060
1061  /**
1062  * Are we allowed to do the requested activity
1063  *
1064  * +------------+------------------------------------------------------+
1065  * | METHOD     | PRIVILEGES                                           |
1066  * +------------+------------------------------------------------------+
1067  * | MKCALENDAR | DAV:bind                                             |
1068  * | REPORT     | DAV:read or CALDAV:read-free-busy (on all referenced |
1069  * |            | resources)                                           |
1070  * +------------+------------------------------------------------------+
1071  *
1072  * @param string $activity The activity we want to do.
1073  */
1074  function AllowedTo( $activity ) {
1075    global $session;
1076    dbg_error_log('caldav', 'Checking whether "%s" is allowed to "%s"', $session->username, $activity);
1077    if ( isset($this->permissions['all']) ) return true;
1078    switch( $activity ) {
1079      case 'all':
1080        return false; // If they got this far then they don't
1081        break;
1082
1083      case "CALDAV:schedule-send-freebusy":
1084        return isset($this->permissions['read']) || isset($this->permissions['urn:ietf:params:xml:ns:caldav:read-free-busy']);
1085        break;
1086
1087      case "CALDAV:schedule-send-invite":
1088        return isset($this->permissions['read']) || isset($this->permissions['urn:ietf:params:xml:ns:caldav:read-free-busy']);
1089        break;
1090
1091      case "CALDAV:schedule-send-reply":
1092        return isset($this->permissions['read']) || isset($this->permissions['urn:ietf:params:xml:ns:caldav:read-free-busy']);
1093        break;
1094
1095      case 'freebusy':
1096        return isset($this->permissions['read']) || isset($this->permissions['urn:ietf:params:xml:ns:caldav:read-free-busy']);
1097        break;
1098
1099      case 'delete':
1100        return isset($this->permissions['write']) || isset($this->permissions['unbind']);
1101        break;
1102
1103      case 'proppatch':
1104        return isset($this->permissions['write']) || isset($this->permissions['write-properties']);
1105        break;
1106
1107      case 'modify':
1108        return isset($this->permissions['write']) || isset($this->permissions['write-content']);
1109        break;
1110
1111      case 'create':
1112        return isset($this->permissions['write']) || isset($this->permissions['bind']);
1113        break;
1114
1115      case 'mkcalendar':
1116      case 'mkcol':
1117        if ( !isset($this->permissions['write']) || !isset($this->permissions['bind']) ) return false;
1118        if ( $this->is_principal ) return false;
1119        if ( $this->path == '/' ) return false;
1120        break;
1121
1122      default:
1123        $test_bits = privilege_to_bits( $activity );
1124//        dbg_error_log( 'caldav', 'request::AllowedTo("%s") (%s) against allowed "%s" => "%s" (%s)',
1125//             (is_array($activity) ? implode(',',$activity) : $activity), decbin($test_bits),
1126//             decbin($this->privileges), ($this->privileges & $test_bits), decbin($this->privileges & $test_bits) );
1127        return (($this->privileges & $test_bits) > 0 );
1128        break;
1129    }
1130
1131    return false;
1132  }
1133
1134
1135
1136  /**
1137  * Return the privileges bits for the current session user to this resource
1138  */
1139  function Privileges() {
1140    return $this->privileges;
1141  }
1142
1143
1144  /**
1145  * Is the user has the privileges to do what is requested.
1146  */
1147  function HavePrivilegeTo( $do_what ) {
1148    $test_bits = privilege_to_bits( $do_what );
1149//    dbg_error_log( 'caldav', 'request::HavePrivilegeTo("%s") [%s] against allowed "%s" => "%s" (%s)',
1150//             (is_array($do_what) ? implode(',',$do_what) : $do_what), decbin($test_bits),
1151//              decbin($this->privileges), ($this->privileges & $test_bits), decbin($this->privileges & $test_bits) );
1152    return ($this->privileges & $test_bits) > 0;
1153  }
1154
1155
1156  /**
1157  * Sometimes it's a perfectly formed request, but we just don't do that :-(
1158  * @param array $unsupported An array of the properties we don't support.
1159  */
1160  function UnsupportedRequest( $unsupported ) {
1161    if ( isset($unsupported) && count($unsupported) > 0 ) {
1162      $badprops = new XMLElement( "prop" );
1163      foreach( $unsupported AS $k => $v ) {
1164        // Not supported at this point...
1165        dbg_error_log("ERROR", " %s: Support for $v:$k properties is not implemented yet", $this->method );
1166        $badprops->NewElement(strtolower($k),false,array("xmlns" => strtolower($v)));
1167      }
1168      $error = new XMLElement("error", $badprops, array("xmlns" => "DAV:") );
1169
1170      $this->XMLResponse( 422, $error );
1171    }
1172  }
1173
1174
1175  /**
1176  * Send a need-privileges error response.  This function will only return
1177  * if the $href is not supplied and the current user has the specified
1178  * permission for the request path.
1179  *
1180  * @param string $privilege The name of the needed privilege.
1181  * @param string $href The unconstructed URI where we needed the privilege.
1182  */
1183  function NeedPrivilege( $privileges, $href=null ) {
1184    if ( is_string($privileges) ) $privileges = array( $privileges );
1185    if ( !isset($href) ) {
1186      if ( $this->HavePrivilegeTo($privileges) ) return;
1187      $href = $this->path;
1188    }
1189
1190    $reply = new XMLDocument( array('DAV:' => '') );
1191    $privnodes = array( $reply->href(ConstructURL($href)), new XMLElement( 'privilege' ) );
1192    // RFC3744 specifies that we can only respond with one needed privilege, so we pick the first.
1193    $reply->NSElement( $privnodes[1], $privileges[0] );
1194    $xml = new XMLElement( 'need-privileges', new XMLElement( 'resource', $privnodes) );
1195    $xmldoc = $reply->Render('error',$xml);
1196    $this->DoResponse( 403, $xmldoc, 'text/xml; charset="utf-8"' );
1197    exit(0);  // Unecessary, but might clarify things
1198  }
1199
1200
1201  /**
1202  * Send an error response for a failed precondition.
1203  *
1204  * @param int $status The status code for the failed precondition.  Normally 403
1205  * @param string $precondition The namespaced precondition tag.
1206  * @param string $explanation An optional text explanation for the failure.
1207  */
1208  function PreconditionFailed( $status, $precondition, $explanation = '') {
1209    $xmldoc = sprintf('<?xml version="1.0" encoding="utf-8" ?>
1210<error xmlns="DAV:">
1211  <%s/>%s
1212</error>', $precondition, $explanation );
1213
1214    $this->DoResponse( $status, $xmldoc, 'text/xml; charset="utf-8"' );
1215    exit(0);  // Unecessary, but might clarify things
1216  }
1217
1218
1219  /**
1220  * Send a simple error informing the client that was a malformed request
1221  *
1222  * @param string $text An optional text description of the failure.
1223  */
1224  function MalformedRequest( $text = 'Bad request' ) {
1225    $this->DoResponse( 400, $text );
1226    exit(0);  // Unecessary, but might clarify things
1227  }
1228
1229
1230  /**
1231  * Send an XML Response.  This function will never return.
1232  *
1233  * @param int $status The HTTP status to respond
1234  * @param XMLElement $xmltree An XMLElement tree to be rendered
1235  */
1236  function XMLResponse( $status, $xmltree ) {
1237    $xmldoc = $xmltree->Render(0,'<?xml version="1.0" encoding="ISO-8859-1" ?>');
1238    $etag = md5($xmldoc);
1239    header("ETag: \"$etag\"");
1240    $this->DoResponse( $status, $xmldoc, 'text/xml; charset="utf-8"' );
1241    exit(0);  // Unecessary, but might clarify things
1242  }
1243
1244  /**
1245  * Utility function we call when we have a simple status-based response to
1246  * return to the client.  Possibly
1247  *
1248  * @param int $status The HTTP status code to send.
1249  * @param string $message The friendly text message to send with the response.
1250  */
1251  function DoResponse( $status, $message="", $content_type="text/plain; charset=\"utf-8\"" ) {
1252    global $session, $c;
1253    @header( sprintf("HTTP/1.1 %d %s", $status, getStatusMessage($status)) );
1254    @header( sprintf("X-DAViCal-Version: DAViCal/%d.%d.%d; DB/%d.%d.%d", $c->code_major, $c->code_minor, $c->code_patch, $c->schema_major, $c->schema_minor, $c->schema_patch) );
1255    @header( "Content-type: ".$content_type );
1256
1257    if ( (isset($c->dbg['ALL']) && $c->dbg['ALL']) || (isset($c->dbg['response']) && $c->dbg['response']) || $status > 399 ) {
1258      $lines = headers_list();
1259      dbg_error_log( "LOG ", "***************** Response Header ****************" );
1260      foreach( $lines AS $v ) {
1261        dbg_error_log( "LOG headers", "-->%s", $v );
1262      }
1263      dbg_error_log( "LOG ", "******************** Response ********************" );
1264      // Log the request in all it's gory detail.
1265      $lines = preg_split( '#[\r\n]+#', $message);
1266      foreach( $lines AS $v ) {
1267        dbg_error_log( "LOG response", "-->%s", $v );
1268      }
1269    }
1270
1271    header( "Content-Length: ".strlen($message) );
1272    echo $message;
1273
1274    if ( isset($c->dbg['caldav']) && $c->dbg['caldav'] ) {
1275      if ( strlen($message) > 100 || strstr($message, "\n") ) {
1276        $message = substr( preg_replace("#\s+#m", ' ', $message ), 0, 100) . (strlen($message) > 100 ? "..." : "");
1277      }
1278
1279      dbg_error_log("caldav", "Status: %d, Message: %s, User: %d, Path: %s", $status, $message, $session->user_no, $this->path);
1280    }
1281    if ( isset($c->dbg['statistics']) && $c->dbg['statistics'] ) {
1282      $script_time = microtime(true) - $c->script_start_time;
1283      @dbg_error_log("statistics", "Method: %s, Status: %d, Script: %5.3lfs, Queries: %5.3lfs, URL: %s",
1284                         $this->method, $status, $script_time, $c->total_query_time, $this->path);
1285    }
1286
1287    exit(0);
1288  }
1289
1290}
1291
Note: See TracBrowser for help on using the repository browser.