Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?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…'";$labelClass = 'detail pw-hidden';// for the $pagefile->description($language) call further belowif($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']) . '…';$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 .= "…" . 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 " ."<a class='InputfieldFileName pw-tooltip' title='$tooltip' target='_blank' href='$pagefile->url' download>$displayName</a> " ."<span class='InputfieldFileStats'>" . str_replace(' ', ' ', $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 accumulatedif(!$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 modeif(!$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 fileif($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> $dragDropLabel</span></span>";$out .= "</div>"; // .InputfieldFileUploadreturn $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 readyreturn 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 filenameif($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 1if($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 filenameif($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 nameif($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-uploadOnlyif($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 descriptionsif($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 Inputfieldreturn false;}}if($this->itemFieldgroup === false) {// item fieldgroup already determined not in usereturn 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;}}