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 | |
---|