<?php
/** 
 * @package     VikWP - Libraries
 * @subpackage  adapter.filesystem
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

JLoader::import('adapter.filesystem.path');
JLoader::import('adapter.filesystem.file');

/**
 * This class provides a common interface for
 * Folder handling in the Wordpress CMS plugins.
 *
 * @since 10.0
 */
class JFolder
{
	/**
	 * Checks whether the given path exists and is a directory.
	 *
	 * @param   string   $path  Folder name.
	 *
	 * @return  boolean  True if path is a folder.
	 */
	public static function exists($path)
	{
		return is_dir(JPath::clean($path));
	}

	/**
	 * Create a folder and all necessary parent folders.
	 *
	 * @param   string   $path  A path to create from the base path.
	 * @param   integer  $mode  Directory permissions to set for folders created. 0755 by default.
	 *
	 * @return  boolean  True if successful.
	 */
	public static function create($path = '', $mode = 0755)
	{
		static $nested = 0;

		$path = JPath::clean($path);

		// check if parent dir exists
		$parent = dirname($path);

		if (!self::exists($parent))
		{
			// prevent infinite loops
			$nested++;

			if (($nested > 20) || ($parent == $path))
			{
				$nested--;

				return false;
			}

			// create the parent directory
			if (self::create($parent, $mode) !== true)
			{
				$nested--;

				return false;
			}

			// OK, parent directory has been created
			$nested--;
		}

		// check if dir already exists
		if (self::exists($path))
		{
			return true;
		}

		// first set umask (no permissions to revoke)
		$origmask = @umask(0);

		// try to create the path
		$ret = @mkdir($path, $mode);

		// reset original umask
		@umask($origmask);

		// return whether the folder has been created or not
		return $ret;
	}

	/**
	 * Utility function to read the files in a folder.
	 *
	 * @param   string   $path           The path of the folder to read.
	 * @param   string   $filter         A filter for file names.
	 * @param   mixed    $recurse        True to recursively search into sub-folders, or an integer to specify the maximum depth.
	 * @param   boolean  $full           True to return the full path to the file.
	 * @param   array    $exclude        Array with names of files which should not be shown in the result.
	 * @param   array    $excludefilter  Array of filter to exclude.
	 * @param   boolean  $naturalSort    False for asort, true for natsort.
	 *
	 * @return  array  	 Files in the given folder.
	 *
	 * @uses 	_items()
	 */
	public static function files($path, $filter = '.', $recurse = false, $full = false, $exclude = array('.svn', 'CVS', '.DS_Store', '__MACOSX'),
		$excludefilter = array('^\..*', '.*~'), $naturalSort = false)
	{
		// make sure the path is a folder
		if (!static::exists($path))
		{
			return false;
		}

		// compute the excludefilter string
		if (count($excludefilter))
		{
			$excludefilter_string = '/(' . implode('|', $excludefilter) . ')/';
		}
		else
		{
			$excludefilter_string = '';
		}

		// get the files
		$arr = self::_items($path, $filter, $recurse, $full, $exclude, $excludefilter_string, true);

		// sort the files based on either natural or alpha method
		if ($naturalSort)
		{
			natsort($arr);
		}
		else
		{
			asort($arr);
		}

		return array_values($arr);
	}

	/**
	 * Utility function to read the folders in a folder.
	 *
	 * @param   string   $path           The path of the folder to read.
	 * @param   string   $filter         A filter for folder names.
	 * @param   mixed    $recurse        True to recursively search into sub-folders, or an integer to specify the maximum depth.
	 * @param   boolean  $full           True to return the full path to the folders.
	 * @param   array    $exclude        Array with names of folders which should not be shown in the result.
	 * @param   array    $excludefilter  Array with regular expressions matching folders which should not be shown in the result.
	 *
	 * @return  array  	 Folders in the given folder.
	 *
	 * @uses 	_items()
	 */
	public static function folders($path, $filter = '.', $recurse = false, $full = false, $exclude = array('.svn', 'CVS', '.DS_Store', '__MACOSX'),
		$excludefilter = array('^\..*'))
	{
		// make sure the path is a folder
		if (!static::exists($path))
		{
			return false;
		}

		// Compute the excludefilter string
		if (count($excludefilter))
		{
			$excludefilter_string = '/(' . implode('|', $excludefilter) . ')/';
		}
		else
		{
			$excludefilter_string = '';
		}

		// Get the folders
		$arr = self::_items($path, $filter, $recurse, $full, $exclude, $excludefilter_string, false);

		// Sort the folders
		asort($arr);

		return array_values($arr);
	}

	/**
	 * Function to read the files/folders in a folder.
	 *
	 * @param   string   $path                  The path of the folder to read.
	 * @param   string   $filter                A filter for file names.
	 * @param   mixed    $recurse               True to recursively search into sub-folders, or an integer to specify the maximum depth.
	 * @param   boolean  $full                  True to return the full path to the file.
	 * @param   array    $exclude               Array with names of files which should not be shown in the result.
	 * @param   string   $excludefilter_string  Regexp of files to exclude.
	 * @param   boolean  $findfiles             True to read the files, false to read the folders.
	 *
	 * @return  array    Search for recursive files.
	 */
	protected static function _items($path, $filter, $recurse, $full, $exclude, $excludefilter_string, $findfiles)
	{
		@set_time_limit(ini_get('max_execution_time'));

		$path = JPath::clean($path);

		$arr = array();

		// Read the source directory
		if (!($handle = @opendir($path)))
		{
			return $arr;
		}

		while (($file = readdir($handle)) !== false)
		{
			if ($file != '.' && $file != '..' && !in_array($file, $exclude)
				&& (empty($excludefilter_string) || !preg_match($excludefilter_string, $file)))
			{
				// Compute the fullpath
				$fullpath = $path . '/' . $file;

				// Compute the isDir flag
				$isDir = is_dir($fullpath);

				if (($isDir xor $findfiles) && preg_match("/$filter/", $file))
				{
					// (fullpath is dir and folders are searched or fullpath is not dir and files are searched) and file matches the filter
					if ($full)
					{
						// Full path is requested
						$arr[] = $fullpath;
					}
					else
					{
						// Filename is requested
						$arr[] = $file;
					}
				}

				if ($isDir && $recurse)
				{
					// Search recursively
					if (is_int($recurse))
					{
						// Until depth 0 is reached
						$arr = array_merge($arr, self::_items($fullpath, $filter, $recurse - 1, $full, $exclude, $excludefilter_string, $findfiles));
					}
					else
					{
						$arr = array_merge($arr, self::_items($fullpath, $filter, $recurse, $full, $exclude, $excludefilter_string, $findfiles));
					}
				}
			}
		}

		closedir($handle);

		return $arr;
	}

	/**
	 * Copy a source folder to a destination.
	 *
	 * @param   string   $src 	 The full folder path.
	 * @param   string   $dest 	 The full destination path.
	 * @param   string   $path 	 An optional base path to use as prefix.
	 * @param   boolean  $force  Copy the folder even if it exists.
	 *
	 * @return  boolean  True on success.
	 *
	 * @uses 	exists()
	 * @uses 	create()
	 */
	public static function copy($src, $dest, $path = '', $force = false)
	{
		@set_time_limit(ini_get('max_execution_time'));

		if ($path)
		{
			$src  = $path . '/' . $src;
			$dest = $path . '/' . $dest;
		}

		$src  = JPath::clean($src);
		$dest = JPath::clean($dest);

		// Get rid of trailing directory separators, if any
		$src = rtrim($src, DIRECTORY_SEPARATOR);
		$dest = rtrim($dest, DIRECTORY_SEPARATOR);

		if (!self::exists($src))
		{
			return false;
		}

		if (self::exists($dest) && !$force)
		{
			return false;
		}

		// Make sure the destination exists
		if (!self::create($dest))
		{
			return false;
		}

		if (!($dh = @opendir($src)))
		{
			return false;
		}
		// Walk through the directory copying files and recursing into folders.
		while (($file = readdir($dh)) !== false)
		{
			$sfid = $src . '/' . $file;
			$dfid = $dest . '/' . $file;

			switch (filetype($sfid))
			{
				case 'dir':
					if ($file != '.' && $file != '..')
					{
						$ret = self::copy($sfid, $dfid, null, $force);

						if ($ret !== true)
						{
							return $ret;
						}
					}
					break;

				case 'file':
					if (!JFile::copy($sfid, $dfid))
					{
						return false;
					}
					break;
			}
		}

		return true;
	}

	/**
	 * Delete a folder with all its sub-folders and files.
	 *
	 * @param   string   $path 	The path to the folder to remove.
	 *
	 * @return  boolean  true on success.
	 *
	 * @uses 	files()
	 * @uses 	folders()
	 */
	public static function delete($path)
	{
		@set_time_limit(ini_get('max_execution_time'));

		if (!$path)
		{
			return false;
		}

		// ensure path gets cleaned and is valid
		$path = JPath::clean($path);
		
		if (!static::exists($path))
		{
			return false;
		}

		// remove all the files in the current folder
		$files = self::files($path, '.', false, true, array(), array());

		if (!empty($files))
		{
			if (JFile::delete($files) !== true)
			{
				return false;
			}
		}

		// remove sub-folders of the current folder
		$folders = self::folders($path, '.', false, true, array(), array());

		foreach ($folders as $folder)
		{
			if (is_link($folder))
			{
				// we don't follow links
				if (JFile::delete($folder) !== true)
				{
					return false;
				}
			}
			else if (self::delete($folder) !== true)
			{
				return false;
			}
		}

		// folder should now be removed as long as the owner is www-data
		if (@rmdir($path))
		{
			return true;
		}
		
		return false;
	}
}
