1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage FileSystem
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 /**
13 * A Folder handling class
14 *
15 * @since 11.1
16 */
17 abstract class JFolder
18 {
19 /**
20 * Copy a folder.
21 *
22 * @param string $src The path to the source folder.
23 * @param string $dest The path to the destination folder.
24 * @param string $path An optional base path to prefix to the file names.
25 * @param boolean $force Force copy.
26 * @param boolean $use_streams Optionally force folder/file overwrites.
27 *
28 * @return boolean True on success.
29 *
30 * @since 11.1
31 * @throws RuntimeException
32 */
33 public static function copy($src, $dest, $path = '', $force = false, $use_streams = false)
34 {
35 @set_time_limit(ini_get('max_execution_time'));
36
37 $FTPOptions = JClientHelper::getCredentials('ftp');
38 $pathObject = new JFilesystemWrapperPath;
39
40 if ($path)
41 {
42 $src = $pathObject->clean($path . '/' . $src);
43 $dest = $pathObject->clean($path . '/' . $dest);
44 }
45
46 // Eliminate trailing directory separators, if any
47 $src = rtrim($src, DIRECTORY_SEPARATOR);
48 $dest = rtrim($dest, DIRECTORY_SEPARATOR);
49
50 if (!self::exists($src))
51 {
52 throw new RuntimeException('Source folder not found', -1);
53 }
54
55 if (self::exists($dest) && !$force)
56 {
57 throw new RuntimeException('Destination folder already exists', -1);
58 }
59
60 // Make sure the destination exists
61 if (!self::create($dest))
62 {
63 throw new RuntimeException('Cannot create destination folder', -1);
64 }
65
66 // If we're using ftp and don't have streams enabled
67 if ($FTPOptions['enabled'] == 1 && !$use_streams)
68 {
69 // Connect the FTP client
70 $ftp = JClientFtp::getInstance($FTPOptions['host'], $FTPOptions['port'], array(), $FTPOptions['user'], $FTPOptions['pass']);
71
72 if (!($dh = @opendir($src)))
73 {
74 throw new RuntimeException('Cannot open source folder', -1);
75 }
76 // Walk through the directory copying files and recursing into folders.
77 while (($file = readdir($dh)) !== false)
78 {
79 $sfid = $src . '/' . $file;
80 $dfid = $dest . '/' . $file;
81
82 switch (filetype($sfid))
83 {
84 case 'dir':
85 if ($file != '.' && $file != '..')
86 {
87 $ret = self::copy($sfid, $dfid, null, $force);
88
89 if ($ret !== true)
90 {
91 return $ret;
92 }
93 }
94 break;
95
96 case 'file':
97 // Translate path for the FTP account
98 $dfid = $pathObject->clean(str_replace(JPATH_ROOT, $FTPOptions['root'], $dfid), '/');
99
100 if (!$ftp->store($sfid, $dfid))
101 {
102 throw new RuntimeException('Copy file failed', -1);
103 }
104 break;
105 }
106 }
107 }
108 else
109 {
110 if (!($dh = @opendir($src)))
111 {
112 throw new RuntimeException('Cannot open source folder', -1);
113 }
114 // Walk through the directory copying files and recursing into folders.
115 while (($file = readdir($dh)) !== false)
116 {
117 $sfid = $src . '/' . $file;
118 $dfid = $dest . '/' . $file;
119
120 switch (filetype($sfid))
121 {
122 case 'dir':
123 if ($file != '.' && $file != '..')
124 {
125 $ret = self::copy($sfid, $dfid, null, $force, $use_streams);
126
127 if ($ret !== true)
128 {
129 return $ret;
130 }
131 }
132 break;
133
134 case 'file':
135 if ($use_streams)
136 {
137 $stream = JFactory::getStream();
138
139 if (!$stream->copy($sfid, $dfid))
140 {
141 throw new RuntimeException('Cannot copy file: ' . $stream->getError(), -1);
142 }
143 }
144 else
145 {
146 if (!@copy($sfid, $dfid))
147 {
148 throw new RuntimeException('Copy file failed', -1);
149 }
150 }
151 break;
152 }
153 }
154 }
155
156 return true;
157 }
158
159 /**
160 * Create a folder -- and all necessary parent folders.
161 *
162 * @param string $path A path to create from the base path.
163 * @param integer $mode Directory permissions to set for folders created. 0755 by default.
164 *
165 * @return boolean True if successful.
166 *
167 * @since 11.1
168 */
169 public static function create($path = '', $mode = 0755)
170 {
171 $FTPOptions = JClientHelper::getCredentials('ftp');
172 static $nested = 0;
173
174 // Check to make sure the path valid and clean
175 $pathObject = new JFilesystemWrapperPath;
176 $path = $pathObject->clean($path);
177
178 // Check if parent dir exists
179 $parent = dirname($path);
180
181 if (!self::exists($parent))
182 {
183 // Prevent infinite loops!
184 $nested++;
185
186 if (($nested > 20) || ($parent == $path))
187 {
188 JLog::add(__METHOD__ . ': ' . JText::_('JLIB_FILESYSTEM_ERROR_FOLDER_LOOP'), JLog::WARNING, 'jerror');
189 $nested--;
190
191 return false;
192 }
193
194 // Create the parent directory
195 if (self::create($parent, $mode) !== true)
196 {
197 // JFolder::create throws an error
198 $nested--;
199
200 return false;
201 }
202
203 // OK, parent directory has been created
204 $nested--;
205 }
206
207 // Check if dir already exists
208 if (self::exists($path))
209 {
210 return true;
211 }
212
213 // Check for safe mode
214 if ($FTPOptions['enabled'] == 1)
215 {
216 // Connect the FTP client
217 $ftp = JClientFtp::getInstance($FTPOptions['host'], $FTPOptions['port'], array(), $FTPOptions['user'], $FTPOptions['pass']);
218
219 // Translate path to FTP path
220 $path = $pathObject->clean(str_replace(JPATH_ROOT, $FTPOptions['root'], $path), '/');
221 $ret = $ftp->mkdir($path);
222 $ftp->chmod($path, $mode);
223 }
224 else
225 {
226 // We need to get and explode the open_basedir paths
227 $obd = ini_get('open_basedir');
228
229 // If open_basedir is set we need to get the open_basedir that the path is in
230 if ($obd != null)
231 {
232 if (IS_WIN)
233 {
234 $obdSeparator = ';';
235 }
236 else
237 {
238 $obdSeparator = ':';
239 }
240
241 // Create the array of open_basedir paths
242 $obdArray = explode($obdSeparator, $obd);
243 $inBaseDir = false;
244
245 // Iterate through open_basedir paths looking for a match
246 foreach ($obdArray as $test)
247 {
248 $test = $pathObject->clean($test);
249
250 if (strpos($path, $test) === 0)
251 {
252 $inBaseDir = true;
253 break;
254 }
255 }
256
257 if ($inBaseDir == false)
258 {
259 // Return false for JFolder::create because the path to be created is not in open_basedir
260 JLog::add(__METHOD__ . ': ' . JText::_('JLIB_FILESYSTEM_ERROR_FOLDER_PATH'), JLog::WARNING, 'jerror');
261
262 return false;
263 }
264 }
265
266 // First set umask
267 $origmask = @umask(0);
268
269 // Create the path
270 if (!$ret = @mkdir($path, $mode))
271 {
272 @umask($origmask);
273 JLog::add(
274 __METHOD__ . ': ' . JText::_('JLIB_FILESYSTEM_ERROR_COULD_NOT_CREATE_DIRECTORY') . 'Path: ' . $path, JLog::WARNING, 'jerror'
275 );
276
277 return false;
278 }
279
280 // Reset umask
281 @umask($origmask);
282 }
283
284 return $ret;
285 }
286
287 /**
288 * Delete a folder.
289 *
290 * @param string $path The path to the folder to delete.
291 *
292 * @return boolean True on success.
293 *
294 * @since 11.1
295 */
296 public static function delete($path)
297 {
298 @set_time_limit(ini_get('max_execution_time'));
299 $pathObject = new JFilesystemWrapperPath;
300
301 // Sanity check
302 if (!$path)
303 {
304 // Bad programmer! Bad Bad programmer!
305 JLog::add(__METHOD__ . ': ' . JText::_('JLIB_FILESYSTEM_ERROR_DELETE_BASE_DIRECTORY'), JLog::WARNING, 'jerror');
306
307 return false;
308 }
309
310 $FTPOptions = JClientHelper::getCredentials('ftp');
311
312 // Check to make sure the path valid and clean
313 $path = $pathObject->clean($path);
314
315 // Is this really a folder?
316 if (!is_dir($path))
317 {
318 JLog::add(JText::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER', $path), JLog::WARNING, 'jerror');
319
320 return false;
321 }
322
323 // Remove all the files in folder if they exist; disable all filtering
324 $files = self::files($path, '.', false, true, array(), array());
325
326 if (!empty($files))
327 {
328 $file = new JFilesystemWrapperFile;
329
330 if ($file->delete($files) !== true)
331 {
332 // JFile::delete throws an error
333 return false;
334 }
335 }
336
337 // Remove sub-folders of folder; disable all filtering
338 $folders = self::folders($path, '.', false, true, array(), array());
339
340 foreach ($folders as $folder)
341 {
342 if (is_link($folder))
343 {
344 // Don't descend into linked directories, just delete the link.
345 $file = new JFilesystemWrapperFile;
346
347 if ($file->delete($folder) !== true)
348 {
349 // JFile::delete throws an error
350 return false;
351 }
352 }
353 elseif (self::delete($folder) !== true)
354 {
355 // JFolder::delete throws an error
356 return false;
357 }
358 }
359
360 if ($FTPOptions['enabled'] == 1)
361 {
362 // Connect the FTP client
363 $ftp = JClientFtp::getInstance($FTPOptions['host'], $FTPOptions['port'], array(), $FTPOptions['user'], $FTPOptions['pass']);
364 }
365
366 // In case of restricted permissions we zap it one way or the other
367 // as long as the owner is either the webserver or the ftp.
368 if (@rmdir($path))
369 {
370 $ret = true;
371 }
372 elseif ($FTPOptions['enabled'] == 1)
373 {
374 // Translate path and delete
375 $path = $pathObject->clean(str_replace(JPATH_ROOT, $FTPOptions['root'], $path), '/');
376
377 // FTP connector throws an error
378 $ret = $ftp->delete($path);
379 }
380 else
381 {
382 JLog::add(JText::sprintf('JLIB_FILESYSTEM_ERROR_FOLDER_DELETE', $path), JLog::WARNING, 'jerror');
383 $ret = false;
384 }
385
386 return $ret;
387 }
388
389 /**
390 * Moves a folder.
391 *
392 * @param string $src The path to the source folder.
393 * @param string $dest The path to the destination folder.
394 * @param string $path An optional base path to prefix to the file names.
395 * @param boolean $use_streams Optionally use streams.
396 *
397 * @return mixed Error message on false or boolean true on success.
398 *
399 * @since 11.1
400 */
401 public static function move($src, $dest, $path = '', $use_streams = false)
402 {
403 $FTPOptions = JClientHelper::getCredentials('ftp');
404 $pathObject = new JFilesystemWrapperPath;
405
406 if ($path)
407 {
408 $src = $pathObject->clean($path . '/' . $src);
409 $dest = $pathObject->clean($path . '/' . $dest);
410 }
411
412 if (!self::exists($src))
413 {
414 return JText::_('JLIB_FILESYSTEM_ERROR_FIND_SOURCE_FOLDER');
415 }
416
417 if (self::exists($dest))
418 {
419 return JText::_('JLIB_FILESYSTEM_ERROR_FOLDER_EXISTS');
420 }
421
422 if ($use_streams)
423 {
424 $stream = JFactory::getStream();
425
426 if (!$stream->move($src, $dest))
427 {
428 return JText::sprintf('JLIB_FILESYSTEM_ERROR_FOLDER_RENAME', $stream->getError());
429 }
430
431 $ret = true;
432 }
433 else
434 {
435 if ($FTPOptions['enabled'] == 1)
436 {
437 // Connect the FTP client
438 $ftp = JClientFtp::getInstance($FTPOptions['host'], $FTPOptions['port'], array(), $FTPOptions['user'], $FTPOptions['pass']);
439
440 // Translate path for the FTP account
441 $src = $pathObject->clean(str_replace(JPATH_ROOT, $FTPOptions['root'], $src), '/');
442 $dest = $pathObject->clean(str_replace(JPATH_ROOT, $FTPOptions['root'], $dest), '/');
443
444 // Use FTP rename to simulate move
445 if (!$ftp->rename($src, $dest))
446 {
447 return JText::_('JLIB_FILESYSTEM_ERROR_RENAME_FILE');
448 }
449
450 $ret = true;
451 }
452 else
453 {
454 if (!@rename($src, $dest))
455 {
456 return JText::_('JLIB_FILESYSTEM_ERROR_RENAME_FILE');
457 }
458
459 $ret = true;
460 }
461 }
462
463 return $ret;
464 }
465
466 /**
467 * Wrapper for the standard file_exists function
468 *
469 * @param string $path Folder name relative to installation dir
470 *
471 * @return boolean True if path is a folder
472 *
473 * @since 11.1
474 */
475 public static function exists($path)
476 {
477 $pathObject = new JFilesystemWrapperPath;
478
479 return is_dir($pathObject->clean($path));
480 }
481
482 /**
483 * Utility function to read the files in a folder.
484 *
485 * @param string $path The path of the folder to read.
486 * @param string $filter A filter for file names.
487 * @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth.
488 * @param boolean $full True to return the full path to the file.
489 * @param array $exclude Array with names of files which should not be shown in the result.
490 * @param array $excludefilter Array of filter to exclude
491 * @param boolean $naturalSort False for asort, true for natsort
492 *
493 * @return array Files in the given folder.
494 *
495 * @since 11.1
496 */
497 public static function files($path, $filter = '.', $recurse = false, $full = false, $exclude = array('.svn', 'CVS', '.DS_Store', '__MACOSX'),
498 $excludefilter = array('^\..*', '.*~'), $naturalSort = false)
499 {
500 // Check to make sure the path valid and clean
501 $pathObject = new JFilesystemWrapperPath;
502 $path = $pathObject->clean($path);
503
504 // Is the path a folder?
505 if (!is_dir($path))
506 {
507 JLog::add(JText::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER_FILES', $path), JLog::WARNING, 'jerror');
508
509 return false;
510 }
511
512 // Compute the excludefilter string
513 if (count($excludefilter))
514 {
515 $excludefilter_string = '/(' . implode('|', $excludefilter) . ')/';
516 }
517 else
518 {
519 $excludefilter_string = '';
520 }
521
522 // Get the files
523 $arr = self::_items($path, $filter, $recurse, $full, $exclude, $excludefilter_string, true);
524
525 // Sort the files based on either natural or alpha method
526 if ($naturalSort)
527 {
528 natsort($arr);
529 }
530 else
531 {
532 asort($arr);
533 }
534
535 return array_values($arr);
536 }
537
538 /**
539 * Utility function to read the folders in a folder.
540 *
541 * @param string $path The path of the folder to read.
542 * @param string $filter A filter for folder names.
543 * @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth.
544 * @param boolean $full True to return the full path to the folders.
545 * @param array $exclude Array with names of folders which should not be shown in the result.
546 * @param array $excludefilter Array with regular expressions matching folders which should not be shown in the result.
547 *
548 * @return array Folders in the given folder.
549 *
550 * @since 11.1
551 */
552 public static function folders($path, $filter = '.', $recurse = false, $full = false, $exclude = array('.svn', 'CVS', '.DS_Store', '__MACOSX'),
553 $excludefilter = array('^\..*'))
554 {
555 // Check to make sure the path valid and clean
556 $pathObject = new JFilesystemWrapperPath;
557 $path = $pathObject->clean($path);
558
559 // Is the path a folder?
560 if (!is_dir($path))
561 {
562 JLog::add(JText::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER_FOLDER', $path), JLog::WARNING, 'jerror');
563
564 return false;
565 }
566
567 // Compute the excludefilter string
568 if (count($excludefilter))
569 {
570 $excludefilter_string = '/(' . implode('|', $excludefilter) . ')/';
571 }
572 else
573 {
574 $excludefilter_string = '';
575 }
576
577 // Get the folders
578 $arr = self::_items($path, $filter, $recurse, $full, $exclude, $excludefilter_string, false);
579
580 // Sort the folders
581 asort($arr);
582
583 return array_values($arr);
584 }
585
586 /**
587 * Function to read the files/folders in a folder.
588 *
589 * @param string $path The path of the folder to read.
590 * @param string $filter A filter for file names.
591 * @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth.
592 * @param boolean $full True to return the full path to the file.
593 * @param array $exclude Array with names of files which should not be shown in the result.
594 * @param string $excludefilter_string Regexp of files to exclude
595 * @param boolean $findfiles True to read the files, false to read the folders
596 *
597 * @return array Files.
598 *
599 * @since 11.1
600 */
601 protected static function _items($path, $filter, $recurse, $full, $exclude, $excludefilter_string, $findfiles)
602 {
603 @set_time_limit(ini_get('max_execution_time'));
604
605 $arr = array();
606
607 // Read the source directory
608 if (!($handle = @opendir($path)))
609 {
610 return $arr;
611 }
612
613 while (($file = readdir($handle)) !== false)
614 {
615 if ($file != '.' && $file != '..' && !in_array($file, $exclude)
616 && (empty($excludefilter_string) || !preg_match($excludefilter_string, $file)))
617 {
618 // Compute the fullpath
619 $fullpath = $path . '/' . $file;
620
621 // Compute the isDir flag
622 $isDir = is_dir($fullpath);
623
624 if (($isDir xor $findfiles) && preg_match("/$filter/", $file))
625 {
626 // (fullpath is dir and folders are searched or fullpath is not dir and files are searched) and file matches the filter
627 if ($full)
628 {
629 // Full path is requested
630 $arr[] = $fullpath;
631 }
632 else
633 {
634 // Filename is requested
635 $arr[] = $file;
636 }
637 }
638
639 if ($isDir && $recurse)
640 {
641 // Search recursively
642 if (is_int($recurse))
643 {
644 // Until depth 0 is reached
645 $arr = array_merge($arr, self::_items($fullpath, $filter, $recurse - 1, $full, $exclude, $excludefilter_string, $findfiles));
646 }
647 else
648 {
649 $arr = array_merge($arr, self::_items($fullpath, $filter, $recurse, $full, $exclude, $excludefilter_string, $findfiles));
650 }
651 }
652 }
653 }
654
655 closedir($handle);
656
657 return $arr;
658 }
659
660 /**
661 * Lists folder in format suitable for tree display.
662 *
663 * @param string $path The path of the folder to read.
664 * @param string $filter A filter for folder names.
665 * @param integer $maxLevel The maximum number of levels to recursively read, defaults to three.
666 * @param integer $level The current level, optional.
667 * @param integer $parent Unique identifier of the parent folder, if any.
668 *
669 * @return array Folders in the given folder.
670 *
671 * @since 11.1
672 */
673 public static function listFolderTree($path, $filter, $maxLevel = 3, $level = 0, $parent = 0)
674 {
675 $dirs = array();
676
677 if ($level == 0)
678 {
679 $GLOBALS['_JFolder_folder_tree_index'] = 0;
680 }
681
682 if ($level < $maxLevel)
683 {
684 $folders = self::folders($path, $filter);
685 $pathObject = new JFilesystemWrapperPath;
686
687 // First path, index foldernames
688 foreach ($folders as $name)
689 {
690 $id = ++$GLOBALS['_JFolder_folder_tree_index'];
691 $fullName = $pathObject->clean($path . '/' . $name);
692 $dirs[] = array(
693 'id' => $id,
694 'parent' => $parent,
695 'name' => $name,
696 'fullname' => $fullName,
697 'relname' => str_replace(JPATH_ROOT, '', $fullName),
698 );
699 $dirs2 = self::listFolderTree($fullName, $filter, $maxLevel, $level + 1, $id);
700 $dirs = array_merge($dirs, $dirs2);
701 }
702 }
703
704 return $dirs;
705 }
706
707 /**
708 * Makes path name safe to use.
709 *
710 * @param string $path The full path to sanitise.
711 *
712 * @return string The sanitised string.
713 *
714 * @since 11.1
715 */
716 public static function makeSafe($path)
717 {
718 $regex = array('#[^A-Za-z0-9_\\\/\(\)\[\]\{\}\#\$\^\+\.\'~`!@&=;,-]#');
719
720 return preg_replace($regex, '', $path);
721 }
722 }
723