PageRenderTime 60ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/cake/libs/folder.php

https://bitbucket.org/meLego/snelcms
PHP | 749 lines | 588 code | 27 blank | 134 comment | 25 complexity | ce4ebe18ddc5430158eb26dd9a18d689 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * Convenience class for handling directories.
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package cake.libs
  16. * @since CakePHP(tm) v 0.2.9
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. /**
  20. * Folder structure browser, lists folders and files.
  21. * Provides an Object interface for Common directory related tasks.
  22. *
  23. * @package cake.libs
  24. */
  25. class Folder {
  26. /**
  27. * Path to Folder.
  28. *
  29. * @var string
  30. * @access public
  31. */
  32. public $path = null;
  33. /**
  34. * Sortedness. Whether or not list results
  35. * should be sorted by name.
  36. *
  37. * @var boolean
  38. * @access public
  39. */
  40. public $sort = false;
  41. /**
  42. * Mode to be used on create. Does nothing on windows platforms.
  43. *
  44. * @var integer
  45. * @access public
  46. */
  47. public $mode = 0755;
  48. /**
  49. * Holds messages from last method.
  50. *
  51. * @var array
  52. * @access private
  53. */
  54. private $__messages = array();
  55. /**
  56. * Holds errors from last method.
  57. *
  58. * @var array
  59. * @access private
  60. */
  61. private $__errors = array();
  62. /**
  63. * Holds array of complete directory paths.
  64. *
  65. * @var array
  66. * @access private
  67. */
  68. private $__directories;
  69. /**
  70. * Holds array of complete file paths.
  71. *
  72. * @var array
  73. * @access private
  74. */
  75. private $__files;
  76. /**
  77. * Constructor.
  78. *
  79. * @param string $path Path to folder
  80. * @param boolean $create Create folder if not found
  81. * @param mixed $mode Mode (CHMOD) to apply to created folder, false to ignore
  82. */
  83. function __construct($path = false, $create = false, $mode = false) {
  84. if (empty($path)) {
  85. $path = TMP;
  86. }
  87. if ($mode) {
  88. $this->mode = $mode;
  89. }
  90. if (!file_exists($path) && $create === true) {
  91. $this->create($path, $this->mode);
  92. }
  93. if (!Folder::isAbsolute($path)) {
  94. $path = realpath($path);
  95. }
  96. if (!empty($path)) {
  97. $this->cd($path);
  98. }
  99. }
  100. /**
  101. * Return current path.
  102. *
  103. * @return string Current path
  104. */
  105. public function pwd() {
  106. return $this->path;
  107. }
  108. /**
  109. * Change directory to $path.
  110. *
  111. * @param string $path Path to the directory to change to
  112. * @return string The new path. Returns false on failure
  113. */
  114. public function cd($path) {
  115. $path = $this->realpath($path);
  116. if (is_dir($path)) {
  117. return $this->path = $path;
  118. }
  119. return false;
  120. }
  121. /**
  122. * Returns an array of the contents of the current directory.
  123. * The returned array holds two arrays: One of directories and one of files.
  124. *
  125. * @param boolean $sort Whether you want the results sorted, set this and the sort property
  126. * to false to get unsorted results.
  127. * @param mixed $exceptions Either an array or boolean true will not grab dot files
  128. * @param boolean $fullPath True returns the full path
  129. * @return mixed Contents of current directory as an array, an empty array on failure
  130. */
  131. public function read($sort = true, $exceptions = false, $fullPath = false) {
  132. $dirs = $files = array();
  133. if (!$this->pwd()) {
  134. return array($dirs, $files);
  135. }
  136. if (is_array($exceptions)) {
  137. $exceptions = array_flip($exceptions);
  138. }
  139. $skipHidden = isset($exceptions['.']) || $exceptions === true;
  140. if (false === ($dir = @opendir($this->path))) {
  141. return array($dirs, $files);
  142. }
  143. while (false !== ($item = readdir($dir))) {
  144. if ($item === '.' || $item === '..' || ($skipHidden && $item[0] === '.') || isset($exceptions[$item])) {
  145. continue;
  146. }
  147. $path = Folder::addPathElement($this->path, $item);
  148. if (is_dir($path)) {
  149. $dirs[] = $fullPath ? $path : $item;
  150. } else {
  151. $files[] = $fullPath ? $path : $item;
  152. }
  153. }
  154. if ($sort || $this->sort) {
  155. sort($dirs);
  156. sort($files);
  157. }
  158. closedir($dir);
  159. return array($dirs, $files);
  160. }
  161. /**
  162. * Returns an array of all matching files in current directory.
  163. *
  164. * @param string $pattern Preg_match pattern (Defaults to: .*)
  165. * @param boolean $sort Whether results should be sorted.
  166. * @return array Files that match given pattern
  167. */
  168. public function find($regexpPattern = '.*', $sort = false) {
  169. list($dirs, $files) = $this->read($sort);
  170. return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files)); ;
  171. }
  172. /**
  173. * Returns an array of all matching files in and below current directory.
  174. *
  175. * @param string $pattern Preg_match pattern (Defaults to: .*)
  176. * @param boolean $sort Whether results should be sorted.
  177. * @return array Files matching $pattern
  178. */
  179. public function findRecursive($pattern = '.*', $sort = false) {
  180. if (!$this->pwd()) {
  181. return array();
  182. }
  183. $startsOn = $this->path;
  184. $out = $this->_findRecursive($pattern, $sort);
  185. $this->cd($startsOn);
  186. return $out;
  187. }
  188. /**
  189. * Private helper function for findRecursive.
  190. *
  191. * @param string $pattern Pattern to match against
  192. * @param boolean $sort Whether results should be sorted.
  193. * @return array Files matching pattern
  194. * @access private
  195. */
  196. function _findRecursive($pattern, $sort = false) {
  197. list($dirs, $files) = $this->read($sort);
  198. $found = array();
  199. foreach ($files as $file) {
  200. if (preg_match('/^' . $pattern . '$/i', $file)) {
  201. $found[] = Folder::addPathElement($this->path, $file);
  202. }
  203. }
  204. $start = $this->path;
  205. foreach ($dirs as $dir) {
  206. $this->cd(Folder::addPathElement($start, $dir));
  207. $found = array_merge($found, $this->findRecursive($pattern, $sort));
  208. }
  209. return $found;
  210. }
  211. /**
  212. * Returns true if given $path is a Windows path.
  213. *
  214. * @param string $path Path to check
  215. * @return boolean true if windows path, false otherwise
  216. */
  217. public static function isWindowsPath($path) {
  218. return (preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) == '\\\\');
  219. }
  220. /**
  221. * Returns true if given $path is an absolute path.
  222. *
  223. * @param string $path Path to check
  224. * @return bool true if path is absolute.
  225. */
  226. public static function isAbsolute($path) {
  227. return !empty($path) && ($path[0] === '/' || preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) == '\\\\');
  228. }
  229. /**
  230. * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
  231. *
  232. * @param string $path Path to check
  233. * @return string Set of slashes ("\\" or "/")
  234. */
  235. public static function normalizePath($path) {
  236. return Folder::correctSlashFor($path);
  237. }
  238. /**
  239. * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
  240. *
  241. * @param string $path Path to check
  242. * @return string Set of slashes ("\\" or "/")
  243. */
  244. public static function correctSlashFor($path) {
  245. return (Folder::isWindowsPath($path)) ? '\\' : '/';
  246. }
  247. /**
  248. * Returns $path with added terminating slash (corrected for Windows or other OS).
  249. *
  250. * @param string $path Path to check
  251. * @return string Path with ending slash
  252. */
  253. public static function slashTerm($path) {
  254. if (Folder::isSlashTerm($path)) {
  255. return $path;
  256. }
  257. return $path . Folder::correctSlashFor($path);
  258. }
  259. /**
  260. * Returns $path with $element added, with correct slash in-between.
  261. *
  262. * @param string $path Path
  263. * @param string $element Element to and at end of path
  264. * @return string Combined path
  265. */
  266. public static function addPathElement($path, $element) {
  267. return rtrim($path, DS) . DS . $element;
  268. }
  269. /**
  270. * Returns true if the File is in a given CakePath.
  271. *
  272. * @param string $path The path to check.
  273. * @return bool
  274. */
  275. public function inCakePath($path = '') {
  276. $dir = substr(Folder::slashTerm(ROOT), 0, -1);
  277. $newdir = $dir . $path;
  278. return $this->inPath($newdir);
  279. }
  280. /**
  281. * Returns true if the File is in given path.
  282. *
  283. * @param string $path The path to check that the current pwd() resides with in.
  284. * @param boolean $reverse
  285. * @return bool
  286. */
  287. public function inPath($path = '', $reverse = false) {
  288. $dir = Folder::slashTerm($path);
  289. $current = Folder::slashTerm($this->pwd());
  290. if (!$reverse) {
  291. $return = preg_match('/^(.*)' . preg_quote($dir, '/') . '(.*)/', $current);
  292. } else {
  293. $return = preg_match('/^(.*)' . preg_quote($current, '/') . '(.*)/', $dir);
  294. }
  295. return (bool)$return;
  296. }
  297. /**
  298. * Change the mode on a directory structure recursively. This includes changing the mode on files as well.
  299. *
  300. * @param string $path The path to chmod
  301. * @param integer $mode octal value 0755
  302. * @param boolean $recursive chmod recursively, set to false to only change the current directory.
  303. * @param array $exceptions array of files, directories to skip
  304. * @return boolean Returns TRUE on success, FALSE on failure
  305. */
  306. public function chmod($path, $mode = false, $recursive = true, $exceptions = array()) {
  307. if (!$mode) {
  308. $mode = $this->mode;
  309. }
  310. if ($recursive === false && is_dir($path)) {
  311. if (@chmod($path, intval($mode, 8))) {
  312. $this->__messages[] = __('%s changed to %s', $path, $mode);
  313. return true;
  314. }
  315. $this->__errors[] = __('%s NOT changed to %s', $path, $mode);
  316. return false;
  317. }
  318. if (is_dir($path)) {
  319. $paths = $this->tree($path);
  320. foreach ($paths as $type) {
  321. foreach ($type as $key => $fullpath) {
  322. $check = explode(DS, $fullpath);
  323. $count = count($check);
  324. if (in_array($check[$count - 1], $exceptions)) {
  325. continue;
  326. }
  327. if (@chmod($fullpath, intval($mode, 8))) {
  328. $this->__messages[] = __('%s changed to %s', $fullpath, $mode);
  329. } else {
  330. $this->__errors[] = __('%s NOT changed to %s', $fullpath, $mode);
  331. }
  332. }
  333. }
  334. if (empty($this->__errors)) {
  335. return true;
  336. }
  337. }
  338. return false;
  339. }
  340. /**
  341. * Returns an array of nested directories and files in each directory
  342. *
  343. * @param string $path the directory path to build the tree from
  344. * @param mixed $exceptions Array of files to exclude, defaults to excluding hidden files.
  345. * @param string $type either file or dir. null returns both files and directories
  346. * @return mixed array of nested directories and files in each directory
  347. */
  348. public function tree($path, $exceptions = true, $type = null) {
  349. $original = $this->path;
  350. $path = rtrim($path, DS);
  351. if (!$this->cd($path)) {
  352. if ($type === null) {
  353. return array(array(), array());
  354. }
  355. return array();
  356. }
  357. $this->__files = array();
  358. $this->__directories = array($this->realpath($path));
  359. $directories = array();
  360. if ($exceptions === false) {
  361. $exceptions = true;
  362. }
  363. while (!empty($this->__directories)) {
  364. $dir = array_pop($this->__directories);
  365. $this->__tree($dir, $exceptions);
  366. $directories[] = $dir;
  367. }
  368. if ($type === null) {
  369. return array($directories, $this->__files);
  370. }
  371. if ($type === 'dir') {
  372. return $directories;
  373. }
  374. $this->cd($original);
  375. return $this->__files;
  376. }
  377. /**
  378. * Private method to list directories and files in each directory
  379. *
  380. * @param string $path The Path to read.
  381. * @param mixed $exceptions Array of files to exclude from the read that will be performed.
  382. * @access private
  383. */
  384. function __tree($path, $exceptions) {
  385. $this->path = $path;
  386. list($dirs, $files) = $this->read(false, $exceptions, true);
  387. $this->__directories = array_merge($this->__directories, $dirs);
  388. $this->__files = array_merge($this->__files, $files);
  389. }
  390. /**
  391. * Create a directory structure recursively. Can be used to create
  392. * deep path structures like `/foo/bar/baz/shoe/horn`
  393. *
  394. * @param string $pathname The directory structure to create
  395. * @param integer $mode octal value 0755
  396. * @return boolean Returns TRUE on success, FALSE on failure
  397. */
  398. public function create($pathname, $mode = false) {
  399. if (is_dir($pathname) || empty($pathname)) {
  400. return true;
  401. }
  402. if (!$mode) {
  403. $mode = $this->mode;
  404. }
  405. if (is_file($pathname)) {
  406. $this->__errors[] = __('%s is a file', $pathname);
  407. return false;
  408. }
  409. $pathname = rtrim($pathname, DS);
  410. $nextPathname = substr($pathname, 0, strrpos($pathname, DS));
  411. if ($this->create($nextPathname, $mode)) {
  412. if (!file_exists($pathname)) {
  413. $old = umask(0);
  414. if (mkdir($pathname, $mode)) {
  415. umask($old);
  416. $this->__messages[] = __('%s created', $pathname);
  417. return true;
  418. } else {
  419. umask($old);
  420. $this->__errors[] = __('%s NOT created', $pathname);
  421. return false;
  422. }
  423. }
  424. }
  425. return false;
  426. }
  427. /**
  428. * Returns the size in bytes of this Folder and its contents.
  429. *
  430. * @param string $directory Path to directory
  431. * @return int size in bytes of current folder
  432. */
  433. public function dirsize() {
  434. $size = 0;
  435. $directory = Folder::slashTerm($this->path);
  436. $stack = array($directory);
  437. $count = count($stack);
  438. for ($i = 0, $j = $count; $i < $j; ++$i) {
  439. if (is_file($stack[$i])) {
  440. $size += filesize($stack[$i]);
  441. } elseif (is_dir($stack[$i])) {
  442. $dir = dir($stack[$i]);
  443. if ($dir) {
  444. while (false !== ($entry = $dir->read())) {
  445. if ($entry === '.' || $entry === '..') {
  446. continue;
  447. }
  448. $add = $stack[$i] . $entry;
  449. if (is_dir($stack[$i] . $entry)) {
  450. $add = Folder::slashTerm($add);
  451. }
  452. $stack[] = $add;
  453. }
  454. $dir->close();
  455. }
  456. }
  457. $j = count($stack);
  458. }
  459. return $size;
  460. }
  461. /**
  462. * Recursively Remove directories if the system allows.
  463. *
  464. * @param string $path Path of directory to delete
  465. * @return boolean Success
  466. */
  467. public function delete($path = null) {
  468. if (!$path) {
  469. $path = $this->pwd();
  470. }
  471. if (!$path) {
  472. return null;
  473. }
  474. $path = Folder::slashTerm($path);
  475. if (is_dir($path) === true) {
  476. $normalFiles = glob($path . '*');
  477. $hiddenFiles = glob($path . '\.?*');
  478. $normalFiles = $normalFiles ? $normalFiles : array();
  479. $hiddenFiles = $hiddenFiles ? $hiddenFiles : array();
  480. $files = array_merge($normalFiles, $hiddenFiles);
  481. if (is_array($files)) {
  482. foreach ($files as $file) {
  483. if (preg_match('/(\.|\.\.)$/', $file)) {
  484. continue;
  485. }
  486. if (is_file($file) === true) {
  487. if (@unlink($file)) {
  488. $this->__messages[] = __('%s removed', $file);
  489. } else {
  490. $this->__errors[] = __('%s NOT removed', $file);
  491. }
  492. } elseif (is_dir($file) === true && $this->delete($file) === false) {
  493. return false;
  494. }
  495. }
  496. }
  497. $path = substr($path, 0, strlen($path) - 1);
  498. if (rmdir($path) === false) {
  499. $this->__errors[] = __('%s NOT removed', $path);
  500. return false;
  501. } else {
  502. $this->__messages[] = __('%s removed', $path);
  503. }
  504. }
  505. return true;
  506. }
  507. /**
  508. * Recursive directory copy.
  509. *
  510. * ### Options
  511. *
  512. * - `to` The directory to copy to.
  513. * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
  514. * - `chmod` The mode to copy the files/directories with.
  515. * - `skip` Files/directories to skip.
  516. *
  517. * @param mixed $options Either an array of options (see above) or a string of the destination directory.
  518. * @return bool Success
  519. */
  520. public function copy($options = array()) {
  521. if (!$this->pwd()) {
  522. return false;
  523. }
  524. $to = null;
  525. if (is_string($options)) {
  526. $to = $options;
  527. $options = array();
  528. }
  529. $options = array_merge(array('to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => array()), $options);
  530. $fromDir = $options['from'];
  531. $toDir = $options['to'];
  532. $mode = $options['mode'];
  533. if (!$this->cd($fromDir)) {
  534. $this->__errors[] = __('%s not found', $fromDir);
  535. return false;
  536. }
  537. if (!is_dir($toDir)) {
  538. $this->create($toDir, $mode);
  539. }
  540. if (!is_writable($toDir)) {
  541. $this->__errors[] = __('%s not writable', $toDir);
  542. return false;
  543. }
  544. $exceptions = array_merge(array('.', '..', '.svn'), $options['skip']);
  545. if ($handle = @opendir($fromDir)) {
  546. while (false !== ($item = readdir($handle))) {
  547. if (!in_array($item, $exceptions)) {
  548. $from = Folder::addPathElement($fromDir, $item);
  549. $to = Folder::addPathElement($toDir, $item);
  550. if (is_file($from)) {
  551. if (copy($from, $to)) {
  552. chmod($to, intval($mode, 8));
  553. touch($to, filemtime($from));
  554. $this->__messages[] = __('%s copied to %s', $from, $to);
  555. } else {
  556. $this->__errors[] = __('%s NOT copied to %s', $from, $to);
  557. }
  558. }
  559. if (is_dir($from) && !file_exists($to)) {
  560. $old = umask(0);
  561. if (mkdir($to, $mode)) {
  562. umask($old);
  563. $old = umask(0);
  564. chmod($to, $mode);
  565. umask($old);
  566. $this->__messages[] = __('%s created', $to);
  567. $options = array_merge($options, array('to'=> $to, 'from'=> $from));
  568. $this->copy($options);
  569. } else {
  570. $this->__errors[] = __('%s not created', $to);
  571. }
  572. }
  573. }
  574. }
  575. closedir($handle);
  576. } else {
  577. return false;
  578. }
  579. if (!empty($this->__errors)) {
  580. return false;
  581. }
  582. return true;
  583. }
  584. /**
  585. * Recursive directory move.
  586. *
  587. * ### Options
  588. *
  589. * - `to` The directory to copy to.
  590. * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
  591. * - `chmod` The mode to copy the files/directories with.
  592. * - `skip` Files/directories to skip.
  593. *
  594. * @param array $options (to, from, chmod, skip)
  595. * @return boolean Success
  596. */
  597. public function move($options) {
  598. $to = null;
  599. if (is_string($options)) {
  600. $to = $options;
  601. $options = (array)$options;
  602. }
  603. $options = array_merge(
  604. array('to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => array()),
  605. $options
  606. );
  607. if ($this->copy($options)) {
  608. if ($this->delete($options['from'])) {
  609. return (bool)$this->cd($options['to']);
  610. }
  611. }
  612. return false;
  613. }
  614. /**
  615. * get messages from latest method
  616. *
  617. * @return array
  618. */
  619. public function messages() {
  620. return $this->__messages;
  621. }
  622. /**
  623. * get error from latest method
  624. *
  625. * @return array
  626. */
  627. public function errors() {
  628. return $this->__errors;
  629. }
  630. /**
  631. * Get the real path (taking ".." and such into account)
  632. *
  633. * @param string $path Path to resolve
  634. * @return string The resolved path
  635. */
  636. function realpath($path) {
  637. $path = str_replace('/', DS, trim($path));
  638. if (strpos($path, '..') === false) {
  639. if (!Folder::isAbsolute($path)) {
  640. $path = Folder::addPathElement($this->path, $path);
  641. }
  642. return $path;
  643. }
  644. $parts = explode(DS, $path);
  645. $newparts = array();
  646. $newpath = '';
  647. if ($path[0] === DS) {
  648. $newpath = DS;
  649. }
  650. while (($part = array_shift($parts)) !== NULL) {
  651. if ($part === '.' || $part === '') {
  652. continue;
  653. }
  654. if ($part === '..') {
  655. if (!empty($newparts)) {
  656. array_pop($newparts);
  657. continue;
  658. } else {
  659. return false;
  660. }
  661. }
  662. $newparts[] = $part;
  663. }
  664. $newpath .= implode(DS, $newparts);
  665. return Folder::slashTerm($newpath);
  666. }
  667. /**
  668. * Returns true if given $path ends in a slash (i.e. is slash-terminated).
  669. *
  670. * @param string $path Path to check
  671. * @return boolean true if path ends with slash, false otherwise
  672. */
  673. public static function isSlashTerm($path) {
  674. $lastChar = $path[strlen($path) - 1];
  675. return $lastChar === '/' || $lastChar === '\\';
  676. }
  677. }