From f60604db6087e2371d3bdf24d26782c80d82e598 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:31:21 +0200 Subject: [PATCH 1/5] Use attribute `ReadonlyModelProperty` to control `Model::set()` write access --- .../src/Model/Attributes/ReadonlyModelProperty.php | 10 ++++++++++ formwork/src/Model/Model.php | 12 ++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 formwork/src/Model/Attributes/ReadonlyModelProperty.php diff --git a/formwork/src/Model/Attributes/ReadonlyModelProperty.php b/formwork/src/Model/Attributes/ReadonlyModelProperty.php new file mode 100644 index 000000000..13b1350d7 --- /dev/null +++ b/formwork/src/Model/Attributes/ReadonlyModelProperty.php @@ -0,0 +1,10 @@ +isPromoted()) { + if ($this->isReadonly($key)) { + throw new BadMethodCallException(sprintf('Cannot set readonly model property %s::$%s', static::class, $key)); + } + // If defined use a setter if (method_exists($this, $setter = 'set' . ucfirst($key))) { $this->{$setter}($value); @@ -140,4 +146,10 @@ public function data(): array { return $this->data; } + + private function isReadonly(string $property): bool + { + $attributes = (new ReflectionProperty($this, $property))->getAttributes(ReadonlyModelProperty::class, ReflectionAttribute::IS_INSTANCEOF); + return $attributes !== []; + } } From 57c7798976747313926b50dfc01a34e8d97789e1 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:39:20 +0200 Subject: [PATCH 2/5] Implement file metadata --- formwork/config/system.yaml | 1 + formwork/src/Files/File.php | 32 +++++++++++++++++++++++++++++++- formwork/src/Images/Image.php | 12 +++++++++--- formwork/src/Pages/Page.php | 9 +++++++-- panel/translations/de.yaml | 2 ++ panel/translations/en.yaml | 2 ++ panel/translations/es.yaml | 2 ++ panel/translations/fr.yaml | 2 ++ panel/translations/it.yaml | 2 ++ panel/translations/pt.yaml | 2 ++ panel/translations/ru.yaml | 2 ++ site/schemes/files/file.yaml | 1 + site/schemes/files/image.yaml | 15 +++++++++++++++ 13 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 site/schemes/files/file.yaml create mode 100644 site/schemes/files/image.yaml diff --git a/formwork/config/system.yaml b/formwork/config/system.yaml index 080db0d00..609b3abbf 100644 --- a/formwork/config/system.yaml +++ b/formwork/config/system.yaml @@ -43,6 +43,7 @@ fields: files: allowedExtensions: [] + metadataExtension: .meta.yaml images: jpegQuality: 85 diff --git a/formwork/src/Files/File.php b/formwork/src/Files/File.php index c30deedcc..7e80ac313 100644 --- a/formwork/src/Files/File.php +++ b/formwork/src/Files/File.php @@ -2,49 +2,64 @@ namespace Formwork\Files; +use Formwork\App; use Formwork\Data\Contracts\Arrayable; use Formwork\Files\Exceptions\FileUriGenerationException; +use Formwork\Model\Attributes\ReadonlyModelProperty; +use Formwork\Model\Model; +use Formwork\Parsers\Yaml; use Formwork\Utils\FileSystem; use Formwork\Utils\MimeType; use Formwork\Utils\Str; use RuntimeException; use Stringable; -class File implements Arrayable, Stringable +class File extends Model implements Arrayable, Stringable { + protected const MODEL_IDENTIFIER = 'file'; + + protected const SCHEME_IDENTIFIER = 'files.file'; + /** * File name */ + #[ReadonlyModelProperty] protected string $name; /** * File extension */ + #[ReadonlyModelProperty] protected string $extension; /** * File MIME type */ + #[ReadonlyModelProperty] protected string $mimeType; /** * File type in a human-readable format */ + #[ReadonlyModelProperty] protected ?string $type = null; /** * File size in a human-readable format */ + #[ReadonlyModelProperty] protected string $size; /** * File last modified time */ + #[ReadonlyModelProperty] protected int $lastModifiedTime; /** * File hash */ + #[ReadonlyModelProperty] protected string $hash; protected FileUriGenerator $uriGenerator; @@ -58,6 +73,7 @@ public function __construct(protected string $path) { $this->name = basename($path); $this->extension = FileSystem::extension($path); + $this->loadData(); } public function __toString(): string @@ -190,6 +206,20 @@ public function toArray(): array ]; } + private function loadData(): void + { + $app = App::instance(); + + $this->scheme = $app->schemes()->get(static::SCHEME_IDENTIFIER); + $this->fields = $this->scheme->fields(); + + $metadataFile = $this->path . $app->config()->get('system.files.metadataExtension'); + + $this->data = FileSystem::exists($metadataFile) ? Yaml::parseFile($metadataFile) : []; + + $this->fields->setValues($this->data); + } + /** * Match MIME type with an array of extensions * diff --git a/formwork/src/Images/Image.php b/formwork/src/Images/Image.php index 1f96e7dac..a1beb46db 100644 --- a/formwork/src/Images/Image.php +++ b/formwork/src/Images/Image.php @@ -31,6 +31,7 @@ use Formwork\Images\Transform\Sharpen; use Formwork\Images\Transform\Smoothen; use Formwork\Images\Transform\TransformCollection; +use Formwork\Model\Attributes\ReadonlyModelProperty; use Formwork\Utils\FileSystem; use Formwork\Utils\MimeType; use Formwork\Utils\Uri; @@ -38,16 +39,20 @@ class Image extends File { - protected string $path; + protected const MODEL_IDENTIFIER = 'image'; + protected const SCHEME_IDENTIFIER = 'files.image'; + + #[ReadonlyModelProperty] protected AbstractHandler $handler; + #[ReadonlyModelProperty] protected ImageInfo $info; + #[ReadonlyModelProperty] protected TransformCollection $transforms; - protected string $mimeType; - + #[ReadonlyModelProperty] protected ?string $type = 'image'; /** @@ -312,6 +317,7 @@ public function process(?string $mimeType = null, bool $forceCache = false): Ima } $image = new Image($path, $this->options); + $image->data = $this->data; $image->uriGenerator = $this->uriGenerator; $image->transforms = $this->transforms; $image->handler = $this->handler; diff --git a/formwork/src/Pages/Page.php b/formwork/src/Pages/Page.php index f1eeca316..3d7998e3a 100644 --- a/formwork/src/Pages/Page.php +++ b/formwork/src/Pages/Page.php @@ -568,8 +568,13 @@ protected function loadFiles(): void $languages[] = $language; } } - } elseif (in_array($extension, $config->get('system.files.allowedExtensions'), true)) { - $files[] = App::instance()->getService(FileFactory::class)->make(FileSystem::joinPaths($this->path, $file)); + } else { + if (Str::endsWith($file, $config->get('system.files.metadataExtension'))) { + continue; + } + if (in_array($extension, $config->get('system.files.allowedExtensions'), true)) { + $files[] = App::instance()->getService(FileFactory::class)->make(FileSystem::joinPaths($this->path, $file)); + } } } } diff --git a/panel/translations/de.yaml b/panel/translations/de.yaml index b88a94949..602585fd0 100644 --- a/panel/translations/de.yaml +++ b/panel/translations/de.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: Die Seite existiert nicht oder die Anfr panel.errors.error.notFound.heading: Oops, Seite nicht gefunden! panel.errors.error.notFound.status: Nicht gefunden panel.files.actions: Aktionen +panel.files.metadata: Metadaten +panel.files.metadata.alternativeText: Alternativtext panel.files.viewAsList: Als Liste anzeigen panel.files.viewAsThumbnails: Als Miniaturansichten anzeigen panel.login.attempt.failed: Anmeldeversuch fehlgeschlagen! Versuchen Sie es erneut. diff --git a/panel/translations/en.yaml b/panel/translations/en.yaml index d2476db9e..0fff3ad3f 100644 --- a/panel/translations/en.yaml +++ b/panel/translations/en.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: The page does not exist or the request panel.errors.error.notFound.heading: Oops, page not found! panel.errors.error.notFound.status: Not found panel.files.actions: Actions +panel.files.metadata: Metadata +panel.files.metadata.alternativeText: Alternative text panel.files.viewAsList: View as list panel.files.viewAsThumbnails: View as thumbnails panel.login.attempt.failed: Login attempt failed! Try again. diff --git a/panel/translations/es.yaml b/panel/translations/es.yaml index 6cb3a0aa7..0eb290cf2 100644 --- a/panel/translations/es.yaml +++ b/panel/translations/es.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: La página no existe o la solicitud no panel.errors.error.notFound.heading: ¡Ups, página no encontrada! panel.errors.error.notFound.status: No encontrado panel.files.actions: Acciones +panel.files.metadata: Metadatos +panel.files.metadata.alternativeText: Texto alternativo panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: ¡Intento de inicio de sesión fallido! Intenta de nuevo. diff --git a/panel/translations/fr.yaml b/panel/translations/fr.yaml index a34ff5f40..1de8336cc 100644 --- a/panel/translations/fr.yaml +++ b/panel/translations/fr.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: La page n’existe pas ou la demande n panel.errors.error.notFound.heading: Oups, page non trouvée! panel.errors.error.notFound.status: Pas trouvé panel.files.actions: Actions +panel.files.metadata: Métadonnées +panel.files.metadata.alternativeText: Texte alternatif panel.files.viewAsList: Afficher en liste panel.files.viewAsThumbnails: Afficher en vignettes panel.login.attempt.failed: La tentative de connexion a échoué! Réessayer. diff --git a/panel/translations/it.yaml b/panel/translations/it.yaml index a431bd700..d96953ab5 100644 --- a/panel/translations/it.yaml +++ b/panel/translations/it.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: La pagina non esiste o la richiesta non panel.errors.error.notFound.heading: Oops, pagina non trovata! panel.errors.error.notFound.status: Non trovato panel.files.actions: Azioni +panel.files.metadata: Metadati +panel.files.metadata.alternativeText: Testo alternativo panel.files.viewAsList: Visualizza come lista panel.files.viewAsThumbnails: Visualizza come miniature panel.login.attempt.failed: Tentativo di accesso fallito! Riprova. diff --git a/panel/translations/pt.yaml b/panel/translations/pt.yaml index 260334214..48ff0a997 100644 --- a/panel/translations/pt.yaml +++ b/panel/translations/pt.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: A página não existe ou a solicitaçã panel.errors.error.notFound.heading: Ops, página não encontrada! panel.errors.error.notFound.status: Não encontrado panel.files.actions: Ações +panel.files.metadata: Metadados +panel.files.metadata.alternativeText: Texto alternativo panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: Falha ao tentar efetuar login! Tente novamente. diff --git a/panel/translations/ru.yaml b/panel/translations/ru.yaml index cca773fbe..b6b150b52 100644 --- a/panel/translations/ru.yaml +++ b/panel/translations/ru.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: Страница не существу panel.errors.error.notFound.heading: К сожалению, страница не найдена! panel.errors.error.notFound.status: Не обнаружена panel.files.actions: Действия +panel.files.metadata: Метаданные +panel.files.metadata.alternativeText: Альтернативный текст panel.files.viewAsList: Просмотр в виде списка panel.files.viewAsThumbnails: Просмотр в виде миниатюр panel.login.attempt.failed: Войти попытка не удалась! Попробуйте еще раз. diff --git a/site/schemes/files/file.yaml b/site/schemes/files/file.yaml new file mode 100644 index 000000000..44a777071 --- /dev/null +++ b/site/schemes/files/file.yaml @@ -0,0 +1 @@ +title: File diff --git a/site/schemes/files/image.yaml b/site/schemes/files/image.yaml new file mode 100644 index 000000000..44b1f0490 --- /dev/null +++ b/site/schemes/files/image.yaml @@ -0,0 +1,15 @@ +title: Image + +extend: files.file + +layout: + type: sections + sections: + metadata: + label: '{{panel.files.metadata}}' + fields: [alt] + +fields: + alt: + type: text + label: '{{panel.files.metadata.alternativeText}}' From ec9bc5c40c1d09f11783c52b3313ead70e7a97c3 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:42:06 +0200 Subject: [PATCH 3/5] Allow file metadata editing from panel --- .../src/Panel/Controllers/PagesController.php | 50 ++++++ panel/routes.php | 2 +- panel/translations/de.yaml | 1 + panel/translations/en.yaml | 1 + panel/translations/es.yaml | 1 + panel/translations/fr.yaml | 1 + panel/translations/it.yaml | 1 + panel/translations/pt.yaml | 1 + panel/translations/ru.yaml | 1 + panel/views/pages/file.php | 165 ++++++++++-------- 10 files changed, 146 insertions(+), 78 deletions(-) diff --git a/formwork/src/Panel/Controllers/PagesController.php b/formwork/src/Panel/Controllers/PagesController.php index 2d1c212d6..b68faac78 100644 --- a/formwork/src/Panel/Controllers/PagesController.php +++ b/formwork/src/Panel/Controllers/PagesController.php @@ -5,6 +5,7 @@ use Formwork\Exceptions\TranslatedException; use Formwork\Fields\Exceptions\ValidationException; use Formwork\Fields\FieldCollection; +use Formwork\Files\File; use Formwork\Files\FileUploader; use Formwork\Http\Files\UploadedFile; use Formwork\Http\JsonResponse; @@ -509,10 +510,32 @@ public function file(RouteParams $routeParams): Response $files = $page->files(); $file = $files->get($filename); + + switch ($this->request->method()) { + case RequestMethod::GET: + $data = $file->data(); + + $file->fields()->setValues($data); + + break; + + case RequestMethod::POST: + $data = $this->request->input(); + + $file->fields()->setValues($data)->validate(); + + $this->updateFileMetadata($file, $file->fields()); + + $this->panel()->notify($this->translate('panel.files.metadata.updated'), 'success'); + + return $this->redirect($this->generateRoute('panel.pages.file', ['page' => $page->route(), 'filename' => $filename])); + } + $fileIndex = $files->indexOf($file); $this->modal('renameFile'); $this->modal('deleteFile'); + $this->modal('changes'); return new Response($this->view('pages.file', [ 'title' => $file->name(), @@ -586,6 +609,33 @@ protected function createPage(FieldCollection $fieldCollection): Page return $this->site()->retrievePage($path); } + protected function updateFileMetadata(File $file, FieldCollection $fieldCollection): void + { + $data = $file->data(); + + $scheme = $file->scheme(); + + $defaults = $scheme->fields()->pluck('default'); + + foreach ($fieldCollection as $field) { + if ($field->isEmpty() || (Arr::has($defaults, $field->name()) && Arr::get($defaults, $field->name()) === $field->value())) { + unset($data[$field->name()]); + continue; + } + + $data[$field->name()] = $field->value(); + } + + $metaFile = $file->path() . $this->config->get('system.files.metadataExtension'); + + if ($data === [] && FileSystem::exists($metaFile)) { + FileSystem::delete($metaFile); + return; + } + + FileSystem::write($metaFile, Yaml::encode($data)); + } + /** * Update a page */ diff --git a/panel/routes.php b/panel/routes.php index d0ff9bc31..6c146e1cb 100644 --- a/panel/routes.php +++ b/panel/routes.php @@ -163,7 +163,7 @@ 'panel.pages.file' => [ 'path' => '/pages/{page}/file/{filename}/', 'action' => 'Formwork\Panel\Controllers\PagesController@file', - 'methods' => ['GET'], + 'methods' => ['GET', 'POST'], ], 'panel.pages.delete' => [ diff --git a/panel/translations/de.yaml b/panel/translations/de.yaml index 602585fd0..684832313 100644 --- a/panel/translations/de.yaml +++ b/panel/translations/de.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Nicht gefunden panel.files.actions: Aktionen panel.files.metadata: Metadaten panel.files.metadata.alternativeText: Alternativtext +panel.files.metadata.updated: Dateimetadaten aktualisiert panel.files.viewAsList: Als Liste anzeigen panel.files.viewAsThumbnails: Als Miniaturansichten anzeigen panel.login.attempt.failed: Anmeldeversuch fehlgeschlagen! Versuchen Sie es erneut. diff --git a/panel/translations/en.yaml b/panel/translations/en.yaml index 0fff3ad3f..31f20bd50 100644 --- a/panel/translations/en.yaml +++ b/panel/translations/en.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Not found panel.files.actions: Actions panel.files.metadata: Metadata panel.files.metadata.alternativeText: Alternative text +panel.files.metadata.updated: File metadata updated panel.files.viewAsList: View as list panel.files.viewAsThumbnails: View as thumbnails panel.login.attempt.failed: Login attempt failed! Try again. diff --git a/panel/translations/es.yaml b/panel/translations/es.yaml index 0eb290cf2..2fc5ab2ca 100644 --- a/panel/translations/es.yaml +++ b/panel/translations/es.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: No encontrado panel.files.actions: Acciones panel.files.metadata: Metadatos panel.files.metadata.alternativeText: Texto alternativo +panel.files.metadata.updated: Metadatos del archivo actualizados panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: ¡Intento de inicio de sesión fallido! Intenta de nuevo. diff --git a/panel/translations/fr.yaml b/panel/translations/fr.yaml index 1de8336cc..099e2728f 100644 --- a/panel/translations/fr.yaml +++ b/panel/translations/fr.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Pas trouvé panel.files.actions: Actions panel.files.metadata: Métadonnées panel.files.metadata.alternativeText: Texte alternatif +panel.files.metadata.updated: Métadonnées du fichier mises à jour panel.files.viewAsList: Afficher en liste panel.files.viewAsThumbnails: Afficher en vignettes panel.login.attempt.failed: La tentative de connexion a échoué! Réessayer. diff --git a/panel/translations/it.yaml b/panel/translations/it.yaml index d96953ab5..695e18abf 100644 --- a/panel/translations/it.yaml +++ b/panel/translations/it.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Non trovato panel.files.actions: Azioni panel.files.metadata: Metadati panel.files.metadata.alternativeText: Testo alternativo +panel.files.metadata.updated: Metadati del file aggiornati panel.files.viewAsList: Visualizza come lista panel.files.viewAsThumbnails: Visualizza come miniature panel.login.attempt.failed: Tentativo di accesso fallito! Riprova. diff --git a/panel/translations/pt.yaml b/panel/translations/pt.yaml index 48ff0a997..09bc8f371 100644 --- a/panel/translations/pt.yaml +++ b/panel/translations/pt.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Não encontrado panel.files.actions: Ações panel.files.metadata: Metadados panel.files.metadata.alternativeText: Texto alternativo +panel.files.metadata.updated: Metadados do ficheiro atualizados panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: Falha ao tentar efetuar login! Tente novamente. diff --git a/panel/translations/ru.yaml b/panel/translations/ru.yaml index b6b150b52..ed02c617e 100644 --- a/panel/translations/ru.yaml +++ b/panel/translations/ru.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Не обнаружена panel.files.actions: Действия panel.files.metadata: Метаданные panel.files.metadata.alternativeText: Альтернативный текст +panel.files.metadata.updated: Метаданные файла обновлены panel.files.viewAsList: Просмотр в виде списка panel.files.viewAsThumbnails: Просмотр в виде миниатюр panel.login.attempt.failed: Войти попытка не удалась! Попробуйте еще раз. diff --git a/panel/views/pages/file.php b/panel/views/pages/file.php index 9c92cda0c..5c78ea675 100644 --- a/panel/views/pages/file.php +++ b/panel/views/pages/file.php @@ -1,94 +1,105 @@ layout('panel') ?> -