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 (https://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 <https://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 https://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 https://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 = array();
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|false 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_string($path) && 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: $newFound = $this->findRecursive($pattern);
275:
276: foreach ($newFound as $item) {
277: $found[] = $item;
278: }
279: }
280:
281: return $found;
282: }
283:
284: /**
285: * Returns true if given $path is a Windows path.
286: *
287: * @param string $path Path to check
288: *
289: * @return boolean true if windows path, false otherwise
290: * @access public
291: * @static
292: */
293: public function isWindowsPath($path)
294: {
295: if (preg_match('/^[A-Z]:\\\\/i', (string)$path)) {
296: return true;
297: }
298:
299: return false;
300: }
301:
302: /**
303: * Returns true if given $path is an absolute path.
304: *
305: * @param string $path Path to check
306: *
307: * @return bool
308: * @access public
309: * @static
310: */
311: public function isAbsolute($path)
312: {
313: $match = preg_match('/^\\//', $path) || preg_match('/^[A-Z]:\\//i', $path);
314:
315: return $match;
316: }
317:
318: /**
319: * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
320: *
321: * @param string $path Path to check
322: *
323: * @return string Set of slashes ("\\" or "/")
324: * @access public
325: * @static
326: */
327: public function normalizePath($path)
328: {
329: if ($this->isWindowsPath($path)) {
330: return '\\';
331: }
332:
333: return '/';
334: }
335:
336: /**
337: * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
338: *
339: * @param string $path Path to check
340: *
341: * @return string Set of slashes ("\\" or "/")
342: * @access public
343: * @static
344: */
345: public function correctSlashFor($path)
346: {
347: if ($this->isWindowsPath($path)) {
348: return '\\';
349: }
350:
351: return '/';
352: }
353:
354: /**
355: * Returns $path with added terminating slash (corrected for Windows or other OS).
356: *
357: * @param string $path Path to check
358: *
359: * @return string Path with ending slash
360: * @access public
361: * @static
362: */
363: public function slashTerm($path)
364: {
365: if ($this->isSlashTerm($path)) {
366: return $path;
367: }
368:
369: return $path . $this->correctSlashFor($path);
370: }
371:
372: /**
373: * Returns $path with $element added, with correct slash in-between.
374: *
375: * @param string $path Path
376: * @param string $element Element to and at end of path
377: *
378: * @return string Combined path
379: * @access public
380: * @static
381: */
382: public function addPathElement($path, $element)
383: {
384: return $this->slashTerm($path) . $element;
385: }
386:
387: /**
388: * Returns true if the File is in a given XoopsPath.
389: *
390: * @param string $path
391: *
392: * @return bool
393: * @access public
394: */
395: public function inXoopsPath($path = '')
396: {
397: $dir = substr($this->slashTerm(XOOPS_ROOT_PATH), 0, -1);
398: $newdir = $dir . $path;
399:
400: return $this->inPath($newdir);
401: }
402:
403: /**
404: * Returns true if the File is in given path.
405: *
406: * @param string $path
407: * @param bool $reverse
408: *
409: * @return bool
410: * @access public
411: */
412: public function inPath($path = '', $reverse = false)
413: {
414: $dir = $this->slashTerm($path);
415: $current = $this->slashTerm($this->pwd());
416: if (!$reverse) {
417: $return = preg_match('/^(.*)' . preg_quote($dir, '/') . '(.*)/', $current);
418: } else {
419: $return = preg_match('/^(.*)' . preg_quote($current, '/') . '(.*)/', $dir);
420: }
421: if ($return == 1) {
422: return true;
423: } else {
424: return false;
425: }
426: }
427:
428: /**
429: * Change the mode on a directory structure recursively.
430: *
431: * @param string $path The path to chmod
432: * @param bool|int $mode octal value 0755
433: * @param boolean $recursive chmod recursively
434: * @param array $exceptions array of files, directories to skip
435: *
436: * @return boolean Returns TRUE on success, FALSE on failure
437: * @access public
438: */
439: public function chmod($path, $mode = false, $recursive = true, $exceptions = array())
440: {
441: if (!$mode) {
442: $mode = $this->mode;
443: }
444: if ($recursive === false && is_dir($path)) {
445: if (chmod($path, intval($mode, 8))) {
446: $this->messages[] = sprintf('%s changed to %s', $path, $mode);
447:
448: return true;
449: } else {
450: $this->errors[] = sprintf('%s NOT changed to %s', $path, $mode);
451:
452: return false;
453: }
454: }
455: if (is_dir($path)) {
456: list($paths) = $this->tree($path);
457: foreach ($paths as $key => $fullpath) {
458: $check = explode('/', $fullpath);
459: $count = count($check);
460:
461: if (in_array($check[$count - 1], $exceptions)) {
462: continue;
463: }
464:
465: if (chmod($fullpath, intval($mode, 8))) {
466: $this->messages[] = sprintf('%s changed to %s', $fullpath, $mode);
467: } else {
468: $this->errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode);
469: }
470: }
471: if (empty($this->errors)) {
472: return true;
473: }
474: }
475:
476: return false;
477: }
478:
479: /**
480: * Returns an array of nested directories and files in each directory
481: *
482: * @param string $path the directory path to build the tree from
483: * @param boolean $hidden return hidden files and directories
484: * @param string $type either file or dir. null returns both files and directories
485: *
486: * @return mixed array of nested directories and files in each directory
487: * @access public
488: */
489: public function tree($path, $hidden = true, $type = null)
490: {
491: $path = rtrim($path, '/');
492: $this->files = array();
493: $this->directories = array(
494: $path);
495: $directories = array();
496: while (count($this->directories)) {
497: $dir = array_pop($this->directories);
498: $this->_tree($dir, $hidden);
499: $directories[] = $dir;
500: }
501: if ($type === null) {
502: return array(
503: $directories,
504: $this->files);
505: }
506: if ($type === 'dir') {
507: return $directories;
508: }
509:
510: return $this->files;
511: }
512:
513: /**
514: * Private method to list directories and files in each directory
515: *
516: * @param string $path
517: * @param $hidden
518: *
519: * @internal param $ $ = boolean $hidden
520: * @access private
521: */
522: public function _tree($path, $hidden)
523: {
524: if (is_dir($path)) {
525: $dirHandle = opendir($path);
526: while (false !== ($item = readdir($dirHandle))) {
527: $found = false;
528: if (($hidden === true && $item !== '.' && $item !== '..') || ($hidden === false && !preg_match('/^\\.(.*)$/', $item))) {
529: $found = $path . '/' . $item;
530: }
531: if ($found !== false) {
532: if (is_dir($found)) {
533: $this->directories[] = $found;
534: } else {
535: $this->files[] = $found;
536: }
537: }
538: }
539: closedir($dirHandle);
540: }
541: }
542:
543: /**
544: * Create a directory structure recursively.
545: *
546: * @param string $pathname The directory structure to create
547: * @param bool|int $mode octal value 0755
548: *
549: * @return boolean Returns TRUE on success, FALSE on failure
550: * @access public
551: */
552: public function create($pathname, $mode = false)
553: {
554: if (is_dir($pathname) || empty($pathname)) {
555: return true;
556: }
557: if (!$mode) {
558: $mode = $this->mode;
559: }
560: if (is_file($pathname)) {
561: $this->errors[] = sprintf('%s is a file', $pathname);
562:
563: return true;
564: }
565: $nextPathname = substr($pathname, 0, strrpos($pathname, '/'));
566: if ($this->create($nextPathname, $mode)) {
567: if (!file_exists($pathname)) {
568: if (mkdir($pathname, intval($mode, 8))) {
569: $this->messages[] = sprintf('%s created', $pathname);
570:
571: return true;
572: } else {
573: $this->errors[] = sprintf('%s NOT created', $pathname);
574:
575: return false;
576: }
577: }
578: }
579:
580: return true;
581: }
582:
583: /**
584: * Returns the size in bytes of this Folder.
585: *
586: * @return int $size
587: * @access public
588: */
589: public function dirsize()
590: {
591: $size = 0;
592: $directory = $this->slashTerm($this->path);
593: $stack = array($directory);
594: $count = count($stack);
595: for ($i = 0, $j = $count; $i < $j; ++$i) {
596: if (is_file($stack[$i])) {
597: $size += filesize($stack[$i]);
598: } else {
599: if (is_dir($stack[$i])) {
600: $dir = dir($stack[$i]);
601: if ($dir) {
602: while (false !== ($entry = $dir->read())) {
603: if ($entry === '.' || $entry === '..') {
604: continue;
605: }
606: $add = $stack[$i] . $entry;
607: if (is_dir($stack[$i] . $entry)) {
608: $add = $this->slashTerm($add);
609: }
610: $stack[] = $add;
611: }
612: $dir->close();
613: }
614: }
615: }
616: $j = count($stack);
617: }
618:
619: return $size;
620: }
621:
622: /**
623: * Recursively Remove directories if system allow.
624: *
625: * @param string $path Path of directory to delete
626: *
627: * @return boolean Success
628: * @access public
629: */
630: public function delete($path)
631: {
632: $path = $this->slashTerm($path);
633: if (is_dir($path) === true) {
634: $files = glob($path . '*', GLOB_NOSORT);
635: $normal_files = glob($path . '*');
636: $hidden_files = glob($path . '\.?*');
637: $files = array_merge($normal_files, $hidden_files);
638: if (is_array($files)) {
639: foreach ($files as $file) {
640: if (preg_match("/(\.|\.\.)$/", $file)) {
641: continue;
642: }
643: if (is_file($file) === true) {
644: if (unlink($file)) {
645: $this->messages[] = sprintf('%s removed', $path);
646: } else {
647: $this->errors[] = sprintf('%s NOT removed', $path);
648: }
649: } else {
650: if (is_dir($file) === true) {
651: if ($this->delete($file) === false) {
652: return false;
653: }
654: }
655: }
656: }
657: }
658: $path = substr($path, 0, strlen($path) - 1);
659: if (rmdir($path) === false) {
660: $this->errors[] = sprintf('%s NOT removed', $path);
661:
662: return false;
663: } else {
664: $this->messages[] = sprintf('%s removed', $path);
665: }
666: }
667:
668: return true;
669: }
670:
671: /**
672: * Copies files and directories from one directory to another
673: *
674: * @param array|string $options An array of options or a string representing the target directory
675: * If a string is provided, it will be used as the target directory and other options will be set to their default values
676: * @return bool Returns true on success, false on failure
677: */
678: public function copy($options = array())
679: {
680: $to = null;
681: if (is_string($options)) {
682: $to = $options;
683: $options = array();
684: }
685: $options = array_merge(array(
686: 'to' => $to,
687: 'from' => $this->path,
688: 'mode' => $this->mode,
689: 'skip' => array()), $options);
690:
691: $fromDir = $options['from'];
692: $toDir = $options['to'];
693: $mode = $options['mode'];
694: if (!$this->cd($fromDir)) {
695: $this->errors[] = sprintf('%s not found', $fromDir);
696:
697: return false;
698: }
699: if (!is_dir($toDir)) {
700: mkdir($toDir, $mode);
701: }
702: if (!is_writable($toDir)) {
703: $this->errors[] = sprintf('%s not writable', $toDir);
704:
705: return false;
706: }
707: $exceptions = array_merge(array(
708: '.',
709: '..',
710: '.svn'), $options['skip']);
711: $handle = opendir($fromDir);
712: if ($handle) {
713: while (false !== ($item = readdir($handle))) {
714: if (!in_array($item, $exceptions)) {
715: $from = $this->addPathElement($fromDir, $item);
716: $to = $this->addPathElement($toDir, $item);
717: if (is_file($from)) {
718: if (copy($from, $to)) {
719: chmod($to, intval($mode, 8));
720: touch($to, filemtime($from));
721: $this->messages[] = sprintf('%s copied to %s', $from, $to);
722: } else {
723: $this->errors[] = sprintf('%s NOT copied to %s', $from, $to);
724: }
725: }
726:
727: if (is_dir($from)) {
728: if (!is_dir($to)) {
729: if (mkdir($to, intval($mode, 8)) || is_dir($to)) {
730: chmod($to, intval($mode, 8));
731: $this->messages[] = sprintf('%s created', $to);
732:
733: $options['to'] = $to;
734: $options['from'] = $from;
735:
736: $this->copy($options);
737: } else {
738: // Ensure $this->errors is an array before adding an element
739: if (is_array($this->errors)) {
740: $this->errors[] = sprintf('%s not created', $to);
741: }
742: }
743: }
744: }
745: }
746: }
747: closedir($handle);
748: } else {
749: return false;
750: }
751: if (!empty($this->errors)) {
752: return false;
753: }
754:
755: return true;
756: }
757:
758: /**
759: * Recursive directory move.
760: *
761: * @param array|string $options (to, from, chmod, skip)
762: *
763: * @return string|boolean Success
764: * @access public
765: */
766: public function move($options)
767: {
768: $to = null;
769: if (is_string($options)) {
770: $to = $options;
771: $options = (array)$options;
772: }
773: $options = array_merge(array(
774: 'to' => $to,
775: 'from' => $this->path,
776: 'mode' => $this->mode,
777: 'skip' => array()), $options);
778: if ($this->copy($options)) {
779: if ($this->delete($options['from'])) {
780: return $this->cd($options['to']);
781: }
782: }
783:
784: return false;
785: }
786:
787: /**
788: * get messages from latest method
789: *
790: * @return array
791: * @access public
792: */
793: public function messages()
794: {
795: return $this->messages;
796: }
797:
798: /**
799: * get error from latest method
800: *
801: * @return array
802: * @access public
803: */
804: public function errors()
805: {
806: return $this->errors;
807: }
808:
809: /**
810: * Get the real path (taking ".." and such into account)
811: *
812: * @param string $path Path to resolve
813: *
814: * @return string|false The resolved path
815: */
816: public function realpath($path)
817: {
818: $path = trim($path);
819: if (strpos($path, '..') === false) {
820: if (!$this->isAbsolute($path)) {
821: $path = $this->addPathElement($this->path, $path);
822: }
823:
824: return $path;
825: }
826: $parts = explode('/', $path);
827: $newparts = array();
828: $newpath = $path[0] === '/' ? '/' : '';
829: while (($part = array_shift($parts)) !== null) {
830: if ($part === '.' || $part == '') {
831: continue;
832: }
833: if ($part === '..') {
834: if (count($newparts) > 0) {
835: array_pop($newparts);
836: continue;
837: } else {
838: return false;
839: }
840: }
841: $newparts[] = $part;
842: }
843: $newpath .= implode('/', $newparts);
844: if ((strlen($path) > 1) && $path[strlen($path) - 1] === '/') {
845: $newpath .= '/';
846: }
847:
848: return $newpath;
849: }
850:
851: /**
852: * Returns true if given $path ends in a slash (i.e. is slash-terminated).
853: *
854: * @param string $path Path to check
855: *
856: * @return boolean true if path ends with slash, false otherwise
857: * @access public
858: * @static
859: */
860: public function isSlashTerm($path)
861: {
862: if (preg_match('/[\/\\\]$/', (string)$path)) {
863: return true;
864: }
865:
866: return false;
867: }
868: }
869: