[3733] | 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 | |
---|
| 17 | require_once("XMLDocument.php"); |
---|
| 18 | require_once("CalDAVPrincipal.php"); |
---|
| 19 | include("DAVTicket.php"); |
---|
| 20 | include_once("drivers_ldap.php"); |
---|
| 21 | |
---|
| 22 | define('DEPTH_INFINITY', 9999); |
---|
| 23 | |
---|
| 24 | /** |
---|
| 25 | * A class for collecting things to do with this request. |
---|
| 26 | * |
---|
| 27 | * @package davical |
---|
| 28 | */ |
---|
| 29 | class 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 |
---|
| 334 | INSERT 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 ) |
---|
| 339 | EOSQL; |
---|
| 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 | |
---|