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();
}
}