Rev 22 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** An Inputfield for handling file uploads** @property string $extensions Allowed file extensions, space separated* @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 void 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' => 126,'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;/*** 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('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'),);$this->isAjax = $this->wire('input')->get('InputfieldFileAjax')|| $this->wire('input')->get('reloadInputfieldAjax')|| $this->wire('input')->get('renderInputfieldAjax');$this->setMaxFilesize(trim(ini_get('post_max_size')));$this->uploadOnlyMode = (int) $this->wire('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;$this->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;}}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* @return string**/protected function pagefileId(Pagefile $pagefile) {return $this->name . "_" . $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) {if($n) {}$out = '';$tabs = '';static $hasLangTabs = null;static $langTabSettings = array();if($this->renderValueMode) {if($this->wire('languages')) {$description = $pagefile->description($this->wire('user')->language);} else {$description = $pagefile->description;}if(strlen($description)) $description ="<div class='InputfieldFileDescription detail'>" . $this->wire('sanitizer')->entities1($description) . "</div>";return $description;}if($this->descriptionRows > 0) {$userLanguage = $this->wire('user')->language;$languages = $this->noLang ? null : $this->wire('languages');$defaultDescriptionFieldLabel = $this->wire('sanitizer')->entities1($this->labels['description']);if(!$userLanguage || !$languages || $languages->count() < 2) {$numLanguages = 0;$languages = array(null);} else {$numLanguages = $languages->count();if(is_null($hasLangTabs)) {$hasLangTabs = $this->wire('modules')->isInstalled('LanguageTabs');if($hasLangTabs) {/** @var LanguageTabs $languageTabs */$languageTabs = $this->wire('modules')->getModule('LanguageTabs');$langTabSettings = $languageTabs->getSettings();}}}foreach($languages as $language) {$descriptionFieldName = "description_$id";$descriptionFieldLabel = $defaultDescriptionFieldLabel;$labelClass = "detail";$attrStr = '';if($language) {$tabField = empty($langTabSettings['tabField']) ? 'title' : $langTabSettings['tabField'];$descriptionFieldLabel = (string) $language->getUnformatted($tabField);if(empty($descriptionFieldLabel)) $descriptionFieldLabel = $language->get('name');$descriptionFieldLabel = $this->wire('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';}$attrStr = "name='$descriptionFieldName' id='$descriptionFieldName' $attrStr";$out .= "<label for='$descriptionFieldName' class='$labelClass'>$descriptionFieldLabel</label>";$description = $this->wire('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) $out .= "<script>setupLanguageTabs($('#wrap_" . $this->attr('id') . "'));</script>";}}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) {if($n) {}$tagsLabel = $this->wire('sanitizer')->entities($this->labels['tags']) . '…';$tagsStr = $this->wire('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'];$out ="<p class='InputfieldFileInfo InputfieldItemHeader ui-state-default ui-widget-header'>" .wireIconMarkupFile($pagefile->basename, "fa-fw HideIfEmpty") . ' ' ."<a class='InputfieldFileName' title='$pagefile->basename' target='_blank' href='{$pagefile->url}'>$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) {// don't delete files when in render single field or fields modeif(!$this->wire('input')->get('field') && !$this->wire('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 $k => $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->extensions;if($this->unzip && !$this->maxFiles) $extensions .= ' zip';$formatExtensions = $this->formatExtensions($extensions);$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;}public function renderReady(Inputfield $parent = null, $renderValueMode = false) {/** @var Config $config */$config = $this->wire('config');$this->addClass('InputfieldNoFocus', 'wrapClass');if(!$renderValueMode) $this->addClass('InputfieldHasUpload', 'wrapClass');if($this->useTags) {$this->wire('modules')->get('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);}public function ___render() {if(!$this->extensions) $this->error($this->_('No file extensions are defined for this field.'));$numItems = wireCount($this->value);if($this->allowCollapsedItems()) $this->addClass('InputfieldItemListCollapse', 'wrapClass');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);}public function ___renderValue() {$this->renderValueMode = true;$out = $this->render();$this->renderValueMode = false;return $out;}protected function ___fileAdded(Pagefile $pagefile) {if($this->noUpload) return;$isValid = $this->wire('sanitizer')->validateFile($pagefile->filename(), array('pagefile' => $pagefile));if($isValid === false) {$errors = $this->wire('sanitizer')->errors('clear array');throw new WireException($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);}$pagefile->createdUser = $this->wire('user');$pagefile->modifiedUser = $this->wire('user');}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()) {$metadata['description'] = $pagefile->description;/** @var Languages $languages */$languages = $this->wire('languages');if($languages && !$this->noLang) {foreach($languages as $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* @throws WireException**/protected function ___processInputAddFile($filename) {$total = count($this->value);$metadata = array();$rm = null;if($this->maxFiles > 1 && $total >= $this->maxFiles) return;// allow replacement of file if maxFiles is 1if($this->maxFiles == 1 && $total) {$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;foreach($ul->getOverwrittenFiles() as $bakFile => $newFile) {if(basename($newFile) != $filename) continue;$this->wire('files')->unlink($newFile);$this->wire('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;}}$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());}}/*** 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 && $replaceFile instanceof Pagefile) {$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 = $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);$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');$keys = $languages ? 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) {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;}if(isset($input['delete_' . $id])) {$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($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) {$f->error("$this->label ($pagefile->name): $error");}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(explode(' ', trim($this->extensions)));foreach($ul->execute() as $filename) {$this->processInputAddFile($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 string $extensions* @return string**/protected function formatExtensions($extensions) {return $this->wire('sanitizer')->entities(str_replace(' ', ', ', trim($extensions)));}/*** 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) {// 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;$this->itemFieldgroup = $template->fieldgroup;}/** @var Page $page */$page = $pagefiles->getFieldsPage();$id = $item ? ('_' . $this->pagefileId($item)) : '';$inputfields = $this->itemFieldgroup->getPageInputfields($page, $id, '', false);if(!$inputfields) return false;/** @var Languages|null $languages */$languages = $this->wire('languages');foreach($inputfields->getAll() as $f) {if(!$item) {// prepare inputfields for render rather than populating them$f->renderReady();continue;}/** @var Inputfield $f */$name = str_replace($id, '', $f->name);$value = $item ? $item->getFieldValue($name) : null;if($value === null) continue;if($languages && $f->getSetting('useLanguages') && $value instanceof LanguagesValueInterface) {foreach($languages as $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 {$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();/** @var InputfieldCheckbox $f */$f = $this->modules->get("InputfieldCheckbox");$f->attr('name', 'unzip');$f->attr('value', 1);$f->setAttribute('checked', $this->unzip ? 'checked' : '');$f->label = $this->_('Decompress ZIP files?');$f->description = $this->_("If checked, ZIP archives will be decompressed and all valid files added as uploads (if supported by the hosting environment). Max files must be set to 0 (no max) in order for ZIP uploads to be functional."); // Decompress ZIP files description$f->collapsed = Inputfield::collapsedBlank;$inputfields->append($f);$f = $this->modules->get("InputfieldCheckbox");$f->attr('name', 'overwrite');$f->label = $this->_('Overwrite existing files?');$f->description = $this->_('If checked, a file uploaded with the same name as an existing file will replace the existing file (description and tags will remain). If not checked, uploaded filenames will be renamed to be unique.'); // Overwrite description$f->notes = $this->_('Please note that when this option is enabled, AJAX-uploaded files are saved with the page immediately at upload, rather than when you click "save". As a result, you may wish to leave this option unchecked unless you have a specific need for it.'); // Overwrite notesif($this->overwrite) $f->attr('checked', 'checked');$f->collapsed = Inputfield::collapsedBlank;$inputfields->append($f);$f = $this->modules->get("InputfieldInteger");$f->attr('name', 'descriptionRows');$f->attr('value', $this->descriptionRows !== null ? (int) $this->descriptionRows : 1);//$f->minValue = 0;//$f->maxValue = 30;$f->label = $this->_('Number of rows for description field?');$f->description = $this->_("Enter the number of rows available for the file description field, or enter 0 to not have a description field."); // Number of rows description$inputfields->append($f);if($this->wire('languages') && $this->descriptionRows >= 1) {$f = $this->modules->get("InputfieldCheckbox");$f->attr('name', 'noLang');$f->attr('value', 1);$f->setAttribute('checked', $this->noLang ? 'checked' : '');$f->label = $this->_('Disable multi-language descriptions?');$f->description = $this->_('By default, descriptions are multi-language when you have Language Support installed. If you want to disable multi-language descriptions, check this box.'); // Disable multi-language description$inputfields->append($f);}return $inputfields;}}