1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-14 10:45:54 +02:00

Update files and images fields with the ability to require FileValidatorModule validation or manually whitelisted extensions, with the first being the svg file extension.

This commit is contained in:
Ryan Cramer
2020-09-24 13:43:26 -04:00
parent 15dc362ba5
commit f2a313723a
3 changed files with 219 additions and 16 deletions

View File

@@ -1419,6 +1419,85 @@ class FieldtypeFile extends FieldtypeMulti implements ConfigurableModule {
} }
/**
* Check file extensions for given field and return array of validity information
*
* @param Field|Inputfield $field
* @param array $validateExtensions Extensions to require validation for, or omit for default.
* @return array Returns associative array with the following:
* - `valid` (array): valid extensions, including those that have been whitelisted or are covered by FileValidator modules.
* - `invalid` (array): extensions that are potentially bad and have not been whitelisted or covered by a FileValidator module.
* - `whitelist` (array): previously invalid extensions that have been manually whitelisted.
* - `validators` (array): Associative array of [ 'ext' => [ 'FileValidatorModule' ] ] showing whats covered by FileValidator modules.
* @throws WireException
* @since 3.0.167
*
*/
public function getValidFileExtensions($field, array $validateExtensions = array()) {
if(!$field instanceof Field && !$field instanceof Inputfield) {
throw new WireException("This method requires a Field or Inputfield object");
}
if(empty($validateExtensions)) {
$validateExtensions = array('svg');
} else {
foreach($validateExtensions as $key => $ext) {
$validateExtensions[$key] = strtolower(trim($ext));
}
}
$extensions = array();
$badExtensions = array();
$whitelistExtensions = array();
$extensionsStr = $field->get('extensions');
$okExtensions = $field->get('okExtensions');
$validators = array();
if(!is_array($okExtensions)) $okExtensions = array();
$extensionsStr = trim(str_replace(array("\n", ",", "."), ' ', $extensionsStr));
foreach(explode(' ', $extensionsStr) as $ext) {
$ext = strtolower(trim($ext));
if(!strlen($ext)) continue;
$extensions[$ext] = $ext;
}
foreach($extensions as $ext) {
// check if extension requires a FileValidator
if(!in_array($ext, $validateExtensions)) continue;
// if extension was manually whitelisted, then accept it as valid
if(in_array($ext, $okExtensions, true)) {
$whitelistExtensions[$ext] = $ext;
continue;
}
// if a module validates extension then good
$moduleNames = $this->wire()->sanitizer->validateFile("test.$ext", array(
'dryrun' => true,
'getArray' => true
));
if(count($moduleNames)) {
$validators[$ext] = $moduleNames;
continue;
}
// if extension has no validator then remove it from valid list and add to the naughty list
unset($extensions[$ext]);
$badExtensions[$ext] = $ext;
}
return array(
'valid' => $extensions, // valid extensions, including those that have been whitelisted
'invalid' => $badExtensions, // extensions that are potentially bad and have not been whitelisted
'whitelist' => $whitelistExtensions, // previously invalid extensions that have been whitelisted
'validators' => $validators, // file validators in use indexed by file extension
);
}
/** /**
* Field config * Field config
* *
@@ -1429,7 +1508,9 @@ class FieldtypeFile extends FieldtypeMulti implements ConfigurableModule {
public function ___getConfigInputfields(Field $field) { public function ___getConfigInputfields(Field $field) {
$inputfields = parent::___getConfigInputfields($field); $inputfields = parent::___getConfigInputfields($field);
$extensionInfo = $this->getValidFileExtensions($field);
$fileValidatorsUrl = 'https://modules.processwire.com/categories/file-validator/';
// extensions // extensions
/** @var InputfieldTextarea $f */ /** @var InputfieldTextarea $f */
$f = $this->modules->get('InputfieldTextarea'); $f = $this->modules->get('InputfieldTextarea');
@@ -1438,8 +1519,41 @@ class FieldtypeFile extends FieldtypeMulti implements ConfigurableModule {
$f->attr('rows', 3); $f->attr('rows', 3);
$f->label = $this->_('Valid File Extensions'); $f->label = $this->_('Valid File Extensions');
$f->description = $this->_('Enter all file extensions allowed by this upload field. Separate each extension by a space. No periods or commas. This field is not case sensitive.'); // Valid file extensions description $f->description = $this->_('Enter all file extensions allowed by this upload field. Separate each extension by a space. No periods or commas. This field is not case sensitive.'); // Valid file extensions description
if(count($extensionInfo['invalid']) && !$this->wire()->input->is('POST')) {
foreach($extensionInfo['invalid'] as $ext) {
$error = sprintf(
$this->_('File extension %s must be removed, whitelisted, or have a [file validator](%s) module installed.'),
strtoupper($ext),
$fileValidatorsUrl
);
$this->error($error, Notice::allowMarkdown | Notice::noGroup);
}
}
$inputfields->append($f); $inputfields->append($f);
if(count($extensionInfo['invalid']) || count($extensionInfo['whitelist'])) {
/** @var array $okExtensions */
$badExtensions = array_merge($extensionInfo['invalid'], $extensionInfo['whitelist']);
ksort($badExtensions);
/** @var InputfieldCheckboxes $f Whitelisted file extensions */
$f = $this->modules->get('InputfieldCheckboxes');
$f->attr('name', 'okExtensions');
$f->label = $this->_('File extensions to allow without validation (whitelist)');
$f->icon = 'warning';
foreach($badExtensions as $ext) $f->addOption($ext);
$f->description =
sprintf(
$this->_('These file extensions need a [file validator module](%s) installed. Unchecked extensions have been disabled for safety.'),
$fileValidatorsUrl
) . ' ' .
$this->_('To ignore and allow the file extension without file validation, check the box next to it (not recommended).') . ' ' .
$this->_('If you dont need an extension, please remove it from your valid file extensions list.');
$f->attr('value', $extensionInfo['whitelist']);
$inputfields->add($f);
} else {
$field->set('okExtensions', array());
}
// max files // max files
/** @var InputfieldInteger $f */ /** @var InputfieldInteger $f */
$f = $this->modules->get('InputfieldInteger'); $f = $this->modules->get('InputfieldInteger');

View File

@@ -4,6 +4,7 @@
* An Inputfield for handling file uploads * An Inputfield for handling file uploads
* *
* @property string $extensions Allowed file extensions, space separated * @property string $extensions Allowed file extensions, space separated
* @property array $okExtensions File extensions that are whitelisted if any in $extensions are problematic. (3.0.167+)
* @property int $maxFiles Maximum number of files allowed * @property int $maxFiles Maximum number of files allowed
* @property int $maxFilesize Maximum file size * @property int $maxFilesize Maximum file size
* @property bool $useTags Whether or not tags are enabled * @property bool $useTags Whether or not tags are enabled
@@ -118,6 +119,14 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
*/ */
protected $itemFieldgroup = null; protected $itemFieldgroup = null;
/**
* Cached result from FieldtypeFile::getValidFileExtension()
*
* @var array
*
*/
protected $extensionsInfo = array();
/** /**
* Initialize the InputfieldFile * Initialize the InputfieldFile
* *
@@ -128,6 +137,7 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
// note: these two fields originate from FieldtypeFile. // note: these two fields originate from FieldtypeFile.
// Initializing them here ensures this Inputfield has the values set automatically. // Initializing them here ensures this Inputfield has the values set automatically.
$this->set('extensions', ''); $this->set('extensions', '');
$this->set('okExtensions', array()); // manually whitelisted problematic extensions
$this->set('maxFiles', 0); $this->set('maxFiles', 0);
$this->set('maxFilesize', 0); $this->set('maxFilesize', 0);
$this->set('useTags', 0); $this->set('useTags', 0);
@@ -588,9 +598,8 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
unset($attrs['value']); unset($attrs['value']);
if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]'; if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]';
$extensions = $this->extensions; $extensions = $this->getAllowedExtensions();
if($this->unzip && !$this->maxFiles) $extensions .= ' zip'; $formatExtensions = $this->formatExtensions();
$formatExtensions = $this->formatExtensions($extensions);
$chooseLabel = $this->labels['choose-file']; $chooseLabel = $this->labels['choose-file'];
$dragDropLabel = $this->labels['drag-drop']; $dragDropLabel = $this->labels['drag-drop'];
$attrStr = $this->getAttributesString($attrs); $attrStr = $this->getAttributesString($attrs);
@@ -635,6 +644,14 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
return $out; return $out;
} }
/**
* Render ready
*
* @param Inputfield|null $parent
* @param bool $renderValueMode
* @return bool
*
*/
public function renderReady(Inputfield $parent = null, $renderValueMode = false) { public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
/** @var Config $config */ /** @var Config $config */
@@ -691,6 +708,12 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
return parent::renderReady($parent, $renderValueMode); return parent::renderReady($parent, $renderValueMode);
} }
/**
* Render Inputfield input
*
* @return string
*
*/
public function ___render() { public function ___render() {
if(!$this->extensions) $this->error($this->_('No file extensions are defined for this field.')); if(!$this->extensions) $this->error($this->_('No file extensions are defined for this field.'));
$numItems = wireCount($this->value); $numItems = wireCount($this->value);
@@ -704,7 +727,13 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
} }
return $this->renderList($this->value) . $this->renderUpload($this->value); return $this->renderList($this->value) . $this->renderUpload($this->value);
} }
/**
* Render Inputfield value
*
* @return string
*
*/
public function ___renderValue() { public function ___renderValue() {
$this->renderValueMode = true; $this->renderValueMode = true;
$out = $this->render(); $out = $this->render();
@@ -712,17 +741,25 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
return $out; return $out;
} }
/**
* File added hook
*
* @param Pagefile $pagefile
* @throws WireException
*
*/
protected function ___fileAdded(Pagefile $pagefile) { protected function ___fileAdded(Pagefile $pagefile) {
if($this->noUpload) return; if($this->noUpload) return;
$sanitizer = $this->wire()->sanitizer;
$isValid = $this->wire('sanitizer')->validateFile($pagefile->filename(), array( $isValid = $sanitizer->validateFile($pagefile->filename(), array(
'pagefile' => $pagefile 'pagefile' => $pagefile
)); ));
if($isValid === false) { if($isValid === false) {
$errors = $this->wire('sanitizer')->errors('clear array'); $errors = $sanitizer->errors('clear array');
throw new WireException( throw new WireException(
$this->_('File failed validation') . "$pagefile->basename - " . $this->_('File failed validation') .
(count($errors) ? ": " . implode(', ', $errors) : "") (count($errors) ? ": " . implode(', ', $errors) : "")
); );
} else if($isValid === null) { } else if($isValid === null) {
@@ -744,7 +781,15 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
$pagefile->createdUser = $this->wire('user'); $pagefile->createdUser = $this->wire('user');
$pagefile->modifiedUser = $this->wire('user'); $pagefile->modifiedUser = $this->wire('user');
} }
/**
* Get markup for added file
*
* @param Pagefile $pagefile
* @param int $n
* @return string
*
*/
protected function fileAddedGetMarkup(Pagefile $pagefile, $n) { protected function fileAddedGetMarkup(Pagefile $pagefile, $n) {
return $this->renderItemWrap($this->renderItem($pagefile, $this->pagefileId($pagefile), $n)); return $this->renderItemWrap($this->renderItem($pagefile, $this->pagefileId($pagefile), $n));
} }
@@ -1077,7 +1122,7 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
$ul->setExtractArchives(true); $ul->setExtractArchives(true);
} }
$ul->setValidExtensions(explode(' ', trim($this->extensions))); $ul->setValidExtensions($this->getAllowedExtensions(true));
foreach($ul->execute() as $filename) { foreach($ul->execute() as $filename) {
$this->processInputAddFile($filename); $this->processInputAddFile($filename);
@@ -1194,12 +1239,56 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
/** /**
* Format list of file extensions for output with upload field * Format list of file extensions for output with upload field
* *
* @param string $extensions * @param array|string $extensions
* @return string * @return string
* *
*/ */
protected function formatExtensions($extensions) { protected function formatExtensions($extensions = '') {
return $this->wire('sanitizer')->entities(str_replace(' ', ', ', trim($extensions))); $sanitizer = $this->wire()->sanitizer;
$badExtensions = array();
if(empty($extensions)) {
$info = $this->getExtensionsInfo();
$extensions = $info['valid'];
$badExtensions = $info['invalid'];
} else if(is_string($extensions)) {
while(strpos($extensions, ' ') !== false) $extensions = str_replace(' ', ' ', $extensions);
$extensions = explode(' ', trim($extensions));
}
$out = $sanitizer->entities(implode(', ', $extensions));
if(count($badExtensions)) {
if($out) $out .= ', ';
$out .= '<s>' . $sanitizer->entities(implode(', ', $badExtensions)) . '</s>';
}
return $out;
}
/**
* Get allowed file extensions
*
* @param bool $getArray
* @return array|string
* @since 3.0.167
*
*/
protected function getAllowedExtensions($getArray = false) {
$info = $this->getExtensionsInfo();
$extensions = $info['valid'];
if($this->unzip && !$this->maxFiles) if(!in_array('zip', $extensions)) $extensions[] = 'zip';
return $getArray ? $extensions : implode(' ', $extensions);
}
/**
* Get extensions info (see FieldtypeFile::getValidFileExtensions)
*
* @return array
* @since 3.0.167
*
*/
protected function getExtensionsInfo() {
if(empty($this->extensionsInfo)) {
$this->extensionsInfo = $this->wire()->fieldtypes->FieldtypeFile->getValidFileExtensions($this);
}
return $this->extensionsInfo;
} }
/** /**

View File

@@ -9,6 +9,7 @@
* Accessible Properties * Accessible Properties
* *
* @property string $extensions Space separated list of allowed image extensions (default="JPG JPEG GIF PNG") * @property string $extensions Space separated list of allowed image extensions (default="JPG JPEG GIF PNG")
* @property array $okExtensions Array of manually whitelisted extensions, for instance [ 'SVG' ] must be manually whitelisted if allowed. (default=[])
* @property int|string $maxWidth Max width for uploaded images, larger will be sized down (default='') * @property int|string $maxWidth Max width for uploaded images, larger will be sized down (default='')
* @property int|string $maxHeight Max height for uploaded images, larger will be sized down (default='') * @property int|string $maxHeight Max height for uploaded images, larger will be sized down (default='')
* @property float $maxSize Maximum number of megapixels for client-side resize, i.e. 1.7 is ~1600x1000, alt. to maxWidth/maxHeight (default=0). * @property float $maxSize Maximum number of megapixels for client-side resize, i.e. 1.7 is ~1600x1000, alt. to maxWidth/maxHeight (default=0).
@@ -320,8 +321,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]'; if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]';
$attrStr = $this->getAttributesString($attrs); $attrStr = $this->getAttributesString($attrs);
$extensions = $this->extensions; $extensions = $this->getAllowedExtensions();
if($this->unzip && !$this->maxFiles) $extensions .= ' zip';
$formatExtensions = $this->formatExtensions($extensions); $formatExtensions = $this->formatExtensions($extensions);
$chooseLabel = $this->labels['choose-file']; $chooseLabel = $this->labels['choose-file'];
@@ -388,7 +388,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
/** @var Pageimage $pagefile */ /** @var Pageimage $pagefile */
if($pagefile->ext() == 'svg') { if($pagefile->ext() === 'svg') {
parent::___fileAdded($pagefile); parent::___fileAdded($pagefile);
return; return;
} }