Ticket #238: webdav-extensions.php

File webdav-extensions.php, 13.9 KB (added by vl, 14 years ago)

PHP script for webdav

Line 
1<?php
2
3# PHP FCGI to Extend NGINX WebDAV
4# Written by Jason LaPorte (jason@agoragames.com)
5#
6# Copyright (C) 2009 Agora Games, Inc.
7#
8# This software is provided 'as-is', without any express or implied warranty.
9# In no event will the authors be held liable for any damages arising from the
10# use of this software.
11#
12# Permission is granted to anyone to use this software for any purpose,
13# including commercial applications, and to alter it and redistribute it
14# freely, subject to the following restrictions:
15#
16# 1. The origin of this software must not be misrepresented; you must not
17# claim that you wrote the original software. If you use this software
18# in a product, an acknowledgment in the product documentation would be
19# appreciated but is not required.
20# 2. Altered source versions must be plainly marked as such, and must not be
21# misrepresented as being the original software.
22# 3. This notice may not be removed or altered from any source distribution.
23#
24# MIME type determination, miscellaneous error handling, and general code
25# structure adapted from YUNO's similar Perl CGI script[1]. More information
26# about this script can be found on the Agora Games website[2].
27#
28# [1]: http://plan9.aichi-u.ac.jp/netlib/webappls/webdav.cgi
29# [2]: http://blog.agoragames.com/2009/03/20/webdav-nginx-play-nice/
30#
31# Version History:
32# * 3/17/2009: V1. (Initial version.)
33# * 3/19/2009: V2. (Added basic support for the COPY and MOVE methods. Added
34# ZLib license for public distribution.)
35# * 3/20/2009: V3. (Added recursive_copy and recursive_unlink functions,
36# eliminating the need to fork processes.)
37#
38# To-Do:
39# * Support (or, at least, stub out) PROPPATCH, LOCK, and UNLOCK operations.
40# * Properly support the DEPTH header in COPY requests.
41# * Properly support the various options for PROPFIND requests. (Ha!)
42
43# Recursively delete $src. This is equivalent to UNIX's "rm -rf". Be very
44# careful how you use it. Returns true on success and false on failure. (On
45# failure, files that could not be deleted will obviously remain.)
46function recursive_unlink ($src) {
47 if (file_exists ($src)) {
48 if (is_dir ($src)) {
49 foreach (scandir ($src) as $child)
50 if ($child != '.' && $child != '..')
51 recursive_unlink ("$src/$child");
52
53 return rmdir ($src);
54 }
55
56 else
57 return unlink ($src);
58 }
59
60 return false;
61}
62
63# Recursively copy $src to $dest. If $dest already exists, it will be
64# overwritten. (This makes this function not quite semantically identical to
65# UNIX's "cp -r".) Returns true on success and false on failure. On failure,
66# $dest may be destroyed, regardless of whether it existed before the copy or
67# not. Thus, if you call this function, treat $dest as forfeit.
68# FIXME: We should preserve permissions in the copy.
69function recursive_copy ($src, $dest) {
70 if (file_exists ($src)) {
71 recursive_unlink ($dest);
72
73 if (is_dir ($src)) {
74 mkdir ($dest);
75
76 foreach (scandir ($src) as $child)
77 if ($child != '.' && $child != '..')
78 if (!recursive_copy ("$src/$child", "$dest/$child")) {
79 recursive_unlink ($dest);
80 return false;
81 }
82
83 return true;
84 }
85
86 else if (is_link ($src))
87 return symlink (readlink ($src), $dest);
88
89 else
90 return copy ($src, $dest);
91 }
92
93 return false;
94}
95
96# Validate $key, treating it as $default if not supplied, according to the
97# possible values in $options.
98function validate ($key, $default, $options) {
99 if (is_null ($key) || $key === '') $key = $default;
100 return $options[$key];
101}
102
103# Gets the MIME type of a particular file by examining its file extension. This
104# could be greatly improved by doing something similar to the UNIX "file"
105# command (e.g. examining headers), but this is a quick and easy hack.
106function mime_type ($path) {
107 # I love how PHP makes me want to kill myself. Why doesn't it support
108 # nonscalar constants?
109 $mime_types = array (
110 'aif' => 'audio/x-aiff',
111 'aiff' => 'audio/x-aiff',
112 'asc' => 'text/plain',
113 'atom' => 'text/plain',
114 'au' => 'audio/basic',
115 'avi' => 'video/x-msvideo',
116 'bmp' => 'image/bmp',
117 'c' => 'text/plain',
118 'cc' => 'text/plain',
119 'cgi' => 'text/plain',
120 'cpp' => 'text/plain',
121 'css' => 'text/css',
122 'cxx' => 'text/plain',
123 'doc' => 'application/msword',
124 'dv' => 'video/x-dv',
125 'eps' => 'application/postscript',
126 'gif' => 'image/gif',
127 'gz' => 'application/x-gzip',
128 'h' => 'text/plain',
129 'hpp' => 'text/plain',
130 'hqx' => 'application/mac-binhex40',
131 'htm' => 'text/html',
132 'html' => 'text/html',
133 'hxx' => 'text/plain',
134 'jar' => 'application/java-archive',
135 'jav' => 'text/plain',
136 'java' => 'text/plain',
137 'jpeg' => 'image/jpeg',
138 'jpg' => 'image/jpeg',
139 'js' => 'text/plain',
140 'lzh' => 'application/x-lzh',
141 'm' => 'text/plain',
142 'm4a' => 'audio/mp4a-latm',
143 'mid' => 'audio/midi',
144 'midi' => 'audio/midi',
145 'mm' => 'text/plain',
146 'mov' => 'video/quicktime',
147 'mp2' => 'audio/mpeg',
148 'mp3' => 'audio/mpeg',
149 'mp4' => 'video/mp4',
150 'mpeg' => 'video/mpeg',
151 'mpg' => 'video/mpeg',
152 'ogg' => 'application/ogg',
153 'pdf' => 'application/pdf',
154 'php' => 'text/plain',
155 'pict' => 'image/pict',
156 'pl' => 'text/plain',
157 'png' => 'image/png',
158 'ppt' => 'application/vnd.ms-powerpoint',
159 'ps' => 'application/postscript',
160 'py' => 'text/plain',
161 'rb' => 'text/plain',
162 'rdf' => 'text/plain',
163 'rm' => 'audio/x-pn-realaudio',
164 'rtf' => 'text/rtf',
165 'sh' => 'text/plain',
166 'shtml' => 'text/html',
167 'snd' => 'audio/basic',
168 'svg' => 'image/svg+xml',
169 'swf' => 'application/x-shockwave-flash',
170 'tar' => 'application/x-tar',
171 'tex' => 'application/x-tex',
172 'tif' => 'image/tiff',
173 'tiff' => 'image/tiff',
174 'txt' => 'text/plain',
175 'vrml' => 'model/vrml',
176 'wav' => 'audio/x-wav',
177 'wbmp' => 'image/vnd.wap.wbmp',
178 'wrl' => 'model/vrml',
179 'xbm' => 'image/x-xbitmap',
180 'xhtml' => 'text/html',
181 'xls' => 'application/vnd.ms-excel',
182 'xml' => 'text/xml',
183 'xpm' => 'image/x-xpixmap',
184 'xsl' => 'text/xsl',
185 'zip' => 'application/zip'
186 );
187
188 $extension = substr (strrchr ($path, '.'), 1);
189 $mime = $mime_types[$extension];
190
191 return $mime ? $mime : 'application/octet-stream';
192}
193
194# PROPFIND is recursive in nature, so it gets its own function. Since we
195# assume "allprop" is set, it's not especially complicated: just stat() a file
196# and format the results as XML. (Granted, the XML formatting is kinda silly,
197# but you can blame the WebDAV folks for that.)
198function propfind ($root, $path, $depth) {
199 $href = str_replace (array ('%2F', '+'),
200 array ('/', '%20'),
201 urlencode ($path));
202 $file = $root . $path;
203 $exists = file_exists ($file);
204 $dir = NULL;
205 $stat = NULL;
206
207 if ($href === '')
208 $href = '/';
209
210 if ($exists) {
211 $dir = is_dir ($file);
212 $stat = stat ($file);
213 }
214
215 echo ('<response>');
216 echo ("<href>$href</href>");
217 echo ('<propstat>');
218
219 # File not found.
220 if (!$exists)
221 echo ('<status>HTTP/1.1 404 File Not Found</status>');
222
223 # If we can't stat the file, it's probably a permissions issue. (I use a
224 # 403 and not a 401 because the client can never recover from the error--
225 # it's based on the server's permissions, not the client's.)
226 else if (!$stat)
227 echo ('<status>HTTP/1.1 403 Forbidden</status>');
228
229 else {
230 echo ('<status>HTTP/1.1 200 OK</status>');
231 echo ('<prop>');
232
233 $name = htmlspecialchars (basename ($file));
234 $created = gmdate ('c', $stat['ctime']);
235 $modified = gmdate ('c', $stat['mtime']);
236
237 # Display various general properties.
238 echo ("<displayname>$name</displayname>");
239 echo ("<creationdate>$created</creationdate>");
240 echo ("<getlastmodified>$modified</getlastmodified>");
241 echo ('<supportedlock/>');
242
243 # If it's a directory, say so.
244 if ($dir)
245 echo ('<resourcetype><collection/></resourcetype>');
246
247 # Otherwise, print out statistics that only make sense on files.
248 else {
249 $size = $stat['size'];
250 $mime = mime_type ($path);
251 $etag = "{$stat['dev']}-{$stat['ino']}-{$stat['mtime']}";
252
253 echo ('<resourcetype/>');
254 echo ("<getcontentlength>$size</getcontentlength>");
255 echo ("<getcontenttype>$mime</getcontenttype>");
256 echo ("<getetag>$etag</getetag>");
257 }
258
259 echo ('</prop>');
260 }
261
262 echo ('</propstat>');
263 echo ('</response>');
264
265 # If this is a directory and we're set to recurse, then also print out
266 # PROPFIND responses for all of this directory's children.
267 if ($dir && $depth > 0)
268 foreach (scandir ($file) as $child)
269 if ($child != '.' && $child != '..')
270 propfind ($root, "$path/$child", $depth - 1);
271}
272
273# Response handling begins here. (Determine what method is being called, and
274# respond appropriately.)
275
276$request_method = $_SERVER['REQUEST_METHOD'];
277
278switch ($request_method) {
279 # PROPFIND supports a truly staggering amount of options and flags to limit
280 # or define the various pieces of data you're interested in retrieving. We
281 # pretend that the client has always specified "allprop" (that is, complete
282 # information about everything), and make it the client's responsibility to
283 # pull out less information if so desired.
284 case 'PROPFIND':
285 # Figure out what file they're looking at.
286 $document_root = $_SERVER['DOCUMENT_ROOT'];
287 $document_uri = urldecode (rtrim ($_SERVER['DOCUMENT_URI'], '/'));
288
289 # Figure out what depth to recurse to. Valid values are 0, 1, and infinity.
290 $depth = validate ($_SERVER['DEPTH'],
291 'infinity',
292 array ('0' => 0, '1' => 1, 'infinity' => 'infinity'));
293
294 # Choke if they specify an invalid depth.
295 if (is_null ($depth))
296 header ('HTTP/1.1 400 Bad Request');
297
298 # "allprop" with an infinite depth is a scary proposition. Supporting it is
299 # both optional and stupid, so I don't.
300 else if ($depth === 'infinity') {
301 header ('HTTP/1.1 403 Forbidden');
302 header ('Content-Type: text/xml');
303
304 echo ('<error xmlns="DAV:">');
305 echo ('<propfind-finite-depth/>');
306 echo ('</error>');
307 }
308
309 # Otherwise, give them the requested information.
310 else {
311 header ('HTTP/1.1 207 Multi-Status');
312 header ('Content-Type: text/xml');
313
314 echo ('<multistatus xmlns="DAV:">');
315 propfind ($document_root, $document_uri, $depth);
316 echo ('</multistatus>');
317 }
318
319 break;
320
321 # We handle COPY and MOVE together, since they're almost identical. COPY
322 # should support the Depth header, which modifies the semantics of the copy.
323 # We ignore it and assume an infinite depth. Additionally, we do not support
324 # copies between servers--all copies must be local. Finally, we don't
325 # properly check for disk space errors, but a generic 500 should be good
326 # enough.
327 case 'COPY':
328 case 'MOVE':
329 # Figure out what files they're looking at.
330 $host = $_SERVER['HOST'];
331 $document_root = $_SERVER['DOCUMENT_ROOT'];
332 $document_uri = urldecode (rtrim ($_SERVER['DOCUMENT_URI'], '/'));
333
334 $destination = NULL;
335 $destination_host = NULL;
336 $url = parse_url ($_SERVER['DESTINATION']);
337
338 if ($url) {
339 $destination = urldecode (rtrim ($url['path'], '/'));
340 $destination_host = $url['host'];
341 }
342
343 $source_exists = file_exists ($document_root . $document_uri);
344 $destination_exists = file_exists ($document_root . $destination);
345
346 # Do we overwrite the destination file?
347 $overwrite = validate ($ENV_['OVERWRITE'],
348 'T',
349 array ('T' => true, 't' => true,
350 'F' => false, 'f' => false));
351
352 # Choke if they specify an invalid destination or depth.
353 if (is_null ($destination))
354 header ('HTTP/1.1 400 Bad Request');
355
356 # Disallow copying/moving to a remote host.
357 else if ($host != $destination_host)
358 header ('HTTP/1.1 502 Bad Gateway');
359
360 # Fail if the source doesn't exist.
361 else if (!$source_exists)
362 header ('HTTP/1.1 404 File Not Found');
363
364 # Disallow copying/moving a file to itself.
365 else if ($document_uri == $destination)
366 header ('HTTP/1.1 403 Forbidden');
367
368 # Fail if the destination file exists and they said they didn't want to
369 # overwrite it.
370 else if ($overwrite == false && $destination_exists)
371 header ('HTTP/1.1 412 Precondition Failed');
372
373 # If we're doing a copy, copy the files.
374 # FIXME: We resort to shell since PHP doesn't support a recursive copy and
375 # I didn't want to bother implementing it (though it would be a good idea
376 # to do so at some point).
377 else if ($request_method == 'COPY' &&
378 !recursive_copy ($document_root . $document_uri,
379 $document_root . $destination))
380 header ('500 Internal Server Error');
381
382 # If we're doing a move, move the files.
383 else if ($request_method == 'MOVE' &&
384 !rename ($document_root . $document_uri,
385 $document_root . $destination))
386 header ('500 Internal Server Error');
387
388 else
389 header ($destination_exists ?
390 'HTTP/1.1 204 No Content' :
391 'HTTP/1.1 201 Created');
392
393 break;
394
395 # In case they ask, pretend that we actually know what we're talking about.
396 # (The only methods conspicuously absent are PROPPATCH, LOCK, and UNLOCK. We
397 # tell the clients that we don't support them.)
398 case 'OPTIONS':
399 header ('HTTP/1.1 200 OK');
400 header ('Allow: OPTIONS, GET, HEAD, POST, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND');
401 header ('DAV: 1, 2');
402 break;
403
404 # The following methods are valid but unimplemented. In theory, NGINX is
405 # supposed to handle each of them anyway.
406 case 'GET':
407 case 'HEAD':
408 case 'POST':
409 case 'PUT':
410 case 'DELETE':
411 case 'MKCOL':
412 header ('HTTP/1.1 501 Not Implemented');
413 break;
414
415 # Any other methods are unknown.
416 default:
417 header ('HTTP/1.1 400 Bad Request');
418 break;
419}
420
421?>