| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Services\FileSystem\Drivers\Local; |
| 4 |
|
| 5 |
use FluentCart\App\App; |
| 6 |
use FluentCart\App\Helpers\Helper; |
| 7 |
use FluentCart\App\Modules\StorageDrivers\BaseStorageDriver; |
| 8 |
use FluentCart\App\Modules\StorageDrivers\Local\Local as LocalStorageDriver; |
| 9 |
use FluentCart\App\Services\FileSystem\Drivers\BaseDriver; |
| 10 |
use FluentCart\App\Services\FileSystem\StoragePath; |
| 11 |
use FluentCart\Framework\Support\Arr; |
| 12 |
use FluentCart\Framework\Support\Str; |
| 13 |
|
| 14 |
class LocalDriver extends BaseDriver |
| 15 |
{ |
| 16 |
public function getDirName(): string |
| 17 |
{ |
| 18 |
return $this->dirName; |
| 19 |
} |
| 20 |
|
| 21 |
|
| 22 |
public function __construct(?string $dirPath = null, ?string $dirName = null) |
| 23 |
{ |
| 24 |
parent::__construct($dirPath, $dirName); |
| 25 |
$this->dirName = $dirName ?? 'fluent-cart'; |
| 26 |
$this->dirPath = $dirPath ?? $this->getDefaultDirPath(); |
| 27 |
$this->storageDriver = new LocalStorageDriver(); |
| 28 |
|
| 29 |
global $wp_filesystem; |
| 30 |
|
| 31 |
// Initialize the WP_Filesystem API |
| 32 |
if (empty($wp_filesystem)) { |
| 33 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 34 |
WP_Filesystem(); |
| 35 |
} |
| 36 |
|
| 37 |
$this->ensureDirectoryExist(); |
| 38 |
|
| 39 |
} |
| 40 |
|
| 41 |
protected function getDefaultDirPath(): string |
| 42 |
{ |
| 43 |
return wp_get_upload_dir()['basedir'] . DIRECTORY_SEPARATOR . $this->getDirName(); |
| 44 |
} |
| 45 |
|
| 46 |
private function ensureDirectoryExist() { |
| 47 |
|
| 48 |
|
| 49 |
$uploadDirectory = $this->getDefaultDirPath(); |
| 50 |
global $wp_filesystem; |
| 51 |
// Check if directory exists |
| 52 |
if (! $wp_filesystem->is_dir($uploadDirectory)) { |
| 53 |
$wp_filesystem->mkdir($uploadDirectory, FS_CHMOD_DIR); |
| 54 |
} |
| 55 |
} |
| 56 |
|
| 57 |
|
| 58 |
public function listFiles(array $params = []): array |
| 59 |
{ |
| 60 |
$filesArray = []; |
| 61 |
$searchTerm = $params['search'] ?? ''; |
| 62 |
|
| 63 |
$files = scandir($this->getDefaultDirPath()); |
| 64 |
|
| 65 |
$maxFile = App::request()->get('per_page', 10); |
| 66 |
|
| 67 |
$fileCount = 1; |
| 68 |
|
| 69 |
foreach ($files as $file) { |
| 70 |
// Skip hidden files (starting with dot) |
| 71 |
if (str_starts_with($file, '.')) { |
| 72 |
continue; |
| 73 |
} |
| 74 |
|
| 75 |
if (str_ends_with($file, '.php')) { |
| 76 |
continue; |
| 77 |
} |
| 78 |
|
| 79 |
// Apply search filter if provided |
| 80 |
if (!empty($searchTerm) && stripos($file, $searchTerm) === false) { |
| 81 |
continue; |
| 82 |
} |
| 83 |
|
| 84 |
$filePath = $this->getDefaultDirPath() . '/' . $file; |
| 85 |
if (is_file($filePath)) { |
| 86 |
$filesArray[] = [ |
| 87 |
'name' => $file, |
| 88 |
'size' => filesize($filePath), |
| 89 |
'driver' => 'local', |
| 90 |
'bucket' => '' |
| 91 |
]; |
| 92 |
} |
| 93 |
|
| 94 |
$fileCount++; |
| 95 |
if($fileCount > $maxFile) { |
| 96 |
break; |
| 97 |
} |
| 98 |
} |
| 99 |
|
| 100 |
return $filesArray; |
| 101 |
} |
| 102 |
|
| 103 |
public function uploadFile($localFilePath, $uploadToFilePath, $file, $params = []) |
| 104 |
{ |
| 105 |
global $wp_filesystem; |
| 106 |
|
| 107 |
$uploadToFilePath = sanitize_file_name($uploadToFilePath); |
| 108 |
|
| 109 |
|
| 110 |
$fileInfo = $file->toArray(); |
| 111 |
$fileSize =Arr::get($fileInfo, 'size_in_bytes', 0); |
| 112 |
|
| 113 |
$originalName = $file->getClientOriginalName(); |
| 114 |
$sanitizedName = sanitize_file_name($originalName); |
| 115 |
$checked = wp_check_filetype_and_ext( |
| 116 |
$localFilePath, |
| 117 |
$sanitizedName |
| 118 |
); |
| 119 |
|
| 120 |
|
| 121 |
if (empty($checked['ext']) || empty($checked['type'])) { |
| 122 |
return new \WP_Error( |
| 123 |
'invalid_file_type', |
| 124 |
__('Invalid or unsupported file type.', 'fluent-cart') |
| 125 |
); |
| 126 |
} |
| 127 |
|
| 128 |
$blockedExts = ['php','phtml','html','htm','svg','exe','sh','bat','cmd','dll']; |
| 129 |
|
| 130 |
$blockedExts = apply_filters('fluent_cart/local_file_blocked_extensions',$blockedExts,[ |
| 131 |
'localFilePath' => $localFilePath, |
| 132 |
'uploadToFilePath' => $uploadToFilePath, |
| 133 |
'fileInfo' => $fileInfo, |
| 134 |
'fileSize' => $fileSize, |
| 135 |
'originalName' => $originalName, |
| 136 |
'file' => $file, |
| 137 |
'checked_data' => $checked, |
| 138 |
]); |
| 139 |
if (in_array($checked['ext'], $blockedExts, true)) { |
| 140 |
return new \WP_Error( |
| 141 |
'forbidden_type', |
| 142 |
__('This file type is not allowed.', 'fluent-cart') |
| 143 |
); |
| 144 |
} |
| 145 |
|
| 146 |
// Generate unique filename |
| 147 |
$uploadToFilePath = $uploadToFilePath . '__fluent-cart__.' . time() . '.' . $file->getClientOriginalExtension(); |
| 148 |
|
| 149 |
// Define destination |
| 150 |
$destination = trailingslashit( $this->getDefaultDirPath() ) . $uploadToFilePath; |
| 151 |
|
| 152 |
// Read file contents from temp path |
| 153 |
$contents = @file_get_contents( $localFilePath ); |
| 154 |
|
| 155 |
if ( $contents === false ) { |
| 156 |
return new \WP_Error( 'failed_to_read', __( 'Failed to read uploaded file', 'fluent-cart' ) ); |
| 157 |
} |
| 158 |
|
| 159 |
// Write file using WP_Filesystem |
| 160 |
$isUploaded = $wp_filesystem->put_contents( $destination, $contents, FS_CHMOD_FILE ); |
| 161 |
|
| 162 |
// Optionally delete temp file using WP_Filesystem for compliance |
| 163 |
if ( $wp_filesystem->exists( $localFilePath ) ) { |
| 164 |
$wp_filesystem->delete( $localFilePath ); |
| 165 |
} |
| 166 |
|
| 167 |
if ( ! $isUploaded ) { |
| 168 |
return new \WP_Error( 'failed_to_upload', __( 'Failed to upload file', 'fluent-cart' ) ); |
| 169 |
} |
| 170 |
|
| 171 |
return [ |
| 172 |
'message' => __( 'File Uploaded Successfully', 'fluent-cart' ), |
| 173 |
'path' => $uploadToFilePath, |
| 174 |
'file' => [ |
| 175 |
'driver' => 'local', |
| 176 |
'size' => $fileSize, |
| 177 |
'name' => $uploadToFilePath, |
| 178 |
'bucket' => '', |
| 179 |
], |
| 180 |
]; |
| 181 |
} |
| 182 |
|
| 183 |
|
| 184 |
public function getSignedDownloadUrl(string $filePath, $bucket = null, $productDownload = null): string |
| 185 |
{ |
| 186 |
return Helper::generateDownloadFileLink($productDownload); |
| 187 |
} |
| 188 |
|
| 189 |
public function downloadFile(string $filePath, $fileName = null) |
| 190 |
{ |
| 191 |
$fileName = sanitize_file_name($fileName); |
| 192 |
|
| 193 |
$file = $this->getFilePath($filePath); |
| 194 |
if (ob_get_level()) { |
| 195 |
ob_end_clean(); |
| 196 |
} |
| 197 |
|
| 198 |
// A path that escapes the storage directory reports as missing rather |
| 199 |
// than as rejected, so this never answers whether a file outside the |
| 200 |
// directory exists. |
| 201 |
if (!$file || !file_exists($file)) { |
| 202 |
return new \WP_Error('file_not_found', __('File not found', 'fluent-cart')); |
| 203 |
} |
| 204 |
$fileSize = filesize($file); |
| 205 |
$fileName = $fileName ?? basename($filePath); |
| 206 |
$fileName = explode('_____fluent-cart_____', $fileName)[0]; |
| 207 |
$fileName = explode('__fluent-cart__', $fileName)[0]; |
| 208 |
$this->setDownloadHeader($fileName ?? basename($filePath), $file, $fileSize); |
| 209 |
global $wp_filesystem; |
| 210 |
$content = $wp_filesystem->get_contents( $file ); |
| 211 |
if ( $content !== false ) { |
| 212 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 213 |
echo $content; |
| 214 |
} |
| 215 |
exit; |
| 216 |
} |
| 217 |
|
| 218 |
/** |
| 219 |
* Absolute path for a stored file, or '' when $filePath escapes the |
| 220 |
* storage directory. |
| 221 |
* |
| 222 |
* `..` survives both sanitize_text_field() and wp_normalize_path(), so |
| 223 |
* composing the path by concatenation alone let a relative path address any |
| 224 |
* file the web user could reach. |
| 225 |
*/ |
| 226 |
public function getFilePath(string $filePath, $fileName = null): string |
| 227 |
{ |
| 228 |
return StoragePath::contain($this->dirPath, $filePath); |
| 229 |
} |
| 230 |
|
| 231 |
protected function retrieveFileForDownload(string $downloadableFilePath) |
| 232 |
{ |
| 233 |
// TODO: Implement retrieveFileForDownload() method. |
| 234 |
} |
| 235 |
|
| 236 |
public function deleteFile(string $filePath) |
| 237 |
{ |
| 238 |
|
| 239 |
if ( !current_user_can('manage_options') ) { |
| 240 |
return new \WP_Error('permission_error', __('You are not allowed to delete file', 'fluent-cart')); |
| 241 |
} |
| 242 |
|
| 243 |
$fullPath = $this->getFilePath($filePath); |
| 244 |
|
| 245 |
// Same as the download path: an escaping path reports as missing so the |
| 246 |
// response cannot be used to probe for files outside the directory. |
| 247 |
if (!$fullPath || !file_exists($fullPath)) { |
| 248 |
return new \WP_Error('file_not_found', __('File not found', 'fluent-cart')); |
| 249 |
} |
| 250 |
|
| 251 |
if (!is_file($fullPath)) { |
| 252 |
return new \WP_Error('not_a_file', __('Path is not a file', 'fluent-cart')); |
| 253 |
} |
| 254 |
|
| 255 |
if (wp_delete_file($fullPath)) { |
| 256 |
return [ |
| 257 |
'message' => __('File Deleted Successfully', 'fluent-cart'), |
| 258 |
'driver' => 'local', |
| 259 |
'path' => $filePath |
| 260 |
]; |
| 261 |
} |
| 262 |
|
| 263 |
return new \WP_Error('failed_to_delete', __('Failed to delete file', 'fluent-cart')); |
| 264 |
} |
| 265 |
} |
| 266 |
|