|
1
|
|
|
<?php // $Id$ |
|
2
|
|
|
/* |
|
3
|
|
|
+----------------------------------------------------------------------+ |
|
4
|
|
|
| Copyright (c) 2002-2007 Christian Stocker, Hartmut Holzgraefe | |
|
5
|
|
|
| All rights reserved | |
|
6
|
|
|
| | |
|
7
|
|
|
| Redistribution and use in source and binary forms, with or without | |
|
8
|
|
|
| modification, are permitted provided that the following conditions | |
|
9
|
|
|
| are met: | |
|
10
|
|
|
| | |
|
11
|
|
|
| 1. Redistributions of source code must retain the above copyright | |
|
12
|
|
|
| notice, this list of conditions and the following disclaimer. | |
|
13
|
|
|
| 2. Redistributions in binary form must reproduce the above copyright | |
|
14
|
|
|
| notice, this list of conditions and the following disclaimer in | |
|
15
|
|
|
| the documentation and/or other materials provided with the | |
|
16
|
|
|
| distribution. | |
|
17
|
|
|
| 3. The names of the authors may not be used to endorse or promote | |
|
18
|
|
|
| products derived from this software without specific prior | |
|
19
|
|
|
| written permission. | |
|
20
|
|
|
| | |
|
21
|
|
|
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
|
22
|
|
|
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
|
23
|
|
|
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | |
|
24
|
|
|
| FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | |
|
25
|
|
|
| COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | |
|
26
|
|
|
| INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | |
|
27
|
|
|
| BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
|
28
|
|
|
| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
|
29
|
|
|
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |
|
30
|
|
|
| LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | |
|
31
|
|
|
| ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
|
32
|
|
|
| POSSIBILITY OF SUCH DAMAGE. | |
|
33
|
|
|
+----------------------------------------------------------------------+ |
|
34
|
|
|
*/ |
|
35
|
|
|
|
|
36
|
|
|
require_once __DIR__."/Tools/_parse_propfind.php"; |
|
37
|
|
|
require_once __DIR__."/Tools/_parse_proppatch.php"; |
|
38
|
|
|
require_once __DIR__."/Tools/_parse_lockinfo.php"; |
|
39
|
|
|
|
|
40
|
|
|
/** |
|
41
|
|
|
* Virtual base class for implementing WebDAV servers |
|
42
|
|
|
* |
|
43
|
|
|
* WebDAV server base class, needs to be extended to do useful work |
|
44
|
|
|
* |
|
45
|
|
|
* @package HTTP_WebDAV_Server |
|
46
|
|
|
* @author Hartmut Holzgraefe <[email protected]> |
|
47
|
|
|
* @version @package_version@ |
|
48
|
|
|
*/ |
|
49
|
|
|
class HTTP_WebDAV_Server |
|
50
|
|
|
{ |
|
51
|
|
|
// {{{ Member Variables |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* complete URI for this request |
|
55
|
|
|
* |
|
56
|
|
|
* @var string |
|
57
|
|
|
*/ |
|
58
|
|
|
var $uri; |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* base URI for this request |
|
62
|
|
|
* |
|
63
|
|
|
* @var string |
|
64
|
|
|
*/ |
|
65
|
|
|
var $base_uri; |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* Set if client requires <D:href> to be a url (true) or a path (false). |
|
69
|
|
|
* RFC 4918 allows both: http://www.webdav.org/specs/rfc4918.html#ELEMENT_href |
|
70
|
|
|
* But some clients can NOT deal with one or the other! |
|
71
|
|
|
* |
|
72
|
|
|
* @var boolean |
|
73
|
|
|
*/ |
|
74
|
|
|
var $client_require_href_as_url; |
|
75
|
|
|
|
|
76
|
|
|
/** |
|
77
|
|
|
* Set if client requires or does not allow namespace redundacy. |
|
78
|
|
|
* The XML Namespace specification does allow both |
|
79
|
|
|
* But some clients can NOT deal with one or the other! |
|
80
|
|
|
* |
|
81
|
|
|
* $this->crrnd === false: |
|
82
|
|
|
* <D:multistatus xmlns:D="DAV:"> |
|
83
|
|
|
* <D:response xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"> |
|
84
|
|
|
* <D:href>/egroupware/webdav.php/home/ralf/</D:href> |
|
85
|
|
|
* <D:propstat> |
|
86
|
|
|
* <D:prop> |
|
87
|
|
|
* <D:resourcetype><D:collection /></D:resourcetype> |
|
88
|
|
|
* </D:prop> |
|
89
|
|
|
* <D:status>HTTP/1.1 200 OK</D:status> |
|
90
|
|
|
* </D:propstat> |
|
91
|
|
|
* </D:response> |
|
92
|
|
|
* </D:multistatus> |
|
93
|
|
|
* |
|
94
|
|
|
* $this->crrnd === true: |
|
95
|
|
|
* <multistatus xmlns="DAV:"> |
|
96
|
|
|
* <response xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"> |
|
97
|
|
|
* <href>/egroupware/webdav.php/home/ralf/</href> |
|
98
|
|
|
* <propstat> |
|
99
|
|
|
* <prop> |
|
100
|
|
|
* <resourcetype><collection /></resourcetype> |
|
101
|
|
|
* </prop> |
|
102
|
|
|
* <status>HTTP/1.1 200 OK</status> |
|
103
|
|
|
* </propstat> |
|
104
|
|
|
* </response> |
|
105
|
|
|
* </multistatus> |
|
106
|
|
|
* |
|
107
|
|
|
* @var boolean (client_refuses_redundand_namespace_declarations) |
|
108
|
|
|
*/ |
|
109
|
|
|
var $crrnd = false; |
|
110
|
|
|
|
|
111
|
|
|
/** |
|
112
|
|
|
|
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* URI path for this request |
|
116
|
|
|
* |
|
117
|
|
|
* @var string |
|
118
|
|
|
*/ |
|
119
|
|
|
var $path; |
|
120
|
|
|
|
|
121
|
|
|
/** |
|
122
|
|
|
* Realm string to be used in authentification popups |
|
123
|
|
|
* |
|
124
|
|
|
* @var string |
|
125
|
|
|
*/ |
|
126
|
|
|
var $http_auth_realm = "PHP WebDAV"; |
|
127
|
|
|
|
|
128
|
|
|
/** |
|
129
|
|
|
* String to be used in "X-Dav-Powered-By" header |
|
130
|
|
|
* |
|
131
|
|
|
* @var string |
|
132
|
|
|
*/ |
|
133
|
|
|
var $dav_powered_by = ""; |
|
134
|
|
|
|
|
135
|
|
|
/** |
|
136
|
|
|
* Remember parsed If: (RFC2518/9.4) header conditions |
|
137
|
|
|
* |
|
138
|
|
|
* @var array |
|
139
|
|
|
*/ |
|
140
|
|
|
var $_if_header_uris = array(); |
|
141
|
|
|
|
|
142
|
|
|
/** |
|
143
|
|
|
* HTTP response status/message |
|
144
|
|
|
* |
|
145
|
|
|
* @var string |
|
146
|
|
|
*/ |
|
147
|
|
|
var $_http_status = "200 OK"; |
|
148
|
|
|
|
|
149
|
|
|
/** |
|
150
|
|
|
* encoding of property values passed in |
|
151
|
|
|
* |
|
152
|
|
|
* @var string |
|
153
|
|
|
*/ |
|
154
|
|
|
var $_prop_encoding = "utf-8"; |
|
155
|
|
|
|
|
156
|
|
|
/** |
|
157
|
|
|
* Copy of $_SERVER superglobal array |
|
158
|
|
|
* |
|
159
|
|
|
* Derived classes may extend the constructor to |
|
160
|
|
|
* modify its contents |
|
161
|
|
|
* |
|
162
|
|
|
* @var array |
|
163
|
|
|
*/ |
|
164
|
|
|
var $_SERVER; |
|
165
|
|
|
|
|
166
|
|
|
// }}} |
|
167
|
|
|
|
|
168
|
|
|
// {{{ Constructor |
|
169
|
|
|
|
|
170
|
|
|
/** |
|
171
|
|
|
* Constructor |
|
172
|
|
|
* |
|
173
|
|
|
* @param void |
|
174
|
|
|
*/ |
|
175
|
|
|
function __construct() |
|
176
|
|
|
{ |
|
177
|
|
|
// PHP messages destroy XML output -> switch them off |
|
178
|
|
|
ini_set("display_errors", 0); |
|
179
|
|
|
|
|
180
|
|
|
// copy $_SERVER variables to local _SERVER array |
|
181
|
|
|
// so that derived classes can simply modify these |
|
182
|
|
|
$this->_SERVER = $_SERVER; |
|
183
|
|
|
} |
|
184
|
|
|
|
|
185
|
|
|
// }}} |
|
186
|
|
|
|
|
187
|
|
|
// {{{ ServeRequest() |
|
188
|
|
|
/** |
|
189
|
|
|
* Serve WebDAV HTTP request |
|
190
|
|
|
* |
|
191
|
|
|
* dispatch WebDAV HTTP request to the apropriate method handler |
|
192
|
|
|
* |
|
193
|
|
|
* @param $prefix =null prefix filesystem path with given path, eg. "/webdav" for owncloud 4.5 remote.php |
|
|
|
|
|
|
194
|
|
|
* @return void |
|
195
|
|
|
*/ |
|
196
|
|
|
function ServeRequest($prefix=null) |
|
197
|
|
|
{ |
|
198
|
|
|
// prevent warning in litmus check 'delete_fragment' |
|
199
|
|
|
if (strstr($this->_SERVER["REQUEST_URI"], '#')) { |
|
200
|
|
|
$this->http_status("400 Bad Request"); |
|
201
|
|
|
return; |
|
202
|
|
|
} |
|
203
|
|
|
|
|
204
|
|
|
// default is currently to use just the path, extending class can set $this->client_require_href_as_url depending on user-agent |
|
205
|
|
|
if ($this->client_require_href_as_url) |
|
206
|
|
|
{ |
|
207
|
|
|
// default uri is the complete request uri |
|
208
|
|
|
$uri = (@$this->_SERVER["HTTPS"] === "on" ? "https:" : "http:") . '//'.$this->_SERVER['HTTP_HOST']; |
|
209
|
|
|
} |
|
210
|
|
|
$uri .= $this->_SERVER["SCRIPT_NAME"]; |
|
211
|
|
|
|
|
212
|
|
|
// WebDAV has no concept of a query string and clients (including cadaver) |
|
213
|
|
|
// seem to pass '?' unencoded, so we need to extract the path info out |
|
214
|
|
|
// of the request URI ourselves |
|
215
|
|
|
// if request URI contains a full url, remove schema and domain |
|
216
|
|
|
$matches = null; |
|
217
|
|
|
if (preg_match('|^https?://[^/]+(/.*)$|', $path_info=$this->_SERVER["REQUEST_URI"], $matches)) |
|
218
|
|
|
{ |
|
219
|
|
|
$path_info = $matches[1]; |
|
220
|
|
|
} |
|
221
|
|
|
$path_info_raw = substr($path_info, strlen($this->_SERVER["SCRIPT_NAME"])); |
|
222
|
|
|
|
|
223
|
|
|
// just in case the path came in empty ... |
|
224
|
|
|
if (empty($path_info_raw)) { |
|
225
|
|
|
$path_info_raw = "/"; |
|
226
|
|
|
} |
|
227
|
|
|
|
|
228
|
|
|
$path_info = self::_urldecode($path_info_raw); |
|
229
|
|
|
|
|
230
|
|
|
if ($prefix && strpos($path_info, $prefix) === 0) |
|
231
|
|
|
{ |
|
232
|
|
|
$uri .= $prefix; |
|
233
|
|
|
list(,$path_info) = explode($prefix, $path_info, 2); |
|
234
|
|
|
} |
|
235
|
|
|
|
|
236
|
|
|
$this->base_uri = $uri; |
|
237
|
|
|
$this->uri = $uri . $path_info; |
|
238
|
|
|
|
|
239
|
|
|
// set path |
|
240
|
|
|
// Vfs stores %, # and ? urlencoded, we do the encoding here on a central place |
|
241
|
|
|
$this->path = strtr($path_info,array( |
|
242
|
|
|
'%' => '%25', |
|
243
|
|
|
'#' => '%23', |
|
244
|
|
|
'?' => '%3F', |
|
245
|
|
|
)); |
|
246
|
|
|
if (!strlen($this->path)) { |
|
247
|
|
|
if ($this->_SERVER["REQUEST_METHOD"] == "GET") { |
|
248
|
|
|
// redirect clients that try to GET a collection |
|
249
|
|
|
// WebDAV clients should never try this while |
|
250
|
|
|
// regular HTTP clients might ... |
|
251
|
|
|
header("Location: ".$this->base_uri."/"); |
|
252
|
|
|
return; |
|
253
|
|
|
} else { |
|
254
|
|
|
// if a WebDAV client didn't give a path we just assume '/' |
|
255
|
|
|
$this->path = "/"; |
|
256
|
|
|
} |
|
257
|
|
|
} |
|
258
|
|
|
|
|
259
|
|
|
if (ini_get("magic_quotes_gpc")) { |
|
260
|
|
|
$this->path = stripslashes($this->path); |
|
261
|
|
|
} |
|
262
|
|
|
|
|
263
|
|
|
|
|
264
|
|
|
// identify ourselves |
|
265
|
|
|
if (empty($this->dav_powered_by)) { |
|
266
|
|
|
header("X-Dav-Powered-By: PHP class: ".get_class($this)); |
|
267
|
|
|
} else { |
|
268
|
|
|
header("X-Dav-Powered-By: ".$this->dav_powered_by); |
|
269
|
|
|
} |
|
270
|
|
|
|
|
271
|
|
|
// check authentication |
|
272
|
|
|
// for the motivation for not checking OPTIONS requests on / see |
|
273
|
|
|
// http://pear.php.net/bugs/bug.php?id=5363 |
|
274
|
|
|
if ( ( !(($this->_SERVER['REQUEST_METHOD'] == 'OPTIONS') && ($this->path == "/"))) |
|
275
|
|
|
&& (!$this->_check_auth())) { |
|
276
|
|
|
// RFC2518 says we must use Digest instead of Basic |
|
277
|
|
|
// but Microsoft Clients do not support Digest |
|
278
|
|
|
// and we don't support NTLM and Kerberos |
|
279
|
|
|
// so we are stuck with Basic here |
|
280
|
|
|
header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"'); |
|
281
|
|
|
|
|
282
|
|
|
// Windows seems to require this being the last header sent |
|
283
|
|
|
// (changed according to PECL bug #3138) |
|
284
|
|
|
$this->http_status('401 Unauthorized'); |
|
285
|
|
|
|
|
286
|
|
|
return; |
|
287
|
|
|
} |
|
288
|
|
|
|
|
289
|
|
|
// check |
|
290
|
|
|
if (! $this->_check_if_header_conditions()) { |
|
|
|
|
|
|
291
|
|
|
return; |
|
292
|
|
|
} |
|
293
|
|
|
|
|
294
|
|
|
// detect requested method names |
|
295
|
|
|
$method = strtolower($this->_SERVER["REQUEST_METHOD"]); |
|
296
|
|
|
$wrapper = "http_".$method; |
|
297
|
|
|
|
|
298
|
|
|
// activate HEAD emulation by GET if no HEAD method found |
|
299
|
|
|
if ($method == "head" && !method_exists($this, "head")) { |
|
300
|
|
|
$method = "get"; |
|
301
|
|
|
} |
|
302
|
|
|
|
|
303
|
|
|
if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) { |
|
304
|
|
|
$this->$wrapper(); // call method by name |
|
305
|
|
|
} else { // method not found/implemented |
|
306
|
|
|
if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") { |
|
307
|
|
|
$error = '412 Precondition failed'; |
|
308
|
|
|
} else { |
|
309
|
|
|
$error = '405 Method not allowed'; |
|
310
|
|
|
header("Allow: ".join(", ", $this->_allow())); // tell client what's allowed |
|
311
|
|
|
} |
|
312
|
|
|
$this->http_status($error); |
|
313
|
|
|
echo "<html><head><title>Error $error</title></head>\n"; |
|
314
|
|
|
echo "<body><h1>$error</h1>\n"; |
|
315
|
|
|
echo "The requested could not by handled by this server.\n"; |
|
316
|
|
|
echo '(URI ' . $this->_SERVER['REQUEST_URI'] . ")<br>\n<br>\n"; |
|
317
|
|
|
echo "</body></html>\n"; |
|
318
|
|
|
} |
|
319
|
|
|
} |
|
320
|
|
|
|
|
321
|
|
|
// }}} |
|
322
|
|
|
|
|
323
|
|
|
// {{{ abstract WebDAV methods |
|
324
|
|
|
|
|
325
|
|
|
// {{{ GET() |
|
326
|
|
|
/** |
|
327
|
|
|
* GET implementation |
|
328
|
|
|
* |
|
329
|
|
|
* overload this method to retrieve resources from your server |
|
330
|
|
|
* <br> |
|
331
|
|
|
* |
|
332
|
|
|
* |
|
333
|
|
|
* @abstract |
|
334
|
|
|
* @param array &$params Array of input and output parameters |
|
335
|
|
|
* <br><b>input</b><ul> |
|
336
|
|
|
* <li> path - |
|
337
|
|
|
* </ul> |
|
338
|
|
|
* <br><b>output</b><ul> |
|
339
|
|
|
* <li> size - |
|
340
|
|
|
* </ul> |
|
341
|
|
|
* @returns int HTTP-Statuscode |
|
342
|
|
|
*/ |
|
343
|
|
|
|
|
344
|
|
|
/* abstract |
|
345
|
|
|
function GET(&$params) |
|
346
|
|
|
{ |
|
347
|
|
|
// dummy entry for PHPDoc |
|
348
|
|
|
} |
|
349
|
|
|
*/ |
|
350
|
|
|
|
|
351
|
|
|
// }}} |
|
352
|
|
|
|
|
353
|
|
|
// {{{ PUT() |
|
354
|
|
|
/** |
|
355
|
|
|
* PUT implementation |
|
356
|
|
|
* |
|
357
|
|
|
* PUT implementation |
|
358
|
|
|
* |
|
359
|
|
|
* @abstract |
|
360
|
|
|
* @param array &$params |
|
361
|
|
|
* @returns int HTTP-Statuscode |
|
362
|
|
|
*/ |
|
363
|
|
|
|
|
364
|
|
|
/* abstract |
|
365
|
|
|
function PUT() |
|
366
|
|
|
{ |
|
367
|
|
|
// dummy entry for PHPDoc |
|
368
|
|
|
} |
|
369
|
|
|
*/ |
|
370
|
|
|
|
|
371
|
|
|
// }}} |
|
372
|
|
|
|
|
373
|
|
|
// {{{ COPY() |
|
374
|
|
|
|
|
375
|
|
|
/** |
|
376
|
|
|
* COPY implementation |
|
377
|
|
|
* |
|
378
|
|
|
* COPY implementation |
|
379
|
|
|
* |
|
380
|
|
|
* @abstract |
|
381
|
|
|
* @param array &$params |
|
382
|
|
|
* @returns int HTTP-Statuscode |
|
383
|
|
|
*/ |
|
384
|
|
|
|
|
385
|
|
|
/* abstract |
|
386
|
|
|
function COPY() |
|
387
|
|
|
{ |
|
388
|
|
|
// dummy entry for PHPDoc |
|
389
|
|
|
} |
|
390
|
|
|
*/ |
|
391
|
|
|
|
|
392
|
|
|
// }}} |
|
393
|
|
|
|
|
394
|
|
|
// {{{ MOVE() |
|
395
|
|
|
|
|
396
|
|
|
/** |
|
397
|
|
|
* MOVE implementation |
|
398
|
|
|
* |
|
399
|
|
|
* MOVE implementation |
|
400
|
|
|
* |
|
401
|
|
|
* @abstract |
|
402
|
|
|
* @param array &$params |
|
403
|
|
|
* @returns int HTTP-Statuscode |
|
404
|
|
|
*/ |
|
405
|
|
|
|
|
406
|
|
|
/* abstract |
|
407
|
|
|
function MOVE() |
|
408
|
|
|
{ |
|
409
|
|
|
// dummy entry for PHPDoc |
|
410
|
|
|
} |
|
411
|
|
|
*/ |
|
412
|
|
|
|
|
413
|
|
|
// }}} |
|
414
|
|
|
|
|
415
|
|
|
// {{{ DELETE() |
|
416
|
|
|
|
|
417
|
|
|
/** |
|
418
|
|
|
* DELETE implementation |
|
419
|
|
|
* |
|
420
|
|
|
* DELETE implementation |
|
421
|
|
|
* |
|
422
|
|
|
* @abstract |
|
423
|
|
|
* @param array &$params |
|
424
|
|
|
* @returns int HTTP-Statuscode |
|
425
|
|
|
*/ |
|
426
|
|
|
|
|
427
|
|
|
/* abstract |
|
428
|
|
|
function DELETE() |
|
429
|
|
|
{ |
|
430
|
|
|
// dummy entry for PHPDoc |
|
431
|
|
|
} |
|
432
|
|
|
*/ |
|
433
|
|
|
// }}} |
|
434
|
|
|
|
|
435
|
|
|
// {{{ PROPFIND() |
|
436
|
|
|
|
|
437
|
|
|
/** |
|
438
|
|
|
* PROPFIND implementation |
|
439
|
|
|
* |
|
440
|
|
|
* PROPFIND implementation |
|
441
|
|
|
* |
|
442
|
|
|
* @abstract |
|
443
|
|
|
* @param array &$params |
|
444
|
|
|
* @returns int HTTP-Statuscode |
|
445
|
|
|
*/ |
|
446
|
|
|
|
|
447
|
|
|
/* abstract |
|
448
|
|
|
function PROPFIND() |
|
449
|
|
|
{ |
|
450
|
|
|
// dummy entry for PHPDoc |
|
451
|
|
|
} |
|
452
|
|
|
*/ |
|
453
|
|
|
|
|
454
|
|
|
// }}} |
|
455
|
|
|
|
|
456
|
|
|
// {{{ PROPPATCH() |
|
457
|
|
|
|
|
458
|
|
|
/** |
|
459
|
|
|
* PROPPATCH implementation |
|
460
|
|
|
* |
|
461
|
|
|
* PROPPATCH implementation |
|
462
|
|
|
* |
|
463
|
|
|
* @abstract |
|
464
|
|
|
* @param array &$params |
|
465
|
|
|
* @returns int HTTP-Statuscode |
|
466
|
|
|
*/ |
|
467
|
|
|
|
|
468
|
|
|
/* abstract |
|
469
|
|
|
function PROPPATCH() |
|
470
|
|
|
{ |
|
471
|
|
|
// dummy entry for PHPDoc |
|
472
|
|
|
} |
|
473
|
|
|
*/ |
|
474
|
|
|
// }}} |
|
475
|
|
|
|
|
476
|
|
|
// {{{ LOCK() |
|
477
|
|
|
|
|
478
|
|
|
/** |
|
479
|
|
|
* LOCK implementation |
|
480
|
|
|
* |
|
481
|
|
|
* LOCK implementation |
|
482
|
|
|
* |
|
483
|
|
|
* @abstract |
|
484
|
|
|
* @param array &$params |
|
485
|
|
|
* @returns int HTTP-Statuscode |
|
486
|
|
|
*/ |
|
487
|
|
|
|
|
488
|
|
|
/* abstract |
|
489
|
|
|
function LOCK() |
|
490
|
|
|
{ |
|
491
|
|
|
// dummy entry for PHPDoc |
|
492
|
|
|
} |
|
493
|
|
|
*/ |
|
494
|
|
|
// }}} |
|
495
|
|
|
|
|
496
|
|
|
// {{{ UNLOCK() |
|
497
|
|
|
|
|
498
|
|
|
/** |
|
499
|
|
|
* UNLOCK implementation |
|
500
|
|
|
* |
|
501
|
|
|
* UNLOCK implementation |
|
502
|
|
|
* |
|
503
|
|
|
* @abstract |
|
504
|
|
|
* @param array &$params |
|
505
|
|
|
* @returns int HTTP-Statuscode |
|
506
|
|
|
*/ |
|
507
|
|
|
|
|
508
|
|
|
/* abstract |
|
509
|
|
|
function UNLOCK() |
|
510
|
|
|
{ |
|
511
|
|
|
// dummy entry for PHPDoc |
|
512
|
|
|
} |
|
513
|
|
|
*/ |
|
514
|
|
|
// }}} |
|
515
|
|
|
|
|
516
|
|
|
// {{{ ACL() |
|
517
|
|
|
|
|
518
|
|
|
/** |
|
519
|
|
|
* ACL implementation |
|
520
|
|
|
* |
|
521
|
|
|
* ACL implementation |
|
522
|
|
|
* |
|
523
|
|
|
* @abstract |
|
524
|
|
|
* @param array &$params |
|
525
|
|
|
* @returns int HTTP-Statuscode |
|
526
|
|
|
*/ |
|
527
|
|
|
|
|
528
|
|
|
/* abstract |
|
529
|
|
|
function ACL() |
|
530
|
|
|
{ |
|
531
|
|
|
// dummy entry for PHPDoc |
|
532
|
|
|
} |
|
533
|
|
|
*/ |
|
534
|
|
|
// }}} |
|
535
|
|
|
|
|
536
|
|
|
// }}} |
|
537
|
|
|
|
|
538
|
|
|
// {{{ other abstract methods |
|
539
|
|
|
|
|
540
|
|
|
// {{{ check_auth() |
|
541
|
|
|
|
|
542
|
|
|
/** |
|
543
|
|
|
* check authentication |
|
544
|
|
|
* |
|
545
|
|
|
* overload this method to retrieve and confirm authentication information |
|
546
|
|
|
* |
|
547
|
|
|
* @abstract |
|
548
|
|
|
* @param string type Authentication type, e.g. "basic" or "digest" |
|
549
|
|
|
* @param string username Transmitted username |
|
550
|
|
|
* @param string passwort Transmitted password |
|
551
|
|
|
* @returns bool Authentication status |
|
552
|
|
|
*/ |
|
553
|
|
|
|
|
554
|
|
|
/* abstract |
|
555
|
|
|
function checkAuth($type, $username, $password) |
|
556
|
|
|
{ |
|
557
|
|
|
// dummy entry for PHPDoc |
|
558
|
|
|
} |
|
559
|
|
|
*/ |
|
560
|
|
|
|
|
561
|
|
|
// }}} |
|
562
|
|
|
|
|
563
|
|
|
// {{{ checklock() |
|
564
|
|
|
|
|
565
|
|
|
/** |
|
566
|
|
|
* check lock status for a resource |
|
567
|
|
|
* |
|
568
|
|
|
* overload this method to return shared and exclusive locks |
|
569
|
|
|
* active for this resource |
|
570
|
|
|
* |
|
571
|
|
|
* @abstract |
|
572
|
|
|
* @param string resource Resource path to check |
|
573
|
|
|
* @returns array An array of lock entries each consisting |
|
574
|
|
|
* of 'type' ('shared'/'exclusive'), 'token' and 'timeout' |
|
575
|
|
|
*/ |
|
576
|
|
|
|
|
577
|
|
|
/* abstract |
|
578
|
|
|
function checklock($resource) |
|
579
|
|
|
{ |
|
580
|
|
|
// dummy entry for PHPDoc |
|
581
|
|
|
} |
|
582
|
|
|
*/ |
|
583
|
|
|
|
|
584
|
|
|
// }}} |
|
585
|
|
|
|
|
586
|
|
|
// }}} |
|
587
|
|
|
|
|
588
|
|
|
// {{{ WebDAV HTTP method wrappers |
|
589
|
|
|
|
|
590
|
|
|
// {{{ http_OPTIONS() |
|
591
|
|
|
|
|
592
|
|
|
/** |
|
593
|
|
|
* OPTIONS method handler |
|
594
|
|
|
* |
|
595
|
|
|
* The OPTIONS method handler creates a valid OPTIONS reply |
|
596
|
|
|
* including Dav: and Allowed: headers |
|
597
|
|
|
* based on the implemented methods found in the actual instance |
|
598
|
|
|
* |
|
599
|
|
|
* @param void |
|
600
|
|
|
* @return void |
|
601
|
|
|
*/ |
|
602
|
|
|
function http_OPTIONS() |
|
603
|
|
|
{ |
|
604
|
|
|
// Microsoft clients default to the Frontpage protocol |
|
605
|
|
|
// unless we tell them to use WebDAV |
|
606
|
|
|
header("MS-Author-Via: DAV"); |
|
607
|
|
|
|
|
608
|
|
|
// get allowed methods |
|
609
|
|
|
$allow = $this->_allow(); |
|
610
|
|
|
|
|
611
|
|
|
// dav header |
|
612
|
|
|
$dav = array(1); // assume we are always dav class 1 compliant |
|
613
|
|
|
if (isset($allow['LOCK'])) { |
|
614
|
|
|
$dav[] = 2; // dav class 2 requires that locking is supported |
|
615
|
|
|
} |
|
616
|
|
|
|
|
617
|
|
|
// allow extending class to modify DAV and Allow headers |
|
618
|
|
|
if (method_exists($this,'OPTIONS')) { |
|
619
|
|
|
$this->OPTIONS($this->path,$dav,$allow); |
|
620
|
|
|
} |
|
621
|
|
|
|
|
622
|
|
|
// tell clients what we found |
|
623
|
|
|
$this->http_status("200 OK"); |
|
624
|
|
|
header("DAV: " .join(", ", $dav)); |
|
625
|
|
|
header("Allow: ".join(", ", $allow)); |
|
626
|
|
|
|
|
627
|
|
|
header("Content-length: 0"); |
|
628
|
|
|
} |
|
629
|
|
|
|
|
630
|
|
|
// }}} |
|
631
|
|
|
|
|
632
|
|
|
|
|
633
|
|
|
// {{{ http_PROPFIND() |
|
634
|
|
|
|
|
635
|
|
|
/** |
|
636
|
|
|
* Should the whole PROPFIND request (xml) be stored |
|
637
|
|
|
* |
|
638
|
|
|
* @var boolean |
|
639
|
|
|
*/ |
|
640
|
|
|
var $store_request = false; |
|
641
|
|
|
/** |
|
642
|
|
|
* Content of (last) PROPFIND request |
|
643
|
|
|
* |
|
644
|
|
|
* @var string |
|
645
|
|
|
*/ |
|
646
|
|
|
var $request; |
|
647
|
|
|
|
|
648
|
|
|
/** |
|
649
|
|
|
* PROPFIND method handler |
|
650
|
|
|
* |
|
651
|
|
|
* @param string $handler ='PROPFIND' allows to use method eg. for CalDAV REPORT |
|
652
|
|
|
* @return void |
|
653
|
|
|
*/ |
|
654
|
|
|
function http_PROPFIND($handler='PROPFIND') |
|
655
|
|
|
{ |
|
656
|
|
|
$options = Array(); |
|
657
|
|
|
$files = Array(); |
|
658
|
|
|
|
|
659
|
|
|
$options["path"] = $this->path; |
|
660
|
|
|
|
|
661
|
|
|
// search depth from header (default is "infinity) |
|
662
|
|
|
if (isset($this->_SERVER['HTTP_DEPTH'])) { |
|
663
|
|
|
$options["depth"] = $this->_SERVER["HTTP_DEPTH"]; |
|
664
|
|
|
} else { |
|
665
|
|
|
$options["depth"] = "infinity"; |
|
666
|
|
|
} |
|
667
|
|
|
|
|
668
|
|
|
// analyze request payload |
|
669
|
|
|
$propinfo = new _parse_propfind("php://input", $this->store_request, $handler); |
|
670
|
|
|
if ($this->store_request) $this->request = $propinfo->request; |
|
671
|
|
|
if ($propinfo->error) { |
|
672
|
|
|
$this->http_status("400 Bad Request"); |
|
673
|
|
|
if (method_exists($this, 'log')) $this->log('Error parsing propfind: '.$propinfo->error); |
|
674
|
|
|
return; |
|
675
|
|
|
} |
|
676
|
|
|
$options['root'] = $propinfo->root; |
|
677
|
|
|
$options['props'] = $propinfo->props; |
|
678
|
|
|
if ($propinfo->filters) |
|
|
|
|
|
|
679
|
|
|
$options['filters'] = $propinfo->filters; |
|
680
|
|
|
if ($propinfo->other) |
|
|
|
|
|
|
681
|
|
|
$options['other'] = $propinfo->other; |
|
682
|
|
|
|
|
683
|
|
|
// call user handler |
|
684
|
|
|
if (!($retval =$this->$handler($options, $files))) { |
|
685
|
|
|
$files = array("files" => array()); |
|
686
|
|
|
if (method_exists($this, "checkLock")) { |
|
687
|
|
|
// is locked? |
|
688
|
|
|
$lock = $this->checkLock($this->path); |
|
689
|
|
|
|
|
690
|
|
|
if (is_array($lock) && count($lock)) { |
|
691
|
|
|
$created = isset($lock['created']) ? $lock['created'] : time(); |
|
692
|
|
|
$modified = isset($lock['modified']) ? $lock['modified'] : time(); |
|
693
|
|
|
$files['files'][] = array("path" => self::_slashify($this->path), |
|
694
|
|
|
"props" => array($this->mkprop("displayname", $this->path), |
|
695
|
|
|
$this->mkprop("creationdate", $created), |
|
696
|
|
|
$this->mkprop("getlastmodified", $modified), |
|
697
|
|
|
$this->mkprop("resourcetype", ""), |
|
698
|
|
|
$this->mkprop("getcontenttype", ""), |
|
699
|
|
|
$this->mkprop("getcontentlength", 0)) |
|
700
|
|
|
); |
|
701
|
|
|
} |
|
702
|
|
|
} |
|
703
|
|
|
|
|
704
|
|
|
if (empty($files['files'])) { |
|
705
|
|
|
$this->http_status("404 Not Found"); |
|
706
|
|
|
return; |
|
707
|
|
|
} |
|
708
|
|
|
} |
|
709
|
|
|
|
|
710
|
|
|
// now we generate the reply header ... |
|
711
|
|
|
if ($retval === true) |
|
712
|
|
|
{ |
|
713
|
|
|
$this->http_status('207 Multi-Status'); |
|
714
|
|
|
} |
|
715
|
|
|
elseif (is_string($retval)) |
|
716
|
|
|
{ |
|
717
|
|
|
$this->http_status($retval); |
|
718
|
|
|
header('Content-Type: text/html'); |
|
719
|
|
|
echo "<html><head><title>Error $retval</title></head>\n"; |
|
720
|
|
|
echo "<body><h1>$retval</h1>\n"; |
|
721
|
|
|
switch (substr($retval, 0 ,3)) |
|
722
|
|
|
{ |
|
723
|
|
|
case '501': // Not Implemented |
|
724
|
|
|
echo "The requested feature is not (yet) supported by this server.\n"; |
|
725
|
|
|
break; |
|
726
|
|
|
default: |
|
727
|
|
|
echo "The request could not be handled by this server.\n"; |
|
728
|
|
|
} |
|
729
|
|
|
echo '(URI ' . $this->_SERVER['REQUEST_URI'] . ")<br>\n<br>\n"; |
|
730
|
|
|
echo "</body></html>\n"; |
|
731
|
|
|
return; |
|
732
|
|
|
} |
|
733
|
|
|
// dav header |
|
734
|
|
|
$dav = array(1); // assume we are always dav class 1 compliant |
|
735
|
|
|
$allow = false; |
|
736
|
|
|
|
|
737
|
|
|
// allow extending class to modify DAV |
|
738
|
|
|
if (method_exists($this,'OPTIONS')) { |
|
739
|
|
|
$this->OPTIONS($this->path,$dav,$allow); |
|
740
|
|
|
} |
|
741
|
|
|
header("DAV: " .join(", ", $dav)); |
|
742
|
|
|
header('Content-Type: text/xml; charset="utf-8"'); |
|
743
|
|
|
|
|
744
|
|
|
// add Vary and Preference-Applied header for Prefer: return=minimal |
|
745
|
|
|
if (isset($this->_SERVER['HTTP_PREFER']) && in_array('return=minimal', preg_split('/, ?/', $this->_SERVER['HTTP_PREFER']))) |
|
746
|
|
|
{ |
|
747
|
|
|
header("Preference-Applied: return=minimal"); |
|
748
|
|
|
header("Vary: Prefer"); |
|
749
|
|
|
} |
|
750
|
|
|
|
|
751
|
|
|
// ... and payload |
|
752
|
|
|
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; |
|
753
|
|
|
echo $this->crrnd ? "<multistatus xmlns=\"DAV:\">\n" : "<D:multistatus xmlns:D=\"DAV:\">\n"; |
|
754
|
|
|
|
|
755
|
|
|
$this->multistatus_responses($files['files'], $options['props']); |
|
756
|
|
|
|
|
757
|
|
|
// WebDAV sync report sync-token, can be either the sync-token or a callback (called with params in $files['sync-token-params']) |
|
758
|
|
|
if (isset($files['sync-token'])) |
|
759
|
|
|
{ |
|
760
|
|
|
echo ($this->crrnd ? " <" : " <D:")."sync-token>". |
|
761
|
|
|
htmlspecialchars(!is_callable($files['sync-token']) ? $files['sync-token'] : |
|
762
|
|
|
call_user_func_array($files['sync-token'], (array)$files['sync-token-params'])). |
|
763
|
|
|
($this->crrnd ? "</" : "</D:")."sync-token>\n"; |
|
764
|
|
|
} |
|
765
|
|
|
|
|
766
|
|
|
echo '</'.($this->crrnd?'':'D:')."multistatus>\n"; |
|
767
|
|
|
} |
|
768
|
|
|
|
|
769
|
|
|
/** |
|
770
|
|
|
* Render (echo) XML for given multistatus responses |
|
771
|
|
|
* |
|
772
|
|
|
* @param array|Iterator $files |
|
773
|
|
|
* @param array|string $props |
|
774
|
|
|
*/ |
|
775
|
|
|
function multistatus_responses(&$files, $props, $initial_ns_hash=null, $initial_ns_defs=null) |
|
776
|
|
|
{ |
|
777
|
|
|
if (!isset($initial_ns_hash)) $initial_ns_hash = array('DAV:' => 'D'); |
|
778
|
|
|
if (!isset($initial_ns_defs)) $initial_ns_defs = 'xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"'; |
|
779
|
|
|
|
|
780
|
|
|
// using an ArrayIterator to prevent foreach from copying the array, |
|
781
|
|
|
// as we cant loop by reference, when an iterator is given in $files |
|
782
|
|
|
if (is_array($files)) |
|
783
|
|
|
{ |
|
784
|
|
|
$files = new ArrayIterator($files); |
|
785
|
|
|
} |
|
786
|
|
|
// support for "Prefer: depth-noroot" header on PROPFIND |
|
787
|
|
|
$skip_root = $this->_SERVER['REQUEST_METHOD'] == 'PROPFIND' && |
|
788
|
|
|
!isset($initial_ns_hash) && // multistatus_response calls itself, do NOT apply skip in that case |
|
789
|
|
|
isset($this->_SERVER['HTTP_PREFER']) && in_array('depth-noroot', preg_split('/, ?/', $this->_SERVER['HTTP_PREFER'])); |
|
790
|
|
|
|
|
791
|
|
|
// now we loop over all returned file entries |
|
792
|
|
|
foreach ($files as $file) { |
|
793
|
|
|
|
|
794
|
|
|
// skip first element (root), if requested by Prefer: depth-noroot |
|
795
|
|
|
if ($skip_root) { |
|
796
|
|
|
$skip_root = false; |
|
797
|
|
|
continue; |
|
798
|
|
|
} |
|
799
|
|
|
|
|
800
|
|
|
// collect namespaces here |
|
801
|
|
|
$ns_hash = $initial_ns_hash; |
|
802
|
|
|
|
|
803
|
|
|
// Microsoft Clients need this special namespace for date and time values |
|
804
|
|
|
$ns_defs = $initial_ns_defs; |
|
805
|
|
|
|
|
806
|
|
|
// nothing to do if no properties were returend for a file |
|
807
|
|
|
if (isset($file["props"]) && is_array($file["props"])) { |
|
808
|
|
|
|
|
809
|
|
|
// now loop over all returned properties |
|
810
|
|
|
foreach ($file["props"] as &$prop) { |
|
811
|
|
|
// as a convenience feature we do not require that user handlers |
|
812
|
|
|
// restrict returned properties to the requested ones |
|
813
|
|
|
// here we strip all unrequested entries out of the response |
|
814
|
|
|
|
|
815
|
|
|
// this can happen if we have allprop and prop in one propfind: |
|
816
|
|
|
// <allprop /><prop><blah /></prop>, eg. blah is not automatic returned by allprop |
|
817
|
|
|
switch(is_array($props) ? $props[0] : $props) { |
|
818
|
|
|
case "all": |
|
819
|
|
|
// nothing to remove |
|
820
|
|
|
break; |
|
821
|
|
|
|
|
822
|
|
|
case "names": |
|
823
|
|
|
// only the names of all existing properties were requested |
|
824
|
|
|
// so we remove all values |
|
825
|
|
|
unset($prop["val"]); |
|
826
|
|
|
break; |
|
827
|
|
|
|
|
828
|
|
|
default: |
|
829
|
|
|
$found = false; |
|
830
|
|
|
|
|
831
|
|
|
// search property name in requested properties |
|
832
|
|
|
foreach ((array)$props as $reqprop) { |
|
833
|
|
|
if ( $reqprop["name"] == $prop["name"] |
|
834
|
|
|
&& @$reqprop["xmlns"] == $prop["ns"]) { |
|
835
|
|
|
$found = true; |
|
836
|
|
|
break; |
|
837
|
|
|
} |
|
838
|
|
|
} |
|
839
|
|
|
|
|
840
|
|
|
// unset property and continue with next one if not found/requested |
|
841
|
|
|
if (!$found) { |
|
842
|
|
|
$prop=""; |
|
843
|
|
|
continue(2); |
|
844
|
|
|
} |
|
845
|
|
|
break; |
|
846
|
|
|
} |
|
847
|
|
|
|
|
848
|
|
|
// namespace handling |
|
849
|
|
|
if (empty($prop["ns"])) continue; // no namespace |
|
850
|
|
|
$ns = $prop["ns"]; |
|
851
|
|
|
//if ($ns == "DAV:") continue; // default namespace |
|
852
|
|
|
if (isset($ns_hash[$ns])) continue; // already known |
|
853
|
|
|
|
|
854
|
|
|
// register namespace |
|
855
|
|
|
$ns_name = "ns".(count($ns_hash) + 1); |
|
856
|
|
|
$ns_hash[$ns] = $ns_name; |
|
857
|
|
|
$ns_defs .= " xmlns:$ns_name=\"$ns\""; |
|
858
|
|
|
} |
|
859
|
|
|
|
|
860
|
|
|
// we also need to add empty entries for properties that were requested |
|
861
|
|
|
// but for which no values where returned by the user handler |
|
862
|
|
|
if (is_array($props)) { |
|
863
|
|
|
foreach ($props as $reqprop) { |
|
864
|
|
|
if (!is_array($reqprop) || $reqprop['name']=="") continue; // skip empty entries, or 'all' if <allprop /> used together with <prop> |
|
865
|
|
|
|
|
866
|
|
|
$found = false; |
|
867
|
|
|
|
|
868
|
|
|
// check if property exists in result |
|
869
|
|
|
foreach ($file["props"] as &$prop) { |
|
870
|
|
|
if (is_array($prop) && $reqprop["name"] == $prop["name"] |
|
871
|
|
|
&& @$reqprop["xmlns"] == $prop["ns"]) { |
|
872
|
|
|
$found = true; |
|
873
|
|
|
break; |
|
874
|
|
|
} |
|
875
|
|
|
} |
|
876
|
|
|
|
|
877
|
|
|
if (!$found) { |
|
878
|
|
|
if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") { |
|
879
|
|
|
// lockdiscovery is handled by the base class |
|
880
|
|
|
$file["props"][] |
|
881
|
|
|
= $this->mkprop("DAV:", |
|
882
|
|
|
"lockdiscovery", |
|
883
|
|
|
$this->lockdiscovery($file['path'])); |
|
884
|
|
|
// only collect $file['noprops'] if we have NO Brief: t and NO Prefer: return=minimal HTTP Header |
|
885
|
|
|
} elseif ((!isset($this->_SERVER['HTTP_BRIEF']) || $this->_SERVER['HTTP_BRIEF'] != 't') && |
|
886
|
|
|
(!isset($this->_SERVER['HTTP_PREFER']) || !in_array('return=minimal', preg_split('/, ?/', $this->_SERVER['HTTP_PREFER'])))) { |
|
887
|
|
|
// add empty value for this property |
|
888
|
|
|
$file["noprops"][] = |
|
889
|
|
|
$this->mkprop($reqprop["xmlns"], $reqprop["name"], ""); |
|
890
|
|
|
|
|
891
|
|
|
// register property namespace if not known yet |
|
892
|
|
|
if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) { |
|
893
|
|
|
$ns_name = "ns".(count($ns_hash) + 1); |
|
894
|
|
|
$ns_hash[$reqprop["xmlns"]] = $ns_name; |
|
895
|
|
|
$ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\""; |
|
896
|
|
|
} |
|
897
|
|
|
} |
|
898
|
|
|
} |
|
899
|
|
|
} |
|
900
|
|
|
} |
|
901
|
|
|
} |
|
902
|
|
|
// ignore empty or incomplete entries |
|
903
|
|
|
if (!is_array($file) || empty($file) || !isset($file["path"])) continue; |
|
904
|
|
|
$path = $file['path']; |
|
905
|
|
|
if (!is_string($path) || $path==="") continue; |
|
906
|
|
|
|
|
907
|
|
|
if ($this->crrnd) |
|
908
|
|
|
{ |
|
909
|
|
|
echo " <response $ns_defs>\n"; |
|
910
|
|
|
} |
|
911
|
|
|
else |
|
912
|
|
|
{ |
|
913
|
|
|
echo " <D:response $ns_defs>\n"; |
|
914
|
|
|
} |
|
915
|
|
|
|
|
916
|
|
|
/* TODO right now the user implementation has to make sure |
|
917
|
|
|
collections end in a slash, this should be done in here |
|
918
|
|
|
by checking the resource attribute */ |
|
919
|
|
|
$href_raw = $this->_mergePaths($this->base_uri, $path); |
|
920
|
|
|
|
|
921
|
|
|
/* minimal urlencoding is needed for the resource path */ |
|
922
|
|
|
$href = $this->_urlencode($href_raw); |
|
923
|
|
|
|
|
924
|
|
|
if ($this->crrnd) |
|
925
|
|
|
{ |
|
926
|
|
|
echo " <href>$href</href>\n"; |
|
927
|
|
|
} |
|
928
|
|
|
else |
|
929
|
|
|
{ |
|
930
|
|
|
echo " <D:href>$href</D:href>\n"; |
|
931
|
|
|
} |
|
932
|
|
|
|
|
933
|
|
|
// report all found properties and their values (if any) |
|
934
|
|
|
if (isset($file["props"]) && is_array($file["props"])) { |
|
935
|
|
|
echo ' <'.($this->crrnd?'':'D:')."propstat>\n"; |
|
936
|
|
|
echo ' <'.($this->crrnd?'':'D:')."prop>\n"; |
|
937
|
|
|
|
|
938
|
|
|
foreach ($file["props"] as &$prop) { |
|
939
|
|
|
|
|
940
|
|
|
if (!is_array($prop)) continue; |
|
941
|
|
|
if (!isset($prop["name"])) continue; |
|
942
|
|
|
|
|
943
|
|
|
if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) { |
|
944
|
|
|
// empty properties (cannot use empty() for check as "0" is a legal value here) |
|
945
|
|
|
if ($prop["ns"]=="DAV:") { |
|
946
|
|
|
echo ' <'.($this->crrnd?'':'D:')."$prop[name]/>\n"; |
|
947
|
|
|
} else if (!empty($prop["ns"])) { |
|
948
|
|
|
echo " <".$ns_hash[$prop["ns"]].":$prop[name]/>\n"; |
|
949
|
|
|
} else { |
|
950
|
|
|
echo " <$prop[name] xmlns=\"\"/>"; |
|
951
|
|
|
} |
|
952
|
|
|
} |
|
953
|
|
|
// multiple level of responses required for expand-property reports |
|
954
|
|
|
elseif(isset($prop['props']) && is_array($prop['val'])) |
|
955
|
|
|
{ |
|
956
|
|
|
if ($prop['ns'] && !isset($ns_hash[$prop['ns']])) { |
|
957
|
|
|
$ns_name = "ns".(count($ns_hash) + 1); |
|
958
|
|
|
$ns_hash[$prop['ns']] = $ns_name; |
|
959
|
|
|
} |
|
960
|
|
|
echo ' <'.$ns_hash[$prop['ns']].":$prop[name]>\n"; |
|
961
|
|
|
$this->multistatus_responses($prop['val'], $prop['props'], $ns_hash, ''); |
|
962
|
|
|
echo ' </'.$ns_hash[$prop['ns']].":$prop[name]>\n"; |
|
963
|
|
|
} else if ($prop["ns"] == "DAV:") { |
|
964
|
|
|
// some WebDAV properties need special treatment |
|
965
|
|
|
switch ($prop["name"]) { |
|
966
|
|
|
case "creationdate": |
|
967
|
|
|
echo ' <'.($this->crrnd?'':'D:')."creationdate ns0:dt=\"dateTime.tz\">" |
|
968
|
|
|
. gmdate("Y-m-d\\TH:i:s\\Z", $prop['val']) |
|
969
|
|
|
. '</'.($this->crrnd?'':'D:')."creationdate>\n"; |
|
970
|
|
|
break; |
|
971
|
|
|
case "getlastmodified": |
|
972
|
|
|
echo ' <'.($this->crrnd?'':'D:')."getlastmodified ns0:dt=\"dateTime.rfc1123\">" |
|
973
|
|
|
. gmdate("D, d M Y H:i:s ", $prop['val']) |
|
974
|
|
|
. "GMT</".($this->crrnd?'':'D:')."getlastmodified>\n"; |
|
975
|
|
|
break; |
|
976
|
|
|
case "supportedlock": |
|
977
|
|
|
echo ' <'.($this->crrnd?'':'D:')."supportedlock>$prop[val]</".($this->crrnd?'':'D:')."supportedlock>\n"; |
|
978
|
|
|
break; |
|
979
|
|
|
case "lockdiscovery": |
|
980
|
|
|
echo ' <'.($this->crrnd?'':'D:')."lockdiscovery>\n"; |
|
981
|
|
|
echo $prop["val"]; |
|
982
|
|
|
echo ' </'.($this->crrnd?'':'D:')."lockdiscovery>\n"; |
|
983
|
|
|
break; |
|
984
|
|
|
// the following are non-standard Microsoft extensions to the DAV namespace |
|
985
|
|
|
case "lastaccessed": |
|
986
|
|
|
echo ' <'.($this->crrnd?'':'D:')."lastaccessed ns0:dt=\"dateTime.rfc1123\">" |
|
987
|
|
|
. gmdate("D, d M Y H:i:s ", $prop['val']) |
|
988
|
|
|
. 'GMT</'.($this->crrnd?'':'D:')."lastaccessed>\n"; |
|
989
|
|
|
break; |
|
990
|
|
|
case "ishidden": |
|
991
|
|
|
echo ' <'.($this->crrnd?'':'D:')."ishidden>" |
|
992
|
|
|
. is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false') |
|
993
|
|
|
. '</'.($this->crrnd?'':'D:')."</D:ishidden>\n"; |
|
994
|
|
|
break; |
|
995
|
|
|
default: |
|
996
|
|
|
$ns_defs = ''; |
|
997
|
|
|
if (is_array($prop['val'])) |
|
998
|
|
|
{ |
|
999
|
|
|
$hns_hash = $ns_hash; |
|
1000
|
|
|
$val = $this->_hierarchical_prop_encode($prop['val'], 'DAV:', $ns_defs, $hns_hash); |
|
1001
|
|
|
} elseif (isset($prop['raw'])) { |
|
1002
|
|
|
$val = $this->_prop_encode('<![CDATA['.$prop['val'].']]>'); |
|
1003
|
|
|
} else { |
|
1004
|
|
|
$val = $this->_prop_encode(htmlspecialchars($prop['val'], ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8')); |
|
1005
|
|
|
} |
|
1006
|
|
|
echo ' <'.($this->crrnd?'':'D:')."$prop[name]$ns_defs>$val". |
|
1007
|
|
|
'</'.($this->crrnd?'':'D:')."$prop[name]>\n"; |
|
1008
|
|
|
break; |
|
1009
|
|
|
} |
|
1010
|
|
|
} else { |
|
1011
|
|
|
// allow multiple values and attributes, required eg. for caldav:supported-calendar-component-set |
|
1012
|
|
|
if ($prop['ns'] && is_array($prop['val'])) { |
|
1013
|
|
|
if (!isset($ns_hash[$prop['ns']])) { |
|
1014
|
|
|
$ns_name = "ns".(count($ns_hash) + 1); |
|
1015
|
|
|
$ns_hash[$prop['ns']] = $ns_name; |
|
1016
|
|
|
} |
|
1017
|
|
|
$vals = $extra_ns = ''; |
|
1018
|
|
|
foreach($prop['val'] as $subprop) |
|
1019
|
|
|
{ |
|
1020
|
|
|
if ($subprop['ns'] && $subprop['ns'] != 'DAV:') { |
|
1021
|
|
|
// register property namespace if not known yet |
|
1022
|
|
|
if (!isset($ns_hash[$subprop['ns']])) { |
|
1023
|
|
|
$ns_name = "ns".(count($ns_hash) + 1); |
|
1024
|
|
|
$ns_hash[$subprop['ns']] = $ns_name; |
|
1025
|
|
|
} else { |
|
1026
|
|
|
$ns_name = $ns_hash[$subprop['ns']]; |
|
1027
|
|
|
} |
|
1028
|
|
|
if (strchr($extra_ns,$extra=' xmlns:'.$ns_name.'="'.$subprop['ns'].'"') === false) { |
|
1029
|
|
|
$extra_ns .= $extra; |
|
1030
|
|
|
} |
|
1031
|
|
|
$ns_name .= ':'; |
|
1032
|
|
|
} elseif ($subprop['ns'] == 'DAV:') { |
|
1033
|
|
|
$ns_name = 'D:'; |
|
1034
|
|
|
} else { |
|
1035
|
|
|
$ns_name = ''; |
|
1036
|
|
|
} |
|
1037
|
|
|
$vals .= "<$ns_name$subprop[name]"; |
|
1038
|
|
|
if (is_array($subprop['val'])) |
|
1039
|
|
|
{ |
|
1040
|
|
|
if (isset($subprop['val'][0])) |
|
1041
|
|
|
{ |
|
1042
|
|
|
$vals .= '>'; |
|
1043
|
|
|
$vals .= $this->_hierarchical_prop_encode($subprop['val'], $subprop['ns'], $ns_defs, $ns_hash); |
|
1044
|
|
|
$vals .= "</$ns_name$subprop[name]>"; |
|
1045
|
|
|
} |
|
1046
|
|
|
else // val contains only attributes, no value |
|
1047
|
|
|
{ |
|
1048
|
|
|
foreach($subprop['val'] as $attr => $val) |
|
1049
|
|
|
{ |
|
1050
|
|
|
$vals .= ' '.$attr.'="'.htmlspecialchars($val, ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8').'"'; |
|
1051
|
|
|
} |
|
1052
|
|
|
$vals .= '/>'; |
|
1053
|
|
|
} |
|
1054
|
|
|
} |
|
1055
|
|
|
else |
|
1056
|
|
|
{ |
|
1057
|
|
|
$vals .= '>'; |
|
1058
|
|
|
if (isset($subprop['raw'])) { |
|
1059
|
|
|
$vals .= '<![CDATA['.$subprop['val'].']]>'; |
|
1060
|
|
|
} else { |
|
1061
|
|
|
// do NOT urlencode mailto href, as no clients understands them |
|
1062
|
|
|
if ($subprop['name'] == 'href' && strpos($subprop['val'], 'mailto:') !== 0) |
|
1063
|
|
|
{ |
|
1064
|
|
|
$subprop['val'] = $this->_urlencode($subprop['val']); |
|
1065
|
|
|
} |
|
1066
|
|
|
$vals .= htmlspecialchars($subprop['val'], ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8'); |
|
1067
|
|
|
} |
|
1068
|
|
|
$vals .= "</$ns_name$subprop[name]>"; |
|
1069
|
|
|
} |
|
1070
|
|
|
} |
|
1071
|
|
|
echo ' <'.$ns_hash[$prop['ns']].":$prop[name]$extra_ns>$vals</".$ns_hash[$prop['ns']].":$prop[name]>\n"; |
|
1072
|
|
|
} else { |
|
1073
|
|
|
if ($prop['raw']) |
|
1074
|
|
|
{ |
|
1075
|
|
|
$val = '<![CDATA['.$prop['val'].']]>'; |
|
1076
|
|
|
} else { |
|
1077
|
|
|
$val = htmlspecialchars($prop['val'], ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8'); |
|
1078
|
|
|
} |
|
1079
|
|
|
$val = $this->_prop_encode($val); |
|
1080
|
|
|
// properties from namespaces != "DAV:" or without any namespace |
|
1081
|
|
|
if ($prop['ns']) { |
|
1082
|
|
|
if ($this->crrnd) { |
|
1083
|
|
|
echo " <$prop[name] xmlns=".'"'.$prop["ns"].'">' |
|
1084
|
|
|
. $val . "</$prop[name]>\n"; |
|
1085
|
|
|
} else { |
|
1086
|
|
|
echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>" |
|
1087
|
|
|
. $val . '</'.$ns_hash[$prop['ns']].":$prop[name]>\n"; |
|
1088
|
|
|
} |
|
1089
|
|
|
} else { |
|
1090
|
|
|
echo " <$prop[name] xmlns=\"\">$val</$prop[name]>\n"; |
|
1091
|
|
|
} |
|
1092
|
|
|
} |
|
1093
|
|
|
} |
|
1094
|
|
|
} |
|
1095
|
|
|
|
|
1096
|
|
|
if ($this->crrnd) |
|
1097
|
|
|
{ |
|
1098
|
|
|
echo " </prop>\n"; |
|
1099
|
|
|
echo " <status>HTTP/1.1 200 OK</status>\n"; |
|
1100
|
|
|
echo " </propstat>\n"; |
|
1101
|
|
|
} |
|
1102
|
|
|
else |
|
1103
|
|
|
{ |
|
1104
|
|
|
echo " </D:prop>\n"; |
|
1105
|
|
|
echo " <D:status>HTTP/1.1 200 OK</D:status>\n"; |
|
1106
|
|
|
echo " </D:propstat>\n"; |
|
1107
|
|
|
} |
|
1108
|
|
|
} |
|
1109
|
|
|
|
|
1110
|
|
|
// now report all properties requested but not found |
|
1111
|
|
|
if (isset($file["noprops"])) { |
|
1112
|
|
|
echo ' <'.($this->crrnd?'':'D:')."propstat>\n"; |
|
1113
|
|
|
echo ' <'.($this->crrnd?'':'D:')."prop>\n"; |
|
1114
|
|
|
|
|
1115
|
|
|
foreach ($file["noprops"] as &$prop) { |
|
1116
|
|
|
if ($prop["ns"] == "DAV:") { |
|
1117
|
|
|
echo ' <'.($this->crrnd?'':'D:')."$prop[name]/>\n"; |
|
1118
|
|
|
} else if ($prop["ns"] == "") { |
|
1119
|
|
|
echo " <$prop[name] xmlns=\"\"/>\n"; |
|
1120
|
|
|
} else { |
|
1121
|
|
|
echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n"; |
|
1122
|
|
|
} |
|
1123
|
|
|
} |
|
1124
|
|
|
|
|
1125
|
|
|
if ($this->crrnd) |
|
1126
|
|
|
{ |
|
1127
|
|
|
echo " </prop>\n"; |
|
1128
|
|
|
echo " <status>HTTP/1.1 404 Not Found</status>\n"; |
|
1129
|
|
|
echo " </propstat>\n"; |
|
1130
|
|
|
} |
|
1131
|
|
|
else |
|
1132
|
|
|
{ |
|
1133
|
|
|
echo " </D:prop>\n"; |
|
1134
|
|
|
echo " <D:status>HTTP/1.1 404 Not Found</D:status>\n"; |
|
1135
|
|
|
echo " </D:propstat>\n"; |
|
1136
|
|
|
} |
|
1137
|
|
|
} |
|
1138
|
|
|
|
|
1139
|
|
|
// 404 Not Found status element for WebDAV sync report |
|
1140
|
|
|
if (!isset($file['props']) && !isset($file['noprops'])) |
|
1141
|
|
|
{ |
|
1142
|
|
|
if ($this->crrnd) |
|
1143
|
|
|
{ |
|
1144
|
|
|
echo " <status>HTTP/1.1 404 Not Found</status>\n"; |
|
1145
|
|
|
} |
|
1146
|
|
|
else |
|
1147
|
|
|
{ |
|
1148
|
|
|
echo " <D:status>HTTP/1.1 404 Not Found</D:status>\n"; |
|
1149
|
|
|
} |
|
1150
|
|
|
} |
|
1151
|
|
|
|
|
1152
|
|
|
echo ' </'.($this->crrnd?'':'D:')."response>\n"; |
|
1153
|
|
|
} |
|
1154
|
|
|
} |
|
1155
|
|
|
|
|
1156
|
|
|
|
|
1157
|
|
|
// }}} |
|
1158
|
|
|
|
|
1159
|
|
|
// {{{ http_PROPPATCH() |
|
1160
|
|
|
|
|
1161
|
|
|
/** |
|
1162
|
|
|
* PROPPATCH method handler |
|
1163
|
|
|
* |
|
1164
|
|
|
* @param void |
|
1165
|
|
|
* @return void |
|
1166
|
|
|
*/ |
|
1167
|
|
|
function http_PROPPATCH() |
|
1168
|
|
|
{ |
|
1169
|
|
|
if ($this->_check_lock_status($this->path)) { |
|
1170
|
|
|
$options = Array(); |
|
1171
|
|
|
|
|
1172
|
|
|
$options["path"] = $this->path; |
|
1173
|
|
|
|
|
1174
|
|
|
$propinfo = new _parse_proppatch("php://input", $this->store_request); |
|
1175
|
|
|
if ($this->store_request) $this->request = $propinfo->request; |
|
1176
|
|
|
|
|
1177
|
|
|
if (!$propinfo->success) { |
|
1178
|
|
|
$this->http_status("400 Error"); |
|
1179
|
|
|
return; |
|
1180
|
|
|
} |
|
1181
|
|
|
|
|
1182
|
|
|
$options['props'] = $propinfo->props; |
|
1183
|
|
|
|
|
1184
|
|
|
$responsedescr = $this->PROPPATCH($options); |
|
|
|
|
|
|
1185
|
|
|
|
|
1186
|
|
|
$this->http_status("207 Multi-Status"); |
|
1187
|
|
|
header('Content-Type: text/xml; charset="utf-8"'); |
|
1188
|
|
|
|
|
1189
|
|
|
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; |
|
1190
|
|
|
|
|
1191
|
|
|
echo "<D:multistatus xmlns:D=\"DAV:\">\n"; |
|
1192
|
|
|
echo ' <'.($this->crrnd?'':'D:')."response>\n"; |
|
1193
|
|
|
echo ' <'.($this->crrnd?'':'D:')."href>".$this->_urlencode($this->_mergePaths($this->_SERVER["SCRIPT_NAME"], $this->path)).'</'.($this->crrnd?'':'D:')."href>\n"; |
|
1194
|
|
|
|
|
1195
|
|
|
foreach ($options["props"] as $prop) { |
|
1196
|
|
|
echo ' <'.($this->crrnd?'':'D:')."propstat>\n"; |
|
1197
|
|
|
echo ' <'.($this->crrnd?'':'D:')."prop><$prop[name] xmlns=\"$prop[ns]\"/></".($this->crrnd?'':'D:')."prop>\n"; |
|
1198
|
|
|
echo ' <'.($this->crrnd?'':'D:')."status>HTTP/1.1 $prop[status]</".($this->crrnd?'':'D:')."status>\n"; |
|
1199
|
|
|
echo ' </'.($this->crrnd?'':'D:')."propstat>\n"; |
|
1200
|
|
|
} |
|
1201
|
|
|
|
|
1202
|
|
|
if ($responsedescr) { |
|
1203
|
|
|
echo ' <'.($this->crrnd?'':'D:')."responsedescription>". |
|
1204
|
|
|
$this->_prop_encode(htmlspecialchars($responsedescr, ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8')). |
|
1205
|
|
|
'</'.($this->crrnd?'':'D:')."responsedescription>\n"; |
|
1206
|
|
|
} |
|
1207
|
|
|
|
|
1208
|
|
|
echo ' </'.($this->crrnd?'':'D:')."response>\n"; |
|
1209
|
|
|
echo '</'.($this->crrnd?'':'D:')."multistatus>\n"; |
|
1210
|
|
|
} else { |
|
1211
|
|
|
$this->http_status("423 Locked"); |
|
1212
|
|
|
} |
|
1213
|
|
|
} |
|
1214
|
|
|
|
|
1215
|
|
|
// }}} |
|
1216
|
|
|
|
|
1217
|
|
|
|
|
1218
|
|
|
// {{{ http_MKCOL() |
|
1219
|
|
|
|
|
1220
|
|
|
/** |
|
1221
|
|
|
* MKCOL method handler |
|
1222
|
|
|
* |
|
1223
|
|
|
* @param void |
|
1224
|
|
|
* @return void |
|
1225
|
|
|
*/ |
|
1226
|
|
|
function http_MKCOL() |
|
1227
|
|
|
{ |
|
1228
|
|
|
$options = Array(); |
|
1229
|
|
|
|
|
1230
|
|
|
$options["path"] = $this->path; |
|
1231
|
|
|
|
|
1232
|
|
|
$stat = $this->MKCOL($options); |
|
|
|
|
|
|
1233
|
|
|
|
|
1234
|
|
|
$this->http_status($stat); |
|
1235
|
|
|
} |
|
1236
|
|
|
|
|
1237
|
|
|
// }}} |
|
1238
|
|
|
|
|
1239
|
|
|
/** |
|
1240
|
|
|
* Check or set if we want ot use compression as transfer encoding |
|
1241
|
|
|
* |
|
1242
|
|
|
* If we use compression via zlib.output_compression as transfer encoding, |
|
1243
|
|
|
* we can NOT send Content-Length headers, as the have to reflect size |
|
1244
|
|
|
* AFTER applying compression/transfer encoding. |
|
1245
|
|
|
* |
|
1246
|
|
|
* @param boolean $set =null |
|
1247
|
|
|
* @return boolean true if we use compression, false otherwise |
|
1248
|
|
|
*/ |
|
1249
|
|
|
public static function use_compression($set=null) |
|
1250
|
|
|
{ |
|
1251
|
|
|
static $compression = null; |
|
1252
|
|
|
if (isset($set)) |
|
1253
|
|
|
{ |
|
1254
|
|
|
ini_set('zlib.output_compression', $compression=(boolean)$set); |
|
1255
|
|
|
} |
|
1256
|
|
|
elseif (!isset($compression)) |
|
1257
|
|
|
{ |
|
1258
|
|
|
$compression = (boolean)ini_get('zlib.output_compression'); |
|
1259
|
|
|
} |
|
1260
|
|
|
//error_log(__METHOD__."(".array2string($set).") returning ".array2string($compression)); |
|
1261
|
|
|
return $compression; |
|
1262
|
|
|
} |
|
1263
|
|
|
|
|
1264
|
|
|
// {{{ http_GET() |
|
1265
|
|
|
|
|
1266
|
|
|
/** |
|
1267
|
|
|
* GET method handler |
|
1268
|
|
|
* |
|
1269
|
|
|
* @param void |
|
1270
|
|
|
* @return void |
|
1271
|
|
|
*/ |
|
1272
|
|
|
function http_GET() |
|
1273
|
|
|
{ |
|
1274
|
|
|
// TODO check for invalid stream |
|
1275
|
|
|
$options = Array(); |
|
1276
|
|
|
$options["path"] = $this->path; |
|
1277
|
|
|
|
|
1278
|
|
|
$this->_get_ranges($options); |
|
1279
|
|
|
|
|
1280
|
|
|
if (true === ($status = $this->GET($options))) { |
|
|
|
|
|
|
1281
|
|
|
if (!headers_sent()) { |
|
1282
|
|
|
$status = "200 OK"; |
|
1283
|
|
|
|
|
1284
|
|
|
if (!isset($options['mimetype'])) { |
|
1285
|
|
|
$options['mimetype'] = "application/octet-stream"; |
|
1286
|
|
|
} |
|
1287
|
|
|
// switching off zlib.output_compression for everything but text files, |
|
1288
|
|
|
// as the double compression of zip files makes problems eg. with lighttpd |
|
1289
|
|
|
// and anyway little sense with with other content like pictures |
|
1290
|
|
|
if (substr($options['mimetype'],0,5) != 'text/') |
|
1291
|
|
|
{ |
|
1292
|
|
|
self::use_compression(false); |
|
1293
|
|
|
} |
|
1294
|
|
|
header("Content-type: $options[mimetype]"); |
|
1295
|
|
|
|
|
1296
|
|
|
if (isset($options['mtime'])) { |
|
1297
|
|
|
header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT"); |
|
1298
|
|
|
} |
|
1299
|
|
|
// fix for IE and https, thanks to [email protected] |
|
1300
|
|
|
// see http://us3.php.net/manual/en/function.header.php#83219 |
|
1301
|
|
|
// and http://support.microsoft.com/kb/812935 |
|
1302
|
|
|
header("Cache-Control: maxage=1"); //In seconds |
|
1303
|
|
|
header("Pragma: public"); |
|
1304
|
|
|
|
|
1305
|
|
|
if (isset($options['stream'])) { |
|
1306
|
|
|
// GET handler returned a stream |
|
1307
|
|
|
if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) { |
|
1308
|
|
|
// partial request and stream is seekable |
|
1309
|
|
|
|
|
1310
|
|
|
if (count($options['ranges']) === 1) { |
|
1311
|
|
|
$range = $options['ranges'][0]; |
|
1312
|
|
|
|
|
1313
|
|
|
if (isset($range['start'])) { |
|
1314
|
|
|
fseek($options['stream'], $range['start'], SEEK_SET); |
|
1315
|
|
|
if (feof($options['stream'])) { |
|
1316
|
|
|
$this->http_status($status = "416 Requested range not satisfiable"); |
|
1317
|
|
|
return; |
|
1318
|
|
|
} |
|
1319
|
|
|
|
|
1320
|
|
|
if (!empty($range['end'])) { |
|
1321
|
|
|
$size = $range['end']-$range['start']+1; |
|
1322
|
|
|
$this->http_status($status = "206 Partial content"); |
|
1323
|
|
|
if (!self::use_compression()) header("Content-Length: $size"); |
|
1324
|
|
|
header("Content-Range: bytes $range[start]-$range[end]/" |
|
1325
|
|
|
. (isset($options['size']) ? $options['size'] : "*")); |
|
1326
|
|
|
while ($size > 0 && !feof($options['stream'])) { |
|
1327
|
|
|
$buffer = fread($options['stream'], $size < 8192 ? $size : 8192); |
|
1328
|
|
|
$size -= self::bytes($buffer); |
|
1329
|
|
|
echo $buffer; |
|
1330
|
|
|
} |
|
1331
|
|
|
} else { |
|
1332
|
|
|
$this->http_status($status = "206 Partial content"); |
|
1333
|
|
|
if (isset($options['size'])) { |
|
1334
|
|
|
if (!self::use_compression()) header("Content-Length: ".($options['size'] - $range['start'])); |
|
1335
|
|
|
header("Content-Range: bytes ".$range['start']."-". |
|
1336
|
|
|
(isset($options['size']) ? $options['size']-1 : "")."/" |
|
1337
|
|
|
. (isset($options['size']) ? $options['size'] : "*")); |
|
1338
|
|
|
} |
|
1339
|
|
|
fpassthru($options['stream']); |
|
1340
|
|
|
} |
|
1341
|
|
|
} else { |
|
1342
|
|
|
if (!self::use_compression()) header("Content-length: ".$range['last']); |
|
1343
|
|
|
fseek($options['stream'], -$range['last'], SEEK_END); |
|
1344
|
|
|
fpassthru($options['stream']); |
|
1345
|
|
|
} |
|
1346
|
|
|
} else { |
|
1347
|
|
|
$this->_multipart_byterange_header(); // init multipart |
|
1348
|
|
|
foreach ($options['ranges'] as $range) { |
|
1349
|
|
|
// TODO what if size unknown? 500? |
|
1350
|
|
|
if (isset($range['start'])) { |
|
1351
|
|
|
$from = $range['start']; |
|
1352
|
|
|
$to = !empty($range['end']) ? $range['end'] : $options['size']-1; |
|
1353
|
|
|
} else { |
|
1354
|
|
|
$from = $options['size'] - $range['last']-1; |
|
1355
|
|
|
$to = $options['size'] -1; |
|
1356
|
|
|
} |
|
1357
|
|
|
$total = isset($options['size']) ? $options['size'] : "*"; |
|
1358
|
|
|
$size = $to - $from + 1; |
|
1359
|
|
|
$this->_multipart_byterange_header($options['mimetype'], $from, $to, $total); |
|
1360
|
|
|
|
|
1361
|
|
|
|
|
1362
|
|
|
fseek($options['stream'], $from, SEEK_SET); |
|
1363
|
|
|
while ($size && !feof($options['stream'])) { |
|
1364
|
|
|
$buffer = fread($options['stream'], 4096); |
|
1365
|
|
|
$size -= self::bytes($buffer); |
|
1366
|
|
|
echo $buffer; |
|
1367
|
|
|
} |
|
1368
|
|
|
} |
|
1369
|
|
|
$this->_multipart_byterange_header(); // end multipart |
|
1370
|
|
|
} |
|
1371
|
|
|
} else { |
|
1372
|
|
|
// normal request or stream isn't seekable, return full content |
|
1373
|
|
|
if (isset($options['size']) && !self::use_compression()) { |
|
1374
|
|
|
header("Content-Length: ".$options['size']); |
|
1375
|
|
|
} |
|
1376
|
|
|
fpassthru($options['stream']); |
|
1377
|
|
|
return; // no more headers |
|
1378
|
|
|
} |
|
1379
|
|
|
} elseif (isset($options['data'])) { |
|
1380
|
|
|
if (is_array($options['data'])) { |
|
1381
|
|
|
// reply to partial request |
|
1382
|
|
|
} else { |
|
1383
|
|
|
if (!self::use_compression()) header("Content-Length: ".self::bytes($options['data'])); |
|
1384
|
|
|
echo $options['data']; |
|
1385
|
|
|
} |
|
1386
|
|
|
} |
|
1387
|
|
|
} |
|
1388
|
|
|
} |
|
1389
|
|
|
|
|
1390
|
|
|
if (!headers_sent()) { |
|
1391
|
|
|
if (false === $status) { |
|
1392
|
|
|
$this->http_status("404 not found"); |
|
1393
|
|
|
} else { |
|
1394
|
|
|
// TODO: check setting of headers in various code paths above |
|
1395
|
|
|
$this->http_status("$status"); |
|
1396
|
|
|
} |
|
1397
|
|
|
} |
|
1398
|
|
|
} |
|
1399
|
|
|
|
|
1400
|
|
|
|
|
1401
|
|
|
/** |
|
1402
|
|
|
* parse HTTP Range: header |
|
1403
|
|
|
* |
|
1404
|
|
|
* @param array options array to store result in |
|
|
|
|
|
|
1405
|
|
|
* @return void |
|
1406
|
|
|
*/ |
|
1407
|
|
|
function _get_ranges(&$options) |
|
1408
|
|
|
{ |
|
1409
|
|
|
// process Range: header if present |
|
1410
|
|
|
if (isset($this->_SERVER['HTTP_RANGE'])) { |
|
1411
|
|
|
|
|
1412
|
|
|
// we only support standard "bytes" range specifications for now |
|
1413
|
|
|
$matches = null; |
|
1414
|
|
|
if (preg_match('/bytes\s*=\s*(.+)/', $this->_SERVER['HTTP_RANGE'], $matches)) { |
|
1415
|
|
|
$options["ranges"] = array(); |
|
1416
|
|
|
|
|
1417
|
|
|
// ranges are comma separated |
|
1418
|
|
|
foreach (explode(",", $matches[1]) as $range) { |
|
1419
|
|
|
// ranges are either from-to pairs or just end positions |
|
1420
|
|
|
list($start, $end) = explode("-", $range); |
|
1421
|
|
|
$options["ranges"][] = ($start==="") |
|
1422
|
|
|
? array("last"=>$end) |
|
1423
|
|
|
: array("start"=>$start, "end"=>$end); |
|
1424
|
|
|
} |
|
1425
|
|
|
} |
|
1426
|
|
|
} |
|
1427
|
|
|
} |
|
1428
|
|
|
|
|
1429
|
|
|
/** |
|
1430
|
|
|
* generate separator headers for multipart response |
|
1431
|
|
|
* |
|
1432
|
|
|
* first and last call happen without parameters to generate |
|
1433
|
|
|
* the initial header and closing sequence, all calls inbetween |
|
1434
|
|
|
* require content mimetype, start and end byte position and |
|
1435
|
|
|
* optionaly the total byte length of the requested resource |
|
1436
|
|
|
* |
|
1437
|
|
|
* @param string mimetype |
|
|
|
|
|
|
1438
|
|
|
* @param int start byte position |
|
1439
|
|
|
* @param int end byte position |
|
1440
|
|
|
* @param int total resource byte size |
|
|
|
|
|
|
1441
|
|
|
*/ |
|
1442
|
|
|
function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false) |
|
1443
|
|
|
{ |
|
1444
|
|
|
if ($mimetype === false) { |
|
1445
|
|
|
if (!isset($this->multipart_separator)) { |
|
1446
|
|
|
// initial |
|
1447
|
|
|
|
|
1448
|
|
|
// a little naive, this sequence *might* be part of the content |
|
1449
|
|
|
// but it's really not likely and rather expensive to check |
|
1450
|
|
|
$this->multipart_separator = "SEPARATOR_".md5(microtime()); |
|
|
|
|
|
|
1451
|
|
|
|
|
1452
|
|
|
// generate HTTP header |
|
1453
|
|
|
header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator); |
|
1454
|
|
|
} else { |
|
1455
|
|
|
// final |
|
1456
|
|
|
|
|
1457
|
|
|
// generate closing multipart sequence |
|
1458
|
|
|
echo "\n--{$this->multipart_separator}--"; |
|
1459
|
|
|
} |
|
1460
|
|
|
} else { |
|
1461
|
|
|
// generate separator and header for next part |
|
1462
|
|
|
echo "\n--{$this->multipart_separator}\n"; |
|
1463
|
|
|
echo "Content-type: $mimetype\n"; |
|
1464
|
|
|
echo "Content-range: $from-$to/". ($total === false ? "*" : $total); |
|
1465
|
|
|
echo "\n\n"; |
|
1466
|
|
|
} |
|
1467
|
|
|
} |
|
1468
|
|
|
|
|
1469
|
|
|
|
|
1470
|
|
|
|
|
1471
|
|
|
// }}} |
|
1472
|
|
|
|
|
1473
|
|
|
// {{{ http_HEAD() |
|
1474
|
|
|
|
|
1475
|
|
|
/** |
|
1476
|
|
|
* HEAD method handler |
|
1477
|
|
|
* |
|
1478
|
|
|
* @param void |
|
1479
|
|
|
* @return void |
|
1480
|
|
|
*/ |
|
1481
|
|
|
function http_HEAD() |
|
1482
|
|
|
{ |
|
1483
|
|
|
$status = false; |
|
1484
|
|
|
$options = Array(); |
|
1485
|
|
|
$options["path"] = $this->path; |
|
1486
|
|
|
|
|
1487
|
|
|
if (method_exists($this, "HEAD")) { |
|
1488
|
|
|
$status = $this->head($options); |
|
1489
|
|
|
} else if (method_exists($this, "GET")) { |
|
1490
|
|
|
ob_start(); |
|
1491
|
|
|
$status = $this->GET($options); |
|
1492
|
|
|
if (!isset($options['size'])) { |
|
1493
|
|
|
$options['size'] = ob_get_length(); |
|
1494
|
|
|
} |
|
1495
|
|
|
ob_end_clean(); |
|
1496
|
|
|
} |
|
1497
|
|
|
|
|
1498
|
|
|
if (!isset($options['mimetype'])) { |
|
1499
|
|
|
$options['mimetype'] = "application/octet-stream"; |
|
1500
|
|
|
} |
|
1501
|
|
|
header("Content-type: $options[mimetype]"); |
|
1502
|
|
|
|
|
1503
|
|
|
if (isset($options['mtime'])) { |
|
1504
|
|
|
header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT"); |
|
1505
|
|
|
} |
|
1506
|
|
|
|
|
1507
|
|
|
if (isset($options['size'])) { |
|
1508
|
|
|
header("Content-Length: ".$options['size']); |
|
1509
|
|
|
} |
|
1510
|
|
|
|
|
1511
|
|
|
if ($status === true) $status = "200 OK"; |
|
1512
|
|
|
if ($status === false) $status = "404 Not found"; |
|
1513
|
|
|
|
|
1514
|
|
|
$this->http_status($status); |
|
1515
|
|
|
} |
|
1516
|
|
|
|
|
1517
|
|
|
// }}} |
|
1518
|
|
|
|
|
1519
|
|
|
// {{{ http_POST() |
|
1520
|
|
|
|
|
1521
|
|
|
/** |
|
1522
|
|
|
* POST method handler |
|
1523
|
|
|
* |
|
1524
|
|
|
* @param void |
|
1525
|
|
|
* @return void |
|
1526
|
|
|
*/ |
|
1527
|
|
|
function http_POST() |
|
1528
|
|
|
{ |
|
1529
|
|
|
$status = '405 Method not allowed'; |
|
1530
|
|
|
$options = Array(); |
|
1531
|
|
|
$options['path'] = $this->path; |
|
1532
|
|
|
|
|
1533
|
|
|
if (isset($this->_SERVER['CONTENT_LENGTH'])) |
|
1534
|
|
|
{ |
|
1535
|
|
|
$options['content_length'] = $this->_SERVER['CONTENT_LENGTH']; |
|
1536
|
|
|
} |
|
1537
|
|
|
elseif (isset($this->_SERVER['X-Expected-Entity-Length'])) |
|
1538
|
|
|
{ |
|
1539
|
|
|
// MacOS gives us that hint |
|
1540
|
|
|
$options['content_length'] = $this->_SERVER['X-Expected-Entity-Length']; |
|
1541
|
|
|
} |
|
1542
|
|
|
|
|
1543
|
|
|
// get the Content-type |
|
1544
|
|
|
if (isset($this->_SERVER["CONTENT_TYPE"])) { |
|
1545
|
|
|
// for now we do not support any sort of multipart requests |
|
1546
|
|
|
if (!strncmp($this->_SERVER["CONTENT_TYPE"], 'multipart/', 10)) { |
|
1547
|
|
|
$this->http_status('501 not implemented'); |
|
1548
|
|
|
echo 'The service does not support mulipart POST requests'; |
|
1549
|
|
|
return; |
|
1550
|
|
|
} |
|
1551
|
|
|
$options['content_type'] = $this->_SERVER['CONTENT_TYPE']; |
|
1552
|
|
|
} else { |
|
1553
|
|
|
// default content type if none given |
|
1554
|
|
|
$options['content_type'] = 'application/octet-stream'; |
|
1555
|
|
|
} |
|
1556
|
|
|
|
|
1557
|
|
|
$options['stream'] = fopen('php://input', 'r'); |
|
1558
|
|
|
switch($this->_SERVER['HTTP_CONTENT_ENCODING']) |
|
1559
|
|
|
{ |
|
1560
|
|
|
case 'gzip': |
|
1561
|
|
|
case 'deflate': //zlib |
|
1562
|
|
|
if (extension_loaded('zlib')) |
|
1563
|
|
|
{ |
|
1564
|
|
|
stream_filter_append($options['stream'], 'zlib.inflate', STREAM_FILTER_READ); |
|
1565
|
|
|
} |
|
1566
|
|
|
} |
|
1567
|
|
|
// store request in $this->request, if requested via $this->store_request |
|
1568
|
|
|
if ($this->store_request) |
|
1569
|
|
|
{ |
|
1570
|
|
|
$options['content'] = ''; |
|
1571
|
|
|
while(!feof($options['stream'])) |
|
1572
|
|
|
{ |
|
1573
|
|
|
$options['content'] .= fread($options['stream'],8192); |
|
1574
|
|
|
} |
|
1575
|
|
|
$this->request =& $options['content']; |
|
1576
|
|
|
unset($options['stream']); |
|
1577
|
|
|
} |
|
1578
|
|
|
|
|
1579
|
|
|
/* RFC 2616 2.6 says: "The recipient of the entity MUST NOT |
|
1580
|
|
|
ignore any Content-* (e.g. Content-Range) headers that it |
|
1581
|
|
|
does not understand or implement and MUST return a 501 |
|
1582
|
|
|
(Not Implemented) response in such cases." |
|
1583
|
|
|
*/ |
|
1584
|
|
|
foreach ($this->_SERVER as $key => $val) { |
|
1585
|
|
|
if (strncmp($key, 'HTTP_CONTENT', 11)) continue; |
|
1586
|
|
|
switch ($key) { |
|
1587
|
|
|
case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11 |
|
1588
|
|
|
switch($this->_SERVER['HTTP_CONTENT_ENCODING']) |
|
1589
|
|
|
{ |
|
1590
|
|
|
case 'gzip': |
|
1591
|
|
|
case 'deflate': //zlib |
|
1592
|
|
|
if (extension_loaded('zlib')) break; |
|
1593
|
|
|
// fall through for no zlib support |
|
1594
|
|
|
default: |
|
1595
|
|
|
$this->http_status('415 Unsupported Media Type'); |
|
1596
|
|
|
echo "The service does not support '$val' content encoding"; |
|
1597
|
|
|
return; |
|
1598
|
|
|
} |
|
1599
|
|
|
break; |
|
1600
|
|
|
|
|
1601
|
|
|
case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12 |
|
1602
|
|
|
// we assume it is not critical if this one is ignored |
|
1603
|
|
|
// in the actual POST implementation ... |
|
1604
|
|
|
$options['content_language'] = $val; |
|
1605
|
|
|
break; |
|
1606
|
|
|
|
|
1607
|
|
|
case 'HTTP_CONTENT_LENGTH': |
|
1608
|
|
|
// defined on IIS and has the same value as CONTENT_LENGTH |
|
1609
|
|
|
break; |
|
1610
|
|
|
|
|
1611
|
|
|
case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14 |
|
1612
|
|
|
/* The meaning of the Content-Location header in PUT |
|
1613
|
|
|
or POST requests is undefined; servers are free |
|
1614
|
|
|
to ignore it in those cases. */ |
|
1615
|
|
|
break; |
|
1616
|
|
|
|
|
1617
|
|
|
case 'HTTP_CONTENT_RANGE': // RFC 2616 14.16 |
|
1618
|
|
|
// single byte range requests are supported |
|
1619
|
|
|
// the header format is also specified in RFC 2616 14.16 |
|
1620
|
|
|
// TODO we have to ensure that implementations support this or send 501 instead |
|
1621
|
|
|
$matches = null; |
|
1622
|
|
|
if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) { |
|
1623
|
|
|
$this->http_status('400 bad request'); |
|
1624
|
|
|
echo 'The service does only support single byte ranges'; |
|
1625
|
|
|
return; |
|
1626
|
|
|
} |
|
1627
|
|
|
|
|
1628
|
|
|
$range = array('start'=>$matches[1], 'end'=>$matches[2]); |
|
1629
|
|
|
if (is_numeric($matches[3])) { |
|
1630
|
|
|
$range['total_length'] = $matches[3]; |
|
1631
|
|
|
} |
|
1632
|
|
|
$options['ranges'][] = $range; |
|
1633
|
|
|
|
|
1634
|
|
|
// TODO make sure the implementation supports partial POST |
|
1635
|
|
|
// this has to be done in advance to avoid data being overwritten |
|
1636
|
|
|
// on implementations that do not support this ... |
|
1637
|
|
|
break; |
|
1638
|
|
|
|
|
1639
|
|
|
case 'HTTP_CONTENT_TYPE': |
|
1640
|
|
|
// defined on IIS and has the same value as CONTENT_TYPE |
|
1641
|
|
|
break; |
|
1642
|
|
|
|
|
1643
|
|
|
case 'HTTP_CONTENT_MD5': // RFC 2616 14.15 |
|
1644
|
|
|
// TODO: maybe we can just pretend here? |
|
1645
|
|
|
$this->http_status('501 not implemented'); |
|
1646
|
|
|
echo 'The service does not support content MD5 checksum verification'; |
|
1647
|
|
|
return; |
|
1648
|
|
|
|
|
1649
|
|
|
case 'HTTP_CONTENT_DISPOSITION': |
|
1650
|
|
|
// do NOT care about Content-Disposition in POST requests required by CalDAV managed attachments |
|
1651
|
|
|
break; |
|
1652
|
|
|
|
|
1653
|
|
|
default: |
|
1654
|
|
|
// any other unknown Content-* headers |
|
1655
|
|
|
$this->http_status('501 not implemented'); |
|
1656
|
|
|
echo "The service does not support '$key'"; |
|
1657
|
|
|
return; |
|
1658
|
|
|
} |
|
1659
|
|
|
} |
|
1660
|
|
|
|
|
1661
|
|
|
if (method_exists($this, 'POST')) { |
|
1662
|
|
|
$status = $this->POST($options); |
|
1663
|
|
|
|
|
1664
|
|
|
if ($status === false) { |
|
1665
|
|
|
$status = '400 Something went wrong'; |
|
1666
|
|
|
} else if ($status === true) { |
|
1667
|
|
|
$status = '200 OK'; |
|
1668
|
|
|
} else if (is_resource($status) && get_resource_type($status) == 'stream') { |
|
1669
|
|
|
$stream = $status; |
|
1670
|
|
|
|
|
1671
|
|
|
$status = empty($options['new']) ? '200 OK' : '201 Created'; |
|
1672
|
|
|
|
|
1673
|
|
|
if (!empty($options['ranges'])) { |
|
1674
|
|
|
// TODO multipart support is missing (see also above) |
|
1675
|
|
|
if (0 == fseek($stream, $range[0]['start'], SEEK_SET)) { |
|
1676
|
|
|
$length = $range[0]['end']-$range[0]['start']+1; |
|
1677
|
|
|
if (!fwrite($stream, fread($options['stream'], $length))) { |
|
1678
|
|
|
$status = '403 Forbidden'; |
|
1679
|
|
|
} |
|
1680
|
|
|
} else { |
|
1681
|
|
|
$status = '403 Forbidden'; |
|
1682
|
|
|
} |
|
1683
|
|
|
} else { |
|
1684
|
|
|
while (!feof($options['stream'])) { |
|
1685
|
|
|
if (false === fwrite($stream, fread($options['stream'], 4096))) { |
|
1686
|
|
|
$status = '403 Forbidden'; |
|
1687
|
|
|
break; |
|
1688
|
|
|
} |
|
1689
|
|
|
} |
|
1690
|
|
|
} |
|
1691
|
|
|
fclose($stream); |
|
1692
|
|
|
} |
|
1693
|
|
|
} |
|
1694
|
|
|
$this->http_status($status); |
|
1695
|
|
|
} |
|
1696
|
|
|
|
|
1697
|
|
|
// }}} |
|
1698
|
|
|
|
|
1699
|
|
|
// {{{ http_PUT() |
|
1700
|
|
|
|
|
1701
|
|
|
/** |
|
1702
|
|
|
* PUT method handler |
|
1703
|
|
|
* |
|
1704
|
|
|
* @param void |
|
1705
|
|
|
* @return void |
|
1706
|
|
|
*/ |
|
1707
|
|
|
function http_PUT() |
|
1708
|
|
|
{ |
|
1709
|
|
|
if ($this->_check_lock_status($this->path)) { |
|
1710
|
|
|
$options = Array(); |
|
1711
|
|
|
$options["path"] = $this->path; |
|
1712
|
|
|
|
|
1713
|
|
|
if (isset($this->_SERVER['CONTENT_LENGTH'])) |
|
1714
|
|
|
{ |
|
1715
|
|
|
$options['content_length'] = $this->_SERVER['CONTENT_LENGTH']; |
|
1716
|
|
|
} |
|
1717
|
|
|
elseif (isset($this->_SERVER['X-Expected-Entity-Length'])) |
|
1718
|
|
|
{ |
|
1719
|
|
|
// MacOS gives us that hint |
|
1720
|
|
|
$options['content_length'] = $this->_SERVER['X-Expected-Entity-Length']; |
|
1721
|
|
|
} |
|
1722
|
|
|
|
|
1723
|
|
|
// get the Content-type |
|
1724
|
|
|
if (isset($this->_SERVER["CONTENT_TYPE"])) { |
|
1725
|
|
|
// for now we do not support any sort of multipart requests |
|
1726
|
|
|
if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) { |
|
1727
|
|
|
$this->http_status("501 not implemented"); |
|
1728
|
|
|
echo "The service does not support multipart PUT requests"; |
|
1729
|
|
|
return; |
|
1730
|
|
|
} |
|
1731
|
|
|
$options["content_type"] = $this->_SERVER["CONTENT_TYPE"]; |
|
1732
|
|
|
} else { |
|
1733
|
|
|
// default content type if none given |
|
1734
|
|
|
$options["content_type"] = "application/octet-stream"; |
|
1735
|
|
|
} |
|
1736
|
|
|
|
|
1737
|
|
|
$options["stream"] = fopen("php://input", "r"); |
|
1738
|
|
|
switch($this->_SERVER['HTTP_CONTENT_ENCODING']) |
|
1739
|
|
|
{ |
|
1740
|
|
|
case 'gzip': |
|
1741
|
|
|
case 'deflate': //zlib |
|
1742
|
|
|
if (extension_loaded('zlib')) |
|
1743
|
|
|
{ |
|
1744
|
|
|
stream_filter_append($options['stream'], 'zlib.inflate', STREAM_FILTER_READ); |
|
1745
|
|
|
} |
|
1746
|
|
|
} |
|
1747
|
|
|
// store request in $this->request, if requested via $this->store_request |
|
1748
|
|
|
if ($this->store_request) |
|
1749
|
|
|
{ |
|
1750
|
|
|
$options['content'] = ''; |
|
1751
|
|
|
while(!feof($options['stream'])) |
|
1752
|
|
|
{ |
|
1753
|
|
|
$options['content'] .= fread($options['stream'],8192); |
|
1754
|
|
|
} |
|
1755
|
|
|
$this->request =& $options['content']; |
|
1756
|
|
|
unset($options['stream']); |
|
1757
|
|
|
} |
|
1758
|
|
|
|
|
1759
|
|
|
/* RFC 2616 2.6 says: "The recipient of the entity MUST NOT |
|
1760
|
|
|
ignore any Content-* (e.g. Content-Range) headers that it |
|
1761
|
|
|
does not understand or implement and MUST return a 501 |
|
1762
|
|
|
(Not Implemented) response in such cases." |
|
1763
|
|
|
*/ |
|
1764
|
|
|
foreach ($this->_SERVER as $key => $val) { |
|
1765
|
|
|
if (strncmp($key, "HTTP_CONTENT", 11)) continue; |
|
1766
|
|
|
switch ($key) { |
|
1767
|
|
|
case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11 |
|
1768
|
|
|
switch($this->_SERVER['HTTP_CONTENT_ENCODING']) |
|
1769
|
|
|
{ |
|
1770
|
|
|
case 'gzip': |
|
1771
|
|
|
case 'deflate': //zlib |
|
1772
|
|
|
if (extension_loaded('zlib')) break; |
|
1773
|
|
|
// fall through for no zlib support |
|
1774
|
|
|
default: |
|
1775
|
|
|
$this->http_status('415 Unsupported Media Type'); |
|
1776
|
|
|
echo "The service does not support '$val' content encoding"; |
|
1777
|
|
|
return; |
|
1778
|
|
|
} |
|
1779
|
|
|
break; |
|
1780
|
|
|
|
|
1781
|
|
|
case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12 |
|
1782
|
|
|
// we assume it is not critical if this one is ignored |
|
1783
|
|
|
// in the actual PUT implementation ... |
|
1784
|
|
|
$options["content_language"] = $val; |
|
1785
|
|
|
break; |
|
1786
|
|
|
|
|
1787
|
|
|
case 'HTTP_CONTENT_LENGTH': |
|
1788
|
|
|
// defined on IIS and has the same value as CONTENT_LENGTH |
|
1789
|
|
|
break; |
|
1790
|
|
|
|
|
1791
|
|
|
case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14 |
|
1792
|
|
|
/* The meaning of the Content-Location header in PUT |
|
1793
|
|
|
or POST requests is undefined; servers are free |
|
1794
|
|
|
to ignore it in those cases. */ |
|
1795
|
|
|
break; |
|
1796
|
|
|
|
|
1797
|
|
|
case 'HTTP_CONTENT_RANGE': // RFC 2616 14.16 |
|
1798
|
|
|
// single byte range requests are supported |
|
1799
|
|
|
// the header format is also specified in RFC 2616 14.16 |
|
1800
|
|
|
// TODO we have to ensure that implementations support this or send 501 instead |
|
1801
|
|
|
$matches = null; |
|
1802
|
|
|
if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) { |
|
1803
|
|
|
$this->http_status("400 bad request"); |
|
1804
|
|
|
echo "The service does only support single byte ranges"; |
|
1805
|
|
|
return; |
|
1806
|
|
|
} |
|
1807
|
|
|
|
|
1808
|
|
|
$range = array("start" => $matches[1], "end" => $matches[2]); |
|
1809
|
|
|
if (is_numeric($matches[3])) { |
|
1810
|
|
|
$range["total_length"] = $matches[3]; |
|
1811
|
|
|
} |
|
1812
|
|
|
|
|
1813
|
|
|
if (!isset($options['ranges'])) { |
|
1814
|
|
|
$options['ranges'] = array(); |
|
1815
|
|
|
} |
|
1816
|
|
|
|
|
1817
|
|
|
$options["ranges"][] = $range; |
|
1818
|
|
|
|
|
1819
|
|
|
// TODO make sure the implementation supports partial PUT |
|
1820
|
|
|
// this has to be done in advance to avoid data being overwritten |
|
1821
|
|
|
// on implementations that do not support this ... |
|
1822
|
|
|
break; |
|
1823
|
|
|
|
|
1824
|
|
|
case 'HTTP_CONTENT_TYPE': |
|
1825
|
|
|
// defined on IIS and has the same value as CONTENT_TYPE |
|
1826
|
|
|
break; |
|
1827
|
|
|
|
|
1828
|
|
|
case 'HTTP_CONTENT_MD5': // RFC 2616 14.15 |
|
1829
|
|
|
// TODO: maybe we can just pretend here? |
|
1830
|
|
|
$this->http_status("501 not implemented"); |
|
1831
|
|
|
echo "The service does not support content MD5 checksum verification"; |
|
1832
|
|
|
return; |
|
1833
|
|
|
|
|
1834
|
|
|
default: |
|
1835
|
|
|
// any other unknown Content-* headers |
|
1836
|
|
|
$this->http_status("501 not implemented"); |
|
1837
|
|
|
echo "The service does not support '$key'"; |
|
1838
|
|
|
return; |
|
1839
|
|
|
} |
|
1840
|
|
|
} |
|
1841
|
|
|
|
|
1842
|
|
|
$stat = $this->PUT($options); |
|
|
|
|
|
|
1843
|
|
|
|
|
1844
|
|
|
if ($stat === false) { |
|
1845
|
|
|
$stat = "403 Forbidden"; |
|
1846
|
|
|
} else if (is_resource($stat) && get_resource_type($stat) == "stream") { |
|
1847
|
|
|
$stream = $stat; |
|
1848
|
|
|
|
|
1849
|
|
|
$stat = $options["new"] ? "201 Created" : "204 No Content"; |
|
1850
|
|
|
|
|
1851
|
|
|
if (!empty($options["ranges"])) { |
|
1852
|
|
|
// TODO multipart support is missing (see also above) |
|
1853
|
|
|
if (0 == fseek($stream, $options['ranges'][0]["start"], SEEK_SET)) { |
|
1854
|
|
|
$length = $options['ranges'][0]["end"] - $options['ranges'][0]["start"]+1; |
|
1855
|
|
|
|
|
1856
|
|
|
while (!feof($options['stream'])) { |
|
1857
|
|
|
if ($length <= 0) { |
|
1858
|
|
|
break; |
|
1859
|
|
|
} |
|
1860
|
|
|
|
|
1861
|
|
|
if ($length <= 8192) { |
|
1862
|
|
|
$data = fread($options['stream'], $length); |
|
1863
|
|
|
} else { |
|
1864
|
|
|
$data = fread($options['stream'], 8192); |
|
1865
|
|
|
} |
|
1866
|
|
|
|
|
1867
|
|
|
if ($data === false) { |
|
1868
|
|
|
$stat = "400 Bad request"; |
|
1869
|
|
|
} elseif (strlen($data)) { |
|
1870
|
|
|
if (false === fwrite($stream, $data)) { |
|
1871
|
|
|
$stat = "403 Forbidden"; |
|
1872
|
|
|
break; |
|
1873
|
|
|
} |
|
1874
|
|
|
$length -= strlen($data); |
|
1875
|
|
|
} |
|
1876
|
|
|
} |
|
1877
|
|
|
} else { |
|
1878
|
|
|
$stat = "403 Forbidden"; |
|
1879
|
|
|
} |
|
1880
|
|
|
} else { |
|
1881
|
|
|
while (!feof($options["stream"])) { |
|
1882
|
|
|
if (false === fwrite($stream, fread($options["stream"], 8192))) { |
|
1883
|
|
|
$stat = "403 Forbidden"; |
|
1884
|
|
|
break; |
|
1885
|
|
|
} |
|
1886
|
|
|
} |
|
1887
|
|
|
} |
|
1888
|
|
|
|
|
1889
|
|
|
fclose($stream); |
|
1890
|
|
|
} |
|
1891
|
|
|
|
|
1892
|
|
|
$this->http_status($stat); |
|
1893
|
|
|
} else { |
|
1894
|
|
|
$this->http_status("423 Locked"); |
|
1895
|
|
|
} |
|
1896
|
|
|
} |
|
1897
|
|
|
|
|
1898
|
|
|
// }}} |
|
1899
|
|
|
|
|
1900
|
|
|
|
|
1901
|
|
|
// {{{ http_DELETE() |
|
1902
|
|
|
|
|
1903
|
|
|
/** |
|
1904
|
|
|
* DELETE method handler |
|
1905
|
|
|
* |
|
1906
|
|
|
* @param void |
|
1907
|
|
|
* @return void |
|
1908
|
|
|
*/ |
|
1909
|
|
|
function http_DELETE() |
|
1910
|
|
|
{ |
|
1911
|
|
|
// check RFC 2518 Section 9.2, last paragraph |
|
1912
|
|
|
if (isset($this->_SERVER["HTTP_DEPTH"])) { |
|
1913
|
|
|
if ($this->_SERVER["HTTP_DEPTH"] != "infinity") { |
|
1914
|
|
|
if (stripos($_SERVER['HTTP_USER_AGENT'],'webdrive') !== false) |
|
1915
|
|
|
{ |
|
1916
|
|
|
// pretend we didnt see it, as webdrive does not handle the depth parameter correctly while deleting collections |
|
1917
|
|
|
} |
|
1918
|
|
|
else |
|
1919
|
|
|
{ |
|
1920
|
|
|
$this->http_status("400 Bad Request"); |
|
1921
|
|
|
return; |
|
1922
|
|
|
} |
|
1923
|
|
|
} |
|
1924
|
|
|
} |
|
1925
|
|
|
|
|
1926
|
|
|
// check lock status |
|
1927
|
|
|
if ($this->_check_lock_status($this->path)) { |
|
1928
|
|
|
// ok, proceed |
|
1929
|
|
|
$options = Array(); |
|
1930
|
|
|
$options["path"] = $this->path; |
|
1931
|
|
|
|
|
1932
|
|
|
$stat = $this->DELETE($options); |
|
|
|
|
|
|
1933
|
|
|
|
|
1934
|
|
|
$this->http_status($stat); |
|
1935
|
|
|
} else { |
|
1936
|
|
|
// sorry, its locked |
|
1937
|
|
|
$this->http_status("423 Locked"); |
|
1938
|
|
|
} |
|
1939
|
|
|
} |
|
1940
|
|
|
|
|
1941
|
|
|
// }}} |
|
1942
|
|
|
|
|
1943
|
|
|
// {{{ http_COPY() |
|
1944
|
|
|
|
|
1945
|
|
|
/** |
|
1946
|
|
|
* COPY method handler |
|
1947
|
|
|
* |
|
1948
|
|
|
* @param void |
|
1949
|
|
|
* @return void |
|
1950
|
|
|
*/ |
|
1951
|
|
|
function http_COPY() |
|
1952
|
|
|
{ |
|
1953
|
|
|
// no need to check source lock status here |
|
1954
|
|
|
// destination lock status is always checked by the helper method |
|
1955
|
|
|
$this->_copymove("copy"); |
|
1956
|
|
|
} |
|
1957
|
|
|
|
|
1958
|
|
|
// }}} |
|
1959
|
|
|
|
|
1960
|
|
|
// {{{ http_MOVE() |
|
1961
|
|
|
|
|
1962
|
|
|
/** |
|
1963
|
|
|
* MOVE method handler |
|
1964
|
|
|
* |
|
1965
|
|
|
* @param void |
|
1966
|
|
|
* @return void |
|
1967
|
|
|
*/ |
|
1968
|
|
|
function http_MOVE() |
|
1969
|
|
|
{ |
|
1970
|
|
|
if ($this->_check_lock_status($this->path)) { |
|
1971
|
|
|
// destination lock status is always checked by the helper method |
|
1972
|
|
|
$this->_copymove("move"); |
|
1973
|
|
|
} else { |
|
1974
|
|
|
$this->http_status("423 Locked"); |
|
1975
|
|
|
} |
|
1976
|
|
|
} |
|
1977
|
|
|
|
|
1978
|
|
|
// }}} |
|
1979
|
|
|
|
|
1980
|
|
|
|
|
1981
|
|
|
// {{{ http_LOCK() |
|
1982
|
|
|
|
|
1983
|
|
|
/** |
|
1984
|
|
|
* LOCK method handler |
|
1985
|
|
|
* |
|
1986
|
|
|
* @param void |
|
1987
|
|
|
* @return void |
|
1988
|
|
|
*/ |
|
1989
|
|
|
function http_LOCK() |
|
1990
|
|
|
{ |
|
1991
|
|
|
$options = Array(); |
|
1992
|
|
|
$options["path"] = $this->path; |
|
1993
|
|
|
|
|
1994
|
|
|
if (isset($this->_SERVER['HTTP_DEPTH'])) { |
|
1995
|
|
|
$options["depth"] = $this->_SERVER["HTTP_DEPTH"]; |
|
1996
|
|
|
} else { |
|
1997
|
|
|
$options["depth"] = "infinity"; |
|
1998
|
|
|
} |
|
1999
|
|
|
|
|
2000
|
|
|
if (isset($this->_SERVER["HTTP_TIMEOUT"])) { |
|
2001
|
|
|
$options["timeout"] = explode(",", $this->_SERVER["HTTP_TIMEOUT"]); |
|
2002
|
|
|
} |
|
2003
|
|
|
|
|
2004
|
|
|
if (empty($this->_SERVER['CONTENT_LENGTH']) && !empty($this->_SERVER['HTTP_IF'])) { |
|
2005
|
|
|
// check if locking is possible |
|
2006
|
|
|
if (!$this->_check_lock_status($this->path)) { |
|
2007
|
|
|
$this->http_status("423 Locked"); |
|
2008
|
|
|
return; |
|
2009
|
|
|
} |
|
2010
|
|
|
|
|
2011
|
|
|
// refresh lock |
|
2012
|
|
|
$options["locktoken"] = substr($this->_SERVER['HTTP_IF'], 2, -2); |
|
2013
|
|
|
$options["update"] = $options["locktoken"]; |
|
2014
|
|
|
|
|
2015
|
|
|
// setting defaults for required fields, LOCK() SHOULD overwrite these |
|
2016
|
|
|
$options['owner'] = "unknown"; |
|
2017
|
|
|
$options['scope'] = "exclusive"; |
|
2018
|
|
|
$options['type'] = "write"; |
|
2019
|
|
|
|
|
2020
|
|
|
|
|
2021
|
|
|
$stat = $this->LOCK($options); |
|
|
|
|
|
|
2022
|
|
|
} else { |
|
2023
|
|
|
// extract lock request information from request XML payload |
|
2024
|
|
|
$lockinfo = new _parse_lockinfo("php://input"); |
|
2025
|
|
|
if (!$lockinfo->success) { |
|
2026
|
|
|
$this->http_status("400 bad request"); |
|
2027
|
|
|
} |
|
2028
|
|
|
|
|
2029
|
|
|
// check if locking is possible |
|
2030
|
|
|
if (!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) { |
|
2031
|
|
|
$this->http_status("423 Locked"); |
|
2032
|
|
|
return; |
|
2033
|
|
|
} |
|
2034
|
|
|
|
|
2035
|
|
|
// new lock |
|
2036
|
|
|
$options["scope"] = $lockinfo->lockscope; |
|
2037
|
|
|
$options["type"] = $lockinfo->locktype; |
|
2038
|
|
|
// Todo: lockinfo::owner still contains D:href opening and closing tags, maybe they should be removed here with strip_tags |
|
2039
|
|
|
$options["owner"] = $lockinfo->owner; |
|
2040
|
|
|
$options["locktoken"] = $this->_new_locktoken(); |
|
2041
|
|
|
|
|
2042
|
|
|
$stat = $this->LOCK($options); |
|
2043
|
|
|
} |
|
2044
|
|
|
|
|
2045
|
|
|
if (is_bool($stat)) { |
|
2046
|
|
|
$http_stat = $stat ? "200 OK" : "423 Locked"; |
|
2047
|
|
|
} else { |
|
2048
|
|
|
$http_stat = (string)$stat; |
|
2049
|
|
|
} |
|
2050
|
|
|
$this->http_status($http_stat); |
|
2051
|
|
|
|
|
2052
|
|
|
if ($http_stat{0} == 2) { // 2xx states are ok |
|
2053
|
|
|
if ($options["timeout"]) { |
|
2054
|
|
|
// if multiple timeout values were given we take the first only |
|
2055
|
|
|
if (is_array($options["timeout"])) { |
|
2056
|
|
|
reset($options["timeout"]); |
|
2057
|
|
|
$options["timeout"] = current($options["timeout"]); |
|
2058
|
|
|
} |
|
2059
|
|
|
// if the timeout is numeric only we need to reformat it |
|
2060
|
|
|
if (is_numeric($options["timeout"])) { |
|
2061
|
|
|
// more than a million is considered an absolute timestamp |
|
2062
|
|
|
// less is more likely a relative value |
|
2063
|
|
|
if ($options["timeout"]>1000000) { |
|
2064
|
|
|
$timeout = "Second-".($options['timeout']-time()); |
|
2065
|
|
|
} else { |
|
2066
|
|
|
$timeout = "Second-$options[timeout]"; |
|
2067
|
|
|
} |
|
2068
|
|
|
} else { |
|
2069
|
|
|
// non-numeric values are passed on verbatim, |
|
2070
|
|
|
// no error checking is performed here in this case |
|
2071
|
|
|
// TODO: send "Infinite" on invalid timeout strings? |
|
2072
|
|
|
$timeout = $options["timeout"]; |
|
2073
|
|
|
} |
|
2074
|
|
|
} else { |
|
2075
|
|
|
$timeout = "Infinite"; |
|
2076
|
|
|
} |
|
2077
|
|
|
|
|
2078
|
|
|
header('Content-Type: text/xml; charset="utf-8"'); |
|
2079
|
|
|
header("Lock-Token: <$options[locktoken]>"); |
|
2080
|
|
|
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; |
|
2081
|
|
|
echo "<D:prop xmlns:D=\"DAV:\">\n"; |
|
2082
|
|
|
echo ' <'.($this->crrnd?'':'D:')."lockdiscovery>\n"; |
|
2083
|
|
|
echo ' <'.($this->crrnd?'':'D:')."activelock>\n"; |
|
2084
|
|
|
echo ' <'.($this->crrnd?'':'D:')."lockscope><D:$options[scope]/></".($this->crrnd?'':'D:')."lockscope>\n"; |
|
2085
|
|
|
echo ' <'.($this->crrnd?'':'D:')."locktype><D:$options[type]/></".($this->crrnd?'':'D:')."locktype>\n"; |
|
2086
|
|
|
echo ' <'.($this->crrnd?'':'D:')."depth>$options[depth]</".($this->crrnd?'':'D:')."depth>\n"; |
|
2087
|
|
|
echo ' <'.($this->crrnd?'':'D:')."owner>$options[owner]</".($this->crrnd?'':'D:')."owner>\n"; |
|
2088
|
|
|
echo ' <'.($this->crrnd?'':'D:')."timeout>$timeout</".($this->crrnd?'':'D:')."timeout>\n"; |
|
2089
|
|
|
echo ' <'.($this->crrnd?'':'D:')."locktoken><D:href>$options[locktoken]</D:href></".($this->crrnd?'':'D:')."locktoken>\n"; |
|
2090
|
|
|
echo ' </'.($this->crrnd?'':'D:')."activelock>\n"; |
|
2091
|
|
|
echo ' </'.($this->crrnd?'':'D:')."lockdiscovery>\n"; |
|
2092
|
|
|
echo '</'.($this->crrnd?'':'D:')."prop>\n\n"; |
|
2093
|
|
|
} |
|
2094
|
|
|
} |
|
2095
|
|
|
|
|
2096
|
|
|
|
|
2097
|
|
|
// }}} |
|
2098
|
|
|
|
|
2099
|
|
|
// {{{ http_UNLOCK() |
|
2100
|
|
|
|
|
2101
|
|
|
/** |
|
2102
|
|
|
* UNLOCK method handler |
|
2103
|
|
|
* |
|
2104
|
|
|
* @param void |
|
2105
|
|
|
* @return void |
|
2106
|
|
|
*/ |
|
2107
|
|
|
function http_UNLOCK() |
|
2108
|
|
|
{ |
|
2109
|
|
|
$options = Array(); |
|
2110
|
|
|
$options["path"] = $this->path; |
|
2111
|
|
|
|
|
2112
|
|
|
if (isset($this->_SERVER['HTTP_DEPTH'])) { |
|
2113
|
|
|
$options["depth"] = $this->_SERVER["HTTP_DEPTH"]; |
|
2114
|
|
|
} else { |
|
2115
|
|
|
$options["depth"] = "infinity"; |
|
2116
|
|
|
} |
|
2117
|
|
|
|
|
2118
|
|
|
// strip surrounding <> |
|
2119
|
|
|
$options["token"] = substr(trim($this->_SERVER["HTTP_LOCK_TOKEN"]), 1, -1); |
|
2120
|
|
|
|
|
2121
|
|
|
// call user method |
|
2122
|
|
|
$stat = $this->UNLOCK($options); |
|
|
|
|
|
|
2123
|
|
|
|
|
2124
|
|
|
$this->http_status($stat); |
|
2125
|
|
|
} |
|
2126
|
|
|
|
|
2127
|
|
|
// }}} |
|
2128
|
|
|
|
|
2129
|
|
|
// {{{ http_ACL() |
|
2130
|
|
|
|
|
2131
|
|
|
/** |
|
2132
|
|
|
* ACL method handler |
|
2133
|
|
|
* |
|
2134
|
|
|
* @param void |
|
2135
|
|
|
* @return void |
|
2136
|
|
|
*/ |
|
2137
|
|
|
function http_ACL() |
|
2138
|
|
|
{ |
|
2139
|
|
|
$options = Array(); |
|
2140
|
|
|
$options['path'] = $this->path; |
|
2141
|
|
|
$options['errors'] = array(); |
|
2142
|
|
|
|
|
2143
|
|
|
if (isset($this->_SERVER['HTTP_DEPTH'])) { |
|
2144
|
|
|
$options['depth'] = $this->_SERVER['HTTP_DEPTH']; |
|
2145
|
|
|
} else { |
|
2146
|
|
|
$options['depth'] = 'infinity'; |
|
2147
|
|
|
} |
|
2148
|
|
|
|
|
2149
|
|
|
// call user method |
|
2150
|
|
|
$status = $this->ACL($options); |
|
|
|
|
|
|
2151
|
|
|
|
|
2152
|
|
|
// now we generate the reply header ... |
|
2153
|
|
|
$this->http_status($status); |
|
2154
|
|
|
$content = ''; |
|
2155
|
|
|
|
|
2156
|
|
|
if (is_array($options['errors']) && count($options['errors'])) { |
|
2157
|
|
|
header('Content-Type: text/xml; charset="utf-8"'); |
|
2158
|
|
|
// ... and payload |
|
2159
|
|
|
$content .= "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; |
|
2160
|
|
|
$content .= "<D:error xmlns:D=\"DAV:\"> \n"; |
|
2161
|
|
|
foreach ($options['errors'] as $violation) { |
|
2162
|
|
|
$content .= '<'.($this->crrnd?'':'D:')."$violation/>\n"; |
|
2163
|
|
|
} |
|
2164
|
|
|
$content .= '</'.($this->crrnd?'':'D:')."error>\n"; |
|
2165
|
|
|
} |
|
2166
|
|
|
if (!self::use_compression()) header("Content-Length: ".self::bytes($content)); |
|
2167
|
|
|
if ($content) echo $options['content']; |
|
2168
|
|
|
} |
|
2169
|
|
|
|
|
2170
|
|
|
// }}} |
|
2171
|
|
|
|
|
2172
|
|
|
// }}} |
|
2173
|
|
|
|
|
2174
|
|
|
// {{{ _copymove() |
|
2175
|
|
|
|
|
2176
|
|
|
function _copymove($what) |
|
2177
|
|
|
{ |
|
2178
|
|
|
$options = Array(); |
|
2179
|
|
|
$options["path"] = $this->path; |
|
2180
|
|
|
|
|
2181
|
|
|
if (isset($this->_SERVER["HTTP_DEPTH"])) { |
|
2182
|
|
|
$options["depth"] = $this->_SERVER["HTTP_DEPTH"]; |
|
2183
|
|
|
} else { |
|
2184
|
|
|
$options["depth"] = "infinity"; |
|
2185
|
|
|
} |
|
2186
|
|
|
|
|
2187
|
|
|
$http_header_host = preg_replace("/:80$/", "", $this->_SERVER["HTTP_HOST"]); |
|
2188
|
|
|
|
|
2189
|
|
|
$url = parse_url($this->_SERVER["HTTP_DESTINATION"]); |
|
2190
|
|
|
// Vfs stores %, # and ? urlencoded, we do the encoding here on a central place |
|
2191
|
|
|
$path = strtr(self::_urldecode($url["path"]), array( |
|
2192
|
|
|
'%' => '%25', |
|
2193
|
|
|
'#' => '%23', |
|
2194
|
|
|
'?' => '%3F', |
|
2195
|
|
|
)); |
|
2196
|
|
|
//error_log(__METHOD__."(".array2string($what).") parse_url(HTTP_DESTINATION=".array2string($this->_SERVER["HTTP_DESTINATION"]).")=".array2string($url)." --> ".array2string($path)); |
|
2197
|
|
|
|
|
2198
|
|
|
if (isset($url["host"])) { |
|
2199
|
|
|
// TODO check url scheme, too |
|
2200
|
|
|
$http_host = $url["host"]; |
|
2201
|
|
|
if (isset($url["port"]) && $url["port"] != 80) |
|
2202
|
|
|
$http_host.= ":".$url["port"]; |
|
2203
|
|
|
} else { |
|
2204
|
|
|
// only path given, set host to self |
|
2205
|
|
|
$http_host = $http_header_host; |
|
2206
|
|
|
} |
|
2207
|
|
|
|
|
2208
|
|
|
if ($http_host == $http_header_host && |
|
2209
|
|
|
!strncmp($this->_SERVER["SCRIPT_NAME"], $path, |
|
2210
|
|
|
strlen($this->_SERVER["SCRIPT_NAME"]))) { |
|
2211
|
|
|
$options["dest"] = substr($path, strlen($this->_SERVER["SCRIPT_NAME"])); |
|
2212
|
|
|
if (!$this->_check_lock_status($options["dest"])) { |
|
2213
|
|
|
$this->http_status("423 Locked"); |
|
2214
|
|
|
return; |
|
2215
|
|
|
} |
|
2216
|
|
|
|
|
2217
|
|
|
} else { |
|
2218
|
|
|
$options["dest_url"] = $this->_SERVER["HTTP_DESTINATION"]; |
|
2219
|
|
|
} |
|
2220
|
|
|
|
|
2221
|
|
|
// see RFC 2518 Sections 9.6, 8.8.4 and 8.9.3 |
|
2222
|
|
|
if (isset($this->_SERVER["HTTP_OVERWRITE"])) { |
|
2223
|
|
|
$options["overwrite"] = $this->_SERVER["HTTP_OVERWRITE"] == "T"; |
|
2224
|
|
|
} else { |
|
2225
|
|
|
$options["overwrite"] = true; |
|
2226
|
|
|
} |
|
2227
|
|
|
|
|
2228
|
|
|
$stat = $this->$what($options); |
|
2229
|
|
|
$this->http_status($stat); |
|
2230
|
|
|
} |
|
2231
|
|
|
|
|
2232
|
|
|
// }}} |
|
2233
|
|
|
|
|
2234
|
|
|
// {{{ _allow() |
|
2235
|
|
|
|
|
2236
|
|
|
/** |
|
2237
|
|
|
* check for implemented HTTP methods |
|
2238
|
|
|
* |
|
2239
|
|
|
* @param void |
|
2240
|
|
|
* @return array something |
|
2241
|
|
|
*/ |
|
2242
|
|
|
function _allow() |
|
2243
|
|
|
{ |
|
2244
|
|
|
// OPTIONS is always there |
|
2245
|
|
|
$allow = array("OPTIONS" =>"OPTIONS"); |
|
2246
|
|
|
|
|
2247
|
|
|
// all other METHODS need both a http_method() wrapper |
|
2248
|
|
|
// and a method() implementation |
|
2249
|
|
|
// the base class supplies wrappers only |
|
2250
|
|
|
foreach (get_class_methods($this) as $method) { |
|
2251
|
|
|
if (!strncmp("http_", $method, 5)) { |
|
2252
|
|
|
$method = strtoupper(substr($method, 5)); |
|
2253
|
|
|
if (method_exists($this, $method)) { |
|
2254
|
|
|
$allow[$method] = $method; |
|
2255
|
|
|
} |
|
2256
|
|
|
} |
|
2257
|
|
|
} |
|
2258
|
|
|
|
|
2259
|
|
|
// we can emulate a missing HEAD implemetation using GET |
|
2260
|
|
|
if (isset($allow["GET"])) |
|
2261
|
|
|
$allow["HEAD"] = "HEAD"; |
|
2262
|
|
|
|
|
2263
|
|
|
// no LOCK without checklok() |
|
2264
|
|
|
if (!method_exists($this, "checklock")) { |
|
2265
|
|
|
unset($allow["LOCK"]); |
|
2266
|
|
|
unset($allow["UNLOCK"]); |
|
2267
|
|
|
} |
|
2268
|
|
|
|
|
2269
|
|
|
return $allow; |
|
2270
|
|
|
} |
|
2271
|
|
|
|
|
2272
|
|
|
// }}} |
|
2273
|
|
|
|
|
2274
|
|
|
/** |
|
2275
|
|
|
* helper for property element creation |
|
2276
|
|
|
* |
|
2277
|
|
|
* @param string XML namespace (optional) |
|
2278
|
|
|
* @param string property name |
|
|
|
|
|
|
2279
|
|
|
* @param string property value |
|
2280
|
|
|
* @praram boolen property raw-flag |
|
2281
|
|
|
* @return array property array |
|
2282
|
|
|
*/ |
|
2283
|
|
|
public static function mkprop() |
|
2284
|
|
|
{ |
|
2285
|
|
|
$args = func_get_args(); |
|
2286
|
|
|
switch (count($args)) { |
|
2287
|
|
|
case 4: |
|
2288
|
|
|
return array('ns' => $args[0], |
|
2289
|
|
|
'name' => $args[1], |
|
2290
|
|
|
'val' => $args[2], |
|
2291
|
|
|
'raw' => true); |
|
2292
|
|
|
case 3: |
|
2293
|
|
|
return array('ns' => $args[0], |
|
2294
|
|
|
'name' => $args[1], |
|
2295
|
|
|
'val' => $args[2]); |
|
2296
|
|
|
default: |
|
2297
|
|
|
return array('ns' => 'DAV:', |
|
2298
|
|
|
'name' => $args[0], |
|
2299
|
|
|
'val' => $args[1]); |
|
2300
|
|
|
} |
|
2301
|
|
|
} |
|
2302
|
|
|
|
|
2303
|
|
|
// {{{ _check_auth |
|
2304
|
|
|
|
|
2305
|
|
|
/** |
|
2306
|
|
|
* check authentication if check is implemented |
|
2307
|
|
|
* |
|
2308
|
|
|
* @param void |
|
2309
|
|
|
* @return bool true if authentication succeded or not necessary |
|
2310
|
|
|
*/ |
|
2311
|
|
|
function _check_auth() |
|
2312
|
|
|
{ |
|
2313
|
|
|
if (method_exists($this, "checkAuth")) { |
|
2314
|
|
|
// PEAR style method name |
|
2315
|
|
|
return $this->checkAuth(@$this->_SERVER["AUTH_TYPE"], |
|
2316
|
|
|
@$this->_SERVER["PHP_AUTH_USER"], |
|
2317
|
|
|
@$this->_SERVER["PHP_AUTH_PW"]); |
|
2318
|
|
|
} else if (method_exists($this, "check_auth")) { |
|
2319
|
|
|
// old (pre 1.0) method name |
|
2320
|
|
|
return $this->check_auth(@$this->_SERVER["AUTH_TYPE"], |
|
2321
|
|
|
@$this->_SERVER["PHP_AUTH_USER"], |
|
2322
|
|
|
@$this->_SERVER["PHP_AUTH_PW"]); |
|
2323
|
|
|
} else { |
|
2324
|
|
|
// no method found -> no authentication required |
|
2325
|
|
|
return true; |
|
2326
|
|
|
} |
|
2327
|
|
|
} |
|
2328
|
|
|
|
|
2329
|
|
|
// }}} |
|
2330
|
|
|
|
|
2331
|
|
|
// {{{ UUID stuff |
|
2332
|
|
|
|
|
2333
|
|
|
/** |
|
2334
|
|
|
* generate Unique Universal IDentifier for lock token |
|
2335
|
|
|
* |
|
2336
|
|
|
* @param void |
|
2337
|
|
|
* @return string a new UUID |
|
2338
|
|
|
*/ |
|
2339
|
|
|
public static function _new_uuid() |
|
2340
|
|
|
{ |
|
2341
|
|
|
// use uuid extension from PECL if available |
|
2342
|
|
|
if (function_exists("uuid_create")) { |
|
2343
|
|
|
return uuid_create(); |
|
2344
|
|
|
} |
|
2345
|
|
|
|
|
2346
|
|
|
// fallback |
|
2347
|
|
|
$uuid = md5(microtime().getmypid()); // this should be random enough for now |
|
2348
|
|
|
|
|
2349
|
|
|
// set variant and version fields for 'true' random uuid |
|
2350
|
|
|
$uuid{12} = "4"; |
|
2351
|
|
|
$n = 8 + (ord($uuid{16}) & 3); |
|
2352
|
|
|
$hex = "0123456789abcdef"; |
|
2353
|
|
|
$uuid{16} = $hex{$n}; |
|
2354
|
|
|
|
|
2355
|
|
|
// return formated uuid |
|
2356
|
|
|
return substr($uuid, 0, 8)."-" |
|
2357
|
|
|
. substr($uuid, 8, 4)."-" |
|
2358
|
|
|
. substr($uuid, 12, 4)."-" |
|
2359
|
|
|
. substr($uuid, 16, 4)."-" |
|
2360
|
|
|
. substr($uuid, 20); |
|
2361
|
|
|
} |
|
2362
|
|
|
|
|
2363
|
|
|
/** |
|
2364
|
|
|
* create a new opaque lock token as defined in RFC2518 |
|
2365
|
|
|
* |
|
2366
|
|
|
* @param void |
|
2367
|
|
|
* @return string new RFC2518 opaque lock token |
|
2368
|
|
|
*/ |
|
2369
|
|
|
public static function _new_locktoken() |
|
2370
|
|
|
{ |
|
2371
|
|
|
return "opaquelocktoken:".self::_new_uuid(); |
|
2372
|
|
|
} |
|
2373
|
|
|
|
|
2374
|
|
|
// }}} |
|
2375
|
|
|
|
|
2376
|
|
|
// {{{ WebDAV If: header parsing |
|
2377
|
|
|
|
|
2378
|
|
|
/** |
|
2379
|
|
|
* |
|
2380
|
|
|
* |
|
2381
|
|
|
* @param string header string to parse |
|
|
|
|
|
|
2382
|
|
|
* @param int current parsing position |
|
|
|
|
|
|
2383
|
|
|
* @return array next token (type and value) |
|
2384
|
|
|
*/ |
|
2385
|
|
|
function _if_header_lexer($string, &$pos) |
|
2386
|
|
|
{ |
|
2387
|
|
|
// skip whitespace |
|
2388
|
|
|
while (ctype_space($string{$pos})) { |
|
2389
|
|
|
++$pos; |
|
2390
|
|
|
} |
|
2391
|
|
|
|
|
2392
|
|
|
// already at end of string? |
|
2393
|
|
|
if (strlen($string) <= $pos) { |
|
2394
|
|
|
return false; |
|
2395
|
|
|
} |
|
2396
|
|
|
|
|
2397
|
|
|
// get next character |
|
2398
|
|
|
$c = $string{$pos++}; |
|
2399
|
|
|
|
|
2400
|
|
|
// now it depends on what we found |
|
2401
|
|
|
switch ($c) { |
|
2402
|
|
|
case "<": |
|
2403
|
|
|
// URIs are enclosed in <...> |
|
2404
|
|
|
$pos2 = strpos($string, ">", $pos); |
|
2405
|
|
|
$uri = substr($string, $pos, $pos2 - $pos); |
|
2406
|
|
|
$pos = $pos2 + 1; |
|
2407
|
|
|
return array("URI", $uri); |
|
2408
|
|
|
|
|
2409
|
|
|
case "[": |
|
2410
|
|
|
//Etags are enclosed in [...] |
|
2411
|
|
|
if ($string{$pos} == "W") { |
|
2412
|
|
|
$type = "ETAG_WEAK"; |
|
2413
|
|
|
$pos += 2; |
|
2414
|
|
|
} else { |
|
2415
|
|
|
$type = "ETAG_STRONG"; |
|
2416
|
|
|
} |
|
2417
|
|
|
$pos2 = strpos($string, "]", $pos); |
|
2418
|
|
|
$etag = substr($string, $pos + 1, $pos2 - $pos - 2); |
|
2419
|
|
|
$pos = $pos2 + 1; |
|
2420
|
|
|
return array($type, $etag); |
|
2421
|
|
|
|
|
2422
|
|
|
case "N": |
|
2423
|
|
|
// "N" indicates negation |
|
2424
|
|
|
$pos += 2; |
|
2425
|
|
|
return array("NOT", "Not"); |
|
2426
|
|
|
|
|
2427
|
|
|
default: |
|
2428
|
|
|
// anything else is passed verbatim char by char |
|
2429
|
|
|
return array("CHAR", $c); |
|
2430
|
|
|
} |
|
2431
|
|
|
} |
|
2432
|
|
|
|
|
2433
|
|
|
/** |
|
2434
|
|
|
* parse If: header |
|
2435
|
|
|
* |
|
2436
|
|
|
* @param string header string |
|
2437
|
|
|
* @return array URIs and their conditions |
|
2438
|
|
|
*/ |
|
2439
|
|
|
function _if_header_parser($str) |
|
2440
|
|
|
{ |
|
2441
|
|
|
$pos = 0; |
|
2442
|
|
|
$len = strlen($str); |
|
2443
|
|
|
$uris = array(); |
|
2444
|
|
|
|
|
2445
|
|
|
// parser loop |
|
2446
|
|
|
while ($pos < $len) { |
|
2447
|
|
|
// get next token |
|
2448
|
|
|
$token = $this->_if_header_lexer($str, $pos); |
|
2449
|
|
|
|
|
2450
|
|
|
// check for URI |
|
2451
|
|
|
if ($token[0] == "URI") { |
|
2452
|
|
|
$uri = $token[1]; // remember URI |
|
2453
|
|
|
$token = $this->_if_header_lexer($str, $pos); // get next token |
|
2454
|
|
|
} else { |
|
2455
|
|
|
$uri = ""; |
|
2456
|
|
|
} |
|
2457
|
|
|
|
|
2458
|
|
|
// sanity check |
|
2459
|
|
|
if ($token[0] != "CHAR" || $token[1] != "(") { |
|
2460
|
|
|
return false; |
|
2461
|
|
|
} |
|
2462
|
|
|
|
|
2463
|
|
|
$list = array(); |
|
2464
|
|
|
$level = 1; |
|
2465
|
|
|
$not = ""; |
|
2466
|
|
|
while ($level) { |
|
2467
|
|
|
$token = $this->_if_header_lexer($str, $pos); |
|
2468
|
|
|
if ($token[0] == "NOT") { |
|
2469
|
|
|
$not = "!"; |
|
2470
|
|
|
continue; |
|
2471
|
|
|
} |
|
2472
|
|
|
switch ($token[0]) { |
|
2473
|
|
|
case "CHAR": |
|
2474
|
|
|
switch ($token[1]) { |
|
2475
|
|
|
case "(": |
|
2476
|
|
|
$level++; |
|
2477
|
|
|
break; |
|
2478
|
|
|
case ")": |
|
2479
|
|
|
$level--; |
|
2480
|
|
|
break; |
|
2481
|
|
|
default: |
|
2482
|
|
|
return false; |
|
2483
|
|
|
} |
|
2484
|
|
|
break; |
|
2485
|
|
|
|
|
2486
|
|
|
case "URI": |
|
2487
|
|
|
$list[] = $not."<$token[1]>"; |
|
2488
|
|
|
break; |
|
2489
|
|
|
|
|
2490
|
|
|
case "ETAG_WEAK": |
|
2491
|
|
|
$list[] = $not."[W/'$token[1]']>"; |
|
2492
|
|
|
break; |
|
2493
|
|
|
|
|
2494
|
|
|
case "ETAG_STRONG": |
|
2495
|
|
|
$list[] = $not."['$token[1]']>"; |
|
2496
|
|
|
break; |
|
2497
|
|
|
|
|
2498
|
|
|
default: |
|
2499
|
|
|
return false; |
|
2500
|
|
|
} |
|
2501
|
|
|
$not = ""; |
|
2502
|
|
|
} |
|
2503
|
|
|
|
|
2504
|
|
|
if (@is_array($uris[$uri])) { |
|
2505
|
|
|
$uris[$uri] = array_merge($uris[$uri], $list); |
|
2506
|
|
|
} else { |
|
2507
|
|
|
$uris[$uri] = $list; |
|
2508
|
|
|
} |
|
2509
|
|
|
} |
|
2510
|
|
|
|
|
2511
|
|
|
return $uris; |
|
2512
|
|
|
} |
|
2513
|
|
|
|
|
2514
|
|
|
/** |
|
2515
|
|
|
* check if conditions from "If:" headers are meat |
|
2516
|
|
|
* |
|
2517
|
|
|
* the "If:" header is an extension to HTTP/1.1 |
|
2518
|
|
|
* defined in RFC 2518 section 9.4 |
|
2519
|
|
|
* |
|
2520
|
|
|
* @param void |
|
2521
|
|
|
* @return void |
|
2522
|
|
|
*/ |
|
2523
|
|
|
function _check_if_header_conditions() |
|
2524
|
|
|
{ |
|
2525
|
|
|
if (isset($this->_SERVER["HTTP_IF"])) { |
|
2526
|
|
|
$this->_if_header_uris = |
|
2527
|
|
|
$this->_if_header_parser($this->_SERVER["HTTP_IF"]); |
|
2528
|
|
|
|
|
2529
|
|
|
foreach ($this->_if_header_uris as $uri => $conditions) { |
|
2530
|
|
|
if ($uri == "") { |
|
2531
|
|
|
$uri = $this->uri; |
|
2532
|
|
|
} |
|
2533
|
|
|
// all must match |
|
2534
|
|
|
$state = true; |
|
2535
|
|
|
foreach ($conditions as $condition) { |
|
2536
|
|
|
// lock tokens may be free form (RFC2518 6.3) |
|
2537
|
|
|
// but if opaquelocktokens are used (RFC2518 6.4) |
|
2538
|
|
|
// we have to check the format (litmus tests this) |
|
2539
|
|
|
if (!strncmp($condition, "<opaquelocktoken:", strlen("<opaquelocktoken"))) { |
|
2540
|
|
|
if (!preg_match('/^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$/', $condition)) { |
|
2541
|
|
|
$this->http_status("423 Locked"); |
|
2542
|
|
|
return false; |
|
2543
|
|
|
} |
|
2544
|
|
|
} |
|
2545
|
|
|
if (!$this->_check_uri_condition($uri, $condition)) { |
|
2546
|
|
|
$this->http_status("412 Precondition failed"); |
|
2547
|
|
|
$state = false; |
|
2548
|
|
|
break; |
|
2549
|
|
|
} |
|
2550
|
|
|
} |
|
2551
|
|
|
|
|
2552
|
|
|
// any match is ok |
|
2553
|
|
|
if ($state == true) { |
|
|
|
|
|
|
2554
|
|
|
return true; |
|
2555
|
|
|
} |
|
2556
|
|
|
} |
|
2557
|
|
|
return false; |
|
2558
|
|
|
} |
|
2559
|
|
|
return true; |
|
2560
|
|
|
} |
|
2561
|
|
|
|
|
2562
|
|
|
/** |
|
2563
|
|
|
* Check a single URI condition parsed from an if-header |
|
2564
|
|
|
* |
|
2565
|
|
|
* Check a single URI condition parsed from an if-header |
|
2566
|
|
|
* |
|
2567
|
|
|
* @abstract |
|
2568
|
|
|
* @param string $uri URI to check |
|
2569
|
|
|
* @param string $condition Condition to check for this URI |
|
2570
|
|
|
* @returns bool Condition check result |
|
2571
|
|
|
*/ |
|
2572
|
|
|
function _check_uri_condition($uri, $condition) |
|
2573
|
|
|
{ |
|
2574
|
|
|
unset($uri); // not used, but required by function signature |
|
2575
|
|
|
// not really implemented here, |
|
2576
|
|
|
// implementations must override |
|
2577
|
|
|
|
|
2578
|
|
|
// a lock token can never be from the DAV: scheme |
|
2579
|
|
|
// litmus uses DAV:no-lock in some tests |
|
2580
|
|
|
if (!strncmp("<DAV:", $condition, 5)) { |
|
2581
|
|
|
return false; |
|
2582
|
|
|
} |
|
2583
|
|
|
|
|
2584
|
|
|
return true; |
|
2585
|
|
|
} |
|
2586
|
|
|
|
|
2587
|
|
|
|
|
2588
|
|
|
/** |
|
2589
|
|
|
* |
|
2590
|
|
|
* |
|
2591
|
|
|
* @param string path of resource to check |
|
|
|
|
|
|
2592
|
|
|
* @param bool exclusive lock? |
|
|
|
|
|
|
2593
|
|
|
*/ |
|
2594
|
|
|
function _check_lock_status($path, $exclusive_only = false) |
|
2595
|
|
|
{ |
|
2596
|
|
|
// FIXME depth -> ignored for now |
|
2597
|
|
|
if (method_exists($this, "checkLock")) { |
|
2598
|
|
|
// is locked? |
|
2599
|
|
|
$lock = $this->checkLock($path); |
|
2600
|
|
|
|
|
2601
|
|
|
// ... and lock is not owned? |
|
2602
|
|
|
if (is_array($lock) && count($lock)) { |
|
2603
|
|
|
// FIXME doesn't check uri restrictions yet |
|
2604
|
|
|
if (!isset($this->_SERVER["HTTP_IF"]) || !strstr($this->_SERVER["HTTP_IF"], $lock["token"])) { |
|
2605
|
|
|
if (!$exclusive_only || ($lock["scope"] !== "shared")) |
|
2606
|
|
|
return false; |
|
2607
|
|
|
} |
|
2608
|
|
|
} |
|
2609
|
|
|
} |
|
2610
|
|
|
return true; |
|
2611
|
|
|
} |
|
2612
|
|
|
|
|
2613
|
|
|
|
|
2614
|
|
|
// }}} |
|
2615
|
|
|
|
|
2616
|
|
|
|
|
2617
|
|
|
/** |
|
2618
|
|
|
* Generate lockdiscovery reply from checklock() result |
|
2619
|
|
|
* |
|
2620
|
|
|
* @param string resource path to check |
|
2621
|
|
|
* @return string lockdiscovery response |
|
2622
|
|
|
*/ |
|
2623
|
|
|
function lockdiscovery($path) |
|
2624
|
|
|
{ |
|
2625
|
|
|
// no lock support without checklock() method |
|
2626
|
|
|
if (!method_exists($this, "checklock")) { |
|
2627
|
|
|
return ""; |
|
2628
|
|
|
} |
|
2629
|
|
|
|
|
2630
|
|
|
// collect response here |
|
2631
|
|
|
$activelocks = ""; |
|
2632
|
|
|
|
|
2633
|
|
|
// get checklock() reply |
|
2634
|
|
|
$lock = $this->checklock($path); |
|
2635
|
|
|
|
|
2636
|
|
|
// generate <activelock> block for returned data |
|
2637
|
|
|
if (is_array($lock) && count($lock)) { |
|
2638
|
|
|
// check for 'timeout' or 'expires' |
|
2639
|
|
|
if (!empty($lock["expires"])) { |
|
2640
|
|
|
$timeout = "Second-".($lock["expires"] - time()); |
|
2641
|
|
|
} else if (!empty($lock["timeout"])) { |
|
2642
|
|
|
$timeout = "Second-$lock[timeout]"; |
|
2643
|
|
|
} else { |
|
2644
|
|
|
$timeout = "Infinite"; |
|
2645
|
|
|
} |
|
2646
|
|
|
|
|
2647
|
|
|
// genreate response block |
|
2648
|
|
|
if ($this->crrnd) |
|
2649
|
|
|
{ |
|
2650
|
|
|
$activelocks.= " |
|
2651
|
|
|
<activelock> |
|
2652
|
|
|
<lockscope><$lock[scope]/></lockscope> |
|
2653
|
|
|
<locktype><$lock[type]/></locktype> |
|
2654
|
|
|
<depth>$lock[depth]</depth> |
|
2655
|
|
|
<owner>$lock[owner]</owner> |
|
2656
|
|
|
<timeout>$timeout</timeout> |
|
2657
|
|
|
<locktoken><href>$lock[token]</href></locktoken> |
|
2658
|
|
|
</activelock> |
|
2659
|
|
|
"; |
|
2660
|
|
|
} |
|
2661
|
|
|
else |
|
2662
|
|
|
{ |
|
2663
|
|
|
$activelocks.= " |
|
2664
|
|
|
<D:activelock> |
|
2665
|
|
|
<D:lockscope><D:$lock[scope]/></D:lockscope> |
|
2666
|
|
|
<D:locktype><D:$lock[type]/></D:locktype> |
|
2667
|
|
|
<D:depth>$lock[depth]</D:depth> |
|
2668
|
|
|
<D:owner>$lock[owner]</D:owner> |
|
2669
|
|
|
<D:timeout>$timeout</D:timeout> |
|
2670
|
|
|
<D:locktoken><D:href>$lock[token]</D:href></D:locktoken> |
|
2671
|
|
|
</D:activelock> |
|
2672
|
|
|
"; |
|
2673
|
|
|
} |
|
2674
|
|
|
} |
|
2675
|
|
|
|
|
2676
|
|
|
// return generated response |
|
2677
|
|
|
//error_log(__METHOD__."\n".print_r($activelocks,true)); |
|
2678
|
|
|
return $activelocks; |
|
2679
|
|
|
} |
|
2680
|
|
|
|
|
2681
|
|
|
/** |
|
2682
|
|
|
* set HTTP return status and mirror it in a private header |
|
2683
|
|
|
* |
|
2684
|
|
|
* @param string status code and message |
|
2685
|
|
|
* @return void |
|
2686
|
|
|
*/ |
|
2687
|
|
|
function http_status($status) |
|
2688
|
|
|
{ |
|
2689
|
|
|
// simplified success case |
|
2690
|
|
|
if ($status === true) { |
|
2691
|
|
|
$status = "200 OK"; |
|
2692
|
|
|
} |
|
2693
|
|
|
|
|
2694
|
|
|
// remember status |
|
2695
|
|
|
$this->_http_status = $status; |
|
2696
|
|
|
|
|
2697
|
|
|
// generate HTTP status response |
|
2698
|
|
|
header("HTTP/1.1 $status"); |
|
2699
|
|
|
header("X-WebDAV-Status: $status", true); |
|
2700
|
|
|
} |
|
2701
|
|
|
|
|
2702
|
|
|
/** |
|
2703
|
|
|
* private URL encoding |
|
2704
|
|
|
* |
|
2705
|
|
|
* We use now full url-encoding as required by WebDAV RFC and many clients. |
|
2706
|
|
|
* Formerly HTTP_WebDAV_Server used to encode only: " %&<>+" |
|
2707
|
|
|
* |
|
2708
|
|
|
* @param string URL to encode |
|
|
|
|
|
|
2709
|
|
|
* @return string encoded URL |
|
2710
|
|
|
*/ |
|
2711
|
|
|
public static function _urlencode($url) |
|
2712
|
|
|
{ |
|
2713
|
|
|
return strtr(rawurlencode($url),array( |
|
2714
|
|
|
'%2F' => '/', |
|
2715
|
|
|
'%3A' => ':', |
|
2716
|
|
|
)); |
|
2717
|
|
|
} |
|
2718
|
|
|
|
|
2719
|
|
|
/** |
|
2720
|
|
|
* private version of PHP urldecode |
|
2721
|
|
|
* |
|
2722
|
|
|
* not really needed but added for completenes |
|
2723
|
|
|
* |
|
2724
|
|
|
* @param string URL to decode |
|
2725
|
|
|
* @return string decoded URL |
|
2726
|
|
|
*/ |
|
2727
|
|
|
public static function _urldecode($path) |
|
2728
|
|
|
{ |
|
2729
|
|
|
return rawurldecode($path); |
|
2730
|
|
|
} |
|
2731
|
|
|
|
|
2732
|
|
|
/** |
|
2733
|
|
|
* Encode a hierarchical properties like: |
|
2734
|
|
|
* |
|
2735
|
|
|
* <D:supported-report-set> |
|
2736
|
|
|
* <supported-report> |
|
2737
|
|
|
* <report> |
|
2738
|
|
|
* <addressbook-query xmlns='urn:ietf:params:xml:ns:carddav'/> |
|
2739
|
|
|
* </report> |
|
2740
|
|
|
* </supported-report> |
|
2741
|
|
|
* <supported-report> |
|
2742
|
|
|
* <report> |
|
2743
|
|
|
* <addressbook-multiget xmlns='urn:ietf:params:xml:ns:carddav'/> |
|
2744
|
|
|
* </report> |
|
2745
|
|
|
* </supported-report> |
|
2746
|
|
|
* </D:supported-report-set> |
|
2747
|
|
|
* |
|
2748
|
|
|
* @param array $props |
|
2749
|
|
|
* @param string $ns |
|
2750
|
|
|
* @param strin $ns_defs |
|
2751
|
|
|
* @param array $ns_hash |
|
2752
|
|
|
* @return string |
|
2753
|
|
|
*/ |
|
2754
|
|
|
function _hierarchical_prop_encode(array $props, $ns, &$ns_defs, array &$ns_hash) |
|
2755
|
|
|
{ |
|
2756
|
|
|
$ret = ''; |
|
2757
|
|
|
|
|
2758
|
|
|
//error_log(__METHOD__.'('.array2string($props).')'); |
|
2759
|
|
|
if (isset($props['name'])) $props = array($props); |
|
2760
|
|
|
|
|
2761
|
|
|
foreach($props as $prop) |
|
2762
|
|
|
{ |
|
2763
|
|
|
if (!isset($ns_hash[$prop['ns']])) // unknown namespace |
|
2764
|
|
|
{ |
|
2765
|
|
|
// register namespace |
|
2766
|
|
|
$ns_name = 'ns'.(count($ns_hash) + 1); |
|
2767
|
|
|
$ns_hash[$prop['ns']] = $ns_name; |
|
2768
|
|
|
$ns_defs .= ' xmlns:'.$ns_name.'="'.$prop['ns'].'"'; |
|
2769
|
|
|
} |
|
2770
|
|
|
if (is_array($prop['val'])) |
|
2771
|
|
|
{ |
|
2772
|
|
|
$subprop = $prop['val']; |
|
2773
|
|
|
if (isset($subprop['ns']) || isset($subprop[0]['ns'])) |
|
2774
|
|
|
{ |
|
2775
|
|
|
$ret .= '<'.($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : $ns_hash[$prop['ns']].':').$prop['name']. |
|
2776
|
|
|
(empty($prop['val']) ? '/>' : '>'.$this->_hierarchical_prop_encode($prop['val'], $prop['ns'], $ns_defs, $ns_hash). |
|
2777
|
|
|
'</'.($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : ($this->crrnd ? '' : $ns_hash[$prop['ns']].':')).$prop['name'].'>'); |
|
2778
|
|
|
} |
|
2779
|
|
|
else // val contains only attributes, no value |
|
2780
|
|
|
{ |
|
2781
|
|
|
$vals = ''; |
|
2782
|
|
|
|
|
2783
|
|
|
foreach($subprop as $attr => $val) |
|
2784
|
|
|
{ |
|
2785
|
|
|
$vals .= ' '.$attr.'="'.htmlspecialchars($val, ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8').'"'; |
|
2786
|
|
|
} |
|
2787
|
|
|
|
|
2788
|
|
|
$ret .= '<'.($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : $ns_hash[$prop['ns']].':').$prop['name']. |
|
2789
|
|
|
$vals .'/>'; |
|
2790
|
|
|
} |
|
2791
|
|
|
} |
|
2792
|
|
|
else |
|
2793
|
|
|
{ |
|
2794
|
|
|
if (empty($prop['val'])) |
|
2795
|
|
|
{ |
|
2796
|
|
|
$val = ''; |
|
2797
|
|
|
} |
|
2798
|
|
|
else |
|
2799
|
|
|
{ |
|
2800
|
|
|
if(isset($prop['raw'])) |
|
2801
|
|
|
{ |
|
2802
|
|
|
$val = $this->_prop_encode('<![CDATA['.$prop['val'].']]>'); |
|
2803
|
|
|
} else { |
|
2804
|
|
|
$val = $this->_prop_encode(htmlspecialchars($prop['val'], ENT_NOQUOTES, 'utf-8')); |
|
2805
|
|
|
// do NOT urlencode mailto href, as no clients understands them |
|
2806
|
|
|
if ($prop['name'] == 'href' && stripos($val, 'mailto:') !== 0) |
|
2807
|
|
|
{ |
|
2808
|
|
|
$val = $this->_urlencode($val); |
|
2809
|
|
|
} |
|
2810
|
|
|
} |
|
2811
|
|
|
} |
|
2812
|
|
|
|
|
2813
|
|
|
$ret .= '<'.($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : $ns_hash[$prop['ns']].':').$prop['name']. |
|
2814
|
|
|
(empty($prop['val']) ? ' />' : '>'.$val.'</'. |
|
2815
|
|
|
($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : ($this->crrnd ? '' : $ns_hash[$prop['ns']].':')).$prop['name'].'>'); |
|
2816
|
|
|
} |
|
2817
|
|
|
} |
|
2818
|
|
|
|
|
2819
|
|
|
//error_log(__METHOD__.'('.array2string($props).") crrnd=$this->crrnd returning ".array2string($ret)); |
|
2820
|
|
|
return $ret; |
|
2821
|
|
|
} |
|
2822
|
|
|
|
|
2823
|
|
|
/** |
|
2824
|
|
|
* UTF-8 encode property values if not already done so |
|
2825
|
|
|
* |
|
2826
|
|
|
* @param string text to encode |
|
2827
|
|
|
* @return string utf-8 encoded text |
|
2828
|
|
|
*/ |
|
2829
|
|
|
function _prop_encode($text) |
|
2830
|
|
|
{ |
|
2831
|
|
|
//error_log( __METHOD__."\n" .print_r($text,true)); |
|
2832
|
|
|
//error_log("prop-encode:" . print_r($this->_prop_encoding,true)); |
|
2833
|
|
|
|
|
2834
|
|
|
switch (strtolower($this->_prop_encoding)) { |
|
2835
|
|
|
case "utf-8": |
|
2836
|
|
|
//error_log( __METHOD__."allready encoded\n" .print_r($text,true)); |
|
2837
|
|
|
return $text; |
|
2838
|
|
|
case "iso-8859-1": |
|
2839
|
|
|
case "iso-8859-15": |
|
2840
|
|
|
case "latin-1": |
|
2841
|
|
|
default: |
|
2842
|
|
|
error_log( __METHOD__."utf8 encode\n" .print_r(utf8_encode($text),true)); |
|
2843
|
|
|
return utf8_encode($text); |
|
2844
|
|
|
} |
|
2845
|
|
|
} |
|
2846
|
|
|
|
|
2847
|
|
|
/** |
|
2848
|
|
|
* Slashify - make sure path ends in a slash |
|
2849
|
|
|
* |
|
2850
|
|
|
* @param string directory path |
|
2851
|
|
|
* @returns string directory path wiht trailing slash |
|
2852
|
|
|
*/ |
|
2853
|
|
|
public static function _slashify($path) |
|
2854
|
|
|
{ |
|
2855
|
|
|
//error_log(__METHOD__." called with $path"); |
|
2856
|
|
|
if ($path[self::bytes($path)-1] != '/') { |
|
2857
|
|
|
//error_log(__METHOD__." added slash at the end of path"); |
|
2858
|
|
|
$path = $path."/"; |
|
2859
|
|
|
} |
|
2860
|
|
|
return $path; |
|
2861
|
|
|
} |
|
2862
|
|
|
|
|
2863
|
|
|
/** |
|
2864
|
|
|
* Unslashify - make sure path doesn't in a slash |
|
2865
|
|
|
* |
|
2866
|
|
|
* @param string directory path |
|
2867
|
|
|
* @returns string directory path wihtout trailing slash |
|
2868
|
|
|
*/ |
|
2869
|
|
|
public static function _unslashify($path) |
|
2870
|
|
|
{ |
|
2871
|
|
|
//error_log(__METHOD__." called with $path"); |
|
2872
|
|
|
if ($path[self::bytes($path)-1] == '/') { |
|
2873
|
|
|
$path = substr($path, 0, -1); |
|
2874
|
|
|
//error_log(__METHOD__." removed slash at the end of path"); |
|
2875
|
|
|
} |
|
2876
|
|
|
return $path; |
|
2877
|
|
|
} |
|
2878
|
|
|
|
|
2879
|
|
|
/** |
|
2880
|
|
|
* Merge two paths, make sure there is exactly one slash between them |
|
2881
|
|
|
* |
|
2882
|
|
|
* @param string parent path |
|
2883
|
|
|
* @param string child path |
|
2884
|
|
|
* @return string merged path |
|
2885
|
|
|
*/ |
|
2886
|
|
|
public static function _mergePaths($parent, $child) |
|
2887
|
|
|
{ |
|
2888
|
|
|
//error_log("merge called :\n$parent \n$child\n" . function_backtrace()); |
|
2889
|
|
|
//error_log("merge :\n".print_r($this->_mergePaths($this->_SERVER["SCRIPT_NAME"], $this->path)true)); |
|
2890
|
|
|
if ($child{0} == '/') { |
|
2891
|
|
|
return self::_unslashify($parent).$child; |
|
2892
|
|
|
} else { |
|
2893
|
|
|
return self::_slashify($parent).$child; |
|
2894
|
|
|
} |
|
2895
|
|
|
} |
|
2896
|
|
|
|
|
2897
|
|
|
/** |
|
2898
|
|
|
* mbstring.func_overload save strlen version: counting the bytes not the chars |
|
2899
|
|
|
* |
|
2900
|
|
|
* @param string $str |
|
2901
|
|
|
* @return int |
|
2902
|
|
|
*/ |
|
2903
|
|
|
public static function bytes($str) |
|
2904
|
|
|
{ |
|
2905
|
|
|
static $func_overload=null; |
|
2906
|
|
|
|
|
2907
|
|
|
if (is_null($func_overload)) |
|
2908
|
|
|
{ |
|
2909
|
|
|
$func_overload = @extension_loaded('mbstring') ? ini_get('mbstring.func_overload') : 0; |
|
2910
|
|
|
} |
|
2911
|
|
|
return $func_overload & 2 ? mb_strlen($str,'ascii') : strlen($str); |
|
2912
|
|
|
} |
|
2913
|
|
|
} |
|
2914
|
|
|
|
|
2915
|
|
|
/* |
|
2916
|
|
|
* Local variables: |
|
2917
|
|
|
* tab-width: 4 |
|
2918
|
|
|
* c-basic-offset: 4 |
|
2919
|
|
|
* End: |
|
2920
|
|
|
*/ |
|
2921
|
|
|
|