artabro/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module

1555 lines
45 KiB
Text
Raw Normal View History

2024-08-27 11:35:37 +02:00
<?php namespace ProcessWire;
/**
* An Inputfield for handling file uploads
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @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
* @property string $tagsList Predefined tags
* @property bool|int $unzip Whether or not unzip is enabled
* @property bool|int $overwrite Whether or not overwrite mode is enabled
* @property int $descriptionRows Number of rows for description field (default=1, 0=disable)
* @property string $destinationPath Destination path for uploaded file
* @property string $itemClass Class name(s) for each file item (default=InputfieldFileItem ui-widget ui-widget-content)
* @property bool|int $noUpload Set to true or 1 to disable uploading to this field
* @property bool|int $noLang Set to true or 1 to disable multi-language descriptions
* @property bool|int $noAjax Set to true or 1 to disable ajax uploading
* @property int $uploadOnlyMode Set to true or 1 to disable existing file list display, or 2 to also prevent file from having 'temp' status.
* @property bool|int $noCollapseItem Set to true to disable collapsed items (like for LanguageTranslator tool or other things that add tools to files)
* @property bool|int $noShortName Set to true to disable shortened filenames in output
* @property bool|int $noCustomButton Set to true to disable use of the styled <input type='file'>
* @property Pagefiles|Pagefile|null $value
*
* @method string renderItem($pagefile, $id, $n)
* @method string renderList($value)
* @method string renderUpload($value)
* @method void fileAdded(Pagefile $pagefile)
* @method array extractMetadata(Pagefile $pagefile, array $metadata = array())
* @method Pagefile|null processInputAddFile($filename)
* @method void processInputDeleteFile(Pagefile $pagefile)
* @method bool processInputFile(WireInputData $input, Pagefile $pagefile, $n)
* @method bool processItemInputfields(Pagefile $pagefile, InputfieldWrapper $inputfields, $id, WireInputData $input)
*
*/
class InputfieldFile extends Inputfield implements InputfieldItemList, InputfieldHasSortableValue {
public static function getModuleInfo() {
return array(
'title' => __('Files', __FILE__), // Module Title
'summary' => __('One or more file uploads (sortable)', __FILE__), // Module Summary
'version' => 128,
'permanent' => true,
);
}
/**
* Cache of responses we'll be sending on ajax requests
*
*/
protected $ajaxResponses = array();
/**
* Was a file replaced?
*
*/
protected $singleFileReplacement = false;
/**
* Saved instanceof WireUpload in case API retrieval is needed (see getWireUpload() method)
*
*/
protected $wireUpload = null;
/**
* Set to the current Pagefile item when doing iteration
*
* @var Pagefile|null
*
*/
protected $currentItem = null;
/**
* True when field should behave in an upload only mode
*
* @var bool|int
*
*/
protected $uploadOnlyMode = 0;
/**
* This is true when we are only rendering the value rather than the inputs
*
* @var bool
*
*/
protected $renderValueMode = false;
/**
* True when in ajax mode
*
* @var bool
*
*/
protected $isAjax = false;
/**
* Admin theme specific settings
*
* @var array
*
*/
protected $themeSettings = array();
/**
* Commonly used text labels, translated, indexed by label name
*
* @var array
*
*/
protected $labels = array();
/**
* Cached value of Fieldgroup used for Pagefile custom fields, as used by getItemInputfields() method
*
* @var Fieldgroup|null|bool Null when not yet known, false when known not applicable, Fieldgroup when known and in use
*
*/
protected $itemFieldgroup = null;
/**
* Cached result from FieldtypeFile::getValidFileExtension()
*
* @var array
*
*/
protected $extensionsInfo = array();
/**
* Initialize the InputfieldFile
*
*/
public function init() {
parent::init();
// 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);
$this->set('tagsList', '');
// native to this Inputfield
$this->set('unzip', 0);
$this->set('overwrite', 0);
$this->set('descriptionRows', 1);
$this->set('destinationPath', '');
$this->set('itemClass', 'InputfieldFileItem ui-widget ui-widget-content');
$this->set('noUpload', 0); // set to 1 to disable uploading to this field
$this->set('noLang', 0);
$this->set('noAjax', 0); // disable ajax uploading
$this->set('noCollapseItem', 0);
$this->set('noShortName', 0);
$this->set('noCustomButton', false);
$this->attr('type', 'file');
$this->labels = array(
'description' => $this->_('Description'),
'tags' => $this->_('Tags'),
'drag-drop' => $this->_('drag and drop files in here'),
'delete' => $this->_('Delete'),
'choose-file' => $this->_('Choose File'),
'choose-files' => $this->_('Choose Files'),
);
$input = $this->wire()->input;
$this->isAjax = $input->get('InputfieldFileAjax')
|| $input->get('reloadInputfieldAjax')
|| $input->get('renderInputfieldAjax');
$this->setMaxFilesize(trim(ini_get('post_max_size')));
$this->uploadOnlyMode = (int) $input->get('uploadOnlyMode');
$this->addClass('InputfieldItemList', 'wrapClass');
$this->addClass('InputfieldHasFileList', 'wrapClass');
$themeDefaults = array(
'error' => "<span class='ui-state-error-text'>{out}</span>",
);
$themeSettings = $this->wire()->config->InputfieldFile;
$this->themeSettings = is_array($themeSettings) ? array_merge($themeDefaults, $themeSettings) : $themeDefaults;
}
public function get($key) {
if($key === 'renderValueMode') return $this->renderValueMode;
if($key === 'singleFileReplacement') return $this->singleFileReplacement;
if($key === 'descriptionFieldLabel') return $this->labels['description'];
if($key === 'tagsFieldLabel') return $this->labels['tags'];
if($key === 'deleteLabel') return $this->labels['delete'];
if($key === 'themeSettings') return $this->themeSettings;
return parent::get($key);
}
public function set($key, $value) {
if($key == 'maxFilesize') return $this->setMaxFilesize($value);
return parent::set($key, $value);
}
/**
* Set the max file size in bytes or use string like "30m", "2g" "500k"
*
* @param int|string $filesize
* @return $this
*
*/
public function setMaxFilesize($filesize) {
$max = $this->strToBytes($filesize);
$phpMax = $this->strToBytes(ini_get('upload_max_filesize'));
if($phpMax < $max) $max = $phpMax;
parent::set('maxFilesize', $max);
return $this;
}
/**
* Convert string like "32M" to bytes (integer)
*
* @param string|int $filesize
* @return int
*
*/
protected function strToBytes($filesize) {
if(ctype_digit("$filesize")) {
$bytes = (int) $filesize;
} else {
$filesize = rtrim($filesize, 'bB'); // convert mb=>m, gb=>g, kb=>k
$last = strtolower(substr($filesize, -1));
if(ctype_alpha($last)) $filesize = rtrim($filesize, $last);
$filesize = (int) $filesize;
if($last == 'g') {
$bytes = (($filesize * 1024) * 1024) * 1024;
} else if($last == 'm') {
$bytes = ($filesize * 1024) * 1024;
} else if($last == 'k') {
$bytes = $filesize * 1024;
} else if($filesize > 0) {
$bytes = $filesize;
} else {
$bytes = (5 * 1024) * 1024;
}
}
return $bytes;
}
/**
* Per Inputfield interface, returns true when this field is empty
*
*/
public function isEmpty() {
return !wireCount($this->value);
}
/**
* Set an attribute
*
* @param array|string $key
* @param array|int|string $value
* @return Inputfield|InputfieldFile
*
*/
public function setAttribute($key, $value) {
if($key == 'value') {
if($value instanceof Pagefile) {
// if given a Pagefile rather than a Pagefiles, use the Pagefiles instead
$value = $value->pagefiles;
}
if($value instanceof Pagefiles) {
$page = $value->page;
if($page && $page->template->noLang) $this->noLang = true;
}
}
return parent::setAttribute($key, $value);
}
/**
* Check to ensure that the containing form as an 'enctype' attr needed for uploading files
*
*/
protected function checkFormEnctype() {
$parent = $this->parent;
while($parent) {
if($parent->attr('method') == 'post') {
if(!$parent->attr('enctype')) $parent->attr('enctype', 'multipart/form-data');
break;
}
$parent = $parent->parent;
}
}
/**
* Set the parent of this Inputfield
*
* @param InputfieldWrapper $parent
* @return $this
*
*/
public function setParent(InputfieldWrapper $parent) {
parent::setParent($parent);
$this->checkFormEnctype();
return $this;
}
/**
* Get the unique 'id' attribute for the given Pagefile
*
* @param Pagefile $pagefile
* @param string $context Optional context string (like for repeaters) 3.0.178+
* @return string
*
*/
protected function pagefileId(Pagefile $pagefile, $context = '') {
return $this->name . "_" . $context . $pagefile->hash;
}
/**
* Render a description input for the given Pagefile
*
* @param Pagefile $pagefile
* @param string $id
* @param int $n
* @return string
*
*/
protected function renderItemDescriptionField(Pagefile $pagefile, $id, $n) {
$sanitizer = $this->wire()->sanitizer;
$languages = $this->wire()->languages;
$user = $this->wire()->user;
if($n) {}
$out = '';
$tabs = '';
static $hasLangTabs = null;
static $langTabSettings = array();
if($this->renderValueMode) {
if($languages) {
$description = $pagefile->description($this->noLang ? $languages->getDefault() : $user->language);
} else {
$description = $pagefile->description;
}
if(strlen($description)) $description =
"<div class='InputfieldFileDescription detail'>" .
$sanitizer->entities1($description) .
"</div>";
return $description;
}
if($this->descriptionRows > 0) {
$userLanguage = $languages ? $user->language : null;
$defaultDescriptionFieldLabel = $sanitizer->entities1($this->labels['description']);
if(!$userLanguage || $languages->count() < 2 || $this->noLang) {
$numLanguages = 0;
$forLanguages = array(null);
} else {
$numLanguages = $languages->count();
$forLanguages = $languages;
if(is_null($hasLangTabs)) {
$modules = $this->wire()->modules;
$hasLangTabs = $modules->isInstalled('LanguageTabs');
if($hasLangTabs) {
/** @var LanguageTabs $languageTabs */
$languageTabs = $modules->getModule('LanguageTabs');
$langTabSettings = $languageTabs->getSettings();
}
}
}
foreach($forLanguages as $language) {
$descriptionFieldName = "description_$id";
$descriptionFieldLabel = $defaultDescriptionFieldLabel;
$labelClass = "detail";
$attrStr = '';
if($language) {
/** @var Language $language */
$tabField = empty($langTabSettings['tabField']) ? 'title' : $langTabSettings['tabField'];
$descriptionFieldLabel = (string) $language->getUnformatted($tabField);
if(empty($descriptionFieldLabel)) $descriptionFieldLabel = $language->get('name');
$descriptionFieldLabel = $sanitizer->entities($descriptionFieldLabel);
if(!$language->isDefault()) $descriptionFieldName = "description{$language->id}_$id";
$labelClass .= ' LanguageSupportLabel';
if(!$languages->editable($language)) {
$labelClass .= ' LanguageNotEditable';
$descriptionFieldLabel = "<s>$descriptionFieldLabel</s>";
}
$tabID = "langTab_{$id}__$language";
$aClass = "langTab$language";
if(!empty($langTabSettings['aClass'])) $aClass .= " " . $langTabSettings['aClass'];
$tabs .= "<li><a data-lang='$language' class='$aClass' href='#$tabID'>$descriptionFieldLabel</a></li>";
$out .= "<div class='InputfieldFileDescription LanguageSupport' data-language='$language' id='$tabID'>"; // open wrapper
} else {
$out .= "<div class='InputfieldFileDescription'>"; // open wrapper
$attrStr = "placeholder='$descriptionFieldLabel&hellip;'";
$labelClass = 'detail pw-hidden';
// for the $pagefile->description($language) call further below
if($languages && $this->noLang) $language = $languages->getDefault();
}
$attrStr = "name='$descriptionFieldName' id='$descriptionFieldName' $attrStr";
$out .= "<label for='$descriptionFieldName' class='$labelClass'>$descriptionFieldLabel</label>";
$description = $sanitizer->entities($pagefile->description($language));
if($this->descriptionRows > 1) {
$out .= "<textarea $attrStr rows='$this->descriptionRows'>$description</textarea>";
} else {
$out .= "<input type='text' $attrStr value='$description' />";
}
$out .= "</div>"; // close wrapper
}
if($numLanguages && $hasLangTabs) {
$ulClass = empty($langTabSettings['ulClass']) ? '' : " class='$langTabSettings[ulClass]'";
$ulAttr = empty($langTabSettings['ulAttrs']) ? '' : " $langTabSettings[ulAttrs]";
$out =
"<div class='hasLangTabs langTabsContainer'>" .
"<div class='langTabs'>" .
"<ul $ulAttr$ulClass>$tabs</ul>" .
$out .
"</div>" .
"</div>";
if($this->isAjax) {
$js = 'script';
$out .= "<$js>setupLanguageTabs($('#wrap_" . $this->attr('id') . "'));</$js>";
}
}
}
if($this->useTags) $out .= $this->renderItemTagsField($pagefile, $id, $n);
return $out;
}
/**
* Render the tags input for the given Pagefile
*
* @param Pagefile $pagefile
* @param string $id
* @param int $n
* @return string
*
*/
protected function renderItemTagsField(Pagefile $pagefile, $id, $n) {
$sanitizer = $this->wire()->sanitizer;
if($n) {}
$tagsLabel = $sanitizer->entities($this->labels['tags']) . '&hellip;';
$tagsStr = $sanitizer->entities($pagefile->tags);
$tagsAttr = '';
if($this->useTags >= FieldtypeFile::useTagsPredefined) {
// select predefined
$tagsClass = 'InputfieldFileTagsSelect';
$tagsAttr = "data-cfgname='InputfieldFileTags_{$this->hasField->name}' ";
} else {
// text input
$tagsClass = 'InputfieldFileTagsInput';
}
$out =
"<div class='InputfieldFileTags'>" .
"<label for='tags_$id' class='detail pw-hidden'>$tagsLabel</label>" .
"<input type='text' name='tags_$id' id='tags_$id' value='$tagsStr' " .
"placeholder='$tagsLabel' class='$tagsClass' $tagsAttr/>" .
"</div>";
return $out;
}
/**
* Get a basename for the file, possibly shortened, suitable for display in InputfieldFileList
*
* @param Pagefile $pagefile
* @param int $maxLength
* @return string
*
*/
public function getDisplayBasename(Pagefile $pagefile, $maxLength = 25) {
$displayName = $pagefile->basename;
if($this->noShortName) return $displayName;
if(strlen($displayName) > $maxLength) {
$ext = ".$pagefile->ext";
$maxLength -= (strlen($ext) + 1);
$displayName = basename($displayName, $ext);
$displayName = substr($displayName, 0, $maxLength);
$displayName .= "&hellip;" . ltrim($ext, '.');
}
return $displayName;
}
/**
* Render markup for a file item
*
* @param Pagefile $pagefile
* @param string $id
* @param int $n
* @return string
*
*/
protected function ___renderItem($pagefile, $id, $n) {
$displayName = $this->getDisplayBasename($pagefile);
$deleteLabel = $this->labels['delete'];
$uploadName = $pagefile->uploadName();
$icon = wireIconMarkupFile($pagefile->basename, "fa-fw HideIfEmpty");
$tooltip = $this->wire()->sanitizer->entities($pagefile->basename);
if($uploadName && $uploadName != $pagefile->basename) {
$uploadName = $this->wire()->sanitizer->entities($uploadName);
$icon = "<span class='pw-tooltip' title='$uploadName'>$icon</span>";
}
$out =
"<p class='InputfieldFileInfo InputfieldItemHeader ui-state-default ui-widget-header'>" .
"$icon&nbsp;" .
"<a class='InputfieldFileName pw-tooltip' title='$tooltip' target='_blank' href='$pagefile->url' download>$displayName</a> " .
"<span class='InputfieldFileStats'>" . str_replace(' ', '&nbsp;', $pagefile->filesizeStr) . "</span> ";
if(!$this->renderValueMode) $out .=
"<label class='InputfieldFileDelete'>" .
"<input type='checkbox' name='delete_$id' value='1' title='$deleteLabel' />" .
"<i class='fa fa-fw fa-trash'></i></label>";
$description = $this->renderItemDescriptionField($pagefile, $id, $n);
$class = 'InputfieldFileData ';
$class .= $description ? 'description ui-widget-content' : 'InputfieldFileFields';
$out .= "</p><div class='$class'>" . $description;
$inputfields = $this->getItemInputfields($pagefile);
if($inputfields) $out .= $inputfields->render();
if(!$this->renderValueMode) {
$out .= "<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />";
}
$out .= "</div>";
return $out;
}
/**
* Wrap output of files list item
*
* @param string $out
* @return string
*
*/
protected function renderItemWrap($out) {
// note: using currentItem rather than a new argument since there are now a few modules extending
// this one and if they implement their own calls to this method or version of this method then
// they will get strict notices from php if we add a new argument here.
$item = $this->currentItem;
$id = $item && !$this->renderValueMode ? " id='file_$item->hash'" : "";
return "<li$id class='{$this->itemClass}'>$out</li>";
}
/**
* Render files list ready
*
* @param Pagefiles|null $value
* @throws WireException
* @throws WirePermissionException
*
*/
protected function renderListReady($value) {
if(!$this->renderValueMode) {
// if just rendering the files list (as opposed to saving it), delete any temp files that may have accumulated
if(!$this->overwrite && !count($_POST) && !$this->isAjax && !$this->uploadOnlyMode && !$this->wire()->config->ajax) {
$input = $this->wire()->input;
// don't delete files when in render single field or fields mode
if(!$input->get('field') && !$input->get('fields')) {
if($value instanceof Pagefiles) $value->deleteAllTemp();
}
}
}
}
/**
* Render files list
*
* @param Pagefiles|null $value
* @return string
*
*/
protected function ___renderList($value) {
if(!$value) return '';
$out = '';
$n = 0;
$this->renderListReady($value);
if(!$this->uploadOnlyMode && WireArray::iterable($value)) {
foreach($value as $pagefile) {
$id = $this->pagefileId($pagefile);
$this->currentItem = $pagefile;
$out .= $this->renderItemWrap($this->renderItem($pagefile, $id, $n++));
}
}
$class = 'InputfieldFileList ui-helper-clearfix';
if($this->overwrite && !$this->renderValueMode) $class .= " InputfieldFileOverwrite";
if($out) $out = "<ul class='$class'>$out</ul>";
return $out;
}
/**
* Render upload area
*
* @param Pagefiles|null $value
* @return string
*
*/
protected function ___renderUpload($value) {
if($value) {}
if($this->noUpload || $this->renderValueMode) return '';
// enables user to choose more than one file
if($this->maxFiles != 1) $this->setAttribute('multiple', 'multiple');
$attrs = $this->getAttributes();
unset($attrs['value']);
if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]';
$extensions = $this->getAllowedExtensions();
$formatExtensions = $this->formatExtensions();
$chooseLabel = $this->labels['choose-file'];
$dragDropLabel = $this->labels['drag-drop'];
$attrStr = $this->getAttributesString($attrs);
$out =
"<div " .
"data-maxfilesize='$this->maxFilesize' " .
"data-extensions='$extensions' " .
"data-fieldname='$attrs[name]' " .
"class='InputfieldFileUpload'>
";
if($this->getSetting('noCustomButton')) {
$out .= "<input $attrStr>";
} else {
$out .= "
<div class='InputMask ui-button ui-state-default'>
<span class='ui-button-text'>
<i class='fa fa-fw fa-folder-open-o'></i>$chooseLabel
</span>
<input $attrStr>
</div>
";
}
$out .= "
<span class='InputfieldFileValidExtensions detail'>$formatExtensions</span>
<input type='hidden' class='InputfieldFileMaxFiles' value='$this->maxFiles' />
";
if(!$this->noAjax) $out .= "
<span class='AjaxUploadDropHere description'>
<span>
<i class='fa fa-cloud-upload'></i>&nbsp;$dragDropLabel
</span>
</span>
";
$out .= "</div>"; // .InputfieldFileUpload
return $out;
}
/**
* Render ready
*
* @param Inputfield|null $parent
* @param bool $renderValueMode
* @return bool
*
*/
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
$config = $this->wire()->config;
$this->addClass('InputfieldNoFocus', 'wrapClass');
if(!$renderValueMode) $this->addClass('InputfieldHasUpload', 'wrapClass');
if($this->useTags) {
$jQueryUI = $this->wire()->modules->get('JqueryUI'); /** @var JqueryUI $jQueryUI */
$jQueryUI->use('selectize');
$this->addClass('InputfieldFileHasTags', 'wrapClass');
if($this->useTags >= FieldtypeFile::useTagsPredefined && $this->hasField) {
// predefined tags
$fieldName = $this->hasField->name;
$jsName = "InputfieldFileTags_$fieldName";
$allowUserTags = $this->useTags & FieldtypeFile::useTagsNormal;
$data = $config->js($jsName);
if(!is_array($data)) $data = array();
if(empty($data['tags'])) {
$tags = array();
foreach(explode(' ', (string) $this->get('tagsList')) as $tag) {
$tag = trim($tag);
if(!strlen($tag)) continue;
$tags[strtolower($tag)] = $tag;
}
if($allowUserTags) {
$pagefiles = $this->val();
if($pagefiles instanceof Pagefiles) {
$_tags = $pagefiles->tags(true);
if(count($_tags)) $tags = array_merge($tags, $_tags);
}
}
$data['tags'] = array_values($tags);
$data['allowUserTags'] = $allowUserTags;
$config->js($jsName, $data);
}
$this->wrapAttr('data-configName', $jsName);
} else {
// regular tags text input
}
}
$data = $config->js('InputfieldFile');
if(!is_array($data)) $data = array();
if(empty($data['labels'])) $data['labels'] = array();
if(empty($data['labels']['bad-ext'])) {
$data['labels']['bad-ext'] = $this->_('Unsupported file extension, please use only: EXTENSIONS');
$data['labels']['too-big'] = $this->_('File is too big - maximum allowed size is MAX_KB kb');
$config->js('InputfieldFile', $data);
}
$this->getItemInputfields(); // custom fields ready
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.'));
}
if($this->allowCollapsedItems()) {
$this->addClass('InputfieldItemListCollapse', 'wrapClass');
}
$numItems = (int) wireCount($this->value);
if($numItems === 0) {
$this->addClass('InputfieldFileEmpty', 'wrapClass');
} else if($numItems === 1) {
$this->addClass('InputfieldFileSingle', 'wrapClass');
} else {
$this->addClass('InputfieldFileMultiple', 'wrapClass');
}
return $this->renderList($this->value) . $this->renderUpload($this->value);
}
/**
* Render Inputfield value
*
* @return string
*
*/
public function ___renderValue() {
$this->renderValueMode = true;
$out = $this->render();
$this->renderValueMode = false;
return $out;
}
/**
* File added hook
*
* @param Pagefile $pagefile
* @throws WireException
*
*/
protected function ___fileAdded(Pagefile $pagefile) {
if($this->noUpload) return;
$sanitizer = $this->wire()->sanitizer;
$isValid = $sanitizer->validateFile($pagefile->filename(), array(
'pagefile' => $pagefile
));
if($isValid === false) {
$errors = $sanitizer->errors('clear array');
throw new WireException(
"$pagefile->basename - " . $this->_('File failed validation') .
(count($errors) ? ": " . implode(', ', $errors) : "")
);
} else if($isValid === null) {
// there was no validator available for this file type
}
$message = $this->_('Added file:') . " {$pagefile->basename}"; // Label that precedes an added filename
if($this->isAjax && !$this->noAjax) {
$n = count($this->value);
if($n) $n--; // for sorting
$this->currentItem = $pagefile;
$markup = $this->fileAddedGetMarkup($pagefile, $n);
$this->ajaxResponse(false, $message, $pagefile->url, $pagefile->filesize(), $markup);
} else {
$this->message($message);
}
$user = $this->wire()->user;
$pagefile->createdUser = $user;
$pagefile->modifiedUser = $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));
}
/**
* Given a Pagefile return array of meta data pulled from it
*
* @param Pagefile $pagefile
* @param array $metadata Existing metadata, if applicable
* @return array Associative array of meta data (i.e. description and tags)
*
*/
protected function ___extractMetadata(Pagefile $pagefile, array $metadata = array()) {
$languages = $this->wire()->languages;
if($languages) {
$metadata['description'] = $pagefile->description($languages->getDefault());
} else {
$metadata['description'] = $pagefile->description;
}
if($languages && !$this->noLang) {
foreach($languages as $language) {
/** @var Language $language */
if($language->isDefault()) continue;
$metadata["description$language->id"] = $pagefile->description($language);
}
}
$metadata['tags'] = $pagefile->tags;
$filedata = $pagefile->filedata();
if(count($filedata)) {
$metadata['filedata'] = $filedata;
}
return $metadata;
}
/**
* Process input to add a file
*
* @param string $filename
* @return Pagefile|null Returns Pagefile (added 3.0.212+)
* @throws WireException
*
*/
protected function ___processInputAddFile($filename) {
$total = count($this->value);
$metadata = array();
if($this->maxFiles > 1 && $total >= $this->maxFiles) return null;
// allow replacement of file if maxFiles is 1
if($this->maxFiles == 1 && $total) {
/** @var Pagefile $pagefile */
$pagefile = $this->value->first();
$metadata = $this->extractMetadata($pagefile, $metadata);
$rm = true;
if($filename == $pagefile->basename) {
// use overwrite mode rather than replace mode when single file and same filename
if($this->overwrite) $rm = false;
}
if($rm) {
if($this->overwrite) $this->processInputDeleteFile($pagefile);
$this->singleFileReplacement = true;
}
}
if($this->overwrite) {
$pagefile = $this->value->get($filename);
clearstatcache();
if($pagefile) {
// already have a file of the same name
if($pagefile instanceof Pageimage) $pagefile->removeVariations();
$metadata = $this->extractMetadata($pagefile, $metadata);
} else {
// we don't have a file with the same name as the one that was uploaded
// file must be in another files field on the same page, that could be problematic
$ul = $this->getWireUpload();
// see if any files were overwritten that weren't part of our field
// if so, we need to restore them and issue an error
$err = false;
$files = $this->wire()->files;
foreach($ul->getOverwrittenFiles() as $bakFile => $newFile) {
if(basename($newFile) != $filename) continue;
$files->unlink($newFile);
$files->rename($bakFile, $newFile); // restore
$ul->error(sprintf($this->_('Refused file %s because it is already on the file system and owned by a different field.'), $filename));
$err = true;
}
if($err) return null;
}
}
$this->value->add($filename);
/** @var Pagefile $item */
$item = $this->value->last();
try {
foreach($metadata as $key => $val) {
if($val) $item->$key = $val;
}
// items saved in ajax or uploadOnly mode are temporary till saved in non-ajax/non-uploadOnly
if($this->isAjax && !$this->overwrite) {
if($this->wire()->input->get('InputfieldFileAjax') !== 'noTemp') {
$item->isTemp(true);
}
}
$this->fileAdded($item);
} catch(\Exception $e) {
$item->unlink();
$this->value->remove($item);
throw new WireException($e->getMessage());
}
return $item;
}
/**
* Process input to delete a Pagefile item
*
* @param Pagefile $pagefile
*
*/
protected function ___processInputDeleteFile(Pagefile $pagefile) {
$fileLabel = $this->wire()->config->debug ? $pagefile->url() : $pagefile->name;
$this->message($this->_("Deleted file:") . " $fileLabel"); // Label that precedes a deleted filename
$this->value->delete($pagefile);
$this->trackChange('value');
}
/**
* Process input for one Pagefile
*
* @param WireInputData $input
* @param Pagefile $pagefile
* @param int $n
* @return bool
*
*/
protected function ___processInputFile(WireInputData $input, Pagefile $pagefile, $n) {
$saveFields = false; // allow custom Inputfields to be saved?
$changed = false; // are there any changes to this file?
$id = $this->name . '_' . $pagefile->hash;
if($this->uploadOnlyMode) {
// skip files that aren't present as just uploaded
$key = "sort_$id";
if($input->$key === null) return false;
}
// replace (currently only used by InputfieldImage)
$key = "replace_$id";
$replace = $input->$key;
if($replace) {
if(strpos($replace, '?') !== false) {
list($replace, $unused) = explode('?', $replace);
if($unused) {}
}
$replaceFile = $this->value->getFile($replace);
if($replaceFile instanceof Pagefile) {
// $this->processInputDeleteFile($replaceFile);
// PR#229 to fix #1586:
if($replaceFile->basename !== $pagefile->basename) {
$this->processInputDeleteFile($replaceFile);
}
// ---
if(strtolower($pagefile->ext()) == strtolower($replaceFile->ext())) {
$this->value->rename($pagefile, $replaceFile->name);
}
$changed = true;
}
}
// rename (currently only used by InputfieldImage)
$key = "rename_$id";
$rename = (string) $input->$key;
if(strlen($rename) && $rename != $pagefile->basename(false)) {
$name = $pagefile->basename();
$rename .= "." . $pagefile->ext();
// cleanBasename($basename, $originalize = false, $allowDots = true, $translate = false)
$rename = $pagefile->pagefiles->cleanBasename($rename, true, true, true);
if(strlen($rename)) {
$message = sprintf($this->_('Renamed file "%1$s" to "%2$s"'), $name, $rename);
if($pagefile->rename($rename) !== false) {
$this->message($message);
$changed = true;
} else {
$this->warning($this->_('Failed') . " - $message");
}
}
}
// description and tags
// $languages = $this->noLang ? null : $this->wire()->languages;
$languages = $this->wire()->languages;
$useLanguages = $languages && !$this->noLang;
$keys = $useLanguages ? array('tags') : array('description', 'tags');
foreach($keys as $key) {
if(isset($input[$key . '_' . $id])) {
$value = $input[$key . '_' . $id];
if(is_array($value)) $value = implode(' ', $value);
$value = trim($value);
if($value != $pagefile->$key) {
$pagefile->$key = $value;
$changed = true;
}
}
}
// multi-language descriptions
if($languages) {
foreach($languages as $language) {
/** @var Language $language */
if(!$useLanguages && !$language->isDefault()) continue;
if(!$languages->editable($language)) continue;
$key = $language->isDefault() ? "description_$id" : "description{$language->id}_$id";
if(!isset($input[$key])) continue;
$value = trim($input[$key]);
if($value != $pagefile->description($language)) {
$pagefile->description($language, $value);
$changed = true;
}
}
}
if($this->uploadOnlyMode) {
if($this->uploadOnlyMode === 2) {
$sort = 0; // ensures an isTemp(false) call occurs below
} else {
$sort = null;
}
$changed = true;
} else {
$key = "sort_$id";
$sort = $input->$key;
if($sort !== null) {
$sort = (int) $sort;
$pagefile->set('sort', $sort);
if($n !== $sort) $changed = true;
$saveFields = true;
}
}
if($saveFields) {
// save custom Inputfields
$inputfields = $this->getItemInputfields($pagefile);
if($inputfields && $this->processItemInputfields($pagefile, $inputfields, $id, $input)) $changed = true;
}
$delete = isset($input['delete_' . $id]) ? (int) $input['delete_' . $id] : 0;
if(!empty($delete)) {
$this->processInputDeleteFile($pagefile);
$changed = true;
} else if(!$this->isAjax && !$this->overwrite && $pagefile->isTemp() && $sort !== null) {
// if page saved with temporary items when not ajax, those temporary items become non-temp
$pagefile->isTemp(false);
// @todo should the next statement instead be this below?
// if($this->maxFiles > 0) while(count($this->value) > $this->>maxFiles) { ... } ?
if(((int) $this->maxFiles) === 1) {
while(count($this->value) > 1) {
$item = $this->value->first();
$this->value->remove($item);
}
}
$changed = true;
}
return $changed;
}
/**
* Process custom Inputfields for Pagefile item
*
* @param Pagefile $pagefile
* @param InputfieldWrapper $inputfields
* @param string $id Pagefile ID string
* @param WireInputData $input
* @return bool True if changes detected, false if not
* @since 3.0.142
*
*/
protected function ___processItemInputfields(Pagefile $pagefile, InputfieldWrapper $inputfields, $id, WireInputData $input) {
$changed = false;
$inputfields->resetTrackChanges(true);
$inputfields->processInput($input);
foreach($inputfields->getAll() as $f) {
/** @var Inputfield $f */
foreach($f->getErrors(true) as $error) {
$msg = "$this->label ($pagefile->name): $error";
$this->error($msg);
$f->error($msg);
}
if(!$f->isChanged() && !$pagefile->isTemp()) {
continue;
}
$name = str_replace("_$id", '', $f->attr('name'));
if($f->getSetting('useLanguages')) {
$value = $pagefile->getFieldValue($name);
if(is_object($value)) $value->setFromInputfield($f);
} else {
$value = $f->val();
}
$pagefile->setFieldValue($name, $value, true);
$changed = true;
}
return $changed;
}
/**
* Process input
*
* @param WireInputData $input
* @return self
*
*/
public function ___processInput(WireInputData $input) {
if(is_null($this->value)) {
$this->value = $this->wire(new Pagefiles($this->wire()->page));
}
if(!$this->destinationPath) {
$this->destinationPath = $this->value->path();
}
if(!$this->destinationPath || !is_dir($this->destinationPath)) {
return $this->error($this->_("destinationPath is empty or does not exist"));
}
if(!is_writable($this->destinationPath)) {
return $this->error($this->_("destinationPath is not writable"));
}
$changed = false;
$total = count($this->value);
if(!$this->noUpload) {
if($this->maxFiles <= 1 || $total < $this->maxFiles) {
$ul = $this->getWireUpload();
$ul->setName($this->attr('name'));
$ul->setDestinationPath($this->destinationPath);
$ul->setOverwrite($this->overwrite);
$ul->setAllowAjax($this->noAjax ? false : true);
if($this->maxFilesize) $ul->setMaxFileSize($this->maxFilesize);
if($this->maxFiles == 1) {
$ul->setMaxFiles(1);
} else if($this->maxFiles) {
$maxFiles = $this->maxFiles - $total;
$ul->setMaxFiles($maxFiles);
} else if($this->unzip) {
$ul->setExtractArchives(true);
}
$ul->setValidExtensions($this->getAllowedExtensions(true));
$filenames = $ul->execute();
$originalFilenames = $ul->getOriginalFilenames();
foreach($filenames as $filename) {
$pagefile = $this->processInputAddFile($filename);
if($pagefile && isset($originalFilenames[$filename]) && $originalFilenames[$filename] != $filename) {
$pagefile->filedata('uploadName', $originalFilenames[$filename]);
}
$changed = true;
}
if($this->isAjax && !$this->noAjax) foreach($ul->getErrors() as $error) {
$this->ajaxResponse(true, $error);
}
} else if($this->maxFiles) {
// over the limit
$this->ajaxResponse(true, $this->_("Max file upload limit reached"));
}
}
$n = 0;
foreach($this->value as $pagefile) {
if($this->processInputFile($input, $pagefile, $n)) $changed = true;
$n++;
}
if($changed) {
$this->value->sort('sort');
$this->trackChange('value');
}
if(count($this->ajaxResponses) && $this->isAjax) {
echo $this->renderAjaxResponse();
}
return $this;
}
/**
* Render JSON response to AJAX request
*
* @return string
*
*/
protected function renderAjaxResponse() {
if($this->wire()->input->get('ckeupload')) {
// https://docs.ckeditor.com/ckeditor4/docs/#!/guide/dev_file_upload
$a = $this->ajaxResponses[0];
$response = array(
'uploaded' => $a['error'] ? 0 : 1,
'fileName' => basename($a['file']),
'url' => $a['file'],
'ajaxResponse' => $a, // for InputfieldImage.js
);
if($a['error']) {
$response['error'] = array(
'message' => $a['message']
);
}
return json_encode($response);
} else {
return json_encode($this->ajaxResponses);
}
}
/**
* Send an ajax response
*
* @param bool $error Whether it was successful
* @param string $message Message you want to return
* @param string $file Full path and filename or blank if not applicable
* @param string $size
* @param string $markup
*
*/
protected function ajaxResponse($error, $message, $file = '', $size = '', $markup = '') {
$response = array(
'error' => $error,
'message' => $message,
'file' => $file,
'size' => $size,
'markup' => $markup,
'replace' => $this->singleFileReplacement,
'overwrite' => $this->overwrite
);
$this->ajaxResponses[] = $response;
}
/**
* Return the current WireUpload instance or create a new one if not yet created
*
* @return WireUpload
*
*/
public function getWireUpload() {
if(is_null($this->wireUpload)) {
$this->wireUpload = $this->wire(new WireUpload($this->attr('name')));
}
return $this->wireUpload;
}
/**
* Template method: allow items to be collapsed?
*
* @return bool
*
*/
protected function allowCollapsedItems() {
$allow = $this->descriptionRows == 0 && !$this->useTags && !$this->noCollapseItem;
if($allow && $this->hasField) {
/** @var FieldtypeFile $fieldtype */
$fieldtype = $this->hasField->type;
if($fieldtype->getFieldsTemplate($this->hasField)) $allow = false;
}
return $allow;
}
/**
* Format list of file extensions for output with upload field
*
* @param array|string $extensions
* @return string
*
*/
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;
}
/**
* Get custom Inputfields for editing given Pagefile
*
* @param Pagefile|null $item Specify Pagefile item, or omit to prepare for render ready
* @return bool|InputfieldWrapper
* @since 3.0.142
*
*/
public function getItemInputfields(Pagefile $item = null) {
/** @var Pagefiles $pagefiles */
$value = $this->val();
$pagefiles = $value instanceof Pagefile ? $value->pagefiles : $value;
if(!$pagefiles instanceof Pagefiles) {
if($this->hasPage && $this->hasField) {
$value = $this->hasPage->get($this->hasField->name);
$pagefiles = $value instanceof Pagefile ? $value->pagefiles : $value;
}
if(!$pagefiles instanceof Pagefiles) {
// no value present on this Inputfield
return false;
}
}
if($this->itemFieldgroup === false) {
// item fieldgroup already determined not in use
return false;
}
if($this->itemFieldgroup === null) {
// item fieldgroup not yet determined
$this->itemFieldgroup = false;
$template = $pagefiles->getFieldsTemplate();
if(!$template) return false;
if($this->noLang) $template->setQuietly('noLang', 1);
$this->itemFieldgroup = $template->fieldgroup;
}
$context = '';
if($item) {
$hasPage = $this->hasPage;
if($hasPage && wireInstanceOf($hasPage, 'RepeaterPage')) {
if(strpos($this->name, '_repeater') === false) {
// ensures that custom fields are properly namespaced within repeater
// though note that this prevents it from working when editing a repeater
// page directly, independently of its forPage
$context = "repeater{$hasPage->id}_";
}
}
/*
* The following does not work with nested repeaters, fixed by the above, but kept here for reference
$process = $this->wire()->process;
if($item && $process instanceof WirePageEditor) {
$contextPage = $process->getPage();
if(wireInstanceOf($contextPage, 'RepeaterPage') && strpos($this->name, '_repeater') === false) {
// @var RepeaterPage $contextPage
$forPage = $contextPage->getForPage();
if($forPage->id) $contextPage = $forPage;
$context = "repeater{$contextPage->id}_";
}
}
*/
}
/** @var Page $page */
$page = $pagefiles->getFieldsPage();
$id = $item ? ('_' . $this->pagefileId($item, $context)) : '';
$inputfields = $this->itemFieldgroup->getPageInputfields($page, $id, '', false);
if(!$inputfields) return false;
$languages = $this->wire()->languages;
foreach($inputfields->getAll() as $f) {
/** @var Inputfield $f */
if($f->get('requiredAttr') || $f->attr('required')) {
// required attribute not possible for dynamically changed inputs
$f->set('requiredAttr', 0);
$f->removeAttr('required');
}
if(wireInstanceOf($f, 'InputfieldCKEditor')) {
/** @var InputfieldCKEditor $f */
$ckeField = $f->hasField;
if($ckeField) {
$f->configName = $f->className() . "_$ckeField->name";
$imagesField = $this->hasField;
if($imagesField && $this->itemFieldgroup && $this->itemFieldgroup->hasFieldContext($ckeField)) {
$f->configName .= "_$imagesField->name";
}
}
}
if(!$item) {
// prepare inputfields for render rather than populating them
$f->renderReady();
continue;
}
/** @var Inputfield $f */
$name = str_replace($id, '', $f->name);
$value = $item->getFieldValue($name);
if($value === null) continue;
if($languages && $f->getSetting('useLanguages') && $value instanceof LanguagesValueInterface) {
foreach($languages as $language) {
/** @var Language $language */
$v = $value->getLanguageValue($language->id);
if($language->isDefault()) $f->val($v);
$f->set("value$language->id", $v);
}
} else if($f instanceof InputfieldCheckbox) {
if($value) $f->attr('checked', 'checked');
} else if($f instanceof InputfieldText && is_array($value) && isset($value['data'])) {
// a previously multi-language value that's now a single-language value
$f->val($value['data']);
} else {
$f->val($value);
}
/*
if($f->className() === 'InputfieldCKEditor') {
// CKE does not like being placed in file/image fields.
// I'm sure it's possible, but needs more work and debugging, so it's disabled for now.
$allow = false;
} else {
$allow = true;
}
if(!$allow) {
$inputfields->remove($f);
$this->prependMarkup =
"<p class='ui-state-error-text'>" .
sprintf($this->_('Field “%1$s” type “%2$s” is not supported in field “%3$s”'), $f->label, $f->className(), $this->label) .
'</p>';
$f->getParent()->remove($f);
}
*/
}
return $inputfields;
}
/**
* Configuration settings for InputfieldFile
*
* @return InputfieldWrapper
*
*/
public function ___getConfigInputfields() {
$inputfields = parent::___getConfigInputfields();
require_once($this->wire()->config->paths('InputfieldFile') . 'config.php');
$configuration = new InputfieldFileConfiguration();
$this->wire($configuration);
$configuration->getConfigInputfields($this, $inputfields);
return $inputfields;
}
}