acf
This commit is contained in:
		
							
								
								
									
										398
									
								
								plugins/system/nrframework/NRFramework/File.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								plugins/system/nrframework/NRFramework/File.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,398 @@ | ||||
| <?php | ||||
|  | ||||
| /** | ||||
|  * @author          Tassos Marinos <info@tassos.gr> | ||||
|  * @link            https://www.tassos.gr | ||||
|  * @copyright       Copyright © 2024 Tassos All Rights Reserved | ||||
|  * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later | ||||
| */ | ||||
|  | ||||
| namespace NRFramework; | ||||
|  | ||||
| defined( '_JEXEC' ) or die( 'Restricted access' ); | ||||
|  | ||||
| use NRFramework\Mimes; | ||||
| use Joomla\Filesystem\File as JoomlaFile; | ||||
| use Joomla\Filesystem\Path; | ||||
| use Joomla\CMS\Language\Text; | ||||
| use Joomla\CMS\Factory; | ||||
|  | ||||
| class File  | ||||
| { | ||||
| 	/** | ||||
| 	 * Upload file | ||||
| 	 * | ||||
| 	 * @param	array	$file				The request file as posted by form | ||||
| 	 * @param	string	$upload_folder		The upload folder where the file must be uploaded | ||||
| 	 * @param	string	$allowed_file_types	A comma separated list of allowed file types like: .jpg, .gif, .png | ||||
| 	 * @param	bool	$allow_unsafe		Allow the upload of unsafe files. See JFilterInput::isSafeFile() method. | ||||
| 	 * @param	bool	$random_prefix		If is set to true, the filename will get a random unique prefix | ||||
| 	 * @param	bool	$random_suffix		If is set to true, the filename will get a random unique suffix | ||||
| 	 * | ||||
| 	 * @return	mixed	String on success, Null on failure | ||||
| 	 */ | ||||
| 	public static function upload($file, $upload_folder = null, $allowed_file_types = [], $allow_unsafe = false, $random_prefix = null, $random_suffix = false) | ||||
| 	{ | ||||
| 		// Make sure we have a valid file array | ||||
| 		if (!isset($file['name']) || !isset($file['tmp_name'])) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_UPLOAD_ERROR_CANNOT_UPLOAD_FILE', $file['name'])); | ||||
| 		} | ||||
|  | ||||
| 		// Check file type | ||||
| 		self::checkMimeOrDie($allowed_file_types, $file); | ||||
|  | ||||
| 		/** | ||||
| 		 * Try transiterating the file name using the native php function | ||||
| 		 *  | ||||
| 		 * If the given filename is non-latin, then all characters will be removed from the filename via makeSafe and thus | ||||
| 		 * we wont be able to upload the file. | ||||
| 		 *  | ||||
| 		 * @see https://github.com/joomla/joomla-cms/pull/27974 | ||||
| 		 */ | ||||
| 		if (!defined('t_isJ5') && function_exists('transliterator_transliterate') && function_exists('iconv')) | ||||
| 		{ | ||||
| 			// Using iconv to ignore characters that can't be transliterated | ||||
| 			$file['name'] = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", transliterator_transliterate('Any-Latin; Latin-ASCII;', $file['name'])); | ||||
| 		} | ||||
| 		 | ||||
| 		// Sanitize filename | ||||
| 		$filename = JoomlaFile::makeSafe($file['name']); | ||||
|  | ||||
| 		if (!is_null($random_prefix)) | ||||
| 		{ | ||||
| 			$filename = uniqid($random_prefix) . '_' . $filename; | ||||
| 		} | ||||
|  | ||||
| 		if (is_bool($random_suffix) && $random_suffix === true) | ||||
| 		{ | ||||
| 			$file_data = File::pathinfo($filename); | ||||
| 			$filename = $file_data['filename'] . '_' . uniqid($random_suffix) . '.' . $file_data['extension']; | ||||
| 		} | ||||
|  | ||||
| 		$filename = str_replace(' ', '_', $filename); | ||||
|  | ||||
| 		// Setup the full file name | ||||
| 		$upload_folder = is_null($upload_folder) ? self::getTempFolder() : $upload_folder; | ||||
| 		$destination_file = implode(DIRECTORY_SEPARATOR, [$upload_folder, $filename]); | ||||
|  | ||||
| 		// If file exists, rename to copy_X | ||||
| 		self::uniquefy($destination_file); | ||||
|  | ||||
| 		$destination_file = Path::clean($destination_file); | ||||
|  | ||||
| 		if (!JoomlaFile::upload($file['tmp_name'], $destination_file, false, $allow_unsafe)) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_UPLOAD_ERROR_CANNOT_UPLOAD_FILE', $file['name'])); | ||||
| 		} | ||||
|  | ||||
| 		return $destination_file; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Moves a file from one directory to another. Destination directories will be created if they are not exist. | ||||
| 	 * | ||||
| 	 * @param	string	$source_file		The source file path | ||||
| 	 * @param	string	$destination_file	The destination file path | ||||
| 	 * @param	bool	$replace_existing	Replace same files names, otherwise create a copy in the format copy_X | ||||
| 	 * | ||||
| 	 * @return	mixed	String on success | ||||
| 	 */ | ||||
| 	public static function move($source_file, $destination_file, $replace_existing = false, $hash = false) | ||||
| 	{ | ||||
| 		$destination_folder = dirname($destination_file); | ||||
|  | ||||
| 		// Create destination folders recursively | ||||
| 		if (!self::createDirs($destination_folder)) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_CANNOT_CREATE_FOLDER', $destination_folder)); | ||||
| 		} | ||||
|  | ||||
| 		// Don't replace files with the same name. Instead, append copy_x to this one. | ||||
| 		if (!$replace_existing) | ||||
| 		{ | ||||
| 			self::uniquefy($destination_file, $hash); | ||||
| 		} | ||||
|  | ||||
| 		// Move file to the destination folder | ||||
| 		if (!JoomlaFile::move($source_file, $destination_file)) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_CANNOT_MOVE_FILE', $destination_file)); | ||||
| 		} | ||||
|  | ||||
| 		return Path::clean($destination_file); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Copies a file from one directory to another. | ||||
| 	 *  | ||||
| 	 * @param	string	$source_file		The source file path | ||||
| 	 * @param	string	$destination_file	The destination file path | ||||
| 	 * @param	bool	$replace_existing	Replace same files names, otherwise create a copy in the format copy_X | ||||
| 	 * @param	bool	$hash				Whether to md5 hash the filename | ||||
| 	 *  | ||||
| 	 * @return	mixed	String on success | ||||
| 	 */ | ||||
| 	public static function copy($source_file, $destination_file, $replace_existing = false, $hash = false) | ||||
| 	{ | ||||
| 		$destination_folder = dirname($destination_file); | ||||
|  | ||||
| 		// Create destination folders recursively | ||||
| 		if (!self::createDirs($destination_folder)) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_CANNOT_CREATE_FOLDER', $destination_folder)); | ||||
| 		} | ||||
|  | ||||
| 		// Don't replace files with the same name. Instead, append copy_x to this one. | ||||
| 		if (!$replace_existing) | ||||
| 		{ | ||||
| 			self::uniquefy($destination_file, $hash); | ||||
| 		} | ||||
|  | ||||
| 		// Copy file to the destination folder | ||||
| 		if (!JoomlaFile::copy($source_file, $destination_file)) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_CANNOT_MOVE_FILE', $destination_file)); | ||||
| 		} | ||||
|  | ||||
| 		return Path::clean($destination_file); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Reads (and checks) the temp Joomla folder | ||||
| 	 * | ||||
| 	 * @return string | ||||
| 	 */ | ||||
| 	public static function getTempFolder() | ||||
| 	{ | ||||
| 		$ds = DIRECTORY_SEPARATOR; | ||||
|  | ||||
| 		$tmpdir = Factory::getConfig()->get('tmp_path'); | ||||
|  | ||||
| 		if (realpath($tmpdir) == $ds . 'tmp') | ||||
| 		{ | ||||
| 			$tmpdir = JPATH_SITE . $ds . 'tmp'; | ||||
| 		} | ||||
| 		 | ||||
| 		elseif (!is_dir($tmpdir)) | ||||
| 		{ | ||||
| 			$tmpdir = JPATH_SITE . $ds . 'tmp'; | ||||
| 		} | ||||
|  | ||||
| 		return Path::clean(trim($tmpdir) . $ds); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks if the path exists. If not creates the folders as well as subfolders. | ||||
| 	 *  | ||||
| 	 * @param   string  $path	 The folder path | ||||
| 	 * @param   string  $protect If set to true, each folder will be protected by disabling PHP engine and preventing folder browsing | ||||
| 	 *  | ||||
| 	 * @return  bool | ||||
| 	 */ | ||||
| 	public static function createDirs($path, $protect = true) | ||||
| 	{ | ||||
| 		if (!is_dir($path)) | ||||
| 		{ | ||||
| 			mkdir($path, 0755, true); | ||||
|  | ||||
| 			// New folder created. Let's protect it. | ||||
| 			if ($protect) | ||||
| 			{ | ||||
| 				self::writeHtaccessFile($path); | ||||
| 				self::writeIndexHtmlFile($path); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Make sure the folder is writable | ||||
| 		return @is_writable($path); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks whether a file type is in an allowed list | ||||
| 	 * | ||||
| 	 * @param	mixed	$allowed_types	Array or a comma separated list of allowed file extensions or mime types. Eg: .jpg, .png, applicaton/pdf | ||||
| 	 * @param	string	$file_object	The uploaded file as appears in the $_FILES array | ||||
| 	 * | ||||
| 	 * @return	bool | ||||
| 	 */ | ||||
| 	public static function checkMimeOrDie($allowed_types, $file_object) | ||||
| 	{ | ||||
| 		$file_path = $file_object['tmp_name']; | ||||
| 		$file_name = isset($file_object['name']) ? $file_object['name'] : basename($file_path); | ||||
| 		$safeFilename = strip_tags($file_name); | ||||
| 		$fileExtension = pathinfo($file_name, PATHINFO_EXTENSION); | ||||
| 		 | ||||
| 		// First, validate file by its extension | ||||
| 		if (!Mimes::validateFileExtension($fileExtension, $allowed_types)) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_UPLOAD_INVALID_FILE_EXT', $safeFilename, $fileExtension, $allowed_types)); | ||||
| 		} | ||||
|  | ||||
| 		// Do we have a mime type detected? | ||||
| 		if (!$mime_type = Mimes::detectFileType($file_path)) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_UPLOAD_NO_MIME_TYPE', $safeFilename)); | ||||
| 		} | ||||
|  | ||||
| 		if (!Mimes::check($allowed_types, $mime_type)) | ||||
| 		{ | ||||
| 			self::error(Text::sprintf('NR_UPLOAD_INVALID_FILE_TYPE', $safeFilename, $mime_type, $allowed_types)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Add an .htaccess file to the folder in order to disable PHP engine entirely  | ||||
| 	 * | ||||
| 	 * @param  string $path	The path where to write the file | ||||
| 	 * | ||||
| 	 * @return void | ||||
| 	 */ | ||||
| 	public static function writeHtaccessFile($path) | ||||
| 	{ | ||||
| 		$content = ' | ||||
| 			# Block direct PHP access | ||||
| 			<Files *.php> | ||||
| 				<IfModule !mod_authz_core.c> | ||||
| 					Deny from all | ||||
| 				</IfModule> | ||||
| 				<IfModule mod_authz_core.c> | ||||
| 					Require all denied | ||||
| 				</IfModule> | ||||
| 			</Files> | ||||
| 		'; | ||||
|  | ||||
| 		JoomlaFile::write($path . '/.htaccess', $content); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Creates an empty index.html file to prevent directory listing  | ||||
| 	 * | ||||
| 	 * @param  string $path	The path where to write the file | ||||
| 	 * | ||||
| 	 * @return void | ||||
| 	 */ | ||||
| 	public static function writeIndexHtmlFile($path) | ||||
| 	{ | ||||
| 		$content = '<!DOCTYPE html><title></title>'; | ||||
| 		 | ||||
| 		JoomlaFile::write($path . '/index.html', $content);	 | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Generates a unique filename in case the give name already exists by appending copy_X suffix to filename. | ||||
| 	 * | ||||
| 	 * @param   string  $path	The path to the file. | ||||
| 	 * @param   bool	$hash	MD5 hashes the file name. | ||||
| 	 * | ||||
| 	 * @return  void | ||||
| 	 */ | ||||
| 	public static function uniquefy(&$path, $hash = false) | ||||
| 	{ | ||||
| 		$path_parts = self::pathinfo($path); | ||||
|  | ||||
| 		$dir = $path_parts['dirname']; | ||||
| 		$ext = $path_parts['extension']; | ||||
| 		$actual_name = $path_parts['filename']; | ||||
| 		 | ||||
| 		$original_name = $actual_name; | ||||
|  | ||||
| 		// md5 hash the file name | ||||
| 		if ($hash) | ||||
| 		{ | ||||
| 			$actual_name = md5($actual_name); | ||||
|  | ||||
| 			// Initialize again the path due to md5 hash | ||||
| 			$path = $dir . '/' . $actual_name . '.' . $ext; | ||||
| 		} | ||||
|  | ||||
| 		$i = 1; | ||||
|  | ||||
| 		while(file_exists($dir . '/' . $actual_name . '.' . $ext)) | ||||
| 		{            | ||||
| 			$actual_name = (string) $original_name . '_copy_' . $i; | ||||
|  | ||||
| 			// md5 hash the file name | ||||
| 			if ($hash) | ||||
| 			{ | ||||
| 				$actual_name = md5($actual_name); | ||||
| 			} | ||||
| 			 | ||||
| 			$path = $dir . '/' . $actual_name . '.' . $ext; | ||||
| 			$i++; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns information about a file path with multi-byte support | ||||
| 	 * | ||||
| 	 * @param  string $path   The path to be parsed. | ||||
| 	 * | ||||
| 	 * @return array  | ||||
| 	 */ | ||||
| 	public static function pathinfo($path) | ||||
| 	{ | ||||
| 		// Store temporary the currenty locale | ||||
| 		$currentLocale = setlocale(LC_ALL, 0); | ||||
|  | ||||
| 		setlocale(LC_ALL, 'C.UTF-8'); | ||||
| 		$pathinfo = pathinfo($path); | ||||
|  | ||||
| 		// Set back to previus value | ||||
| 		setlocale(LC_ALL, $currentLocale); | ||||
|  | ||||
| 		return $pathinfo; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Force download of the exported file | ||||
| 	 * | ||||
| 	 * @return void | ||||
| 	 */ | ||||
| 	public static function download($filename, $path = null) | ||||
| 	{ | ||||
|         $path = is_null($path) ? self::getTempFolder() : $path; | ||||
| 		$filename = $path . '/' . $filename; | ||||
|  | ||||
| 		if (!is_file($filename)) | ||||
| 		{ | ||||
| 			self::error('Invalid filename ' . $filename); | ||||
| 		} | ||||
|  | ||||
| 		error_reporting(0); | ||||
|  | ||||
| 		// Send the appropriate headers to force the download in the browser | ||||
| 		header('Content-Description: File Transfer'); | ||||
| 		header('Content-Type: application/octet-stream'); | ||||
| 		header('Content-Disposition: attachment; filename="' . basename($filename) . '"'); | ||||
| 		header('Expires: 0'); | ||||
| 		header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); | ||||
| 		header('Cache-Control: public', false); | ||||
| 		header('Pragma: public'); | ||||
| 		header('Content-Length: ' . @filesize($filename)); | ||||
|  | ||||
|         // Clear the output buffer and disable output buffering | ||||
|         ob_clean(); | ||||
|         flush(); | ||||
|  | ||||
| 		// Read exported file to buffer | ||||
| 		readfile($filename); | ||||
|  | ||||
| 		// Don't leave any clues on the server. Delete the file. | ||||
| 		JoomlaFile::delete($filename); | ||||
|  | ||||
| 		jexit(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Throw a sanitized exception | ||||
| 	 * | ||||
| 	 * @param	string	$error | ||||
| 	 *  | ||||
| 	 * @return	void | ||||
| 	 */ | ||||
| 	private static function error($error) | ||||
| 	{ | ||||
| 		throw new \Exception(htmlspecialchars($error, ENT_QUOTES, 'UTF-8')); | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user