* @git Joomla Component Builder * @copyright Copyright (C) 2015 Vast Development Method. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace VDM\Joomla\Componentbuilder\File; use Joomla\CMS\Factory; use Joomla\CMS\User\User; use Joomla\CMS\Language\Text; use Joomla\Filesystem\File; use VDM\Joomla\Interfaces\Data\ItemInterface as Item; use VDM\Joomla\Interfaces\Data\ItemsInterface as Items; use VDM\Joomla\Data\Guid; use VDM\Joomla\Componentbuilder\File\Type; use VDM\Joomla\Componentbuilder\File\Handler; use VDM\Joomla\Componentbuilder\File\Image; use VDM\Joomla\Utilities\MimeHelper; /** * File Manager Class * * @since 5.0.2 */ class Manager { /** * The Globally Unique Identifier. * * @since 5.0.2 */ use Guid; /** * The Item Class. * * @var Item * @since 5.0.2 */ protected Item $item; /** * The Items Class. * * @var Items * @since 5.0.2 */ protected Items $items; /** * The Type Class. * * @var Type * @since 5.0.2 */ protected Type $type; /** * The Handler Class. * * @var Handler * @since 5.0.2 */ protected Handler $handler; /** * The Image Class. * * @var Image * @since 5.1.1 */ protected Image $image; /** * The active user * * @var User * @since 5.0.2 */ protected User $user; /** * Table Name * * @var string * @since 5.0.2 */ protected string $table = 'file'; /** * Constructor. * * @param Item $item The Item Class. * @param Items $items The Items Class. * @param Type $type The Type Class. * @param Handler $handler The Handler Class. * @param Image $image The Image Class. * * @since 5.0.2 */ public function __construct(Item $item, Items $items, Type $type, Handler $handler, Image $image) { $this->item = $item; $this->items = $items; $this->type = $type; $this->handler = $handler; $this->image = $image; $this->user = Factory::getApplication()->getIdentity(); } /** * Upload a file, of a given file type and link it to an entity. * * @param string $guid The file type guid * @param string $entity The entity guid * @param string $target The target entity name * * @return void * @throws \InvalidArgumentException If the file type is not valid. * @throws \RuntimeException If there is an error during file upload. * @since 5.0.2 */ public function upload(string $guid, string $entity, string $target): void { if (($fileType = $this->type->load($guid, $target)) === null) { throw new \InvalidArgumentException(Text::sprintf('COM_COMPONENTBUILDER_FILE_TYPE_NOT_VALID_IN_S_AREA', $target)); } // make sure the user have permissions to upload this file type if (!in_array($fileType['access'], $this->user->getAuthorisedViewLevels())) { throw new \InvalidArgumentException(Text::sprintf('COM_COMPONENTBUILDER_YOU_DO_NOT_HAVE_PERMISSIONS_TO_UPLOAD_S', $fileType['name'])); } $details = $this->handler ->setEnqueueError(false) ->setLegalFormats($fileType['formats']) ->getFile( $fileType['field'], // The input field name $fileType['type'], // The file type $fileType['filter'], // The filter to use when uploading the file $fileType['path'] // The path to the directory where the file must be placed ); if ($details === null) { // Throw an exception if file details couldn't be retrieved throw new \RuntimeException($this->handler->getErrors()); } if ($fileType['type'] === 'image') { $this->processImages($details, $guid, $entity, $target, $fileType); return; } // store file in the file table $this->item->table($this->getTable())->set( $this->modelFileDetails($details, $guid, $entity, $target, $fileType) ); } /** * Get the file details for download * * @param string $guid The file guid * * @return array|null * @since 5.0.2 */ public function download(string $guid): ?array { if (($file = $this->item->table($this->getTable())->get($guid)) !== null && in_array($file->access, $this->user->getAuthorisedViewLevels())) { return (array) $file; } return null; } /** * Delete a file. * * @param string $guid The file guid * * @return void * @since 5.0.2 */ public function delete(string $guid): void { if (($file = $this->item->table($this->getTable())->get($guid)) !== null && in_array($file->access, $this->user->getAuthorisedViewLevels())) { $this->item->table($this->getTable())->delete($guid); // from DB if (is_file($file->file_path) && is_writable($file->file_path)) { File::delete($file->file_path); // from file system } } } /** * Set the current active table * * @param string $table The table that should be active * * @return self * @since 5.0.2 */ public function table(string $table): self { $this->table = $table; return $this; } /** * Get the current active table * * @return string * @since 5.0.2 */ public function getTable(): string { return $this->table; } /** * Process the image(s) as needed based on crop settings * * @param array $details The uploaded file details. * @param string $guid The file type guid * @param string $entity The entity guid * @param string $target The target entity name * @param array $fileType The file type * * @return void * @since 5.1.1 */ protected function processImages(array $details, string $guid, string $entity, string $target, array $fileType): void { if (empty($fileType['crop'])) { // store file in the file table $this->item->table($this->getTable())->set( $this->modelFileDetails($details, $guid, $entity, $target, $fileType) ); return; } $source = $details['full_path']; $path = $details['path']; $cropping = $fileType['crop']; $placeholders = [ '{number}' => $this->getFileNumber($fileType, $entity), '{name}' => $this->getFileName($details, $entity), '{extension}' => $this->getFileExtension($source) ]; foreach ($cropping as &$crop) { $crop['name'] = str_replace(array_keys($placeholders), array_values($placeholders), $crop['name']); } $images = $this->image->process($source, $path, $cropping); foreach($images as $image) { if (empty($image)) { continue; } $details['name'] = $image['name']; $details['extension'] = $image['extension']; $details['size'] = $image['size']; $details['mime'] = $image['mime']; $details['full_path'] = $image['path']; // store file in the file table $this->item->table($this->getTable())->set( $this->modelFileDetails($details, $guid, $entity, $target, $fileType) ); } // clean up source image if (is_file($source) && is_writable($source)) { File::delete($source); // from file system } } /** * model the file details to store in the file table * * @param array $details The uploaded file details. * @param string $guid The file type guid * @param string $entity The entity guid * @param string $target The target entity name * @param array $fileType The file type * * @return object * @since 5.0.2 */ protected function modelFileDetails(array $details, string $guid, string $entity, string $target, array $fileType): object { return (object) [ 'name' => $details['name'], 'file_type' => $guid, 'extension' => $details['extension'] ?? 'error', 'size' => $details['size'] ?? 0, 'mime' => $details['mime'] ?? '', 'file_path' => $details['full_path'], 'entity_type' => $target, 'entity' => $entity, 'access' => $fileType['download_access'] ?? 1, 'guid' => $this->getGuid('guid'), 'created_by' => $this->user->id ]; } /** * Get the file name without extension for download. * * If the original name is empty, return the entity GUID. * If the name does not contain a '.', return the name as is. * Otherwise, return the name without the final extension. * * @param array $details The uploaded file details. * @param string $entity The entity GUID used as fallback. * * @return string The extracted or fallback file name. * @since 5.1.1 */ protected function getFileName(array $details, string $entity): string { // Check if name is set and non-empty $name = trim($details['name'] ?? ''); // Return entity if name is empty if ($name === '') { return $entity; } // If there is no dot in the name, assume no extension — return as-is if (strpos($name, '.') === false) { return $name; } // Use pathinfo to extract the name without extension $info = pathinfo($name); // Return filename (without extension) return $info['filename'] ?? $name; } /** * Get the file number TODO: not ideal, if images are deleted we need a better solution * * @param array $fileType The uploaded file type details. * @param string $entity The entity guid * * @return int * @since 5.1.1 */ protected function getFileNumber(array $fileType, string $entity): int { if (empty($fileType['crop'])) { return 1; } $number = count($fileType['crop']); $number_files = 1; if (($files = $this->items->table($this->getTable())->values([$entity], 'entity')) !== null) { $total = count($files); if ($total >= $number) { $number_files = round($total / $number); } return ++$number_files; } return $number_files; } /** * Get the file extension * * @param sring $source The full path to the file * * @return string * @since 5.1.1 */ protected function getFileExtension(string $source): string { return MimeHelper::extension($source); } }