source: trunk/unsupported_plugins/PSServer/backend.php @ 1355

Last change on this file since 1355 was 1121, checked in by douglas, 11 years ago

UPDATED Ticket #1328 Provide a stab at an updated Extended File Manager. These new plugins provide document storage, saving files on the local computer with gears, user configuration, and a cleaner seperation of concerns...

File size: 30.4 KB
Line 
1<?php
2/**
3 * File Operations API
4 *
5 * This file contains the new backend File Operations API used for Xinha File
6 * Storage.  It will serve as the documentation for others wishing to implement
7 * the backend in their own language, as well as the PHP implementation.  The
8 * return data will come via the HTTP status in the case of an error, or JSON
9 * data when call has succeeded.
10 *
11 * Some examples of the URLS associated with this API:
12 * ** File Operations **
13 * ?file&rename&filename=''&newname=''
14 * ?file&copy&filename=''
15 * ?file&delete&filename=''
16 *
17 * ** Directory Operations **
18 * ?directory&listing
19 * ?directory&create&dirname=''
20 * ?directory&delete&dirname=''
21 * ?directory&rename&dirname=''&newname=''
22 *
23 * ** Image Operations **
24 * ?image&filename=''&[scale|rotate|convert]
25 *
26 * ** Upload **
27 * ?upload&filedata=[binary|text]&filename=''&replace=[true|false]
28 *
29 * @author Douglas Mayle <douglas@openplans.org>
30 * @version 1.0
31 * @package PersistentStorage
32 *
33 */
34
35/**
36 * Config file
37 */
38require_once('config.inc.php');
39
40
41// Strip slashes if MQGPC is on
42set_magic_quotes_runtime(0);
43if(get_magic_quotes_gpc())
44{
45  $to_clean = array(&$_GET, &$_POST, &$_REQUEST, &$_COOKIE);
46  while(count($to_clean))
47  {
48    $cleaning =& $to_clean[array_pop($junk = array_keys($to_clean))];
49    unset($to_clean[array_pop($junk = array_keys($to_clean))]);
50    foreach(array_keys($cleaning) as $k)
51    {
52      if(is_array($cleaning[$k]))
53      {
54        $to_clean[] =& $cleaning[$k];
55      }
56      else
57      {
58        $cleaning[$k] = stripslashes($cleaning[$k]);
59      }
60    }
61  }
62}
63
64// Set the return headers for a JSON response.
65header('Cache-Control: no-cache, must-revalidate');
66header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
67//header('Content-type: application/json');
68
69
70/**#@+
71 * Constants
72 *
73 * Since this is being used as part of a web interface, we'll set some rather
74 * conservative limits to keep from overloading the user or the backend.
75 */
76
77/**
78 * This is the maximum folder depth to present to the user
79 */
80define('MAX_DEPTH', 10);
81
82/**
83 * This is the maximum number of file entries per folder to show to the user,
84 */
85define('MAX_FILES_PER_FOLDER', 50);
86
87/**
88 * This array contains the default HTTP Response messages
89 *
90 */
91$HTTP_ERRORS = array(
92        'HTTP_SUCCESS_OK' => array('code' => 200, 'message' => 'OK'),
93        'HTTP_SUCCESS_CREATED' => array('code' => 201, 'message' => 'Created'),
94        'HTTP_SUCCESS_ACCEPTED' => array('code' => 202, 'message' => 'Accepted'),
95        'HTTP_SUCCESS_NON_AUTHORITATIVE' => array('code' => 203, 'message' => 'Non-Authoritative Information'),
96        'HTTP_SUCCESS_NO_CONTENT' => array('code' => 204, 'message' => 'No Content'),
97        'HTTP_SUCCESS_RESET_CONTENT' => array('code' => 205, 'message' => 'Reset Content'),
98        'HTTP_SUCCESS_PARTIAL_CONTENT' => array('code' => 206, 'message' => 'Partial Content'),
99
100        'HTTP_REDIRECTION_MULTIPLE_CHOICES' => array('code' => 300, 'message' => 'Multiple Choices'),
101        'HTTP_REDIRECTION_PERMANENT' => array('code' => 301, 'message' => 'Moved Permanently'),
102        'HTTP_REDIRECTION_FOUND' => array('code' => 302, 'message' => 'Found'),
103        'HTTP_REDIRECTION_SEE_OTHER' => array('code' => 303, 'message' => 'See Other'),
104        'HTTP_REDIRECTION_NOT_MODIFIED' => array('code' => 304, 'message' => 'Not Modified'),
105        'HTTP_REDIRECTION_USE_PROXY' => array('code' => 305, 'message' => 'Use Proxy'),
106        'HTTP_REDIRECTION_UNUSED' => array('code' => 306, 'message' => '(Unused)'),
107        'HTTP_REDIRECTION_TEMPORARY' => array('code' => 307, 'message' => 'Temporary Redirect'),
108
109        'HTTP_CLIENT_BAD_REQUEST' => array('code' => 400, 'message' => 'Bad Request'),
110        'HTTP_CLIENT_UNAUTHORIZED' => array('code' => 401, 'message' => 'Unauthorized'),
111        'HTTP_CLIENT_PAYMENT_REQUIRED' => array('code' => 402, 'message' => 'Payment Required'),
112        'HTTP_CLIENT_FORBIDDEN' => array('code' => 403, 'message' => 'Forbidden'),
113        'HTTP_CLIENT_NOT_FOUND' => array('code' => 404, 'message' => 'Not Found'),
114        'HTTP_CLIENT_METHOD_NOT_ALLOWED' => array('code' => 405, 'message' => 'Method Not Allowed'),
115        'HTTP_CLIENT_NOT_ACCEPTABLE' => array('code' => 406, 'message' => 'Not Acceptable'),
116        'HTTP_CLIENT_PROXY_AUTH_REQUIRED' => array('code' => 407, 'message' => 'Proxy Authentication Required'),
117        'HTTP_CLIENT_REQUEST_TIMEOUT' => array('code' => 408, 'message' => 'Request Timeout'),
118        'HTTP_CLIENT_CONFLICT' => array('code' => 409, 'message' => 'Conflict'),
119        'HTTP_CLIENT_GONE' => array('code' => 410, 'message' => 'Gone'),
120        'HTTP_CLIENT_LENGTH_REQUIRED' => array('code' => 411, 'message' => 'Length Required'),
121        'HTTP_CLIENT_PRECONDITION_FAILED' => array('code' => 412, 'message' => 'Precondition Failed'),
122        'HTTP_CLIENT_REQUEST_TOO_LARGE' => array('code' => 413, 'message' => 'Request Entity Too Large'),
123        'HTTP_CLIENT_REQUEST_URI_TOO_LARGE' => array('code' => 414, 'message' => 'Request-URI Too Long'),
124        'HTTP_CLIENT_UNSUPPORTED_MEDIA_TYPE' => array('code' => 415, 'message' => 'Unsupported Media Type'),
125        'HTTP_CLIENT_REQUESTED_RANGE_NOT_POSSIBLE' => array('code' => 416, 'message' => 'Requested Range Not Satisfiable'),
126        'HTTP_CLIENT_EXPECTATION_FAILED' => array('code' => 417, 'message' => 'Expectation Failed'),
127
128        'HTTP_SERVER_INTERNAL' => array('code' => 500, 'message' => 'Internal Server Error'),
129        'HTTP_SERVER_NOT_IMPLEMENTED' => array('code' => 501, 'message' => 'Not Implemented'),
130        'HTTP_SERVER_BAD_GATEWAY' => array('code' => 502, 'message' => 'Bad Gateway'),
131        'HTTP_SERVER_SERVICE_UNAVAILABLE' => array('code' => 503, 'message' => 'Service Unavailable'),
132        'HTTP_SERVER_GATEWAY_TIMEOUT' => array('code' => 504, 'message' => 'Gateway Timeout'),
133        'HTTP_SERVER_UNSUPPORTED_VERSION' => array('code' => 505, 'message' => 'HTTP Version not supported')
134    );
135
136/**
137 * This is a regular expression used to detect reserved or dangerous filenames.
138 * Most NTFS special filenames begin with a dollar sign ('$'), and most Unix
139 * special filenames begin with a period (.), so we'll keep them out of this
140 * list and just prevent those two characters in the first position.  The rest
141 * of the special filenames are included below.
142 */
143define('RESERVED_FILE_NAMES', 'pagefile\.sys|a\.out|core');
144/**
145 * This is a regular expression used to detect invalid file names.  It's more
146 * strict than necessary, to be valid multi-platform, but not posix-strict
147 * because we want to allow unicode filenames.  We do, however, allow path
148 * seperators in the filename because the file could exist in a subdirectory.
149 */
150define('INVALID_FILE_NAME','^[.$]|^(' . RESERVED_FILE_NAMES . ')$|[?%*:|"<>]');
151/**#@-*/
152
153function main($arguments) {
154    $config = get_config(true);
155
156    // Trigger authentication if it's configured.
157    if ($config['capabilities']['user_storage'] && empty($_SERVER['PHP_AUTH_USER'])) {
158        header('WWW-Authenticate: Basic realm="Xinha Persistent Storage"');
159        header('HTTP/1.0 401 Unauthorized');
160        echo "You must login in order to use Persistent Storage";
161        exit;
162    }
163    if (!input_valid($arguments, $config['capabilities'])) {
164        http_error_exit();
165    }
166    if (!method_valid($arguments)) {
167        http_error_exit('HTTP_CLIENT_METHOD_NOT_ALLOWED');
168    }
169    if (!dispatch($arguments)) {
170        http_error_exit();
171    }
172    exit();
173}
174
175main($_REQUEST + $_FILES);
176// ************************************************************
177// ************************************************************
178//                       Helper Functions               
179// ************************************************************
180// ************************************************************
181
182/**
183 * Take the call and properly dispatch it to the methods below.  This method
184 * assumes valid input.
185 */
186function dispatch($arguments) {
187    if (array_key_exists('file', $arguments)) {
188        if (array_key_exists('rename', $arguments)) {
189            if (!file_directory_rename($arguments['filename'], $arguments['newname'], working_directory())) {
190                http_error_exit('HTTP_CLIENT_FORBIDDEN');
191            }
192            return true;
193        }
194        if (array_key_exists('copy', $arguments)) {
195            if (!$newentry = file_copy($arguments['filename'], working_directory())) {
196                http_error_exit('HTTP_CLIENT_FORBIDDEN');
197            }
198            echo json_encode($newentry);
199            return true;
200        }
201        if (array_key_exists('delete', $arguments)) {
202            if (!file_delete($arguments['filename'], working_directory())) {
203                http_error_exit('HTTP_CLIENT_FORBIDDEN');
204            }
205            return true;
206        }
207    }
208    if (array_key_exists('directory', $arguments)) {
209        if (array_key_exists('listing', $arguments)) {
210            echo json_encode(directory_listing());
211            return true;
212        }
213        if (array_key_exists('create', $arguments)) {
214            if (!directory_create($arguments['dirname'], working_directory())) {
215                http_error_exit('HTTP_CLIENT_FORBIDDEN');
216            }
217            return true;
218        }
219        if (array_key_exists('delete', $arguments)) {
220            if (!directory_delete($arguments['dirname'], working_directory())) {
221                http_error_exit('HTTP_CLIENT_FORBIDDEN');
222            }
223            return true;
224        }
225        if (array_key_exists('rename', $arguments)) {
226            if (!file_directory_rename($arguments['dirname'], $arguments['newname'], working_directory())) {
227                http_error_exit('HTTP_CLIENT_FORBIDDEN');
228            }
229            return true;
230        }
231    }
232    if (array_key_exists('image', $arguments)) {
233    }
234    if (array_key_exists('upload', $arguments)) {
235        store_uploaded_file($arguments['filename'], $arguments['filedata'], working_directory());
236        return true;
237    }
238
239    return false;
240}
241
242/**
243 * Validation of the HTTP Method.  For operations that make changes we require
244 * POST.  To err on the side of safety, we'll only allow GET for known safe
245 * operations.  This way, if the API is extended, and the method is not
246 * updated, we will not accidentally expose non-idempotent methods to GET.
247 * This method can only correctly validate the operation if the input is
248 * already known to be valid.
249 *
250 * @param array $arguments The arguments array received by the page.
251 * @return boolean Whether or not the HTTP method is correct for the given input.
252 */
253function method_valid($arguments) {
254    // We assume that the only
255    $method = $_SERVER['REQUEST_METHOD'];
256
257    if ($method == 'GET') {
258      if (array_key_exists('directory', $arguments) && array_key_exists('listing', $arguments)) {
259          return true;
260      }
261
262      return false;
263    }
264
265    if ($method == 'POST') {
266        return true;
267    }
268    return false;
269}
270
271/**
272 * Validation of the user input.  We'll verify what we receive from the user,
273 * and send an error in the case of malformed input.
274 *
275 * Some examples of the URLS associated with this API:
276 * ** File Operations **
277 * ?file&delete&filename=''
278 * ?file&copy&filename=''
279 * ?file&rename&filename=''&newname=''
280 *
281 * ** Directory Operations **
282 * ?directory&listing
283 * ?directory&create&dirname=''
284 * ?directory&delete&dirname=''
285 * ?directory&rename&dirname=''&newname=''
286 *
287 * ** Image Operations **
288 * ?image&filename=''&[scale|rotate|convert]
289 *
290 * ** Upload **
291 * ?upload&filedata=[binary|text]&filename=''&replace=[true|false]
292 *
293 * @param array $arguments The arguments array received by the page.
294 * @param array $capabilities The capabilities config array used to limit operations.
295 * @return boolean Whether or not the input received is valid.
296 */
297function input_valid($arguments, $capabilities) {
298    // This is going to be really ugly code because it's basically a DFA for
299    // parsing arguments.  To make things a little clearer, I'll put a
300    // pseudo-BNF for each block to show the decision structure.
301    //
302    // file[empty] filename[valid] (delete[empty] | copy[empty] | (rename[empty] newname[valid]))
303    if ($capabilities['file_operations'] &&
304        array_key_exists('file', $arguments) &&
305        empty($arguments['file']) &&
306        array_key_exists('filename', $arguments) &&
307        !ereg(INVALID_FILE_NAME, $arguments['filename'])) {
308
309        if (array_key_exists('delete', $arguments) &&
310            empty($arguments['delete']) &&
311            3 == count($arguments)) {
312
313            return true;
314        }
315
316        if (array_key_exists('copy', $arguments) &&
317            empty($arguments['copy']) &&
318            3 == count($arguments)) {
319
320            return true;
321        }
322
323        if (array_key_exists('rename', $arguments) &&
324            empty($arguments['rename']) &&
325            4 == count($arguments)) {
326
327            if (array_key_exists('newname', $arguments) &&
328                !ereg(INVALID_FILE_NAME, $arguments['newname'])) {
329
330                return true;
331            }
332        }
333
334        return false;
335    } elseif (array_key_exists('file', $arguments)) {
336        // This isn't necessary because we'll fall through to false, but I'd
337        // rather return earlier than later.
338        return false;
339    }
340
341    // directory[empty] (listing[empty] | (dirname[valid] (create[empty] | delete[empty] | (rename[empty] newname[valid]))))
342    if ($capabilities['directory_operations'] &&
343        array_key_exists('directory', $arguments) &&
344        empty($arguments['directory'])) {
345
346        if (array_key_exists('listing', $arguments) &&
347            empty($arguments['listing']) &&
348            2 == count($arguments)) {
349
350            return true;
351        }
352
353        if (array_key_exists('dirname', $arguments) &&
354            !ereg(INVALID_FILE_NAME, $arguments['dirname'])) {
355
356            if (array_key_exists('create', $arguments) &&
357                empty($arguments['create']) &&
358                3 == count($arguments)) {
359
360                return true;
361            }
362
363            if (array_key_exists('delete', $arguments) &&
364                empty($arguments['delete']) &&
365                3 == count($arguments)) {
366
367                return true;
368            }
369
370            if (array_key_exists('rename', $arguments) &&
371                empty($arguments['rename']) &&
372                4 == count($arguments)) {
373
374                if (array_key_exists('newname', $arguments) &&
375                    !ereg(INVALID_FILE_NAME, $arguments['newname'])) {
376
377                    return true;
378                }
379            }
380        }
381
382        return false;
383    } elseif (array_key_exists('directory', $arguments)) {
384        // This isn't necessary because we'll fall through to false, but I'd
385        // rather return earlier than later.
386        return false;
387    }
388
389    // image[empty] filename[valid] ((scale[empty] dimensions[valid]) | (rotate[empty] angle[valid]) | (convert[empty] imagetype[valid]))
390    if ($capabilities['image_operations'] &&
391        array_key_exists('image', $arguments) &&
392        empty($arguments['image']) &&
393        array_key_exists('filename', $arguments) &&
394        !ereg(INVALID_FILE_NAME, $arguments['filename']) &&
395        4 == count($arguments)) {
396
397        if (array_key_exists('scale', $arguments) &&
398            empty($arguments['scale']) &&
399            !ereg(INVALID_FILE_NAME, $arguments['dimensions'])) {
400            // TODO: FIX REGEX
401            http_error_exit();
402
403            return true;
404        }
405
406        if (array_key_exists('rotate', $arguments) &&
407            empty($arguments['rotate']) &&
408            !ereg(INVALID_FILE_NAME, $arguments['angle'])) {
409            // TODO: FIX REGEX
410            http_error_exit();
411
412            return true;
413        }
414
415        if (array_key_exists('convert', $arguments) &&
416            empty($arguments['convert']) &&
417            !ereg(INVALID_FILE_NAME, $arguments['imagetype'])) {
418            // TODO: FIX REGEX
419            http_error_exit();
420
421            return true;
422        }
423
424        return false;
425    } elseif (array_key_exists('image', $arguments)) {
426        // This isn't necessary because we'll fall through to false, but I'd
427        // rather return earlier than later.
428        return false;
429    }
430
431    // upload[empty] filedata[binary|text] replace[true|false] filename[valid]?
432    if ($capabilities['upload_operations'] &&
433        array_key_exists('upload', $arguments) &&
434        empty($arguments['upload']) &&
435        array_key_exists('filedata', $arguments) &&
436        !empty($arguments['filedata']) &&
437        array_key_exists('replace', $arguments) &&
438        ereg('true|false', $arguments['replace'])) {
439
440        if (4 == count($arguments) &&
441            array_key_exists('filename', $arguments) &&
442            !ereg(INVALID_FILE_NAME, $arguments['filename'])) {
443
444            return true;
445        }
446
447        if (3 == count($arguments)) {
448
449            return true;
450        }
451
452        return false;
453    } elseif (array_key_exists('upload', $arguments)) {
454        // This isn't necessary because we'll fall through to false, but I'd
455        // rather return earlier than later.
456        return false;
457    }
458
459
460    return false;
461}
462
463/**
464 * HTTP level error handling.
465 * @param integer $code The HTTP error code to return to the client.  This defaults to 400.
466 * @param string $message Error message to send to the client.  This defaults to the standard HTTP error messages.
467 */
468function http_error_exit($error = 'HTTP_CLIENT_BAD_REQUEST', $message='') {
469    global $HTTP_ERRORS;
470    $message = !empty($message) ? $message : "HTTP/1.0 {$HTTP_ERRORS[$error]['code']} {$HTTP_ERRORS[$error]['message']}";
471    header($message);
472    exit($message);
473}
474
475/**
476 * Process the config and return the absolute directory we should be working with,
477 * @return string contains the path of the directory all file operations are limited to.
478 */
479function working_directory() {
480    $config = get_config(true);
481    return realpath(getcwd() . DIRECTORY_SEPARATOR . $config['storage_dir'] . DIRECTORY_SEPARATOR);
482}
483
484/**
485 * Check to see if the supplied filename is inside
486 */
487function directory_contains($container_directory, $checkfile) {
488
489    // Get the canonical directory and canonical filename.  We add a directory
490    // seperator to prevent the user from sidestepping into a sibling directory
491    // that starts with the same prefix. (e.g. from /home/john to
492    // /home/johnson)
493    $container_directory = realpath($container_directory) + DIRECTORY_SEPARATOR;
494    $checkfile = realpath($checkfile);
495
496    // Now that we have the canonical versions, we can do a string comparison
497    // to see if checkfile is inside of container_directory.
498    if (strlen($checkfile) <= strlen($container_directory)) {
499        // We don't consider the directory to be inside of itself.  This
500        // prevents users from trying to perform operations on the container
501        // directory itself.
502        return false;
503    }
504
505    // PHP equivalent of string.startswith()
506    return substr($checkfile, 0, strlen($container_directory)) == $container_directory;
507}
508
509/**#@+
510 *                             Directory Operations
511 * {@internal *****************************************************************
512 * **************************************************************************}}
513 */
514
515/**
516 * Return a directory listing as a PHP array.
517 * @param string $directory The directory to return a listing of.
518 * @param integer $depth The private argument used to limit recursion depth.
519 * @return array representing the directory structure.
520 */
521
522function directory_listing($directory='', $depth=1) {
523    // We return an empty array if the directory is empty
524    $result = array('$type'=>'folder');
525
526    // We won't recurse below MAX_DEPTH.
527    if ($depth > MAX_DEPTH) {
528        return $result;
529    }
530
531    $path = empty($directory) ? working_directory() : $directory;
532
533    // We'll open the directory to check each of the entries
534    if ($dir = opendir($path)) {
535
536        // We'll keep track of how many file we process.
537        $count = 0;
538
539        // For each entry in the file
540        while (($file = readdir($dir)) !== false) {
541
542            // Limit the number of files we process in this folder
543            $count += 1;
544            if ($count > MAX_FILES_PER_FOLDER) {
545                return $result;
546            }
547
548            // Ignore hidden files (this includes special files '.' and '..')
549            if (strlen($file) && ($file[0] == '.')) {
550                continue;
551            }
552
553            $filepath = $path . DIRECTORY_SEPARATOR . $file;
554
555            if (filetype($filepath) == 'dir') {
556                // We'll recurse and add those results
557                $result[$file] = directory_listing($filepath, $depth + 1);
558            } else {
559                // We'll check to see if we can read any image information from
560                // the file.  If so, we know it's an image, and we can return
561                // it's metadata.
562                $imageinfo = @getimagesize($filepath);
563                if ($imageinfo) {
564
565                  $result[$file] = array('$type'=>'image','metadata'=>array(
566                                  'width'=>$imageinfo[0],
567                                  'height'=>$imageinfo[1],
568                                  'mimetype'=>$imageinfo['mime']
569                              ));
570
571                } elseif ($extension = strrpos($file, '.')) {
572                     $extension = substr($file, $extension);
573                     if (($extension == '.htm') || ($extension == '.html')) {
574                         $result[$file] = array('$type'=>'html');
575                     } else {
576                         $result[$file] = array('$type'=>'text');
577                     }
578                } else {
579                    $result[$file] = array('$type'=>'document');
580                }
581            }
582        }
583       
584        closedir($dir);
585    }
586    return $result;
587}
588
589/**
590 * Create a directory, limiting operations to the chroot directory.
591 * @param string $dirname The path to the directory, relative to $chroot.
592 * @param string $chroot Only directories inside this directory or its subdirectories can be affected.
593 * @return boolean Returns TRUE if successful, and FALSE otherwise.
594 */
595function directory_create($dirname, $chroot) {
596    // If chroot is empty, then we will not perform the operation.
597    if (empty($chroot)) {
598        return false;
599    }
600
601    // We have to take the dirname of the parent directory first, since
602    // realpath just returns false if the directory doesn't already exist on
603    // the filesystem.
604    $createparent = realpath(dirname($chroot . DIRECTORY_SEPARATOR . $dirname));
605    $createsub = basename($chroot . DIRECTORY_SEPARATOR . $dirname);
606
607    // The bailout rules for directories that don't exist are complicated
608    // because of having to work around realpath.  If the parent directory is
609    // the same as the chroot, it won't be contained.  For this case, we'll
610    // check to see if the chroot and the parent are the same and allow it only
611    // if the sub portion of dirname is not-empty.
612    if (!directory_contains($chroot, $createparent) &&
613        !(($chroot == $createparent) && !empty($createsub))) {
614        return false;
615    }
616
617    return @mkdir($createparent . DIRECTORY_SEPARATOR . $createsub);
618}
619
620/**
621 * Delete a directory, limiting operations to the chroot directory.
622 * @param string $dirname The path to the directory, relative to $chroot.
623 * @param string $chroot Only directories inside this directory or its subdirectories can be affected.
624 * @return boolean Returns TRUE if successful, and FALSE otherwise.
625 */
626function directory_delete($dirname, $chroot) {
627    // If chroot is empty, then we will not perform the operation.
628    if (empty($chroot)) {
629        return false;
630    }
631
632    // $dirname is relative to $chroot.
633    $dirname = realpath($chroot . DIRECTORY_SEPARATOR . $dirname);
634
635    // Limit directory operations to the supplied directory.
636    if (!directory_contains($chroot, $dirname)) {
637        return false;
638    }
639
640    return @rmdir($dirname);
641}
642
643
644/**#@-*/
645/**#@+
646 *                               File Operations
647 * {@internal *****************************************************************
648 * **************************************************************************}}
649 */
650
651/**
652 * Rename a file or directory, limiting operations to the chroot directory.
653 * @param string $filename The path to the file or directory, relative to $chroot.
654 * @param string $renameto The path to the renamed file or directory, relative to $chroot.
655 * @param string $chroot Only files and directories inside this directory or its subdirectories can be affected.
656 * @return boolean Returns TRUE if successful, and FALSE otherwise.
657 */
658function file_directory_rename($filename, $renameto, $chroot) {
659    // If chroot is empty, then we will not perform the operation.
660    if (empty($chroot)) {
661        return false;
662    }
663
664    // $filename is relative to $chroot.
665    $filename = realpath($chroot . DIRECTORY_SEPARATOR . $filename);
666
667    // We have to take the dirname of the renamed file or directory first,
668    // since realpath just returns false if the file or direcotry doesn't
669    // already exist on the filesystem.
670    $renameparent = realpath(dirname($chroot . DIRECTORY_SEPARATOR . $renameto));
671    $renamefile = basename($chroot . DIRECTORY_SEPARATOR . $renameto);
672
673    // Limit file operations to the supplied directory.
674    if (!directory_contains($chroot, $filename)) {
675        return false;
676    }
677
678    // The bailout rules for the renamed file or directory are more complicated
679    // because of having to work around realpath.  If the renamed parent
680    // directory is the same as the chroot, it won't be contained.  For this
681    // case, we'll check to see if they're the same and allow it only if the
682    // file portion of renameto is not-empty.
683    if (!directory_contains($chroot, $renameparent) &&
684        !(($chroot == $renameparent) && !empty($renamefile))) {
685        return false;
686    }
687
688    return @rename($filename, $renameparent . DIRECTORY_SEPARATOR . $renamefile);
689}
690
691
692/**
693 * Copy a file, limiting operations to the chroot directory.
694 * @param string $filename The path to the file, relative to $chroot.
695 * @param string $chroot Only files inside this directory or its subdirectories can be affected.
696 * @return boolean Returns TRUE if successful, and FALSE otherwise.
697 */
698function file_copy($filename, $chroot) {
699    // If chroot is empty, then we will not perform the operation.
700    if (empty($chroot)) {
701        return false;
702    }
703
704    // $filename is relative to $chroot.
705    $filename = realpath($chroot . DIRECTORY_SEPARATOR . $filename);
706
707    // Limit file operations to the supplied directory.
708    if (!directory_contains($chroot, $filename)) {
709        return false;
710    }
711
712    // The PHP copy function blindly copies over existing files.  We don't wish
713    // this to happen, so we have to perform the copy a bit differently.  If we
714    // do a check to make sure the file exists, there's always the chance of a
715    // race condition where someone else creates the file in between the check
716    // and the copy.  The only safe way to ensure we don't overwrite an
717    // existing file is to call fopen in create-only mode (mode 'x').  If it
718    // succeeds, the file did not exist before, and we've successfully created
719    // it, meaning we own the file.  After that, we can safely copy over our
720    // own file.
721    for ($count=1; $count<MAX_FILES_PER_FOLDER; ++$count) {
722        if (strpos(basename($filename), '.')) {
723            $extpos = strrpos($filename, '.');
724            $copyname = substr($filename, 0, $extpos) . '_' . $count . substr($filename, $extpos);
725        } else {
726            // There's no extension, we we'll just add our copy count.
727            $copyname = $filename . '_' . $count;
728        }
729        if ($file = @fopen($copyname, 'x')) {
730            // We've successfully created a file, so it's ours.  We'll close
731            // our handle.
732            if (!@fclose($file)) {
733                // There was some problem with our file handle.
734                return false;
735            }
736
737            // Now we copy over the file we created.
738            if (!@copy($filename, $copyname)) {
739                // The copy failed, even though we own the file, so we'll clean
740                // up by removing the file and report failure.
741                file_delete($filename, $chroot);
742                return false;
743            }
744
745            return array(basename($copyname)=>array('$type'=>'image'));
746        }
747    }
748
749    return false;
750}
751
752/**
753 * Delete a file, limiting operations to the chroot directory.
754 * @param string $filename The path to the file, relative to $chroot.
755 * @param string $chroot Only files inside this directory or its subdirectories can be affected.
756 * @return boolean Returns TRUE if successful, and FALSE otherwise.
757 */
758function file_delete($filename, $chroot) {
759    // If chroot is empty, then we will not perform the operation.
760    if (empty($chroot)) {
761        return false;
762    }
763
764    // $filename is relative to $chroot.
765    $filename = realpath($chroot . DIRECTORY_SEPARATOR . $filename);
766
767    // Limit file operations to the supplied directory.
768    if (!directory_contains($chroot, $filename)) {
769        return false;
770    }
771
772    return @unlink($filename);
773}
774/**#@-*/
775/**#@+
776 *                              Upload Operations
777 * {@internal *****************************************************************
778 * **************************************************************************}}
779 */
780
781function store_uploaded_file($filename, $filedata, $chroot) {
782
783    // If chroot is empty, then we will not perform the operation.
784    if (empty($chroot)) {
785        return false;
786    }
787
788    // If the filename is empty, it was possibly supplied as part of the
789    // upload.
790    $filename = empty($filename) ? $filedata['name'] : $filename;
791
792    // We have to take the dirname of the parent directory first, since
793    // realpath just returns false if the directory doesn't already exist on
794    // the filesystem.
795    $uploadparent = realpath(dirname($chroot . DIRECTORY_SEPARATOR . $filename));
796    $uploadfile = basename($chroot . DIRECTORY_SEPARATOR . $filename);
797
798    // The bailout rules for directories that don't exist are complicated
799    // because of having to work around realpath.  If the parent directory is
800    // the same as the chroot, it won't be contained.  For this case, we'll
801    // check to see if the chroot and the parent are the same and allow it only
802    // if the sub portion of dirname is not-empty.
803    if (!directory_contains($chroot, $uploadparent) &&
804        !(($chroot == $uploadparent) && !empty($uploadfile))) {
805        return false;
806    }
807 
808    $target_path = $uploadparent . DIRECTORY_SEPARATOR . $uploadfile;
809
810    if (is_array($filedata)) {
811        // We've received the file as an upload, so it's been saved to a temp
812        // directory.  We'll move it to where it belongs.
813     
814        if(move_uploaded_file($filedata['tmp_name'], $target_path)) {
815            return true;
816        }
817    } elseif ($file = @fopen($target_path, 'w')) {
818        // We've received the file as data.  We'll create/open the file and
819        // save the data.
820        @fwrite($file, $filedata);
821        @fclose($file);
822        return true;
823    }
824 
825    return false;
826}
827
828/**#@-*/
829
830?>
Note: See TracBrowser for help on using the repository browser.