1585 lines
47 KiB
Text
1585 lines
47 KiB
Text
|
<?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 later
|
|||
|
return $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 page
|
|||
|
if($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 session
|
|||
|
return $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>=5
|
|||
|
if(!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 names
|
|||
|
if(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=hidden
|
|||
|
if($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 superuser
|
|||
|
if($lowerName == 'check_access' && !$superuser) continue;
|
|||
|
|
|||
|
// don't allow setting of the 'status' property, except for superuser
|
|||
|
if($lowerName == 'status') {
|
|||
|
if(!$superuser) continue;
|
|||
|
$status = (int) $value;
|
|||
|
}
|
|||
|
|
|||
|
// replace URL-compatible comma separators with selector-compatible pipes
|
|||
|
if(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 input
|
|||
|
|
|||
|
if($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=unpublished
|
|||
|
foreach($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 API
|
|||
|
if(!strlen($display)) $display = (string) $this->displayField;
|
|||
|
if(!strlen($display)) $display = 'title path';
|
|||
|
|
|||
|
$display = str_replace(',', ' ', $display);
|
|||
|
$display = explode(' ', $display); // convert to array
|
|||
|
|
|||
|
foreach($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 output
|
|||
|
header("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 interchangably
|
|||
|
if($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 them
|
|||
|
if(!$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 template
|
|||
|
if($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 chosen
|
|||
|
if(!$input->whitelist('template')) {
|
|||
|
// but only for superuser, as we're excluding all admin pages for non-superusers
|
|||
|
if($this->user->isSuperuser()) {
|
|||
|
$repeaters = $adminRootPage->child('name=repeaters, include=all');
|
|||
|
if($repeaters->id) $s .= ", has_parent!=$repeaters->id";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// include hidden pages
|
|||
|
if($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 included
|
|||
|
if($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 type
|
|||
|
if(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 $q
|
|||
|
if(!$this->operator) $this->operator = '%=';
|
|||
|
|
|||
|
// deetermine which operator (if any) is present in $q
|
|||
|
foreach($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 text
|
|||
|
if($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/field
|
|||
|
list($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.subfield
|
|||
|
list($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 mode
|
|||
|
if(!$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 value
|
|||
|
if($display['textOnly']) {
|
|||
|
$value = $page->getText($display['format'], true, false);
|
|||
|
} else {
|
|||
|
$value = $page->getMarkup($display['format']);
|
|||
|
}
|
|||
|
$p[$display['name']] = $value;
|
|||
|
|
|||
|
} else {
|
|||
|
// use display fields
|
|||
|
foreach($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 fields
|
|||
|
foreach($this->wire()->fields as $field) {
|
|||
|
if(!$field->type instanceof FieldtypeText) continue;
|
|||
|
$textFields[$field->name] = $field;
|
|||
|
}
|
|||
|
|
|||
|
// ensure that base/built-in search types are present
|
|||
|
foreach($allSearchTypes as $key) {
|
|||
|
if(!in_array($key, $searchTypesOrder)) $searchTypesOrder[] = $key;
|
|||
|
}
|
|||
|
|
|||
|
// find searchable modules
|
|||
|
foreach($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 description
|
|||
|
foreach($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;
|
|||
|
}
|
|||
|
}
|