summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/html/src/FormElement/FileElement.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/ipl/html/src/FormElement/FileElement.php')
-rw-r--r--vendor/ipl/html/src/FormElement/FileElement.php414
1 files changed, 414 insertions, 0 deletions
diff --git a/vendor/ipl/html/src/FormElement/FileElement.php b/vendor/ipl/html/src/FormElement/FileElement.php
new file mode 100644
index 0000000..d9ca8fd
--- /dev/null
+++ b/vendor/ipl/html/src/FormElement/FileElement.php
@@ -0,0 +1,414 @@
+<?php
+
+namespace ipl\Html\FormElement;
+
+use GuzzleHttp\Psr7\UploadedFile;
+use InvalidArgumentException;
+use ipl\Html\Attributes;
+use ipl\Html\Form;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\I18n\Translation;
+use ipl\Validator\FileValidator;
+use ipl\Validator\ValidatorChain;
+use Psr\Http\Message\UploadedFileInterface;
+use ipl\Html\Common\MultipleAttribute;
+
+use function ipl\Stdlib\get_php_type;
+
+/**
+ * File upload element
+ *
+ * Once the file element is added to the form and the form attribute `enctype` is not set,
+ * it is automatically set to `multipart/form-data`.
+ */
+class FileElement extends InputElement
+{
+ use MultipleAttribute;
+ use Translation;
+
+ protected $type = 'file';
+
+ /** @var UploadedFileInterface|UploadedFileInterface[] */
+ protected $value;
+
+ /** @var UploadedFileInterface[] Files that are stored on disk */
+ protected $files = [];
+
+ /** @var string[] Files to be removed from disk */
+ protected $filesToRemove = [];
+
+ /** @var ?string Path to store files to preserve them across requests */
+ protected $destination;
+
+ /** @var int The default maximum file size */
+ protected static $defaultMaxFileSize;
+
+ public function __construct($name, $attributes = null)
+ {
+ $this->getAttributes()->get('accept')->setSeparator(', ');
+
+ parent::__construct($name, $attributes);
+ }
+
+ /**
+ * Get the path to store files to preserve them across requests
+ *
+ * @return string
+ */
+ public function getDestination(): ?string
+ {
+ return $this->destination;
+ }
+
+ /**
+ * Set the path to store files to preserve them across requests
+ *
+ * Uploaded files are moved to the given directory to
+ * retain the file through automatic form submissions and failed form validations.
+ *
+ * Please note that using file persistence currently has the following drawbacks:
+ *
+ * * Works only if the file element is added to the form during {@link Form::assemble()}.
+ * * Persisted files are not removed automatically.
+ * * Files with the same name override each other.
+ *
+ * @param string $path
+ *
+ * @return $this
+ */
+ public function setDestination(string $path): self
+ {
+ $this->destination = $path;
+
+ return $this;
+ }
+
+ public function getValueAttribute()
+ {
+ // Value attributes of file inputs are set only client-side.
+ return null;
+ }
+
+ public function getNameAttribute()
+ {
+ $name = $this->getName();
+
+ return $this->isMultiple() ? ($name . '[]') : $name;
+ }
+
+ public function hasValue()
+ {
+ if ($this->value === null) {
+ $files = $this->loadFiles();
+ if (empty($files)) {
+ return false;
+ }
+
+ if (! $this->isMultiple()) {
+ $files = $files[0];
+ }
+
+ $this->value = $files;
+ }
+
+ return $this->value !== null;
+ }
+
+ public function setValue($value)
+ {
+ if (! empty($value)) {
+ $fileToTest = $value;
+ if ($this->isMultiple()) {
+ $fileToTest = $value[0];
+ }
+
+ if (! $fileToTest instanceof UploadedFileInterface) {
+ throw new InvalidArgumentException(
+ sprintf('%s is not an uploaded file', get_php_type($fileToTest))
+ );
+ }
+
+ if ($fileToTest->getError() === UPLOAD_ERR_NO_FILE && ! $fileToTest->getClientFilename()) {
+ // This is checked here as it's only about file elements for which no value has been chosen
+ $value = null;
+ } else {
+ $files = $value;
+ if (! $this->isMultiple()) {
+ $files = [$files];
+ }
+
+ /** @var UploadedFileInterface[] $files */
+ $storedFiles = $this->storeFiles(...$files);
+ if (! $this->isMultiple()) {
+ $storedFiles = $storedFiles[0] ?? null;
+ }
+
+ $value = $storedFiles;
+ }
+ } else {
+ $value = null;
+ }
+
+ return parent::setValue($value);
+ }
+
+ /**
+ * Get whether there are any files stored on disk
+ *
+ * @return bool
+ */
+ protected function hasFiles(): bool
+ {
+ return $this->destination !== null && reset($this->files);
+ }
+
+ /**
+ * Load and return all files stored on disk
+ *
+ * @return UploadedFileInterface[]
+ */
+ protected function loadFiles(): array
+ {
+ if (empty($this->files) || $this->destination === null) {
+ return [];
+ }
+
+ foreach ($this->files as $name => $_) {
+ $filePath = $this->getFilePath($name);
+ if (! is_readable($filePath) || ! is_file($filePath)) {
+ // If one file isn't accessible, none is
+ return [];
+ }
+
+ if (in_array($name, $this->filesToRemove, true)) {
+ @unlink($filePath);
+ } else {
+ $this->files[$name] = new UploadedFile(
+ $filePath,
+ filesize($filePath) ?: null,
+ 0,
+ $name,
+ mime_content_type($filePath) ?: null
+ );
+ }
+ }
+
+ $this->files = array_diff_key($this->files, array_flip($this->filesToRemove));
+
+ return array_values($this->files);
+ }
+
+ /**
+ * Store the given files on disk
+ *
+ * @param UploadedFileInterface ...$files
+ *
+ * @return UploadedFileInterface[]
+ */
+ protected function storeFiles(UploadedFileInterface ...$files): array
+ {
+ if ($this->destination === null || ! is_writable($this->destination)) {
+ return $files;
+ }
+
+ $storedFiles = [];
+ foreach ($files as $file) {
+ $name = $file->getClientFilename();
+ $path = $this->getFilePath($name);
+
+ if ($file->getError() !== UPLOAD_ERR_OK) {
+ // The file is still returned as otherwise it won't be validated
+ $storedFiles[] = $file;
+ continue;
+ }
+
+ $file->moveTo($path);
+
+ // Re-created to ensure moveTo() still works if called externally
+ $file = new UploadedFile(
+ $path,
+ $file->getSize(),
+ 0,
+ $name,
+ $file->getClientMediaType()
+ );
+
+ $this->files[$name] = $file;
+ $storedFiles[] = $file;
+ }
+
+ return $storedFiles;
+ }
+
+ /**
+ * Get the file path on disk of the given file
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function getFilePath(string $name): string
+ {
+ return implode(DIRECTORY_SEPARATOR, [$this->destination, sha1($name)]);
+ }
+
+ public function onRegistered(Form $form)
+ {
+ if (! $form->hasAttribute('enctype')) {
+ $form->setAttribute('enctype', 'multipart/form-data');
+ }
+
+ $chosenFiles = (array) $form->getPopulatedValue('chosen_file_' . $this->getName(), []);
+ foreach ($chosenFiles as $chosenFile) {
+ $this->files[$chosenFile] = null;
+ }
+
+ $this->filesToRemove = (array) $form->getPopulatedValue('remove_file_' . $this->getName(), []);
+ }
+
+ protected function addDefaultValidators(ValidatorChain $chain): void
+ {
+ $chain->add(new FileValidator([
+ 'maxSize' => $this->getDefaultMaxFileSize(),
+ 'mimeType' => array_filter(
+ (array) $this->getAttributes()->get('accept')->getValue(),
+ function ($type) {
+ // file inputs also allow file extensions in the accept attribute. These
+ // must not be passed as they don't resemble valid mime type definitions.
+ return is_string($type) && ltrim($type)[0] !== '.';
+ }
+ )
+ ]));
+ }
+
+ protected function registerAttributeCallbacks(Attributes $attributes)
+ {
+ parent::registerAttributeCallbacks($attributes);
+ $this->registerMultipleAttributeCallback($attributes);
+ $this->getAttributes()->registerAttributeCallback('destination', null, [$this, 'setDestination']);
+ }
+
+ /**
+ * Get the system's default maximum file upload size
+ *
+ * @return int
+ */
+ public function getDefaultMaxFileSize(): int
+ {
+ if (static::$defaultMaxFileSize === null) {
+ $ini = $this->convertIniToInteger(trim(static::getPostMaxSize()));
+ $max = $this->convertIniToInteger(trim(static::getUploadMaxFilesize()));
+ $min = max($ini, $max);
+ if ($ini > 0) {
+ $min = min($min, $ini);
+ }
+
+ if ($max > 0) {
+ $min = min($min, $max);
+ }
+
+ static::$defaultMaxFileSize = $min;
+ }
+
+ return static::$defaultMaxFileSize;
+ }
+
+ /**
+ * Converts a ini setting to a integer value
+ *
+ * @param string $setting
+ *
+ * @return int
+ */
+ private function convertIniToInteger(string $setting): int
+ {
+ if (! is_numeric($setting)) {
+ $type = strtoupper(substr($setting, -1));
+ $setting = (int) substr($setting, 0, -1);
+
+ switch ($type) {
+ case 'K':
+ $setting *= 1024;
+ break;
+
+ case 'M':
+ $setting *= 1024 * 1024;
+ break;
+
+ case 'G':
+ $setting *= 1024 * 1024 * 1024;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return (int) $setting;
+ }
+
+ /**
+ * Get the `post_max_size` INI setting
+ *
+ * @return string
+ */
+ protected static function getPostMaxSize(): string
+ {
+ return ini_get('post_max_size') ?: '8M';
+ }
+
+ /**
+ * Get the `upload_max_filesize` INI setting
+ *
+ * @return string
+ */
+ protected static function getUploadMaxFilesize(): string
+ {
+ return ini_get('upload_max_filesize') ?: '2M';
+ }
+
+ protected function assemble()
+ {
+ $doc = new HtmlDocument();
+ if ($this->hasFiles()) {
+ foreach ($this->files as $file) {
+ $doc->addHtml(new HiddenElement('chosen_file_' . $this->getValueOfNameAttribute(), [
+ 'value' => $file->getClientFilename()
+ ]));
+ }
+
+ $this->prependWrapper($doc);
+ }
+ }
+
+ public function renderUnwrapped()
+ {
+ if (! $this->hasValue() || ! $this->hasFiles()) {
+ return parent::renderUnwrapped();
+ }
+
+ $uploadedFiles = new HtmlElement('ul', Attributes::create(['class' => 'uploaded-files']));
+ foreach ($this->files as $file) {
+ $uploadedFiles->addHtml(new HtmlElement(
+ 'li',
+ null,
+ (new ButtonElement('remove_file_' . $this->getValueOfNameAttribute(), Attributes::create([
+ 'type' => 'submit',
+ 'formnovalidate' => true,
+ 'class' => 'remove-uploaded-file',
+ 'value' => $file->getClientFilename(),
+ 'title' => sprintf($this->translate('Remove file "%s"'), $file->getClientFilename())
+ ])))->addHtml(new HtmlElement(
+ 'span',
+ null,
+ new HtmlElement('i', Attributes::create(['class' => ['icon', 'fa', 'fa-xmark']])),
+ Text::create($file->getClientFilename())
+ ))
+ ));
+ }
+
+ return $uploadedFiles->render();
+ }
+}