mirror of
https://github.com/processwire/processwire.git
synced 2025-08-13 18:24:57 +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:
@@ -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 what’s 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
|
||||
*
|
||||
@@ -1429,6 +1508,8 @@ class FieldtypeFile extends FieldtypeMulti implements ConfigurableModule {
|
||||
public function ___getConfigInputfields(Field $field) {
|
||||
|
||||
$inputfields = parent::___getConfigInputfields($field);
|
||||
$extensionInfo = $this->getValidFileExtensions($field);
|
||||
$fileValidatorsUrl = 'https://modules.processwire.com/categories/file-validator/';
|
||||
|
||||
// extensions
|
||||
/** @var InputfieldTextarea $f */
|
||||
@@ -1438,8 +1519,41 @@ class FieldtypeFile extends FieldtypeMulti implements ConfigurableModule {
|
||||
$f->attr('rows', 3);
|
||||
$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
|
||||
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);
|
||||
|
||||
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 don’t 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
|
||||
/** @var InputfieldInteger $f */
|
||||
$f = $this->modules->get('InputfieldInteger');
|
||||
|
@@ -4,6 +4,7 @@
|
||||
* An Inputfield for handling file uploads
|
||||
*
|
||||
* @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 $maxFilesize Maximum file size
|
||||
* @property bool $useTags Whether or not tags are enabled
|
||||
@@ -118,6 +119,14 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
*/
|
||||
protected $itemFieldgroup = null;
|
||||
|
||||
/**
|
||||
* Cached result from FieldtypeFile::getValidFileExtension()
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
*/
|
||||
protected $extensionsInfo = array();
|
||||
|
||||
/**
|
||||
* Initialize the InputfieldFile
|
||||
*
|
||||
@@ -128,6 +137,7 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
// note: these two fields originate from FieldtypeFile.
|
||||
// Initializing them here ensures this Inputfield has the values set automatically.
|
||||
$this->set('extensions', '');
|
||||
$this->set('okExtensions', array()); // manually whitelisted problematic extensions
|
||||
$this->set('maxFiles', 0);
|
||||
$this->set('maxFilesize', 0);
|
||||
$this->set('useTags', 0);
|
||||
@@ -588,9 +598,8 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
unset($attrs['value']);
|
||||
if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]';
|
||||
|
||||
$extensions = $this->extensions;
|
||||
if($this->unzip && !$this->maxFiles) $extensions .= ' zip';
|
||||
$formatExtensions = $this->formatExtensions($extensions);
|
||||
$extensions = $this->getAllowedExtensions();
|
||||
$formatExtensions = $this->formatExtensions();
|
||||
$chooseLabel = $this->labels['choose-file'];
|
||||
$dragDropLabel = $this->labels['drag-drop'];
|
||||
$attrStr = $this->getAttributesString($attrs);
|
||||
@@ -635,6 +644,14 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render ready
|
||||
*
|
||||
* @param Inputfield|null $parent
|
||||
* @param bool $renderValueMode
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
|
||||
|
||||
/** @var Config $config */
|
||||
@@ -691,6 +708,12 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
return parent::renderReady($parent, $renderValueMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Inputfield input
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function ___render() {
|
||||
if(!$this->extensions) $this->error($this->_('No file extensions are defined for this field.'));
|
||||
$numItems = wireCount($this->value);
|
||||
@@ -705,6 +728,12 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
return $this->renderList($this->value) . $this->renderUpload($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Inputfield value
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function ___renderValue() {
|
||||
$this->renderValueMode = true;
|
||||
$out = $this->render();
|
||||
@@ -712,17 +741,25 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* File added hook
|
||||
*
|
||||
* @param Pagefile $pagefile
|
||||
* @throws WireException
|
||||
*
|
||||
*/
|
||||
protected function ___fileAdded(Pagefile $pagefile) {
|
||||
if($this->noUpload) return;
|
||||
$sanitizer = $this->wire()->sanitizer;
|
||||
|
||||
$isValid = $this->wire('sanitizer')->validateFile($pagefile->filename(), array(
|
||||
$isValid = $sanitizer->validateFile($pagefile->filename(), array(
|
||||
'pagefile' => $pagefile
|
||||
));
|
||||
|
||||
if($isValid === false) {
|
||||
$errors = $this->wire('sanitizer')->errors('clear array');
|
||||
$errors = $sanitizer->errors('clear array');
|
||||
throw new WireException(
|
||||
$this->_('File failed validation') .
|
||||
"$pagefile->basename - " . $this->_('File failed validation') .
|
||||
(count($errors) ? ": " . implode(', ', $errors) : "")
|
||||
);
|
||||
} else if($isValid === null) {
|
||||
@@ -745,6 +782,14 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
$pagefile->modifiedUser = $this->wire('user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get markup for added file
|
||||
*
|
||||
* @param Pagefile $pagefile
|
||||
* @param int $n
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function fileAddedGetMarkup(Pagefile $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->setValidExtensions(explode(' ', trim($this->extensions)));
|
||||
$ul->setValidExtensions($this->getAllowedExtensions(true));
|
||||
|
||||
foreach($ul->execute() as $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
|
||||
*
|
||||
* @param string $extensions
|
||||
* @param array|string $extensions
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function formatExtensions($extensions) {
|
||||
return $this->wire('sanitizer')->entities(str_replace(' ', ', ', trim($extensions)));
|
||||
protected function formatExtensions($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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -9,6 +9,7 @@
|
||||
* Accessible Properties
|
||||
*
|
||||
* @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 $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).
|
||||
@@ -320,8 +321,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]';
|
||||
$attrStr = $this->getAttributesString($attrs);
|
||||
|
||||
$extensions = $this->extensions;
|
||||
if($this->unzip && !$this->maxFiles) $extensions .= ' zip';
|
||||
$extensions = $this->getAllowedExtensions();
|
||||
$formatExtensions = $this->formatExtensions($extensions);
|
||||
$chooseLabel = $this->labels['choose-file'];
|
||||
|
||||
@@ -388,7 +388,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
|
||||
/** @var Pageimage $pagefile */
|
||||
|
||||
if($pagefile->ext() == 'svg') {
|
||||
if($pagefile->ext() === 'svg') {
|
||||
parent::___fileAdded($pagefile);
|
||||
return;
|
||||
}
|
||||
|
Reference in New Issue
Block a user