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);
60 var $current_user_principal_xml;
97 protected $privileges;
102 var $supported_privileges;
118 function __construct( $options = array() ) {
119 global $session, $c, $debugging;
121 $this->options = $options;
122 if ( !isset($this->options[
'allow_by_email']) ) $this->options[
'allow_by_email'] =
false;
124 if ( isset($_SERVER[
'HTTP_PREFER']) ) {
125 $this->prefer = explode(
',', $_SERVER[
'HTTP_PREFER']);
127 else if ( isset($_SERVER[
'HTTP_BRIEF']) && (strtoupper($_SERVER[
'HTTP_BRIEF']) ==
'T') ) {
128 $this->prefer = array(
'return=minimal');
131 $this->prefer = array();
146 if ( isset($_SERVER[
'PATH_INFO']) ) {
147 $this->path = $_SERVER[
'PATH_INFO'];
151 if ( isset($_SERVER[
'REQUEST_URI']) ) {
152 if ( preg_match(
'{^(.*?\.php)([^?]*)}', $_SERVER[
'REQUEST_URI'], $matches ) ) {
153 $this->path = $matches[2];
154 if ( substr($this->path,0,1) !=
'/' )
155 $this->path =
'/'.$this->path;
157 else if ( $_SERVER[
'REQUEST_URI'] !=
'/' ) {
158 dbg_error_log(
'LOG',
'Server is not supplying PATH_INFO and REQUEST_URI does not include a PHP program. Wildly guessing "/"!!!');
162 $this->path = rawurldecode($this->path);
165 if ( preg_match(
'#^(/[^/]+/[^/]+).ics$#', $this->path, $matches ) ) {
166 $this->path = $matches[1].
'/';
169 if ( isset($c->replace_path) && isset($c->replace_path[
'from']) && isset($c->replace_path[
'to']) ) {
170 $this->path = preg_replace($c->replace_path[
'from'], $c->replace_path[
'to'], $this->path);
174 $bad_chars_regex =
'/[\\^\\[\\(\\\\]/';
175 if ( preg_match( $bad_chars_regex, $this->path ) ) {
176 $this->DoResponse( 400, translate(
"The calendar path contains illegal characters.") );
178 if ( strstr($this->path,
'//') ) $this->path = preg_replace(
'#//+#',
'/', $this->path);
180 if ( !isset($c->raw_post) ) $c->raw_post = file_get_contents(
'php://input');
181 if ( isset($_SERVER[
'HTTP_CONTENT_ENCODING']) ) {
182 $encoding = $_SERVER[
'HTTP_CONTENT_ENCODING'];
183 @dbg_error_log(
'caldav',
'Content-Encoding: %s', $encoding );
184 $encoding = preg_replace(
'{[^a-z0-9-]}i',
'',$encoding);
185 if ( ! ini_get(
'open_basedir') && (isset($c->dbg[
'ALL']) || isset($c->dbg[
'caldav'])) ) {
186 $fh = fopen(
'/var/log/davical/encoded_data.debug'.$encoding,
'w');
188 fwrite($fh,$c->raw_post);
192 switch( $encoding ) {
194 $this->raw_post = @gzdecode($c->raw_post);
197 $this->raw_post = @gzinflate($c->raw_post);
200 $this->raw_post = @gzuncompress($c->raw_post);
204 if ( empty($this->raw_post) && !empty($c->raw_post) ) {
205 $this->PreconditionFailed(415,
'content-encoding', sprintf(
'Unable to decode "%s" content encoding.', $_SERVER[
'HTTP_CONTENT_ENCODING']));
207 $c->raw_post = $this->raw_post;
210 $this->raw_post = $c->raw_post;
213 if ( isset($debugging) && isset($_GET[
'method']) ) {
214 $_SERVER[
'REQUEST_METHOD'] = $_GET[
'method'];
216 else if ( $_SERVER[
'REQUEST_METHOD'] ==
'POST' && isset($_SERVER[
'HTTP_X_HTTP_METHOD_OVERRIDE']) ){
217 $_SERVER[
'REQUEST_METHOD'] = $_SERVER[
'HTTP_X_HTTP_METHOD_OVERRIDE'];
219 $this->method = $_SERVER[
'REQUEST_METHOD'];
220 $this->content_type = (isset($_SERVER[
'CONTENT_TYPE']) ? $_SERVER[
'CONTENT_TYPE'] :
null);
221 if ( preg_match(
'{^(\S+/\S+?)\s*(;.*)?$}', $this->content_type, $matches ) ) {
222 $this->content_type = $matches[1];
224 if ( strlen($c->raw_post) > 0 ) {
225 if ( $this->method ==
'PROPFIND' || $this->method ==
'REPORT' || $this->method ==
'PROPPATCH' || $this->method ==
'BIND' || $this->method ==
'MKTICKET' || $this->method ==
'ACL' ) {
226 if ( !preg_match(
'{^(text|application)/xml$}', $this->content_type ) ) {
227 @dbg_error_log(
"LOG request",
'Request is "%s" but client set content-type to "%s". Assuming they meant XML!',
228 $this->method, $this->content_type );
229 $this->content_type =
'text/xml';
232 else if ( $this->method ==
'PUT' || $this->method ==
'POST' ) {
233 $this->CoerceContentType();
237 $this->content_type =
'text/plain';
239 $this->user_agent = ((isset($_SERVER[
'HTTP_USER_AGENT']) ? $_SERVER[
'HTTP_USER_AGENT'] :
"Probably Mulberry"));
244 if ( isset($_SERVER[
'HTTP_DEPTH']) ) {
245 $this->depth = $_SERVER[
'HTTP_DEPTH'];
253 switch( $this->method ) {
258 $this->depth =
'infinity';
270 if ( !is_int($this->depth) &&
"infinity" == $this->depth ) $this->depth = DEPTH_INFINITY;
271 $this->depth = intval($this->depth);
276 if ( isset($_SERVER[
'HTTP_DESTINATION']) ) {
277 $this->destination = $_SERVER[
'HTTP_DESTINATION'];
278 if ( preg_match(
'{^(https?)://([a-z.-]+)(:[0-9]+)?(/.*)$}', $this->destination, $matches ) ) {
279 $this->destination = $matches[4];
282 $this->overwrite = ( isset($_SERVER[
'HTTP_OVERWRITE']) && ($_SERVER[
'HTTP_OVERWRITE'] ==
'F') ?
false :
true );
287 if ( isset($_SERVER[
'HTTP_IF']) ) $this->if_clause = $_SERVER[
'HTTP_IF'];
288 if ( isset($_SERVER[
'HTTP_LOCK_TOKEN']) && preg_match(
'#[<]opaquelocktoken:(.*)[>]#', $_SERVER[
'HTTP_LOCK_TOKEN'], $matches ) ) {
289 $this->lock_token = $matches[1];
295 if ( isset($_GET[
'ticket']) ) {
296 $this->ticket =
new DAVTicket($_GET[
'ticket']);
298 else if ( isset($_SERVER[
'HTTP_TICKET']) ) {
299 $this->ticket =
new DAVTicket($_SERVER[
'HTTP_TICKET']);
305 if ( isset($_SERVER[
'HTTP_TIMEOUT']) ) {
306 $timeouts = explode(
',', $_SERVER[
'HTTP_TIMEOUT'] );
307 foreach( $timeouts AS $k => $v ) {
308 if ( strtolower($v) ==
'infinite' ) {
309 $this->timeout = (isset($c->maximum_lock_timeout) ? $c->maximum_lock_timeout : 86400 * 100);
312 elseif ( strtolower(substr($v,0,7)) ==
'second-' ) {
313 $this->timeout = min( intval(substr($v,7)), (isset($c->maximum_lock_timeout) ? $c->maximum_lock_timeout : 86400 * 100) );
317 if ( ! isset($this->timeout) || $this->timeout == 0 ) $this->timeout = (isset($c->default_lock_timeout) ? $c->default_lock_timeout : 900);
320 $this->principal =
new Principal(
'path',$this->path);
333 $sql =
"SELECT * FROM collection WHERE dav_name = :exact_name";
334 $params = array(
':exact_name' => $this->path );
335 if ( !preg_match(
'#/$#', $this->path ) ) {
336 $sql .=
" OR dav_name = :truncated_name OR dav_name = :trailing_slash_name";
337 $params[
':truncated_name'] = preg_replace(
'#[^/]*$#',
'', $this->path);
338 $params[
':trailing_slash_name'] = $this->path.
"/";
340 $sql .=
" ORDER BY LENGTH(dav_name) DESC LIMIT 1";
341 $qry =
new AwlQuery( $sql, $params );
342 if ( $qry->Exec(
'caldav',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
343 if ( $row->dav_name == $this->path.
"/" ) {
344 $this->path = $row->dav_name;
345 dbg_error_log(
"caldav",
"Path is actually a collection - sending Content-Location header." );
346 header(
"Content-Location: ".ConstructURL($this->path) );
349 $this->collection_id = $row->collection_id;
350 $this->collection_path = $row->dav_name;
351 $this->collection_type = ($row->is_calendar ==
't' ?
'calendar' :
'collection');
352 $this->collection = $row;
353 if ( preg_match(
'#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->path, $matches ) ) {
354 $this->collection_type =
'schedule-'. $matches[3].
'box';
356 $this->collection->type = $this->collection_type;
358 else if ( preg_match(
'{^( ( / ([^/]+) / ) \.(in|out)/ ) [^/]*$}x', $this->path, $matches ) ) {
360 $params = array(
':username' => $matches[3],
':parent_container' => $matches[2],
':dav_name' => $matches[1] );
361 $params[
':boxname'] = ($matches[4] ==
'in' ?
' Inbox' :
' Outbox');
362 $this->collection_type =
'schedule-'. $matches[4].
'box';
363 $params[
':resourcetypes'] = sprintf(
'<DAV::collection/><urn:ietf:params:xml:ns:caldav:%s/>', $this->collection_type );
365 INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified, dav_etag, resourcetypes )
366 VALUES( (SELECT user_no FROM usr WHERE username = text(:username)),
367 :parent_container, :dav_name,
368 (SELECT fullname FROM usr WHERE username = text(:username)) || :boxname,
369 FALSE, current_timestamp, current_timestamp,
'1', :resourcetypes )
372 $qry =
new AwlQuery( $sql, $params );
373 $qry->Exec(
'caldav',__LINE__,__FILE__);
374 dbg_error_log(
'caldav',
'Created new collection as "%s".', trim($params[
':boxname']) );
377 $cache = getCacheInstance();
378 $cache->delete(
'collection-'.$params[
':dav_name'],
null );
379 $cache->delete(
'principal-'.$params[
':parent_container'],
null );
381 $qry =
new AwlQuery(
"SELECT * FROM collection WHERE dav_name = :dav_name", array(
':dav_name' => $matches[1] ) );
382 if ( $qry->Exec(
'caldav',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
383 $this->collection_id = $row->collection_id;
384 $this->collection_path = $matches[1];
385 $this->collection = $row;
386 $this->collection->type = $this->collection_type;
389 else if ( preg_match(
'#^((/[^/]+/)calendar-proxy-(read|write))/?[^/]*$#', $this->path, $matches ) ) {
390 $this->collection_type =
'proxy';
391 $this->_is_proxy_request =
true;
392 $this->proxy_type = $matches[3];
393 $this->collection_path = $matches[1].
'/';
394 if ( $this->collection_path == $this->path.
"/" ) {
396 dbg_error_log(
"caldav",
"Path is actually a (proxy) collection - sending Content-Location header." );
397 header(
"Content-Location: ".ConstructURL($this->path) );
400 else if ( $this->options[
'allow_by_email'] && preg_match(
'#^/(\S+@\S+[.]\S+)/?$#', $this->path) ) {
402 $this->collection_id = -1;
403 $this->collection_type =
'email';
404 $this->collection_path = $this->path;
405 $this->_is_principal =
true;
407 else if ( preg_match(
'#^(/[^/?]+)/?$#', $this->path, $matches) || preg_match(
'#^(/principals/[^/]+/[^/]+)/?$#', $this->path, $matches) ) {
408 $this->collection_id = -1;
409 $this->collection_path = $matches[1].
'/';
410 $this->collection_type =
'principal';
411 $this->_is_principal =
true;
412 if ( $this->collection_path == $this->path.
"/" ) {
414 dbg_error_log(
"caldav",
"Path is actually a collection - sending Content-Location header." );
415 header(
"Content-Location: ".ConstructURL($this->path) );
417 if ( preg_match(
'#^(/principals/[^/]+/[^/]+)/?$#', $this->path, $matches) ) {
422 else if ( $this->path ==
'/' ) {
423 $this->collection_id = -1;
424 $this->collection_path =
'/';
425 $this->collection_type =
'root';
428 if ( $this->collection_path == $this->path ) $this->_is_collection =
true;
429 dbg_error_log(
"caldav",
" Collection '%s' is %d, type %s", $this->collection_path, $this->collection_id, $this->collection_type );
434 $this->principal =
new DAVPrincipal( array(
"path" => $this->path,
"options" => $this->options ) );
435 $this->user_no = $this->principal->user_no();
436 $this->username = $this->principal->username();
437 $this->by_email = $this->principal->byEmail();
438 $this->principal_id = $this->principal->principal_id();
440 if ( $this->collection_type ==
'principal' || $this->collection_type ==
'email' || $this->collection_type ==
'proxy' ) {
441 $this->collection = $this->principal->AsCollection();
442 if( $this->collection_type ==
'proxy' ) {
443 $this->collection->is_proxy =
't';
444 $this->collection->type =
'proxy';
445 $this->collection->proxy_type = $this->proxy_type;
446 $this->collection->dav_displayname = sprintf(
'Proxy %s for %s', $this->proxy_type, $this->principal->username() );
449 elseif( $this->collection_type ==
'root' ) {
450 $this->collection = (object) array(
451 'collection_id' => 0,
453 'dav_etag' => md5($c->system_name),
454 'is_calendar' =>
'f',
455 'is_addressbook' =>
'f',
456 'is_principal' =>
'f',
458 'dav_displayname' => $c->system_name,
460 'created' => date(
'Ymd\THis')
467 $this->setPermissions();
474 if ( isset($this->content_type) && preg_match(
'#(application|text)/xml#', $this->content_type ) ) {
475 if ( !isset($this->raw_post) || $this->raw_post ==
'' ) {
476 $this->XMLResponse( 400,
new XMLElement(
'error',
new XMLElement(
'missing-xml'), array(
'xmlns' =>
'DAV:') ) );
478 $xml_parser = xml_parser_create_ns(
'UTF-8');
479 $this->xml_tags = array();
480 xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
481 xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
482 $rc = xml_parse_into_struct( $xml_parser, $this->raw_post, $this->xml_tags );
483 if ( $rc ==
false ) {
484 dbg_error_log(
'ERROR',
'XML parsing error: %s at line %d, column %d',
485 xml_error_string(xml_get_error_code($xml_parser)),
486 xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
487 $this->XMLResponse( 400,
new XMLElement(
'error',
new XMLElement(
'invalid-xml'), array(
'xmlns' =>
'DAV:') ) );
489 xml_parser_free($xml_parser);
490 if ( count($this->xml_tags) ) {
491 dbg_error_log(
"caldav",
" Parsed incoming XML request body." );
494 $this->xml_tags =
null;
495 dbg_error_log(
"ERROR",
"Incoming request sent content-type XML with no XML request body." );
502 if ( isset($_SERVER[
"HTTP_IF_NONE_MATCH"]) ) {
503 $this->etag_none_match = $_SERVER[
"HTTP_IF_NONE_MATCH"];
504 if ( $this->etag_none_match ==
'' ) unset($this->etag_none_match);
506 if ( isset($_SERVER[
"HTTP_IF_MATCH"]) ) {
507 $this->etag_if_match = $_SERVER[
"HTTP_IF_MATCH"];
508 if ( $this->etag_if_match ==
'' ) unset($this->etag_if_match);
525 function setPermissions() {
528 if ( $this->path ==
'/' || $this->path ==
'' ) {
529 $this->privileges = privilege_to_bits( array(
'read',
'read-free-busy',
'read-acl'));
530 dbg_error_log(
"caldav",
"Full read permissions for user accessing /" );
532 else if ( $session->AllowedTo(
"Admin") || $session->principal->user_no() == $this->user_no ) {
533 $this->privileges = privilege_to_bits(
'all');
534 dbg_error_log(
"caldav",
"Full permissions for %s", ( $session->principal->user_no() == $this->user_no ?
"user accessing their own hierarchy" :
"a systems administrator") );
537 $this->privileges = 0;
538 if ( $this->IsPublic() ) {
539 $this->privileges = privilege_to_bits(array(
'read',
'read-free-busy'));
540 dbg_error_log(
"caldav",
"Basic read permissions for user accessing a public collection" );
542 else if ( isset($c->public_freebusy_url) && $c->public_freebusy_url ) {
543 $this->privileges = privilege_to_bits(
'read-free-busy');
544 dbg_error_log(
"caldav",
"Basic free/busy permissions for user accessing a public free/busy URL" );
550 $params = array(
':session_principal_id' => $session->principal->principal_id(),
':scan_depth' => $c->permission_scan_depth );
551 if ( isset($this->by_email) && $this->by_email ) {
552 $sql =
'SELECT pprivs( :session_principal_id::int8, :request_principal_id::int8, :scan_depth::int ) AS perm';
553 $params[
':request_principal_id'] = $this->principal_id;
556 $sql =
'SELECT path_privs( :session_principal_id::int8, :request_path::text, :scan_depth::int ) AS perm';
557 $params[
':request_path'] = $this->path;
559 $qry =
new AwlQuery( $sql, $params );
560 if ( $qry->Exec(
'caldav',__LINE__,__FILE__) && $permission_result = $qry->Fetch() )
561 $this->privileges |= bindec($permission_result->perm);
563 dbg_error_log(
'caldav',
'Restricted permissions for user accessing someone elses hierarchy: %s', decbin($this->privileges) );
564 if ( isset($this->ticket) && $this->ticket->MatchesPath($this->path) ) {
565 $this->privileges |= $this->ticket->privileges();
566 dbg_error_log(
'caldav',
'Applying permissions for ticket "%s" now: %s', $this->ticket->id(), decbin($this->privileges) );
571 $this->permissions = array();
572 $privs = bits_to_privilege($this->privileges);
573 foreach( $privs AS $k => $v ) {
575 case 'DAV::all': $type =
'abstract';
break;
576 case 'DAV::write': $type =
'aggregate';
break;
577 default: $type =
'real';
579 $v = str_replace(
'DAV::',
'', $v);
580 $this->permissions[$v] = $type;
593 function IsLocked() {
594 if ( !isset($this->_locks_found) ) {
595 $this->_locks_found = array();
597 $sql =
'DELETE FROM locks WHERE (start + timeout) < current_timestamp';
598 $qry =
new AwlQuery($sql);
599 $qry->Exec(
'caldav',__LINE__,__FILE__);
604 $sql =
'SELECT * FROM locks WHERE :dav_name::text ~ (\'^\'||dav_name||:pattern_end_match)::text';
605 $qry =
new AwlQuery($sql, array(
':dav_name' => $this->path,
':pattern_end_match' => ($this->IsInfiniteDepth() ?
'' :
'$') ) );
606 if ( $qry->Exec(
'caldav',__LINE__,__FILE__) ) {
607 while( $lock_row = $qry->Fetch() ) {
608 $this->_locks_found[$lock_row->opaquelocktoken] = $lock_row;
612 $this->DoResponse(500,translate(
"Database Error"));
617 foreach( $this->_locks_found AS $lock_token => $lock_row ) {
618 if ( $lock_row->depth == DEPTH_INFINITY || $lock_row->dav_name == $this->path ) {
630 function IsPublic() {
631 if ( isset($this->collection) && isset($this->collection->publicly_readable) && $this->collection->publicly_readable ==
't' ) {
638 private static function supportedPrivileges() {
641 'read' => translate(
'Read the content of a resource or collection'),
643 'bind' => translate(
'Create a resource or collection'),
644 'unbind' => translate(
'Delete a resource or collection'),
645 'write-content' => translate(
'Write content'),
646 'write-properties' => translate(
'Write properties')
648 'urn:ietf:params:xml:ns:caldav:read-free-busy' => translate(
'Read the free/busy information for a calendar collection'),
649 'read-acl' => translate(
'Read ACLs for a resource or collection'),
650 'read-current-user-privilege-set' => translate(
'Read the details of the current user\'s access control to this resource.'),
651 'write-acl' => translate(
'Write ACLs for a resource or collection'),
652 'unlock' => translate(
'Remove a lock'),
654 'urn:ietf:params:xml:ns:caldav:schedule-deliver' => array(
655 'urn:ietf:params:xml:ns:caldav:schedule-deliver-invite'=> translate(
'Deliver scheduling invitations from an organiser to this scheduling inbox'),
656 'urn:ietf:params:xml:ns:caldav:schedule-deliver-reply' => translate(
'Deliver scheduling replies from an attendee to this scheduling inbox'),
657 'urn:ietf:params:xml:ns:caldav:schedule-query-freebusy' => translate(
'Allow free/busy enquiries targeted at the owner of this scheduling inbox')
660 'urn:ietf:params:xml:ns:caldav:schedule-send' => array(
661 'urn:ietf:params:xml:ns:caldav:schedule-send-invite' => translate(
'Send scheduling invitations as an organiser from the owner of this scheduling outbox.'),
662 'urn:ietf:params:xml:ns:caldav:schedule-send-reply' => translate(
'Send scheduling replies as an attendee from the owner of this scheduling outbox.'),
663 'urn:ietf:params:xml:ns:caldav:schedule-send-freebusy' => translate(
'Send free/busy enquiries')
672 function dav_name() {
673 if ( isset($this->path) )
return $this->path;
681 function GetDepthName( ) {
682 if ( $this->IsInfiniteDepth() )
return 'infinity';
690 function DepthRegexTail( $for_collection_report =
false) {
691 if ( $this->IsInfiniteDepth() )
return '';
692 if ( $this->depth == 0 && $for_collection_report )
return '[^/]+$';
693 if ( $this->depth == 0 )
return '$';
702 function GetLockRow( $lock_token ) {
703 if ( isset($this->_locks_found) && isset($this->_locks_found[$lock_token]) ) {
704 return $this->_locks_found[$lock_token];
707 $qry =
new AwlQuery(
'SELECT * FROM locks WHERE opaquelocktoken = :lock_token', array(
':lock_token' => $lock_token ) );
708 if ( $qry->Exec(
'caldav',__LINE__,__FILE__) ) {
709 $lock_row = $qry->Fetch();
710 $this->_locks_found = array( $lock_token => $lock_row );
711 return $this->_locks_found[$lock_token];
714 $this->DoResponse( 500, translate(
"Database Error") );
727 function ValidateLockToken( $lock_token ) {
728 if ( isset($this->lock_token) && $this->lock_token == $lock_token ) {
729 dbg_error_log(
"caldav",
"They supplied a valid lock token. Great!" );
732 if ( isset($this->if_clause) ) {
733 dbg_error_log(
"caldav",
"Checking lock token '%s' against '%s'", $lock_token, $this->if_clause );
734 $tokens = preg_split(
'/[<>]/', $this->if_clause );
735 foreach( $tokens AS $k => $v ) {
736 dbg_error_log(
"caldav",
"Checking lock token '%s' against '%s'", $lock_token, $v );
737 if (
'opaquelocktoken:' == substr( $v, 0, 16 ) ) {
738 if ( substr( $v, 16 ) == $lock_token ) {
739 dbg_error_log(
"caldav",
"Lock token '%s' validated OK against '%s'", $lock_token, $v );
746 @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 );
758 function GetLockDetails( $lock_token ) {
759 if ( !isset($this->_locks_found) &&
false === $this->IsLocked() )
return false;
760 if ( isset($this->_locks_found[$lock_token]) )
return $this->_locks_found[$lock_token];
772 function FailIfLocked() {
773 if ( $existing_lock = $this->IsLocked() ) {
774 dbg_error_log(
"caldav",
"There is a lock on '%s'", $this->path);
775 if ( ! $this->ValidateLockToken($existing_lock) ) {
776 $lock_row = $this->GetLockRow($existing_lock);
780 $response[] =
new XMLElement(
'response', array(
781 new XMLElement(
'href', $lock_row->dav_name ),
782 new XMLElement(
'status',
'HTTP/1.1 423 Resource Locked')
784 if ( $lock_row->dav_name != $this->path ) {
785 $response[] =
new XMLElement(
'response', array(
786 new XMLElement(
'href', $this->path ),
787 new XMLElement(
'propstat', array(
788 new XMLElement(
'prop',
new XMLElement(
'lockdiscovery' ) ),
789 new XMLElement(
'status',
'HTTP/1.1 424 Failed Dependency')
793 $response =
new XMLElement(
"multistatus", $response, array(
'xmlns'=>
'DAV:') );
794 $xmldoc = $response->Render(0,
'<?xml version="1.0" encoding="utf-8" ?>');
795 $this->DoResponse( 207, $xmldoc,
'text/xml; charset="utf-8"' );
798 return $existing_lock;
807 function CoerceContentType() {
808 if ( isset($this->content_type) ) {
809 $type = explode(
'/', $this->content_type, 2);
811 if ( $type[0] ==
'text' ) {
812 if ( !empty($type[1]) && ($type[1] ==
'vcard' || $type[1] ==
'calendar' || $type[1] ==
'x-vcard') ) {
819 $first_word = trim(substr( $this->raw_post, 0, 30));
820 $first_word = strtoupper( preg_replace(
'/\s.*/s',
'', $first_word ) );
821 switch( $first_word ) {
823 dbg_error_log(
'LOG WARNING',
'Application sent content-type of "%s" instead of "text/xml"',
824 (isset($this->content_type)?$this->content_type:
'(null)') );
825 $this->content_type =
'text/xml';
827 case 'BEGIN:VCALENDAR':
828 dbg_error_log(
'LOG WARNING',
'Application sent content-type of "%s" instead of "text/calendar"',
829 (isset($this->content_type)?$this->content_type:
'(null)') );
830 $this->content_type =
'text/calendar';
833 dbg_error_log(
'LOG WARNING',
'Application sent content-type of "%s" instead of "text/vcard"',
834 (isset($this->content_type)?$this->content_type:
'(null)') );
835 $this->content_type =
'text/vcard';
838 dbg_error_log(
'LOG NOTICE',
'Unusual content-type of "%s" and first word of content is "%s"',
839 (isset($this->content_type)?$this->content_type:
'(null)'), $first_word );
841 if ( empty($this->content_type) ) $this->content_type =
'text/plain';
848 function PreferMinimal() {
849 if ( empty($this->prefer) )
return false;
850 foreach( $this->prefer AS $v ) {
851 if ( $v ==
'return=minimal' )
return true;
852 if ( $v ==
'return-minimal' )
return true;
860 function IsCollection( ) {
861 if ( !isset($this->_is_collection) ) {
862 $this->_is_collection = preg_match(
'#/$#', $this->path );
864 return $this->_is_collection;
871 function IsCalendar( ) {
872 if ( !$this->IsCollection() || !isset($this->collection) )
return false;
873 return $this->collection->is_calendar ==
't';
880 function IsAddressBook( ) {
881 if ( !$this->IsCollection() || !isset($this->collection) )
return false;
882 return $this->collection->is_addressbook ==
't';
889 function IsPrincipal( ) {
890 if ( !isset($this->_is_principal) ) {
891 $this->_is_principal = preg_match(
'#^/[^/]+/$#', $this->path );
893 return $this->_is_principal;
900 function IsProxyRequest( ) {
901 if ( !isset($this->_is_proxy_request) ) {
902 $this->_is_proxy_request = preg_match(
'#^/[^/]+/calendar-proxy-(read|write)/?[^/]*$#', $this->path );
904 return $this->_is_proxy_request;
911 function IsInfiniteDepth( ) {
912 return ($this->depth == DEPTH_INFINITY);
919 function CollectionId( ) {
920 return $this->collection_id;
927 function BuildSupportedPrivileges( &$reply, $privs =
null ) {
928 $privileges = array();
929 if ( $privs ===
null ) $privs = self::supportedPrivileges();
930 foreach( $privs AS $k => $v ) {
931 dbg_error_log(
'caldav',
'Adding privilege "%s" which is "%s".', $k, $v );
932 $privilege =
new XMLElement(
'privilege');
933 $reply->NSElement($privilege,$k);
934 $privset = array($privilege);
935 if ( is_array($v) ) {
936 dbg_error_log(
'caldav',
'"%s" is a container of sub-privileges.', $k );
937 $privset = array_merge($privset, $this->BuildSupportedPrivileges($reply,$v));
939 else if ( $v ==
'abstract' ) {
940 dbg_error_log(
'caldav',
'"%s" is an abstract privilege.', $v );
941 $privset[] =
new XMLElement(
'abstract');
943 else if ( strlen($v) > 1 ) {
944 $privset[] =
new XMLElement(
'description', $v);
946 $privileges[] =
new XMLElement(
'supported-privilege',$privset);
965 function AllowedTo( $activity ) {
967 dbg_error_log(
'caldav',
'Checking whether "%s" is allowed to "%s"', $session->principal->username(), $activity);
968 if ( isset($this->permissions[
'all']) )
return true;
969 switch( $activity ) {
974 case "CALDAV:schedule-send-freebusy":
975 return isset($this->permissions[
'read']) || isset($this->permissions[
'urn:ietf:params:xml:ns:caldav:read-free-busy']);
978 case "CALDAV:schedule-send-invite":
979 return isset($this->permissions[
'read']) || isset($this->permissions[
'urn:ietf:params:xml:ns:caldav:read-free-busy']);
982 case "CALDAV:schedule-send-reply":
983 return isset($this->permissions[
'read']) || isset($this->permissions[
'urn:ietf:params:xml:ns:caldav:read-free-busy']);
987 return isset($this->permissions[
'read']) || isset($this->permissions[
'urn:ietf:params:xml:ns:caldav:read-free-busy']);
991 return isset($this->permissions[
'write']) || isset($this->permissions[
'unbind']);
995 return isset($this->permissions[
'write']) || isset($this->permissions[
'write-properties']);
999 return isset($this->permissions[
'write']) || isset($this->permissions[
'write-content']);
1003 return isset($this->permissions[
'write']) || isset($this->permissions[
'bind']);
1008 if ( !isset($this->permissions[
'write']) || !isset($this->permissions[
'bind']) )
return false;
1009 if ( $this->is_principal )
return false;
1010 if ( $this->path ==
'/' )
return false;
1014 $test_bits = privilege_to_bits( $activity );
1018 return (($this->privileges & $test_bits) > 0 );
1030 function Privileges() {
1031 return $this->privileges;
1041 function CheckEtagMatch( $exists, $dest_etag ) {
1045 if ( (isset($this->etag_if_match) && $this->etag_if_match !=
'') ) {
1052 $this->PreconditionFailed(412,
'if-match', translate(
'No resource exists at the destination.'));
1057 if ( isset($c->strict_etag_checking) && $c->strict_etag_checking )
1058 $trim_chars =
'\'\\
" ';
1062 if ( isset($this->etag_if_match) && $this->etag_if_match != '' && $this->etag_if_match != '*'
1063 && trim( $this->etag_if_match, $trim_chars) != trim( $dest_etag, $trim_chars ) ) {
1070 $this->PreconditionFailed(412,'if-match',sprintf('Existing resource ETag of %s does not match %s', $dest_etag, $this->etag_if_match) );
1072 else if ( isset($this->etag_none_match) && $this->etag_none_match != ''
1073 && ($this->etag_none_match == $dest_etag || $this->etag_none_match == '*') ) {
1082 $this->PreconditionFailed(412,'if-none-match', translate( 'Existing resource matches "If-None-Match
" header - not accepted.'));
1092 function HavePrivilegeTo( $do_what ) {
1093 $test_bits = privilege_to_bits( $do_what );
1094 // dbg_error_log( 'caldav', 'request::HavePrivilegeTo("%s
") [%s] against allowed "%s
" => "%s
" (%s)',
1095 // (is_array($do_what) ? implode(',',$do_what) : $do_what), decbin($test_bits),
1096 // decbin($this->privileges), ($this->privileges & $test_bits), decbin($this->privileges & $test_bits) );
1097 return ($this->privileges & $test_bits) > 0;
1105 function UnsupportedRequest( $unsupported ) {
1106 if ( isset($unsupported) && count($unsupported) > 0 ) {
1107 $badprops = new XMLElement( "prop
" );
1108 foreach( $unsupported AS $k => $v ) {
1109 // Not supported at this point...
1110 dbg_error_log("ERROR
", " %s: Support
for $v:$k properties is not implemented yet
", $this->method );
1111 $badprops->NewElement(strtolower($k),false,array("xmlns
" => strtolower($v)));
1113 $error = new XMLElement("error
", $badprops, array("xmlns
" => "DAV:
") );
1115 $this->XMLResponse( 422, $error );
1128 function NeedPrivilege( $privileges, $href=null ) {
1129 if ( is_string($privileges) ) $privileges = array( $privileges );
1130 if ( !isset($href) ) {
1131 if ( $this->HavePrivilegeTo($privileges) ) return;
1132 $href = $this->path;
1135 $reply = new XMLDocument( array('DAV:' => '') );
1136 $privnodes = array( $reply->href(ConstructURL($href)), new XMLElement( 'privilege' ) );
1137 // RFC3744 specifies that we can only respond with one needed privilege, so we pick the first.
1138 $reply->NSElement( $privnodes[1], $privileges[0] );
1139 $xml = new XMLElement( 'need-privileges', new XMLElement( 'resource', $privnodes) );
1140 $xmldoc = $reply->Render('error',$xml);
1141 $this->DoResponse( 403, $xmldoc, 'text/xml; charset="utf-8
"' );
1142 exit(0); // Unecessary, but might clarify things
1153 function PreconditionFailed( $status, $precondition, $explanation = '', $xmlns='DAV:') {
1154 $xmldoc = sprintf('<?xml version="1.0
" encoding="utf-8
" ?>
1157 </error>', $xmlns, str_replace($xmlns.':', '', $precondition), $explanation );
1159 $this->DoResponse( $status, $xmldoc, 'text/xml; charset="utf-8
"' );
1160 exit(0); // Unecessary, but might clarify things
1169 function MalformedRequest( $text = 'Bad request' ) {
1170 $this->DoResponse( 400, $text );
1171 exit(0); // Unecessary, but might clarify things
1181 function XMLResponse( $status, $xmltree ) {
1182 $xmldoc = $xmltree->Render(0,'<?xml version="1.0
" encoding="utf-8
" ?>');
1183 $etag = md5($xmldoc);
1184 if ( !headers_sent() ) header("ETag: \
"$etag\"");
1185 $this->DoResponse( $status, $xmldoc,
'text/xml; charset="utf-8"' );
1189 public static function kill_on_exit() {
1190 posix_kill( getmypid(), 28 );
1200 function DoResponse( $status, $message=
"", $content_type=
"text/plain; charset=\"utf-8\"" ) {
1201 global $session, $c;
1202 if ( !headers_sent() ) @header( sprintf(
"HTTP/1.1 %d %s", $status, getStatusMessage($status)) );
1203 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) );
1204 if ( !headers_sent() ) header(
"Content-type: ".$content_type );
1206 if ( (isset($c->dbg[
'ALL']) && $c->dbg[
'ALL']) || (isset($c->dbg[
'response']) && $c->dbg[
'response'])
1207 || $status == 400 || $status == 402 || $status == 403 || $status > 404 ) {
1208 @dbg_error_log(
"LOG ",
'Response status %03d for %s %s', $status, $this->method, $_SERVER[
'REQUEST_URI'] );
1209 $lines = headers_list();
1210 dbg_error_log(
"LOG ",
"***************** Response Header ****************" );
1211 foreach( $lines AS $v ) {
1212 dbg_error_log(
"LOG headers",
"-->%s", $v );
1214 dbg_error_log(
"LOG ",
"******************** Response ********************" );
1216 $lines = preg_split(
'#[\r\n]+#', $message);
1217 foreach( $lines AS $v ) {
1218 dbg_error_log(
"LOG response",
"-->%s", $v );
1222 $script_finish = microtime(
true);
1223 $script_time = $script_finish - $c->script_start_time;
1224 $message_length = strlen($message);
1225 if ( $message !=
'' ) {
1226 if ( !headers_sent() ) header(
"Content-Length: ".$message_length );
1230 if ( isset($c->dbg[
'caldav']) && $c->dbg[
'caldav'] ) {
1231 if ( $message_length > 100 || strstr($message,
"\n") ) {
1232 $message = substr( preg_replace(
"#\s+#m",
' ', $message ), 0, 100) . ($message_length > 100 ?
"..." :
"");
1235 dbg_error_log(
"caldav",
"Status: %d, Message: %s, User: %d, Path: %s", $status, $message, $session->principal->user_no(), $this->path);
1237 if ( isset($c->dbg[
'statistics']) && $c->dbg[
'statistics'] ) {
1239 if ( function_exists(
'memory_get_usage') ) {
1240 $memory = sprintf(
', Memory: %dk, Peak: %dk', memory_get_usage()/1024, memory_get_peak_usage(
true)/1024);
1242 @dbg_error_log(
"statistics",
"Method: %s, Status: %d, Script: %5.3lfs, Queries: %5.3lfs, URL: %s%s",
1243 $this->method, $status, $script_time, $c->total_query_time, $this->path, $memory);
1248 catch( Exception $ignored ) {}
1250 if ( isset($c->metrics_style) && $c->metrics_style !==
false ) {
1251 $flush_time = microtime(
true) - $script_finish;
1252 $this->DoMetrics($status, $message_length, $script_time, $flush_time);
1255 if ( isset($c->exit_after_memory_exceeds) && function_exists(
'memory_get_peak_usage') && memory_get_peak_usage(
true) > $c->exit_after_memory_exceeds ) {
1256 @dbg_error_log(
"statistics",
"Peak memory use exceeds %d bytes (%d) - killing process %d", $c->exit_after_memory_exceeds, memory_get_peak_usage(
true), getmypid());
1257 register_shutdown_function(
'CalDAVRequest::kill_on_exit' );
1272 function DoMetrics($status, $response_size, $script_time, $flush_time) {
1274 static $ns =
'metrics';
1276 $method = (empty($this->method) ?
'UNKNOWN' : $this->method);
1280 if ( $c->metrics_style !=
'counters' ) {
1281 $cache = getCacheInstance();
1282 if ( $cache->isActive() ) {
1284 $base_key = $method.
':';
1285 $count_like_this = $cache->increment( $ns, $base_key.$status );
1286 $cache->increment( $ns, $base_key.
'size', $response_size );
1287 $cache->increment( $ns, $base_key.
'script_time', intval($script_time * 1000000) );
1288 $cache->increment( $ns, $base_key.
'flush_time', intval($flush_time * 1000000) );
1289 $cache->increment( $ns, $base_key.
'query_time', intval($c->total_query_time * 1000000) );
1291 if ( $count_like_this == 1 ) {
1296 $index = unserialize($cache->get($ns,
'index'));
1297 }
catch (Exception $e) {
1298 $index = array(
'methods' => array(),
'statuses' => array());
1300 $index[
'methods'][$method] = 1;
1301 $index[
'statuses'][$status] = 1;
1302 $cache->set($ns,
'index', serialize($index), 0);
1306 error_log(
"Full statistics are only available with a working Memcache configuration");
1311 if ( $c->metrics_style !=
'memcache' ) {
1312 $qstring =
"SELECT nextval('%s')";
1332 $counter = strtolower($this->method);
1335 $counter =
'unknown';
1338 $qry =
new AwlQuery(
"SELECT nextval('metrics_count_" . $counter .
"')" );
1339 $qry->Exec(
'always',__LINE__,__FILE__);