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