Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire Page Search Process** Provides page searching within the ProcessWire admin** For more details about how Process modules work, please see:* /wire/core/Process.php** ProcessWire 3.x, Copyright 2023 by Ryan Cramer* https://processwire.com** @method string findReady($selector)* @property string $searchFields* @property string $searchFields2* @property string $displayField* @property string $operator Single-word/partial match operator* @property string $operator2 Multi-word operator* @property array $searchTypesOrder* @property array $noSearchTypes** @property bool|int $adminSearchMode Deprecated/no longer in use?***/class ProcessPageSearch extends Process implements ConfigurableModule {static public function getModuleInfo() {return array('title' => 'Page Search','summary' => 'Provides a page search engine for admin use.','version' => 108,'permanent' => true,'permission' => 'page-edit',);}/*** Default operator for text searches**/const defaultOperator = '%=';/*** Native/system sortable properties** @var array**/protected $nativeSorts = array('relevance','name','title','id','status','templates_id','parent_id','created','modified','published','modified_users_id','created_users_id','createdUser','modifiedUser','sort','sortfield',);/*** Names of all Field objects in PW** @var array**/protected $fieldOptions = array();/*** All operators where key is operator and value is description** @var array**/protected $operators = array();/*** Items per pagination** @var int**/protected $resultLimit = 25;/*** Lister instance, when applicable** @var null|ProcessPageLister**/protected $lister = null;/*** Debug mode?** @var bool**/protected $debug = true;public function __construct() {parent::__construct();$this->set('searchFields', 'title body');$this->set('searchFields2', 'title');$this->set('displayField', 'name');$this->set('operator', self::defaultOperator);$this->set('operator2', '~=');$this->set('searchTypesOrder', array('fields', 'templates', 'modules', 'pages', 'trash'));$this->set('noSearchTypes', array()); // search types that have been removed// make nativeSorts indexed by value$sorts = array();foreach($this->nativeSorts as $sort) {$sorts[$sort] = $sort;}$this->nativeSorts = $sorts;}/*** Initialize module**/public function init() {foreach($this->fields as $field) {if($field->type instanceof FieldtypeFieldsetOpen) continue;if($field->type instanceof FieldtypePassword) continue;// @todo add field access control checking$this->fieldOptions[$field->name] = $field->name;}ksort($this->fieldOptions);parent::init();}public function set($key, $value) {if($key == 'searchFields' || $key == 'searchFields2') {if(is_array($value)) $value = implode(' ', $value);} else if($key == 'noSearchTypes' && !is_array($value)) {$value = explode(' ', $value);}return parent::set($key, $value);}/*** Get operators used for searches, where key is operator and value is description** @return array**/static public function getOperators() {$operators = Selectors::getOperators(array('getIndexType' => 'operator','getValueType' => 'label',));unset($operators['#=']); // maybe laterreturn $operators;}/*** Setup items needed for full execution, as opposed to the regular search input that appears on all pages**/protected function fullSetup() {$sanitizer = $this->wire()->sanitizer;$input = $this->wire()->input;$headline = $this->_x('Search', 'headline'); // Headline for search pageif($input->get('processHeadline')) {$headline = $sanitizer->entities($sanitizer->text($input->get('processHeadline')));$this->input->whitelist('processHeadline', $headline);}$this->wire('processHeadline', $headline);$this->operators = self::getOperators();}/*** Hookable function to optionally modify selector before it is sent to $pages->find()** Not applicable when Lister is handling the search/render.** #pw-hooker** @param string $selector Selector that will be used to find pages* @return string Must return the selector (optionally modified)**/public function ___findReady($selector) {return $selector;}/*** Return instance of ProcessPageLister or null if not available** @return ProcessPageLister|null**/protected function getLister() {$modules = $this->wire()->modules;if($this->lister) return $this->lister;if($this->wire()->user->hasPermission('page-lister')) {if($modules->isInstalled('ProcessPageLister')) {$this->lister = $modules->get('ProcessPageLister');}}return $this->lister;}/*** Perform an interactive search and provide a search form (default)**/public function ___execute() {$lister = $this->getLister();$ajax = $this->wire()->config->ajax;$bookmark = (int) $this->wire()->input->get('bookmark');if($lister && ($ajax || $bookmark)) {// we will just let Lister do it's thing, since it remembers settings in sessionreturn $lister->execute();} else {$this->fullSetup();$this->processInput();list($selector, $displaySelector, $initSelector, $defaultSelector) = $this->buildSelector();}if($lister) {if(count($_GET)) $lister->sessionClear();$lister->initSelector = $initSelector;$lister->defaultSelector = $defaultSelector;$lister->defaultSort = 'relevance';$lister->set('limit', $this->resultLimit);$lister->preview = false;$lister->columns = $this->getDisplayFields();return $lister->execute();} else {$selector = $this->findReady($selector);$matches = $this->pages->find($selector);return $this->render($matches, $displaySelector);}}public function executeReset() {$lister = $this->getLister();return $lister ? $lister->executeReset() : '';}public function executeEditBookmark() {$lister = $this->getLister();return $lister ? $lister->executeEditBookmark() : '';}/*** Perform a non-interactive search (based on URL GET vars)** This is the preferred input method for links and ajax queries.** Example /search/for?template=basic-page&body*=example**/public function ___executeFor() {$languages = $this->wire()->languages;$user = $this->wire()->user;$input = $this->wire()->input;$sanitizer = $this->wire()->sanitizer;if($input->get('admin_search')) return $this->executeLive();$this->fullSetup();$selectors = array();$limit = $this->resultLimit;$start = 0;$status = 0;$names = array();$userLanguage = null;$superuser = $user->isSuperuser();$checkEditAccess = false;$hasInclude = '';$n = 0;$selectorName = $input->get('for_selector_name');if($selectorName) {$selector = $this->getForSelector($selectorName);if(strlen($selector)) $selectors['for'] = $selector;}// names to skip (must be lowercase)$skipNames = array('get','display','format_name','for_selector_name','admin_search');// names to convert (keys must be lowercase)$convertNames = array('hasparent' => 'has_parent','checkaccess' => 'check_access',);foreach($input->get as $name => $value) {$lowerName = strtolower(trim($name));if(isset($convertNames[$lowerName])) {$name = $convertNames[$lowerName];$lowerName = strtolower($name);}if(in_array($lowerName, $skipNames)) continue;if($lowerName == 'lang_id') {if($languages) {// force results for specific language$language = $languages->get((int) $value);if(!$language->id) continue;if($user->language->id != $language->id) {$userLanguage = $user->language;$user->language = $language;}}continue;}// operator has no '=', so we'll get the value from the name// so that you can do something like: bedrooms>5 rather than bedrooms>=5if(!strlen($value) && preg_match('/([^<>]+)\s*([<>])\s*([^<>]+)/', $name, $matches)) {$name = $matches[1];$operator = $matches[2];$value = $matches[3];} else {$operator = '=';$operatorChars = preg_quote(implode('', Selectors::getOperatorChars()));if(preg_match('/^(.+?)([' . $operatorChars . ']+)$/', $name, $matches)) {$name = $matches[1];$operator = $matches[2] . '=';// if unsupported operator requested, substitute '='if(!isset($this->operators[$operator])) $operator = '=';}}// replace '-' with '.' since '.' is not allowed in URL variable namesif(strpos($name, '-')) $name = str_replace('-', '.', $name);if(strpos($name, ',')) {$name = $sanitizer->names($name, ',', array('_', '.'));} else {$name = $sanitizer->fieldSubfield($name, 2);}if(!$name) continue;$lowerName = strtolower($name);if($lowerName == 'limit') {$limit = (int) $value;$input->whitelist('limit', $value);continue;}if($lowerName == 'start') {$start = (int) $value;$input->whitelist('start', $value);continue;}// if dealing with a user other than superuser, only allow include=hiddenif($lowerName == 'include') {$name = $lowerName;$value = strtolower($value);if($value != 'hidden' && !$superuser) {if($user->hasPermission('page-edit') && $input->get('admin_search')) {$value = 'unpublished';$checkEditAccess = true;} else {$value = 'hidden';}}$hasInclude = $value;}// don't allow setting of check_access property, except for superuserif($lowerName == 'check_access' && !$superuser) continue;// don't allow setting of the 'status' property, except for superuserif($lowerName == 'status') {if(!$superuser) continue;$status = (int) $value;}// replace URL-compatible comma separators with selector-compatible pipesif(strpos($name, ',')) $name = str_replace(',', '|', $name);$name = $this->filterSelectableFieldName($name);if(!strlen($name)) continue;if(strpos($value, ',')) {// commas between words: split one key=value, into multiple key=value, key=value$valuesAND = explode(',', $value);} else {$valuesAND = array($value);}foreach($valuesAND as $key => $val) {if(strpos($val, '|')) {$valuesOR = explode('|', $val);foreach($valuesOR as $k => $v) {$valuesOR[$k] = $sanitizer->selectorValue($v);}$val = implode('|', $valuesOR);} else {$val = $sanitizer->selectorValue($val);}$valuesAND[$key] = $val;}$value = implode(',', $valuesAND);$input->whitelist($name . rtrim($operator, '='), trim($value, '"\''));foreach($valuesAND as $val) {$n++;$selectors["input-$n"] = "$name$operator$val";}$names[] = $name;} // foreach inputif($start) $selectors['start'] = "start=$start";$selectors['limit'] = "limit=$limit";$displaySelector = implode(',', $selectors);if(!$status && !$hasInclude && $superuser) {// superuser only$selectors['superuser'] = "include=all, status<" . Page::statusTrash;}$selector = implode(', ', $selectors);$selector = $this->findReady($selector);$items = $this->pages->find($selector);if(!$superuser && $checkEditAccess) {// filter out non-editable pages, since some may be included via include=unpublishedforeach($items as $item) {if(!$item->editable()) $items->remove($item);}}$out = $this->render($items, $displaySelector);if($userLanguage) $user->language = $userLanguage;return $out;}/*** Execute live search** @return string**/public function executeLive() {require_once(dirname(__FILE__) . '/ProcessPageSearchLive.php');$liveSearch = new ProcessPageSearchLive($this);$liveSearch->setSearchTypesOrder($this->searchTypesOrder);$liveSearch->setNoSearchTypes($this->noSearchTypes);$liveSearch->setDefaultOperators($this->operator, $this->operator2);if($this->wire()->config->ajax) {header('Content-type: application/json');return $liveSearch->execute();} else {return $liveSearch->executeViewAll();}}/*** Get ID of the repeaters root page ID or 0 if not installed** @return int**/public function getRepeatersPageID() {$session = $this->wire()->session;$repeaterID = $session->getFor($this, 'repeaterID');if(is_int($repeaterID)) return $repeaterID;if($this->wire()->modules->isInstalled('FieldtypeRepeater')) {$repeaterPage = $this->wire()->pages->get("parent_id=" . $this->wire()->config->adminRootPageID . ", " ."name=repeaters, " ."include=all");$repeaterID = $repeaterPage->id;$session->setFor($this, 'repeaterID', (int) $repeaterID);} else {$repeaterID = 0;}return $repeaterID;}/*** Return array of fields to display in results**/protected function getDisplayFields() {$sanitizer = $this->wire()->sanitizer;$input = $this->wire()->input;$display = (string) $input->get('display');if(!strlen($display)) $display = (string) $input->get('get'); // as required by ProcessPageSearch APIif(!strlen($display)) $display = (string) $this->displayField;if(!strlen($display)) $display = 'title path';$display = str_replace(',', ' ', $display);$display = explode(' ', $display); // convert to arrayforeach($display as $key => $name) {$name = $sanitizer->fieldName($name);$display[$key] = $name;if($this->isSelectableFieldName($name)) continue;if(in_array($name, array('url', 'path', 'httpUrl'))) continue;unset($display[$key]);}return array_values($display);}/*** As an alternative to getting specific fields, return a format string** This format string must be pre-populated to session variable:* ProcessPageSearch.[format_name] = '{title} - {path}'; // format string** The name the session variable must be provided as a GET var: format_name=[name]** @return array|string**/protected function getDisplayFormat() {$name = $this->wire()->input->get('format_name');if(empty($name)) return '';$data = $this->wire()->session->getFor($this, "format_" . $name);if(empty($data)) return '';return array('name' => $name,'format' => $data['format'],'textOnly' => $data['textOnly']);}/*** Set a display format** @param string $name Session var name that will be used, output will be returned in JSON results indexed by $name as well.* @param string $format Format string to pass to $page->getMarkup(str)* @param bool $textOnly**/public function setDisplayFormat($name, $format, $textOnly = false) {$this->wire()->session->setFor($this, "format_" . $name, array('format' => $format,'textOnly' => $textOnly));}/*** Set a selector to use when $_GET['for_selector_name'] matches given $name** This is for cases where you don't want the selector to pass through user input,* and you instead just want to pass the name of it via user input. This enables* use of some features that may not be available through user selectors passing* only through user input.** Used in executeFor() mode only.** @param string $name* @param string $selector* @return string Returns URL needed to use this selector* @since 3.0.223**/public function setForSelector($name, $selector) {$this->wire()->session->setFor($this, "for_selector_$name", $selector);return $this->config->urls->admin . 'page/search/for?for_selector_name=' . urlencode($name);}/*** Get selector identified by $name that was previously set with setForSelector()** For executeFor() mode only.** #pw-internal** @param string $name* @return string* @since 3.0.223**/public function getForSelector($name) {return (string) $this->wire()->session->getFor($this, "for_selector_$name");}/*** Render the search results** @param PageArray $matches* @param string $displaySelector* @return string**/protected function render(PageArray $matches, $displaySelector = '') {$input = $this->wire()->input;$ajax = $this->wire()->config->ajax;$out = '';if($displaySelector) {$this->message(sprintf($this->_n('Found %1$d page using selector: %2$s', 'Found %1$d pages using selector: %2$s', $matches->getTotal()),$matches->getTotal(),$displaySelector));}// determine what fields will be displayed$display = array();if($ajax) $display = $this->getDisplayFormat();if(empty($display)) {$display = $this->getDisplayFields();$input->whitelist('display', implode(',', $display));}if($ajax) {// ajax json outputheader("Content-type: application/json");$out = $this->renderMatchesAjax($matches, $display, $displaySelector);} else {// html output$class = '';if((int) $input->get('show_options') !== 0 && $input->urlSegment1 != 'find') {$out = "\n<div id='ProcessPageSearchOptions'>" . $this->renderFullSearchForm() . "</div>";$class = 'show_options';}$out .="\n<div id='ProcessPageSearchResults' class='$class'>" .$this->renderMatchesTable($matches, $display) ."\n</div>";}return $out;}/*** Build a selector based upon interactive choices from the search form** Only used by execute(), not used by executeFor()** ~~~~~* Returns array(* 0 => $selector, // string, main selector for search* 1 => $displaySelector, // string, selector for display purposes* 2 => $initSelector, // string, selector for initialization in Lister (the part user cannot change)* 3 => $defaultSelector // string default selector used by Lister (the part user can change)* );* ~~~~~* @return array**/protected function buildSelector() {$input = $this->wire()->input;$sanitizer = $this->wire()->sanitizer;$user = $this->wire()->user;$pages = $this->wire()->pages;$config = $this->wire()->config;$selector = ''; // for regular ProcessPageSearch// search query text$q = (string) $input->whitelist('q');if(strlen($q)) {// GET vars "property" or "field" can used interchangablyif($input->whitelist('property')) {$searchFields = array($input->whitelist('property'));} else if($input->whitelist('field')) {$searchFields = explode(' ', $input->whitelist('field'));} else {$searchFields = $input->get('live') ? $this->searchFields2 : $this->searchFields;if(is_string($searchFields)) $searchFields = explode(' ', $searchFields);}foreach($searchFields as $fieldName) {$fieldName = $sanitizer->fieldName($fieldName);$selector .= "$fieldName|";}$selector = rtrim($selector, '|') . $this->operator . $sanitizer->selectorValue($q);}// determine if results are sorted by something other than relevance$sort = $input->whitelist('sort');if($sort && $sort != 'relevance') {$reverse = $input->whitelist('reverse') ? "-" : '';$selector .= ", sort=$reverse$sort";// if a specific template isn't requested, then locate the templates that use this field and confine the search to themif(!$input->whitelist('template') && !isset($this->nativeSorts[$sort])) {$templates = array();foreach($this->templates as $template) {if($template->fieldgroup->has($sort)) $templates[] = $template->name;}if(count($templates)) $selector .= ", template=" . implode("|", $templates);}}// determine if search limited to a specific templateif($input->whitelist('template')) {$selector .= ", template=" . $input->whitelist('template');}$trash = $input->whitelist('trash');if($trash !== null && $user->isSuperuser()) {if($trash === 0) {$selector .= ", status!=trash";} else if($trash === 1) {$selector .= ", status=trash, include=all";}}if(!$selector) {if(!$this->lister) $this->error($this->_("No search specified"));return array('','','','');}$selector = trim($selector, ", ");$displaySelector = $selector; // highlight the selector that was used for display purposes$defaultSelector = $selector; // user changable selector in Lister$initSelector = '' ; // non-user changable selector in Lister$s = ''; // anything added to this will be populated to both $selector and $initSelector below// limit results for pagination$s .= ", limit=$this->resultLimit";$adminRootPage = $pages->get($config->adminRootPageID);// exclude admin repeater pages unless the admin template is chosenif(!$input->whitelist('template')) {// but only for superuser, as we're excluding all admin pages for non-superusersif($this->user->isSuperuser()) {$repeaters = $adminRootPage->child('name=repeaters, include=all');if($repeaters->id) $s .= ", has_parent!=$repeaters->id";}}// include hidden pagesif($user->isSuperuser()) {$s .= ", include=all";} else {// non superuser doesn't get any admin pages in their results$s .= ", has_parent!=$adminRootPage";// if user has any kind of edit access, allow unpublished pages to be includedif($user->hasPermission('page-edit')) $s .= ", include=unpublished";}$selector .= $s;$initSelector .= $s;return array($selector, $displaySelector, trim($initSelector, ', '), $defaultSelector);}/*** Process input from the search form**/protected function processInput() {$user = $this->wire()->user;$input = $this->wire()->input;$sanitizer = $this->wire()->sanitizer;// search query$q = $input->get('q');if($q !== null) $this->processInputQuery($q);// search fields (can optionally contain multiple CSV field names)$field = $input->get('field');if($field) {$field = str_replace(',', ' ', $field);$fieldArray = explode(' ', $field);$field = '';foreach($fieldArray as $f) {$f = $sanitizer->fieldName($f);if(!isset($this->fieldOptions[$f]) && !isset($this->nativeSorts[$f])) continue;$field .= $f . " ";}$field = rtrim($field, " ");if($field) {$this->searchFields = $field;$input->whitelist('field', $field);}} else if($input->get('live')) {$input->whitelist('field', $this->searchFields2);} else {$input->whitelist('field', $this->searchFields);}// operator, search typeif(empty($this->operator)) $this->operator = self::defaultOperator;$operator = $input->get('operator');if(!is_null($operator)) {if(array_key_exists($operator, $this->operators)) {$this->operator = substr($this->input->get('operator'), 0, 3);} else if(ctype_digit("$operator")) {$operators = array_keys($this->operators);if(isset($operators[$operator])) $this->operator = $operators[$operator];}$input->whitelist('operator', $this->operator);}// sort$input->whitelist('sort', 'relevance');$sort = $input->get('sort');if($sort) {$sort = $sanitizer->fieldName($sort);if($sort && (isset($this->nativeSorts[$sort]) || isset($this->fieldOptions[$sort]))) {$input->whitelist('sort', $sort);}if($input->get('reverse')) {$input->whitelist('reverse', 1);}}// template$template = $input->get('template');if($template) {$template = $sanitizer->templateName($template);$template = $this->wire()->templates->get($template);if($template && $user->hasPermission('page-view', $template)) {$input->whitelist('template', $template->name);}}// trash (liveSearch)$trash = $input->get('trash');if($trash !== null && $user->isSuperuser()) {$trash = (int) $trash;if($trash === 0 || $trash === 1) {$input->whitelist('trash', $trash);}}// custom property (like 'field', except can contain only one name)$property = $input->get('property');if($property !== null) {$property = $sanitizer->fieldName($property);if($this->isSelectableFieldName($property)) {$input->whitelist('property', $property);}}}/*** Process input for the $q query variable** Since $q can also have type, field/property, operator and search text embedded within it,* this function separates all of those out and populates GET variables for them, when present.** @param $q**/protected function processInputQuery($q) {$input = $this->wire()->input;$sanitizer = $this->wire()->sanitizer;$q = trim($sanitizer->text($q));$redirectUrl = '';$operators = $this->operators;$type = '';$operators['=='] = 'Equals';$operators[':'] = 'Auto'; // alternative to '='// handle cases where search type (template), property, and operator are bundled in with the $qif(!$this->operator) $this->operator = '%=';// deetermine which operator (if any) is present in $qforeach($operators as $operator => $description) {if(strpos($q, $operator) === false) continue;if(!preg_match('/^([^=%$*+<>~^:]+)' . $operator . '([^=%$*+<>~^:]+)$/', $q, $matches)) continue;if($operator === '=') $operator = '?'; // operator to be determined on factors search textif($operator === '==') $operator = '=';$type = $sanitizer->name($matches[1]);$q = trim($matches[2]);break;}if($operator === '?') {// operator was '=': use 'contains words' operator if there is more than one word in $q$operator = strpos($q, ' ') ? '~=' : $this->operator;} else if(empty($operator)) {// operator was not present, only query text was, so use default operator$operator = $this->operator;}$input->get->set('operator', $operator);if(strpos($type, '.')) {// type with property/fieldlist($type, $field) = explode('.', $type, 2);$field = $sanitizer->fieldName(trim($field));$input->get->set('field', $field);} else {$field = '';}if($type == 'pages') {// okay} else if($type == 'trash') {$input->get->set('trash', 1);} else if($type) {$template = $this->wire()->templates->get($type);if($template) {// defined template$input->get->set('template', $template->name);} else {// some other non-page type$redirectUrl = $this->wire()->page->url . 'live/' .'?q=' . urlencode($q) .'&type=' . urlencode($type) .'&property=' . urlencode($field) .'&operator=' . urlencode($operator);}}if($redirectUrl) $this->wire()->session->redirect($redirectUrl);$input->whitelist('q', $q);}/*** Is the given field name selectable?** @param string $name Field "name" or "name1|name2|name3"* @param int $level Greater than 0 when recursive* @return bool**/protected function isSelectableFieldName($name, $level = 0) {$selectable = array('parent','template','template_label','has_parent','hasParent','children','numChildren','num_children','count','path','owner',);$notSelectable = array(// must be lowercase'pass','config','it','display',);$noSubnames = array(// must be lowercase'include','check_access','checkaccess',);$is = false;if(!$level && strpos($name, '|') !== false) {// a|b|c// note: use filterSelectableFieldName to instead remove non-selectable fields$names = explode('|', $name);$cnt = 0;foreach($names as $n) {if(!$this->isSelectableFieldName($n, $level + 1)) $cnt++;}return $cnt == 0;}if(strpos($name, '.')) {// field.subfieldlist($name, $subname) = explode('.', $name, 2);if(strpos($subname, '.') !== false && !$this->isSelectableFieldName($subname, $level + 1)) return false;if(in_array(strtolower($subname), $noSubnames)) return false;if(in_array(strtolower($subname), $notSelectable)) return false;if(!$this->isSelectableFieldName($name, $level + 1)) return false;$field = isset($this->fieldOptions[$name]) ? $this->wire()->fields->get($name) : null;if($field && $field->type) {if(!$field->viewable()) return false;if(strpos($subname, 'owner.') === 0 && wireInstanceOf($field->type, array('FieldtypePage', 'FieldtypeRepeater'))) {list(, $tername) = explode('.', $subname, 2);if($this->isSelectableFieldName($tername, $level + 1)) return true;}$info = $field->type->getSelectorInfo($field);if(isset($info['subfields'][$subname])) return true;if($field->type instanceof FieldtypePage) return $this->isSelectableFieldName($subname, $level + 1);} else if($name === 'parent' || $name === 'children' || $name === 'owner') {if(in_array($subname, $selectable)) return true;if(isset($this->nativeSorts[$subname])) return true;return $this->isSelectableFieldName($subname, $level + 1);}return false;}$lowerName = strtolower($name);if($lowerName == 'path') {if($this->wire()->languages || !$this->wire()->modules->isInstalled('PagePaths')) {$name = 'name';$lowerName = $name;}}if(isset($this->nativeSorts[$name])) {// native sort properties$is = true;} else if(in_array($name, $selectable)) {// always selectable properties$is = true;} else if(!$level && in_array($name, array('include', 'status', 'check_access'))) {// selectable, but only if not OR’d with other fields (level=0), and must be access checked outside this method$is = true;} else if(isset($this->fieldOptions[$name])) {// custom fields$field = $this->wire()->fields->get($name);$is = $field && $field->viewable();}if($is && in_array($lowerName, $notSelectable)) {$is = false;}return $is;}/*** Given string 'name' or 'name1|name2|name3' remove any 'name(s)' that are not selectable and return** @param string $name* @return string* @since 3.0.190**/protected function filterSelectableFieldName($name) {if(!strlen($name)) return '';$onlySingles = array('include', 'check_access', 'checkaccess', 'status');$names = strpos($name, '|') !== false ? explode('|', $name) : array($name);$qty = count($names);foreach($names as $key => $name) {$lowerName = strtolower($name);if(empty($name) || ($qty > 1 && in_array($lowerName, $onlySingles))) {unset($names[$key]);} else if(!$this->isSelectableFieldName($name)) {unset($names[$key]);}}return count($names) ? implode('|', $names) : '';}protected function renderFullSearchForm() {$input = $this->wire()->input;$modules = $this->wire()->modules;// Search options$out = "\n\t<p id='wrap_search_query'>";$out .="\n\t<p id='wrap_search_field'>" ."\n\t<label for='search_field'>" . $this->_('Search in field(s):') . "</label>" ."\n\t<input type='text' name='field' value='" . htmlentities($this->searchFields, ENT_QUOTES) . "' />" ."\n\t</p>";$out .="\n\t<p id='wrap_search_operator'>" ."\n\t<label for='search_operator'>" . $this->_('Type of search:') . "</label>" ."\n\t<select id='search_operator' name='operator'>";$n = 0;foreach($this->operators as $operator => $desc) {$attrs = $this->operator === $operator ? " selected='selected'" : '';$out .= "\n\t\t<option$attrs value='$n'>$desc (a" . htmlentities($operator) . "b)</option>";$n++;}$out .="\n\t</select>" ."\n\t</p>";$out .="\n\t<label class='ui-priority-primary' for='search_query'>" . $this->_('Search for:') . "</label>" ."\n\t<input id='search_query' type='text' name='q' value='" . htmlentities($input->whitelist('q'), ENT_QUOTES, "UTF-8") . "' />" ."\n\t<input type='hidden' name='show_options' value='1' />" ."\n\t</p>";// Advanced$advCollapsed = true;$out2 ="\n\t<p id='wrap_search_template'>" ."\n\t<label for='search_template'>" . $this->_('Limit to template:') . "</label>" ."\n\t<select id='search_template' name='template'>" ."\n\t\t<option></option>";$templateName = $input->whitelist('template');if($templateName) $advCollapsed = false;foreach($this->wire()->templates as $template) {$attrs = $template->name === $templateName ? " selected='selected'" : '';$out2 .= "\n\t<option$attrs>$template->name</option>";}$out2 .="\n\t</select>" ."\n\t</p>";$out2.="\n\t<p id='wrap_search_sort'>" ."\n\t<label for='search_sort'>" . $this->_('Sort by:') . "</label>" ."\n\t<select id='search_sort' name='sort'>";$sorts = $this->nativeSorts + $this->fieldOptions;$sort = $input->whitelist('sort');if($sort && $sort != 'relevance') $advCollapsed = false;foreach($sorts as $s) {if(strpos($s, ' ')) continue; // skip over multi fields$attrs = '';if($s === $sort) $attrs = " selected='selected'";$out2 .= "\n\t\t<option$attrs>$s</option>";}$out2 .="\n\t</select>" ."\n\t</p>";if($sort != 'relevance') {$reverse = $input->whitelist('reverse');$out2 .="\n\t<p id='wrap_search_options'>" ."\n\t<label><input type='checkbox' name='reverse' value='1' " . ($reverse ? "checked='checked' " : '') . "/> " . $this->_('Reverse sort?') . "</label>" ."\n\t</p>";if($reverse) $advCollapsed = false;}$display = $input->whitelist('display');$out2 .="\n\t<p id='wrap_search_display'>" ."\n\t<label for='search_display'>" . $this->_('Display field(s):') . "</label>" ."\n\t<input type='text' name='display' value='" . htmlentities($display, ENT_QUOTES) . "' />" ."\n\t</p>";if($display && $display != 'title,path') $advCollapsed = false;/** @var InputfieldSubmit $submit */$submit = $modules->get("InputfieldSubmit");$submit->attr('name', 'submit');$submit->attr('value', $this->_x('Search', 'submit')); // Search submit button for advanced search$out .= "<p>" . $submit->render() . "</p>";/** @var InputfieldForm $form */$form = $modules->get("InputfieldForm");$form->attr('id', 'ProcessPageSearchOptionsForm');$form->method = 'get';$form->action = './';/** @var InputfieldMarkup $field */$field = $modules->get("InputfieldMarkup");$field->label = $this->_("Search Options");$field->value = $out;$form->add($field);/** @var InputfieldMarkup $field */$field = $modules->get("InputfieldMarkup");if($advCollapsed) $field->collapsed = Inputfield::collapsedYes;$field->label = $this->_("Advanced");$field->value = $out2;$form->add($field);return $form->render();}/*** Render a table of matches** @param PageArray $matches* @param array $display Fields to display (from getDisplayFields method)* @return string**/protected function renderMatchesTable(PageArray $matches, array $display) {$input = $this->wire()->input;$config = $this->wire()->config;$modules = $this->wire()->modules;if(!count($matches)) return '';if(!count($display)) $display = array('path');/** @var MarkupAdminDataTable $table */$table = $modules->get("MarkupAdminDataTable");$table->setSortable(false);$table->setEncodeEntities(false);$header = $display;$header[] = "";$table->headerRow($header);foreach($matches as $match) {$match->setOutputFormatting(true);$editUrl = "{$config->urls->admin}page/edit/?id={$match->id}";$viewUrl = $match->url();$row = array();foreach($display as $name) {$value = $match->get($name);if($value instanceof Page) $value = $value->name;$value = strip_tags($value);if($name == 'created' || $name == 'modified' || $name == 'published') $value = date('Y-m-d H:i:s', $value);$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');$row[] = "<a href='$viewUrl'>$value</a>";}$row[] = $match->editable() ? "<a class='action' href='$editUrl'>" . $this->_('edit') . "</a>" : ' ';$table->row($row);}if($matches->getTotal() > count($matches)) {/** @var MarkupPagerNav $pager */$pager = $modules->get('MarkupPagerNav');if($input->urlSegment1 == 'for') $pager->setBaseUrl($this->wire()->page->url . "for/");$pager = $pager->render($matches);} else {$pager = '';}$out = $pager . $table->render() . $pager;return $out;}/*** Render the provided matches as a JSON string for AJAX use** @param PageArray $matches* @param array $display Array of fields to display, or display format associative array* @param string $selector* @return string**/protected function renderMatchesAjax(PageArray $matches, $display, $selector) {$a = array('selector' => $selector,'total' => $matches->getTotal(),'limit' => $matches->getLimit(),'start' => $matches->getStart(),'matches' => array(),);// determine which template label we'll be asking for (for multi-language support)$templateLabel = 'label';if($this->wire()->languages) {$language = $this->wire()->user->language;if($language && !$language->isDefault()) $templateLabel = "label$language";}foreach($matches as $page) {/** @var Page $page */$p = array('id' => $page->id,'parent_id' => $page->parent_id,'template' => $page->template->name,'path' => $page->path,'name' => $page->name,);if($this->adminSearchMode) {// don't include non-editable pages in admin search modeif(!$page->editable()) {$a['total']--;continue;}// include the type of match and URL to edit, when in adminSearchMode$p['type'] = $this->_x('Pages', 'match-type');$p['editUrl'] = $page->editable() ? $page->editUrl() : '';}if(isset($display['name']) && isset($display['format'])) {// use display format, returning a 'value' property containing the formatted valueif($display['textOnly']) {$value = $page->getText($display['format'], true, false);} else {$value = $page->getMarkup($display['format']);}$p[$display['name']] = $value;} else {// use display fieldsforeach($display as $key) {if($key == 'template_label') {$p['template_label'] = $page->template->$templateLabel ? $page->template->$templateLabel : $page->template->label;if(empty($p['template_label'])) $p['template_label'] = $page->template->name;continue;}$value = $page->get($key);if(empty($value) && $this->adminSearchMode) {if($key == 'title') $value = $page->name; // prevent empty title}if(is_object($value)) $value = $this->setupObjectMatch($value);if(is_array($value)) $value = $this->setupArrayMatch($value);$p[$key] = $value;}}$a['matches'][] = $p;}return json_encode($a);}/*** Convert object to an array where possible, otherwise convert to a string** For use by renderMatchesAjax** @param Page|WireData|WireArray|Wire|object $o* @return array|string**/protected function setupObjectMatch($o) {if($o instanceof Page) {return array('id' => $o->id,'parent_id' => $o->parent_id,'template' => $o->template->name,'name' => $o->name,'path' => $o->path,'title' => $o->title);}if($o instanceof WireData || $o instanceof WireArray) return $o->getArray();return (string) $o;}/*** Filter an array converting any indexes containing objects to arrays or strings** For use by renderMatchesAjax** @param array $a* @return array**/protected function setupArrayMatch(array $a) {foreach($a as $key => $value) {if(is_object($value)) $a[$key] = $this->setupObjectMatch($value);else if(is_array($value)) $a[$key] = $this->setupArrayMatch($value);}return $a;}/*** Render search for that submits to this process** @param string $placeholder Value for placeholder attribute in search input* @return string**/public function renderSearchForm($placeholder = '') {$sanitizer = $this->wire()->sanitizer;$q = substr((string) $this->wire()->input->get('q'), 0, 128);$q = $sanitizer->entities($q);$adminURL = $this->wire()->config->urls->admin;if($placeholder) {$placeholder = $sanitizer->entities1($placeholder);$placeholder = " placeholder='$placeholder'";} else {$placeholder = '';}$action = $adminURL . 'page/search/live/';$out ="\n<form id='ProcessPageSearchForm' data-action='$action' action='$action' method='get'>" ."\n\t<label for='ProcessPageSearchQuery'><i class='fa fa-search'></i></label>" ."\n\t<input type='text' id='ProcessPageSearchQuery' name='q' value='$q' $placeholder />" ."\n\t<input type='submit' id='ProcessPageSearchSubmit' name='search' value='Search' />" ."\n\t<input type='hidden' name='show_options' value='1' />" ."\n\t<span id='ProcessPageSearchStatus'></span>" ."\n</form>";return $out;}public function getModuleConfigInputfields(array $data) {$modules = $this->wire()->modules;$adminLiveSearchLabel = $this->_('Admin live search');$inputfields = $this->wire(new InputfieldWrapper());$textFields = array();$allSearchTypes = array('pages', 'trash', 'modules');$textOperators = Selectors::getOperators(array('compareType' => Selector::compareTypeFind,'getIndexType' => 'operator','getValueType' => 'label',));$textOperators['='] = SelectorEqual::getLabel();unset($textOperators['#=']);if(!isset($data['searchTypesOrder'])) $data['searchTypesOrder'] = array();if(!isset($data['noSearchTypes'])) $data['noSearchTypes'] = array();$searchTypesOrder = &$data['searchTypesOrder'];$noSearchTypes = &$data['noSearchTypes'];// find all text fieldsforeach($this->wire()->fields as $field) {if(!$field->type instanceof FieldtypeText) continue;$textFields[$field->name] = $field;}// ensure that base/built-in search types are presentforeach($allSearchTypes as $key) {if(!in_array($key, $searchTypesOrder)) $searchTypesOrder[] = $key;}// find searchable modulesforeach($modules as $module) {$info = $modules->getModuleInfoVerbose($module);if(empty($info['searchable'])) continue;$name = $info['searchable'];if(is_bool($name) || ctype_digit($name)) $name = $info['name'];$allSearchTypes[$name] = $name;if(!in_array($name, $searchTypesOrder)) $searchTypesOrder[] = $name;}/** @var InputfieldFieldset $fieldset */$fieldset = $modules->get('InputfieldFieldset');$fieldset->label = $adminLiveSearchLabel;$fieldset->icon = 'search';$inputfields->add($fieldset);/** @var InputfieldAsmSelect $f */$f = $modules->get('InputfieldAsmSelect');$f->attr('name', 'searchTypesOrder');$f->label = $this->_('Search order');$f->description =$this->_('These are the types of searches that will be performed during an admin live search.') . ' ' .$this->_('Drag them to the order you want the search results to be listed in.');foreach($allSearchTypes as $name) {$label = $name;if(in_array($name, $noSearchTypes)) $label .= ' ' . $this->_('(excluded)');$f->addOption($name, $label);}$f->attr('value', $searchTypesOrder);$f->setAsmSelectOption('deletable', false);$f->setAsmSelectOption('addable', false);$fieldset->add($f);/** @var InputfieldAsmSelect $f */$f = $modules->get('InputfieldAsmSelect');$f->attr('name', 'noSearchTypes');$f->label = $this->_('Exclude search types');$f->description =$this->_('Select any search types that you want to exclude from live search. These might be types you don’t often need to search.') . ' ' .$this->_('The more types excluded, the faster the live search will perform.') . ' ' .$this->_('Any selected types can still be searched if asked for specifically in the search.') . ' ' .$this->_('For example, if you excluded the “trash” type, it could still be searched if you prefixed your search with “trash=”, like “trash=hello”.');foreach($allSearchTypes as $name) {$f->addOption($name);}$f->attr('value', $noSearchTypes);$fieldset->add($f);/** @var InputfieldFieldset $fieldset */$fieldset = $modules->get('InputfieldFieldset');$fieldset->label = $adminLiveSearchLabel . ' ' . $this->_('(settings for pages type)');$fieldset->icon = 'search';$fieldset->themeOffset = 'm';$inputfields->add($fieldset);/** @var InputfieldAsmSelect $f */$f = $modules->get('InputfieldAsmSelect');$f->attr('name', 'searchFields2');$f->label = $this->_('Page fields to search');$f->description =$this->_('This applies to search results from “pages” and “trash” only.') . ' ' .$this->_("We recommend limiting this to 1 or 2 fields at the most to ensure the live search is fast. Typically you would just search the “title” field."); // Fields to search descriptionforeach($textFields as $field) $f->addOption($field->name);$value = isset($data['searchFields2']) ? $data['searchFields2'] : array('title');$value = !is_array($value) ? explode(' ', $value) : $value;$f->value = $value;$fieldset->add($f);/** @var InputfieldAsmSelect $f */$f = $modules->get('InputfieldAsmSelect');$f->attr('name', 'searchFields');$f->label = $this->_('Page fields to search if user hits “enter” in the search box');$f->description =$this->_('Typically this would be the same as above, but you might also want to add additional field(s).') . ' ' .$this->_('For instance, rather than just searching the “title” field, you might want to also search a “body” field as well.');foreach($textFields as $field) $f->addOption($field->name);$value = isset($data['searchFields']) ? $data['searchFields'] : array('title', 'body');$value = !is_array($value) ? explode(' ', $value) : $value;$f->value = $value;$fieldset->append($f);/** @var InputfieldSelect $f */$f = $modules->get("InputfieldSelect");$f->attr('name', 'operator');$f->attr('value', isset($data['operator']) ? $data['operator'] : self::defaultOperator);$f->label = $this->_('Default search operator for single and partial word searches');$f->columnWidth = 50;foreach($textOperators as $operator => $label) {$f->addOption($operator, "$operator $label");}$fieldset->append($f);/** @var InputfieldSelect $f */$f = $modules->get("InputfieldSelect");$f->attr('name', 'operator2');$f->attr('value', isset($data['operator2']) ? $data['operator2'] : '~=');$f->label = $this->_('Default search operator for multi-word (phrase) searches');$f->columnWidth = 50;foreach($textOperators as $operator => $label) {$f->addOption($operator, "$operator $label");}$fieldset->append($f);// displayField: no longer used, except if user lacks page-lister permission/** @var InputfieldHidden $f */$f = $modules->get("InputfieldHidden");$f->attr('name', 'displayField');$f->attr('value', isset($data['displayField']) ? $data['displayField'] : 'name');$f->label = $this->_("Default field name(s) to display in search results");$f->description = $this->_("If specifying more than one field, separate each with a space.");$inputfields->append($f);return $inputfields;}}