modfilehandler.class.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. <?php
  2. /*
  3. * This file is part of MODX Revolution.
  4. *
  5. * Copyright (c) MODX, LLC. All Rights Reserved.
  6. *
  7. * For complete copyright and license information, see the COPYRIGHT and LICENSE
  8. * files found in the top-level directory of this distribution.
  9. */
  10. /**
  11. * Assists with directory/file manipulation
  12. *
  13. * @package modx
  14. */
  15. class modFileHandler {
  16. /**
  17. * An array of configuration properties for the class
  18. * @var array $config
  19. */
  20. public $config = array();
  21. /**
  22. * The current context in which this File Manager instance should operate
  23. * @var modContext|null $context
  24. */
  25. public $context = null;
  26. /**
  27. * The constructor for the modFileHandler class
  28. *
  29. * @param modX &$modx A reference to the modX object.
  30. * @param array $config An array of options.
  31. */
  32. function __construct(modX &$modx, array $config = array()) {
  33. $this->modx =& $modx;
  34. $this->config = array_merge($this->config, $this->modx->_userConfig, $config);
  35. if (!isset($this->config['context'])) {
  36. $this->config['context'] = $this->modx->context->get('key');
  37. }
  38. $this->context = $this->modx->getContext($this->config['context']);
  39. }
  40. /**
  41. * Dynamically creates a modDirectory or modFile object.
  42. *
  43. * The object is created based on the type of resource provided.
  44. *
  45. * @param string $path The absolute path to the filesystem resource.
  46. * @param array $options Optional. An array of options for the object.
  47. * @param string $overrideClass Optional. If provided, will force creation
  48. * of the object as the specified class.
  49. * @return modFile|modDirectory The appropriate modFile/modDirectory object
  50. */
  51. public function make($path, array $options = array(), $overrideClass = '') {
  52. $path = $this->sanitizePath($path);
  53. if (!empty($overrideClass)) {
  54. $class = $overrideClass;
  55. } else {
  56. if (is_dir($path)) {
  57. $path = $this->postfixSlash($path);
  58. $class = 'modDirectory';
  59. } else {
  60. $class = 'modFile';
  61. }
  62. }
  63. return new $class($this, $path, $options);
  64. }
  65. /**
  66. * Get the modX base path for the user.
  67. *
  68. * @return string The base path
  69. */
  70. public function getBasePath() {
  71. $basePath = $this->context->getOption('filemanager_path', '', $this->config);
  72. /* expand placeholders */
  73. $basePath = str_replace(array(
  74. '{base_path}',
  75. '{core_path}',
  76. '{assets_path}',
  77. ), array(
  78. $this->context->getOption('base_path', MODX_BASE_PATH, $this->config),
  79. $this->context->getOption('core_path', MODX_CORE_PATH, $this->config),
  80. $this->context->getOption('assets_path', MODX_ASSETS_PATH, $this->config),
  81. ), $basePath);
  82. return !empty($basePath) ? $this->postfixSlash($basePath) : $basePath;
  83. }
  84. /**
  85. * Get base URL of file manager
  86. *
  87. * @return string The base URL
  88. */
  89. public function getBaseUrl() {
  90. $baseUrl = $this->context->getOption('filemanager_url', $this->context->getOption('rb_base_url', MODX_BASE_URL, $this->config), $this->config);
  91. /* expand placeholders */
  92. $baseUrl = str_replace(array(
  93. '{base_url}',
  94. '{core_url}',
  95. '{assets_url}',
  96. ), array(
  97. $this->context->getOption('base_url', MODX_BASE_PATH, $this->config),
  98. $this->context->getOption('core_url', MODX_CORE_PATH, $this->config),
  99. $this->context->getOption('assets_url', MODX_ASSETS_PATH, $this->config),
  100. ), $baseUrl);
  101. return !empty($baseUrl) ? $this->postfixSlash($baseUrl) : $baseUrl;
  102. }
  103. /**
  104. * Sanitize the specified path
  105. *
  106. * @param string $path The path to clean
  107. * @return string The sanitized path
  108. */
  109. public function sanitizePath($path) {
  110. return preg_replace(array("/\.*[\/|\\\]/i", "/[\/|\\\]+/i"), array('/', '/'), $path);
  111. }
  112. /**
  113. * Ensures that the passed path has a / at the end
  114. *
  115. * @param string $path
  116. * @return string The postfixed path
  117. */
  118. public function postfixSlash($path) {
  119. $len = strlen($path);
  120. if (substr($path, $len - 1, $len) != '/') {
  121. $path .= '/';
  122. }
  123. return $path;
  124. }
  125. /**
  126. * Gets the directory path for a given file
  127. *
  128. * @param string $fileName The path for a file
  129. * @return string The directory path of the given file
  130. */
  131. public function getDirectoryFromFile($fileName) {
  132. $dir = dirname($fileName);
  133. return $this->postfixSlash($dir);
  134. }
  135. /**
  136. * Tells if a file is a binary file or not.
  137. *
  138. * @param string $file
  139. * @return boolean True if a binary file.
  140. */
  141. public function isBinary($file) {
  142. if (!file_exists($file) || !is_file($file)) {
  143. return false;
  144. }
  145. if (filesize($file) > 0 && class_exists('\finfo')) {
  146. $finfo = new \finfo(FILEINFO_MIME);
  147. return substr($finfo->file($file), 0, 4) !== 'text';
  148. }
  149. $fh = @fopen($file, 'r');
  150. $blk = @fread($fh, 512);
  151. @fclose($fh);
  152. @clearstatcache();
  153. return (substr_count($blk, "^ -~" /*. "^\r\n"*/) / 512 > 0.3) || (substr_count($blk, "\x00") > 0);
  154. }
  155. }
  156. /**
  157. * Abstract class for handling file system resources (files or folders).
  158. *
  159. * Not to be instantiated directly - you should implement your own derivative class.
  160. *
  161. * @package modx
  162. */
  163. abstract class modFileSystemResource {
  164. /**
  165. * @var string The absolute path of the file system resource
  166. */
  167. protected $path;
  168. /**
  169. * @var modFileHandler A reference to a modFileHandler instance
  170. */
  171. public $fileHandler;
  172. /**
  173. * @var array An array of file system resource specific options
  174. */
  175. public $options = array();
  176. /**
  177. * Constructor for modFileSystemResource
  178. *
  179. * @param modFileHandler $fh A reference to the modFileHandler object
  180. * @param string $path The path to the fs resource
  181. * @param array $options An array of specific options
  182. */
  183. function __construct(modFileHandler &$fh, $path, array $options = array()) {
  184. $this->fileHandler =& $fh;
  185. $this->path = $path;
  186. $this->options = array_merge(array(
  187. ), $options);
  188. }
  189. /**
  190. * Get the path of the fs resource.
  191. * @return string The path of the fs resource
  192. */
  193. public function getPath() {
  194. return $this->path;
  195. }
  196. /**
  197. * Validate chmod mode.
  198. *
  199. * @param $mode
  200. * @return bool
  201. */
  202. public function isValidMode($mode) {
  203. if (!preg_match('/^[0-7]{4}$/', $mode)) {
  204. return false;
  205. }
  206. return true;
  207. }
  208. /**
  209. * Chmods the resource to the specified mode.
  210. *
  211. * @param string $mode
  212. * @return boolean True if successful
  213. */
  214. public function chmod($mode) {
  215. $mode = $this->parseMode($mode);
  216. return @chmod($this->path, $mode);
  217. }
  218. /**
  219. * Sets the group permission for the fs resource
  220. * @param mixed $grp
  221. * @return boolean True if successful
  222. */
  223. public function chgrp($grp) {
  224. if ($this->isLink() && function_exists('lchgrp')) {
  225. return @lchgrp($this->path, $grp);
  226. } else {
  227. return @chgrp($this->path, $grp);
  228. }
  229. }
  230. /**
  231. * Sets the owner for the fs resource
  232. *
  233. * @param mixed $owner
  234. * @return boolean True if successful
  235. */
  236. public function chown($owner) {
  237. if ($this->isLink() && function_exists('lchown')) {
  238. return @lchown($this->path, $owner);
  239. } else {
  240. return @chown($this->path, $owner);
  241. }
  242. }
  243. /**
  244. * Check to see if the fs resource exists
  245. *
  246. * @return boolean True if exists
  247. */
  248. public function exists() {
  249. return file_exists($this->path);
  250. }
  251. /**
  252. * Check to see if the fs resource is readable
  253. *
  254. * @return boolean True if readable
  255. */
  256. public function isReadable() {
  257. return is_readable($this->path);
  258. }
  259. /**
  260. * Check to see if the fs resource is writable
  261. *
  262. * @return boolean True if writable
  263. */
  264. public function isWritable() {
  265. return is_writable($this->path);
  266. }
  267. /**
  268. * Check to see if fs resource is symlink
  269. *
  270. * @return boolean True if symlink
  271. */
  272. public function isLink() {
  273. return is_link($this->path);
  274. }
  275. /**
  276. * Gets the permission group for the fs resource
  277. *
  278. * @return string The group name of the fs resource
  279. */
  280. public function getGroup() {
  281. return filegroup($this->path);
  282. }
  283. /**
  284. * Alias for chgrp
  285. *
  286. * @see chgrp
  287. * @param string $grp
  288. * @return boolean
  289. */
  290. public function setGroup($grp) {
  291. return $this->chgrp($grp);
  292. }
  293. /**
  294. * Renames the file/folder
  295. *
  296. * @param string $newPath The new path for the fs resource
  297. * @return boolean True if successful
  298. */
  299. public function rename($newPath) {
  300. $newPath = $this->fileHandler->sanitizePath($newPath);
  301. if (!$this->isWritable()) return false;
  302. if (file_exists($newPath)) return false;
  303. return @rename($this->path, $newPath);
  304. }
  305. /**
  306. * Alias for rename
  307. *
  308. * @param string $newPath The new path to move fs resource
  309. * @return boolean True if successful
  310. */
  311. public function move($newPath) {
  312. return $this->rename($newPath);
  313. }
  314. /**
  315. * Parses a string mode into octal format
  316. *
  317. * @param string $mode The octal to parse
  318. * @return string The new mode in decimal format
  319. */
  320. protected function parseMode($mode = '') {
  321. return octdec($mode);
  322. }
  323. /**
  324. * Gets the parent containing directory of this fs resource
  325. *
  326. * @param boolean $raw Whether or not to return a modDirectory or string path
  327. * @return modDirectory|string Returns either a modDirectory object of the
  328. * parent directory, or the absolute path of the parent, depending on
  329. * whether or not $raw is set to true.
  330. */
  331. public function getParentDirectory($raw = false) {
  332. $ppath = dirname($this->path) . '/';
  333. $ppath = str_replace('//', '/', $ppath);
  334. if ($raw) return $ppath;
  335. $directory = $this->fileHandler->make($ppath,array(),'modDirectory');
  336. return $directory;
  337. }
  338. }
  339. /**
  340. * File implementation of modFileSystemResource
  341. *
  342. * @package modx
  343. */
  344. class modFile extends modFileSystemResource {
  345. /**
  346. * @var string The content of the resource
  347. */
  348. protected $content = '';
  349. /**
  350. * @see modFileSystemResource.parseMode
  351. * @param string $mode
  352. * @return string
  353. */
  354. protected function parseMode($mode = '') {
  355. if (empty($mode)) {
  356. $mode = $this->fileHandler->context->getOption('new_file_permissions', '0644', $this->fileHandler->config);
  357. }
  358. return parent::parseMode($mode);
  359. }
  360. /**
  361. * Actually create the file on the file system
  362. *
  363. * @param string $content The content of the file to write
  364. * @param string $mode The perms to write with the file
  365. * @return boolean True if successful
  366. */
  367. public function create($content = '', $mode = 'w+') {
  368. if ($this->exists()) return false;
  369. $result = false;
  370. $fp = @fopen($this->path, 'w+');
  371. if ($fp) {
  372. @fwrite($fp, $content);
  373. @fclose($fp);
  374. $result = file_exists($this->path);
  375. if ($result) {
  376. $mode = $this->parseMode();
  377. if (empty($mode)) {
  378. $mode = octdec($this->fileHandler->modx->getOption('new_file_permissions', null, '0644'));
  379. }
  380. @chmod($this->path, $mode);
  381. }
  382. }
  383. return $result;
  384. }
  385. /**
  386. * Temporarily set (but not save) the content of the file
  387. * @param string $content The content
  388. */
  389. public function setContent($content) {
  390. $this->content = $content;
  391. }
  392. /**
  393. * Get the contents of the file
  394. *
  395. * @return string The contents of the file
  396. */
  397. public function getContents() {
  398. $content = @file_get_contents($this->path);
  399. if ($content === false) {
  400. $content = $this->content;
  401. }
  402. return $content;
  403. }
  404. /**
  405. * Alias for save()
  406. *
  407. * @see modDirectory::write
  408. * @param string $content
  409. * @param string $mode
  410. * @return boolean
  411. */
  412. public function write($content = null, $mode = 'w+') {
  413. return $this->save($content, $mode);
  414. }
  415. /**
  416. * Writes the content of the modFile object to the actual file.
  417. *
  418. * @param string $content Optional. If not using setContent, this will set
  419. * the content to write.
  420. * @param string $mode The mode in which to write
  421. * @return boolean The result of the fwrite
  422. */
  423. public function save($content = null, $mode = 'w+') {
  424. if ($content !== null) $this->content = $content;
  425. $result = false;
  426. $fp = @fopen($this->path, $mode);
  427. if ($fp) {
  428. $result = @fwrite($fp, $this->content);
  429. @fclose($fp);
  430. }
  431. return $result;
  432. }
  433. /**
  434. * Unpack a zip archive to a specified location.
  435. *
  436. * @uses compression.xPDOZip OR compression.PclZip
  437. *
  438. * @param string $this->getPath() An absolute file system location to a valid zip archive.
  439. * @param string $to A file system location to extract the contents of the archive to.
  440. * @param array $options an array of optional options, primarily for the xPDOZip class
  441. * @return array|string|boolean An array of unpacked files, a string in case of cli functions or false on failure.
  442. */
  443. public function unpack($to = '', $options = array()) {
  444. $results = false;
  445. /** @var xPDOZip $archive */
  446. $archive = $this->fileHandler->modx->getService('archive', 'compression.xPDOZip', XPDO_CORE_PATH, $this->path);
  447. if ($archive) {
  448. $results = $archive->unpack($to, $options);
  449. }
  450. return $results;
  451. }
  452. /**
  453. * Gets the size of the file
  454. *
  455. * @return int The size of the file, in bytes
  456. */
  457. public function getSize() {
  458. $size = @filesize($this->path);
  459. if ($size === false) {
  460. if ( function_exists('mb_strlen') ) {
  461. $size = mb_strlen($this->content, '8bit');
  462. } else {
  463. $size = strlen($this->content);
  464. }
  465. }
  466. return $size;
  467. }
  468. /**
  469. * Gets the last accessed time of the file
  470. *
  471. * @param string $timeFormat The format, in strftime format, of the time
  472. * @return string The formatted time
  473. */
  474. public function getLastAccessed($timeFormat = '%b %d, %Y %I:%M:%S %p') {
  475. return strftime($timeFormat, fileatime($this->path));
  476. }
  477. /**
  478. * Gets the last modified time of the file
  479. *
  480. * @param string $timeFormat The format, in strftime format, of the time
  481. * @return string The formatted time
  482. */
  483. public function getLastModified($timeFormat = '%b %d, %Y %I:%M:%S %p') {
  484. return strftime($timeFormat, filemtime($this->path));
  485. }
  486. /**
  487. * Gets the file extension of the file
  488. *
  489. * @return string The file extension of the file
  490. */
  491. public function getExtension() {
  492. return pathinfo($this->path, PATHINFO_EXTENSION);
  493. }
  494. /**
  495. * Gets the basename, or only the filename without the path, of the file
  496. *
  497. * @return string The basename of the file
  498. */
  499. public function getBaseName() {
  500. return ltrim(strrchr($this->path, '/'), '/');
  501. }
  502. /**
  503. * Sends the file as a download
  504. *
  505. * @param array $options Optional configuration options like mimetype and filename
  506. *
  507. * @return downloadable file
  508. */
  509. public function download($options = array()) {
  510. $options = array_merge(array(
  511. 'mimetype' => 'application/octet-stream',
  512. 'filename' => '"' . $this->getBasename() . '"',
  513. ), $options);
  514. $output = $this->getContents();
  515. header('Content-type: ' . $options['mimetype']);
  516. header('Content-Disposition: attachment; filename=' . $options['filename']);
  517. header('Content-Length: ' . $this->getSize());
  518. echo $output;
  519. die();
  520. }
  521. /**
  522. * Deletes the file from the filesystem
  523. *
  524. * @return boolean True if successful
  525. */
  526. public function remove() {
  527. if (!$this->exists()) return false;
  528. return @unlink($this->path);
  529. }
  530. }
  531. /**
  532. * Representation of a directory
  533. *
  534. * @package modx
  535. */
  536. class modDirectory extends modFileSystemResource {
  537. /**
  538. * Actually creates the new directory on the file system.
  539. *
  540. * @param string $mode Optional. The permissions of the new directory.
  541. * @return boolean True if successful
  542. */
  543. public function create($mode = '') {
  544. $mode = $this->parseMode($mode);
  545. if (empty($mode)) {
  546. $mode = octdec($this->fileHandler->modx->getOption('new_folder_permissions',null,'0775'));
  547. }
  548. if ($this->exists()) return false;
  549. return $this->fileHandler->modx->cacheManager->writeTree($this->path,array(
  550. 'new_folder_permissions' => $mode,
  551. ));
  552. }
  553. /**
  554. * @see modFileSystemResource::parseMode
  555. *
  556. * @param string $mode
  557. * @return boolean
  558. */
  559. protected function parseMode($mode = '') {
  560. if (empty($mode)) {
  561. $mode = $this->fileHandler->context->getOption('new_folder_permissions', '0755', $this->fileHandler->config);
  562. }
  563. return parent::parseMode($mode);
  564. }
  565. /**
  566. * Removes the directory from the file system, recursively removing
  567. * subdirectories and files.
  568. *
  569. * @param array $options Options for removal.
  570. * @return boolean True if successful
  571. */
  572. public function remove($options = array()) {
  573. if ($this->path == '/') return false;
  574. $options = array_merge(array(
  575. 'deleteTop' => true,
  576. 'skipDirs' => false,
  577. 'extensions' => array(),
  578. ), $options);
  579. $this->fileHandler->modx->getCacheManager();
  580. return $this->fileHandler->modx->cacheManager->deleteTree($this->path, $options);
  581. }
  582. /**
  583. * Iterates over a modDirectory object and returns an array of all containing files and optionally directories,
  584. * can run recursive, filter by file extension(s) or filenames and sort the resulting list with the specified sort options
  585. * an anonymous callback function can be passed to modify the output on the fly, by default an array of paths is returned
  586. *
  587. * @param array $options Options for iterating the directory.
  588. * @option boolean recursive If subdirectories should be scanned as well
  589. * @option boolean sort If the resulting array should be sorted
  590. * @option string sortdir What sort order should be applied: SORT_ASC|SORT_DESC
  591. * @optoin string sortflag What sort flag should be applied: SORT_REGULAR, SORT_NATURAL, SORT_NUMERIC etc
  592. * @option boolean skiphidden If hidden directories and files should be ignored, defaults to true
  593. * @option boolean skipdirs If directories should be skipped in the resulting array, defaults to true
  594. * @option string|array skip Comma separated list or array of filenames (including extension) that should be ignored
  595. * @option string|array extensions Comma separated list or array of file extensions to filter files by
  596. * @option boolean|function callback Anonymous function to modify each output item, $item will be passed as argument
  597. *
  598. * @return array
  599. */
  600. public function getList($options = array()) {
  601. $options = array_merge(array(
  602. 'recursive' => false,
  603. 'sort' => false,
  604. 'sortdir' => SORT_ASC,
  605. 'sortflag' => SORT_REGULAR,
  606. 'skiphidden' => true,
  607. 'skipdirs' => true,
  608. 'skip' => array(),
  609. 'extensions' => array(),
  610. 'callback' => false,
  611. ), $options);
  612. $items = array();
  613. $mb = $this->fileHandler->modx->getOption('use_multibyte', null, false);
  614. $mbencoding = $this->fileHandler->modx->getOption('modx_charset', null, 'UTF-8');
  615. $extensions = !is_array($options['extensions']) ? explode(',', $options['extensions']) : $options['extensions'];
  616. $skip = !is_array($options['skip']) ? explode(',', $options['skip']) : $options['skip'];
  617. $iterator = $options['recursive'] ? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->path, FilesystemIterator::CURRENT_AS_SELF)) : new DirectoryIterator($this->path);
  618. foreach ($iterator as $item) {
  619. $skipfile = !empty($skip) ? in_array($item->getFilename(), $skip) : false;
  620. $ishidden = false;
  621. if ($options['skiphidden']) {
  622. // check for hidden folder, also hide with visible ones inside
  623. // but don't skip weird filenames like "...and-there-was-silence.avi"
  624. if ($item->isDot() || preg_match('/(\/\.\w+|\\\.\w+)/', $item->getPath())) {
  625. continue;
  626. }
  627. // check for hidden file (probably works only on UNIX filesystems)
  628. $ishidden = preg_match('/^(\.\w+)/i', $item->getFilename());
  629. } else if (!$options['skipdirs']) {
  630. // always exclude . and .. directory navigators, only relevant when including folders
  631. $ishidden = $item->isDot();
  632. }
  633. if (($item->isFile() || $item->isDir() && !$options['skipdirs']) && !$ishidden && !$skipfile) {
  634. $additem = true;
  635. if (!empty($options['extensions'])) {
  636. // if min PHP version is 5.3.6 we can use $item->getExtension()
  637. $extension = pathinfo($item->getPathname(), PATHINFO_EXTENSION);
  638. $extension = $mb ? mb_strtolower($extension, $mbencoding) : strtolower($extension);
  639. if (!in_array($extension, $extensions)) {
  640. $additem = false;
  641. }
  642. }
  643. if (!$additem) {
  644. continue;
  645. } else if (is_callable($options['callback'])) {
  646. $callback = call_user_func($options['callback'], $item);
  647. if (!empty($callback)) {
  648. $items[] = $callback;
  649. }
  650. } else {
  651. $items[] = $item->isDir() ? $item->getPathname() . DIRECTORY_SEPARATOR : $item->getPathname();
  652. }
  653. }
  654. }
  655. if (!empty($options['sort'])) {
  656. array_multisort($items, $options['sortdir'], $options['sortflag'], $items);
  657. }
  658. return $items;
  659. }
  660. }