* @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; /** * 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'), ); $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' => "{out}", ); $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; } 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) { 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 = "
" . $this->wire('sanitizer')->entities1($description) . "
"; 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 = "$descriptionFieldLabel"; } $tabID = "langTab_{$id}__$language"; $aClass = "langTab$language"; if(!empty($langTabSettings['aClass'])) $aClass .= " " . $langTabSettings['aClass']; $tabs .= "
  • $descriptionFieldLabel
  • "; $out .= "
    "; // open wrapper } else { $out .= "
    "; // open wrapper $attrStr = "placeholder='$descriptionFieldLabel…'"; $labelClass = 'detail pw-hidden'; } $attrStr = "name='$descriptionFieldName' id='$descriptionFieldName' $attrStr"; $out .= ""; $description = $this->wire('sanitizer')->entities($pagefile->description($language)); if($this->descriptionRows > 1) { $out .= ""; } else { $out .= ""; } $out .= "
    "; // close wrapper } if($numLanguages && $hasLangTabs) { $ulClass = empty($langTabSettings['ulClass']) ? '' : " class='$langTabSettings[ulClass]'"; $ulAttr = empty($langTabSettings['ulAttrs']) ? '' : " $langTabSettings[ulAttrs]"; $out = "
      $tabs
    $out
    "; if($this->isAjax) $out .= ""; } } 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 = "
    " . "" . "" . "
    "; 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 = "

    " . wireIconMarkupFile($pagefile->basename, "fa-fw HideIfEmpty") . ' ' . "$displayName " . "" . str_replace(' ', ' ', $pagefile->filesizeStr) . " "; if(!$this->renderValueMode) $out .= ""; $description = $this->renderItemDescriptionField($pagefile, $id, $n); $class = 'InputfieldFileData '; $class .= $description ? 'description ui-widget-content' : 'InputfieldFileFields'; $out .= "

    " . $description; $inputfields = $this->getItemInputfields($pagefile); if($inputfields) $out .= $inputfields->render(); if(!$this->renderValueMode) { $out .= ""; } $out .= "
    "; 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 "$out"; } /** * 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) { // don't delete files when in render single field or fields mode if(!$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 = ""; 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 = "
    "; if($this->getSetting('noCustomButton')) { $out .= ""; } else { $out .= "
    $chooseLabel
    "; } $out .= " $formatExtensions "; if(!$this->noAjax) $out .= "  $dragDropLabel "; $out .= "
    "; // .InputfieldFileUpload return $out; } /** * Render ready * * @param Inputfield|null $parent * @param bool $renderValueMode * @return bool * */ 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 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.')); $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); } /** * 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); } $pagefile->createdUser = $this->wire('user'); $pagefile->modifiedUser = $this->wire('user'); } /** * Get markup for added file * * @param Pagefile $pagefile * @param int $n * @return string * */ protected function fileAddedGetMarkup(Pagefile $pagefile, $n) { 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 1 if($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 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; 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-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()); } } /** * 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 descriptions if($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) { $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)); 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 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 .= '' . $sanitizer->entities(implode(', ', $badExtensions)) . ''; } 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; $this->itemFieldgroup = $template->fieldgroup; } $context = ''; $process = $this->wire()->process; if($item && $process instanceof WirePageEditor) { $contextPage = $process->getPage(); if(wireInstanceOf($contextPage, 'RepeaterPage')) { $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; /** @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 = "

    " . sprintf($this->_('Field “%1$s” type “%2$s” is not supported in field “%3$s”'), $f->label, $f->className(), $this->label) . '

    '; $f->getParent()->remove($f); } } return $inputfields; } /** * Configuration settings for InputfieldFile * * @return InputfieldWrapper * */ public function ___getConfigInputfields() { $inputfields = parent::___getConfigInputfields(); require_once(__DIR__ . '/config.php'); $configuration = new InputfieldFileConfiguration(); $this->wire($configuration); $configuration->getConfigInputfields($this, $inputfields); return $inputfields; } }