A more efficient query for GET including sub-collections.
[davical.git] / inc / CalDAVRequest.php
blobbdfd0c7beabebea8e79e6fb279c08199f713b761
1 <?php
2 /**
3 * Functions that are needed for all CalDAV Requests
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.
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
17 require_once("AwlCache.php");
18 require_once("XMLDocument.php");
19 require_once("DAVPrincipal.php");
20 require_once("DAVTicket.php");
22 define('DEPTH_INFINITY', 9999);
24 /**
25 * A class for collecting things to do with this request.
27 * @package davical
29 class CalDAVRequest
31 var $options;
33 /**
34 * The raw data sent along with the request
36 var $raw_post;
38 /**
39 * The HTTP request method: PROPFIND, LOCK, REPORT, OPTIONS, etc...
41 var $method;
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.
47 var $depth;
49 /**
50 * The 'principal' (user/resource/...) which this request seeks to access
51 * @var DAVPrincipal
53 var $principal;
55 /**
56 * The 'current_user_principal_xml' the DAV:current-user-principal answer. An
57 * XMLElement object with an <href> or <unauthenticated> fragment.
59 var $current_user_principal_xml;
61 /**
62 * The user agent making the request.
64 var $user_agent;
66 /**
67 * The ID of the collection containing this path, or of this path if it is a collection
69 var $collection_id;
71 /**
72 * The path corresponding to the collection_id
74 var $collection_path;
76 /**
77 * The type of collection being requested:
78 * calendar, schedule-inbox, schedule-outbox
80 var $collection_type;
82 /**
83 * The type of collection being requested:
84 * calendar, schedule-inbox, schedule-outbox
86 protected $exists;
88 /**
89 * The value of any 'Destionation:' header, if present.
91 var $destination;
93 /**
94 * The decimal privileges allowed by this user to the identified resource.
96 protected $privileges;
98 /**
99 * A static structure of supported privileges.
101 var $supported_privileges;
104 * A DAVTicket object, if there is a ?ticket=id or Ticket: id with this request
106 public $ticket;
109 * An array of values from the 'Prefer' header. At present only 'return-minimal' is acted on in any way - you
110 * can test that value with the PreferMinimal() method.
112 private $prefer;
115 * Create a new CalDAVRequest object.
117 function __construct( $options = array() ) {
118 global $session, $c, $debugging;
120 $this->options = $options;
121 if ( !isset($this->options['allow_by_email']) ) $this->options['allow_by_email'] = false;
123 if ( isset($_SERVER['HTTP_PREFER']) ) {
124 $this->prefer = explode( ',', $_SERVER['HTTP_PREFER']);
126 else if ( isset($_SERVER['HTTP_BRIEF']) && (strtoupper($_SERVER['HTTP_PREFER']) == 'T') ) {
127 $this->prefer = array( 'return-minimal');
129 else
130 $this->prefer = array();
133 * Our path is /<script name>/<user name>/<user controlled> if it ends in
134 * a trailing '/' then it is referring to a DAV 'collection' but otherwise
135 * it is referring to a DAV data item.
137 * Permissions are controlled as follows:
138 * 1. if there is no <user name> component, the request has read privileges
139 * 2. if the requester is an admin, the request has read/write priviliges
140 * 3. if there is a <user name> component which matches the logged on user
141 * then the request has read/write privileges
142 * 4. otherwise we query the defined relationships between users and use
143 * the minimum privileges returned from that analysis.
145 $this->path = (isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : "/");
146 $this->path = rawurldecode($this->path);
148 /** Allow a request for .../calendar.ics to translate into the calendar URL */
149 if ( preg_match( '#^(/[^/]+/[^/]+).ics$#', $this->path, $matches ) ) {
150 $this->path = $matches[1]. '/';
153 // dbg_error_log( "caldav", "Sanitising path '%s'", $this->path );
154 $bad_chars_regex = '/[\\^\\[\\(\\\\]/';
155 if ( preg_match( $bad_chars_regex, $this->path ) ) {
156 $this->DoResponse( 400, translate("The calendar path contains illegal characters.") );
158 if ( strstr($this->path,'//') ) $this->path = preg_replace( '#//+#', '/', $this->path);
160 if ( !isset($c->raw_post) ) $c->raw_post = file_get_contents( 'php://input');
161 if ( isset($_SERVER['HTTP_CONTENT_ENCODING']) ) {
162 $encoding = $_SERVER['HTTP_CONTENT_ENCODING'];
163 @dbg_error_log('caldav', 'Content-Encoding: %s', $encoding );
164 $encoding = preg_replace('{[^a-z0-9-]}i','',$encoding);
165 if ( ! ini_get('open_basedir') && (isset($c->dbg['ALL']) || isset($c->dbg['caldav'])) ) {
166 $fh = fopen('/tmp/encoded_data.'.$encoding,'w');
167 if ( $fh ) {
168 fwrite($fh,$c->raw_post);
169 fclose($fh);
172 switch( $encoding ) {
173 case 'gzip':
174 $this->raw_post = @gzdecode($c->raw_post);
175 break;
176 case 'deflate':
177 $this->raw_post = @gzinflate($c->raw_post);
178 break;
179 case 'compress':
180 $this->raw_post = @gzuncompress($c->raw_post);
181 break;
182 default:
184 if ( empty($this->raw_post) && !empty($c->raw_post) ) {
185 $this->PreconditionFailed(415, 'content-encoding', sprintf('Unable to decode "%s" content encoding.', $_SERVER['HTTP_CONTENT_ENCODING']));
187 $c->raw_post = $this->raw_post;
189 else {
190 $this->raw_post = $c->raw_post;
193 if ( isset($debugging) && isset($_GET['method']) ) {
194 $_SERVER['REQUEST_METHOD'] = $_GET['method'];
196 else if ( $_SERVER['REQUEST_METHOD'] == 'POST' && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) ){
197 $_SERVER['REQUEST_METHOD'] = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
199 $this->method = $_SERVER['REQUEST_METHOD'];
200 $this->content_type = (isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null);
201 if ( preg_match( '{^(\S+/\S+)\s*(;.*)?$}', $this->content_type, $matches ) ) {
202 $this->content_type = $matches[1];
204 if ( strlen($c->raw_post) > 0 ) {
205 if ( $this->method == 'PROPFIND' || $this->method == 'REPORT' || $this->method == 'PROPPATCH' || $this->method == 'BIND' || $this->method == 'MKTICKET' || $this->method == 'ACL' ) {
206 if ( !preg_match( '{^(text|application)/xml$}', $this->content_type ) ) {
207 @dbg_error_log( "LOG request", 'Request is "%s" but client set content-type to "%s". Assuming they meant XML!',
208 $this->method, $this->content_type );
209 $this->content_type = 'text/xml';
212 else if ( $this->method == 'PUT' || $this->method == 'POST' ) {
213 $this->CoerceContentType();
216 else {
217 $this->content_type = 'text/plain';
219 $this->user_agent = ((isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : "Probably Mulberry"));
222 * A variety of requests may set the "Depth" header to control recursion
224 if ( isset($_SERVER['HTTP_DEPTH']) ) {
225 $this->depth = $_SERVER['HTTP_DEPTH'];
227 else {
229 * Per rfc2518, section 9.2, 'Depth' might not always be present, and if it
230 * is not present then a reasonable request-type-dependent default should be
231 * chosen.
233 switch( $this->method ) {
234 case 'DELETE':
235 case 'MOVE':
236 case 'COPY':
237 case 'LOCK':
238 $this->depth = 'infinity';
239 break;
241 case 'REPORT':
242 $this->depth = 0;
243 break;
245 case 'PROPFIND':
246 default:
247 $this->depth = 0;
250 if ( !is_int($this->depth) && "infinity" == $this->depth ) $this->depth = DEPTH_INFINITY;
251 $this->depth = intval($this->depth);
254 * MOVE/COPY use a "Destination" header and (optionally) an "Overwrite" one.
256 if ( isset($_SERVER['HTTP_DESTINATION']) ) {
257 $this->destination = $_SERVER['HTTP_DESTINATION'];
258 if ( preg_match('{^(https?)://([a-z.-]+)(:[0-9]+)?(/.*)$}', $this->destination, $matches ) ) {
259 $this->destination = $matches[4];
262 $this->overwrite = ( isset($_SERVER['HTTP_OVERWRITE']) && ($_SERVER['HTTP_OVERWRITE'] == 'F') ? false : true ); // RFC4918, 9.8.4 says default True.
265 * LOCK things use an "If" header to hold the lock in some cases, and "Lock-token" in others
267 if ( isset($_SERVER['HTTP_IF']) ) $this->if_clause = $_SERVER['HTTP_IF'];
268 if ( isset($_SERVER['HTTP_LOCK_TOKEN']) && preg_match( '#[<]opaquelocktoken:(.*)[>]#', $_SERVER['HTTP_LOCK_TOKEN'], $matches ) ) {
269 $this->lock_token = $matches[1];
273 * Check for an access ticket.
275 if ( isset($_GET['ticket']) ) {
276 $this->ticket = new DAVTicket($_GET['ticket']);
278 else if ( isset($_SERVER['HTTP_TICKET']) ) {
279 $this->ticket = new DAVTicket($_SERVER['HTTP_TICKET']);
283 * LOCK things use a "Timeout" header to set a series of reducing alternative values
285 if ( isset($_SERVER['HTTP_TIMEOUT']) ) {
286 $timeouts = explode( ',', $_SERVER['HTTP_TIMEOUT'] );
287 foreach( $timeouts AS $k => $v ) {
288 if ( strtolower($v) == 'infinite' ) {
289 $this->timeout = (isset($c->maximum_lock_timeout) ? $c->maximum_lock_timeout : 86400 * 100);
290 break;
292 elseif ( strtolower(substr($v,0,7)) == 'second-' ) {
293 $this->timeout = min( intval(substr($v,7)), (isset($c->maximum_lock_timeout) ? $c->maximum_lock_timeout : 86400 * 100) );
294 break;
297 if ( ! isset($this->timeout) || $this->timeout == 0 ) $this->timeout = (isset($c->default_lock_timeout) ? $c->default_lock_timeout : 900);
300 $this->principal = new Principal('path',$this->path);
303 * RFC2518, 5.2: URL pointing to a collection SHOULD end in '/', and if it does not then
304 * we SHOULD return a Content-location header with the correction...
306 * We therefore look for a collection which matches one of the following URLs:
307 * - The exact request.
308 * - If the exact request, doesn't end in '/', then the request URL with a '/' appended
309 * - The request URL truncated to the last '/'
310 * The collection URL for this request is therefore the longest row in the result, so we
311 * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1"
313 $sql = "SELECT * FROM collection WHERE dav_name = :exact_name";
314 $params = array( ':exact_name' => $this->path );
315 if ( !preg_match( '#/$#', $this->path ) ) {
316 $sql .= " OR dav_name = :truncated_name OR dav_name = :trailing_slash_name";
317 $params[':truncated_name'] = preg_replace( '#[^/]*$#', '', $this->path);
318 $params[':trailing_slash_name'] = $this->path."/";
320 $sql .= " ORDER BY LENGTH(dav_name) DESC LIMIT 1";
321 $qry = new AwlQuery( $sql, $params );
322 if ( $qry->Exec('caldav',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
323 if ( $row->dav_name == $this->path."/" ) {
324 $this->path = $row->dav_name;
325 dbg_error_log( "caldav", "Path is actually a collection - sending Content-Location header." );
326 header( "Content-Location: ".ConstructURL($this->path) );
329 $this->collection_id = $row->collection_id;
330 $this->collection_path = $row->dav_name;
331 $this->collection_type = ($row->is_calendar == 't' ? 'calendar' : 'collection');
332 $this->collection = $row;
333 if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->path, $matches ) ) {
334 $this->collection_type = 'schedule-'. $matches[3]. 'box';
336 $this->collection->type = $this->collection_type;
338 else if ( preg_match( '{^( ( / ([^/]+) / ) \.(in|out)/ ) [^/]*$}x', $this->path, $matches ) ) {
339 // The request is for a scheduling inbox or outbox (or something inside one) and we should auto-create it
340 $params = array( ':username' => $matches[3], ':parent_container' => $matches[2], ':dav_name' => $matches[1] );
341 $params[':boxname'] = ($matches[4] == 'in' ? ' Inbox' : ' Outbox');
342 $this->collection_type = 'schedule-'. $matches[4]. 'box';
343 $params[':resourcetypes'] = sprintf('<DAV::collection/><urn:ietf:params:xml:ns:caldav:%s/>', $this->collection_type );
344 $sql = <<<EOSQL
345 INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified, dav_etag, resourcetypes )
346 VALUES( (SELECT user_no FROM usr WHERE username = text(:username)),
347 :parent_container, :dav_name,
348 (SELECT fullname FROM usr WHERE username = text(:username)) || :boxname,
349 FALSE, current_timestamp, current_timestamp, '1', :resourcetypes )
350 EOSQL;
352 $qry = new AwlQuery( $sql, $params );
353 $qry->Exec('caldav',__LINE__,__FILE__);
354 dbg_error_log( 'caldav', 'Created new collection as "%s".', trim($params[':boxname']) );
356 // Uncache anything to do with the collection
357 $cache = getCacheInstance();
358 $cache->delete( 'collection-'.$params[':dav_name'], null );
359 $cache->delete( 'principal-'.$params[':parent_container'], null );
361 $qry = new AwlQuery( "SELECT * FROM collection WHERE dav_name = :dav_name", array( ':dav_name' => $matches[1] ) );
362 if ( $qry->Exec('caldav',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
363 $this->collection_id = $row->collection_id;
364 $this->collection_path = $matches[1];
365 $this->collection = $row;
366 $this->collection->type = $this->collection_type;
369 else if ( preg_match( '#^((/[^/]+/)calendar-proxy-(read|write))/?[^/]*$#', $this->path, $matches ) ) {
370 $this->collection_type = 'proxy';
371 $this->_is_proxy_request = true;
372 $this->proxy_type = $matches[3];
373 $this->collection_path = $matches[1].'/'; // Enforce trailling '/'
374 if ( $this->collection_path == $this->path."/" ) {
375 $this->path .= '/';
376 dbg_error_log( "caldav", "Path is actually a (proxy) collection - sending Content-Location header." );
377 header( "Content-Location: ".ConstructURL($this->path) );
380 else if ( $this->options['allow_by_email'] && preg_match( '#^/(\S+@\S+[.]\S+)/?$#', $this->path) ) {
381 /** @todo we should deprecate this now that Evolution 2.27 can do scheduling extensions */
382 $this->collection_id = -1;
383 $this->collection_type = 'email';
384 $this->collection_path = $this->path;
385 $this->_is_principal = true;
387 else if ( preg_match( '#^(/[^/]+)/?$#', $this->path, $matches) || preg_match( '#^(/principals/[^/]+/[^/]+)/?$#', $this->path, $matches) ) {
388 $this->collection_id = -1;
389 $this->collection_path = $matches[1].'/'; // Enforce trailling '/'
390 $this->collection_type = 'principal';
391 $this->_is_principal = true;
392 if ( $this->collection_path == $this->path."/" ) {
393 $this->path .= '/';
394 dbg_error_log( "caldav", "Path is actually a collection - sending Content-Location header." );
395 header( "Content-Location: ".ConstructURL($this->path) );
397 if ( preg_match( '#^(/principals/[^/]+/[^/]+)/?$#', $this->path, $matches) ) {
398 // Force a depth of 0 on these, which are at the wrong URL.
399 $this->depth = 0;
402 else if ( $this->path == '/' ) {
403 $this->collection_id = -1;
404 $this->collection_path = '/';
405 $this->collection_type = 'root';
408 if ( $this->collection_path == $this->path ) $this->_is_collection = true;
409 dbg_error_log( "caldav", " Collection '%s' is %d, type %s", $this->collection_path, $this->collection_id, $this->collection_type );
412 * Extract the user whom we are accessing
414 $this->principal = new DAVPrincipal( array( "path" => $this->path, "options" => $this->options ) );
415 $this->user_no = $this->principal->user_no();
416 $this->username = $this->principal->username();
417 $this->by_email = $this->principal->byEmail();
418 $this->principal_id = $this->principal->principal_id();
420 if ( $this->collection_type == 'principal' || $this->collection_type == 'email' || $this->collection_type == 'proxy' ) {
421 $this->collection = $this->principal->AsCollection();
422 if( $this->collection_type == 'proxy' ) {
423 $this->collection = $this->principal->AsCollection();
424 $this->collection->is_proxy = 't';
425 $this->collection->type = 'proxy';
426 $this->collection->proxy_type = $this->proxy_type;
427 $this->collection->dav_displayname = sprintf('Proxy %s for %s', $this->proxy_type, $this->principal->username() );
430 elseif( $this->collection_type == 'root' ) {
431 $this->collection = (object) array(
432 'collection_id' => 0,
433 'dav_name' => '/',
434 'dav_etag' => md5($c->system_name),
435 'is_calendar' => 'f',
436 'is_addressbook' => 'f',
437 'is_principal' => 'f',
438 'user_no' => 0,
439 'dav_displayname' => $c->system_name,
440 'type' => 'root',
441 'created' => date('Ymd\THis')
446 * Evaluate our permissions for accessing the target
448 $this->setPermissions();
450 $this->supported_methods = array(
451 'OPTIONS' => '',
452 'PROPFIND' => '',
453 'REPORT' => '',
454 'DELETE' => '',
455 'LOCK' => '',
456 'UNLOCK' => '',
457 'MOVE' => '',
458 'ACL' => ''
460 if ( $this->IsCollection() ) {
461 switch ( $this->collection_type ) {
462 case 'root':
463 case 'email':
464 // We just override the list completely here.
465 $this->supported_methods = array(
466 'OPTIONS' => '',
467 'PROPFIND' => '',
468 'REPORT' => ''
470 break;
471 case 'schedule-inbox':
472 case 'schedule-outbox':
473 $this->supported_methods = array_merge(
474 $this->supported_methods,
475 array(
476 'POST' => '', 'GET' => '', 'PUT' => '', 'HEAD' => '', 'PROPPATCH' => ''
479 break;
480 case 'calendar':
481 $this->supported_methods['GET'] = '';
482 $this->supported_methods['PUT'] = '';
483 $this->supported_methods['HEAD'] = '';
484 break;
485 case 'collection':
486 case 'principal':
487 $this->supported_methods['GET'] = '';
488 $this->supported_methods['PUT'] = '';
489 $this->supported_methods['HEAD'] = '';
490 $this->supported_methods['MKCOL'] = '';
491 $this->supported_methods['MKCALENDAR'] = '';
492 $this->supported_methods['PROPPATCH'] = '';
493 $this->supported_methods['BIND'] = '';
494 break;
497 else {
498 $this->supported_methods = array_merge(
499 $this->supported_methods,
500 array(
501 'GET' => '',
502 'HEAD' => '',
503 'PUT' => ''
508 $this->supported_reports = array(
509 'DAV::principal-property-search' => '',
510 'DAV::expand-property' => '',
511 'DAV::sync-collection' => ''
513 if ( isset($this->collection) && $this->collection->is_calendar ) {
514 $this->supported_reports = array_merge(
515 $this->supported_reports,
516 array(
517 'urn:ietf:params:xml:ns:caldav:calendar-query' => '',
518 'urn:ietf:params:xml:ns:caldav:calendar-multiget' => '',
519 'urn:ietf:params:xml:ns:caldav:free-busy-query' => ''
523 if ( isset($this->collection) && $this->collection->is_addressbook ) {
524 $this->supported_reports = array_merge(
525 $this->supported_reports,
526 array(
527 'urn:ietf:params:xml:ns:carddav:addressbook-query' => '',
528 'urn:ietf:params:xml:ns:carddav:addressbook-multiget' => ''
535 * If the content we are receiving is XML then we parse it here. RFC2518 says we
536 * should reasonably expect to see either text/xml or application/xml
538 if ( isset($this->content_type) && preg_match( '#(application|text)/xml#', $this->content_type ) ) {
539 if ( !isset($this->raw_post) || $this->raw_post == '' ) {
540 $this->XMLResponse( 400, new XMLElement( 'error', new XMLElement('missing-xml'), array( 'xmlns' => 'DAV:') ) );
542 $xml_parser = xml_parser_create_ns('UTF-8');
543 $this->xml_tags = array();
544 xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
545 xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
546 $rc = xml_parse_into_struct( $xml_parser, $this->raw_post, $this->xml_tags );
547 if ( $rc == false ) {
548 dbg_error_log( 'ERROR', 'XML parsing error: %s at line %d, column %d',
549 xml_error_string(xml_get_error_code($xml_parser)),
550 xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
551 $this->XMLResponse( 400, new XMLElement( 'error', new XMLElement('invalid-xml'), array( 'xmlns' => 'DAV:') ) );
553 xml_parser_free($xml_parser);
554 if ( count($this->xml_tags) ) {
555 dbg_error_log( "caldav", " Parsed incoming XML request body." );
557 else {
558 $this->xml_tags = null;
559 dbg_error_log( "ERROR", "Incoming request sent content-type XML with no XML request body." );
564 * Look out for If-None-Match or If-Match headers
566 if ( isset($_SERVER["HTTP_IF_NONE_MATCH"]) ) {
567 $this->etag_none_match = $_SERVER["HTTP_IF_NONE_MATCH"];
568 if ( $this->etag_none_match == '' ) unset($this->etag_none_match);
570 if ( isset($_SERVER["HTTP_IF_MATCH"]) ) {
571 $this->etag_if_match = $_SERVER["HTTP_IF_MATCH"];
572 if ( $this->etag_if_match == '' ) unset($this->etag_if_match);
578 * Permissions are controlled as follows:
579 * 1. if the path is '/', the request has read privileges
580 * 2. if the requester is an admin, the request has read/write priviliges
581 * 3. if there is a <user name> component which matches the logged on user
582 * then the request has read/write privileges
583 * 4. otherwise we query the defined relationships between users and use
584 * the minimum privileges returned from that analysis.
586 * @param int $user_no The current user number
589 function setPermissions() {
590 global $c, $session;
592 if ( $this->path == '/' || $this->path == '' ) {
593 $this->privileges = privilege_to_bits( array('read','read-free-busy','read-acl'));
594 dbg_error_log( "caldav", "Full read permissions for user accessing /" );
596 else if ( $session->AllowedTo("Admin") || $session->principal->user_no() == $this->user_no ) {
597 $this->privileges = privilege_to_bits('all');
598 dbg_error_log( "caldav", "Full permissions for %s", ( $session->principal->user_no() == $this->user_no ? "user accessing their own hierarchy" : "a systems administrator") );
600 else {
601 $this->privileges = 0;
602 if ( $this->IsPublic() ) {
603 $this->privileges = privilege_to_bits(array('read','read-free-busy'));
604 dbg_error_log( "caldav", "Basic read permissions for user accessing a public collection" );
606 else if ( isset($c->public_freebusy_url) && $c->public_freebusy_url ) {
607 $this->privileges = privilege_to_bits('read-free-busy');
608 dbg_error_log( "caldav", "Basic free/busy permissions for user accessing a public free/busy URL" );
612 * In other cases we need to query the database for permissions
614 $params = array( ':session_principal_id' => $session->principal->principal_id(), ':scan_depth' => $c->permission_scan_depth );
615 if ( isset($this->by_email) && $this->by_email ) {
616 $sql ='SELECT pprivs( :session_principal_id::int8, :request_principal_id::int8, :scan_depth::int ) AS perm';
617 $params[':request_principal_id'] = $this->principal_id;
619 else {
620 $sql = 'SELECT path_privs( :session_principal_id::int8, :request_path::text, :scan_depth::int ) AS perm';
621 $params[':request_path'] = $this->path;
623 $qry = new AwlQuery( $sql, $params );
624 if ( $qry->Exec('caldav',__LINE__,__FILE__) && $permission_result = $qry->Fetch() )
625 $this->privileges |= bindec($permission_result->perm);
627 dbg_error_log( 'caldav', 'Restricted permissions for user accessing someone elses hierarchy: %s', decbin($this->privileges) );
628 if ( isset($this->ticket) && $this->ticket->MatchesPath($this->path) ) {
629 $this->privileges |= $this->ticket->privileges();
630 dbg_error_log( 'caldav', 'Applying permissions for ticket "%s" now: %s', $this->ticket->id(), decbin($this->privileges) );
634 /** convert privileges into older style permissions */
635 $this->permissions = array();
636 $privs = bits_to_privilege($this->privileges);
637 foreach( $privs AS $k => $v ) {
638 switch( $v ) {
639 case 'DAV::all': $type = 'abstract'; break;
640 case 'DAV::write': $type = 'aggregate'; break;
641 default: $type = 'real';
643 $v = str_replace('DAV::', '', $v);
644 $this->permissions[$v] = $type;
651 * Checks whether the resource is locked, returning any lock token, or false
653 * @todo This logic does not catch all locking scenarios. For example an infinite
654 * depth request should check the permissions for all collections and resources within
655 * that. At present we only maintain permissions on a per-collection basis though.
657 function IsLocked() {
658 if ( !isset($this->_locks_found) ) {
659 $this->_locks_found = array();
661 $sql = 'DELETE FROM locks WHERE (start + timeout) < current_timestamp';
662 $qry = new AwlQuery($sql);
663 $qry->Exec('caldav',__LINE__,__FILE__);
666 * Find the locks that might apply and load them into an array
668 $sql = 'SELECT * FROM locks WHERE :dav_name::text ~ (\'^\'||dav_name||:pattern_end_match)::text';
669 $qry = new AwlQuery($sql, array( ':dav_name' => $this->path, ':pattern_end_match' => ($this->IsInfiniteDepth() ? '' : '$') ) );
670 if ( $qry->Exec('caldav',__LINE__,__FILE__) ) {
671 while( $lock_row = $qry->Fetch() ) {
672 $this->_locks_found[$lock_row->opaquelocktoken] = $lock_row;
675 else {
676 $this->DoResponse(500,translate("Database Error"));
677 // Does not return.
681 foreach( $this->_locks_found AS $lock_token => $lock_row ) {
682 if ( $lock_row->depth == DEPTH_INFINITY || $lock_row->dav_name == $this->path ) {
683 return $lock_token;
687 return false; // Nothing matched
692 * Checks whether the collection is public
694 function IsPublic() {
695 if ( isset($this->collection) && isset($this->collection->publicly_readable) && $this->collection->publicly_readable == 't' ) {
696 return true;
698 return false;
702 private static function supportedPrivileges() {
703 return array(
704 'all' => array(
705 'read' => translate('Read the content of a resource or collection'),
706 'write' => array(
707 'bind' => translate('Create a resource or collection'),
708 'unbind' => translate('Delete a resource or collection'),
709 'write-content' => translate('Write content'),
710 'write-properties' => translate('Write properties')
712 'urn:ietf:params:xml:ns:caldav:read-free-busy' => translate('Read the free/busy information for a calendar collection'),
713 'read-acl' => translate('Read ACLs for a resource or collection'),
714 'read-current-user-privilege-set' => translate('Read the details of the current user\'s access control to this resource.'),
715 'write-acl' => translate('Write ACLs for a resource or collection'),
716 'unlock' => translate('Remove a lock'),
718 'urn:ietf:params:xml:ns:caldav:schedule-deliver' => array(
719 'urn:ietf:params:xml:ns:caldav:schedule-deliver-invite'=> translate('Deliver scheduling invitations from an organiser to this scheduling inbox'),
720 'urn:ietf:params:xml:ns:caldav:schedule-deliver-reply' => translate('Deliver scheduling replies from an attendee to this scheduling inbox'),
721 'urn:ietf:params:xml:ns:caldav:schedule-query-freebusy' => translate('Allow free/busy enquiries targeted at the owner of this scheduling inbox')
724 'urn:ietf:params:xml:ns:caldav:schedule-send' => array(
725 'urn:ietf:params:xml:ns:caldav:schedule-send-invite' => translate('Send scheduling invitations as an organiser from the owner of this scheduling outbox.'),
726 'urn:ietf:params:xml:ns:caldav:schedule-send-reply' => translate('Send scheduling replies as an attendee from the owner of this scheduling outbox.'),
727 'urn:ietf:params:xml:ns:caldav:schedule-send-freebusy' => translate('Send free/busy enquiries')
734 * Returns the dav_name of the resource in our internal namespace
736 function dav_name() {
737 if ( isset($this->path) ) return $this->path;
738 return null;
743 * Returns the name for this depth: 0, 1, infinity
745 function GetDepthName( ) {
746 if ( $this->IsInfiniteDepth() ) return 'infinity';
747 return $this->depth;
751 * Returns the tail of a Regex appropriate for this Depth, when appended to
754 function DepthRegexTail( $for_collection_report = false) {
755 if ( $this->IsInfiniteDepth() ) return '';
756 if ( $this->depth == 0 && $for_collection_report ) return '[^/]+$';
757 if ( $this->depth == 0 ) return '$';
758 return '[^/]*/?$';
762 * Returns the locked row, either from the cache or from the database
764 * @param string $dav_name The resource which we want to know the lock status for
766 function GetLockRow( $lock_token ) {
767 if ( isset($this->_locks_found) && isset($this->_locks_found[$lock_token]) ) {
768 return $this->_locks_found[$lock_token];
771 $qry = new AwlQuery('SELECT * FROM locks WHERE opaquelocktoken = :lock_token', array( ':lock_token' => $lock_token ) );
772 if ( $qry->Exec('caldav',__LINE__,__FILE__) ) {
773 $lock_row = $qry->Fetch();
774 $this->_locks_found = array( $lock_token => $lock_row );
775 return $this->_locks_found[$lock_token];
777 else {
778 $this->DoResponse( 500, translate("Database Error") );
781 return false; // Nothing matched
786 * Checks to see whether the lock token given matches one of the ones handed in
787 * with the request.
789 * @param string $lock_token The opaquelocktoken which we are looking for
791 function ValidateLockToken( $lock_token ) {
792 if ( isset($this->lock_token) && $this->lock_token == $lock_token ) {
793 dbg_error_log( "caldav", "They supplied a valid lock token. Great!" );
794 return true;
796 if ( isset($this->if_clause) ) {
797 dbg_error_log( "caldav", "Checking lock token '%s' against '%s'", $lock_token, $this->if_clause );
798 $tokens = preg_split( '/[<>]/', $this->if_clause );
799 foreach( $tokens AS $k => $v ) {
800 dbg_error_log( "caldav", "Checking lock token '%s' against '%s'", $lock_token, $v );
801 if ( 'opaquelocktoken:' == substr( $v, 0, 16 ) ) {
802 if ( substr( $v, 16 ) == $lock_token ) {
803 dbg_error_log( "caldav", "Lock token '%s' validated OK against '%s'", $lock_token, $v );
804 return true;
809 else {
810 @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 );
813 return false;
818 * Returns the DB object associated with a lock token, or false.
820 * @param string $lock_token The opaquelocktoken which we are looking for
822 function GetLockDetails( $lock_token ) {
823 if ( !isset($this->_locks_found) && false === $this->IsLocked() ) return false;
824 if ( isset($this->_locks_found[$lock_token]) ) return $this->_locks_found[$lock_token];
825 return false;
830 * This will either (a) return false if no locks apply, or (b) return the lock_token
831 * which the request successfully included to open the lock, or:
832 * (c) respond directly to the client with the failure.
834 * @return mixed false (no lock) or opaquelocktoken (opened lock)
836 function FailIfLocked() {
837 if ( $existing_lock = $this->IsLocked() ) { // NOTE Assignment in if() is expected here.
838 dbg_error_log( "caldav", "There is a lock on '%s'", $this->path);
839 if ( ! $this->ValidateLockToken($existing_lock) ) {
840 $lock_row = $this->GetLockRow($existing_lock);
842 * Already locked - deny it
844 $response[] = new XMLElement( 'response', array(
845 new XMLElement( 'href', $lock_row->dav_name ),
846 new XMLElement( 'status', 'HTTP/1.1 423 Resource Locked')
848 if ( $lock_row->dav_name != $this->path ) {
849 $response[] = new XMLElement( 'response', array(
850 new XMLElement( 'href', $this->path ),
851 new XMLElement( 'propstat', array(
852 new XMLElement( 'prop', new XMLElement( 'lockdiscovery' ) ),
853 new XMLElement( 'status', 'HTTP/1.1 424 Failed Dependency')
857 $response = new XMLElement( "multistatus", $response, array('xmlns'=>'DAV:') );
858 $xmldoc = $response->Render(0,'<?xml version="1.0" encoding="utf-8" ?>');
859 $this->DoResponse( 207, $xmldoc, 'text/xml; charset="utf-8"' );
860 // Which we won't come back from
862 return $existing_lock;
864 return false;
869 * Coerces the Content-type of the request into something valid/appropriate
871 function CoerceContentType() {
872 if ( isset($this->content_type) ) {
873 $type = explode( '/', $this->content_type, 2);
874 /** @todo: Perhaps we should look at the target collection type, also. */
875 if ( $type[0] == 'text' ) {
876 if ( !empty($type[1]) && ($type[1] == 'vcard' || $type[1] == 'calendar' || $type[1] == 'x-vcard') ) {
877 return;
882 /** Null (or peculiar) content-type supplied so we have to try and work it out... */
883 $first_word = trim(substr( $this->raw_post, 0, 30));
884 $first_word = strtoupper( preg_replace( '/\s.*/s', '', $first_word ) );
885 switch( $first_word ) {
886 case '<?XML':
887 dbg_error_log( 'LOG WARNING', 'Application sent content-type of "%s" instead of "text/xml"',
888 (isset($this->content_type)?$this->content_type:'(null)') );
889 $this->content_type = 'text/xml';
890 break;
891 case 'BEGIN:VCALENDAR':
892 dbg_error_log( 'LOG WARNING', 'Application sent content-type of "%s" instead of "text/calendar"',
893 (isset($this->content_type)?$this->content_type:'(null)') );
894 $this->content_type = 'text/calendar';
895 break;
896 case 'BEGIN:VCARD':
897 dbg_error_log( 'LOG WARNING', 'Application sent content-type of "%s" instead of "text/vcard"',
898 (isset($this->content_type)?$this->content_type:'(null)') );
899 $this->content_type = 'text/vcard';
900 break;
901 default:
902 dbg_error_log( 'LOG NOTICE', 'Unusual content-type of "%s" and first word of content is "%s"',
903 (isset($this->content_type)?$this->content_type:'(null)'), $first_word );
905 if ( empty($this->content_type) ) $this->content_type = 'text/plain';
910 * Returns true if the 'Prefer: return-minimal' or 'Brief: t' were present in the request headers.
912 function PreferMinimal() {
913 if ( empty($this->prefer) ) return false;
914 foreach( $this->prefer AS $v ) {
915 if ( $v == 'return-minimal' ) return true;
917 return false;
921 * Returns true if the URL referenced by this request points at a collection.
923 function IsCollection( ) {
924 if ( !isset($this->_is_collection) ) {
925 $this->_is_collection = preg_match( '#/$#', $this->path );
927 return $this->_is_collection;
932 * Returns true if the URL referenced by this request points at a calendar collection.
934 function IsCalendar( ) {
935 if ( !$this->IsCollection() || !isset($this->collection) ) return false;
936 return $this->collection->is_calendar == 't';
941 * Returns true if the URL referenced by this request points at an addressbook collection.
943 function IsAddressBook( ) {
944 if ( !$this->IsCollection() || !isset($this->collection) ) return false;
945 return $this->collection->is_addressbook == 't';
950 * Returns true if the URL referenced by this request points at a principal.
952 function IsPrincipal( ) {
953 if ( !isset($this->_is_principal) ) {
954 $this->_is_principal = preg_match( '#^/[^/]+/$#', $this->path );
956 return $this->_is_principal;
961 * Returns true if the URL referenced by this request is within a proxy URL
963 function IsProxyRequest( ) {
964 if ( !isset($this->_is_proxy_request) ) {
965 $this->_is_proxy_request = preg_match( '#^/[^/]+/calendar-proxy-(read|write)/?[^/]*$#', $this->path );
967 return $this->_is_proxy_request;
972 * Returns true if the request asked for infinite depth
974 function IsInfiniteDepth( ) {
975 return ($this->depth == DEPTH_INFINITY);
980 * Returns the ID of the collection of, or containing this request
982 function CollectionId( ) {
983 return $this->collection_id;
988 * Returns the array of supported privileges converted into XMLElements
990 function BuildSupportedPrivileges( &$reply, $privs = null ) {
991 $privileges = array();
992 if ( $privs === null ) $privs = self::supportedPrivileges();
993 foreach( $privs AS $k => $v ) {
994 dbg_error_log( 'caldav', 'Adding privilege "%s" which is "%s".', $k, $v );
995 $privilege = new XMLElement('privilege');
996 $reply->NSElement($privilege,$k);
997 $privset = array($privilege);
998 if ( is_array($v) ) {
999 dbg_error_log( 'caldav', '"%s" is a container of sub-privileges.', $k );
1000 $privset = array_merge($privset, $this->BuildSupportedPrivileges($reply,$v));
1002 else if ( $v == 'abstract' ) {
1003 dbg_error_log( 'caldav', '"%s" is an abstract privilege.', $v );
1004 $privset[] = new XMLElement('abstract');
1006 else if ( strlen($v) > 1 ) {
1007 $privset[] = new XMLElement('description', $v);
1009 $privileges[] = new XMLElement('supported-privilege',$privset);
1011 return $privileges;
1016 * Are we allowed to do the requested activity
1018 * +------------+------------------------------------------------------+
1019 * | METHOD | PRIVILEGES |
1020 * +------------+------------------------------------------------------+
1021 * | MKCALENDAR | DAV:bind |
1022 * | REPORT | DAV:read or CALDAV:read-free-busy (on all referenced |
1023 * | | resources) |
1024 * +------------+------------------------------------------------------+
1026 * @param string $activity The activity we want to do.
1028 function AllowedTo( $activity ) {
1029 global $session;
1030 dbg_error_log('caldav', 'Checking whether "%s" is allowed to "%s"', $session->principal->username(), $activity);
1031 if ( isset($this->permissions['all']) ) return true;
1032 switch( $activity ) {
1033 case 'all':
1034 return false; // If they got this far then they don't
1035 break;
1037 case "CALDAV:schedule-send-freebusy":
1038 return isset($this->permissions['read']) || isset($this->permissions['urn:ietf:params:xml:ns:caldav:read-free-busy']);
1039 break;
1041 case "CALDAV:schedule-send-invite":
1042 return isset($this->permissions['read']) || isset($this->permissions['urn:ietf:params:xml:ns:caldav:read-free-busy']);
1043 break;
1045 case "CALDAV:schedule-send-reply":
1046 return isset($this->permissions['read']) || isset($this->permissions['urn:ietf:params:xml:ns:caldav:read-free-busy']);
1047 break;
1049 case 'freebusy':
1050 return isset($this->permissions['read']) || isset($this->permissions['urn:ietf:params:xml:ns:caldav:read-free-busy']);
1051 break;
1053 case 'delete':
1054 return isset($this->permissions['write']) || isset($this->permissions['unbind']);
1055 break;
1057 case 'proppatch':
1058 return isset($this->permissions['write']) || isset($this->permissions['write-properties']);
1059 break;
1061 case 'modify':
1062 return isset($this->permissions['write']) || isset($this->permissions['write-content']);
1063 break;
1065 case 'create':
1066 return isset($this->permissions['write']) || isset($this->permissions['bind']);
1067 break;
1069 case 'mkcalendar':
1070 case 'mkcol':
1071 if ( !isset($this->permissions['write']) || !isset($this->permissions['bind']) ) return false;
1072 if ( $this->is_principal ) return false;
1073 if ( $this->path == '/' ) return false;
1074 break;
1076 default:
1077 $test_bits = privilege_to_bits( $activity );
1078 // dbg_error_log( 'caldav', 'request::AllowedTo("%s") (%s) against allowed "%s" => "%s" (%s)',
1079 // (is_array($activity) ? implode(',',$activity) : $activity), decbin($test_bits),
1080 // decbin($this->privileges), ($this->privileges & $test_bits), decbin($this->privileges & $test_bits) );
1081 return (($this->privileges & $test_bits) > 0 );
1082 break;
1085 return false;
1091 * Return the privileges bits for the current session user to this resource
1093 function Privileges() {
1094 return $this->privileges;
1099 * Check that the incoming Etag matches the one for the existing (or non-existing) resource.
1101 * @param boolean $exists Whether the destination exists
1102 * @param string $dest_etag The etag for the destination.
1104 function CheckEtagMatch( $exists, $dest_etag ) {
1105 global $c;
1107 if ( ! $exists ) {
1108 if ( (isset($this->etag_if_match) && $this->etag_if_match != '') ) {
1110 * RFC2068, 14.25:
1111 * If none of the entity tags match, or if "*" is given and no current
1112 * entity exists, the server MUST NOT perform the requested method, and
1113 * MUST return a 412 (Precondition Failed) response.
1115 $this->PreconditionFailed(412, 'if-match', translate('No resource exists at the destination.'));
1118 else {
1120 if ( isset($c->strict_etag_checking) && $c->strict_etag_checking )
1121 $trim_chars = '\'\\" ';
1122 else
1123 $trim_chars = ' ';
1125 if ( isset($this->etag_if_match) && $this->etag_if_match != '' && trim( $this->etag_if_match, $trim_chars) != trim( $dest_etag, $trim_chars ) ) {
1127 * RFC2068, 14.25:
1128 * If none of the entity tags match, or if "*" is given and no current
1129 * entity exists, the server MUST NOT perform the requested method, and
1130 * MUST return a 412 (Precondition Failed) response.
1132 $this->PreconditionFailed(412,'if-match',sprintf('Existing resource ETag of <<%s>> does not match <<%s>>', $dest_etag, $this->etag_if_match) );
1134 else if ( isset($this->etag_none_match) && $this->etag_none_match != ''
1135 && ($this->etag_none_match == $dest_etag || $this->etag_none_match == '*') ) {
1137 * RFC2068, 14.26:
1138 * If any of the entity tags match the entity tag of the entity that
1139 * would have been returned in the response to a similar GET request
1140 * (without the If-None-Match header) on that resource, or if "*" is
1141 * given and any current entity exists for that resource, then the
1142 * server MUST NOT perform the requested method.
1144 $this->PreconditionFailed(412,'if-none-match', translate( 'Existing resource matches "If-None-Match" header - not accepted.'));
1152 * Is the user has the privileges to do what is requested.
1154 function HavePrivilegeTo( $do_what ) {
1155 $test_bits = privilege_to_bits( $do_what );
1156 // dbg_error_log( 'caldav', 'request::HavePrivilegeTo("%s") [%s] against allowed "%s" => "%s" (%s)',
1157 // (is_array($do_what) ? implode(',',$do_what) : $do_what), decbin($test_bits),
1158 // decbin($this->privileges), ($this->privileges & $test_bits), decbin($this->privileges & $test_bits) );
1159 return ($this->privileges & $test_bits) > 0;
1164 * Sometimes it's a perfectly formed request, but we just don't do that :-(
1165 * @param array $unsupported An array of the properties we don't support.
1167 function UnsupportedRequest( $unsupported ) {
1168 if ( isset($unsupported) && count($unsupported) > 0 ) {
1169 $badprops = new XMLElement( "prop" );
1170 foreach( $unsupported AS $k => $v ) {
1171 // Not supported at this point...
1172 dbg_error_log("ERROR", " %s: Support for $v:$k properties is not implemented yet", $this->method );
1173 $badprops->NewElement(strtolower($k),false,array("xmlns" => strtolower($v)));
1175 $error = new XMLElement("error", $badprops, array("xmlns" => "DAV:") );
1177 $this->XMLResponse( 422, $error );
1183 * Send a need-privileges error response. This function will only return
1184 * if the $href is not supplied and the current user has the specified
1185 * permission for the request path.
1187 * @param string $privilege The name of the needed privilege.
1188 * @param string $href The unconstructed URI where we needed the privilege.
1190 function NeedPrivilege( $privileges, $href=null ) {
1191 if ( is_string($privileges) ) $privileges = array( $privileges );
1192 if ( !isset($href) ) {
1193 if ( $this->HavePrivilegeTo($privileges) ) return;
1194 $href = $this->path;
1197 $reply = new XMLDocument( array('DAV:' => '') );
1198 $privnodes = array( $reply->href(ConstructURL($href)), new XMLElement( 'privilege' ) );
1199 // RFC3744 specifies that we can only respond with one needed privilege, so we pick the first.
1200 $reply->NSElement( $privnodes[1], $privileges[0] );
1201 $xml = new XMLElement( 'need-privileges', new XMLElement( 'resource', $privnodes) );
1202 $xmldoc = $reply->Render('error',$xml);
1203 $this->DoResponse( 403, $xmldoc, 'text/xml; charset="utf-8"' );
1204 exit(0); // Unecessary, but might clarify things
1209 * Send an error response for a failed precondition.
1211 * @param int $status The status code for the failed precondition. Normally 403
1212 * @param string $precondition The namespaced precondition tag.
1213 * @param string $explanation An optional text explanation for the failure.
1215 function PreconditionFailed( $status, $precondition, $explanation = '', $xmlns='DAV:') {
1216 $xmldoc = sprintf('<?xml version="1.0" encoding="utf-8" ?>
1217 <error xmlns="%s">
1218 <%s/>%s
1219 </error>', $xmlns, str_replace($xmlns.':', '', $precondition), $explanation );
1221 $this->DoResponse( $status, $xmldoc, 'text/xml; charset="utf-8"' );
1222 exit(0); // Unecessary, but might clarify things
1227 * Send a simple error informing the client that was a malformed request
1229 * @param string $text An optional text description of the failure.
1231 function MalformedRequest( $text = 'Bad request' ) {
1232 $this->DoResponse( 400, $text );
1233 exit(0); // Unecessary, but might clarify things
1238 * Send an XML Response. This function will never return.
1240 * @param int $status The HTTP status to respond
1241 * @param XMLElement $xmltree An XMLElement tree to be rendered
1243 function XMLResponse( $status, $xmltree ) {
1244 $xmldoc = $xmltree->Render(0,'<?xml version="1.0" encoding="utf-8" ?>');
1245 $etag = md5($xmldoc);
1246 if ( !headers_sent() ) header("ETag: \"$etag\"");
1247 $this->DoResponse( $status, $xmldoc, 'text/xml; charset="utf-8"' );
1248 exit(0); // Unecessary, but might clarify things
1252 * Utility function we call when we have a simple status-based response to
1253 * return to the client. Possibly
1255 * @param int $status The HTTP status code to send.
1256 * @param string $message The friendly text message to send with the response.
1258 function DoResponse( $status, $message="", $content_type="text/plain; charset=\"utf-8\"" ) {
1259 global $session, $c;
1260 if ( !headers_sent() ) @header( sprintf("HTTP/1.1 %d %s", $status, getStatusMessage($status)) );
1261 if ( !headers_sent() ) @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) );
1262 if ( !headers_sent() ) header( "Content-type: ".$content_type );
1264 if ( (isset($c->dbg['ALL']) && $c->dbg['ALL']) || (isset($c->dbg['response']) && $c->dbg['response'])
1265 || $status == 400 || $status == 402 || $status == 403 || $status > 404 ) {
1266 @dbg_error_log( "LOG ", 'Response status %03d for %s %s', $status, $this->method, $_SERVER['REQUEST_URI'] );
1267 $lines = headers_list();
1268 dbg_error_log( "LOG ", "***************** Response Header ****************" );
1269 foreach( $lines AS $v ) {
1270 dbg_error_log( "LOG headers", "-->%s", $v );
1272 dbg_error_log( "LOG ", "******************** Response ********************" );
1273 // Log the request in all it's gory detail.
1274 $lines = preg_split( '#[\r\n]+#', $message);
1275 foreach( $lines AS $v ) {
1276 dbg_error_log( "LOG response", "-->%s", $v );
1280 if ( $message != '' ) {
1281 if ( !headers_sent() ) header( "Content-Length: ".strlen($message) );
1282 echo $message;
1285 if ( isset($c->dbg['caldav']) && $c->dbg['caldav'] ) {
1286 if ( strlen($message) > 100 || strstr($message, "\n") ) {
1287 $message = substr( preg_replace("#\s+#m", ' ', $message ), 0, 100) . (strlen($message) > 100 ? "..." : "");
1290 dbg_error_log("caldav", "Status: %d, Message: %s, User: %d, Path: %s", $status, $message, $session->principal->user_no(), $this->path);
1292 if ( isset($c->dbg['statistics']) && $c->dbg['statistics'] ) {
1293 $script_time = microtime(true) - $c->script_start_time;
1294 @dbg_error_log("statistics", "Method: %s, Status: %d, Script: %5.3lfs, Queries: %5.3lfs, URL: %s",
1295 $this->method, $status, $script_time, $c->total_query_time, $this->path);
1297 while ( ob_get_level() > 0 ) ob_end_flush();
1298 exit(0);