1: <?php
2: /**
3: * Folder engine For XOOPS
4: *
5: * You may not change or alter any portion of this comment or credits
6: * of supporting developers from this source code or any supporting source code
7: * which is considered copyrighted (c) material of the original comment or credit authors.
8: * This program is distributed in the hope that it will be useful,
9: * but WITHOUT ANY WARRANTY; without even the implied warranty of
10: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11: *
12: * @copyright (c) 2005-2016 XOOPS Project (www.xoops.org)
13: * @license GNU GPL 2 (http://www.gnu.org/licenses/gpl-2.0.html)
14: * @package class
15: * @subpackage file
16: * @since 2.3.0
17: * @author Taiwen Jiang <phppp@users.sourceforge.net>
18: */
19:
20: /**
21: * Convenience class for handling directories.
22: *
23: * PHP versions 4 and 5
24: *
25: * CakePHP(tm) : Rapid Development Framework <http://www.cakephp.org/>
26: * Copyright 2005-2008, Cake Software Foundation, Inc.
27: * 1785 E. Sahara Avenue, Suite 490-204
28: * Las Vegas, Nevada 89104
29: *
30: * Licensed under The MIT License
31: * Redistributions of files must retain the above copyright notice.
32: *
33: * @filesource
34: * @copyright Copyright 2005-2008, Cake Software Foundation, Inc.
35: * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
36: * @package cake
37: * @subpackage cake.cake.libs
38: * @since CakePHP(tm) v 0.2.9
39: * @modifiedby $LastChangedBy: beckmi $
40: * @lastmodified $Date: 2015-06-06 17:59:41 -0400 (Sat, 06 Jun 2015) $
41: * @license http://www.opensource.org/licenses/mit-license.php The MIT License
42: */
43:
44: /**
45: * Folder structure browser, lists folders and files.
46: *
47: * Long description for class
48: *
49: * @package cake
50: * @subpackage cake.cake.libs
51: */
52: class XoopsFolderHandler
53: {
54: /**
55: * Path to Folder.
56: *
57: * @var string
58: * @access public
59: */
60: public $path;
61:
62: /**
63: * Sortedness.
64: *
65: * @var boolean
66: * @access public
67: */
68: public $sort = false;
69:
70: /**
71: * mode to be used on create.
72: *
73: * @var boolean
74: * @access public
75: */
76: public $mode = '0755';
77:
78: /**
79: * holds messages from last method.
80: *
81: * @var array
82: * @access private
83: */
84: public $messages = array();
85:
86: /**
87: * holds errors from last method.
88: *
89: * @var array
90: * @access private
91: */
92: public $errors = false;
93:
94: /**
95: * holds array of complete directory paths.
96: *
97: * @var array
98: * @access private
99: */
100: public $directories;
101:
102: /**
103: * holds array of complete file paths.
104: *
105: * @var array
106: * @access private
107: */
108: public $files;
109:
110: /**
111: * Constructor.
112: *
113: * @param bool|string $path Path to folder
114: * @param boolean $create Create folder if not found
115: * @param mixed $mode Mode (CHMOD) to apply to created folder, false to ignore
116: */
117: public function __construct($path = false, $create = true, $mode = false)
118: {
119: if (empty($path)) {
120: $path = XOOPS_VAR_PATH . '/caches/xoops_cache';
121: }
122: if ($mode) {
123: $this->mode = intval($mode, 8);
124: }
125: if (!file_exists($path) && $create == true) {
126: $this->create($path, $this->mode);
127: }
128: if (!$this->isAbsolute($path)) {
129: $path = realpath($path);
130: }
131: $this->cd($path);
132: }
133:
134: /**
135: * Return current path.
136: *
137: * @return string Current path
138: * @access public
139: */
140: public function pwd()
141: {
142: return $this->path;
143: }
144:
145: /**
146: * Change directory to $desired_path.
147: *
148: * @param string $path Path to the directory to change to
149: *
150: * @return string The new path. Returns false on failure
151: * @access public
152: */
153: public function cd($path)
154: {
155: $path = $this->realpath($path);
156: if (is_dir($path) && file_exists($path)) {
157: return $this->path = $path;
158: }
159:
160: return false;
161: }
162:
163: /**
164: * Returns an array of the contents of the current directory, or false on failure.
165: * The returned array holds two arrays: one of dirs and one of files.
166: *
167: * @param boolean $sort
168: * @param mixed $exceptions either an array or boolean true will no grab dot files
169: *
170: * @return mixed Contents of current directory as an array, false on failure
171: * @access public
172: */
173: public function read($sort = true, $exceptions = false)
174: {
175: $dirs = $files = array();
176: $dir = opendir($this->path);
177: if ($dir !== false) {
178: while (false !== ($n = readdir($dir))) {
179: $item = false;
180: if (is_array($exceptions)) {
181: if (!in_array($n, $exceptions)) {
182: $item = $n;
183: }
184: } else {
185: if ((!preg_match('/^\\.+$/', $n) && $exceptions === false) || ($exceptions === true && !preg_match('/^\\.(.*)$/', $n))) {
186: $item = $n;
187: }
188: }
189: if ($item !== false) {
190: if (is_dir($this->addPathElement($this->path, $item))) {
191: $dirs[] = $item;
192: } else {
193: $files[] = $item;
194: }
195: }
196: }
197: if ($sort || $this->sort) {
198: sort($dirs);
199: sort($files);
200: }
201: closedir($dir);
202: }
203:
204: return array(
205: $dirs,
206: $files);
207: }
208:
209: /**
210: * Returns an array of all matching files in current directory.
211: *
212: * @param string $regexp_pattern Preg_match pattern (Defaults to: .*)
213: * @param bool $sort
214: *
215: * @return array Files that match given pattern
216: * @access public
217: */
218: public function find($regexp_pattern = '.*', $sort = false)
219: {
220: $data = $this->read($sort);
221: if (!is_array($data)) {
222: return array();
223: }
224: list($dirs, $files) = $data;
225: $found = array();
226: foreach ($files as $file) {
227: if (preg_match("/^{$regexp_pattern}$/i", $file)) {
228: $found[] = $file;
229: }
230: }
231:
232: return $found;
233: }
234:
235: /**
236: * Returns an array of all matching files in and below current directory.
237: *
238: * @param string $pattern Preg_match pattern (Defaults to: .*)
239: * @param bool $sort
240: *
241: * @return array Files matching $pattern
242: * @access public
243: */
244: public function findRecursive($pattern = '.*', $sort = false)
245: {
246: $startsOn = $this->path;
247: $out = $this->_findRecursive($pattern, $sort);
248: $this->cd($startsOn);
249:
250: return $out;
251: }
252:
253: /**
254: * Private helper function for findRecursive.
255: *
256: * @param string $pattern Pattern to match against
257: * @param bool $sort
258: *
259: * @return array Files matching pattern
260: * @access private
261: */
262: public function _findRecursive($pattern, $sort = false)
263: {
264: list($dirs, $files) = $this->read($sort);
265: $found = array();
266: foreach ($files as $file) {
267: if (preg_match("/^{$pattern}$/i", $file)) {
268: $found[] = $this->addPathElement($this->path, $file);
269: }
270: }
271: $start = $this->path;
272: foreach ($dirs as $dir) {
273: $this->cd($this->addPathElement($start, $dir));
274: $found = array_merge($found, $this->findRecursive($pattern));
275: }
276:
277: return $found;
278: }
279:
280: /**
281: * Returns true if given $path is a Windows path.
282: *
283: * @param string $path Path to check
284: *
285: * @return boolean true if windows path, false otherwise
286: * @access public
287: * @static
288: */
289: public function isWindowsPath($path)
290: {
291: if (preg_match('/^[A-Z]:\\\\/i', $path)) {
292: return true;
293: }
294:
295: return false;
296: }
297:
298: /**
299: * Returns true if given $path is an absolute path.
300: *
301: * @param string $path Path to check
302: *
303: * @return bool
304: * @access public
305: * @static
306: */
307: public function isAbsolute($path)
308: {
309: $match = preg_match('/^\\//', $path) || preg_match('/^[A-Z]:\\//i', $path);
310:
311: return $match;
312: }
313:
314: /**
315: * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
316: *
317: * @param string $path Path to check
318: *
319: * @return string Set of slashes ("\\" or "/")
320: * @access public
321: * @static
322: */
323: public function normalizePath($path)
324: {
325: if (XoopsFolderHandler::isWindowsPath($path)) {
326: return '\\';
327: }
328:
329: return '/';
330: }
331:
332: /**
333: * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
334: *
335: * @param string $path Path to check
336: *
337: * @return string Set of slashes ("\\" or "/")
338: * @access public
339: * @static
340: */
341: public function correctSlashFor($path)
342: {
343: if (XoopsFolderHandler::isWindowsPath($path)) {
344: return '\\';
345: }
346:
347: return '/';
348: }
349:
350: /**
351: * Returns $path with added terminating slash (corrected for Windows or other OS).
352: *
353: * @param string $path Path to check
354: *
355: * @return string Path with ending slash
356: * @access public
357: * @static
358: */
359: public function slashTerm($path)
360: {
361: if (XoopsFolderHandler::isSlashTerm($path)) {
362: return $path;
363: }
364:
365: return $path . XoopsFolderHandler::correctSlashFor($path);
366: }
367:
368: /**
369: * Returns $path with $element added, with correct slash in-between.
370: *
371: * @param string $path Path
372: * @param string $element Element to and at end of path
373: *
374: * @return string Combined path
375: * @access public
376: * @static
377: */
378: public function addPathElement($path, $element)
379: {
380: return $this->slashTerm($path) . $element;
381: }
382:
383: /**
384: * Returns true if the File is in a given XoopsPath.
385: *
386: * @param string $path
387: *
388: * @return bool
389: * @access public
390: */
391: public function inXoopsPath($path = '')
392: {
393: $dir = substr($this->slashTerm(XOOPS_ROOT_PATH), 0, -1);
394: $newdir = $dir . $path;
395:
396: return $this->inPath($newdir);
397: }
398:
399: /**
400: * Returns true if the File is in given path.
401: *
402: * @param string $path
403: * @param bool $reverse
404: *
405: * @return bool
406: * @access public
407: */
408: public function inPath($path = '', $reverse = false)
409: {
410: $dir = $this->slashTerm($path);
411: $current = $this->slashTerm($this->pwd());
412: if (!$reverse) {
413: $return = preg_match('/^(.*)' . preg_quote($dir, '/') . '(.*)/', $current);
414: } else {
415: $return = preg_match('/^(.*)' . preg_quote($current, '/') . '(.*)/', $dir);
416: }
417: if ($return == 1) {
418: return true;
419: } else {
420: return false;
421: }
422: }
423:
424: /**
425: * Change the mode on a directory structure recursively.
426: *
427: * @param string $path The path to chmod
428: * @param bool|int $mode octal value 0755
429: * @param boolean $recursive chmod recursively
430: * @param array $exceptions array of files, directories to skip
431: *
432: * @return boolean Returns TRUE on success, FALSE on failure
433: * @access public
434: */
435: public function chmod($path, $mode = false, $recursive = true, $exceptions = array())
436: {
437: if (!$mode) {
438: $mode = $this->mode;
439: }
440: if ($recursive === false && is_dir($path)) {
441: if (chmod($path, intval($mode, 8))) {
442: $this->messages[] = sprintf('%s changed to %s', $path, $mode);
443:
444: return true;
445: } else {
446: $this->errors[] = sprintf('%s NOT changed to %s', $path, $mode);
447:
448: return false;
449: }
450: }
451: if (is_dir($path)) {
452: list($paths) = $this->tree($path);
453: foreach ($paths as $key => $fullpath) {
454: $check = explode('/', $fullpath);
455: $count = count($check);
456:
457: if (in_array($check[$count - 1], $exceptions)) {
458: continue;
459: }
460:
461: if (chmod($fullpath, intval($mode, 8))) {
462: $this->messages[] = sprintf('%s changed to %s', $fullpath, $mode);
463: } else {
464: $this->errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode);
465: }
466: }
467: if (empty($this->errors)) {
468: return true;
469: }
470: }
471:
472: return false;
473: }
474:
475: /**
476: * Returns an array of nested directories and files in each directory
477: *
478: * @param string $path the directory path to build the tree from
479: * @param boolean $hidden return hidden files and directories
480: * @param string $type either file or dir. null returns both files and directories
481: *
482: * @return mixed array of nested directories and files in each directory
483: * @access public
484: */
485: public function tree($path, $hidden = true, $type = null)
486: {
487: $path = rtrim($path, '/');
488: $this->files = array();
489: $this->directories = array(
490: $path);
491: $directories = array();
492: while (count($this->directories)) {
493: $dir = array_pop($this->directories);
494: $this->_tree($dir, $hidden);
495: $directories[] = $dir;
496: }
497: if ($type === null) {
498: return array(
499: $directories,
500: $this->files);
501: }
502: if ($type === 'dir') {
503: return $directories;
504: }
505:
506: return $this->files;
507: }
508:
509: /**
510: * Private method to list directories and files in each directory
511: *
512: * @param string $path
513: * @param $hidden
514: *
515: * @internal param $ $ = boolean $hidden
516: * @access private
517: */
518: public function _tree($path, $hidden)
519: {
520: if (is_dir($path)) {
521: $dirHandle = opendir($path);
522: while (false !== ($item = readdir($dirHandle))) {
523: $found = false;
524: if (($hidden === true && $item !== '.' && $item !== '..') || ($hidden === false && !preg_match('/^\\.(.*)$/', $item))) {
525: $found = $path . '/' . $item;
526: }
527: if ($found !== false) {
528: if (is_dir($found)) {
529: $this->directories[] = $found;
530: } else {
531: $this->files[] = $found;
532: }
533: }
534: }
535: closedir($dirHandle);
536: }
537: }
538:
539: /**
540: * Create a directory structure recursively.
541: *
542: * @param string $pathname The directory structure to create
543: * @param bool|int $mode octal value 0755
544: *
545: * @return boolean Returns TRUE on success, FALSE on failure
546: * @access public
547: */
548: public function create($pathname, $mode = false)
549: {
550: if (is_dir($pathname) || empty($pathname)) {
551: return true;
552: }
553: if (!$mode) {
554: $mode = $this->mode;
555: }
556: if (is_file($pathname)) {
557: $this->errors[] = sprintf('%s is a file', $pathname);
558:
559: return true;
560: }
561: $nextPathname = substr($pathname, 0, strrpos($pathname, '/'));
562: if ($this->create($nextPathname, $mode)) {
563: if (!file_exists($pathname)) {
564: if (mkdir($pathname, intval($mode, 8))) {
565: $this->messages[] = sprintf('%s created', $pathname);
566:
567: return true;
568: } else {
569: $this->errors[] = sprintf('%s NOT created', $pathname);
570:
571: return false;
572: }
573: }
574: }
575:
576: return true;
577: }
578:
579: /**
580: * Returns the size in bytes of this Folder.
581: *
582: * @return int $size
583: * @access public
584: */
585: public function dirsize()
586: {
587: $size = 0;
588: $directory = $this->slashTerm($this->path);
589: $stack = array($directory);
590: $count = count($stack);
591: for ($i = 0, $j = $count; $i < $j; ++$i) {
592: if (is_file($stack[$i])) {
593: $size += filesize($stack[$i]);
594: } else {
595: if (is_dir($stack[$i])) {
596: $dir = dir($stack[$i]);
597: if ($dir) {
598: while (false !== ($entry = $dir->read())) {
599: if ($entry === '.' || $entry === '..') {
600: continue;
601: }
602: $add = $stack[$i] . $entry;
603: if (is_dir($stack[$i] . $entry)) {
604: $add = $this->slashTerm($add);
605: }
606: $stack[] = $add;
607: }
608: $dir->close();
609: }
610: }
611: }
612: $j = count($stack);
613: }
614:
615: return $size;
616: }
617:
618: /**
619: * Recursively Remove directories if system allow.
620: *
621: * @param string $path Path of directory to delete
622: *
623: * @return boolean Success
624: * @access public
625: */
626: public function delete($path)
627: {
628: $path = $this->slashTerm($path);
629: if (is_dir($path) === true) {
630: $files = glob($path . '*', GLOB_NOSORT);
631: $normal_files = glob($path . '*');
632: $hidden_files = glob($path . '\.?*');
633: $files = array_merge($normal_files, $hidden_files);
634: if (is_array($files)) {
635: foreach ($files as $file) {
636: if (preg_match("/(\.|\.\.)$/", $file)) {
637: continue;
638: }
639: if (is_file($file) === true) {
640: if (unlink($file)) {
641: $this->messages[] = sprintf('%s removed', $path);
642: } else {
643: $this->errors[] = sprintf('%s NOT removed', $path);
644: }
645: } else {
646: if (is_dir($file) === true) {
647: if ($this->delete($file) === false) {
648: return false;
649: }
650: }
651: }
652: }
653: }
654: $path = substr($path, 0, strlen($path) - 1);
655: if (rmdir($path) === false) {
656: $this->errors[] = sprintf('%s NOT removed', $path);
657:
658: return false;
659: } else {
660: $this->messages[] = sprintf('%s removed', $path);
661: }
662: }
663:
664: return true;
665: }
666:
667: /**
668: * Recursive directory copy.
669: *
670: * @param array|string $options (to, from, chmod, skip)
671: *
672: * @return bool
673: * @access public
674: */
675: public function copy($options = array())
676: {
677: $to = null;
678: if (is_string($options)) {
679: $to = $options;
680: $options = array();
681: }
682: $options = array_merge(array(
683: 'to' => $to,
684: 'from' => $this->path,
685: 'mode' => $this->mode,
686: 'skip' => array()), $options);
687:
688: $fromDir = $options['from'];
689: $toDir = $options['to'];
690: $mode = $options['mode'];
691: if (!$this->cd($fromDir)) {
692: $this->errors[] = sprintf('%s not found', $fromDir);
693:
694: return false;
695: }
696: if (!is_dir($toDir)) {
697: mkdir($toDir, $mode);
698: }
699: if (!is_writable($toDir)) {
700: $this->errors[] = sprintf('%s not writable', $toDir);
701:
702: return false;
703: }
704: $exceptions = array_merge(array(
705: '.',
706: '..',
707: '.svn'), $options['skip']);
708: $handle = opendir($fromDir);
709: if ($handle) {
710: while (false !== ($item = readdir($handle))) {
711: if (!in_array($item, $exceptions)) {
712: $from = $this->addPathElement($fromDir, $item);
713: $to = $this->addPathElement($toDir, $item);
714: if (is_file($from)) {
715: if (copy($from, $to)) {
716: chmod($to, intval($mode, 8));
717: touch($to, filemtime($from));
718: $this->messages[] = sprintf('%s copied to %s', $from, $to);
719: } else {
720: $this->errors[] = sprintf('%s NOT copied to %s', $from, $to);
721: }
722: }
723: if (is_dir($from) && !file_exists($to)) {
724: if (mkdir($to, intval($mode, 8))) {
725: chmod($to, intval($mode, 8));
726: $this->messages[] = sprintf('%s created', $to);
727: $options = array_merge($options, array(
728: 'to' => $to,
729: 'from' => $from));
730: $this->copy($options);
731: } else {
732: $this->errors[] = sprintf('%s not created', $to);
733: }
734: }
735: }
736: }
737: closedir($handle);
738: } else {
739: return false;
740: }
741: if (!empty($this->errors)) {
742: return false;
743: }
744:
745: return true;
746: }
747:
748: /**
749: * Recursive directory move.
750: *
751: * @param array|string $options (to, from, chmod, skip)
752: *
753: * @return boolean Success
754: * @access public
755: */
756: public function move($options)
757: {
758: $to = null;
759: if (is_string($options)) {
760: $to = $options;
761: $options = (array)$options;
762: }
763: $options = array_merge(array(
764: 'to' => $to,
765: 'from' => $this->path,
766: 'mode' => $this->mode,
767: 'skip' => array()), $options);
768: if ($this->copy($options)) {
769: if ($this->delete($options['from'])) {
770: return $this->cd($options['to']);
771: }
772: }
773:
774: return false;
775: }
776:
777: /**
778: * get messages from latest method
779: *
780: * @return array
781: * @access public
782: */
783: public function messages()
784: {
785: return $this->messages;
786: }
787:
788: /**
789: * get error from latest method
790: *
791: * @return array
792: * @access public
793: */
794: public function errors()
795: {
796: return $this->errors;
797: }
798:
799: /**
800: * Get the real path (taking ".." and such into account)
801: *
802: * @param string $path Path to resolve
803: *
804: * @return string The resolved path
805: */
806: public function realpath($path)
807: {
808: $path = trim($path);
809: if (strpos($path, '..') === false) {
810: if (!$this->isAbsolute($path)) {
811: $path = $this->addPathElement($this->path, $path);
812: }
813:
814: return $path;
815: }
816: $parts = explode('/', $path);
817: $newparts = array();
818: $newpath = $path{0} === '/' ? '/' : '';
819: while (($part = array_shift($parts)) !== null) {
820: if ($part === '.' || $part == '') {
821: continue;
822: }
823: if ($part === '..') {
824: if (count($newparts) > 0) {
825: array_pop($newparts);
826: continue;
827: } else {
828: return false;
829: }
830: }
831: $newparts[] = $part;
832: }
833: $newpath .= implode('/', $newparts);
834: if (strlen($path > 1) && $path{strlen($path) - 1} === '/') {
835: $newpath .= '/';
836: }
837:
838: return $newpath;
839: }
840:
841: /**
842: * Returns true if given $path ends in a slash (i.e. is slash-terminated).
843: *
844: * @param string $path Path to check
845: *
846: * @return boolean true if path ends with slash, false otherwise
847: * @access public
848: * @static
849: */
850: public function isSlashTerm($path)
851: {
852: if (preg_match('/[\/\\\]$/', $path)) {
853: return true;
854: }
855:
856: return false;
857: }
858: }
859: