- Notifications
You must be signed in to change notification settings - Fork1k
Description
Hi@mevdschee, first of all I would like to thank you for this fantastic project: you had a beautiful idea and it helped me create very interesting works, and for this I wanted to give my contribution too. The only thing that in my opinion was missing (and that I needed in the past) was an integrated file manager. I then started to create a custom middleware. It works, it does its dirty job, but I want to discuss with you and the whole community to understand if it is taking the right direction before continuing with writing the code in vain. I am therefore sharing with all of you the code and a mini-documentation that I have written to help you understand what I have done so far. Please test it (not in production, although it works) and let me know what you think!
I commented the code as much as possible where necessary, read it! Let's collaborate!
namespaceController\Custom {useException;useImagick;useImagickException;usePsr\Http\Message\ResponseInterface;usePsr\Http\Message\ServerRequestInterface;useRecursiveDirectoryIterator;useRecursiveIteratorIterator;useTqdev\PhpCrudApi\Cache\Cache;useTqdev\PhpCrudApi\Column\ReflectionService;useTqdev\PhpCrudApi\Controller\Responder;useTqdev\PhpCrudApi\Database\GenericDB;useTqdev\PhpCrudApi\Middleware\Router\Router;useTqdev\PhpCrudApi\ResponseFactory;class FileManagerController{/** * @var Responder $responder The responder instance used to send responses. */private$responder;/** * @var Cache $cache The cache instance used for caching data. */private$cache;/** * @var string ENDPOINT The directory where files are uploaded. */privateconstENDPOINT ='/files';/** * @var string UPLOAD_FOLDER_NAME The name of the folder where files are uploaded. */privateconstUPLOAD_FOLDER_NAME ='uploads';/** * @var int MIN_REQUIRED_DISK_SPACE The minimum required disk space for file uploads in bytes. */privateconstMIN_REQUIRED_DISK_SPACE =104857600;// 100MB in bytes/** * @var string $dir The directory where files are uploaded. */private$dir;/** * @var array PHP_FILE_UPLOAD_ERRORS An array mapping PHP file upload error codes to error messages. */privateconstPHP_FILE_UPLOAD_ERRORS = [0 =>'There is no error, the file uploaded with success',1 =>'The uploaded file exceeds the upload_max_filesize directive',2 =>'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',3 =>'The uploaded file was only partially uploaded',4 =>'No file was uploaded',6 =>'Missing a temporary folder',7 =>'Failed to write file to disk.',8 =>'A PHP extension stopped the file upload.', ];/** * @var array MIME_WHITE_LIST An array of allowed MIME types for file uploads. */privateconstMIME_WHITE_LIST = ['image/*',// Images'video/*',// Videos'audio/*',// Audios'application/pdf',// PDF'application/x-zip-compressed',// ZIP'application/zip',// ZIP'application/msword',// DOC'application/vnd.openxmlformats-officedocument.wordprocessingml.document',// DOCX'application/vnd.ms-excel',// XLS'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',// XLSX'application/vnd.ms-powerpoint',// PPT'application/vnd.openxmlformats-officedocument.presentationml.presentation',// PPTX'application/xml',// XML'text/xml',// XML'application/json',// JSON'text/csv',// CSV ];/** * FileManagerController constructor. * * This constructor initializes the FileManagerController by setting up the default directory, * initializing the responder and cache instances, and registering the routes for file-related operations. * * @param Router $router The router instance used to register routes. * @param Responder $responder The responder instance used to send responses. * @param GenericDB $db The database instance used for database operations. * @param ReflectionService $reflection The reflection service instance used for column reflection. * @param Cache $cache The cache instance used for caching data. */publicfunction__construct(Router$router,Responder$responder,GenericDB$db,ReflectionService$reflection,Cache$cache) {$this->dir =__DIR__ .DIRECTORY_SEPARATOR .$this::UPLOAD_FOLDER_NAME;$this->validateDefaultDir();$this->responder =$responder;$this->cache =$cache;$router->register('GET',$this::ENDPOINT,array($this,'_initFileRequest'));$router->register('GET',$this::ENDPOINT .'/limits',array($this,'_initLimits'));$router->register('GET',$this::ENDPOINT .'/view',array($this,'_initFileView'));$router->register('GET',$this::ENDPOINT .'/download',array($this,'_initFileDownload'));$router->register('GET',$this::ENDPOINT .'/stats',array($this,'_initStats'));$router->register('GET',$this::ENDPOINT .'/img_resize',array($this,'_initImgResize'));$router->register('GET',$this::ENDPOINT .'/img_cpr',array($this,'_initImgCompress'));$router->register('POST',$this::ENDPOINT .'/upload',array($this,'_initFileUpload'));$router->register('POST',$this::ENDPOINT .'/move',array($this,'_initFileMove'));$router->register('POST',$this::ENDPOINT .'/rename',array($this,'_initFileRename'));$router->register('POST',$this::ENDPOINT .'/copy',array($this,'_initFileCopy'));$router->register('DELETE',$this::ENDPOINT .'/delete',array($this,'_initFileDelete')); }/** * Retrieves statistics about the files and folders in the default directory. * * This method calculates the total size, number of files, and number of folders * in the default directory. It returns a response containing these statistics. * * @param ServerRequestInterface $request The server request instance. * @return ResponseInterface The response containing the statistics of the directory. */publicfunction_initStats(ServerRequestInterface$request):ResponseInterface {$total_size =0;$total_files =0;$total_folders =0;$directoryIterator =newRecursiveDirectoryIterator($this->dir, RecursiveDirectoryIterator::SKIP_DOTS);$iterator =newRecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::SELF_FIRST);foreach ($iteratoras$file) {if ($file->isFile()) {$total_size +=$file->getSize();$total_files++; }elseif ($file->isDir()) {$total_folders++; } }$total_size =$this->formatFileSize($total_size);return$this->responder->success(['total_files' =>$total_files,'total_folders' =>$total_folders,'total_size' =>$total_size, ]); }/** * Handles a file list request. * * This method processes a request to view the contents of a specified directory. It validates the input parameters, * checks if the directory exists, and returns the list of files in the directory. If the directory is not found, * it returns an appropriate error response. * * @param ServerRequestInterface $request The server request containing query parameters. * @return ResponseInterface The response containing the list of files in the directory or an error message. * * Query Parameters: * - dir (string, optional): The directory to view. Defaults to the root directory. * - with_md5 (bool, optional): Whether to include the MD5 hash of the files in the response. Defaults to false. * - recursive (bool, optional): Whether to recursively list files in subdirectories. Defaults to false. * * @throws Exception If there is an error during the file request process. */publicfunction_initFileRequest(ServerRequestInterface$request):ResponseInterface {$body =$request->getQueryParams();$requested_dir =$body['dir'] ??null;$with_md5 =$body['with_md5'] ??false;$recursive =$body['recursive'] ??false;if ($requested_dir !==null) {$requested_dir =str_replace('/',DIRECTORY_SEPARATOR,$requested_dir); }$dir =$requested_dir ?$this->dir .DIRECTORY_SEPARATOR .$requested_dir :$this->dir;$show_dir =$requested_dir ?$requested_dir :'root';if (!is_dir($dir)) {return$this->responder->error(404,'Directory not found'); }else {return$this->responder->success(['current_directory' =>$show_dir,'files' =>$this->readFiles($dir,$with_md5,$recursive)]); } }/** * Views a specified file. * * This method handles the viewing of a file from the specified directory. It validates the input parameters, * checks if the file exists, and returns the file as a response for viewing. If the file is not found or * any error occurs, it returns an appropriate error response. * * @param ServerRequestInterface $request The server request containing query parameters. * @return ResponseInterface The response containing the file for viewing or an error message. * * Query Parameters: * - filename (string): The name of the file to be viewed. * - filedir (string, optional): The directory of the file to be viewed. Defaults to the root directory. * * @throws Exception If there is an error during the file viewing process. */publicfunction_initFileView(ServerRequestInterface$request):ResponseInterface {$body =$request->getQueryParams();$filename =$this->sanitizeFilename($body['filename']) ??null;$filedir =$this->sanitizeDir($body['filedir'],true) ??null;if ($filename ===null) {return$this->responder->error(400,'No file specified'); }$filePath =$filedir .DIRECTORY_SEPARATOR .$filename;if (!file_exists($filePath)) {return$this->responder->error(404,'File not found'); }$mimeType =mime_content_type($filePath);$file =file_get_contents($filePath);$response = ResponseFactory::from(200,$mimeType,$file);$response =$response->withHeader('Content-Disposition','inline; filename=' .$filename);$response =$response->withHeader('X-Filename',$filename);return$response; }/** * Handles file upload from the server request. * * @param ServerRequestInterface $request The server request containing the uploaded files. * @return ResponseInterface The response indicating the result of the file upload process. * * The method performs the following steps: * - Retrieves the uploaded files from the request. * - Checks if any file is uploaded, returns an error response if no file is uploaded. * - Parses the request body to get the directory path and compression options. * - Creates the directory if it does not exist. * - Processes each uploaded file: * - Checks for upload errors. * - Verifies memory limit for the file size. * - Sanitizes the filename. * - Verifies the MIME type of the file. * - Checks if the file already exists in the directory. * - If image compression is enabled and the file is an image, compresses the image and saves it as a .webp file. * - Moves the uploaded file to the target directory. * - Collects the result status for each file, including any errors encountered. * - Returns a response with the overall result status, including the number of successfully uploaded files and errors. */publicfunction_initFileUpload(ServerRequestInterface$request):ResponseInterface {$uploadedFiles =$request->getUploadedFiles();$uploadedFiles =$uploadedFiles['file'] ??null;if ($uploadedFiles ===null) {return$this->responder->error(400,'No file uploaded.'); }$body =$request->getParsedBody();$dir =$this->sanitizeDir($body->dir,true);$compress_images =filter_var($body->compress_images ??false,FILTER_VALIDATE_BOOLEAN,FILTER_NULL_ON_FAILURE) ??false;$compress_images_quality =$this->sanitizeQualityValue($body->compress_images_quality) ??80;if ($dir ===null) {return$this->responder->error(400,'Invalid directory specified.'); }if (!is_dir($dir)) {mkdir($dir,0755,true); }if (!is_array($uploadedFiles)) {$uploadedFiles = [$uploadedFiles]; }$result_status = [];$count =0;$total_uploaded_successfully =0;foreach ($uploadedFilesas$uploadedFile) {$count++;if ($uploadedFile->getError() ===UPLOAD_ERR_OK) {if (!$this->checkMemoryLimit($uploadedFile->getSize())) {$result_status[$count] = ['status' =>'ERROR','message' =>'Not enough memory to process file, file not uploaded.','error' =>'Memory limit would be exceeded','file_name' =>$uploadedFile->getClientFilename(), ];continue; }$filename =$this->sanitizeFilename($uploadedFile->getClientFilename());$tmpStream =$uploadedFile->getStream();$tmpPath =$tmpStream->getMetadata('uri');$isAllowed =$this->verifyMimeType($tmpPath);if (!$isAllowed) {$result_status[$count] = ['status' =>'ERROR','message' =>'Error uploading file','error' =>'Invalid file type!','file_name' =>$uploadedFile->getClientFilename(), ];continue; }if($compress_images &&$this->isImage($tmpPath)){$new_filename =$this->convertFileExtension($filename,'webp');if (file_exists($dir .DIRECTORY_SEPARATOR .$new_filename)) {$result_status[$count] = ['status' =>'ERROR','message' =>'Error uploading file','error' =>'File already exists in this directory','file_name' =>$new_filename, ];continue; }if ($this->isImage($tmpPath)) {try {$compressed_image =$this->compressImage($tmpPath,$compress_images_quality);$newFilePath =$dir .DIRECTORY_SEPARATOR .$new_filename;$compressed_image->writeImage($newFilePath);$result_status[$count] = ['compression_image_status' =>'OK','new_file_size' =>$this->formatFileSize(filesize($newFilePath)),'new_file_name' =>$new_filename,'new_file_md5' =>md5_file($newFilePath),'total_savings' =>"-" .$this->formatFileSize(filesize($tmpPath) -filesize($newFilePath)), ]; }catch (Exception$e) {$result_status[$count] = ['compression_image_status' =>'ERROR','message' =>'Error during image compression:' .$e->getMessage(), ]; } }else {$result_status[$count]['compression_image_status'] ="Not compressed, is not an image"; } }else {if (file_exists($dir .DIRECTORY_SEPARATOR .$filename)) {$result_status[$count] = ['status' =>'ERROR','message' =>'Error uploading file','error' =>'File already exists in this directory','file_name' =>$uploadedFile->getClientFilename(), ];continue; }$uploadedFile->moveTo($dir .DIRECTORY_SEPARATOR .$filename);$result_status[$count] = ['status' =>'OK','message' =>'File uploaded successfully','file_name' =>$filename,'file_size' =>$this->formatFileSize($uploadedFile->getSize()),'md5' =>md5_file($dir .DIRECTORY_SEPARATOR .$filename), ]; }$total_uploaded_successfully++; }else {$result_status[$count] = ['status' =>'ERROR','message' =>'Error uploading file','file_name' =>$uploadedFile->getClientFilename(),'error' =>$this::PHP_FILE_UPLOAD_ERRORS[$uploadedFile->getError()], ]; } }$result_status['total_uploaded_successfully'] =$total_uploaded_successfully ."/" .$count;$result_status['total_errors'] =$count -$total_uploaded_successfully;return$this->responder->success($result_status); }/** * Downloads a specified file. * * This method handles the download of a file from the specified directory. It validates the input parameters, * checks if the file exists, and returns the file as a response for download. If the file is not found or * any error occurs, it returns an appropriate error response. * * @param ServerRequestInterface $request The server request containing query parameters. * @return ResponseInterface The response containing the file for download or an error message. * * Query Parameters: * - filename (string): The name of the file to be downloaded. * - filedir (string, optional): The directory of the file to be downloaded. Defaults to the root directory. * * @throws Exception If there is an error during the file download process. */publicfunction_initFileDownload(ServerRequestInterface$request):ResponseInterface {$body =$request->getQueryParams();$filename =$this->sanitizeFilename($body['filename']) ??null;$filedir =$this->sanitizeDir($body['filedir'],true) ??null;if ($filename ===nullor$filename ==="") {return$this->responder->error(400,'No file specified'); }$filePath =$filedir .DIRECTORY_SEPARATOR .$filename;if (!file_exists($filePath)) {return$this->responder->error(404,'File not found'); }$response = ResponseFactory::from(200,'application/octet-stream',file_get_contents($filePath));$response =$response->withHeader('Content-Disposition','attachment; filename=' .$filename);return$response; }/** * Deletes a specified file. * * This method deletes a file in the specified directory. It validates the input parameters, * checks if the file exists, and attempts to delete it. If successful, it returns a success response. * * @param ServerRequestInterface $request The server request containing parsed body parameters. * @return ResponseInterface The response indicating the result of the delete operation. * * Parsed Body Parameters: * - filename (string): The name of the file to be deleted. * - filedir (string, optional): The directory of the file to be deleted. Defaults to the root directory. * * @throws Exception If there is an error during the delete process. */publicfunction_initFileDelete(ServerRequestInterface$request):ResponseInterface {$body =$request->getParsedBody();$filename =$this->sanitizeFilename($body->filename) ??null;$filedir =$this->sanitizeDir($body->filedir) ??null;if ($filename ===null) {return$this->responder->error(400,'No file specified'); }if ($filedir !==null) {$filedir =str_replace('/',DIRECTORY_SEPARATOR,$filedir); }else {$filedir =''; }$filePath =$this->dir .DIRECTORY_SEPARATOR .$filedir .DIRECTORY_SEPARATOR .$filename;if (!file_exists($filePath)) {return$this->responder->error(404,'File [' .$filename .'] not found in this directory, nothing deleted'); }if (!$this->lockFile($filePath)) {return$this->responder->error(500,'Unable to lock file for deletion'); }try {if (!unlink($filePath)) {return$this->responder->error(500,'Error deleting file'); }return$this->responder->success(['message' =>'File [' .$filename .'] deleted successfully']); }finally {$this->unlockFile($filePath); } }/** * Moves a specified file to a new directory. * * This method moves a file from its current directory to a new directory. It validates the input parameters, * checks if the file exists, and attempts to move it. If successful, it returns a success response. * * @param ServerRequestInterface $request The server request containing parsed body parameters. * @return ResponseInterface The response indicating the result of the move operation. * * Parsed Body Parameters: * - filename (string): The name of the file to be moved. * - filedir (string, optional): The current directory of the file. Defaults to the root directory. * - new_dir (string): The new directory to move the file to. * * @throws Exception If there is an error during the move process. */publicfunction_initFileMove(ServerRequestInterface$request):ResponseInterface {$body =$request->getParsedBody();$filename =$this->sanitizeFilename($body->filename) ??null;$filedir =$this->sanitizeDir($body->filedir) ??null;$new_dir =$this->sanitizeDir($body->new_filedir) ??null;if ($filename ===null) {return$this->responder->error(400,'No file specified'); }if ($new_dir ===null) {return$this->responder->error(400,'No new directory specified'); }else {$new_dir =str_replace('/',DIRECTORY_SEPARATOR,$new_dir); }if ($filedir !==null) {$filedir =str_replace('/',DIRECTORY_SEPARATOR,$filedir); }else {$filedir =''; }$filePath =$this->dir .DIRECTORY_SEPARATOR .$filedir .DIRECTORY_SEPARATOR .$filename;$newPath =$this->dir .DIRECTORY_SEPARATOR .$new_dir .DIRECTORY_SEPARATOR .$filename;if (!file_exists($filePath)) {return$this->responder->error(404,'File [' .$filename .'] not found, nothing moved'); }if (file_exists($newPath)) {return$this->responder->error(409,'File [' .$filename .'] already exists in [' .$new_dir .']. Nothing moved.'); }if (!is_dir($this->dir .DIRECTORY_SEPARATOR .$new_dir)) {mkdir($this->dir .DIRECTORY_SEPARATOR .$new_dir,0755,true); }if (!$this->lockFile($filePath)) {return$this->responder->error(500,'Unable to lock source file'); }if (!$this->lockFile($newPath)) {return$this->responder->error(500,'Unable to lock dest file'); }try {if (!rename($filePath,$newPath)) {return$this->responder->error(500,'Error moving file'); }return$this->responder->success(['message' =>'File [' .$filename .'] moved successfully to [' .$new_dir .']']); }finally {$this->unlockFile($filePath);$this->unlockFile($newPath); } }/** * Initializes the file copy process. * * @param ServerRequestInterface $request The server request containing the file details. * @return ResponseInterface The response indicating the result of the file copy operation. * * The function performs the following steps: * 1. Parses the request body to get the filename, current directory, and new directory. * 2. Sanitizes the filename and directory paths. * 3. Validates the presence of the filename and new directory. * 4. Constructs the source and destination file paths. * 5. Checks if the source file exists and if the destination file already exists. * 6. Creates the new directory if it does not exist. * 7. Locks the source and destination files to prevent concurrent access. * 8. Copies the file from the source to the destination. * 9. Unlocks the files after the copy operation. * 10. Returns a success response if the file is copied successfully, or an error response if any step fails. */publicfunction_initFileCopy(ServerRequestInterface$request):ResponseInterface {$body =$request->getParsedBody();$filename =$this->sanitizeFilename($body->filename) ??null;$filedir =$this->sanitizeDir($body->filedir,true) ??null;$new_dir =$this->sanitizeDir($body->new_filedir,true) ??null;if ($filename ===null) {return$this->responder->error(400,'No file specified'); }if ($new_dir ===null) {return$this->responder->error(400,'No new directory specified'); }$filePath =$filedir .DIRECTORY_SEPARATOR .$filename;$newPath =$new_dir .DIRECTORY_SEPARATOR .$filename;if (!file_exists($filePath)) {return$this->responder->error(404,'File [' .$filename .'] not found in ['.$filePath .'], nothing copied'); }if (!is_dir($new_dir)) {mkdir($new_dir,0755,true); }if (file_exists($newPath)) {return$this->responder->error(409,'File [' .$filename .'] already exists in [' .$new_dir .']'); }// Lock only source fileif (!$this->lockFile($filePath)) {return$this->responder->error(500,'Unable to lock source file'); }try {if (!copy($filePath,$newPath)) {return$this->responder->error(500,'Error copying file'); }return$this->responder->success(['message' =>'File [' .$filename .'] copied successfully to [' .$new_dir .']']); }finally {$this->unlockFile($filePath); } }/** * Renames a specified file. * * This method renames a file in the specified directory. It validates the input parameters, * checks if the file exists, and attempts to rename it. If successful, it returns a success response. * * @param ServerRequestInterface $request The server request containing parsed body parameters. * @return ResponseInterface The response indicating the result of the rename operation. * * Parsed Body Parameters: * - filename (string): The current name of the file to be renamed. * - new_filename (string): The new name for the file. * - filedir (string, optional): The directory of the file to be renamed. Defaults to the root directory. * * @throws Exception If there is an error during the renaming process. */publicfunction_initFileRename(ServerRequestInterface$request):ResponseInterface {$body =$request->getParsedBody();$filename =$this->sanitizeFilename($body->filename) ??null;$new_filename =$this->sanitizeFilename($body->new_filename) ??null;$filedir =$this->sanitizeDir($body->filedir) ??'';if ($filename ===null) {return$this->responder->error(400,'No file specified'); }if ($new_filename ===null) {return$this->responder->error(400,'No new filename specified'); }$filePath =$this->dir .DIRECTORY_SEPARATOR .$filedir .DIRECTORY_SEPARATOR .$filename;$newPath =$this->dir .DIRECTORY_SEPARATOR .$filedir .DIRECTORY_SEPARATOR .$new_filename;if (!file_exists($filePath)) {return$this->responder->error(404,'File [' .$filename .'] not found, nothing renamed'); }if (file_exists($newPath)) {return$this->responder->error(409,'File [' .$new_filename .'] already exists in this directory. Nothing renamed.'); }if (!$this->lockFile($filePath)) {return$this->responder->error(500,'Unable to lock source file'); }try {if (!rename($filePath,$newPath)) {return$this->responder->error(500,'Error renaming file'); }return$this->responder->success(['message' =>'File [' .$filename .'] renamed successfully to [' .$new_filename .']']); }finally {$this->unlockFile($newPath); } }/** * Resizes an image to the specified dimension. * * This method checks if the Imagick extension is enabled, validates the input parameters, * and resizes the specified image file to the desired dimension. The resized image * is cached to improve performance for subsequent requests. * * @param ServerRequestInterface $request The server request containing query parameters. * @return ResponseInterface The response containing the resized image or an error message. * * Query Parameters: * - filedir (string): The directory of the file to be resized. * - filename (string): The name of the file to be resized. * - dimension (string): The dimension to resize ('width' or 'height'). * - dimension_value (int): The value of the dimension to resize to. * * @throws ImagickException If there is an error during image resizing. */publicfunction_initImgResize(ServerRequestInterface$request):ResponseInterface {if (!extension_loaded('imagick')) {return$this->responder->error(500,'Imagick extension is not enabled'); }$body =$request->getQueryParams();$filedir =$this->sanitizeDir($body['filedir']) ??null;$filename =$this->sanitizeFilename($body['filename']) ??null;$dimension =$this->sanitizeDimension($body['dimension']) ??null;$dimension_value =$this->sanitizeDimensionValue($body['dimension_value']) ??null;if ($filedir !==null) {$filedir =str_replace('/',DIRECTORY_SEPARATOR,$filedir); }else {$filedir =''; }if ($filename ===null) {return$this->responder->error(400,'No file specified'); }if ($dimension ===null) {return$this->responder->error(400,'No valid dimension specified'); }if ($dimension_value ===null) {return$this->responder->error(400,'No dimension value specified'); }$filePath =$this->dir .DIRECTORY_SEPARATOR .$filedir .DIRECTORY_SEPARATOR .$filename;if (!file_exists($filePath)) {return$this->responder->error(404,'File [' .$filename .'] not found, nothing resized'); }if (!$this->isImage($filePath)) {return$this->responder->error(400,'File is not an image'); }$fileHash =md5_file($filePath);$cacheKey ="resize_{$filename}_{$dimension}_{$dimension_value}_{$fileHash}";if ($this->cache->get($cacheKey)) {$imageData =$this->cache->get($cacheKey); }else {try {$resized_img =$this->resizeImage($filePath,$dimension,$dimension_value);$imageData =$resized_img->getImageBlob();$this->cache->set($cacheKey,$imageData); }catch (ImagickException$e) {return$this->responder->error(500,'Error resizing image:' .$e->getMessage()); } }$response = ResponseFactory::from(200,'image',$imageData);$response =$response->withHeader('Content-Length',strlen($imageData));$response =$response->withHeader('Content-Disposition','inline; filename=' .$filename);return$response; }/** * Initializes image compression. * * This method checks if the Imagick extension is enabled, validates the input parameters, * and compresses the specified image file to the desired quality. The compressed image * is cached to improve performance for subsequent requests. * * @param ServerRequestInterface $request The server request containing query parameters. * @return ResponseInterface The response containing the compressed image or an error message. * * Query Parameters: * - filedir (string): The directory of the file to be compressed. * - filename (string): The name of the file to be compressed. * - quality (int): The quality of the compressed image (default is 80). * * @throws ImagickException If there is an error during image compression. */publicfunction_initImgCompress(ServerRequestInterface$request):ResponseInterface {if (!extension_loaded('imagick')) {return$this->responder->error(500,'Imagick extension is not enabled'); }$body =$request->getQueryParams();$filedir =$this->sanitizeDir($body['filedir']) ??'';$filename =$this->sanitizeFilename($body['filename']) ??null;$quality =$this->sanitizeQualityValue($body['quality']) ??80;if ($filename ===null) {return$this->responder->error(400,'No file specified'); }$filePath =$this->dir .DIRECTORY_SEPARATOR .$filedir .DIRECTORY_SEPARATOR .$filename;$fileHash =md5_file($filePath);$cacheKey ="compress_{$filename}_{$quality}_{$fileHash}";if (!file_exists($filePath)) {return$this->responder->error(404,'File [' .$filename .'] not found in this directory, nothing compressed'); }if (!$this->isImage($filePath)) {return$this->responder->error(400,'File is not an image'); }if ($this->cache->get($cacheKey)) {$imageData =$this->cache->get($cacheKey); }else {try {$compressed_img =$this->compressImage($filePath,$quality);$imageData =$compressed_img->getImageBlob();$this->cache->set($cacheKey,$imageData); }catch (ImagickException$e) {return$this->responder->error(500,'Error compressing image:' .$e->getMessage()); } }$response = ResponseFactory::from(200,'image/webp',$imageData);$response =$response->withHeader('Content-Length',strlen($imageData));$response =$response->withHeader('Content-Disposition','inline; filename=' .$filename);return$response; }/** * Initializes the limits for file uploads based on server configuration. * * This method calculates the maximum file upload size by taking the minimum value * between 'upload_max_filesize' and 'post_max_size' from the PHP configuration. * It then returns a response with the maximum size in bytes, a formatted version * of the maximum size, and a list of allowed MIME types. * * @param ServerRequestInterface $request The server request instance. * @return ResponseInterface The response containing the upload limits and allowed MIME types. */publicfunction_initLimits(ServerRequestInterface$request):ResponseInterface {$maxBytes =min($this->convertToBytes(ini_get('upload_max_filesize')),$this->convertToBytes(ini_get('post_max_size')) );return$this->responder->success(['max_size' =>$maxBytes,'max_size_formatted' =>$this->formatFileSize($maxBytes),'mime_types' =>$this::MIME_WHITE_LIST, ]); }/** * Validates the default directory path. * * This method performs several checks to ensure that the default directory path is valid: * - Checks if the path is empty. * - Attempts to create the directory if it does not exist. * - Verifies that the path is a directory. * - Checks if the directory is readable and writable. * - Attempts to write and delete a test file in the directory. * * @return bool|ResponseInterface Returns true if the directory is valid, otherwise returns an error response. */publicfunctionvalidateDefaultDir():bool |ResponseInterface {// Check if the path is emptyif (empty($this->dir)) {return$this->responder->error(403,'The default directory path cannot be empty. Config one first.'); }$minRequiredSpace =$this::MIN_REQUIRED_DISK_SPACE;$freeSpace =disk_free_space($this->dir);if ($freeSpace ===false) {return$this->responder->error(500,"Cannot determine free space on disk."); }if ($freeSpace <$minRequiredSpace) {return$this->responder->error(500,sprintf("Insufficient disk space. At least %s required, %s available",$this->formatFileSize($minRequiredSpace),$this->formatFileSize($freeSpace) )); }// If the directory does not exist, try to create itif (!file_exists($this->dir)) {try {if (!mkdir($this->dir,0755,true)) {return$this->responder->error(403,"Unable to create the default directory:" .$this->dir); }// Check that the permissions have been set correctlychmod($this->dir,0755); }catch (Exception$e) {return$this->responder->error(500,"Error creating the default directory:" .$e->getMessage()); } }// Check that it is a directoryif (!is_dir($this->dir)) {return$this->responder->error(403,"The default dir path exists but is not a directory:" .$this->dir); }// Check permissionsif (!is_readable($this->dir)) {return$this->responder->error(403,"The default directory is not readable:" .$this->dir); }if (!is_writable($this->dir)) {return$this->responder->error(403,"The default directory is not writable:" .$this->dir); }// Check if we can actually write a test file$testFile =$this->dir .DIRECTORY_SEPARATOR .'.write_test';try {if (file_put_contents($testFile,'') ===false) {return$this->responder->error(403,"Unable to write to the default directory."); }unlink($testFile); }catch (Exception$e) {return$this->responder->error(500,"Write test failed on default directory:" .$e->getMessage()); }if (!$this->generateSecurityServerFile()) {return$this->responder->error(500,"Error generating security file in the default directory."); }returntrue; }privatefunctiongenerateSecurityServerFile():bool {$serverSoftware =strtolower($_SERVER['SERVER_SOFTWARE'] ??'');try {if (strpos($serverSoftware,'apache') !==false) {return$this->generateApacheSecurityFile(); }elseif (strpos($serverSoftware,'nginx') !==false) {return$this->generateNginxSecurityFile(); }return$this->generateApacheSecurityFile(); }catch (Exception$e) {returnfalse; } }privatefunctiongenerateApacheSecurityFile():bool {$securityFile =__DIR__ .DIRECTORY_SEPARATOR .'.htaccess';$newContent ="# BEGIN PHP CRUD API FILE MANAGER\n" .'<Directory "/' .$this::UPLOAD_FOLDER_NAME .'">' ."\n" .' Options -Indexes' ."\n" .' Order deny,allow' ."\n" .' Deny from all' ."\n" .'</Directory>' ."\n" ."# END PHP CRUD API FILE MANAGER";return$this->appendConfigIfNotExists($securityFile,$newContent); }privatefunctiongenerateNginxSecurityFile():bool {$securityFile =__DIR__ .DIRECTORY_SEPARATOR .'nginx.conf';$newContent ="# BEGIN PHP CRUD API FILE MANAGER\n" .'location /' .$this::UPLOAD_FOLDER_NAME .' {' ."\n" .' deny all;' ."\n" .' autoindex off;' ."\n" .'}' ."\n" ."# END PHP CRUD API FILE MANAGER";return$this->appendConfigIfNotExists($securityFile,$newContent); }privatefunctionappendConfigIfNotExists(string$filePath,string$newContent):bool {if (file_exists($filePath)) {$currentContent =file_get_contents($filePath);if (strpos($currentContent,$newContent) !==false) {returntrue;// Configuration already exists }returnfile_put_contents($filePath,$currentContent ."\n" .$newContent) !==false; }returnfile_put_contents($filePath,$newContent) !==false; }/** * Reads the files in the specified directory and returns an array of file information. * * @param string $dir The directory to read files from. If null, the default directory will be used. * @param bool $with_md5 Whether to include the MD5 hash of the files in the returned array. * @param bool $recursive Whether to read files recursively from subdirectories. * @return array An array of file information. Each file information includes: * - name: The name of the file. * - type: The MIME type of the file. * - path: The web path to the file. * - size: The formatted size of the file (only for files, not directories). * - created_on: The creation date of the file. * - modified_on: The last modified date of the file. * - md5: The MD5 hash of the file (if $with_md5 is true). * - files: An array of files within the directory (if the file is a directory). * @throws Exception If the directory cannot be opened. */publicfunctionreadFiles($dir,$with_md5,$recursive):array {$dir =$dir ??$this->dir;if (!is_dir($dir)) {return ["Error: dir requested not found"]; }$files = [];$current_dir = @opendir($dir);if ($current_dir ===false) {thrownewException("Impossibile aprire la directory:{$dir}"); }$isEmpty =true;while (($file =readdir($current_dir)) !==false) {if ($file ==='.' ||$file ==='..') {continue; }$isEmpty =false;$filePath =$dir .DIRECTORY_SEPARATOR .$file;$viewWebPath =$this->getPublicUrl($file,'view',$dir);$downloadWebPath =$this->getPublicUrl($file,'download',$dir);try {$size =filesize($filePath);$formattedSize =$this->formatFileSize($size);// Get MIME type$mimeType =mime_content_type($filePath) ?:'application/octet-stream';if (is_dir($filePath)) {$files[] = ['name' =>$file,'type' =>$mimeType,'created_on' =>date('Y-m-d H:i:s',filectime($filePath)),'modified_on' =>date('Y-m-d H:i:s',filemtime($filePath)),'files' =>$recursive ?$this->readFiles($filePath,$with_md5,$recursive) :'Request recursivity to view files', ]; }else {$fileData = ['name' =>$file,'type' =>$mimeType,'view_url' =>$viewWebPath,'download_url' =>$downloadWebPath,'size' =>$formattedSize,'created_on' =>date('Y-m-d H:i:s',filectime($filePath)),'modified_on' =>date('Y-m-d H:i:s',filemtime($filePath)), ];if ($with_md5) {$fileData['md5'] =md5_file($filePath); }$files[] =$fileData; } }catch (Exception$e) {continue;// Skip files causing errors } }closedir($current_dir);if ($isEmpty) {return ["0: Empty directory"]; }sort($files);return$files; }/** * Formats a file size in bytes to a human-readable format. * * @param int $size The file size in bytes. * @return string The formatted file size. */publicfunctionformatFileSize(int$size):string {$units = ['bytes','KB','MB','GB'];$power =$size >0 ?floor(log($size,1024)) :0;$formattedSize =number_format($size /pow(1024,$power),2) .'' .$units[$power];return$formattedSize; }/** * Resizes an image to the specified dimension. * * @param string $img_src The source path of the image to be resized. * @param string $dimension The dimension to resize ('width' or 'height'). * @param int $dimension_value The value of the dimension to resize to. * @return bool|Imagick|ResponseInterface Returns the resized Imagick object on success, false on failure, or a ResponseInterface on invalid dimension. * @throws ImagickException If an error occurs during image processing. */publicfunctionresizeImage($img_src,$dimension,$dimension_value):bool |Imagick |ResponseInterface {try {// Crea un nuovo oggetto Imagick$image =newImagick($img_src);// Ottieni le dimensioni originali dell'immagine$originalWidth =$image->getImageWidth();$originalHeight =$image->getImageHeight();// Calcola le nuove dimensioniif ($dimension =='width') {$newWidth =ceil($dimension_value);$newHeight =ceil(($originalHeight /$originalWidth) *$newWidth); }elseif ($dimension =='height') {$newHeight =ceil($dimension_value);$newWidth =ceil(($originalWidth /$originalHeight) *$newHeight); }else {return$this->responder->error(400,'Invalid dimension specified'); }// Ridimensiona l'immagine$image->resizeImage($newWidth,$newHeight, Imagick::FILTER_LANCZOS,1);return$image; }catch (ImagickException$e) {echo"Errore:" .$e->getMessage();returnfalse; } }/** * Compresses an image by reducing its quality and converting it to the WebP format. * * @param string $img_src The path to the source image file. * @param int|string $quality The quality level for the compressed image (default is 80). * @return bool|Imagick Returns the compressed Imagick object on success, or false on failure. * @throws ImagickException If an error occurs during image processing. */publicfunctioncompressImage($img_src,$quality ='80'):bool |Imagick {try {$image =newImagick($img_src);$image->stripImage();$image->setImageCompressionQuality($quality);$image->setImageFormat('webp');return$image; }catch (ImagickException$e) {echo"Errore:" .$e->getMessage();returnfalse; } }/** * Checks if the given file path points to a valid image. * * @param string $filePath The path to the file to check. * @return bool True if the file is an image, false otherwise. */publicfunctionisImage($filePath):bool {$imageInfo = @getimagesize($filePath);if ($imageInfo ===false) {returnfalse; }$mimeType =$imageInfo['mime'];if (strpos($mimeType,'image/') !==0) {returnfalse; }returntrue; }/** * Convert a shorthand byte value from a PHP configuration directive to an integer value. * * @param string $value The shorthand byte value (e.g., '2M', '512K'). * @return int The byte value as an integer. */privatefunctionconvertToBytes(string$val):int {if (empty($val)) {return0; }$val =trim($val);$last =strtolower($val[strlen($val) -1]);$multiplier =1;switch ($last) {case'g':$multiplier =1024 *1024 *1024;break;case'm':$multiplier =1024 *1024;break;case'k':$multiplier =1024;break;default:if (!is_numeric($last)) {$val =substr($val,0, -1); }break; }returnmax(0, (int)$val *$multiplier); }/** * Generates a public URL for a specified file. * * @param string|null $dir The directory of the file (optional). * @param string $filename The name of the file. * @param string $type The type of operation (default 'view'). * @return string The generated public URL. */privatefunctiongetPublicUrl(string$filename,string$type ='view', ?string$dir =null):string {$base =$_SERVER['HTTP_HOST'] .$_SERVER['SCRIPT_NAME'];$publicPath =$base .$this::ENDPOINT .'/' .$type .'?filename=' .urlencode($filename);if ($dir !==null) {$dir =str_replace(DIRECTORY_SEPARATOR,'/',$dir);$pos =strpos($dir,$this::UPLOAD_FOLDER_NAME);if ($pos !==false) {$dir =substr($dir,$pos +strlen($this::UPLOAD_FOLDER_NAME)); }if ($dir !=='') {$publicPath .='&filedir=' .urlencode($dir); } }return$publicPath; }/** * Sanitize a directory path to ensure it is safe and valid. * * This method normalizes directory separators, removes unsafe characters, * and ensures the path does not traverse outside the root directory. * * @param string|null $path The directory path to sanitize. If null or empty, returns the root directory. * @param bool $full Whether to return the full path or just the sanitized relative path. * @return string The sanitized directory path. If the path is invalid, returns the root directory or null. */privatefunctionsanitizeDir(?string$path,bool$full =false):string {// Input validationif ($path ===null ||trim($path) ==='') {return$full ?$this->dir .DIRECTORY_SEPARATOR :null; }// Normalize separators and remove leading/trailing spaces$path =trim(str_replace(['\\','/'],DIRECTORY_SEPARATOR,$path));// Remove directory traversal sequences$path =preg_replace('/\.{2,}/','',$path);// Keep only safe characters for directory names// [a-zA-Z0-9] - alphanumeric characters// [\-\_] - dashes and underscores// [\s] - spaces// [' . preg_quote(DIRECTORY_SEPARATOR) . '] - directory separator$path =preg_replace('/[^a-zA-Z0-9\-\_\s' .preg_quote(DIRECTORY_SEPARATOR) .']/u','',$path);// Remove multiple consecutive separators$path =preg_replace('/' .preg_quote(DIRECTORY_SEPARATOR) .'{2,}/',DIRECTORY_SEPARATOR,$path);// Remove leading/trailing separators$path =trim($path,DIRECTORY_SEPARATOR);// Build full path$fullPath =$this->dir .DIRECTORY_SEPARATOR .$path;// Verify path does not escape the rootif (strpos($fullPath,$this->dir) !==0) {return$full ?$this->dir .DIRECTORY_SEPARATOR :null; }return$full ?$fullPath :$path; }privatefunctionsanitizeFilename($filename):array |string |null {if ($filename ===null) {returnnull; }else {strval($filename); }$filename =preg_replace('/[^a-zA-Z0-9\-\_\.\s]/','',$filename);return$filename; }privatefunctionsanitizeDimension($dimension):string |null {$dimension =strval($dimension);$dimension =strtolower($dimension);returnin_array($dimension, ['width','height']) ?$dimension :null; }privatefunctionsanitizeDimensionValue($dimension_value):int |null {$dimension_value =intval($dimension_value);$formatted =filter_var($dimension_value,FILTER_VALIDATE_INT, ['options' => ['min_range' =>1]] );return$formatted !==false ?$formatted :null; }privatefunctionsanitizeQualityValue($quality_value):int |null {$quality_value =intval($quality_value);$formatted =filter_var($quality_value,FILTER_VALIDATE_INT, ['options' => ['min_range' =>1,'max_range' =>100]] );return$formatted !==false ?$formatted :null; }privatefunctionverifyMimeType($filepath):bool {$finfo =finfo_open(FILEINFO_MIME_TYPE);$mimeType =finfo_file($finfo,$filepath);finfo_close($finfo);return$this->isMimeTypeAllowed($mimeType); }privatefunctionisMimeTypeAllowed(string$mimeType):bool {foreach ($this::MIME_WHITE_LISTas$allowedType) {$pattern ='#^' .str_replace('*','.*',$allowedType) .'$#';if (preg_match($pattern,$mimeType)) {returntrue; } }returnfalse; }/** * Checks if there is enough memory available to process a file of the given size. * * @param int $fileSize The size of the file in bytes * @return bool True if there is enough memory, false otherwise */privatefunctioncheckMemoryLimit(int$fileSize):bool {$memoryLimit =$this->convertToBytes(ini_get('memory_limit'));$currentMemory =memory_get_usage();$neededMemory =$fileSize *2.2;// Factor 2.2 for safe marginreturn ($currentMemory +$neededMemory) <$memoryLimit; }/** * Locks a file for exclusive access. * * @param string $path The path to the file to lock. * @return bool True if the file was successfully locked, false otherwise. */privatefunctionlockFile(string$path):bool {$fileHandle =fopen($path,'r+');if ($fileHandle ===false) {returnfalse; }if (!flock($fileHandle,LOCK_EX)) {fclose($fileHandle);returnfalse; }returntrue; }/** * Unlocks a file. * * @param string $path The path to the file to unlock. * @return bool True if the file was successfully unlocked, false otherwise. */privatefunctionunlockFile(string$path):bool {$fileHandle =fopen($path,'r+');if ($fileHandle ===false) {returnfalse; }$result =flock($fileHandle,LOCK_UN);fclose($fileHandle);return$result; }/** * Converts the file extension of a given filename to a new extension. * * @param string $filename The name of the file whose extension is to be changed. * @param string $newExtension The new extension to be applied to the file. * @return string The filename with the new extension. */privatefunctionconvertFileExtension(string$filename,string$newExtension):string {$pathInfo =pathinfo($filename);return$pathInfo['filename'] .'.' .$newExtension; }}}