PageRenderTime 41ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/scripts/xinha/plugins/PSServer/backend.php

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