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