Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
20
core/modules/search/config/install/search.settings.yml
Normal file
20
core/modules/search/config/install/search.settings.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
and_or_limit: 7
|
||||
default_page: node_search
|
||||
index:
|
||||
cron_limit: 100
|
||||
overlap_cjk: true
|
||||
minimum_word_size: 3
|
||||
tag_weights:
|
||||
h1: 25
|
||||
h2: 18
|
||||
h3: 15
|
||||
h4: 14
|
||||
h5: 9
|
||||
h6: 6
|
||||
u: 3
|
||||
b: 3
|
||||
i: 3
|
||||
strong: 3
|
||||
em: 3
|
||||
a: 10
|
||||
logging: false
|
90
core/modules/search/config/schema/search.schema.yml
Normal file
90
core/modules/search/config/schema/search.schema.yml
Normal file
|
@ -0,0 +1,90 @@
|
|||
# Schema for the configuration files of the search module.
|
||||
|
||||
search.settings:
|
||||
type: config_object
|
||||
label: 'Search settings'
|
||||
mapping:
|
||||
and_or_limit:
|
||||
type: integer
|
||||
label: 'AND/OR combination limit'
|
||||
default_page:
|
||||
type: string
|
||||
label: 'Default search page'
|
||||
index:
|
||||
type: mapping
|
||||
label: 'Indexing settings'
|
||||
mapping:
|
||||
cron_limit:
|
||||
type: integer
|
||||
label: 'Number of items to index per cron run'
|
||||
overlap_cjk:
|
||||
type: boolean
|
||||
label: 'Simple CJK handling'
|
||||
minimum_word_size:
|
||||
type: integer
|
||||
label: 'Minimum word length to index'
|
||||
tag_weights:
|
||||
type: mapping
|
||||
label: 'HTML tags weight'
|
||||
mapping:
|
||||
h1:
|
||||
type: integer
|
||||
label: 'Tag h1 weight'
|
||||
h2:
|
||||
type: integer
|
||||
label: 'Tag h2 weight'
|
||||
h3:
|
||||
type: integer
|
||||
label: 'Tag h3 weight'
|
||||
h4:
|
||||
type: integer
|
||||
label: 'Tag h4 weight'
|
||||
h5:
|
||||
type: integer
|
||||
label: 'Tag h5 weight'
|
||||
h6:
|
||||
type: integer
|
||||
label: 'Tag h6 weight'
|
||||
u:
|
||||
type: integer
|
||||
label: 'Tag u weight'
|
||||
b:
|
||||
type: integer
|
||||
label: 'Tag b weight'
|
||||
i:
|
||||
type: integer
|
||||
label: 'Tag i weight'
|
||||
strong:
|
||||
type: integer
|
||||
label: 'Tag strong weight'
|
||||
em:
|
||||
type: integer
|
||||
label: 'Tag em weight'
|
||||
a:
|
||||
type: integer
|
||||
label: 'Tag a weight'
|
||||
logging:
|
||||
type: boolean
|
||||
label: 'Log searches'
|
||||
|
||||
search.page.*:
|
||||
type: config_entity
|
||||
label: 'Search page'
|
||||
mapping:
|
||||
id:
|
||||
type: string
|
||||
label: 'ID'
|
||||
label:
|
||||
type: label
|
||||
label: 'Label'
|
||||
path:
|
||||
type: string
|
||||
label: 'Search page path'
|
||||
weight:
|
||||
type: integer
|
||||
label: 'Weight'
|
||||
plugin:
|
||||
type: string
|
||||
label: 'Plugin'
|
||||
configuration:
|
||||
type: search.plugin.[%parent.plugin]
|
40
core/modules/search/config/schema/search.views.schema.yml
Normal file
40
core/modules/search/config/schema/search.views.schema.yml
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Schema for the views plugins of the Search module.
|
||||
|
||||
views.argument.search:
|
||||
type: views_argument
|
||||
label: 'Query key'
|
||||
|
||||
views.field.search_score:
|
||||
type: views.field.numeric
|
||||
label: 'Search score'
|
||||
mapping:
|
||||
alternate_sort:
|
||||
type: string
|
||||
label: 'Alternative sort'
|
||||
alternate_order:
|
||||
type: string
|
||||
label: 'Alternate sort order'
|
||||
|
||||
views.filter.search:
|
||||
type: views_filter
|
||||
label: 'Link to node'
|
||||
mapping:
|
||||
operator:
|
||||
type: string
|
||||
label: 'On empty input'
|
||||
|
||||
views.filter_value.search_keywords:
|
||||
type: string
|
||||
label: 'Search keywords'
|
||||
|
||||
views.row.search_view:
|
||||
type: views_row
|
||||
label: 'Source link'
|
||||
mapping:
|
||||
score:
|
||||
type: boolean
|
||||
label: 'Display score'
|
||||
|
||||
views.sort.search_score:
|
||||
type: views_sort
|
||||
label: 'Search score'
|
14
core/modules/search/css/search.admin.css
Normal file
14
core/modules/search/css/search.admin.css
Normal file
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* @file
|
||||
* Styles for administration pages.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add search page select/submit.
|
||||
*/
|
||||
.search-admin-settings .container-inline {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.search-admin-settings label[for="edit-search-type"] {
|
||||
display: block;
|
||||
}
|
7
core/modules/search/css/search.theme.css
Normal file
7
core/modules/search/css/search.theme.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* @file
|
||||
* Stylesheet for results generated by the Search module.
|
||||
*/
|
||||
ol.search-results {
|
||||
list-style: none;
|
||||
}
|
85
core/modules/search/search.api.php
Normal file
85
core/modules/search/search.api.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Search module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Preprocess text for search.
|
||||
*
|
||||
* This hook is called to preprocess both the text added to the search index
|
||||
* and the keywords users have submitted for searching. The same processing
|
||||
* needs to be applied to both so that searches will find matches.
|
||||
*
|
||||
* Possible uses:
|
||||
* - Adding spaces between words of Chinese or Japanese text.
|
||||
* - Stemming words down to their root words to allow matches between, for
|
||||
* instance, walk, walked, walking, and walks in searching.
|
||||
* - Expanding abbreviations and acronyms that occur in text.
|
||||
*
|
||||
* @param string $text
|
||||
* The text to preprocess. This is a single piece of plain text extracted
|
||||
* from between two HTML tags or from the search query. It will not contain
|
||||
* any HTML entities or HTML tags.
|
||||
* @param string|null $langcode
|
||||
* The language code for the language the text is in, if known. When this hook
|
||||
* is invoked during search indexing, the language will most likely be known
|
||||
* and passed in. This is left up to the search plugin;
|
||||
* \Drupal\node\Plugin\Search\NodeSearch does pass in the node
|
||||
* language. However, when this hook is invoked during searching, in order to
|
||||
* let a module apply the same preprocessing to the search keywords and
|
||||
* indexed text so they will match, $langcode will be NULL. A hook
|
||||
* implementation can call the getCurrentLanguage() method on the
|
||||
* 'language_manager' service to determine the current language and act
|
||||
* accordingly.
|
||||
*
|
||||
* @return string
|
||||
* The text after preprocessing. Note that if your module decides not to
|
||||
* alter the text, it should return the original text. Also, after
|
||||
* preprocessing, words in the text should be separated by a space.
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
function hook_search_preprocess($text, $langcode = NULL) {
|
||||
// If the language is not set, get it from the language manager.
|
||||
if (!isset($langcode)) {
|
||||
$langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
|
||||
}
|
||||
|
||||
// If the langcode is set to 'en' then add variations of the word "testing"
|
||||
// which can also be found during English language searches.
|
||||
if ($langcode == 'en') {
|
||||
// Add the alternate verb forms for the word "testing".
|
||||
if ($text == 'we are testing') {
|
||||
$text .= ' test tested';
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter search plugin definitions.
|
||||
*
|
||||
* @param array $definitions
|
||||
* The array of search plugin definitions, keyed by plugin ID.
|
||||
*
|
||||
* @see \Drupal\search\Annotation\SearchPlugin
|
||||
* @see \Drupal\search\SearchPluginManager
|
||||
*/
|
||||
function hook_search_plugin_alter(array &$definitions) {
|
||||
if (isset($definitions['node_search'])) {
|
||||
$definitions['node_search']['title'] = t('Nodes');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
||||
|
7
core/modules/search/search.info.yml
Normal file
7
core/modules/search/search.info.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
name: Search
|
||||
type: module
|
||||
description: 'Enables site-wide keyword searching.'
|
||||
package: Core
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
configure: entity.search_page.collection
|
156
core/modules/search/search.install
Normal file
156
core/modules/search/search.install
Normal file
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update, and uninstall functions for the Search module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function search_schema() {
|
||||
$schema['search_dataset'] = array(
|
||||
'description' => 'Stores items that will be searched.',
|
||||
'fields' => array(
|
||||
'sid' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'Search item ID, e.g. node ID for nodes.',
|
||||
),
|
||||
'langcode' => array(
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => '12',
|
||||
'not null' => TRUE,
|
||||
'description' => 'The {languages}.langcode of the item variant.',
|
||||
'default' => '',
|
||||
),
|
||||
'type' => array(
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'description' => 'Type of item, e.g. node.',
|
||||
),
|
||||
'data' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'size' => 'big',
|
||||
'description' => 'List of space-separated words from the item.',
|
||||
),
|
||||
'reindex' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'Set to force node reindexing.',
|
||||
),
|
||||
),
|
||||
'primary key' => array('sid', 'langcode', 'type'),
|
||||
);
|
||||
|
||||
$schema['search_index'] = array(
|
||||
'description' => 'Stores the search index, associating words, items and scores.',
|
||||
'fields' => array(
|
||||
'word' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'The {search_total}.word that is associated with the search item.',
|
||||
),
|
||||
'sid' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {search_dataset}.sid of the searchable item to which the word belongs.',
|
||||
),
|
||||
'langcode' => array(
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => '12',
|
||||
'not null' => TRUE,
|
||||
'description' => 'The {languages}.langcode of the item variant.',
|
||||
'default' => '',
|
||||
),
|
||||
'type' => array(
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'description' => 'The {search_dataset}.type of the searchable item to which the word belongs.',
|
||||
),
|
||||
'score' => array(
|
||||
'type' => 'float',
|
||||
'not null' => FALSE,
|
||||
'description' => 'The numeric score of the word, higher being more important.',
|
||||
),
|
||||
),
|
||||
'indexes' => array(
|
||||
'sid_type' => array('sid', 'langcode', 'type'),
|
||||
),
|
||||
'foreign keys' => array(
|
||||
'search_dataset' => array(
|
||||
'table' => 'search_dataset',
|
||||
'columns' => array(
|
||||
'sid' => 'sid',
|
||||
'langcode' => 'langcode',
|
||||
'type' => 'type',
|
||||
),
|
||||
),
|
||||
),
|
||||
'primary key' => array('word', 'sid', 'langcode', 'type'),
|
||||
);
|
||||
|
||||
$schema['search_total'] = array(
|
||||
'description' => 'Stores search totals for words.',
|
||||
'fields' => array(
|
||||
'word' => array(
|
||||
'description' => 'Primary Key: Unique word in the search index.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'count' => array(
|
||||
'description' => "The count of the word in the index using Zipf's law to equalize the probability distribution.",
|
||||
'type' => 'float',
|
||||
'not null' => FALSE,
|
||||
),
|
||||
),
|
||||
'primary key' => array('word'),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*
|
||||
* For the Status Report, return information about search index status.
|
||||
*/
|
||||
function search_requirements($phase) {
|
||||
$requirements = array();
|
||||
|
||||
if ($phase == 'runtime') {
|
||||
$remaining = 0;
|
||||
$total = 0;
|
||||
$search_page_repository = \Drupal::service('search.search_page_repository');
|
||||
foreach ($search_page_repository->getIndexableSearchPages() as $entity) {
|
||||
$status = $entity->getPlugin()->indexStatus();
|
||||
$remaining += $status['remaining'];
|
||||
$total += $status['total'];
|
||||
}
|
||||
|
||||
$done = $total - $remaining;
|
||||
// Use floor() to calculate the percentage, so if it is not quite 100%, it
|
||||
// will show as 99%, to indicate "almost done".
|
||||
$percent = ($total > 0 ? floor(100 * $done / $total) : 100);
|
||||
$requirements['search_status'] = array(
|
||||
'title' => t('Search index progress'),
|
||||
'value' => t('@percent% (@remaining remaining)', array('@percent' => $percent, '@remaining' => $remaining)),
|
||||
'severity' => REQUIREMENT_INFO,
|
||||
);
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
10
core/modules/search/search.libraries.yml
Normal file
10
core/modules/search/search.libraries.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
drupal.search.results:
|
||||
version: VERSION
|
||||
css:
|
||||
theme:
|
||||
css/search.theme.css: {}
|
||||
|
||||
admin:
|
||||
css:
|
||||
theme:
|
||||
css/search.admin.css: {}
|
10
core/modules/search/search.links.menu.yml
Normal file
10
core/modules/search/search.links.menu.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
search.view:
|
||||
title: Search
|
||||
route_name: search.view
|
||||
enabled: 0
|
||||
entity.search_page.collection:
|
||||
title: 'Search pages'
|
||||
parent: system.admin_config_search
|
||||
description: 'Configure search pages and search indexing options.'
|
||||
route_name: entity.search_page.collection
|
||||
weight: -10
|
3
core/modules/search/search.links.task.yml
Normal file
3
core/modules/search/search.links.task.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
search.plugins:
|
||||
class: \Drupal\Core\Menu\LocalTaskDefault
|
||||
deriver: \Drupal\search\Plugin\Derivative\SearchLocalTask
|
872
core/modules/search/search.module
Normal file
872
core/modules/search/search.module
Normal file
|
@ -0,0 +1,872 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Enables site-wide keyword searching.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\Component\Utility\Xss;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
|
||||
/**
|
||||
* Matches all 'N' Unicode character classes (numbers)
|
||||
*/
|
||||
define('PREG_CLASS_NUMBERS',
|
||||
'\x{30}-\x{39}\x{b2}\x{b3}\x{b9}\x{bc}-\x{be}\x{660}-\x{669}\x{6f0}-\x{6f9}' .
|
||||
'\x{966}-\x{96f}\x{9e6}-\x{9ef}\x{9f4}-\x{9f9}\x{a66}-\x{a6f}\x{ae6}-\x{aef}' .
|
||||
'\x{b66}-\x{b6f}\x{be7}-\x{bf2}\x{c66}-\x{c6f}\x{ce6}-\x{cef}\x{d66}-\x{d6f}' .
|
||||
'\x{e50}-\x{e59}\x{ed0}-\x{ed9}\x{f20}-\x{f33}\x{1040}-\x{1049}\x{1369}-' .
|
||||
'\x{137c}\x{16ee}-\x{16f0}\x{17e0}-\x{17e9}\x{17f0}-\x{17f9}\x{1810}-\x{1819}' .
|
||||
'\x{1946}-\x{194f}\x{2070}\x{2074}-\x{2079}\x{2080}-\x{2089}\x{2153}-\x{2183}' .
|
||||
'\x{2460}-\x{249b}\x{24ea}-\x{24ff}\x{2776}-\x{2793}\x{3007}\x{3021}-\x{3029}' .
|
||||
'\x{3038}-\x{303a}\x{3192}-\x{3195}\x{3220}-\x{3229}\x{3251}-\x{325f}\x{3280}-' .
|
||||
'\x{3289}\x{32b1}-\x{32bf}\x{ff10}-\x{ff19}');
|
||||
|
||||
/**
|
||||
* Matches all 'P' Unicode character classes (punctuation)
|
||||
*/
|
||||
define('PREG_CLASS_PUNCTUATION',
|
||||
'\x{21}-\x{23}\x{25}-\x{2a}\x{2c}-\x{2f}\x{3a}\x{3b}\x{3f}\x{40}\x{5b}-\x{5d}' .
|
||||
'\x{5f}\x{7b}\x{7d}\x{a1}\x{ab}\x{b7}\x{bb}\x{bf}\x{37e}\x{387}\x{55a}-\x{55f}' .
|
||||
'\x{589}\x{58a}\x{5be}\x{5c0}\x{5c3}\x{5f3}\x{5f4}\x{60c}\x{60d}\x{61b}\x{61f}' .
|
||||
'\x{66a}-\x{66d}\x{6d4}\x{700}-\x{70d}\x{964}\x{965}\x{970}\x{df4}\x{e4f}' .
|
||||
'\x{e5a}\x{e5b}\x{f04}-\x{f12}\x{f3a}-\x{f3d}\x{f85}\x{104a}-\x{104f}\x{10fb}' .
|
||||
'\x{1361}-\x{1368}\x{166d}\x{166e}\x{169b}\x{169c}\x{16eb}-\x{16ed}\x{1735}' .
|
||||
'\x{1736}\x{17d4}-\x{17d6}\x{17d8}-\x{17da}\x{1800}-\x{180a}\x{1944}\x{1945}' .
|
||||
'\x{2010}-\x{2027}\x{2030}-\x{2043}\x{2045}-\x{2051}\x{2053}\x{2054}\x{2057}' .
|
||||
'\x{207d}\x{207e}\x{208d}\x{208e}\x{2329}\x{232a}\x{23b4}-\x{23b6}\x{2768}-' .
|
||||
'\x{2775}\x{27e6}-\x{27eb}\x{2983}-\x{2998}\x{29d8}-\x{29db}\x{29fc}\x{29fd}' .
|
||||
'\x{3001}-\x{3003}\x{3008}-\x{3011}\x{3014}-\x{301f}\x{3030}\x{303d}\x{30a0}' .
|
||||
'\x{30fb}\x{fd3e}\x{fd3f}\x{fe30}-\x{fe52}\x{fe54}-\x{fe61}\x{fe63}\x{fe68}' .
|
||||
'\x{fe6a}\x{fe6b}\x{ff01}-\x{ff03}\x{ff05}-\x{ff0a}\x{ff0c}-\x{ff0f}\x{ff1a}' .
|
||||
'\x{ff1b}\x{ff1f}\x{ff20}\x{ff3b}-\x{ff3d}\x{ff3f}\x{ff5b}\x{ff5d}\x{ff5f}-' .
|
||||
'\x{ff65}');
|
||||
|
||||
/**
|
||||
* Matches CJK (Chinese, Japanese, Korean) letter-like characters.
|
||||
*
|
||||
* This list is derived from the "East Asian Scripts" section of
|
||||
* http://www.unicode.org/charts/index.html, as well as a comment on
|
||||
* http://unicode.org/reports/tr11/tr11-11.html listing some character
|
||||
* ranges that are reserved for additional CJK ideographs.
|
||||
*
|
||||
* The character ranges do not include numbers, punctuation, or symbols, since
|
||||
* these are handled separately in search. Note that radicals and strokes are
|
||||
* considered symbols. (See
|
||||
* http://www.unicode.org/Public/UNIDATA/extracted/DerivedGeneralCategory.txt)
|
||||
*
|
||||
* @see search_expand_cjk()
|
||||
*/
|
||||
define('PREG_CLASS_CJK', '\x{1100}-\x{11FF}\x{3040}-\x{309F}\x{30A1}-\x{318E}' .
|
||||
'\x{31A0}-\x{31B7}\x{31F0}-\x{31FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FCF}' .
|
||||
'\x{A000}-\x{A48F}\x{A4D0}-\x{A4FD}\x{A960}-\x{A97F}\x{AC00}-\x{D7FF}' .
|
||||
'\x{F900}-\x{FAFF}\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}\x{FF66}-\x{FFDC}' .
|
||||
'\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}');
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function search_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
case 'help.page.search':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The Search module provides the ability to set up search pages based on plugins provided by other modules. In Drupal core, there are two page-type plugins: the Content page type provides keyword searching for content managed by the Node module, and the Users page type provides keyword searching for registered users. Contributed modules may provide other page-type plugins. For more information, see <a href="!search-module">the online documentation for the Search module</a>.', array('!search-module' => 'https://www.drupal.org/documentation/modules/search')) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Configuring search pages') . '</dt>';
|
||||
$output .= '<dd>' . t('To configure search pages, visit the <a href="!search-settings">Search pages page</a>. In the Search pages section, you can add a new search page, edit the configuration of existing search pages, enable and disable search pages, and choose the default search page. Each enabled search page has a URL path starting with <em>search</em>, and each will appear as a tab or local task link on the <a href="!search-url">search page</a>; you can configure the text that is shown in the tab. In addition, some search page plugins have additional settings that you can configure for each search page.', array('!search-settings' => \Drupal::url('entity.search_page.collection'), '!search-url' => \Drupal::url('search.view'))) . '</dd>';
|
||||
$output .= '<dt>' . t('Managing the search index') . '</dt>';
|
||||
$output .= '<dd>' . t('Some search page plugins, such as the core Content search page, index searchable text using the Drupal core search index, and will not work unless content is indexed. Indexing is done during <em>cron</em> runs, so it requires a <a href="!cron">cron maintenance task</a> to be set up. There are also several settings affecting indexing that can be configured on the <a href="!search-settings">Search pages page</a>: the number of items to index per cron run, the minimum word length to index, and how to handle Chinese, Japanese, and Korean characters.', array('!cron' => \Drupal::url('system.cron_settings'), '!search-settings' => \Drupal::url('entity.search_page.collection'))) . '</dd>';
|
||||
$output .= '<dd>' . t('Modules providing search page plugins generally ensure that content-related actions on your site (creating, editing, or deleting content and comments) automatically cause affected content items to be marked for indexing or reindexing at the next cron run. When content is marked for reindexing, the previous content remains in the index until cron runs, at which time it is replaced by the new content. However, there are some actions related to the structure of your site that do not cause affected content to be marked for reindexing. Examples of structure-related actions that affect content include deleting or editing taxonomy terms, enabling or disabling modules that add text to content (such as Taxonomy, Comment, and field-providing modules), and modifying the fields or display parameters of your content types. If you take one of these actions and you want to ensure that the search index is updated to reflect your changed site structure, you can mark all content for reindexing by clicking the "Re-index site" button on the <a href="!search-settings">Search pages page</a>. If you have a lot of content on your site, it may take several cron runs for the content to be reindexed.', array('!search-settings' => \Drupal::url('entity.search_page.collection'))) . '</dd>';
|
||||
$output .= '<dt>' . t('Displaying the Search block') . '</dt>';
|
||||
$output .= '<dd>' . t('The Search module includes a block, which can be enabled and configured on the <a href="!blocks">Block layout page</a>, if you have the Block module enabled; the default block title is Search, and it is the Search form block in the Forms category, if you wish to add another instance. The block is available to users with the <a href="!search_permission">Use search</a> permission, and it performs a search using the configured default search page.', array('!blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? \Drupal::url('block.admin_display') : '#', '!search_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-search')))) . '</dd>';
|
||||
$output .= '<dt>' . t('Searching your site') . '</dt>';
|
||||
$output .= '<dd>' . t('Users with <a href="!search_permission">Use search</a> permission can use the Search block and <a href="!search">Search page</a>. Users with the <a href="!node_permission">View published content</a> permission can use configured search pages of type <em>Content</em> to search for content containing exact keywords; in addition, users with <a href="!search_permission">Use advanced search</a> permission can use more complex search filtering. Users with the <a href="!user_permission">View user information</a> permission can use configured search pages of type <em>Users</em> to search for active users containing the keyword anywhere in the username, and users with the <a href="!user_permission">Administer users</a> permission can search for active and blocked users, by email address or username keyword.', array('!search' => \Drupal::url('search.view'), '!search_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-search')), '!node_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-node')), '!user_permission' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-user')))) . '</dd>';
|
||||
$output .= '<dt>' . t('Extending the Search module') . '</dt>';
|
||||
$output .= '<dd>' . t('By default, the Search module only supports exact keyword matching in content searches. You can modify this behavior by installing a language-specific stemming module for your language (such as <a href="!porterstemmer_url">Porter Stemmer</a> for American English), which allows words such as walk, walking, and walked to be matched in the Search module. Another approach is to use a third-party search technology with stemming or partial word matching features built in, such as <a href="!solr_url">Apache Solr</a> or <a href="!sphinx_url">Sphinx</a>. There are also contributed modules that provide additional search pages. These and other <a href="!contrib-search">search-related contributed modules</a> can be downloaded by visiting Drupal.org.', array('!contrib-search' => 'https://www.drupal.org/project/project_module?f[2]=im_vid_3%3A105', '!porterstemmer_url' => 'https://www.drupal.org/project/porterstemmer', '!solr_url' => 'https://www.drupal.org/project/apachesolr', '!sphinx_url' => 'https://www.drupal.org/project/sphinx')) . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
|
||||
case 'entity.search_page.collection':
|
||||
return '<p>' . t('The search engine maintains an index of words found in your site\'s content. To build and maintain this index, a correctly configured <a href="!cron">cron maintenance task</a> is required. Indexing behavior can be adjusted using the settings below.', array('!cron' => \Drupal::url('system.status'))) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
function search_theme() {
|
||||
return array(
|
||||
'search_result' => array(
|
||||
'variables' => array('result' => NULL, 'plugin_id' => NULL),
|
||||
'file' => 'search.pages.inc',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK() for block templates.
|
||||
*/
|
||||
function search_preprocess_block(&$variables) {
|
||||
if ($variables['plugin_id'] == 'search_form_block') {
|
||||
$variables['attributes']['role'] = 'search';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears either a part of, or the entire search index.
|
||||
*
|
||||
* This function is meant for use by search page plugins, or for building a
|
||||
* user interface that lets users clear all or parts of the search index.
|
||||
*
|
||||
* @param string|null $type
|
||||
* (optional) The plugin ID or other machine-readable type for the items to
|
||||
* remove from the search index. If omitted, $sid and $langcode are ignored
|
||||
* and the entire search index is cleared.
|
||||
* @param string|null $sid
|
||||
* (optional) The ID of the items to remove from the search index. If
|
||||
* omitted, all items matching $type are cleared, and $langcode is ignored.
|
||||
* @param string|null $langcode
|
||||
* (optional) Language code of the item to remove from the search index. If
|
||||
* omitted, all items matching $sid and $type are cleared.
|
||||
*/
|
||||
function search_index_clear($type = NULL, $sid = NULL, $langcode = NULL) {
|
||||
$query_index = db_delete('search_index');
|
||||
$query_dataset = db_delete('search_dataset');
|
||||
if ($type) {
|
||||
$query_index->condition('type', $type);
|
||||
$query_dataset->condition('type', $type);
|
||||
if ($sid) {
|
||||
$query_index->condition('sid', $sid);
|
||||
$query_dataset->condition('sid', $sid);
|
||||
if ($langcode) {
|
||||
$query_index->condition('langcode', $langcode);
|
||||
$query_dataset->condition('langcode', $langcode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$query_index->execute();
|
||||
$query_dataset->execute();
|
||||
|
||||
if ($type) {
|
||||
// Invalidate all render cache items that contain data from this index.
|
||||
Cache::invalidateTags(['search_index:' . $type]);
|
||||
}
|
||||
else {
|
||||
// Invalidate all render cache items that contain data from any index.
|
||||
Cache::invalidateTags(['search_index']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a word as "dirty" (changed), or retrieves the list of dirty words.
|
||||
*
|
||||
* This is used during indexing (cron). Words that are dirty have outdated
|
||||
* total counts in the search_total table, and need to be recounted.
|
||||
*/
|
||||
function search_dirty($word = NULL) {
|
||||
$dirty = &drupal_static(__FUNCTION__, array());
|
||||
if ($word !== NULL) {
|
||||
$dirty[$word] = TRUE;
|
||||
}
|
||||
else {
|
||||
return $dirty;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_cron().
|
||||
*
|
||||
* Fires updateIndex() in the plugins for all indexable active search pages,
|
||||
* and cleans up dirty words.
|
||||
*
|
||||
* @see search_dirty()
|
||||
*/
|
||||
function search_cron() {
|
||||
// We register a shutdown function to ensure that search_total is always up
|
||||
// to date.
|
||||
drupal_register_shutdown_function('search_update_totals');
|
||||
|
||||
/** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
|
||||
$search_page_repository = \Drupal::service('search.search_page_repository');
|
||||
foreach ($search_page_repository->getIndexableSearchPages() as $entity) {
|
||||
$entity->getPlugin()->updateIndex();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the {search_total} database table.
|
||||
*
|
||||
* This function is called on shutdown to ensure that {search_total} is always
|
||||
* up to date (even if cron times out or otherwise fails).
|
||||
*/
|
||||
function search_update_totals() {
|
||||
// Update word IDF (Inverse Document Frequency) counts for new/changed words.
|
||||
foreach (search_dirty() as $word => $dummy) {
|
||||
// Get total count
|
||||
$total = db_query("SELECT SUM(score) FROM {search_index} WHERE word = :word", array(':word' => $word), array('target' => 'replica'))->fetchField();
|
||||
// Apply Zipf's law to equalize the probability distribution.
|
||||
$total = log10(1 + 1/(max(1, $total)));
|
||||
db_merge('search_total')
|
||||
->key('word', $word)
|
||||
->fields(array('count' => $total))
|
||||
->execute();
|
||||
}
|
||||
// Find words that were deleted from search_index, but are still in
|
||||
// search_total. We use a LEFT JOIN between the two tables and keep only the
|
||||
// rows which fail to join.
|
||||
$result = db_query("SELECT t.word AS realword, i.word FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL", array(), array('target' => 'replica'));
|
||||
$or = db_or();
|
||||
foreach ($result as $word) {
|
||||
$or->condition('word', $word->realword);
|
||||
}
|
||||
if (count($or) > 0) {
|
||||
db_delete('search_total')
|
||||
->condition($or)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies and preprocesses text for searching.
|
||||
*
|
||||
* Processing steps:
|
||||
* - Entities are decoded.
|
||||
* - Text is lower-cased and diacritics (accents) are removed.
|
||||
* - hook_search_preprocess() is invoked.
|
||||
* - CJK (Chinese, Japanese, Korean) characters are processed, depending on
|
||||
* the search settings.
|
||||
* - Punctuation is processed (removed or replaced with spaces, depending on
|
||||
* where it is; see code for details).
|
||||
* - Words are truncated to 50 characters maximum.
|
||||
*
|
||||
* @param string $text
|
||||
* Text to simplify.
|
||||
* @param string|null $langcode
|
||||
* Language code for the language of $text, if known.
|
||||
*
|
||||
* @return string
|
||||
* Simplified and processed text.
|
||||
*
|
||||
* @see hook_search_preprocess()
|
||||
*/
|
||||
function search_simplify($text, $langcode = NULL) {
|
||||
// Decode entities to UTF-8
|
||||
$text = Html::decodeEntities($text);
|
||||
|
||||
// Lowercase
|
||||
$text = Unicode::strtolower($text);
|
||||
|
||||
// Remove diacitics.
|
||||
$text = \Drupal::service('transliteration')->removeDiacritics($text);
|
||||
|
||||
// Call an external processor for word handling.
|
||||
search_invoke_preprocess($text, $langcode);
|
||||
|
||||
// Simple CJK handling
|
||||
if (\Drupal::config('search.settings')->get('index.overlap_cjk')) {
|
||||
$text = preg_replace_callback('/[' . PREG_CLASS_CJK . ']+/u', 'search_expand_cjk', $text);
|
||||
}
|
||||
|
||||
// To improve searching for numerical data such as dates, IP addresses
|
||||
// or version numbers, we consider a group of numerical characters
|
||||
// separated only by punctuation characters to be one piece.
|
||||
// This also means that searching for e.g. '20/03/1984' also returns
|
||||
// results with '20-03-1984' in them.
|
||||
// Readable regexp: ([number]+)[punctuation]+(?=[number])
|
||||
$text = preg_replace('/([' . PREG_CLASS_NUMBERS . ']+)[' . PREG_CLASS_PUNCTUATION . ']+(?=[' . PREG_CLASS_NUMBERS . '])/u', '\1', $text);
|
||||
|
||||
// Multiple dot and dash groups are word boundaries and replaced with space.
|
||||
// No need to use the unicode modifier here because 0-127 ASCII characters
|
||||
// can't match higher UTF-8 characters as the leftmost bit of those are 1.
|
||||
$text = preg_replace('/[.-]{2,}/', ' ', $text);
|
||||
|
||||
// The dot, underscore and dash are simply removed. This allows meaningful
|
||||
// search behavior with acronyms and URLs. See unicode note directly above.
|
||||
$text = preg_replace('/[._-]+/', '', $text);
|
||||
|
||||
// With the exception of the rules above, we consider all punctuation,
|
||||
// marks, spacers, etc, to be a word boundary.
|
||||
$text = preg_replace('/[' . Unicode::PREG_CLASS_WORD_BOUNDARY . ']+/u', ' ', $text);
|
||||
|
||||
// Truncate everything to 50 characters.
|
||||
$words = explode(' ', $text);
|
||||
array_walk($words, '_search_index_truncate');
|
||||
$text = implode(' ', $words);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits CJK (Chinese, Japanese, Korean) text into tokens.
|
||||
*
|
||||
* The Search module matches exact words, where a word is defined to be a
|
||||
* sequence of characters delimited by spaces or punctuation. CJK languages are
|
||||
* written in long strings of characters, though, not split up into words. So
|
||||
* in order to allow search matching, we split up CJK text into tokens
|
||||
* consisting of consecutive, overlapping sequences of characters whose length
|
||||
* is equal to the 'minimum_word_size' variable. This tokenizing is only done
|
||||
* if the 'overlap_cjk' variable is TRUE.
|
||||
*
|
||||
* @param array $matches
|
||||
* This function is a callback for preg_replace_callback(), which is called
|
||||
* from search_simplify(). So, $matches is an array of regular expression
|
||||
* matches, which means that $matches[0] contains the matched text -- a
|
||||
* string of CJK characters to tokenize.
|
||||
*
|
||||
* @return string
|
||||
* Tokenized text, starting and ending with a space character.
|
||||
*/
|
||||
function search_expand_cjk($matches) {
|
||||
$min = \Drupal::config('search.settings')->get('index.minimum_word_size');
|
||||
$str = $matches[0];
|
||||
$length = Unicode::strlen($str);
|
||||
// If the text is shorter than the minimum word size, don't tokenize it.
|
||||
if ($length <= $min) {
|
||||
return ' ' . $str . ' ';
|
||||
}
|
||||
$tokens = ' ';
|
||||
// Build a FIFO queue of characters.
|
||||
$chars = array();
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
// Add the next character off the beginning of the string to the queue.
|
||||
$current = Unicode::substr($str, 0, 1);
|
||||
$str = substr($str, strlen($current));
|
||||
$chars[] = $current;
|
||||
if ($i >= $min - 1) {
|
||||
// Make a token of $min characters, and add it to the token string.
|
||||
$tokens .= implode('', $chars) . ' ';
|
||||
// Shift out the first character in the queue.
|
||||
array_shift($chars);
|
||||
}
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies and splits a string into words for indexing.
|
||||
*
|
||||
* @param string $text
|
||||
* Text to process.
|
||||
* @param string|null $langcode
|
||||
* Language code for the language of $text, if known.
|
||||
*
|
||||
* @return array
|
||||
* Array of words in the simplified, preprocessed text.
|
||||
*
|
||||
* @see search_simplify()
|
||||
*/
|
||||
function search_index_split($text, $langcode = NULL) {
|
||||
$last = &drupal_static(__FUNCTION__);
|
||||
$lastsplit = &drupal_static(__FUNCTION__ . ':lastsplit');
|
||||
|
||||
if ($last == $text) {
|
||||
return $lastsplit;
|
||||
}
|
||||
// Process words
|
||||
$text = search_simplify($text, $langcode);
|
||||
$words = explode(' ', $text);
|
||||
|
||||
// Save last keyword result
|
||||
$last = $text;
|
||||
$lastsplit = $words;
|
||||
|
||||
return $words;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for array_walk in search_index_split.
|
||||
*/
|
||||
function _search_index_truncate(&$text) {
|
||||
if (is_numeric($text)) {
|
||||
$text = ltrim($text, '0');
|
||||
}
|
||||
$text = Unicode::truncate($text, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes hook_search_preprocess() to simplify text.
|
||||
*
|
||||
* @param string $text
|
||||
* Text to preprocess, passed by reference and altered in place.
|
||||
* @param string|null $langcode
|
||||
* Language code for the language of $text, if known.
|
||||
*/
|
||||
function search_invoke_preprocess(&$text, $langcode = NULL) {
|
||||
foreach (\Drupal::moduleHandler()->getImplementations('search_preprocess') as $module) {
|
||||
$text = \Drupal::moduleHandler()->invoke($module, 'search_preprocess', array($text, $langcode));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the full-text search index for a particular item.
|
||||
*
|
||||
* @param string $type
|
||||
* The plugin ID or other machine-readable type of this item,
|
||||
* which should be less than 64 bytes.
|
||||
* @param int $sid
|
||||
* An ID number identifying this particular item (e.g., node ID).
|
||||
* @param string $langcode
|
||||
* Language code for the language of the text being indexed.
|
||||
* @param string $text
|
||||
* The content of this item. Must be a piece of HTML or plain text.
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
function search_index($type, $sid, $langcode, $text) {
|
||||
$minimum_word_size = \Drupal::config('search.settings')->get('index.minimum_word_size');
|
||||
|
||||
// Multipliers for scores of words inside certain HTML tags. The weights are
|
||||
// stored in config so that modules can overwrite the default weights.
|
||||
// Note: 'a' must be included for link ranking to work.
|
||||
$tags = \Drupal::config('search.settings')->get('index.tag_weights');
|
||||
|
||||
// Strip off all ignored tags to speed up processing, but insert space before
|
||||
// and after them to keep word boundaries.
|
||||
$text = str_replace(array('<', '>'), array(' <', '> '), $text);
|
||||
$text = strip_tags($text, '<' . implode('><', array_keys($tags)) . '>');
|
||||
|
||||
// Split HTML tags from plain text.
|
||||
$split = preg_split('/\s*<([^>]+?)>\s*/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
// Note: PHP ensures the array consists of alternating delimiters and literals
|
||||
// and begins and ends with a literal (inserting $null as required).
|
||||
|
||||
$tag = FALSE; // Odd/even counter. Tag or no tag.
|
||||
$score = 1; // Starting score per word
|
||||
$accum = ' '; // Accumulator for cleaned up data
|
||||
$tagstack = array(); // Stack with open tags
|
||||
$tagwords = 0; // Counter for consecutive words
|
||||
$focus = 1; // Focus state
|
||||
|
||||
$scored_words = array(); // Accumulator for words for index
|
||||
|
||||
foreach ($split as $value) {
|
||||
if ($tag) {
|
||||
// Increase or decrease score per word based on tag
|
||||
list($tagname) = explode(' ', $value, 2);
|
||||
$tagname = Unicode::strtolower($tagname);
|
||||
// Closing or opening tag?
|
||||
if ($tagname[0] == '/') {
|
||||
$tagname = substr($tagname, 1);
|
||||
// If we encounter unexpected tags, reset score to avoid incorrect boosting.
|
||||
if (!count($tagstack) || $tagstack[0] != $tagname) {
|
||||
$tagstack = array();
|
||||
$score = 1;
|
||||
}
|
||||
else {
|
||||
// Remove from tag stack and decrement score
|
||||
$score = max(1, $score - $tags[array_shift($tagstack)]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isset($tagstack[0]) && $tagstack[0] == $tagname) {
|
||||
// None of the tags we look for make sense when nested identically.
|
||||
// If they are, it's probably broken HTML.
|
||||
$tagstack = array();
|
||||
$score = 1;
|
||||
}
|
||||
else {
|
||||
// Add to open tag stack and increment score
|
||||
array_unshift($tagstack, $tagname);
|
||||
$score += $tags[$tagname];
|
||||
}
|
||||
}
|
||||
// A tag change occurred, reset counter.
|
||||
$tagwords = 0;
|
||||
}
|
||||
else {
|
||||
// Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty values
|
||||
if ($value != '') {
|
||||
$words = search_index_split($value, $langcode);
|
||||
foreach ($words as $word) {
|
||||
// Add word to accumulator
|
||||
$accum .= $word . ' ';
|
||||
// Check wordlength
|
||||
if (is_numeric($word) || Unicode::strlen($word) >= $minimum_word_size) {
|
||||
if (!isset($scored_words[$word])) {
|
||||
$scored_words[$word] = 0;
|
||||
}
|
||||
$scored_words[$word] += $score * $focus;
|
||||
// Focus is a decaying value in terms of the amount of unique words up to this point.
|
||||
// From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words.
|
||||
$focus = min(1, .01 + 3.5 / (2 + count($scored_words) * .015));
|
||||
}
|
||||
$tagwords++;
|
||||
// Too many words inside a single tag probably mean a tag was accidentally left open.
|
||||
if (count($tagstack) && $tagwords >= 15) {
|
||||
$tagstack = array();
|
||||
$score = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$tag = !$tag;
|
||||
}
|
||||
|
||||
// Remove the item $sid from the search index, and invalidate the relevant
|
||||
// cache tags.
|
||||
search_index_clear($type, $sid, $langcode);
|
||||
|
||||
// Insert cleaned up data into dataset
|
||||
db_insert('search_dataset')
|
||||
->fields(array(
|
||||
'sid' => $sid,
|
||||
'langcode' => $langcode,
|
||||
'type' => $type,
|
||||
'data' => $accum,
|
||||
'reindex' => 0,
|
||||
))
|
||||
->execute();
|
||||
|
||||
// Insert results into search index
|
||||
foreach ($scored_words as $word => $score) {
|
||||
// If a word already exists in the database, its score gets increased
|
||||
// appropriately. If not, we create a new record with the appropriate
|
||||
// starting score.
|
||||
db_merge('search_index')
|
||||
->keys(array(
|
||||
'word' => $word,
|
||||
'sid' => $sid,
|
||||
'langcode' => $langcode,
|
||||
'type' => $type,
|
||||
))
|
||||
->fields(array('score' => $score))
|
||||
->expression('score', 'score + :score', array(':score' => $score))
|
||||
->execute();
|
||||
search_dirty($word);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the timestamp on indexed items to 'now' to force reindexing.
|
||||
*
|
||||
* This function is meant for use by search page plugins, or for building a
|
||||
* user interface that lets users mark all or parts of the search index for
|
||||
* reindexing.
|
||||
*
|
||||
* @param string $type
|
||||
* (optional) The plugin ID or other machine-readable type of this item. If
|
||||
* omitted, the entire search index is marked for reindexing, and $sid and
|
||||
* $langcode are ignored.
|
||||
* @param int $sid
|
||||
* (optional) An ID number identifying this particular item (e.g., node ID).
|
||||
* If omitted, everything matching $type is marked, and $langcode is ignored.
|
||||
* @param string $langcode
|
||||
* (optional) The language code to clear. If omitted, everything matching
|
||||
* $type and $sid is marked.
|
||||
*/
|
||||
function search_mark_for_reindex($type = NULL, $sid = NULL, $langcode = NULL) {
|
||||
$query = db_update('search_dataset')
|
||||
->fields(array('reindex' => REQUEST_TIME))
|
||||
// Only mark items that were not previously marked for reindex, so that
|
||||
// marked items maintain their priority by request time.
|
||||
->condition('reindex', 0);
|
||||
|
||||
if ($type) {
|
||||
$query->condition('type', $type);
|
||||
if ($sid) {
|
||||
$query->condition('sid', $sid);
|
||||
if ($langcode) {
|
||||
$query->condition('langcode', $langcode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @defgroup search Search interface
|
||||
* @{
|
||||
* The Drupal search interface manages a global search mechanism.
|
||||
*
|
||||
* Modules may plug into this system to provide searches of different types of
|
||||
* data. Most of the system is handled by the Search module, so this must be
|
||||
* enabled for all of the search features to work.
|
||||
*
|
||||
* There are two ways to interact with the search system:
|
||||
* - Specifically for searching nodes, you can implement
|
||||
* hook_node_update_index() and hook_node_search_result(). However, note that
|
||||
* the search system already indexes all visible output of a node; i.e.,
|
||||
* everything displayed normally during node viewing. This is
|
||||
* usually sufficient. You should only use this mechanism if you want
|
||||
* additional, non-visible data to be indexed.
|
||||
* - Define a plugin implementing \Drupal\search\Plugin\SearchInterface and
|
||||
* annotated as \Drupal\search\Annotation\SearchPlugin. This will create a
|
||||
* search page type that users can use to set up one or more search pages.
|
||||
* Each of these corresponds to a tab on the /search page, which can be
|
||||
* used to perform searches. You will also need to implement the execute()
|
||||
* method from the interface to perform the search. A base class is provided
|
||||
* in \Drupal\search\Plugin\SearchPluginBase. For more information about
|
||||
* plugins, see the @link plugin_api Plugin API topic. @endlink
|
||||
*
|
||||
* If your module needs to provide a more complicated search form, then you
|
||||
* need to implement it yourself. In that case, you may wish to define it as a
|
||||
* local task (tab) under the /search page (e.g. /search/mymodule) so that users
|
||||
* can easily find it.
|
||||
*
|
||||
* @see plugin_api
|
||||
* @see annotation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns snippets from a piece of text, with search keywords highlighted.
|
||||
*
|
||||
* Used for formatting search results.
|
||||
*
|
||||
* @param string $keys
|
||||
* A string containing a search query.
|
||||
* @param string $text
|
||||
* The text to extract fragments from.
|
||||
* @param string|null $langcode
|
||||
* Language code for the language of $text, if known.
|
||||
*
|
||||
* @return string
|
||||
* A string containing HTML for the excerpt.
|
||||
*/
|
||||
function search_excerpt($keys, $text, $langcode = NULL) {
|
||||
// We highlight around non-indexable or CJK characters.
|
||||
$boundary = '(?:(?<=[' . Unicode::PREG_CLASS_WORD_BOUNDARY . PREG_CLASS_CJK . '])|(?=[' . Unicode::PREG_CLASS_WORD_BOUNDARY . PREG_CLASS_CJK . ']))';
|
||||
|
||||
// Extract positive keywords and phrases.
|
||||
preg_match_all('/ ("([^"]+)"|(?!OR)([^" ]+))/', ' ' . $keys, $matches);
|
||||
$keys = array_merge($matches[2], $matches[3]);
|
||||
|
||||
// Prepare text by stripping HTML tags and decoding HTML entities.
|
||||
$text = strip_tags(str_replace(array('<', '>'), array(' <', '> '), $text));
|
||||
$text = Html::decodeEntities($text);
|
||||
$text_length = strlen($text);
|
||||
|
||||
// Make a list of unique keywords that are actually found in the text,
|
||||
// which could be items in $keys or replacements that are equivalent through
|
||||
// search_simplify().
|
||||
$temp_keys = array();
|
||||
foreach ($keys as $key) {
|
||||
$key = _search_find_match_with_simplify($key, $text, $boundary, $langcode);
|
||||
if (isset($key)) {
|
||||
// Quote slashes so they can be used in regular expressions.
|
||||
$temp_keys[] = preg_quote($key, '/');
|
||||
}
|
||||
}
|
||||
// Several keywords could have simplified down to the same thing, so pick
|
||||
// out the unique ones.
|
||||
$keys = array_unique($temp_keys);
|
||||
|
||||
// Extract fragments of about 60 characters around keywords, bounded by word
|
||||
// boundary characters. Try to reach 256 characters, using second occurrences
|
||||
// if necessary.
|
||||
$ranges = array();
|
||||
$length = 0;
|
||||
$look_start = array();
|
||||
$remaining_keys = $keys;
|
||||
|
||||
while ($length < 256 && !empty($remaining_keys)) {
|
||||
$found_keys = array();
|
||||
foreach ($remaining_keys as $key) {
|
||||
if ($length >= 256) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Remember where we last found $key, in case we are coming through a
|
||||
// second time.
|
||||
if (!isset($look_start[$key])) {
|
||||
$look_start[$key] = 0;
|
||||
}
|
||||
|
||||
// See if we can find $key after where we found it the last time. Since
|
||||
// we are requiring a match on a word boundary, make sure $text starts
|
||||
// and ends with a space.
|
||||
$matches = array();
|
||||
if (preg_match('/' . $boundary . $key . $boundary . '/iu', ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) {
|
||||
$found_position = $matches[0][1];
|
||||
$look_start[$key] = $found_position + 1;
|
||||
// Keep track of which keys we found this time, in case we need to
|
||||
// pass through again to find more text.
|
||||
$found_keys[] = $key;
|
||||
|
||||
// Locate a space before and after this match, leaving about 60
|
||||
// characters of context on each end.
|
||||
$before = strpos(' ' . $text, ' ', max(0, $found_position - 61));
|
||||
if ($before !== FALSE && $before <= $found_position) {
|
||||
if ($text_length > $found_position + 60) {
|
||||
$after = strrpos(substr($text, 0, $found_position + 60), ' ', $found_position);
|
||||
}
|
||||
else {
|
||||
$after = $text_length;
|
||||
}
|
||||
if ($after !== FALSE && $after > $found_position) {
|
||||
// Account for the spaces we added.
|
||||
$before = max($before - 1, 0);
|
||||
if ($before < $after) {
|
||||
// Save this range.
|
||||
$ranges[$before] = $after;
|
||||
$length += $after - $before;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Next time through this loop, only look for keys we found this time,
|
||||
// if any.
|
||||
$remaining_keys = $found_keys;
|
||||
}
|
||||
|
||||
if (empty($ranges)) {
|
||||
// We didn't find any keyword matches, so just return the first part of the
|
||||
// text. We also need to re-encode any HTML special characters that we
|
||||
// entity-decoded above.
|
||||
return SafeMarkup::checkPlain(Unicode::truncate($text, 256, TRUE, TRUE));
|
||||
}
|
||||
|
||||
// Sort the text ranges by starting position.
|
||||
ksort($ranges);
|
||||
|
||||
// Collapse overlapping text ranges into one. The sorting makes it O(n).
|
||||
$new_ranges = array();
|
||||
$max_end = 0;
|
||||
foreach ($ranges as $this_from => $this_to) {
|
||||
$max_end = max($max_end, $this_to);
|
||||
if (!isset($working_from)) {
|
||||
// This is the first time through this loop: initialize.
|
||||
$working_from = $this_from;
|
||||
$working_to = $this_to;
|
||||
continue;
|
||||
}
|
||||
if ($this_from <= $working_to) {
|
||||
// The ranges overlap: combine them.
|
||||
$working_to = max($working_to, $this_to);
|
||||
}
|
||||
else {
|
||||
// The ranges do not overlap: save the working range and start a new one.
|
||||
$new_ranges[$working_from] = $working_to;
|
||||
$working_from = $this_from;
|
||||
$working_to = $this_to;
|
||||
}
|
||||
}
|
||||
// Save the remaining working range.
|
||||
$new_ranges[$working_from] = $working_to;
|
||||
|
||||
// Fetch text within the combined ranges we found.
|
||||
$out = array();
|
||||
foreach ($new_ranges as $from => $to) {
|
||||
$out[] = substr($text, $from, $to - $from);
|
||||
}
|
||||
|
||||
// Combine the text chunks with "…" separators. The "…" needs to be
|
||||
// translated. Let translators have the … separator text as one chunk.
|
||||
$ellipses = explode('!excerpt', t('… !excerpt … !excerpt …'));
|
||||
$text = (isset($new_ranges[0]) ? '' : $ellipses[0]) . implode($ellipses[1], $out) . (($max_end < strlen($text) - 1) ? $ellipses[2] : '');
|
||||
$text = SafeMarkup::checkPlain($text);
|
||||
|
||||
// Highlight keywords. Must be done at once to prevent conflicts ('strong'
|
||||
// and '<strong>').
|
||||
$text = trim(preg_replace('/' . $boundary . '(?:' . implode('|', $keys) . ')' . $boundary . '/iu', '<strong>\0</strong>', ' ' . $text . ' '));
|
||||
return Xss::filter($text, ['strong']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "defgroup search".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Finds an appropriate keyword in text.
|
||||
*
|
||||
* @param string $key
|
||||
* The keyword to find.
|
||||
* @param string $text
|
||||
* The text to search for the keyword.
|
||||
* @param string $boundary
|
||||
* Regular expression for boundary characters between words.
|
||||
* @param string|null $langcode
|
||||
* Language code for the language of $text, if known.
|
||||
*
|
||||
* @return
|
||||
* A segment of $text that is between word boundary characters that either
|
||||
* matches $key directly, or matches $key when both this text segment and
|
||||
* $key are processed by search_simplify(). If a matching text segment is
|
||||
* not located, NULL is returned.
|
||||
*/
|
||||
function _search_find_match_with_simplify($key, $text, $boundary, $langcode = NULL) {
|
||||
// See if $key appears as-is. When testing, make sure $text starts/ends with
|
||||
// a space, because we require $key to be surrounded by word boundary
|
||||
// characters.
|
||||
$temp = trim($key);
|
||||
if ($temp == '') {
|
||||
return NULL;
|
||||
}
|
||||
if (preg_match('/' . $boundary . preg_quote($temp, '/') . $boundary . '/iu', ' ' . $text . ' ')) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
// Run both text and key through search_simplify.
|
||||
$simplified_key = trim(search_simplify($key, $langcode));
|
||||
$simplified_text = trim(search_simplify($text, $langcode));
|
||||
if ($simplified_key == '' || $simplified_text == '' || strpos($simplified_text, $simplified_key) === FALSE) {
|
||||
// The simplified keyword and text do not match at all, or are empty.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Split $text into words, keeping track of where the word boundaries are.
|
||||
$words = preg_split('/' . $boundary . '/iu', $text, NULL, PREG_SPLIT_OFFSET_CAPTURE);
|
||||
// Add an entry pointing to the end of the string, for the loop below.
|
||||
$words[] = array('', strlen($text));
|
||||
$num_words = count($words);
|
||||
|
||||
// Find the smallest segment of complete words in $text that we can simplify
|
||||
// to match $simplified_key.
|
||||
$start_position = 0;
|
||||
$word_end = 0;
|
||||
for ($word_index = 0; $word_index < $num_words; $word_index++) {
|
||||
// See if we can move the starting position out from our previously-saved
|
||||
// best position to here and still have a match.
|
||||
$trial_position = $words[$word_index][1];
|
||||
if ($trial_position < strlen($text)) {
|
||||
$candidate = substr($text, $trial_position);
|
||||
$test_text = trim(search_simplify($candidate, $langcode));
|
||||
if (strpos($test_text, $simplified_key) !== FALSE) {
|
||||
$start_position = $trial_position;
|
||||
$word_end = $trial_position + strlen($words[$word_index][0]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// See if we can end at our currently-saved word-ending position and still
|
||||
// match, in which case this is the minimal matching string.
|
||||
if ($word_end > $start_position) {
|
||||
$candidate = substr($text, $start_position, $word_end - $start_position);
|
||||
$test_text = trim(search_simplify($candidate, $langcode));
|
||||
if (strpos($test_text, $simplified_key) !== FALSE) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the end position of this word for the next time through this loop.
|
||||
$word_end = $trial_position + strlen($words[$word_index][0]);
|
||||
}
|
||||
|
||||
// If we get here, we couldn't find a match.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter() for the search_block_form form.
|
||||
*
|
||||
* Since the exposed form is a GET form, we don't want it to send the form
|
||||
* tokens. However, you cannot make this happen in the form builder function
|
||||
* itself, because the tokens are added to the form after the builder function
|
||||
* is called. So, we have to do it in a form_alter.
|
||||
*
|
||||
* @see \Drupal\search\Form\SearchBlockForm
|
||||
*/
|
||||
function search_form_search_block_form_alter(&$form, FormStateInterface $form_state) {
|
||||
$form['form_build_id']['#access'] = FALSE;
|
||||
$form['form_token']['#access'] = FALSE;
|
||||
$form['form_id']['#access'] = FALSE;
|
||||
}
|
68
core/modules/search/search.pages.inc
Normal file
68
core/modules/search/search.pages.inc
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* User page callbacks for the Search module.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_theme_suggestions_HOOK().
|
||||
*/
|
||||
function search_theme_suggestions_search_result(array $variables) {
|
||||
return array('search_result__' . $variables['plugin_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares variables for individual search result templates.
|
||||
*
|
||||
* Default template: search-result.html.twig
|
||||
*
|
||||
* @param array $variables
|
||||
* An array with the following elements:
|
||||
* - result: Individual search result.
|
||||
* - plugin_id: Plugin the search results came from.
|
||||
* - title_prefix: Additional output populated by modules, intended to be
|
||||
* displayed in front of the main title tag that appears in the template.
|
||||
* - title_suffix: Additional output populated by modules, intended to be
|
||||
* displayed after the main title tag that appears in the template.
|
||||
* - title_attributes: HTML attributes for the title.
|
||||
* - content_attributes: HTML attributes for the content.
|
||||
*/
|
||||
function template_preprocess_search_result(&$variables) {
|
||||
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
|
||||
|
||||
$result = $variables['result'];
|
||||
$variables['url'] = check_url($result['link']);
|
||||
$variables['title'] = SafeMarkup::checkPlain($result['title']);
|
||||
if (isset($result['language']) && $result['language'] != $language_interface->getId() && $result['language'] != LanguageInterface::LANGCODE_NOT_SPECIFIED) {
|
||||
$variables['title_attributes']['lang'] = $result['language'];
|
||||
$variables['content_attributes']['lang'] = $result['language'];
|
||||
}
|
||||
|
||||
$info = array();
|
||||
if (!empty($result['plugin_id'])) {
|
||||
$info['plugin_id'] = SafeMarkup::checkPlain($result['plugin_id']);
|
||||
}
|
||||
if (!empty($result['user'])) {
|
||||
$info['user'] = $result['user'];
|
||||
}
|
||||
if (!empty($result['date'])) {
|
||||
$info['date'] = format_date($result['date'], 'short');
|
||||
}
|
||||
if (isset($result['extra']) && is_array($result['extra'])) {
|
||||
$info = array_merge($info, $result['extra']);
|
||||
}
|
||||
// Check for existence. User search does not include snippets.
|
||||
$variables['snippet'] = isset($result['snippet']) ? $result['snippet'] : '';
|
||||
// Provide separated and grouped meta information..
|
||||
$variables['info_split'] = $info;
|
||||
$variables['info'] = array(
|
||||
'#type' => 'inline_template',
|
||||
'#template' => '{{ info|safe_join(" - ") }}',
|
||||
'#context' => array('info' => $info),
|
||||
);
|
||||
}
|
||||
|
6
core/modules/search/search.permissions.yml
Normal file
6
core/modules/search/search.permissions.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
administer search:
|
||||
title: 'Administer search'
|
||||
search content:
|
||||
title: 'Use search'
|
||||
use advanced search:
|
||||
title: 'Use advanced search'
|
65
core/modules/search/search.routing.yml
Normal file
65
core/modules/search/search.routing.yml
Normal file
|
@ -0,0 +1,65 @@
|
|||
entity.search_page.collection:
|
||||
path: '/admin/config/search/pages'
|
||||
defaults:
|
||||
_entity_list: 'search_page'
|
||||
_title: 'Search pages'
|
||||
requirements:
|
||||
_permission: 'administer search'
|
||||
|
||||
search.reindex_confirm:
|
||||
path: '/admin/config/search/pages/reindex'
|
||||
defaults:
|
||||
_form: '\Drupal\search\Form\ReindexConfirm'
|
||||
_title: 'Clear index'
|
||||
requirements:
|
||||
_permission: 'administer search'
|
||||
|
||||
search.add_type:
|
||||
path: '/admin/config/search/pages/add/{search_plugin_id}'
|
||||
defaults:
|
||||
_entity_form: 'search_page.add'
|
||||
_title: 'Add new search page'
|
||||
requirements:
|
||||
_entity_create_access: 'search_page'
|
||||
|
||||
entity.search_page.edit_form:
|
||||
path: '/admin/config/search/pages/manage/{search_page}'
|
||||
defaults:
|
||||
_entity_form: 'search_page.edit'
|
||||
_title_callback: '\Drupal\search\Controller\SearchController::editTitle'
|
||||
requirements:
|
||||
_entity_access: 'search_page.update'
|
||||
|
||||
entity.search_page.enable:
|
||||
path: '/admin/config/search/pages/manage/{search_page}/enable'
|
||||
defaults:
|
||||
_controller: '\Drupal\search\Controller\SearchController::performOperation'
|
||||
op: 'enable'
|
||||
requirements:
|
||||
_entity_access: 'search_page.update'
|
||||
|
||||
entity.search_page.disable:
|
||||
path: '/admin/config/search/pages/manage/{search_page}/disable'
|
||||
defaults:
|
||||
_controller: '\Drupal\search\Controller\SearchController::performOperation'
|
||||
op: 'disable'
|
||||
requirements:
|
||||
_entity_access: 'search_page.disable'
|
||||
|
||||
entity.search_page.set_default:
|
||||
path: '/admin/config/search/pages/manage/{search_page}/set-default'
|
||||
defaults:
|
||||
_controller: '\Drupal\search\Controller\SearchController::setAsDefault'
|
||||
requirements:
|
||||
_entity_access: 'search_page.update'
|
||||
|
||||
entity.search_page.delete_form:
|
||||
path: '/admin/config/search/pages/manage/{search_page}/delete'
|
||||
defaults:
|
||||
_entity_form: 'search_page.delete'
|
||||
_title: 'Delete'
|
||||
requirements:
|
||||
_entity_access: 'search_page.delete'
|
||||
|
||||
route_callbacks:
|
||||
- '\Drupal\search\Routing\SearchPageRoutes::routes'
|
8
core/modules/search/search.services.yml
Normal file
8
core/modules/search/search.services.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
services:
|
||||
plugin.manager.search:
|
||||
class: Drupal\search\SearchPluginManager
|
||||
parent: default_plugin_manager
|
||||
|
||||
search.search_page_repository:
|
||||
class: Drupal\search\SearchPageRepository
|
||||
arguments: ['@config.factory', '@entity.manager']
|
45
core/modules/search/src/Annotation/SearchPlugin.php
Normal file
45
core/modules/search/src/Annotation/SearchPlugin.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Annotation\SearchPlugin.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
|
||||
/**
|
||||
* Defines a SearchPlugin type annotation object.
|
||||
*
|
||||
* SearchPlugin classes define search types for the core Search module. Each
|
||||
* search type can be used to create search pages from the Search settings page.
|
||||
*
|
||||
* @see SearchPluginBase
|
||||
*
|
||||
* @ingroup search
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class SearchPlugin extends Plugin {
|
||||
|
||||
/**
|
||||
* A unique identifier for the search plugin.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* The title for the search page tab.
|
||||
*
|
||||
* @todo This will potentially be translated twice or cached with the wrong
|
||||
* translation until the search tabs are converted to local task plugins.
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*/
|
||||
public $title;
|
||||
|
||||
}
|
234
core/modules/search/src/Controller/SearchController.php
Normal file
234
core/modules/search/src/Controller/SearchController.php
Normal file
|
@ -0,0 +1,234 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Controller\SearchController.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Controller;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Config\ConfigFactory;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\search\SearchPageInterface;
|
||||
use Drupal\search\SearchPageRepositoryInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Route controller for search.
|
||||
*/
|
||||
class SearchController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The search page repository.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageRepositoryInterface
|
||||
*/
|
||||
protected $searchPageRepository;
|
||||
|
||||
/**
|
||||
* A logger instance.
|
||||
*
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Constructs a new search controller.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
|
||||
* The search page repository.
|
||||
* @param \Psr\Log\LoggerInterface $logger
|
||||
* A logger instance.
|
||||
*/
|
||||
public function __construct(SearchPageRepositoryInterface $search_page_repository, LoggerInterface $logger) {
|
||||
$this->searchPageRepository = $search_page_repository;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('search.search_page_repository'),
|
||||
$container->get('logger.factory')->get('search')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a render array for the search page.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request object.
|
||||
* @param \Drupal\search\SearchPageInterface $entity
|
||||
* The search page entity.
|
||||
*
|
||||
* @return array
|
||||
* The search form and search results build array.
|
||||
*/
|
||||
public function view(Request $request, SearchPageInterface $entity) {
|
||||
$build = array();
|
||||
$plugin = $entity->getPlugin();
|
||||
|
||||
// Build the form first, because it may redirect during the submit,
|
||||
// and we don't want to build the results based on last time's request.
|
||||
if ($request->query->has('keys')) {
|
||||
$keys = trim($request->get('keys'));
|
||||
$plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
|
||||
}
|
||||
|
||||
$build['#title'] = $plugin->suggestedTitle();
|
||||
$build['search_form'] = $this->entityFormBuilder()->getForm($entity, 'search');
|
||||
|
||||
// Build search results, if keywords or other search parameters are in the
|
||||
// GET parameters. Note that we need to try the search if 'keys' is in
|
||||
// there at all, vs. being empty, due to advanced search.
|
||||
$results = array();
|
||||
if ($request->query->has('keys')) {
|
||||
if ($plugin->isSearchExecutable()) {
|
||||
// Log the search.
|
||||
if ($this->config('search.settings')->get('logging')) {
|
||||
$this->logger->notice('Searched %type for %keys.', array('%keys' => $keys, '%type' => $entity->label()));
|
||||
}
|
||||
|
||||
// Collect the search results.
|
||||
$results = $plugin->buildResults();
|
||||
}
|
||||
else {
|
||||
// The search not being executable means that no keywords or other
|
||||
// conditions were entered.
|
||||
drupal_set_message($this->t('Please enter some keywords.'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
if (count($results)) {
|
||||
$build['search_results_title'] = array(
|
||||
'#markup' => '<h2>' . $this->t('Search results') . '</h2>',
|
||||
);
|
||||
}
|
||||
|
||||
$build['search_results'] = array(
|
||||
'#theme' => array('item_list__search_results__' . $plugin->getPluginId(), 'item_list__search_results'),
|
||||
'#items' => $results,
|
||||
'#empty' => array(
|
||||
'#markup' => '<h3>' . $this->t('Your search yielded no results.') . '</h3>',
|
||||
),
|
||||
'#list_type' => 'ol',
|
||||
'#cache' => array(
|
||||
'tags' => $entity->getCacheTags(),
|
||||
),
|
||||
'#context' => array(
|
||||
'plugin' => $plugin->getPluginId(),
|
||||
),
|
||||
);
|
||||
|
||||
// If this plugin uses a search index, then also add the cache tag tracking
|
||||
// that search index, so that cached search result pages are invalidated
|
||||
// when necessary.
|
||||
if ($plugin->getType()) {
|
||||
$build['search_results']['#cache']['tags'][] = 'search_index';
|
||||
$build['search_results']['#cache']['tags'][] = 'search_index:' . $plugin->getType();
|
||||
}
|
||||
|
||||
$build['pager'] = array(
|
||||
'#type' => 'pager',
|
||||
);
|
||||
|
||||
$build['#attached']['library'][] = 'search/drupal.search.results';
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a render array for the search help page.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request object.
|
||||
* @param \Drupal\search\SearchPageInterface $entity
|
||||
* The search page entity.
|
||||
*
|
||||
* @return array
|
||||
* The search help page.
|
||||
*/
|
||||
public function searchHelp(SearchPageInterface $entity) {
|
||||
$build = array();
|
||||
|
||||
$build['search_help'] = $entity->getPlugin()->getHelp();
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to a search page.
|
||||
*
|
||||
* This is used to redirect from /search to the default search page.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageInterface $entity
|
||||
* The search page entity.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
* A redirect to the search page.
|
||||
*/
|
||||
public function redirectSearchPage(SearchPageInterface $entity) {
|
||||
return $this->redirect('search.view_' . $entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Route title callback.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageInterface $search_page
|
||||
* The search page entity.
|
||||
*
|
||||
* @return string
|
||||
* The title for the search page edit form.
|
||||
*/
|
||||
public function editTitle(SearchPageInterface $search_page) {
|
||||
return $this->t('Edit %label search page', array('%label' => $search_page->label()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an operation on the search page entity.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageInterface $search_page
|
||||
* The search page entity.
|
||||
* @param string $op
|
||||
* The operation to perform, usually 'enable' or 'disable'.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
* A redirect back to the search settings page.
|
||||
*/
|
||||
public function performOperation(SearchPageInterface $search_page, $op) {
|
||||
$search_page->$op()->save();
|
||||
|
||||
if ($op == 'enable') {
|
||||
drupal_set_message($this->t('The %label search page has been enabled.', array('%label' => $search_page->label())));
|
||||
}
|
||||
elseif ($op == 'disable') {
|
||||
drupal_set_message($this->t('The %label search page has been disabled.', array('%label' => $search_page->label())));
|
||||
}
|
||||
|
||||
$url = $search_page->urlInfo('collection');
|
||||
return $this->redirect($url->getRouteName(), $url->getRouteParameters(), $url->getOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the search page as the default.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageInterface $search_page
|
||||
* The search page entity.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
* A redirect to the search settings page.
|
||||
*/
|
||||
public function setAsDefault(SearchPageInterface $search_page) {
|
||||
// Set the default page to this search page.
|
||||
$this->searchPageRepository->setDefaultSearchPage($search_page);
|
||||
|
||||
drupal_set_message($this->t('The default search page is now %label. Be sure to check the ordering of your search pages.', array('%label' => $search_page->label())));
|
||||
return $this->redirect('entity.search_page.collection');
|
||||
}
|
||||
|
||||
}
|
264
core/modules/search/src/Entity/SearchPage.php
Normal file
264
core/modules/search/src/Entity/SearchPage.php
Normal file
|
@ -0,0 +1,264 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Entity\SearchPage.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityBase;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Component\Plugin\ConfigurablePluginInterface;
|
||||
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
|
||||
use Drupal\search\Plugin\SearchIndexingInterface;
|
||||
use Drupal\search\Plugin\SearchPluginCollection;
|
||||
use Drupal\search\SearchPageInterface;
|
||||
|
||||
/**
|
||||
* Defines a configured search page.
|
||||
*
|
||||
* @ConfigEntityType(
|
||||
* id = "search_page",
|
||||
* label = @Translation("Search page"),
|
||||
* handlers = {
|
||||
* "access" = "Drupal\search\SearchPageAccessControlHandler",
|
||||
* "list_builder" = "Drupal\search\SearchPageListBuilder",
|
||||
* "form" = {
|
||||
* "add" = "Drupal\search\Form\SearchPageAddForm",
|
||||
* "edit" = "Drupal\search\Form\SearchPageEditForm",
|
||||
* "search" = "Drupal\search\Form\SearchPageForm",
|
||||
* "delete" = "Drupal\Core\Entity\EntityDeleteForm"
|
||||
* }
|
||||
* },
|
||||
* admin_permission = "administer search",
|
||||
* links = {
|
||||
* "edit-form" = "/admin/config/search/pages/manage/{search_page}",
|
||||
* "delete-form" = "/admin/config/search/pages/manage/{search_page}/delete",
|
||||
* "enable" = "/admin/config/search/pages/manage/{search_page}/enable",
|
||||
* "disable" = "/admin/config/search/pages/manage/{search_page}/disable",
|
||||
* "set-default" = "/admin/config/search/pages/manage/{search_page}/set-default",
|
||||
* "collection" = "/admin/config/search/pages",
|
||||
* },
|
||||
* config_prefix = "page",
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "label" = "label",
|
||||
* "weight" = "weight",
|
||||
* "status" = "status"
|
||||
* },
|
||||
* config_export = {
|
||||
* "id",
|
||||
* "label",
|
||||
* "path",
|
||||
* "weight",
|
||||
* "plugin",
|
||||
* "configuration",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class SearchPage extends ConfigEntityBase implements SearchPageInterface, EntityWithPluginCollectionInterface {
|
||||
|
||||
/**
|
||||
* The name (plugin ID) of the search page entity.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The label of the search page entity.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* The configuration of the search page entity.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $configuration = array();
|
||||
|
||||
/**
|
||||
* The search plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
/**
|
||||
* The path this search page will appear upon.
|
||||
*
|
||||
* This value is appended to 'search/' when building the path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* The weight of the search page.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $weight;
|
||||
|
||||
/**
|
||||
* The plugin collection that stores search plugins.
|
||||
*
|
||||
* @var \Drupal\search\Plugin\SearchPluginCollection
|
||||
*/
|
||||
protected $pluginCollection;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPlugin() {
|
||||
return $this->getPluginCollection()->get($this->plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the creation of the search page's LazyPluginCollection.
|
||||
*
|
||||
* @return \Drupal\Component\Plugin\LazyPluginCollection
|
||||
* The search page's plugin collection.
|
||||
*/
|
||||
protected function getPluginCollection() {
|
||||
if (!$this->pluginCollection) {
|
||||
$this->pluginCollection = new SearchPluginCollection($this->searchPluginManager(), $this->plugin, $this->configuration, $this->id());
|
||||
}
|
||||
return $this->pluginCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPluginCollections() {
|
||||
return array('configuration' => $this->getPluginCollection());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setPlugin($plugin_id) {
|
||||
$this->plugin = $plugin_id;
|
||||
$this->getPluginCollection()->addInstanceID($plugin_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isIndexable() {
|
||||
return $this->status() && $this->getPlugin() instanceof SearchIndexingInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isDefaultSearch() {
|
||||
return $this->searchPageRepository()->getDefaultSearchPage() == $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPath() {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getWeight() {
|
||||
return $this->weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function postCreate(EntityStorageInterface $storage) {
|
||||
parent::postCreate($storage);
|
||||
|
||||
// @todo Use self::applyDefaultValue() once
|
||||
// https://www.drupal.org/node/2004756 is in.
|
||||
if (!isset($this->weight)) {
|
||||
$this->weight = $this->isDefaultSearch() ? -10 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
|
||||
parent::postSave($storage, $update);
|
||||
$this->routeBuilder()->setRebuildNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function postDelete(EntityStorageInterface $storage, array $entities) {
|
||||
parent::postDelete($storage, $entities);
|
||||
|
||||
$search_page_repository = \Drupal::service('search.search_page_repository');
|
||||
if (!$search_page_repository->isSearchActive()) {
|
||||
$search_page_repository->clearDefaultSearchPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
|
||||
/** @var $a \Drupal\search\SearchPageInterface */
|
||||
/** @var $b \Drupal\search\SearchPageInterface */
|
||||
$a_status = (int) $a->status();
|
||||
$b_status = (int) $b->status();
|
||||
if ($a_status != $b_status) {
|
||||
return ($a_status > $b_status) ? -1 : 1;
|
||||
}
|
||||
return parent::sort($a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the route builder.
|
||||
*
|
||||
* @return \Drupal\Core\Routing\RouteBuilderInterface
|
||||
* An object for state storage.
|
||||
*/
|
||||
protected function routeBuilder() {
|
||||
return \Drupal::service('router.builder');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the config factory.
|
||||
*
|
||||
* @return \Drupal\Core\Config\ConfigFactoryInterface
|
||||
* A config factory object.
|
||||
*/
|
||||
protected function configFactory() {
|
||||
return \Drupal::service('config.factory');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the search page repository.
|
||||
*
|
||||
* @return \Drupal\search\SearchPageRepositoryInterface
|
||||
* A search page repository object.
|
||||
*/
|
||||
protected function searchPageRepository() {
|
||||
return \Drupal::service('search.search_page_repository');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the search plugin manager.
|
||||
*
|
||||
* @return \Drupal\Component\Plugin\PluginManagerInterface
|
||||
* A search plugin manager object.
|
||||
*/
|
||||
protected function searchPluginManager() {
|
||||
return \Drupal::service('plugin.manager.search');
|
||||
}
|
||||
|
||||
}
|
75
core/modules/search/src/Form/ReindexConfirm.php
Normal file
75
core/modules/search/src/Form/ReindexConfirm.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Form\ReindexConfirm.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Form;
|
||||
|
||||
use Drupal\Core\Form\ConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Provides the search reindex confirmation form.
|
||||
*/
|
||||
class ReindexConfirm extends ConfirmFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'search_reindex_confirm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\Core\Form\ConfirmFormBase::getQuestion().
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Are you sure you want to re-index the site?');
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\Core\Form\ConfirmFormBase::getDescription().
|
||||
*/
|
||||
public function getDescription() {
|
||||
return $this->t("This will re-index content in the search indexes of all active search pages. Searching will continue to work, but new content won't be indexed until all existing content has been re-indexed. This action cannot be undone.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\Core\Form\ConfirmFormBase::getConfirmText().
|
||||
*/
|
||||
public function getConfirmText() {
|
||||
return $this->t('Re-index site');
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\Core\Form\ConfirmFormBase::getCancelText().
|
||||
*/
|
||||
public function getCancelText() {
|
||||
return $this->t('Cancel');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCancelUrl() {
|
||||
return new Url('entity.search_page.collection');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
if ($form['confirm']) {
|
||||
// Ask each active search page to mark itself for re-index.
|
||||
$search_page_repository = \Drupal::service('search.search_page_repository');
|
||||
foreach ($search_page_repository->getIndexableSearchPages() as $entity) {
|
||||
$entity->getPlugin()->markForReindex();
|
||||
}
|
||||
drupal_set_message($this->t('All search indexes will be rebuilt.'));
|
||||
$form_state->setRedirectUrl($this->getCancelUrl());
|
||||
}
|
||||
}
|
||||
}
|
124
core/modules/search/src/Form/SearchBlockForm.php
Normal file
124
core/modules/search/src/Form/SearchBlockForm.php
Normal file
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Form\SearchBlockForm.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Form;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\search\SearchPageRepositoryInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Builds the search form for the search block.
|
||||
*/
|
||||
class SearchBlockForm extends FormBase {
|
||||
|
||||
/**
|
||||
* The search page repository.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageRepositoryInterface
|
||||
*/
|
||||
protected $searchPageRepository;
|
||||
|
||||
/**
|
||||
* The config factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs a new SearchBlockForm.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
|
||||
* The search page repository.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory.
|
||||
* @param \Drupal\Core\Render\RendererInterface
|
||||
* The renderer.
|
||||
*/
|
||||
public function __construct(SearchPageRepositoryInterface $search_page_repository, ConfigFactoryInterface $config_factory, RendererInterface $renderer) {
|
||||
$this->searchPageRepository = $search_page_repository;
|
||||
$this->configFactory = $config_factory;
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('search.search_page_repository'),
|
||||
$container->get('config.factory'),
|
||||
$container->get('renderer')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'search_block_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
// Set up the form to submit using GET to the correct search page.
|
||||
$entity_id = $this->searchPageRepository->getDefaultSearchPage();
|
||||
if (!$entity_id) {
|
||||
$form['message'] = array(
|
||||
'#markup' => $this->t('Search is currently disabled'),
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
$route = 'search.view_' . $entity_id;
|
||||
$form['#action'] = $this->url($route);
|
||||
$form['#token'] = FALSE;
|
||||
$form['#method'] = 'get';
|
||||
|
||||
$form['keys'] = array(
|
||||
'#type' => 'search',
|
||||
'#title' => $this->t('Search'),
|
||||
'#title_display' => 'invisible',
|
||||
'#size' => 15,
|
||||
'#default_value' => '',
|
||||
'#attributes' => array('title' => $this->t('Enter the terms you wish to search for.')),
|
||||
);
|
||||
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Search'),
|
||||
// Prevent op from showing up in the query string.
|
||||
'#name' => '',
|
||||
);
|
||||
|
||||
// SearchPageRepository::getDefaultSearchPage() depends on search.settings.
|
||||
$this->renderer->addCacheableDependency($form, $this->configFactory->get('search.settings'));
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
// This form submits to the search page, so processing happens there.
|
||||
}
|
||||
}
|
50
core/modules/search/src/Form/SearchPageAddForm.php
Normal file
50
core/modules/search/src/Form/SearchPageAddForm.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Form\SearchPageAddForm.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Form;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Provides a form for adding a search page.
|
||||
*/
|
||||
class SearchPageAddForm extends SearchPageFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, $search_plugin_id = NULL) {
|
||||
$this->entity->setPlugin($search_plugin_id);
|
||||
$definition = $this->entity->getPlugin()->getPluginDefinition();
|
||||
$this->entity->set('label', $definition['title']);
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function actions(array $form, FormStateInterface $form_state) {
|
||||
$actions = parent::actions($form, $form_state);
|
||||
$actions['submit']['#value'] = $this->t('Add search page');
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
// If there is no default search page, make the added search the default.
|
||||
if (!$this->searchPageRepository->getDefaultSearchPage()) {
|
||||
$this->searchPageRepository->setDefaultSearchPage($this->entity);
|
||||
}
|
||||
|
||||
parent::save($form, $form_state);
|
||||
|
||||
drupal_set_message($this->t('The %label search page has been added.', array('%label' => $this->entity->label())));
|
||||
}
|
||||
|
||||
}
|
35
core/modules/search/src/Form/SearchPageEditForm.php
Normal file
35
core/modules/search/src/Form/SearchPageEditForm.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Form\SearchPageEditForm.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Form;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Provides a form for editing a search page.
|
||||
*/
|
||||
class SearchPageEditForm extends SearchPageFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function actions(array $form, FormStateInterface $form_state) {
|
||||
$actions = parent::actions($form, $form_state);
|
||||
$actions['submit']['#value'] = $this->t('Save search page');
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
parent::save($form, $form_state);
|
||||
|
||||
drupal_set_message($this->t('The %label search page has been updated.', array('%label' => $this->entity->label())));
|
||||
}
|
||||
|
||||
}
|
109
core/modules/search/src/Form/SearchPageForm.php
Normal file
109
core/modules/search/src/Form/SearchPageForm.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Form\SearchPageForm.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Provides a search form for site wide search.
|
||||
*
|
||||
* Search plugins can define method searchFormAlter() to alter the form. If they
|
||||
* have additional or substitute fields, they will need to override the form
|
||||
* submit, making sure to redirect with a GET parameter of 'keys' included, to
|
||||
* trigger the search being processed by the controller, and adding in any
|
||||
* additional query parameters they need to execute search.
|
||||
*/
|
||||
class SearchPageForm extends EntityForm {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\search\SearchPageInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'search_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$plugin = $this->entity->getPlugin();
|
||||
$form_state->set('search_page_id', $this->entity->id());
|
||||
|
||||
$form['basic'] = array(
|
||||
'#type' => 'container',
|
||||
'#attributes' => array(
|
||||
'class' => array('container-inline'),
|
||||
),
|
||||
);
|
||||
$form['basic']['keys'] = array(
|
||||
'#type' => 'search',
|
||||
'#title' => $this->t('Enter your keywords'),
|
||||
'#default_value' => $plugin->getKeywords(),
|
||||
'#size' => 30,
|
||||
'#maxlength' => 255,
|
||||
);
|
||||
|
||||
// processed_keys is used to coordinate keyword passing between other forms
|
||||
// that hook into the basic search form.
|
||||
$form['basic']['processed_keys'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => '',
|
||||
);
|
||||
$form['basic']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Search'),
|
||||
);
|
||||
|
||||
$form['help_link'] = array(
|
||||
'#type' => 'link',
|
||||
'#url' => new Url('search.help_' . $this->entity->id()),
|
||||
'#title' => $this->t('Search help'),
|
||||
'#options' => array('attributes' => array('class' => 'search-help-link')),
|
||||
);
|
||||
|
||||
// Allow the plugin to add to or alter the search form.
|
||||
$plugin->searchFormAlter($form, $form_state);
|
||||
|
||||
return parent::form($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function actions(array $form, FormStateInterface $form_state) {
|
||||
// The submit button is added in the form directly.
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
// Redirect to the search page with keywords in the GET parameters.
|
||||
// Plugins with additional search parameters will need to provide their
|
||||
// own form submit handler to replace this, so they can put their values
|
||||
// into the GET as well. If so, make sure to put 'keys' into the GET
|
||||
// parameters so that the search results generation is triggered.
|
||||
$query = $this->entity->getPlugin()->buildSearchUrlQuery($form_state);
|
||||
$route = 'search.view_' . $form_state->get('search_page_id');
|
||||
$form_state->setRedirect(
|
||||
$route,
|
||||
array(),
|
||||
array('query' => $query)
|
||||
);
|
||||
}
|
||||
}
|
185
core/modules/search/src/Form/SearchPageFormBase.php
Normal file
185
core/modules/search/src/Form/SearchPageFormBase.php
Normal file
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Form\SearchPageFormBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Entity\Query\QueryFactory;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\PluginFormInterface;
|
||||
use Drupal\search\SearchPageRepositoryInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a base form for search pages.
|
||||
*/
|
||||
abstract class SearchPageFormBase extends EntityForm {
|
||||
|
||||
/**
|
||||
* The entity being used by this form.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The search plugin being configured.
|
||||
*
|
||||
* @var \Drupal\search\Plugin\SearchInterface
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
/**
|
||||
* The entity query factory.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryFactory
|
||||
*/
|
||||
protected $entityQuery;
|
||||
|
||||
/**
|
||||
* The search page repository.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageRepositoryInterface
|
||||
*/
|
||||
protected $searchPageRepository;
|
||||
|
||||
/**
|
||||
* Constructs a new search form.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\Query\QueryFactory $entity_query
|
||||
* The entity query.
|
||||
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
|
||||
* The search page repository.
|
||||
*/
|
||||
public function __construct(QueryFactory $entity_query, SearchPageRepositoryInterface $search_page_repository) {
|
||||
$this->entityQuery = $entity_query;
|
||||
$this->searchPageRepository = $search_page_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity.query'),
|
||||
$container->get('search.search_page_repository')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBaseFormId() {
|
||||
return 'search_entity_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$this->plugin = $this->entity->getPlugin();
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$form['label'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Label'),
|
||||
'#description' => $this->t('The label for this search page.'),
|
||||
'#default_value' => $this->entity->label(),
|
||||
'#maxlength' => '255',
|
||||
);
|
||||
|
||||
$form['id'] = array(
|
||||
'#type' => 'machine_name',
|
||||
'#default_value' => $this->entity->id(),
|
||||
'#disabled' => !$this->entity->isNew(),
|
||||
'#maxlength' => 64,
|
||||
'#machine_name' => array(
|
||||
'exists' => array($this, 'exists'),
|
||||
),
|
||||
);
|
||||
$form['path'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Path'),
|
||||
'#field_prefix' => 'search/',
|
||||
'#default_value' => $this->entity->getPath(),
|
||||
'#maxlength' => '255',
|
||||
);
|
||||
$form['plugin'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $this->entity->get('plugin'),
|
||||
);
|
||||
|
||||
if ($this->plugin instanceof PluginFormInterface) {
|
||||
$form += $this->plugin->buildConfigurationForm($form, $form_state);
|
||||
}
|
||||
|
||||
return parent::form($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the search page entity already exists.
|
||||
*
|
||||
* @param string $id
|
||||
* The search configuration ID.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the search configuration exists, FALSE otherwise.
|
||||
*/
|
||||
public function exists($id) {
|
||||
$entity = $this->entityQuery->get('search_page')
|
||||
->condition('id', $id)
|
||||
->execute();
|
||||
return (bool) $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate(array $form, FormStateInterface $form_state) {
|
||||
parent::validate($form, $form_state);
|
||||
|
||||
// Ensure each path is unique.
|
||||
$path = $this->entityQuery->get('search_page')
|
||||
->condition('path', $form_state->getValue('path'))
|
||||
->condition('id', $form_state->getValue('id'), '<>')
|
||||
->execute();
|
||||
if ($path) {
|
||||
$form_state->setErrorByName('path', $this->t('The search page path must be unique.'));
|
||||
}
|
||||
|
||||
if ($this->plugin instanceof PluginFormInterface) {
|
||||
$this->plugin->validateConfigurationForm($form, $form_state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
parent::submitForm($form, $form_state);
|
||||
|
||||
if ($this->plugin instanceof PluginFormInterface) {
|
||||
$this->plugin->submitConfigurationForm($form, $form_state);
|
||||
}
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
$this->entity->save();
|
||||
|
||||
$form_state->setRedirectUrl($this->entity->urlInfo('collection'));
|
||||
}
|
||||
|
||||
}
|
40
core/modules/search/src/Plugin/Block/SearchBlock.php
Normal file
40
core/modules/search/src/Plugin/Block/SearchBlock.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\Block\SearchBlock.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin\Block;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Block\BlockBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a 'Search form' block.
|
||||
*
|
||||
* @Block(
|
||||
* id = "search_form_block",
|
||||
* admin_label = @Translation("Search form"),
|
||||
* category = @Translation("Forms")
|
||||
* )
|
||||
*/
|
||||
class SearchBlock extends BlockBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function blockAccess(AccountInterface $account) {
|
||||
return AccessResult::allowedIfHasPermission($account, 'search content');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build() {
|
||||
return \Drupal::formBuilder()->getForm('Drupal\search\Form\SearchBlockForm');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\ConfigurableSearchPluginBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Provides a base implementation for a configurable Search plugin.
|
||||
*/
|
||||
abstract class ConfigurableSearchPluginBase extends SearchPluginBase implements ConfigurableSearchPluginInterface {
|
||||
|
||||
/**
|
||||
* The unique ID for the search page using this plugin.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $searchPageId;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
$this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $this->configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defaultConfiguration() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfiguration() {
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setConfiguration(array $configuration) {
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function calculateDependencies() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSearchPageId($search_page_id) {
|
||||
$this->searchPageId = $search_page_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\ConfigurableSearchPluginInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\ConfigurablePluginInterface;
|
||||
use Drupal\Core\Plugin\PluginFormInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for a configurable Search plugin.
|
||||
*/
|
||||
interface ConfigurableSearchPluginInterface extends ConfigurablePluginInterface, PluginFormInterface, SearchInterface {
|
||||
|
||||
/**
|
||||
* Sets the ID for the search page using this plugin.
|
||||
*
|
||||
* @param string $search_page_id
|
||||
* The search page ID.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function setSearchPageId($search_page_id);
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\Derivative\SearchLocalTask.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\search\SearchPageRepositoryInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides local tasks for each search page.
|
||||
*/
|
||||
class SearchLocalTask extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
/**
|
||||
* The search page repository.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageRepositoryInterface
|
||||
*/
|
||||
protected $searchPageRepository;
|
||||
|
||||
/**
|
||||
* Constructs a new SearchLocalTask.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
|
||||
* The search page repository.
|
||||
*/
|
||||
public function __construct(SearchPageRepositoryInterface $search_page_repository) {
|
||||
$this->searchPageRepository = $search_page_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('search.search_page_repository')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
$this->derivatives = array();
|
||||
|
||||
if ($default = $this->searchPageRepository->getDefaultSearchPage()) {
|
||||
$active_search_pages = $this->searchPageRepository->getActiveSearchPages();
|
||||
foreach ($this->searchPageRepository->sortSearchPages($active_search_pages) as $entity_id => $entity) {
|
||||
$this->derivatives[$entity_id] = array(
|
||||
'title' => $entity->label(),
|
||||
'route_name' => 'search.view_' . $entity_id,
|
||||
'base_route' => 'search.plugins:' . $default,
|
||||
'weight' => $entity->getWeight(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
}
|
91
core/modules/search/src/Plugin/SearchIndexingInterface.php
Normal file
91
core/modules/search/src/Plugin/SearchIndexingInterface.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\SearchIndexingInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin;
|
||||
|
||||
/**
|
||||
* Defines an optional interface for SearchPlugin objects using an index.
|
||||
*
|
||||
* Plugins implementing this interface will have these methods invoked during
|
||||
* search_cron() and via the search module administration form. Plugins not
|
||||
* implementing this interface are assumed to be using their own methods for
|
||||
* searching, not involving separate index tables.
|
||||
*
|
||||
* The user interface for managing search pages displays the indexing status for
|
||||
* search pages implementing this interface. It also allows users to configure
|
||||
* default settings for indexing, and refers to the "default search index". If
|
||||
* your search page plugin uses its own indexing mechanism instead of the
|
||||
* default search index, or overrides the default indexing settings, you should
|
||||
* make this clear on the settings page or other documentation for your plugin.
|
||||
*
|
||||
* Multiple search pages can be created for each search plugin, so you will need
|
||||
* to choose whether these search pages should share an index (in which case
|
||||
* they must not use any search page-specific configuration while indexing) or
|
||||
* they will have separate indexes (which will use additional server resources).
|
||||
*/
|
||||
interface SearchIndexingInterface {
|
||||
|
||||
/**
|
||||
* Updates the search index for this plugin.
|
||||
*
|
||||
* This method is called every cron run if the plugin has been set as
|
||||
* an active search module on the Search settings page
|
||||
* (admin/config/search/pages). It allows your module to add items to the
|
||||
* built-in search index using search_index(), or to add them to your module's
|
||||
* own indexing mechanism.
|
||||
*
|
||||
* When implementing this method, your module should index content items that
|
||||
* were modified or added since the last run. There is a time limit for cron,
|
||||
* so it is advisable to limit how many items you index per run using
|
||||
* config('search.settings')->get('index.cron_limit') or with your own
|
||||
* setting. And since the cron run could time out and abort in the middle of
|
||||
* your run, you should update any needed internal bookkeeping on when items
|
||||
* have last been indexed as you go rather than waiting to the end of
|
||||
* indexing.
|
||||
*/
|
||||
public function updateIndex();
|
||||
|
||||
/**
|
||||
* Clears the search index for this plugin.
|
||||
*
|
||||
* When a request is made to clear all items from the search index related to
|
||||
* this plugin, this method will be called. If this plugin uses the default
|
||||
* search index, this method can call search_index_clear($type) to remove
|
||||
* indexed items from the search database.
|
||||
*
|
||||
* @see search_index_clear()
|
||||
*/
|
||||
public function indexClear();
|
||||
|
||||
/**
|
||||
* Marks the search index for reindexing for this plugin.
|
||||
*
|
||||
* When a request is made to mark all items from the search index related to
|
||||
* this plugin for reindexing, this method will be called. If this plugin uses
|
||||
* the default search index, this method can call
|
||||
* search_mark_for_reindex($type) to mark the items in the search database for
|
||||
* reindexing.
|
||||
*
|
||||
* @see search_mark_for_reindex()
|
||||
*/
|
||||
public function markForReindex();
|
||||
|
||||
/**
|
||||
* Reports the status of indexing.
|
||||
*
|
||||
* The core search module only invokes this method on active module plugins.
|
||||
* Implementing modules do not need to check whether they are active when
|
||||
* calculating their return values.
|
||||
*
|
||||
* @return array
|
||||
* An associative array with the key-value pairs:
|
||||
* - remaining: The number of items left to index.
|
||||
* - total: The total number of items to index.
|
||||
*/
|
||||
public function indexStatus();
|
||||
|
||||
}
|
153
core/modules/search/src/Plugin/SearchInterface.php
Normal file
153
core/modules/search/src/Plugin/SearchInterface.php
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\SearchInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Defines a common interface for all SearchPlugin objects.
|
||||
*/
|
||||
interface SearchInterface extends PluginInspectionInterface {
|
||||
|
||||
/**
|
||||
* Sets the keywords, parameters, and attributes to be used by execute().
|
||||
*
|
||||
* @param string $keywords
|
||||
* The keywords to use in a search.
|
||||
* @param array $parameters
|
||||
* Array of parameters as an associative array. This is expected to
|
||||
* be the query string from the current request.
|
||||
* @param array $attributes
|
||||
* Array of attributes, usually from the current request object.
|
||||
*
|
||||
* @return \Drupal\search\Plugin\SearchInterface
|
||||
* A search plugin object for chaining.
|
||||
*/
|
||||
public function setSearch($keywords, array $parameters, array $attributes);
|
||||
|
||||
/**
|
||||
* Returns the currently set keywords of the plugin instance.
|
||||
*
|
||||
* @return string
|
||||
* The keywords.
|
||||
*/
|
||||
public function getKeywords();
|
||||
|
||||
/**
|
||||
* Returns the current parameters set using setSearch().
|
||||
*
|
||||
* @return array
|
||||
* The parameters.
|
||||
*/
|
||||
public function getParameters();
|
||||
|
||||
/**
|
||||
* Returns the currently set attributes (from the request).
|
||||
*
|
||||
* @return array
|
||||
* The attributes.
|
||||
*/
|
||||
public function getAttributes();
|
||||
|
||||
/**
|
||||
* Verifies if the values set via setSearch() are valid and sufficient.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the search settings are valid and sufficient to execute a search,
|
||||
* and FALSE if not.
|
||||
*/
|
||||
public function isSearchExecutable();
|
||||
|
||||
/**
|
||||
* Returns the search index type this plugin uses.
|
||||
*
|
||||
* @return string|null
|
||||
* The type used by this search plugin in the search index, or NULL if this
|
||||
* plugin does not use the search index.
|
||||
*
|
||||
* @see search_index()
|
||||
* @see search_index_clear()
|
||||
*/
|
||||
public function getType();
|
||||
|
||||
/**
|
||||
* Executes the search.
|
||||
*
|
||||
* @return array
|
||||
* A structured list of search results.
|
||||
*/
|
||||
public function execute();
|
||||
|
||||
/**
|
||||
* Executes the search and builds render arrays for the result items.
|
||||
*
|
||||
* @return array
|
||||
* An array of render arrays of search result items (generally each item
|
||||
* has '#theme' set to 'search_result'), or an empty array if there are no
|
||||
* results.
|
||||
*/
|
||||
public function buildResults();
|
||||
|
||||
/**
|
||||
* Provides a suggested title for a page of search results.
|
||||
*
|
||||
* @return string
|
||||
* The translated suggested page title.
|
||||
*/
|
||||
public function suggestedTitle();
|
||||
|
||||
/**
|
||||
* Returns the searching help.
|
||||
*
|
||||
* @return array
|
||||
* Render array for the searching help.
|
||||
*/
|
||||
public function getHelp();
|
||||
|
||||
/**
|
||||
* Alters the search form when being built for a given plugin.
|
||||
*
|
||||
* The core search module only invokes this method on active module plugins
|
||||
* when building a form for them in
|
||||
* \Drupal\search\Form\SearchPageForm::form(). A plugin implementing this
|
||||
* will also need to implement the buildSearchUrlQuery() method.
|
||||
*
|
||||
* @param array $form
|
||||
* Nested array of form elements that comprise the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form. The arguments that
|
||||
* \Drupal::formBuilder()->getForm() was originally called with are
|
||||
* available in the array $form_state->getBuildInfo()['args'].
|
||||
*
|
||||
* @see SearchInterface::buildSearchUrlQuery()
|
||||
*/
|
||||
public function searchFormAlter(array &$form, FormStateInterface $form_state);
|
||||
|
||||
/**
|
||||
* Builds the URL GET query parameters array for search.
|
||||
*
|
||||
* When the search form is submitted, a redirect is generated with the
|
||||
* search input as GET query parameters. Plugins using the searchFormAlter()
|
||||
* method to add form elements to the search form will need to override this
|
||||
* method to gather the form input and add it to the GET query parameters.
|
||||
*
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The form state, with submitted form information.
|
||||
*
|
||||
* @return array
|
||||
* An array of GET query parameters containing all relevant form values
|
||||
* to process the search. The 'keys' element must be present in order to
|
||||
* trigger generation of search results, even if it is empty or unused by
|
||||
* the search plugin.
|
||||
*
|
||||
* @see SearchInterface::searchFormAlter()
|
||||
*/
|
||||
public function buildSearchUrlQuery(FormStateInterface $form_state);
|
||||
|
||||
}
|
164
core/modules/search/src/Plugin/SearchPluginBase.php
Normal file
164
core/modules/search/src/Plugin/SearchPluginBase.php
Normal file
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\SearchPluginBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a base class for plugins wishing to support search.
|
||||
*/
|
||||
abstract class SearchPluginBase extends PluginBase implements ContainerFactoryPluginInterface, SearchInterface {
|
||||
|
||||
/**
|
||||
* The keywords to use in a search.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $keywords;
|
||||
|
||||
/**
|
||||
* Array of parameters from the query string from the request.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $searchParameters;
|
||||
|
||||
/**
|
||||
* Array of attributes - usually from the request object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $searchAttributes;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSearch($keywords, array $parameters, array $attributes) {
|
||||
$this->keywords = (string) $keywords;
|
||||
$this->searchParameters = $parameters;
|
||||
$this->searchAttributes = $attributes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getKeywords() {
|
||||
return $this->keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParameters() {
|
||||
return $this->searchParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAttributes() {
|
||||
return $this->searchAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSearchExecutable() {
|
||||
// Default implementation suitable for plugins that only use keywords.
|
||||
return !empty($this->keywords);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getType() {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildResults() {
|
||||
$results = $this->execute();
|
||||
|
||||
$built = array();
|
||||
foreach ($results as $result) {
|
||||
$built[] = array(
|
||||
'#theme' => 'search_result',
|
||||
'#result' => $result,
|
||||
'#plugin_id' => $this->getPluginId(),
|
||||
);
|
||||
}
|
||||
|
||||
return $built;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function searchFormAlter(array &$form, FormStateInterface $form_state) {
|
||||
// Empty default implementation.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function suggestedTitle() {
|
||||
// If the user entered a search string, truncate it and append it to the
|
||||
// title.
|
||||
if (!empty($this->keywords)) {
|
||||
return $this->t('Search for @keywords', array('@keywords' => Unicode::truncate($this->keywords, 60, TRUE, TRUE)));
|
||||
}
|
||||
// Use the default 'Search' title.
|
||||
return $this->t('Search');
|
||||
}
|
||||
|
||||
/*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildSearchUrlQuery(FormStateInterface $form_state) {
|
||||
// Grab the keywords entered in the form and put them as 'keys' in the GET.
|
||||
$keys = trim($form_state->getValue('keys'));
|
||||
$query = array('keys' => $keys);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getHelp() {
|
||||
// This default search help is appropriate for plugins like NodeSearch
|
||||
// that use the SearchQuery class.
|
||||
$help = array('list' => array(
|
||||
'#theme' => 'item_list',
|
||||
'#items' => array(
|
||||
$this->t('Search looks for exact, case-insensitive keywords; keywords shorter than a minimum length are ignored.'),
|
||||
$this->t('Use upper-case OR to get more results. Example: cat OR dog (content contains either "cat" or "dog").'),
|
||||
$this->t('You can use upper-case AND to require all words, but this is the same as the default behavior. Example: cat AND dog (same as cat dog, content must contain both "cat" and "dog").'),
|
||||
$this->t('Use quotes to search for a phrase. Example: "the cat eats mice".'),
|
||||
$this->t('You can precede keywords by - to exclude them; you must still have at least one "positive" keyword. Example: cat -dog (content must contain cat and cannot contain dog).'),
|
||||
),
|
||||
));
|
||||
|
||||
return $help;
|
||||
}
|
||||
|
||||
}
|
64
core/modules/search/src/Plugin/SearchPluginCollection.php
Normal file
64
core/modules/search/src/Plugin/SearchPluginCollection.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\SearchPluginCollection.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin;
|
||||
|
||||
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
|
||||
/**
|
||||
* Provides a container for lazily loading search plugins.
|
||||
*/
|
||||
class SearchPluginCollection extends DefaultSingleLazyPluginCollection {
|
||||
|
||||
/**
|
||||
* The unique ID for the search page using this plugin collection.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $searchPageId;
|
||||
|
||||
/**
|
||||
* Constructs a new SearchPluginCollection.
|
||||
*
|
||||
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
|
||||
* The manager to be used for instantiating plugins.
|
||||
* @param string $instance_id
|
||||
* The ID of the plugin instance.
|
||||
* @param array $configuration
|
||||
* An array of configuration.
|
||||
* @param string $search_page_id
|
||||
* The unique ID of the search page using this plugin.
|
||||
*/
|
||||
public function __construct(PluginManagerInterface $manager, $instance_id, array $configuration, $search_page_id) {
|
||||
parent::__construct($manager, $instance_id, $configuration);
|
||||
|
||||
$this->searchPageId = $search_page_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return \Drupal\search\Plugin\SearchInterface
|
||||
*/
|
||||
public function &get($instance_id) {
|
||||
return parent::get($instance_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function initializePlugin($instance_id) {
|
||||
parent::initializePlugin($instance_id);
|
||||
|
||||
$plugin_instance = $this->pluginInstances[$instance_id];
|
||||
if ($plugin_instance instanceof ConfigurableSearchPluginInterface) {
|
||||
$plugin_instance->setSearchPageId($this->searchPageId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
125
core/modules/search/src/Plugin/views/argument/Search.php
Normal file
125
core/modules/search/src/Plugin/views/argument/Search.php
Normal file
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\views\argument\Search.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin\views\argument;
|
||||
|
||||
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
|
||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\Views;
|
||||
|
||||
/**
|
||||
* Argument handler for search keywords.
|
||||
*
|
||||
* @ingroup views_argument_handlers
|
||||
*
|
||||
* @ViewsArgument("search")
|
||||
*/
|
||||
class Search extends ArgumentPluginBase {
|
||||
|
||||
/**
|
||||
* A search query to use for parsing search keywords.
|
||||
*
|
||||
* @var \Drupal\search\ViewsSearchQuery
|
||||
*/
|
||||
protected $searchQuery = NULL;
|
||||
|
||||
/**
|
||||
* The search type name (value of {search_index}.type in the database).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $searchType;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
|
||||
parent::init($view, $display, $options);
|
||||
|
||||
$this->searchType = $this->definition['search_type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up and parses the search query.
|
||||
*
|
||||
* @param string $input
|
||||
* The search keywords entered by the user.
|
||||
*/
|
||||
protected function queryParseSearchExpression($input) {
|
||||
if (!isset($this->searchQuery)) {
|
||||
$this->searchQuery = db_select('search_index', 'i', array('target' => 'replica'))->extend('Drupal\search\ViewsSearchQuery');
|
||||
$this->searchQuery->searchExpression($input, $this->searchType);
|
||||
$this->searchQuery->publicParseSearchExpression();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
$required = FALSE;
|
||||
$this->queryParseSearchExpression($this->argument);
|
||||
if (!isset($this->searchQuery)) {
|
||||
$required = TRUE;
|
||||
}
|
||||
else {
|
||||
$words = $this->searchQuery->words();
|
||||
if (empty($words)) {
|
||||
$required = TRUE;
|
||||
}
|
||||
}
|
||||
if ($required) {
|
||||
if ($this->operator == 'required') {
|
||||
$this->query->addWhere(0, 'FALSE');
|
||||
}
|
||||
}
|
||||
else {
|
||||
$search_index = $this->ensureMyTable();
|
||||
|
||||
$search_condition = db_and();
|
||||
|
||||
// Create a new join to relate the 'search_total' table to our current 'search_index' table.
|
||||
$definition = array(
|
||||
'table' => 'search_total',
|
||||
'field' => 'word',
|
||||
'left_table' => $search_index,
|
||||
'left_field' => 'word',
|
||||
);
|
||||
$join = Views::pluginManager('join')->createInstance('standard', $definition);
|
||||
$search_total = $this->query->addRelationship('search_total', $join, $search_index);
|
||||
|
||||
$this->search_score = $this->query->addField('', "$search_index.score * $search_total.count", 'score', array('function' => 'sum'));
|
||||
|
||||
$search_condition->condition("$search_index.type", $this->searchType);
|
||||
|
||||
$search_dataset = $this->query->addTable('node_search_dataset');
|
||||
$conditions = $this->searchQuery->conditions();
|
||||
$condition_conditions =& $conditions->conditions();
|
||||
foreach ($condition_conditions as $key => &$condition) {
|
||||
// Make sure we just look at real conditions.
|
||||
if (is_numeric($key)) {
|
||||
// Replace the conditions with the table alias of views.
|
||||
$this->searchQuery->conditionReplaceString('d.', "$search_dataset.", $condition);
|
||||
}
|
||||
}
|
||||
$search_conditions =& $search_condition->conditions();
|
||||
$search_conditions = array_merge($search_conditions, $condition_conditions);
|
||||
|
||||
$this->query->addWhere(0, $search_condition);
|
||||
$this->query->addGroupBy("$search_index.sid");
|
||||
$matches = $this->searchQuery->matches();
|
||||
$placeholder = $this->placeholder();
|
||||
$this->query->addHavingExpression(0, "COUNT(*) >= $placeholder", array($placeholder => $matches));
|
||||
}
|
||||
|
||||
// Set to NULL to prevent PDO exception when views object is cached
|
||||
// and to clear out memory.
|
||||
$this->searchQuery = NULL;
|
||||
}
|
||||
|
||||
}
|
51
core/modules/search/src/Plugin/views/field/Score.php
Normal file
51
core/modules/search/src/Plugin/views/field/Score.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\views\field\Score.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin\views\field;
|
||||
|
||||
use Drupal\views\Plugin\views\field\NumericField;
|
||||
use Drupal\views\ResultRow;
|
||||
|
||||
/**
|
||||
* Field handler for search score.
|
||||
*
|
||||
* @ingroup views_field_handlers
|
||||
*
|
||||
* @ViewsField("search_score")
|
||||
*/
|
||||
class Score extends NumericField {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
// Check to see if the search filter added 'score' to the table.
|
||||
// Our filter stores it as $handler->search_score -- and we also
|
||||
// need to check its relationship to make sure that we're using the same
|
||||
// one or obviously this won't work.
|
||||
foreach ($this->view->filter as $handler) {
|
||||
if (isset($handler->search_score) && ($handler->relationship == $this->relationship)) {
|
||||
$this->field_alias = $handler->search_score;
|
||||
$this->tableAlias = $handler->tableAlias;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide this field if no search filter is in place.
|
||||
$this->options['exclude'] = TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render(ResultRow $values) {
|
||||
// Only render if we exist.
|
||||
if (isset($this->tableAlias)) {
|
||||
return parent::render($values);
|
||||
}
|
||||
}
|
||||
}
|
198
core/modules/search/src/Plugin/views/filter/Search.php
Normal file
198
core/modules/search/src/Plugin/views/filter/Search.php
Normal file
|
@ -0,0 +1,198 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\views\filter\Search.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin\views\filter;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\views\Plugin\views\filter\FilterPluginBase;
|
||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\Views;
|
||||
|
||||
/**
|
||||
* Filter handler for search keywords.
|
||||
*
|
||||
* @ingroup views_filter_handlers
|
||||
*
|
||||
* @ViewsFilter("search_keywords")
|
||||
*/
|
||||
class Search extends FilterPluginBase {
|
||||
|
||||
/**
|
||||
* This filter is always considered multiple-valued.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $alwaysMultiple = TRUE;
|
||||
|
||||
/**
|
||||
* A search query to use for parsing search keywords.
|
||||
*
|
||||
* @var \Drupal\search\ViewsSearchQuery
|
||||
*/
|
||||
protected $searchQuery = NULL;
|
||||
|
||||
/**
|
||||
* TRUE if the search query has been parsed.
|
||||
*/
|
||||
protected $parsed = FALSE;
|
||||
|
||||
/**
|
||||
* The search type name (value of {search_index}.type in the database).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $searchType;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
|
||||
parent::init($view, $display, $options);
|
||||
|
||||
$this->searchType = $this->definition['search_type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
|
||||
$options['operator']['default'] = 'optional';
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function operatorForm(&$form, FormStateInterface $form_state) {
|
||||
$form['operator'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => $this->t('On empty input'),
|
||||
'#default_value' => $this->operator,
|
||||
'#options' => array(
|
||||
'optional' => $this->t('Show All'),
|
||||
'required' => $this->t('Show None'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function valueForm(&$form, FormStateInterface $form_state) {
|
||||
$form['value'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#size' => 15,
|
||||
'#default_value' => $this->value,
|
||||
'#attributes' => array('title' => $this->t('Search keywords')),
|
||||
'#title' => !$form_state->get('exposed') ? $this->t('Keywords') : '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateExposed(&$form, FormStateInterface $form_state) {
|
||||
if (!isset($this->options['expose']['identifier'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = $this->options['expose']['identifier'];
|
||||
if (!$form_state->isValueEmpty($key)) {
|
||||
$this->queryParseSearchExpression($form_state->getValue($key));
|
||||
if (count($this->searchQuery->words()) == 0) {
|
||||
$form_state->setErrorByName($key, $this->formatPlural(\Drupal::config('search.settings')->get('index.minimum_word_size'), 'You must include at least one positive keyword with 1 character or more.', 'You must include at least one positive keyword with @count characters or more.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up and parses the search query.
|
||||
*
|
||||
* @param string $input
|
||||
* The search keywords entered by the user.
|
||||
*/
|
||||
protected function queryParseSearchExpression($input) {
|
||||
if (!isset($this->searchQuery)) {
|
||||
$this->parsed = TRUE;
|
||||
$this->searchQuery = db_select('search_index', 'i', array('target' => 'replica'))->extend('Drupal\search\ViewsSearchQuery');
|
||||
$this->searchQuery->searchExpression($input, $this->searchType);
|
||||
$this->searchQuery->publicParseSearchExpression();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
// Since attachment views don't validate the exposed input, parse the search
|
||||
// expression if required.
|
||||
if (!$this->parsed) {
|
||||
$this->queryParseSearchExpression($this->value);
|
||||
}
|
||||
$required = FALSE;
|
||||
if (!isset($this->searchQuery)) {
|
||||
$required = TRUE;
|
||||
}
|
||||
else {
|
||||
$words = $this->searchQuery->words();
|
||||
if (empty($words)) {
|
||||
$required = TRUE;
|
||||
}
|
||||
}
|
||||
if ($required) {
|
||||
if ($this->operator == 'required') {
|
||||
$this->query->addWhere($this->options['group'], 'FALSE');
|
||||
}
|
||||
}
|
||||
else {
|
||||
$search_index = $this->ensureMyTable();
|
||||
|
||||
$search_condition = db_and();
|
||||
|
||||
// Create a new join to relate the 'search_total' table to our current
|
||||
// 'search_index' table.
|
||||
$definition = array(
|
||||
'table' => 'search_total',
|
||||
'field' => 'word',
|
||||
'left_table' => $search_index,
|
||||
'left_field' => 'word',
|
||||
);
|
||||
$join = Views::pluginManager('join')->createInstance('standard', $definition);
|
||||
|
||||
$search_total = $this->query->addRelationship('search_total', $join, $search_index);
|
||||
|
||||
$this->search_score = $this->query->addField('', "$search_index.score * $search_total.count", 'score', array('function' => 'sum'));
|
||||
|
||||
$search_condition->condition("$search_index.type", $this->searchType);
|
||||
$search_dataset = $this->query->addTable('node_search_dataset');
|
||||
$conditions = $this->searchQuery->conditions();
|
||||
$condition_conditions =& $conditions->conditions();
|
||||
foreach ($condition_conditions as $key => &$condition) {
|
||||
// Make sure we just look at real conditions.
|
||||
if (is_numeric($key)) {
|
||||
// Replace the conditions with the table alias of views.
|
||||
$this->searchQuery->conditionReplaceString('d.', "$search_dataset.", $condition);
|
||||
}
|
||||
}
|
||||
$search_conditions =& $search_condition->conditions();
|
||||
$search_conditions = array_merge($search_conditions, $condition_conditions);
|
||||
|
||||
$this->query->addWhere($this->options['group'], $search_condition);
|
||||
$this->query->addGroupBy("$search_index.sid");
|
||||
$matches = $this->searchQuery->matches();
|
||||
$placeholder = $this->placeholder();
|
||||
$this->query->addHavingExpression($this->options['group'], "COUNT(*) >= $placeholder", array($placeholder => $matches));
|
||||
}
|
||||
// Set to NULL to prevent PDO exception when views object is cached.
|
||||
$this->searchQuery = NULL;
|
||||
}
|
||||
|
||||
}
|
58
core/modules/search/src/Plugin/views/row/SearchRow.php
Normal file
58
core/modules/search/src/Plugin/views/row/SearchRow.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\views\row\SearchRow.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin\views\row;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\views\Plugin\views\row\RowPluginBase;
|
||||
|
||||
/**
|
||||
* Row handler plugin for displaying search results.
|
||||
*
|
||||
* @ViewsRow(
|
||||
* id = "search_view",
|
||||
* title = @Translation("Search results"),
|
||||
* help = @Translation("Provides a row plugin to display search results.")
|
||||
* )
|
||||
*/
|
||||
class SearchRow extends RowPluginBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
|
||||
$options['score'] = array('default' => TRUE);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
$form['score'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Display score'),
|
||||
'#default_value' => $this->options['score'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render($row) {
|
||||
return array(
|
||||
'#theme' => $this->themeFunctions(),
|
||||
'#view' => $this->view,
|
||||
'#options' => $this->options,
|
||||
'#row' => $row,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
43
core/modules/search/src/Plugin/views/sort/Score.php
Normal file
43
core/modules/search/src/Plugin/views/sort/Score.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Plugin\views\sort\Score.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Plugin\views\sort;
|
||||
|
||||
use Drupal\views\Plugin\views\sort\SortPluginBase;
|
||||
|
||||
/**
|
||||
* Sort handler for sorting by search score.
|
||||
*
|
||||
* @ingroup views_sort_handlers
|
||||
*
|
||||
* @ViewsSort("search_score")
|
||||
*/
|
||||
class Score extends SortPluginBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
// Check to see if the search filter/argument added 'score' to the table.
|
||||
// Our filter stores it as $handler->search_score -- and we also
|
||||
// need to check its relationship to make sure that we're using the same
|
||||
// one or obviously this won't work.
|
||||
foreach (array('filter', 'argument') as $type) {
|
||||
foreach ($this->view->{$type} as $handler) {
|
||||
if (isset($handler->search_score) && $handler->relationship == $this->relationship) {
|
||||
$this->query->addOrderBy(NULL, NULL, $this->options['order'], $handler->search_score);
|
||||
$this->tableAlias = $handler->tableAlias;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do nothing if there is no filter/argument in place. There is no way
|
||||
// to sort on scores.
|
||||
}
|
||||
|
||||
}
|
122
core/modules/search/src/Routing/SearchPageRoutes.php
Normal file
122
core/modules/search/src/Routing/SearchPageRoutes.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Routing\SearchPageRoutes.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Routing;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\search\SearchPageRepositoryInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Provides dynamic routes for search.
|
||||
*/
|
||||
class SearchPageRoutes implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The search page repository.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageRepositoryInterface
|
||||
*/
|
||||
protected $searchPageRepository;
|
||||
|
||||
/**
|
||||
* Constructs a new search route subscriber.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
|
||||
* The search page repository.
|
||||
*/
|
||||
public function __construct(SearchPageRepositoryInterface $search_page_repository) {
|
||||
$this->searchPageRepository = $search_page_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('search.search_page_repository')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of route objects.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\Route[]
|
||||
* An array of route objects.
|
||||
*/
|
||||
public function routes() {
|
||||
$routes = array();
|
||||
// @todo Decide if /search should continue to redirect to /search/$default,
|
||||
// or just perform the appropriate search.
|
||||
if ($default_page = $this->searchPageRepository->getDefaultSearchPage()) {
|
||||
$routes['search.view'] = new Route(
|
||||
'/search',
|
||||
array(
|
||||
'_controller' => 'Drupal\search\Controller\SearchController::redirectSearchPage',
|
||||
'_title' => 'Search',
|
||||
'entity' => $default_page,
|
||||
),
|
||||
array(
|
||||
'_entity_access' => 'entity.view',
|
||||
'_permission' => 'search content',
|
||||
),
|
||||
array(
|
||||
'parameters' => array(
|
||||
'entity' => array(
|
||||
'type' => 'entity:search_page',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
$active_pages = $this->searchPageRepository->getActiveSearchPages();
|
||||
foreach ($active_pages as $entity_id => $entity) {
|
||||
$routes["search.view_$entity_id"] = new Route(
|
||||
'/search/' . $entity->getPath(),
|
||||
array(
|
||||
'_controller' => 'Drupal\search\Controller\SearchController::view',
|
||||
'_title' => 'Search',
|
||||
'entity' => $entity_id,
|
||||
),
|
||||
array(
|
||||
'_entity_access' => 'entity.view',
|
||||
'_permission' => 'search content',
|
||||
),
|
||||
array(
|
||||
'parameters' => array(
|
||||
'entity' => array(
|
||||
'type' => 'entity:search_page',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$routes["search.help_$entity_id"] = new Route(
|
||||
'/search/' . $entity->getPath() . '/help',
|
||||
array(
|
||||
'_controller' => 'Drupal\search\Controller\SearchController::searchHelp',
|
||||
'_title' => 'Search help',
|
||||
'entity' => $entity_id,
|
||||
),
|
||||
array(
|
||||
'_entity_access' => 'entity.view',
|
||||
'_permission' => 'search content',
|
||||
),
|
||||
array(
|
||||
'parameters' => array(
|
||||
'entity' => array(
|
||||
'type' => 'entity:search_page',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
return $routes;
|
||||
}
|
||||
|
||||
}
|
49
core/modules/search/src/SearchPageAccessControlHandler.php
Normal file
49
core/modules/search/src/SearchPageAccessControlHandler.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\SearchPageAccessControlHandler.
|
||||
*/
|
||||
|
||||
namespace Drupal\search;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Access\AccessibleInterface;
|
||||
use Drupal\Core\Entity\EntityAccessControlHandler;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Defines the access control handler for the search page entity type.
|
||||
*
|
||||
* @see \Drupal\search\Entity\SearchPage
|
||||
*/
|
||||
class SearchPageAccessControlHandler extends EntityAccessControlHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
|
||||
/** @var $entity \Drupal\search\SearchPageInterface */
|
||||
if (in_array($operation, array('delete', 'disable'))) {
|
||||
if ($entity->isDefaultSearch()) {
|
||||
return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
|
||||
}
|
||||
else {
|
||||
return parent::checkAccess($entity, $operation, $langcode, $account)->cacheUntilEntityChanges($entity);
|
||||
}
|
||||
}
|
||||
if ($operation == 'view') {
|
||||
if (!$entity->status()) {
|
||||
return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
|
||||
}
|
||||
$plugin = $entity->getPlugin();
|
||||
if ($plugin instanceof AccessibleInterface) {
|
||||
return $plugin->access($operation, $account, TRUE)->cacheUntilEntityChanges($entity);
|
||||
}
|
||||
return AccessResult::allowed()->cacheUntilEntityChanges($entity);
|
||||
}
|
||||
return parent::checkAccess($entity, $operation, $langcode, $account);
|
||||
}
|
||||
|
||||
}
|
65
core/modules/search/src/SearchPageInterface.php
Normal file
65
core/modules/search/src/SearchPageInterface.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\SearchPageInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\search;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface defining a search page entity.
|
||||
*/
|
||||
interface SearchPageInterface extends ConfigEntityInterface {
|
||||
|
||||
/**
|
||||
* Returns the search plugin.
|
||||
*
|
||||
* @return \Drupal\search\Plugin\SearchInterface
|
||||
* The search plugin used by this search page entity.
|
||||
*/
|
||||
public function getPlugin();
|
||||
|
||||
/**
|
||||
* Sets the search plugin.
|
||||
*
|
||||
* @param string $plugin_id
|
||||
* The search plugin ID.
|
||||
*/
|
||||
public function setPlugin($plugin_id);
|
||||
|
||||
/**
|
||||
* Determines if this search page entity is currently the default search.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this search page entity is the default search, FALSE otherwise.
|
||||
*/
|
||||
public function isDefaultSearch();
|
||||
|
||||
/**
|
||||
* Determines if this search page entity is indexable.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this search page entity is indexable, FALSE otherwise.
|
||||
*/
|
||||
public function isIndexable();
|
||||
|
||||
/**
|
||||
* Returns the path for the search.
|
||||
*
|
||||
* @return string
|
||||
* The part of the path for this search page that comes after 'search'.
|
||||
*/
|
||||
public function getPath();
|
||||
|
||||
/**
|
||||
* Returns the weight for the page.
|
||||
*
|
||||
* @return int
|
||||
* The page weight.
|
||||
*/
|
||||
public function getWeight();
|
||||
|
||||
}
|
385
core/modules/search/src/SearchPageListBuilder.php
Normal file
385
core/modules/search/src/SearchPageListBuilder.php
Normal file
|
@ -0,0 +1,385 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\SearchPageListBuilder.
|
||||
*/
|
||||
|
||||
namespace Drupal\search;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Config\Entity\DraggableListBuilder;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Form\ConfigFormBaseTrait;
|
||||
use Drupal\Core\Form\FormInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class to build a listing of search page entities.
|
||||
*
|
||||
* @see \Drupal\search\Entity\SearchPage
|
||||
*/
|
||||
class SearchPageListBuilder extends DraggableListBuilder implements FormInterface {
|
||||
use ConfigFormBaseTrait;
|
||||
|
||||
/**
|
||||
* The entities being listed.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageInterface[]
|
||||
*/
|
||||
protected $entities = array();
|
||||
|
||||
/**
|
||||
* Stores the configuration factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* The search manager.
|
||||
*
|
||||
* @var \Drupal\search\SearchPluginManager
|
||||
*/
|
||||
protected $searchManager;
|
||||
|
||||
/**
|
||||
* Constructs a new SearchPageListBuilder object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
|
||||
* The entity storage class.
|
||||
* @param \Drupal\search\SearchPluginManager $search_manager
|
||||
* The search plugin manager.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The factory for configuration objects.
|
||||
*/
|
||||
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, SearchPluginManager $search_manager, ConfigFactoryInterface $config_factory) {
|
||||
parent::__construct($entity_type, $storage);
|
||||
$this->configFactory = $config_factory;
|
||||
$this->searchManager = $search_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$entity_type,
|
||||
$container->get('entity.manager')->getStorage($entity_type->id()),
|
||||
$container->get('plugin.manager.search'),
|
||||
$container->get('config.factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'search_admin_settings';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditableConfigNames() {
|
||||
return ['search.settings'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildHeader() {
|
||||
$header['label'] = array(
|
||||
'data' => $this->t('Label'),
|
||||
);
|
||||
$header['url'] = array(
|
||||
'data' => $this->t('URL'),
|
||||
'class' => array(RESPONSIVE_PRIORITY_LOW),
|
||||
);
|
||||
$header['plugin'] = array(
|
||||
'data' => $this->t('Type'),
|
||||
'class' => array(RESPONSIVE_PRIORITY_LOW),
|
||||
);
|
||||
$header['status'] = array(
|
||||
'data' => $this->t('Status'),
|
||||
);
|
||||
$header['progress'] = array(
|
||||
'data' => $this->t('Indexing progress'),
|
||||
'class' => array(RESPONSIVE_PRIORITY_MEDIUM),
|
||||
);
|
||||
return $header + parent::buildHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRow(EntityInterface $entity) {
|
||||
/** @var $entity \Drupal\search\SearchPageInterface */
|
||||
$row['label'] = $this->getLabel($entity);
|
||||
$row['url']['#markup'] = 'search/' . $entity->getPath();
|
||||
// If the search page is active, link to it.
|
||||
if ($entity->status()) {
|
||||
$row['url'] = array(
|
||||
'#type' => 'link',
|
||||
'#title' => $row['url'],
|
||||
'#url' => Url::fromRoute('search.view_' . $entity->id()),
|
||||
);
|
||||
}
|
||||
|
||||
$definition = $entity->getPlugin()->getPluginDefinition();
|
||||
$row['plugin']['#markup'] = $definition['title'];
|
||||
|
||||
if ($entity->isDefaultSearch()) {
|
||||
$status = $this->t('Default');
|
||||
}
|
||||
elseif ($entity->status()) {
|
||||
$status = $this->t('Enabled');
|
||||
}
|
||||
else {
|
||||
$status = $this->t('Disabled');
|
||||
}
|
||||
$row['status']['#markup'] = $status;
|
||||
|
||||
if ($entity->isIndexable()) {
|
||||
$status = $entity->getPlugin()->indexStatus();
|
||||
$row['progress']['#markup'] = $this->t('%num_indexed of %num_total indexed', array(
|
||||
'%num_indexed' => $status['total'] - $status['remaining'],
|
||||
'%num_total' => $status['total']
|
||||
));
|
||||
}
|
||||
else {
|
||||
$row['progress']['#markup'] = $this->t('Does not use index');
|
||||
}
|
||||
|
||||
return $row + parent::buildRow($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::buildForm($form, $form_state);
|
||||
$search_settings = $this->config('search.settings');
|
||||
// Collect some stats.
|
||||
$remaining = 0;
|
||||
$total = 0;
|
||||
foreach ($this->entities as $entity) {
|
||||
if ($entity->isIndexable() && $status = $entity->getPlugin()->indexStatus()) {
|
||||
$remaining += $status['remaining'];
|
||||
$total += $status['total'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->moduleHandler->loadAllIncludes('admin.inc');
|
||||
$count = $this->formatPlural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.');
|
||||
$done = $total - $remaining;
|
||||
// Use floor() to calculate the percentage, so if it is not quite 100%, it
|
||||
// will show as 99%, to indicate "almost done".
|
||||
$percentage = $total > 0 ? floor(100 * $done / $total) : 100;
|
||||
$percentage .= '%';
|
||||
$status = '<p><strong>' . $this->t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '</strong></p>';
|
||||
$form['status'] = array(
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Indexing progress'),
|
||||
'#open' => TRUE,
|
||||
);
|
||||
$form['status']['status'] = array('#markup' => $status);
|
||||
$form['status']['wipe'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Re-index site'),
|
||||
'#submit' => array('::searchAdminReindexSubmit'),
|
||||
);
|
||||
|
||||
$items = array(10, 20, 50, 100, 200, 500);
|
||||
$items = array_combine($items, $items);
|
||||
|
||||
// Indexing throttle:
|
||||
$form['indexing_throttle'] = array(
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Indexing throttle'),
|
||||
'#open' => TRUE,
|
||||
);
|
||||
$form['indexing_throttle']['cron_limit'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Number of items to index per cron run'),
|
||||
'#default_value' => $search_settings->get('index.cron_limit'),
|
||||
'#options' => $items,
|
||||
'#description' => $this->t('The maximum number of items indexed in each pass of a <a href="@cron">cron maintenance task</a>. If necessary, reduce the number of items to prevent timeouts and memory errors while indexing. Some search page types may have their own setting for this.', array('@cron' => \Drupal::url('system.status'))),
|
||||
);
|
||||
// Indexing settings:
|
||||
$form['indexing_settings'] = array(
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Default indexing settings'),
|
||||
'#open' => TRUE,
|
||||
);
|
||||
$form['indexing_settings']['info'] = array(
|
||||
'#markup' => $this->t("<p>Search pages that use an index may use the default index provided by the Search module, or they may use a different indexing mechanism. These settings are for the default index. <em>Changing these settings will cause the default search index to be rebuilt to reflect the new settings. Searching will continue to work, based on the existing index, but new content won't be indexed until all existing content has been re-indexed.</em></p><p><em>The default settings should be appropriate for the majority of sites.</em></p>")
|
||||
);
|
||||
$form['indexing_settings']['minimum_word_size'] = array(
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Minimum word length to index'),
|
||||
'#default_value' => $search_settings->get('index.minimum_word_size'),
|
||||
'#min' => 1,
|
||||
'#max' => 1000,
|
||||
'#description' => $this->t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).')
|
||||
);
|
||||
$form['indexing_settings']['overlap_cjk'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Simple CJK handling'),
|
||||
'#default_value' => $search_settings->get('index.overlap_cjk'),
|
||||
'#description' => $this->t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.')
|
||||
);
|
||||
|
||||
// Indexing settings:
|
||||
$form['logging'] = array(
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Logging'),
|
||||
'#open' => TRUE,
|
||||
);
|
||||
|
||||
$form['logging']['logging'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Log searches'),
|
||||
'#default_value' => $search_settings->get('logging'),
|
||||
'#description' => $this->t('If checked, all searches will be logged. Uncheck to skip logging. Logging may affect performance.'),
|
||||
);
|
||||
|
||||
$form['search_pages'] = array(
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Search pages'),
|
||||
'#open' => TRUE,
|
||||
);
|
||||
$form['search_pages']['add_page'] = array(
|
||||
'#type' => 'container',
|
||||
'#attributes' => array(
|
||||
'class' => array('container-inline'),
|
||||
),
|
||||
'#attached' => [
|
||||
'library' => [
|
||||
'search/admin',
|
||||
],
|
||||
],
|
||||
);
|
||||
// In order to prevent validation errors for the parent form, this cannot be
|
||||
// required, see self::validateAddSearchPage().
|
||||
$form['search_pages']['add_page']['search_type'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Search page type'),
|
||||
'#empty_option' => $this->t('- Choose page type -'),
|
||||
'#options' => array_map(function ($definition) {
|
||||
return $definition['title'];
|
||||
}, $this->searchManager->getDefinitions()),
|
||||
);
|
||||
$form['search_pages']['add_page']['add_search_submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Add new page'),
|
||||
'#validate' => array('::validateAddSearchPage'),
|
||||
'#submit' => array('::submitAddSearchPage'),
|
||||
'#limit_validation_errors' => array(array('search_type')),
|
||||
);
|
||||
|
||||
// Move the listing into the search_pages element.
|
||||
$form['search_pages'][$this->entitiesKey] = $form[$this->entitiesKey];
|
||||
$form['search_pages'][$this->entitiesKey]['#empty'] = $this->t('No search pages have been configured.');
|
||||
unset($form[$this->entitiesKey]);
|
||||
|
||||
$form['actions']['#type'] = 'actions';
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save configuration'),
|
||||
'#button_type' => 'primary',
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultOperations(EntityInterface $entity) {
|
||||
/** @var $entity \Drupal\search\SearchPageInterface */
|
||||
$operations = parent::getDefaultOperations($entity);
|
||||
|
||||
// Prevent the default search from being disabled or deleted.
|
||||
if ($entity->isDefaultSearch()) {
|
||||
unset($operations['disable'], $operations['delete']);
|
||||
}
|
||||
else {
|
||||
$operations['default'] = array(
|
||||
'title' => $this->t('Set as default'),
|
||||
'url' => Url::fromRoute('entity.search_page.set_default', [
|
||||
'search_page' => $entity->id(),
|
||||
]),
|
||||
'weight' => 50,
|
||||
);
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
parent::submitForm($form, $form_state);
|
||||
|
||||
$search_settings = $this->config('search.settings');
|
||||
// If these settings change, the default index needs to be rebuilt.
|
||||
if (($search_settings->get('index.minimum_word_size') != $form_state->getValue('minimum_word_size')) || ($search_settings->get('index.overlap_cjk') != $form_state->getValue('overlap_cjk'))) {
|
||||
$search_settings->set('index.minimum_word_size', $form_state->getValue('minimum_word_size'));
|
||||
$search_settings->set('index.overlap_cjk', $form_state->getValue('overlap_cjk'));
|
||||
// Specifically mark items in the default index for reindexing, since
|
||||
// these settings are used in the search_index() function.
|
||||
drupal_set_message($this->t('The default search index will be rebuilt.'));
|
||||
search_mark_for_reindex();
|
||||
}
|
||||
|
||||
$search_settings
|
||||
->set('index.cron_limit', $form_state->getValue('cron_limit'))
|
||||
->set('logging', $form_state->getValue('logging'))
|
||||
->save();
|
||||
|
||||
drupal_set_message($this->t('The configuration options have been saved.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for the reindex button on the search admin settings
|
||||
* form.
|
||||
*/
|
||||
public function searchAdminReindexSubmit(array &$form, FormStateInterface $form_state) {
|
||||
// Send the user to the confirmation page.
|
||||
$form_state->setRedirect('search.reindex_confirm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for adding a new search page.
|
||||
*/
|
||||
public function validateAddSearchPage(array &$form, FormStateInterface $form_state) {
|
||||
if ($form_state->isValueEmpty('search_type')) {
|
||||
$form_state->setErrorByName('search_type', $this->t('You must select the new search page type.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for adding a new search page.
|
||||
*/
|
||||
public function submitAddSearchPage(array &$form, FormStateInterface $form_state) {
|
||||
$form_state->setRedirect(
|
||||
'search.add_type',
|
||||
array('search_plugin_id' => $form_state->getValue('search_type'))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
127
core/modules/search/src/SearchPageRepository.php
Normal file
127
core/modules/search/src/SearchPageRepository.php
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\SearchPageRepository.
|
||||
*/
|
||||
|
||||
namespace Drupal\search;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* Provides a repository for Search Page config entities.
|
||||
*/
|
||||
class SearchPageRepository implements SearchPageRepositoryInterface {
|
||||
|
||||
/**
|
||||
* The config factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* The search page storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Constructs a new SearchPageRepository.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory.
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
|
||||
* The entity manager.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, EntityManagerInterface $entity_manager) {
|
||||
$this->configFactory = $config_factory;
|
||||
$this->storage = $entity_manager->getStorage('search_page');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getActiveSearchPages() {
|
||||
$ids = $this->getQuery()
|
||||
->condition('status', TRUE)
|
||||
->execute();
|
||||
return $this->storage->loadMultiple($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSearchActive() {
|
||||
return (bool) $this->getQuery()
|
||||
->condition('status', TRUE)
|
||||
->range(0, 1)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIndexableSearchPages() {
|
||||
return array_filter($this->getActiveSearchPages(), function (SearchPageInterface $search) {
|
||||
return $search->isIndexable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultSearchPage() {
|
||||
// Find all active search pages (without loading them).
|
||||
$search_pages = $this->getQuery()
|
||||
->condition('status', TRUE)
|
||||
->execute();
|
||||
|
||||
// If the default page is active, return it.
|
||||
$default = $this->configFactory->get('search.settings')->get('default_page');
|
||||
if (isset($search_pages[$default])) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// Otherwise, use the first active search page.
|
||||
return is_array($search_pages) ? reset($search_pages) : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function clearDefaultSearchPage() {
|
||||
$this->configFactory->getEditable('search.settings')->clear('default_page')->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setDefaultSearchPage(SearchPageInterface $search_page) {
|
||||
$this->configFactory->getEditable('search.settings')->set('default_page', $search_page->id())->save();
|
||||
$search_page->enable()->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function sortSearchPages($search_pages) {
|
||||
$entity_type = $this->storage->getEntityType();
|
||||
uasort($search_pages, array($entity_type->getClass(), 'sort'));
|
||||
return $search_pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an entity query instance.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\Query\QueryInterface
|
||||
* The query instance.
|
||||
*/
|
||||
protected function getQuery() {
|
||||
return $this->storage->getQuery();
|
||||
}
|
||||
|
||||
}
|
73
core/modules/search/src/SearchPageRepositoryInterface.php
Normal file
73
core/modules/search/src/SearchPageRepositoryInterface.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\SearchPageRepositoryInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\search;
|
||||
|
||||
/**
|
||||
* Provides the interface for a repository Search Page entities.
|
||||
*/
|
||||
interface SearchPageRepositoryInterface {
|
||||
|
||||
/**
|
||||
* Returns all active search page entities.
|
||||
*
|
||||
* @return \Drupal\search\SearchPageInterface[]
|
||||
* An array of active search page entities.
|
||||
*/
|
||||
public function getActiveSearchPages();
|
||||
|
||||
/**
|
||||
* Returns whether search is active.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if at least one search is active, FALSE otherwise.
|
||||
*/
|
||||
public function isSearchActive();
|
||||
|
||||
/**
|
||||
* Returns all active, indexable search page entities.
|
||||
*
|
||||
* @return \Drupal\search\SearchPageInterface[]
|
||||
* An array of indexable search page entities.
|
||||
*/
|
||||
public function getIndexableSearchPages();
|
||||
|
||||
/**
|
||||
* Returns the default search page.
|
||||
*
|
||||
* @return \Drupal\search\SearchPageInterface|bool
|
||||
* The search page entity, or FALSE if no pages are active.
|
||||
*/
|
||||
public function getDefaultSearchPage();
|
||||
|
||||
/**
|
||||
* Sets a given search page as the default.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageInterface $search_page
|
||||
* The search page entity.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function setDefaultSearchPage(SearchPageInterface $search_page);
|
||||
|
||||
/**
|
||||
* Clears the default search page.
|
||||
*/
|
||||
public function clearDefaultSearchPage();
|
||||
|
||||
/**
|
||||
* Sorts a list of search pages.
|
||||
*
|
||||
* @param \Drupal\search\SearchPageInterface[] $search_pages
|
||||
* The unsorted list of search pages.
|
||||
*
|
||||
* @return \Drupal\search\SearchPageInterface[]
|
||||
* The sorted list of search pages.
|
||||
*/
|
||||
public function sortSearchPages($search_pages);
|
||||
|
||||
}
|
35
core/modules/search/src/SearchPluginManager.php
Normal file
35
core/modules/search/src/SearchPluginManager.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\SearchPluginManager.
|
||||
*/
|
||||
|
||||
namespace Drupal\search;
|
||||
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
|
||||
/**
|
||||
* SearchExecute plugin manager.
|
||||
*/
|
||||
class SearchPluginManager extends DefaultPluginManager {
|
||||
|
||||
/**
|
||||
* Constructs SearchPluginManager
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke the alter hook with.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/Search', $namespaces, $module_handler, 'Drupal\search\Plugin\SearchInterface', 'Drupal\search\Annotation\SearchPlugin');
|
||||
$this->setCacheBackend($cache_backend, 'search_plugins');
|
||||
$this->alterInfo('search_plugin');
|
||||
}
|
||||
}
|
654
core/modules/search/src/SearchQuery.php
Normal file
654
core/modules/search/src/SearchQuery.php
Normal file
|
@ -0,0 +1,654 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\SearchQuery.
|
||||
*
|
||||
* Search query extender and helper functions.
|
||||
*/
|
||||
|
||||
namespace Drupal\search;
|
||||
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\Core\Database\Query\SelectExtender;
|
||||
use Drupal\Core\Database\Query\SelectInterface;
|
||||
use Drupal\Core\Database\StatementEmpty;
|
||||
|
||||
/**
|
||||
* Performs a query on the full-text search index for a word or words.
|
||||
*
|
||||
* This query is used by search plugins that use the search index (not all
|
||||
* search plugins do, as some use a different searching mechanism). It
|
||||
* assumes you have set up a query on the {search_index} table with alias 'i',
|
||||
* and will only work if the user is searching for at least one "positive"
|
||||
* keyword or phrase.
|
||||
*
|
||||
* For efficiency, users of this query can run the prepareAndNormalize()
|
||||
* method to figure out if there are any search results, before fully setting
|
||||
* up and calling execute() to execute the query. The scoring expressions are
|
||||
* not needed until the execute() step. However, it's not really necessary
|
||||
* to do this, because this class's execute() method does that anyway.
|
||||
*
|
||||
* During both the prepareAndNormalize() and execute() steps, there can be
|
||||
* problems. Call getStatus() to figure out if the query is OK or not.
|
||||
*
|
||||
* The query object is given the tag 'search_$type' and can be further
|
||||
* extended with hook_query_alter().
|
||||
*/
|
||||
class SearchQuery extends SelectExtender {
|
||||
|
||||
/**
|
||||
* Indicates no positive keywords were in the search expression.
|
||||
*
|
||||
* Positive keywords are words that are searched for, as opposed to negative
|
||||
* keywords, which are words that are excluded. To count as a keyword, a
|
||||
* word must be at least
|
||||
* \Drupal::config('search.settings')->get('index.minimum_word_size')
|
||||
* characters.
|
||||
*
|
||||
* @see SearchQuery::getStatus()
|
||||
*/
|
||||
const NO_POSITIVE_KEYWORDS = 1;
|
||||
|
||||
/**
|
||||
* Indicates that part of the search expression was ignored.
|
||||
*
|
||||
* To prevent Denial of Service attacks, only
|
||||
* \Drupal::config('search.settings')->get('and_or_limit') expressions
|
||||
* (positive keywords, phrases, negative keywords) are allowed; this flag
|
||||
* indicates that expressions existed past that limit and they were removed.
|
||||
*
|
||||
* @see SearchQuery::getStatus()
|
||||
*/
|
||||
const EXPRESSIONS_IGNORED = 2;
|
||||
|
||||
/**
|
||||
* Indicates that lower-case "or" was in the search expression.
|
||||
*
|
||||
* The word "or" in lower case was found in the search expression. This
|
||||
* probably means someone was trying to do an OR search but used lower-case
|
||||
* instead of upper-case.
|
||||
*
|
||||
* @see SearchQuery::getStatus()
|
||||
*/
|
||||
const LOWER_CASE_OR = 4;
|
||||
|
||||
/**
|
||||
* Indicates that no positive keyword matches were found.
|
||||
*
|
||||
* @see SearchQuery::getStatus()
|
||||
*/
|
||||
const NO_KEYWORD_MATCHES = 8;
|
||||
|
||||
/**
|
||||
* The keywords and advanced search options that are entered by the user.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $searchExpression;
|
||||
|
||||
/**
|
||||
* The type of search (search type).
|
||||
*
|
||||
* This maps to the value of the type column in search_index, and is usually
|
||||
* equal to the machine-readable name of the plugin or the search page.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* Parsed-out positive and negative search keys.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $keys = array('positive' => array(), 'negative' => array());
|
||||
|
||||
/**
|
||||
* Indicates whether the query conditions are simple or complex (LIKE).
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $simple = TRUE;
|
||||
|
||||
/**
|
||||
* Conditions that are used for exact searches.
|
||||
*
|
||||
* This is always used for the second step in the query, but is not part of
|
||||
* the preparation step unless $this->simple is FALSE.
|
||||
*
|
||||
* @var DatabaseCondition
|
||||
*/
|
||||
protected $conditions;
|
||||
|
||||
/**
|
||||
* Indicates how many matches for a search query are necessary.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $matches = 0;
|
||||
|
||||
/**
|
||||
* Array of positive search words.
|
||||
*
|
||||
* These words have to match against {search_index}.word.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $words = array();
|
||||
|
||||
/**
|
||||
* Multiplier to normalize the keyword score.
|
||||
*
|
||||
* This value is calculated by the preparation step, and is used as a
|
||||
* multiplier of the word scores to make sure they are between 0 and 1.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected $normalize = 0;
|
||||
|
||||
/**
|
||||
* Indicates whether the preparation step has been executed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $executedPrepare = FALSE;
|
||||
|
||||
/**
|
||||
* A bitmap of status conditions, described in getStatus().
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @see SearchQuery::getStatus()
|
||||
*/
|
||||
protected $status = 0;
|
||||
|
||||
/**
|
||||
* The word score expressions.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see SearchQuery::addScore()
|
||||
*/
|
||||
protected $scores = array();
|
||||
|
||||
/**
|
||||
* Arguments for the score expressions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $scoresArguments = array();
|
||||
|
||||
/**
|
||||
* The number of 'i.relevance' occurrences in score expressions.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $relevance_count = 0;
|
||||
|
||||
/**
|
||||
* Multipliers for score expressions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $multiply = array();
|
||||
|
||||
/**
|
||||
* Sets the search query expression.
|
||||
*
|
||||
* @param string $expression
|
||||
* A search string, which can contain keywords and options.
|
||||
* @param string $type
|
||||
* The search type. This maps to {search_index}.type in the database.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function searchExpression($expression, $type) {
|
||||
$this->searchExpression = $expression;
|
||||
$this->type = $type;
|
||||
|
||||
// Add query tag.
|
||||
$this->addTag('search_' . $type);
|
||||
|
||||
// Initialize conditions and status.
|
||||
$this->conditions = db_and();
|
||||
$this->status = 0;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the search query into SQL conditions.
|
||||
*
|
||||
* Sets up the following variables:
|
||||
* - $this->keys
|
||||
* - $this->words
|
||||
* - $this->conditions
|
||||
* - $this->simple
|
||||
* - $this->matches
|
||||
*/
|
||||
protected function parseSearchExpression() {
|
||||
// Matches words optionally prefixed by a - sign. A word in this case is
|
||||
// something between two spaces, optionally quoted.
|
||||
preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' ' . $this->searchExpression , $keywords, PREG_SET_ORDER);
|
||||
|
||||
if (count($keywords) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Classify tokens.
|
||||
$in_or = FALSE;
|
||||
$limit_combinations = \Drupal::config('search.settings')->get('and_or_limit');
|
||||
// The first search expression does not count as AND.
|
||||
$and_count = -1;
|
||||
$or_count = 0;
|
||||
foreach ($keywords as $match) {
|
||||
if ($or_count && $and_count + $or_count >= $limit_combinations) {
|
||||
// Ignore all further search expressions to prevent Denial-of-Service
|
||||
// attacks using a high number of AND/OR combinations.
|
||||
$this->status |= SearchQuery::EXPRESSIONS_IGNORED;
|
||||
break;
|
||||
}
|
||||
|
||||
// Strip off phrase quotes.
|
||||
$phrase = FALSE;
|
||||
if ($match[2]{0} == '"') {
|
||||
$match[2] = substr($match[2], 1, -1);
|
||||
$phrase = TRUE;
|
||||
$this->simple = FALSE;
|
||||
}
|
||||
|
||||
// Simplify keyword according to indexing rules and external
|
||||
// preprocessors. Use same process as during search indexing, so it
|
||||
// will match search index.
|
||||
$words = search_simplify($match[2]);
|
||||
// Re-explode in case simplification added more words, except when
|
||||
// matching a phrase.
|
||||
$words = $phrase ? array($words) : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY);
|
||||
// Negative matches.
|
||||
if ($match[1] == '-') {
|
||||
$this->keys['negative'] = array_merge($this->keys['negative'], $words);
|
||||
}
|
||||
// OR operator: instead of a single keyword, we store an array of all
|
||||
// OR'd keywords.
|
||||
elseif ($match[2] == 'OR' && count($this->keys['positive'])) {
|
||||
$last = array_pop($this->keys['positive']);
|
||||
// Starting a new OR?
|
||||
if (!is_array($last)) {
|
||||
$last = array($last);
|
||||
}
|
||||
$this->keys['positive'][] = $last;
|
||||
$in_or = TRUE;
|
||||
$or_count++;
|
||||
continue;
|
||||
}
|
||||
// AND operator: implied, so just ignore it.
|
||||
elseif ($match[2] == 'AND' || $match[2] == 'and') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Plain keyword.
|
||||
else {
|
||||
if ($match[2] == 'or') {
|
||||
// Lower-case "or" instead of "OR" is a warning condition.
|
||||
$this->status |= SearchQuery::LOWER_CASE_OR;
|
||||
}
|
||||
if ($in_or) {
|
||||
// Add to last element (which is an array).
|
||||
$this->keys['positive'][count($this->keys['positive']) - 1] = array_merge($this->keys['positive'][count($this->keys['positive']) - 1], $words);
|
||||
}
|
||||
else {
|
||||
$this->keys['positive'] = array_merge($this->keys['positive'], $words);
|
||||
$and_count++;
|
||||
}
|
||||
}
|
||||
$in_or = FALSE;
|
||||
}
|
||||
|
||||
// Convert keywords into SQL statements.
|
||||
$has_and = FALSE;
|
||||
$has_or = FALSE;
|
||||
// Positive matches.
|
||||
foreach ($this->keys['positive'] as $key) {
|
||||
// Group of ORed terms.
|
||||
if (is_array($key) && count($key)) {
|
||||
// If we had already found one OR, this is another one AND-ed with the
|
||||
// first, meaning it is not a simple query.
|
||||
if ($has_or) {
|
||||
$this->simple = FALSE;
|
||||
}
|
||||
$has_or = TRUE;
|
||||
$has_new_scores = FALSE;
|
||||
$queryor = db_or();
|
||||
foreach ($key as $or) {
|
||||
list($num_new_scores) = $this->parseWord($or);
|
||||
$has_new_scores |= $num_new_scores;
|
||||
$queryor->condition('d.data', "% $or %", 'LIKE');
|
||||
}
|
||||
if (count($queryor)) {
|
||||
$this->conditions->condition($queryor);
|
||||
// A group of OR keywords only needs to match once.
|
||||
$this->matches += ($has_new_scores > 0);
|
||||
}
|
||||
}
|
||||
// Single ANDed term.
|
||||
else {
|
||||
$has_and = TRUE;
|
||||
list($num_new_scores, $num_valid_words) = $this->parseWord($key);
|
||||
$this->conditions->condition('d.data', "% $key %", 'LIKE');
|
||||
if (!$num_valid_words) {
|
||||
$this->simple = FALSE;
|
||||
}
|
||||
// Each AND keyword needs to match at least once.
|
||||
$this->matches += $num_new_scores;
|
||||
}
|
||||
}
|
||||
if ($has_and && $has_or) {
|
||||
$this->simple = FALSE;
|
||||
}
|
||||
|
||||
// Negative matches.
|
||||
foreach ($this->keys['negative'] as $key) {
|
||||
$this->conditions->condition('d.data', "% $key %", 'NOT LIKE');
|
||||
$this->simple = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a word or phrase for parseQuery().
|
||||
*
|
||||
* Splits a phrase into words. Adds its words to $this->words, if it is not
|
||||
* already there. Returns a list containing the number of new words found,
|
||||
* and the total number of words in the phrase.
|
||||
*/
|
||||
protected function parseWord($word) {
|
||||
$num_new_scores = 0;
|
||||
$num_valid_words = 0;
|
||||
|
||||
// Determine the scorewords of this word/phrase.
|
||||
$split = explode(' ', $word);
|
||||
foreach ($split as $s) {
|
||||
$num = is_numeric($s);
|
||||
if ($num || Unicode::strlen($s) >= \Drupal::config('search.settings')->get('index.minimum_word_size')) {
|
||||
if (!isset($this->words[$s])) {
|
||||
$this->words[$s] = $s;
|
||||
$num_new_scores++;
|
||||
}
|
||||
$num_valid_words++;
|
||||
}
|
||||
}
|
||||
|
||||
// Return matching snippet and number of added words.
|
||||
return array($num_new_scores, $num_valid_words);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the query and calculates the normalization factor.
|
||||
*
|
||||
* After the query is normalized the keywords are weighted to give the results
|
||||
* a relevancy score. The query is ready for execution after this.
|
||||
*
|
||||
* Error and warning conditions can apply. Call getStatus() after calling
|
||||
* this method to retrieve them.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if at least one keyword matched the search index; FALSE if not.
|
||||
*/
|
||||
public function prepareAndNormalize() {
|
||||
$this->parseSearchExpression();
|
||||
$this->executedPrepare = TRUE;
|
||||
|
||||
if (count($this->words) == 0) {
|
||||
// Although the query could proceed, there is no point in joining
|
||||
// with other tables and attempting to normalize if there are no
|
||||
// keywords present.
|
||||
$this->status |= SearchQuery::NO_POSITIVE_KEYWORDS;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Build the basic search query: match the entered keywords.
|
||||
$or = db_or();
|
||||
foreach ($this->words as $word) {
|
||||
$or->condition('i.word', $word);
|
||||
}
|
||||
$this->condition($or);
|
||||
|
||||
// Add keyword normalization information to the query.
|
||||
$this->join('search_total', 't', 'i.word = t.word');
|
||||
$this
|
||||
->condition('i.type', $this->type)
|
||||
->groupBy('i.type')
|
||||
->groupBy('i.sid');
|
||||
|
||||
// If the query is simple, we should have calculated the number of
|
||||
// matching words we need to find, so impose that criterion. For non-
|
||||
// simple queries, this condition could lead to incorrectly deciding not
|
||||
// to continue with the full query.
|
||||
if ($this->simple) {
|
||||
$this->having('COUNT(*) >= :matches', array(':matches' => $this->matches));
|
||||
}
|
||||
|
||||
// Clone the query object to calculate normalization.
|
||||
$normalize_query = clone $this->query;
|
||||
|
||||
// For complex search queries, add the LIKE conditions; if the query is
|
||||
// simple, we do not need them for normalization.
|
||||
if (!$this->simple) {
|
||||
$normalize_query->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type AND i.langcode = d.langcode');
|
||||
if (count($this->conditions)) {
|
||||
$normalize_query->condition($this->conditions);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate normalization, which is the max of all the search scores for
|
||||
// positive keywords in the query. And note that the query could have other
|
||||
// fields added to it by the user of this extension.
|
||||
$normalize_query->addExpression('SUM(i.score * t.count)', 'calculated_score');
|
||||
$result = $normalize_query
|
||||
->range(0, 1)
|
||||
->orderBy('calculated_score', 'DESC')
|
||||
->execute()
|
||||
->fetchObject();
|
||||
if (isset($result->calculated_score)) {
|
||||
$this->normalize = (float) $result->calculated_score;
|
||||
}
|
||||
|
||||
if ($this->normalize) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// If the normalization value was zero, that indicates there were no
|
||||
// matches to the supplied positive keywords.
|
||||
$this->status |= SearchQuery::NO_KEYWORD_MATCHES;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function preExecute(SelectInterface $query = NULL) {
|
||||
if (!$this->executedPrepare) {
|
||||
$this->prepareAndNormalize();
|
||||
}
|
||||
|
||||
if (!$this->normalize) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return parent::preExecute($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom score expression to the search query.
|
||||
*
|
||||
* Score expressions are used to order search results. If no calls to
|
||||
* addScore() have taken place, a default keyword relevance score will be
|
||||
* used. However, if at least one call to addScore() has taken place, the
|
||||
* keyword relevance score is not automatically added.
|
||||
*
|
||||
* Note that you must use this method to add ordering to your searches, and
|
||||
* not call orderBy() directly, when using the SearchQuery extender. This is
|
||||
* because of the two-pass system the SearchQuery class uses to normalize
|
||||
* scores.
|
||||
*
|
||||
* @param string $score
|
||||
* The score expression, which should evaluate to a number between 0 and 1.
|
||||
* The string 'i.relevance' in a score expression will be replaced by a
|
||||
* measure of keyword relevance between 0 and 1.
|
||||
* @param array $arguments
|
||||
* Query arguments needed to provide values to the score expression.
|
||||
* @param float $multiply
|
||||
* If set, the score is multiplied with this value. However, all scores
|
||||
* with multipliers are then divided by the total of all multipliers, so
|
||||
* that overall, the normalization is maintained.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addScore($score, $arguments = array(), $multiply = FALSE) {
|
||||
if ($multiply) {
|
||||
$i = count($this->multiply);
|
||||
// Modify the score expression so it is multiplied by the multiplier,
|
||||
// with a divisor to renormalize. Note that the ROUND here is necessary
|
||||
// for PostgreSQL and SQLite in order to ensure that the :multiply_* and
|
||||
// :total_* arguments are treated as a numeric type, because the
|
||||
// PostgreSQL PDO driver sometimes puts values in as strings instead of
|
||||
// numbers in complex expressions like this.
|
||||
$score = "(ROUND(:multiply_$i, 4)) * COALESCE(($score), 0) / (ROUND(:total_$i, 4))";
|
||||
// Add an argument for the multiplier. The :total_$i argument is taken
|
||||
// care of in the execute() method, which is when the total divisor is
|
||||
// calculated.
|
||||
$arguments[':multiply_' . $i] = $multiply;
|
||||
$this->multiply[] = $multiply;
|
||||
}
|
||||
|
||||
// Search scoring needs a way to include a keyword relevance in the score.
|
||||
// For historical reasons, this is done by putting 'i.relevance' into the
|
||||
// search expression. So, use string replacement to change this to a
|
||||
// calculated query expression, counting the number of occurrences so
|
||||
// in the execute() method we can add arguments.
|
||||
while (($pos = strpos($score, 'i.relevance')) !== FALSE) {
|
||||
$pieces = explode('i.relevance', $score, 2);
|
||||
$score = implode('((ROUND(:normalization_' . $this->relevance_count . ', 4)) * i.score * t.count)', $pieces);
|
||||
$this->relevance_count++;
|
||||
}
|
||||
|
||||
$this->scores[] = $score;
|
||||
$this->scoresArguments += $arguments;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the search.
|
||||
*
|
||||
* The complex conditions are applied to the query including score
|
||||
* expressions and ordering.
|
||||
*
|
||||
* Error and warning conditions can apply. Call getStatus() after calling
|
||||
* this method to retrieve them.
|
||||
*
|
||||
* @return \Drupal\Core\Database\StatementInterface|null
|
||||
* A query result set containing the results of the query.
|
||||
*/
|
||||
public function execute() {
|
||||
if (!$this->preExecute($this)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add conditions to the query.
|
||||
$this->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type AND i.langcode = d.langcode');
|
||||
if (count($this->conditions)) {
|
||||
$this->condition($this->conditions);
|
||||
}
|
||||
|
||||
// Add default score (keyword relevance) if there are not any defined.
|
||||
if (empty($this->scores)) {
|
||||
$this->addScore('i.relevance');
|
||||
}
|
||||
|
||||
if (count($this->multiply)) {
|
||||
// Re-normalize scores with multipliers by dividing by the total of all
|
||||
// multipliers. The expressions were altered in addScore(), so here just
|
||||
// add the arguments for the total.
|
||||
$sum = array_sum($this->multiply);
|
||||
for ($i = 0; $i < count($this->multiply); $i++) {
|
||||
$this->scoresArguments[':total_' . $i] = $sum;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add arguments for the keyword relevance normalization number.
|
||||
$normalization = 1.0 / $this->normalize;
|
||||
for ($i = 0; $i < $this->relevance_count; $i++ ) {
|
||||
$this->scoresArguments[':normalization_' . $i] = $normalization;
|
||||
}
|
||||
|
||||
// Add all scores together to form a query field.
|
||||
$this->addExpression('SUM(' . implode(' + ', $this->scores) . ')', 'calculated_score', $this->scoresArguments);
|
||||
|
||||
// If an order has not yet been set for this query, add a default order
|
||||
// that sorts by the calculated sum of scores.
|
||||
if (count($this->getOrderBy()) == 0) {
|
||||
$this->orderBy('calculated_score', 'DESC');
|
||||
}
|
||||
|
||||
// Add query metadata.
|
||||
$this
|
||||
->addMetaData('normalize', $this->normalize)
|
||||
->fields('i', array('type', 'sid'));
|
||||
return $this->query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the default count query for SearchQuery.
|
||||
*
|
||||
* Since SearchQuery always uses GROUP BY, we can default to a subquery. We
|
||||
* also add the same conditions as execute() because countQuery() is called
|
||||
* first.
|
||||
*/
|
||||
public function countQuery() {
|
||||
if (!$this->executedPrepare) {
|
||||
$this->prepareAndNormalize();
|
||||
}
|
||||
|
||||
// Clone the inner query.
|
||||
$inner = clone $this->query;
|
||||
|
||||
// Add conditions to query.
|
||||
$inner->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type');
|
||||
if (count($this->conditions)) {
|
||||
$inner->condition($this->conditions);
|
||||
}
|
||||
|
||||
// Remove existing fields and expressions, they are not needed for a count
|
||||
// query.
|
||||
$fields =& $inner->getFields();
|
||||
$fields = array();
|
||||
$expressions =& $inner->getExpressions();
|
||||
$expressions = array();
|
||||
|
||||
// Add sid as the only field and count them as a subquery.
|
||||
$count = db_select($inner->fields('i', array('sid')), NULL, array('target' => 'replica'));
|
||||
|
||||
// Add the COUNT() expression.
|
||||
$count->addExpression('COUNT(*)');
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query status bitmap.
|
||||
*
|
||||
* @return int
|
||||
* A bitmap indicating query status. Zero indicates there were no problems.
|
||||
* A non-zero value is a combination of one or more of the following flags:
|
||||
* - SearchQuery::NO_POSITIVE_KEYWORDS
|
||||
* - SearchQuery::EXPRESSIONS_IGNORED
|
||||
* - SearchQuery::LOWER_CASE_OR
|
||||
* - SearchQuery::NO_KEYWORD_MATCHES
|
||||
*/
|
||||
public function getStatus() {
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchAdvancedSearchFormTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Indexes content and tests the advanced search form.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchAdvancedSearchFormTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* A node to use for testing.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
// Create and login user.
|
||||
$test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes'));
|
||||
$this->drupalLogin($test_user);
|
||||
|
||||
// Create initial node.
|
||||
$this->node = $this->drupalCreateNode();
|
||||
|
||||
// First update the index. This does the initial processing.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
|
||||
// Then, run the shutdown function. Testing is a unique case where indexing
|
||||
// and searching has to happen in the same request, so running the shutdown
|
||||
// function manually is needed to finish the indexing process.
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test using the search form with GET and POST queries.
|
||||
* Test using the advanced search form to limit search to nodes of type "Basic page".
|
||||
*/
|
||||
function testNodeType() {
|
||||
$this->assertTrue($this->node->getType() == 'page', 'Node type is Basic page.');
|
||||
|
||||
// Assert that the dummy title doesn't equal the real title.
|
||||
$dummy_title = 'Lorem ipsum';
|
||||
$this->assertNotEqual($dummy_title, $this->node->label(), "Dummy title doesn't equal node title.");
|
||||
|
||||
// Search for the dummy title with a GET query.
|
||||
$this->drupalGet('search/node', array('query' => array('keys' => $dummy_title)));
|
||||
$this->assertNoText($this->node->label(), 'Basic page node is not found with dummy title.');
|
||||
|
||||
// Search for the title of the node with a GET query.
|
||||
$this->drupalGet('search/node', array('query' => array('keys' => $this->node->label())));
|
||||
$this->assertText($this->node->label(), 'Basic page node is found with GET query.');
|
||||
|
||||
// Search for the title of the node with a POST query.
|
||||
$edit = array('or' => $this->node->label());
|
||||
$this->drupalPostForm('search/node', $edit, t('Advanced search'));
|
||||
$this->assertText($this->node->label(), 'Basic page node is found with POST query.');
|
||||
|
||||
// Advanced search type option.
|
||||
$this->drupalPostForm('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search'));
|
||||
$this->assertText($this->node->label(), 'Basic page node is found with POST query and type:page.');
|
||||
|
||||
$this->drupalPostForm('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search'));
|
||||
$this->assertText('search yielded no results', 'Article node is not found with POST query and type:article.');
|
||||
}
|
||||
}
|
108
core/modules/search/src/Tests/SearchBlockTest.php
Normal file
108
core/modules/search/src/Tests/SearchBlockTest.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchBlockTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests if the search form block is available.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchBlockTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('block');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create and login user.
|
||||
$admin_user = $this->drupalCreateUser(array('administer blocks', 'search content'));
|
||||
$this->drupalLogin($admin_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the search form block can be placed and works.
|
||||
*/
|
||||
public function testSearchFormBlock() {
|
||||
|
||||
// Test availability of the search block in the admin "Place blocks" list.
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$this->assertLinkByHref('/admin/structure/block/add/search_form_block/classy', 0,
|
||||
'Did not find the search block in block candidate list.');
|
||||
|
||||
$block = $this->drupalPlaceBlock('search_form_block');
|
||||
|
||||
$this->drupalGet('');
|
||||
$this->assertText($block->label(), 'Block title was found.');
|
||||
|
||||
// Test a normal search via the block form, from the front page.
|
||||
$terms = array('keys' => 'test');
|
||||
$this->submitGetForm('', $terms, t('Search'));
|
||||
$this->assertResponse(200);
|
||||
$this->assertText('Your search yielded no results');
|
||||
|
||||
// Test a search from the block on a 404 page.
|
||||
$this->drupalGet('foo');
|
||||
$this->assertResponse(404);
|
||||
$this->submitGetForm(NULL, $terms, t('Search'));
|
||||
$this->assertResponse(200);
|
||||
$this->assertText('Your search yielded no results');
|
||||
|
||||
$visibility = $block->getVisibility();
|
||||
$visibility['request_path']['pages'] = 'search';
|
||||
$block->setVisibilityConfig('request_path', $visibility['request_path']);
|
||||
|
||||
$this->submitGetForm('', $terms, t('Search'));
|
||||
$this->assertResponse(200);
|
||||
$this->assertText('Your search yielded no results');
|
||||
|
||||
// Confirm that the form submits to the default search page.
|
||||
/** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
|
||||
$search_page_repository = \Drupal::service('search.search_page_repository');
|
||||
$entity_id = $search_page_repository->getDefaultSearchPage();
|
||||
$this->assertEqual(
|
||||
$this->getUrl(),
|
||||
\Drupal::url('search.view_' . $entity_id, array(), array('query' => array('keys' => $terms['keys']), 'absolute' => TRUE)),
|
||||
'Submitted to correct url.'
|
||||
);
|
||||
|
||||
// Test an empty search via the block form, from the front page.
|
||||
$terms = array('keys' => '');
|
||||
$this->submitGetForm('', $terms, t('Search'));
|
||||
$this->assertResponse(200);
|
||||
$this->assertText('Please enter some keywords');
|
||||
|
||||
// Confirm that the user is redirected to the search page, when form is
|
||||
// submitted empty.
|
||||
$this->assertEqual(
|
||||
$this->getUrl(),
|
||||
\Drupal::url('search.view_' . $entity_id, array(), array('query' => array('keys' => ''), 'absolute' => TRUE)),
|
||||
'Redirected to correct url.'
|
||||
);
|
||||
|
||||
// Test that after entering a too-short keyword in the form, you can then
|
||||
// search again with a longer keyword. First test using the block form.
|
||||
$this->submitGetForm('node', array('keys' => $this->randomMachineName(1)), t('Search'));
|
||||
$this->assertText('You must include at least one positive keyword', 'Keyword message is displayed when searching for short word');
|
||||
$this->assertNoText(t('Please enter some keywords'), 'With short word entered, no keywords message is not displayed');
|
||||
$this->submitGetForm(NULL, array('keys' => $this->randomMachineName()), t('Search'), 'search-block-form');
|
||||
$this->assertNoText('You must include at least one positive keyword', 'Keyword message is not displayed when searching for long word after short word search');
|
||||
|
||||
// Same test again, using the search page form for the second search this
|
||||
// time.
|
||||
$this->submitGetForm('node', array('keys' => $this->randomMachineName(1)), t('Search'));
|
||||
$this->drupalPostForm(NULL, array('keys' => $this->randomMachineName()), t('Search'), array(), array(), 'search-form');
|
||||
$this->assertNoText('You must include at least one positive keyword', 'Keyword message is not displayed when searching for long word after short word search');
|
||||
|
||||
}
|
||||
|
||||
}
|
119
core/modules/search/src/Tests/SearchCommentCountToggleTest.php
Normal file
119
core/modules/search/src/Tests/SearchCommentCountToggleTest.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchCommentCountToggleTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
|
||||
/**
|
||||
* Tests that comment count display toggles properly on comment status of node.
|
||||
*
|
||||
* Issue 537278
|
||||
*
|
||||
* - Nodes with comment status set to Open should always how comment counts
|
||||
* - Nodes with comment status set to Closed should show comment counts
|
||||
* only when there are comments
|
||||
* - Nodes with comment status set to Hidden should never show comment counts
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchCommentCountToggleTest extends SearchTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('node', 'comment');
|
||||
|
||||
/**
|
||||
* A user with permission to search and post comments.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $searchingUser;
|
||||
|
||||
/**
|
||||
* Array of nodes available to search.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $searchableNodes;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create searching user.
|
||||
$this->searchingUser = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'post comments', 'skip comment approval'));
|
||||
|
||||
// Login with sufficient privileges.
|
||||
$this->drupalLogin($this->searchingUser);
|
||||
|
||||
// Add a comment field.
|
||||
$this->addDefaultCommentField('node', 'article');
|
||||
// Create initial nodes.
|
||||
$node_params = array('type' => 'article', 'body' => array(array('value' => 'SearchCommentToggleTestCase')));
|
||||
|
||||
$this->searchableNodes['1 comment'] = $this->drupalCreateNode($node_params);
|
||||
$this->searchableNodes['0 comments'] = $this->drupalCreateNode($node_params);
|
||||
|
||||
// Create a comment array
|
||||
$edit_comment = array();
|
||||
$edit_comment['subject[0][value]'] = $this->randomMachineName();
|
||||
$edit_comment['comment_body[0][value]'] = $this->randomMachineName();
|
||||
|
||||
// Post comment to the test node with comment
|
||||
$this->drupalPostForm('comment/reply/node/' . $this->searchableNodes['1 comment']->id() . '/comment', $edit_comment, t('Save'));
|
||||
|
||||
// First update the index. This does the initial processing.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
|
||||
// Then, run the shutdown function. Testing is a unique case where indexing
|
||||
// and searching has to happen in the same request, so running the shutdown
|
||||
// function manually is needed to finish the indexing process.
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that comment count display toggles properly on comment status of node
|
||||
*/
|
||||
function testSearchCommentCountToggle() {
|
||||
// Search for the nodes by string in the node body.
|
||||
$edit = array(
|
||||
'keys' => "'SearchCommentToggleTestCase'",
|
||||
);
|
||||
$this->drupalGet('search/node');
|
||||
|
||||
// Test comment count display for nodes with comment status set to Open
|
||||
$this->drupalPostForm(NULL, $edit, t('Search'));
|
||||
$this->assertText(t('0 comments'), 'Empty comment count displays for nodes with comment status set to Open');
|
||||
$this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Open');
|
||||
|
||||
// Test comment count display for nodes with comment status set to Closed
|
||||
$this->searchableNodes['0 comments']->set('comment', CommentItemInterface::CLOSED);
|
||||
$this->searchableNodes['0 comments']->save();
|
||||
$this->searchableNodes['1 comment']->set('comment', CommentItemInterface::CLOSED);
|
||||
$this->searchableNodes['1 comment']->save();
|
||||
|
||||
$this->drupalPostForm(NULL, $edit, t('Search'));
|
||||
$this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Closed');
|
||||
$this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Closed');
|
||||
|
||||
// Test comment count display for nodes with comment status set to Hidden
|
||||
$this->searchableNodes['0 comments']->set('comment', CommentItemInterface::HIDDEN);
|
||||
$this->searchableNodes['0 comments']->save();
|
||||
$this->searchableNodes['1 comment']->set('comment', CommentItemInterface::HIDDEN);
|
||||
$this->searchableNodes['1 comment']->save();
|
||||
|
||||
$this->drupalPostForm(NULL, $edit, t('Search'));
|
||||
$this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Hidden');
|
||||
$this->assertNoText(t('1 comment'), 'Non-empty comment count does not display for nodes with comment status set to Hidden');
|
||||
}
|
||||
}
|
307
core/modules/search/src/Tests/SearchCommentTest.php
Normal file
307
core/modules/search/src/Tests/SearchCommentTest.php
Normal file
|
@ -0,0 +1,307 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchCommentTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\user\RoleInterface;
|
||||
|
||||
/**
|
||||
* Tests integration searching comments.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchCommentTest extends SearchTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'node', 'comment');
|
||||
|
||||
/**
|
||||
* Test subject for comments.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $commentSubject;
|
||||
|
||||
/**
|
||||
* ID for the administrator role.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $adminRole;
|
||||
|
||||
/**
|
||||
* A user with various administrative permissions.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Test node for searching.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$full_html_format = entity_create('filter_format', array(
|
||||
'format' => 'full_html',
|
||||
'name' => 'Full HTML',
|
||||
'weight' => 1,
|
||||
'filters' => array(),
|
||||
));
|
||||
$full_html_format->save();
|
||||
|
||||
// Create and log in an administrative user having access to the Full HTML
|
||||
// text format.
|
||||
$permissions = array(
|
||||
'administer filters',
|
||||
$full_html_format->getPermissionName(),
|
||||
'administer permissions',
|
||||
'create page content',
|
||||
'post comments',
|
||||
'skip comment approval',
|
||||
'access comments',
|
||||
);
|
||||
$this->adminUser = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
// Add a comment field.
|
||||
$this->addDefaultCommentField('node', 'article');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that comments are rendered using proper format in search results.
|
||||
*/
|
||||
function testSearchResultsComment() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
// Create basic_html format that escapes all HTML.
|
||||
$basic_html_format = entity_create('filter_format', array(
|
||||
'format' => 'basic_html',
|
||||
'name' => 'Basic HTML',
|
||||
'weight' => 1,
|
||||
'filters' => array(
|
||||
'filter_html_escape' => array('status' => 1),
|
||||
),
|
||||
'roles' => array(RoleInterface::AUTHENTICATED_ID),
|
||||
));
|
||||
$basic_html_format->save();
|
||||
|
||||
$comment_body = 'Test comment body';
|
||||
|
||||
// Make preview optional.
|
||||
$field = FieldConfig::loadByName('node', 'article', 'comment');
|
||||
$field->setSetting('preview', DRUPAL_OPTIONAL);
|
||||
$field->save();
|
||||
|
||||
// Allow anonymous users to search content.
|
||||
$edit = array(
|
||||
RoleInterface::ANONYMOUS_ID . '[search content]' => 1,
|
||||
RoleInterface::ANONYMOUS_ID . '[access comments]' => 1,
|
||||
RoleInterface::ANONYMOUS_ID . '[post comments]' => 1,
|
||||
);
|
||||
$this->drupalPostForm('admin/people/permissions', $edit, t('Save permissions'));
|
||||
|
||||
// Create a node.
|
||||
$node = $this->drupalCreateNode(array('type' => 'article'));
|
||||
// Post a comment using 'Full HTML' text format.
|
||||
$edit_comment = array();
|
||||
$edit_comment['subject[0][value]'] = 'Test comment subject';
|
||||
$edit_comment['comment_body[0][value]'] = '<h1>' . $comment_body . '</h1>';
|
||||
$full_html_format_id = 'full_html';
|
||||
$edit_comment['comment_body[0][format]'] = $full_html_format_id;
|
||||
$this->drupalPostForm('comment/reply/node/' . $node->id() .'/comment', $edit_comment, t('Save'));
|
||||
|
||||
// Invoke search index update.
|
||||
$this->drupalLogout();
|
||||
$this->cronRun();
|
||||
|
||||
// Search for the comment subject.
|
||||
$edit = array(
|
||||
'keys' => "'" . $edit_comment['subject[0][value]'] . "'",
|
||||
);
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$node_storage->resetCache(array($node->id()));
|
||||
$node2 = $node_storage->load($node->id());
|
||||
$this->assertText($node2->label(), 'Node found in search results.');
|
||||
$this->assertText($edit_comment['subject[0][value]'], 'Comment subject found in search results.');
|
||||
|
||||
// Search for the comment body.
|
||||
$edit = array(
|
||||
'keys' => "'" . $comment_body . "'",
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Search'));
|
||||
$this->assertText($node2->label(), 'Node found in search results.');
|
||||
|
||||
// Verify that comment is rendered using proper format.
|
||||
$this->assertText($comment_body, 'Comment body text found in search results.');
|
||||
$this->assertNoRaw(t('n/a'), 'HTML in comment body is not hidden.');
|
||||
$this->assertNoEscaped($edit_comment['comment_body[0][value]'], 'HTML in comment body is not escaped.');
|
||||
|
||||
// Hide comments.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$node->set('comment', CommentItemInterface::HIDDEN);
|
||||
$node->save();
|
||||
|
||||
// Invoke search index update.
|
||||
$this->drupalLogout();
|
||||
$this->cronRun();
|
||||
|
||||
// Search for $title.
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText(t('Your search yielded no results.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify access rules for comment indexing with different permissions.
|
||||
*/
|
||||
function testSearchResultsCommentAccess() {
|
||||
$comment_body = 'Test comment body';
|
||||
$this->commentSubject = 'Test comment subject';
|
||||
$roles = $this->adminUser->getRoles(TRUE);
|
||||
$this->adminRole = $roles[0];
|
||||
|
||||
// Create a node.
|
||||
// Make preview optional.
|
||||
$field = FieldConfig::loadByName('node', 'article', 'comment');
|
||||
$field->setSetting('preview', DRUPAL_OPTIONAL);
|
||||
$field->save();
|
||||
$this->node = $this->drupalCreateNode(array('type' => 'article'));
|
||||
|
||||
// Post a comment using 'Full HTML' text format.
|
||||
$edit_comment = array();
|
||||
$edit_comment['subject[0][value]'] = $this->commentSubject;
|
||||
$edit_comment['comment_body[0][value]'] = '<h1>' . $comment_body . '</h1>';
|
||||
$this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit_comment, t('Save'));
|
||||
|
||||
$this->drupalLogout();
|
||||
$this->setRolePermissions(RoleInterface::ANONYMOUS_ID);
|
||||
$this->assertCommentAccess(FALSE, 'Anon user has search permission but no access comments permission, comments should not be indexed');
|
||||
|
||||
$this->setRolePermissions(RoleInterface::ANONYMOUS_ID, TRUE);
|
||||
$this->assertCommentAccess(TRUE, 'Anon user has search permission and access comments permission, comments should be indexed');
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/people/permissions');
|
||||
|
||||
// Disable search access for authenticated user to test admin user.
|
||||
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID, FALSE, FALSE);
|
||||
|
||||
$this->setRolePermissions($this->adminRole);
|
||||
$this->assertCommentAccess(FALSE, 'Admin user has search permission but no access comments permission, comments should not be indexed');
|
||||
|
||||
$this->drupalGet('node/' . $this->node->id());
|
||||
$this->setRolePermissions($this->adminRole, TRUE);
|
||||
$this->assertCommentAccess(TRUE, 'Admin user has search permission and access comments permission, comments should be indexed');
|
||||
|
||||
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID);
|
||||
$this->assertCommentAccess(FALSE, 'Authenticated user has search permission but no access comments permission, comments should not be indexed');
|
||||
|
||||
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID, TRUE);
|
||||
$this->assertCommentAccess(TRUE, 'Authenticated user has search permission and access comments permission, comments should be indexed');
|
||||
|
||||
// Verify that access comments permission is inherited from the
|
||||
// authenticated role.
|
||||
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID, TRUE, FALSE);
|
||||
$this->setRolePermissions($this->adminRole);
|
||||
$this->assertCommentAccess(TRUE, 'Admin user has search permission and no access comments permission, but comments should be indexed because admin user inherits authenticated user\'s permission to access comments');
|
||||
|
||||
// Verify that search content permission is inherited from the authenticated
|
||||
// role.
|
||||
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID, TRUE, TRUE);
|
||||
$this->setRolePermissions($this->adminRole, TRUE, FALSE);
|
||||
$this->assertCommentAccess(TRUE, 'Admin user has access comments permission and no search permission, but comments should be indexed because admin user inherits authenticated user\'s permission to search');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set permissions for role.
|
||||
*/
|
||||
function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) {
|
||||
$permissions = array(
|
||||
'access comments' => $access_comments,
|
||||
'search content' => $search_content,
|
||||
);
|
||||
user_role_change_permissions($rid, $permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search index and search for comment.
|
||||
*/
|
||||
function assertCommentAccess($assume_access, $message) {
|
||||
// Invoke search index update.
|
||||
search_mark_for_reindex('node_search', $this->node->id());
|
||||
$this->cronRun();
|
||||
|
||||
// Search for the comment subject.
|
||||
$edit = array(
|
||||
'keys' => "'" . $this->commentSubject . "'",
|
||||
);
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
|
||||
if ($assume_access) {
|
||||
$expected_node_result = $this->assertText($this->node->label());
|
||||
$expected_comment_result = $this->assertText($this->commentSubject);
|
||||
}
|
||||
else {
|
||||
$expected_node_result = $this->assertText(t('Your search yielded no results.'));
|
||||
$expected_comment_result = $this->assertText(t('Your search yielded no results.'));
|
||||
}
|
||||
$this->assertTrue($expected_node_result && $expected_comment_result, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that 'add new comment' does not appear in search results or index.
|
||||
*/
|
||||
function testAddNewComment() {
|
||||
// Create a node with a short body.
|
||||
$settings = array(
|
||||
'type' => 'article',
|
||||
'title' => 'short title',
|
||||
'body' => array(array('value' => 'short body text')),
|
||||
);
|
||||
|
||||
$user = $this->drupalCreateUser(array(
|
||||
'search content',
|
||||
'create article content',
|
||||
'access content',
|
||||
'post comments',
|
||||
'access comments',
|
||||
));
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$node = $this->drupalCreateNode($settings);
|
||||
// Verify that if you view the node on its own page, 'add new comment'
|
||||
// is there.
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
$this->assertText(t('Add new comment'));
|
||||
|
||||
// Run cron to index this page.
|
||||
$this->drupalLogout();
|
||||
$this->cronRun();
|
||||
|
||||
// Search for 'comment'. Should be no results.
|
||||
$this->drupalLogin($user);
|
||||
$this->drupalPostForm('search/node', array('keys' => 'comment'), t('Search'));
|
||||
$this->assertText(t('Your search yielded no results'));
|
||||
|
||||
// Search for the node title. Should be found, and 'Add new comment' should
|
||||
// not be part of the search snippet.
|
||||
$this->drupalPostForm('search/node', array('keys' => 'short'), t('Search'));
|
||||
$this->assertText($node->label(), 'Search for keyword worked');
|
||||
$this->assertNoText(t('Add new comment'));
|
||||
}
|
||||
}
|
378
core/modules/search/src/Tests/SearchConfigSettingsFormTest.php
Normal file
378
core/modules/search/src/Tests/SearchConfigSettingsFormTest.php
Normal file
|
@ -0,0 +1,378 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchConfigSettingsFormTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Verify the search config settings form.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchConfigSettingsFormTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('block', 'search_extra_type', 'test_page_test');
|
||||
|
||||
/**
|
||||
* User who can search and administer search.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $searchUser;
|
||||
|
||||
/**
|
||||
* Node indexed for searching.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $searchNode;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Login as a user that can create and search content.
|
||||
$this->searchUser = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks', 'access site reports'));
|
||||
$this->drupalLogin($this->searchUser);
|
||||
|
||||
// Add a single piece of content and index it.
|
||||
$node = $this->drupalCreateNode();
|
||||
$this->searchNode = $node;
|
||||
// Link the node to itself to test that it's only indexed once. The content
|
||||
// also needs the word "pizza" so we can use it as the search keyword.
|
||||
$body_key = 'body[0][value]';
|
||||
$edit[$body_key] = \Drupal::l($node->label(), $node->urlInfo()) . ' pizza sandwich';
|
||||
$this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
|
||||
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Enable the search block.
|
||||
$this->drupalPlaceBlock('search_form_block');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the search settings form.
|
||||
*/
|
||||
function testSearchSettingsPage() {
|
||||
|
||||
// Test that the settings form displays the correct count of items left to index.
|
||||
$this->drupalGet('admin/config/search/pages');
|
||||
$this->assertText(t('There are @count items left to index.', array('@count' => 0)));
|
||||
|
||||
// Test the re-index button.
|
||||
$this->drupalPostForm('admin/config/search/pages', array(), t('Re-index site'));
|
||||
$this->assertText(t('Are you sure you want to re-index the site'));
|
||||
$this->drupalPostForm('admin/config/search/pages/reindex', array(), t('Re-index site'));
|
||||
$this->assertText(t('All search indexes will be rebuilt'));
|
||||
$this->drupalGet('admin/config/search/pages');
|
||||
$this->assertText(t('There is 1 item left to index.'));
|
||||
|
||||
// Test that the form saves with the default values.
|
||||
$this->drupalPostForm('admin/config/search/pages', array(), t('Save configuration'));
|
||||
$this->assertText(t('The configuration options have been saved.'), 'Form saves with the default values.');
|
||||
|
||||
// Test that the form does not save with an invalid word length.
|
||||
$edit = array(
|
||||
'minimum_word_size' => $this->randomMachineName(3),
|
||||
);
|
||||
$this->drupalPostForm('admin/config/search/pages', $edit, t('Save configuration'));
|
||||
$this->assertNoText(t('The configuration options have been saved.'), 'Form does not save with an invalid word length.');
|
||||
|
||||
// Test logging setting. It should be off by default.
|
||||
$text = $this->randomMachineName(5);
|
||||
$this->drupalPostForm('search/node', array('keys' => $text), t('Search'));
|
||||
$this->drupalGet('admin/reports/dblog');
|
||||
$this->assertNoLink('Searched Content for ' . $text . '.', 'Search was not logged');
|
||||
|
||||
// Turn on logging.
|
||||
$edit = array('logging' => TRUE);
|
||||
$this->drupalPostForm('admin/config/search/pages', $edit, t('Save configuration'));
|
||||
$text = $this->randomMachineName(5);
|
||||
$this->drupalPostForm('search/node', array('keys' => $text), t('Search'));
|
||||
$this->drupalGet('admin/reports/dblog');
|
||||
$this->assertLink('Searched Content for ' . $text . '.', 0, 'Search was logged');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies plugin-supplied settings form.
|
||||
*/
|
||||
function testSearchModuleSettingsPage() {
|
||||
$this->drupalGet('admin/config/search/pages');
|
||||
$this->clickLink(t('Edit'), 1);
|
||||
|
||||
// Ensure that the default setting was picked up from the default config
|
||||
$this->assertTrue($this->xpath('//select[@id="edit-extra-type-settings-boost"]//option[@value="bi" and @selected="selected"]'), 'Module specific settings are picked up from the default config');
|
||||
|
||||
// Change extra type setting and also modify a common search setting.
|
||||
$edit = array(
|
||||
'extra_type_settings[boost]' => 'ii',
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save search page'));
|
||||
|
||||
// Ensure that the modifications took effect.
|
||||
$this->assertRaw(t('The %label search page has been updated.', array('%label' => 'Dummy search type')));
|
||||
$this->drupalGet('admin/config/search/pages/manage/dummy_search_type');
|
||||
$this->assertTrue($this->xpath('//select[@id="edit-extra-type-settings-boost"]//option[@value="ii" and @selected="selected"]'), 'Module specific settings can be changed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that you can disable individual search plugins.
|
||||
*/
|
||||
function testSearchModuleDisabling() {
|
||||
// Array of search plugins to test: 'keys' are the keywords to search for,
|
||||
// and 'text' is the text to assert is on the results page.
|
||||
$plugin_info = array(
|
||||
'node_search' => array(
|
||||
'keys' => 'pizza',
|
||||
'text' => $this->searchNode->label(),
|
||||
),
|
||||
'user_search' => array(
|
||||
'keys' => $this->searchUser->getUsername(),
|
||||
'text' => $this->searchUser->getEmail(),
|
||||
),
|
||||
'dummy_search_type' => array(
|
||||
'keys' => 'foo',
|
||||
'text' => 'Dummy search snippet to display',
|
||||
),
|
||||
);
|
||||
$plugins = array_keys($plugin_info);
|
||||
/** @var $entities \Drupal\search\SearchPageInterface[] */
|
||||
$entities = entity_load_multiple('search_page');
|
||||
// Disable all of the search pages.
|
||||
foreach ($entities as $entity) {
|
||||
$entity->disable()->save();
|
||||
}
|
||||
|
||||
// Test each plugin if it's enabled as the only search plugin.
|
||||
foreach ($entities as $entity_id => $entity) {
|
||||
// Set this as default.
|
||||
$this->drupalGet("admin/config/search/pages/manage/$entity_id/set-default");
|
||||
|
||||
// Run a search from the correct search URL.
|
||||
$info = $plugin_info[$entity_id];
|
||||
$this->drupalGet('search/' . $entity->getPath(), array('query' => array('keys' => $info['keys'])));
|
||||
$this->assertResponse(200);
|
||||
$this->assertNoText('no results', $entity->label() . ' search found results');
|
||||
$this->assertText($info['text'], 'Correct search text found');
|
||||
|
||||
// Verify that other plugin search tab labels are not visible.
|
||||
foreach ($plugins as $other) {
|
||||
if ($other != $entity_id) {
|
||||
$label = $entities[$other]->label();
|
||||
$this->assertNoText($label, $label . ' search tab is not shown');
|
||||
}
|
||||
}
|
||||
|
||||
// Run a search from the search block on the node page. Verify you get
|
||||
// to this plugin's search results page.
|
||||
$terms = array('keys' => $info['keys']);
|
||||
$this->submitGetForm('node', $terms, t('Search'));
|
||||
$current = $this->getURL();
|
||||
$expected = \Drupal::url('search.view_' . $entity->id(), array(), array('query' => array('keys' => $info['keys']), 'absolute' => TRUE));
|
||||
$this->assertEqual( $current, $expected, 'Block redirected to right search page');
|
||||
|
||||
// Try an invalid search path, which should 404.
|
||||
$this->drupalGet('search/not_a_plugin_path');
|
||||
$this->assertResponse(404);
|
||||
|
||||
$entity->disable()->save();
|
||||
}
|
||||
|
||||
// Test with all search plugins enabled. When you go to the search
|
||||
// page or run search, all plugins should be shown.
|
||||
foreach ($entities as $entity) {
|
||||
$entity->enable()->save();
|
||||
}
|
||||
// Set the node search as default.
|
||||
$this->drupalGet('admin/config/search/pages/manage/node_search/set-default');
|
||||
|
||||
$paths = array(
|
||||
array('path' => 'search/node', 'options' => array('query' => array('keys' => 'pizza'))),
|
||||
array('path' => 'search/node', 'options' => array()),
|
||||
);
|
||||
|
||||
foreach ($paths as $item) {
|
||||
$this->drupalGet($item['path'], $item['options']);
|
||||
foreach ($plugins as $entity_id) {
|
||||
$label = $entities[$entity_id]->label();
|
||||
$this->assertText($label, format_string('%label search tab is shown', array('%label' => $label)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the ordering of search pages on a clean install.
|
||||
*/
|
||||
public function testDefaultSearchPageOrdering() {
|
||||
$this->drupalGet('search');
|
||||
$elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary'));
|
||||
$this->assertIdentical((string) $elements[0]['href'], \Drupal::url('search.view_node_search'));
|
||||
$this->assertIdentical((string) $elements[1]['href'], \Drupal::url('search.view_dummy_search_type'));
|
||||
$this->assertIdentical((string) $elements[2]['href'], \Drupal::url('search.view_user_search'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests multiple search pages of the same type.
|
||||
*/
|
||||
public function testMultipleSearchPages() {
|
||||
$this->assertDefaultSearch('node_search', 'The default page is set to the installer default.');
|
||||
$search_storage = \Drupal::entityManager()->getStorage('search_page');
|
||||
$entities = $search_storage->loadMultiple();
|
||||
$search_storage->delete($entities);
|
||||
$this->assertDefaultSearch(FALSE);
|
||||
|
||||
// Ensure that no search pages are configured.
|
||||
$this->drupalGet('admin/config/search/pages');
|
||||
$this->assertText(t('No search pages have been configured.'));
|
||||
|
||||
// Add a search page.
|
||||
$edit = array();
|
||||
$edit['search_type'] = 'search_extra_type_search';
|
||||
$this->drupalPostForm(NULL, $edit, t('Add new page'));
|
||||
$this->assertTitle('Add new search page | Drupal');
|
||||
|
||||
$first = array();
|
||||
$first['label'] = $this->randomString();
|
||||
$first_id = $first['id'] = strtolower($this->randomMachineName(8));
|
||||
$first['path'] = strtolower($this->randomMachineName(8));
|
||||
$this->drupalPostForm(NULL, $first, t('Add search page'));
|
||||
$this->assertDefaultSearch($first_id, 'The default page matches the only search page.');
|
||||
$this->assertRaw(t('The %label search page has been added.', array('%label' => $first['label'])));
|
||||
|
||||
// Attempt to add a search page with an existing path.
|
||||
$edit = array();
|
||||
$edit['search_type'] = 'search_extra_type_search';
|
||||
$this->drupalPostForm(NULL, $edit, t('Add new page'));
|
||||
$edit = array();
|
||||
$edit['label'] = $this->randomString();
|
||||
$edit['id'] = strtolower($this->randomMachineName(8));
|
||||
$edit['path'] = $first['path'];
|
||||
$this->drupalPostForm(NULL, $edit, t('Add search page'));
|
||||
$this->assertText(t('The search page path must be unique.'));
|
||||
|
||||
// Add a second search page.
|
||||
$second = array();
|
||||
$second['label'] = $this->randomString();
|
||||
$second_id = $second['id'] = strtolower($this->randomMachineName(8));
|
||||
$second['path'] = strtolower($this->randomMachineName(8));
|
||||
$this->drupalPostForm(NULL, $second, t('Add search page'));
|
||||
$this->assertDefaultSearch($first_id, 'The default page matches the only search page.');
|
||||
|
||||
// Ensure both search pages have their tabs displayed.
|
||||
$this->drupalGet('search');
|
||||
$elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary'));
|
||||
$this->assertIdentical((string) $elements[0]['href'], Url::fromRoute('search.view_' . $first_id)->toString());
|
||||
$this->assertIdentical((string) $elements[1]['href'], Url::fromRoute('search.view_' . $second_id)->toString());
|
||||
|
||||
// Switch the weight of the search pages and check the order of the tabs.
|
||||
$edit = array(
|
||||
'entities[' . $first_id . '][weight]' => 10,
|
||||
'entities[' . $second_id . '][weight]' => -10,
|
||||
);
|
||||
$this->drupalPostForm('admin/config/search/pages', $edit, t('Save configuration'));
|
||||
$this->drupalGet('search');
|
||||
$elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary'));
|
||||
$this->assertIdentical((string) $elements[0]['href'], Url::fromRoute('search.view_' . $second_id)->toString());
|
||||
$this->assertIdentical((string) $elements[1]['href'], Url::fromRoute('search.view_' . $first_id)->toString());
|
||||
|
||||
// Check the initial state of the search pages.
|
||||
$this->drupalGet('admin/config/search/pages');
|
||||
$this->verifySearchPageOperations($first_id, TRUE, FALSE, FALSE, FALSE);
|
||||
$this->verifySearchPageOperations($second_id, TRUE, TRUE, TRUE, FALSE);
|
||||
|
||||
// Change the default search page.
|
||||
$this->clickLink(t('Set as default'));
|
||||
$this->assertRaw(t('The default search page is now %label. Be sure to check the ordering of your search pages.', array('%label' => $second['label'])));
|
||||
$this->verifySearchPageOperations($first_id, TRUE, TRUE, TRUE, FALSE);
|
||||
$this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
|
||||
|
||||
// Disable the first search page.
|
||||
$this->clickLink(t('Disable'));
|
||||
$this->assertResponse(200);
|
||||
$this->assertNoLink(t('Disable'));
|
||||
$this->verifySearchPageOperations($first_id, TRUE, TRUE, FALSE, TRUE);
|
||||
$this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
|
||||
|
||||
// Enable the first search page.
|
||||
$this->clickLink(t('Enable'));
|
||||
$this->assertResponse(200);
|
||||
$this->verifySearchPageOperations($first_id, TRUE, TRUE, TRUE, FALSE);
|
||||
$this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
|
||||
|
||||
// Test deleting.
|
||||
$this->clickLink(t('Delete'));
|
||||
$this->assertRaw(t('Are you sure you want to delete the search page %label?', array('%label' => $first['label'])));
|
||||
$this->drupalPostForm(NULL, array(), t('Delete'));
|
||||
$this->assertRaw(t('The search page %label has been deleted.', array('%label' => $first['label'])));
|
||||
$this->verifySearchPageOperations($first_id, FALSE, FALSE, FALSE, FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the search page operations match expectations.
|
||||
*
|
||||
* @param string $id
|
||||
* The search page ID to check.
|
||||
* @param bool $edit
|
||||
* Whether the edit link is expected.
|
||||
* @param bool $delete
|
||||
* Whether the delete link is expected.
|
||||
* @param bool $disable
|
||||
* Whether the disable link is expected.
|
||||
* @param bool $enable
|
||||
* Whether the enable link is expected.
|
||||
*/
|
||||
protected function verifySearchPageOperations($id, $edit, $delete, $disable, $enable) {
|
||||
if ($edit) {
|
||||
$this->assertLinkByHref("admin/config/search/pages/manage/$id");
|
||||
}
|
||||
else {
|
||||
$this->assertNoLinkByHref("admin/config/search/pages/manage/$id");
|
||||
}
|
||||
if ($delete) {
|
||||
$this->assertLinkByHref("admin/config/search/pages/manage/$id/delete");
|
||||
}
|
||||
else {
|
||||
$this->assertNoLinkByHref("admin/config/search/pages/manage/$id/delete");
|
||||
}
|
||||
if ($disable) {
|
||||
$this->assertLinkByHref("admin/config/search/pages/manage/$id/disable");
|
||||
}
|
||||
else {
|
||||
$this->assertNoLinkByHref("admin/config/search/pages/manage/$id/disable");
|
||||
}
|
||||
if ($enable) {
|
||||
$this->assertLinkByHref("admin/config/search/pages/manage/$id/enable");
|
||||
}
|
||||
else {
|
||||
$this->assertNoLinkByHref("admin/config/search/pages/manage/$id/enable");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the default search page matches expectations.
|
||||
*
|
||||
* @param string $expected
|
||||
* The expected search page.
|
||||
* @param string $message
|
||||
* (optional) A message to display with the assertion.
|
||||
* @param string $group
|
||||
* (optional) The group this message is in.
|
||||
*/
|
||||
protected function assertDefaultSearch($expected, $message = '', $group = 'Other') {
|
||||
/** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
|
||||
$search_page_repository = \Drupal::service('search.search_page_repository');
|
||||
$this->assertIdentical($search_page_repository->getDefaultSearchPage(), $expected, $message, $group);
|
||||
}
|
||||
|
||||
}
|
89
core/modules/search/src/Tests/SearchEmbedFormTest.php
Normal file
89
core/modules/search/src/Tests/SearchEmbedFormTest.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchEmbedFormTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Verifies that a form embedded in search results works.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchEmbedFormTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('search_embedded_form');
|
||||
|
||||
/**
|
||||
* Node used for testing.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
/**
|
||||
* Count of how many times the form has been submitted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $submitCount = 0;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a user and a node, and update the search index.
|
||||
$test_user = $this->drupalCreateUser(array('access content', 'search content', 'administer nodes'));
|
||||
$this->drupalLogin($test_user);
|
||||
|
||||
$this->node = $this->drupalCreateNode();
|
||||
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Set up a dummy initial count of times the form has been submitted.
|
||||
$this->submitCount = \Drupal::state()->get('search_embedded_form.submit_count');
|
||||
$this->refreshVariables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the embedded form appears and can be submitted.
|
||||
*/
|
||||
function testEmbeddedForm() {
|
||||
// First verify we can submit the form from the module's page.
|
||||
$this->drupalPostForm('search_embedded_form',
|
||||
array('name' => 'John'),
|
||||
t('Send away'));
|
||||
$this->assertText(t('Test form was submitted'), 'Form message appears');
|
||||
$count = \Drupal::state()->get('search_embedded_form.submit_count');
|
||||
$this->assertEqual($this->submitCount + 1, $count, 'Form submission count is correct');
|
||||
$this->submitCount = $count;
|
||||
|
||||
// Now verify that we can see and submit the form from the search results.
|
||||
$this->drupalGet('search/node', array('query' => array('keys' => $this->node->label())));
|
||||
$this->assertText(t('Your name'), 'Form is visible');
|
||||
$this->drupalPostForm(NULL,
|
||||
array('name' => 'John'),
|
||||
t('Send away'));
|
||||
$this->assertText(t('Test form was submitted'), 'Form message appears');
|
||||
$count = \Drupal::state()->get('search_embedded_form.submit_count');
|
||||
$this->assertEqual($this->submitCount + 1, $count, 'Form submission count is correct');
|
||||
$this->submitCount = $count;
|
||||
|
||||
// Now verify that if we submit the search form, it doesn't count as
|
||||
// our form being submitted.
|
||||
$this->drupalPostForm('search',
|
||||
array('keys' => 'foo'),
|
||||
t('Search'));
|
||||
$this->assertNoText(t('Test form was submitted'), 'Form message does not appear');
|
||||
$count = \Drupal::state()->get('search_embedded_form.submit_count');
|
||||
$this->assertEqual($this->submitCount, $count, 'Form submission count is correct');
|
||||
$this->submitCount = $count;
|
||||
}
|
||||
}
|
83
core/modules/search/src/Tests/SearchExactTest.php
Normal file
83
core/modules/search/src/Tests/SearchExactTest.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchExactTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests that searching for a phrase gets the correct page count.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchExactTest extends SearchTestBase {
|
||||
/**
|
||||
* Tests that the correct number of pager links are found for both keywords and phrases.
|
||||
*/
|
||||
function testExactQuery() {
|
||||
// Login with sufficient privileges.
|
||||
$user = $this->drupalCreateUser(array('create page content', 'search content'));
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$settings = array(
|
||||
'type' => 'page',
|
||||
'title' => 'Simple Node',
|
||||
);
|
||||
// Create nodes with exact phrase.
|
||||
for ($i = 0; $i <= 17; $i++) {
|
||||
$settings['body'] = array(array('value' => 'love pizza'));
|
||||
$this->drupalCreateNode($settings);
|
||||
}
|
||||
// Create nodes containing keywords.
|
||||
for ($i = 0; $i <= 17; $i++) {
|
||||
$settings['body'] = array(array('value' => 'love cheesy pizza'));
|
||||
$this->drupalCreateNode($settings);
|
||||
}
|
||||
// Create another node and save it for later.
|
||||
$settings['body'] = array(array('value' => 'Druplicon'));
|
||||
$node = $this->drupalCreateNode($settings);
|
||||
|
||||
// Update the search index.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Refresh variables after the treatment.
|
||||
$this->refreshVariables();
|
||||
|
||||
// Test that the correct number of pager links are found for keyword search.
|
||||
$edit = array('keys' => 'love pizza');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.');
|
||||
$this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.');
|
||||
$this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.');
|
||||
$this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.');
|
||||
|
||||
// Test that the correct number of pager links are found for exact phrase search.
|
||||
$edit = array('keys' => '"love pizza"');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.');
|
||||
$this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.');
|
||||
|
||||
// Check that with post settings turned on the post information is displayed.
|
||||
$node_type_config = \Drupal::configFactory()->getEditable('node.type.page');
|
||||
$node_type_config->set('display_submitted', TRUE);
|
||||
$node_type_config->save();
|
||||
|
||||
$edit = array('keys' => 'Druplicon');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText($user->getUsername(), 'Basic page node displays author name when post settings are on.');
|
||||
$this->assertText(format_date($node->getChangedTime(), 'short'), 'Basic page node displays post date when post settings are on.');
|
||||
|
||||
// Check that with post settings turned off the user and changed date
|
||||
// information is not displayed.
|
||||
$node_type_config->set('display_submitted', FALSE);
|
||||
$node_type_config->save();
|
||||
$edit = array('keys' => 'Druplicon');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoText($user->getUsername(), 'Basic page node does not display author name when post settings are off.');
|
||||
$this->assertNoText(format_date($node->getChangedTime(), 'short'), 'Basic page node does not display post date when post settings are off.');
|
||||
|
||||
}
|
||||
}
|
163
core/modules/search/src/Tests/SearchExcerptTest.php
Normal file
163
core/modules/search/src/Tests/SearchExcerptTest.php
Normal file
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchExcerptTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests the search_excerpt() function.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchExcerptTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('search', 'search_langcode_test');
|
||||
|
||||
/**
|
||||
* Tests search_excerpt() with several simulated search keywords.
|
||||
*
|
||||
* Passes keywords and a sample marked up string, "The quick
|
||||
* brown fox jumps over the lazy dog", and compares it to the
|
||||
* correctly marked up string. The correctly marked up string
|
||||
* contains either highlighted keywords or the original marked
|
||||
* up string if no keywords matched the string.
|
||||
*/
|
||||
function testSearchExcerpt() {
|
||||
// Make some text with entities and tags.
|
||||
$text = 'The <strong>quick</strong> <a href="#">brown</a> fox & jumps <h2>over</h2> the lazy dog';
|
||||
// Note: The search_excerpt() function adds some extra spaces -- not
|
||||
// important for HTML formatting. Remove these for comparison.
|
||||
$expected = 'The quick brown fox & jumps over the lazy dog';
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('nothing', $text));
|
||||
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string is returned when keyword is not found in short string');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('fox', $text));
|
||||
$this->assertEqual($result, 'The quick brown <strong>fox</strong> & jumps over the lazy dog', 'Found keyword is highlighted');
|
||||
|
||||
$expected = '<strong>The</strong> quick brown fox & jumps over <strong>the</strong> lazy dog';
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('The', $text));
|
||||
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Keyword is highlighted at beginning of short string');
|
||||
|
||||
$expected = 'The quick brown fox & jumps over the lazy <strong>dog</strong>';
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('dog', $text));
|
||||
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Keyword is highlighted at end of short string');
|
||||
|
||||
$longtext = str_repeat(str_replace('brown', 'silver', $text) . ' ', 10) . $text . str_repeat(' ' . str_replace('brown', 'pink', $text), 10);
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('brown', $longtext));
|
||||
$expected = '… silver fox & jumps over the lazy dog The quick <strong>brown</strong> fox & jumps over the lazy dog The quick …';
|
||||
$this->assertEqual($result, $expected, 'Snippet around keyword in long text is correctly capped');
|
||||
|
||||
$longtext = str_repeat($text . ' ', 10);
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('nothing', $longtext));
|
||||
$expected = 'The quick brown fox & jumps over the lazy dog';
|
||||
$this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected');
|
||||
|
||||
$entities = str_repeat('készítése ', 20);
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities));
|
||||
$this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt');
|
||||
$this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt');
|
||||
|
||||
// The node body that will produce this rendered $text is:
|
||||
// 123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678 +‘ +‘ +‘ ‘
|
||||
$text = "<div class=\"field field-name-body field-type-text-with-summary field-label-hidden\"><div class=\"field-items\"><div class=\"field-item even\" property=\"content:encoded\"><p>123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678 +‘ +‘ +‘ ‘</p>\n</div></div></div> ";
|
||||
$result = search_excerpt('HTMLTest', $text);
|
||||
$this->assertFalse(empty($result), 'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests search_excerpt() with search keywords matching simplified words.
|
||||
*
|
||||
* Excerpting should handle keywords that are matched only after going through
|
||||
* search_simplify(). This test passes keywords that match simplified words
|
||||
* and compares them with strings that contain the original unsimplified word.
|
||||
*/
|
||||
function testSearchExcerptSimplified() {
|
||||
$lorem1 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero.';
|
||||
$lorem2 = 'Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci.';
|
||||
|
||||
// Make some text with some keywords that will get simplified.
|
||||
$text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2;
|
||||
// Note: The search_excerpt() function adds some extra spaces -- not
|
||||
// important for HTML formatting. Remove these for comparison.
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('123456.7890', $text));
|
||||
$this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with exact match');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('1234567890', $text));
|
||||
$this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with simplified match');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('Number 1234567890', $text));
|
||||
$this->assertTrue(strpos($result, '<strong>Number</strong>: <strong>123456.7890</strong>') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('"Number 1234567890"', $text));
|
||||
$this->assertTrue(strpos($result, '<strong>Number: 123456.7890</strong>') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('"Hyphenated onetwo"', $text));
|
||||
$this->assertTrue(strpos($result, '<strong>Hyphenated: one-two</strong>') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('"abc def"', $text));
|
||||
$this->assertTrue(strpos($result, '<strong>abc,def</strong>') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match');
|
||||
|
||||
// Test phrases with characters which are being truncated.
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('"ipsum _"', $text));
|
||||
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part containing "_" is ignored.');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('"ipsum 0000"', $text));
|
||||
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part "0000" is ignored.');
|
||||
|
||||
// Test combination of the valid keyword and keyword containing only
|
||||
// characters which are being truncated during simplification.
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('ipsum _', $text));
|
||||
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "_" is ignored.');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('ipsum 0000', $text));
|
||||
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "0000" is ignored.');
|
||||
|
||||
// Test using the hook_search_preprocess() from the test module.
|
||||
// The hook replaces "finding" or "finds" with "find".
|
||||
// So, if we search for "find" or "finds" or "finding", we should
|
||||
// highlight "finding".
|
||||
$text = "this tests finding a string";
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('finds', $text, 'ex'));
|
||||
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, search for finds');
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('find', $text, 'ex'));
|
||||
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, search for find');
|
||||
|
||||
// Just to be sure, test with the replacement at the beginning and end.
|
||||
$text = "finding at the beginning";
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('finds', $text, 'ex'));
|
||||
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, text at start');
|
||||
|
||||
$text = "at the end finding";
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('finds', $text, 'ex'));
|
||||
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, text at end');
|
||||
|
||||
// Testing with a one-to-many replacement: the test module replaces DIC
|
||||
// with Dependency Injection Container.
|
||||
$text = "something about the DIC is happening";
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('Dependency', $text, 'ex'));
|
||||
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym first word');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('Injection', $text, 'ex'));
|
||||
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym second word');
|
||||
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('Container', $text, 'ex'));
|
||||
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym third word');
|
||||
|
||||
// Testing with a many-to-one replacement: the test module replaces
|
||||
// hypertext markup language with HTML.
|
||||
$text = "we always use hypertext markup language to describe things";
|
||||
$result = preg_replace('| +|', ' ', search_excerpt('html', $text, 'ex'));
|
||||
$this->assertTrue(strpos($result, '<strong>hypertext markup language</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym many to one');
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchKeywordsConditionsTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Verify the search without keywords set and extra conditions.
|
||||
*
|
||||
* Verifies that a plugin can override the isSearchExecutable() method to allow
|
||||
* searching without keywords set and that GET query parameters are made
|
||||
* available to plugins during search execution.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchKeywordsConditionsTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('comment', 'search_extra_type', 'test_page_test');
|
||||
|
||||
/**
|
||||
* A user with permission to search and post comments.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $searchingUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create searching user.
|
||||
$this->searchingUser = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval'));
|
||||
// Login with sufficient privileges.
|
||||
$this->drupalLogin($this->searchingUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the keywords are captured and conditions respected.
|
||||
*/
|
||||
function testSearchKeywordsConditions() {
|
||||
// No keys, not conditions - no results.
|
||||
$this->drupalGet('search/dummy_path');
|
||||
$this->assertNoText('Dummy search snippet to display');
|
||||
// With keys - get results.
|
||||
$keys = 'bike shed ' . $this->randomMachineName();
|
||||
$this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys)));
|
||||
$this->assertText("Dummy search snippet to display. Keywords: {$keys}");
|
||||
$keys = 'blue drop ' . $this->randomMachineName();
|
||||
$this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys)));
|
||||
$this->assertText("Dummy search snippet to display. Keywords: {$keys}");
|
||||
// Add some conditions and keys.
|
||||
$keys = 'moving drop ' . $this->randomMachineName();
|
||||
$this->drupalGet("search/dummy_path", array('query' => array('keys' => 'bike', 'search_conditions' => $keys)));
|
||||
$this->assertText("Dummy search snippet to display.");
|
||||
$this->assertRaw(print_r(array('keys' => 'bike', 'search_conditions' => $keys), TRUE));
|
||||
}
|
||||
}
|
141
core/modules/search/src/Tests/SearchLanguageTest.php
Normal file
141
core/modules/search/src/Tests/SearchLanguageTest.php
Normal file
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchLanguageTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests advanced search with different languages added.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchLanguageTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('language');
|
||||
|
||||
/**
|
||||
* Array of nodes available to search.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $searchableNodes;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create and login user.
|
||||
$test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages', 'administer site configuration'));
|
||||
$this->drupalLogin($test_user);
|
||||
|
||||
// Add a new language.
|
||||
ConfigurableLanguage::createFromLangcode('es')->save();
|
||||
|
||||
// Make the body field translatable. The title is already translatable by
|
||||
// definition. The parent class has already created the article and page
|
||||
// content types.
|
||||
$field_storage = FieldStorageConfig::loadByName('node', 'body');
|
||||
$field_storage->setTranslatable(TRUE);
|
||||
$field_storage->save();
|
||||
|
||||
// Create a few page nodes with multilingual body values.
|
||||
$default_format = filter_default_format();
|
||||
$nodes = array(
|
||||
array(
|
||||
'title' => 'First node en',
|
||||
'type' => 'page',
|
||||
'body' => array(array('value' => $this->randomMachineName(32), 'format' => $default_format)),
|
||||
'langcode' => 'en',
|
||||
),
|
||||
array(
|
||||
'title' => 'Second node this is the Spanish title',
|
||||
'type' => 'page',
|
||||
'body' => array(array('value' => $this->randomMachineName(32), 'format' => $default_format)),
|
||||
'langcode' => 'es',
|
||||
),
|
||||
array(
|
||||
'title' => 'Third node en',
|
||||
'type' => 'page',
|
||||
'body' => array(array('value' => $this->randomMachineName(32), 'format' => $default_format)),
|
||||
'langcode' => 'en',
|
||||
),
|
||||
);
|
||||
$this->searchableNodes = [];
|
||||
foreach ($nodes as $setting) {
|
||||
$this->searchableNodes[] = $this->drupalCreateNode($setting);
|
||||
}
|
||||
|
||||
// Add English translation to the second node.
|
||||
$translation = $this->searchableNodes[1]->addTranslation('en', array('title' => 'Second node en'));
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$this->searchableNodes[1]->save();
|
||||
|
||||
// Add Spanish translation to the third node.
|
||||
$translation = $this->searchableNodes[2]->addTranslation('es', array('title' => 'Third node es'));
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$this->searchableNodes[2]->save();
|
||||
|
||||
// Update the index and then run the shutdown method.
|
||||
$plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
$plugin->updateIndex();
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
function testLanguages() {
|
||||
// Add predefined language.
|
||||
$edit = array('predefined_langcode' => 'fr');
|
||||
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
|
||||
$this->assertText('French', 'Language added successfully.');
|
||||
|
||||
// Now we should have languages displayed.
|
||||
$this->drupalGet('search/node');
|
||||
$this->assertText(t('Languages'), 'Languages displayed to choose from.');
|
||||
$this->assertText(t('English'), 'English is a possible choice.');
|
||||
$this->assertText(t('French'), 'French is a possible choice.');
|
||||
|
||||
// Ensure selecting no language does not make the query different.
|
||||
$this->drupalPostForm('search/node', array(), t('Advanced search'));
|
||||
$this->assertUrl(\Drupal::url('search.view_node_search', [], ['query' => ['keys' => ''], 'absolute' => TRUE]), [], 'Correct page redirection, no language filtering.');
|
||||
|
||||
// Pick French and ensure it is selected.
|
||||
$edit = array('language[fr]' => TRUE);
|
||||
$this->drupalPostForm('search/node', $edit, t('Advanced search'));
|
||||
// Get the redirected URL.
|
||||
$url = $this->getUrl();
|
||||
$parts = parse_url($url);
|
||||
$query_string = isset($parts['query']) ? rawurldecode($parts['query']) : '';
|
||||
$this->assertTrue(strpos($query_string, '=language:fr') !== FALSE, 'Language filter language:fr add to the query string.');
|
||||
|
||||
// Search for keyword node and language filter as Spanish.
|
||||
$edit = array('keys' => 'node', 'language[es]' => TRUE);
|
||||
$this->drupalPostForm('search/node', $edit, t('Advanced search'));
|
||||
// Check for Spanish results.
|
||||
$this->assertLink('Second node this is the Spanish title', 0, 'Second node Spanish title found in search results');
|
||||
$this->assertLink('Third node es', 0, 'Third node Spanish found in search results');
|
||||
// Ensure that results doesn't contain other language nodes.
|
||||
$this->assertNoLink('First node en', 'Search results does not contain first English node');
|
||||
$this->assertNoLink('Second node en', 'Search results does not contain second English node');
|
||||
$this->assertNoLink('Third node en', 'Search results does not contain third English node');
|
||||
|
||||
// Change the default language and delete English.
|
||||
$path = 'admin/config/regional/language';
|
||||
$this->drupalGet($path);
|
||||
$this->assertFieldChecked('edit-site-default-language-en', 'Default language updated.');
|
||||
$edit = array(
|
||||
'site_default_language' => 'fr',
|
||||
);
|
||||
$this->drupalPostForm($path, $edit, t('Save configuration'));
|
||||
$this->assertNoFieldChecked('edit-site-default-language-en', 'Default language updated.');
|
||||
$this->drupalPostForm('admin/config/regional/language/delete/en', array(), t('Delete'));
|
||||
}
|
||||
}
|
241
core/modules/search/src/Tests/SearchMatchTest.php
Normal file
241
core/modules/search/src/Tests/SearchMatchTest.php
Normal file
|
@ -0,0 +1,241 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchMatchTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
// The search index can contain different types of content. Typically the type
|
||||
// is 'node'. Here we test with _test_ and _test2_ as the type.
|
||||
const SEARCH_TYPE = '_test_';
|
||||
const SEARCH_TYPE_2 = '_test2_';
|
||||
const SEARCH_TYPE_JPN = '_test3_';
|
||||
|
||||
/**
|
||||
* Indexes content and queries it.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchMatchTest extends SearchTestBase {
|
||||
/**
|
||||
* Test search indexing.
|
||||
*/
|
||||
function testMatching() {
|
||||
$this->_setup();
|
||||
$this->_testQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a small index of items to test against.
|
||||
*/
|
||||
function _setup() {
|
||||
$this->config('search.settings')->set('index.minimum_word_size', 3)->save();
|
||||
|
||||
for ($i = 1; $i <= 7; ++$i) {
|
||||
search_index(SEARCH_TYPE, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText($i));
|
||||
}
|
||||
for ($i = 1; $i <= 5; ++$i) {
|
||||
search_index(SEARCH_TYPE_2, $i + 7, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText2($i));
|
||||
}
|
||||
// No getText builder function for Japanese text; just a simple array.
|
||||
foreach (array(
|
||||
13 => '以呂波耳・ほへとち。リヌルヲ。',
|
||||
14 => 'ドルーパルが大好きよ!',
|
||||
15 => 'コーヒーとケーキ',
|
||||
) as $i => $jpn) {
|
||||
search_index(SEARCH_TYPE_JPN, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $jpn);
|
||||
}
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* _test_: Helper method for generating snippets of content.
|
||||
*
|
||||
* Generated items to test against:
|
||||
* 1 ipsum
|
||||
* 2 dolore sit
|
||||
* 3 sit am ut
|
||||
* 4 am ut enim am
|
||||
* 5 ut enim am minim veniam
|
||||
* 6 enim am minim veniam es cillum
|
||||
* 7 am minim veniam es cillum dolore eu
|
||||
*/
|
||||
function getText($n) {
|
||||
$words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu.");
|
||||
return implode(' ', array_slice($words, $n - 1, $n));
|
||||
}
|
||||
|
||||
/**
|
||||
* _test2_: Helper method for generating snippets of content.
|
||||
*
|
||||
* Generated items to test against:
|
||||
* 8 dear
|
||||
* 9 king philip
|
||||
* 10 philip came over
|
||||
* 11 came over from germany
|
||||
* 12 over from germany swimming
|
||||
*/
|
||||
function getText2($n) {
|
||||
$words = explode(' ', "Dear King Philip came over from Germany swimming.");
|
||||
return implode(' ', array_slice($words, $n - 1, $n));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run predefine queries looking for indexed terms.
|
||||
*/
|
||||
function _testQueries() {
|
||||
// Note: OR queries that include short words in OR groups are only accepted
|
||||
// if the ORed terms are ANDed with at least one long word in the rest of
|
||||
// the query. Examples:
|
||||
// enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut)
|
||||
// is good, and
|
||||
// dolore OR ut = (dolore) OR (ut)
|
||||
// is bad. This is a design limitation to avoid full table scans.
|
||||
$queries = array(
|
||||
// Simple AND queries.
|
||||
'ipsum' => array(1),
|
||||
'enim' => array(4, 5, 6),
|
||||
'xxxxx' => array(),
|
||||
'enim minim' => array(5, 6),
|
||||
'enim xxxxx' => array(),
|
||||
'dolore eu' => array(7),
|
||||
'dolore xx' => array(),
|
||||
'ut minim' => array(5),
|
||||
'xx minim' => array(),
|
||||
'enim veniam am minim ut' => array(5),
|
||||
// Simple OR and AND/OR queries.
|
||||
'dolore OR ipsum' => array(1, 2, 7),
|
||||
'dolore OR xxxxx' => array(2, 7),
|
||||
'dolore OR ipsum OR enim' => array(1, 2, 4, 5, 6, 7),
|
||||
'ipsum OR dolore sit OR cillum' => array(2, 7),
|
||||
'minim dolore OR ipsum' => array(7),
|
||||
'dolore OR ipsum veniam' => array(7),
|
||||
'minim dolore OR ipsum OR enim' => array(5, 6, 7),
|
||||
'dolore xx OR yy' => array(),
|
||||
'xxxxx dolore OR ipsum' => array(),
|
||||
// Sequence of OR queries.
|
||||
'minim' => array(5, 6, 7),
|
||||
'minim OR xxxx' => array(5, 6, 7),
|
||||
'minim OR xxxx OR minim' => array(5, 6, 7),
|
||||
'minim OR xxxx minim' => array(5, 6, 7),
|
||||
'minim OR xxxx minim OR yyyy' => array(5, 6, 7),
|
||||
'minim OR xxxx minim OR cillum' => array(6, 7, 5),
|
||||
'minim OR xxxx minim OR xxxx' => array(5, 6, 7),
|
||||
// Negative queries.
|
||||
'dolore -sit' => array(7),
|
||||
'dolore -eu' => array(2),
|
||||
'dolore -xxxxx' => array(2, 7),
|
||||
'dolore -xx' => array(2, 7),
|
||||
// Phrase queries.
|
||||
'"dolore sit"' => array(2),
|
||||
'"sit dolore"' => array(),
|
||||
'"am minim veniam es"' => array(6, 7),
|
||||
'"minim am veniam es"' => array(),
|
||||
// Mixed queries.
|
||||
'"am minim veniam es" OR dolore' => array(2, 6, 7),
|
||||
'"minim am veniam es" OR "dolore sit"' => array(2),
|
||||
'"minim am veniam es" OR "sit dolore"' => array(),
|
||||
'"am minim veniam es" -eu' => array(6),
|
||||
'"am minim veniam" -"cillum dolore"' => array(5, 6),
|
||||
'"am minim veniam" -"dolore cillum"' => array(5, 6, 7),
|
||||
'xxxxx "minim am veniam es" OR dolore' => array(),
|
||||
'xx "minim am veniam es" OR dolore' => array()
|
||||
);
|
||||
foreach ($queries as $query => $results) {
|
||||
$result = db_select('search_index', 'i')
|
||||
->extend('Drupal\search\SearchQuery')
|
||||
->searchExpression($query, SEARCH_TYPE)
|
||||
->execute();
|
||||
|
||||
$set = $result ? $result->fetchAll() : array();
|
||||
$this->_testQueryMatching($query, $set, $results);
|
||||
$this->_testQueryScores($query, $set, $results);
|
||||
}
|
||||
|
||||
// These queries are run against the second index type, SEARCH_TYPE_2.
|
||||
$queries = array(
|
||||
// Simple AND queries.
|
||||
'ipsum' => array(),
|
||||
'enim' => array(),
|
||||
'enim minim' => array(),
|
||||
'dear' => array(8),
|
||||
'germany' => array(11, 12),
|
||||
);
|
||||
foreach ($queries as $query => $results) {
|
||||
$result = db_select('search_index', 'i')
|
||||
->extend('Drupal\search\SearchQuery')
|
||||
->searchExpression($query, SEARCH_TYPE_2)
|
||||
->execute();
|
||||
|
||||
$set = $result ? $result->fetchAll() : array();
|
||||
$this->_testQueryMatching($query, $set, $results);
|
||||
$this->_testQueryScores($query, $set, $results);
|
||||
}
|
||||
|
||||
// These queries are run against the third index type, SEARCH_TYPE_JPN.
|
||||
$queries = array(
|
||||
// Simple AND queries.
|
||||
'呂波耳' => array(13),
|
||||
'以呂波耳' => array(13),
|
||||
'ほへと ヌルヲ' => array(13),
|
||||
'とちリ' => array(),
|
||||
'ドルーパル' => array(14),
|
||||
'パルが大' => array(14),
|
||||
'コーヒー' => array(15),
|
||||
'ヒーキ' => array(),
|
||||
);
|
||||
foreach ($queries as $query => $results) {
|
||||
$result = db_select('search_index', 'i')
|
||||
->extend('Drupal\search\SearchQuery')
|
||||
->searchExpression($query, SEARCH_TYPE_JPN)
|
||||
->execute();
|
||||
|
||||
$set = $result ? $result->fetchAll() : array();
|
||||
$this->_testQueryMatching($query, $set, $results);
|
||||
$this->_testQueryScores($query, $set, $results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the matching abilities of the engine.
|
||||
*
|
||||
* Verify if a query produces the correct results.
|
||||
*/
|
||||
function _testQueryMatching($query, $set, $results) {
|
||||
// Get result IDs.
|
||||
$found = array();
|
||||
foreach ($set as $item) {
|
||||
$found[] = $item->sid;
|
||||
}
|
||||
|
||||
// Compare $results and $found.
|
||||
sort($found);
|
||||
sort($results);
|
||||
$this->assertEqual($found, $results, "Query matching '$query'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the scoring abilities of the engine.
|
||||
*
|
||||
* Verify if a query produces normalized, monotonous scores.
|
||||
*/
|
||||
function _testQueryScores($query, $set, $results) {
|
||||
// Get result scores.
|
||||
$scores = array();
|
||||
foreach ($set as $item) {
|
||||
$scores[] = $item->calculated_score;
|
||||
}
|
||||
|
||||
// Check order.
|
||||
$sorted = $scores;
|
||||
sort($sorted);
|
||||
$this->assertEqual($scores, array_reverse($sorted), "Query order '$query'");
|
||||
|
||||
// Check range.
|
||||
$this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'");
|
||||
}
|
||||
}
|
329
core/modules/search/src/Tests/SearchMultilingualEntityTest.php
Normal file
329
core/modules/search/src/Tests/SearchMultilingualEntityTest.php
Normal file
|
@ -0,0 +1,329 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchMultilingualEntityTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests entities with multilingual fields.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchMultilingualEntityTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* List of searchable nodes.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $searchableNodes = array();
|
||||
|
||||
/**
|
||||
* Node search plugin.
|
||||
*
|
||||
* @var \Drupal\node\Plugin\Search\NodeSearch
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
public static $modules = array('language', 'locale', 'comment');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a user who can administer search, do searches, see the status
|
||||
// report, and administer cron. Log in.
|
||||
$user = $this->drupalCreateUser(array('administer search', 'search content', 'use advanced search', 'access content', 'access site reports', 'administer site configuration'));
|
||||
$this->drupalLogin($user);
|
||||
|
||||
// Make sure that auto-cron is disabled.
|
||||
$this->config('system.cron')->set('threshold.autorun', 0)->save();
|
||||
|
||||
// Set up the search plugin.
|
||||
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
|
||||
// Check indexing counts before adding any nodes.
|
||||
$this->assertIndexCounts(0, 0, 'before adding nodes');
|
||||
$this->assertDatabaseCounts(0, 0, 'before adding nodes');
|
||||
|
||||
// Add two new languages.
|
||||
ConfigurableLanguage::createFromLangcode('hu')->save();
|
||||
ConfigurableLanguage::createFromLangcode('sv')->save();
|
||||
|
||||
// Make the body field translatable. The title is already translatable by
|
||||
// definition. The parent class has already created the article and page
|
||||
// content types.
|
||||
$field_storage = FieldStorageConfig::loadByName('node', 'body');
|
||||
$field_storage->setTranslatable(TRUE);
|
||||
$field_storage->save();
|
||||
|
||||
// Create a few page nodes with multilingual body values.
|
||||
$default_format = filter_default_format();
|
||||
$nodes = array(
|
||||
array(
|
||||
'title' => 'First node en',
|
||||
'type' => 'page',
|
||||
'body' => array(array('value' => $this->randomMachineName(32), 'format' => $default_format)),
|
||||
'langcode' => 'en',
|
||||
),
|
||||
array(
|
||||
'title' => 'Second node this is the English title',
|
||||
'type' => 'page',
|
||||
'body' => array(array('value' => $this->randomMachineName(32), 'format' => $default_format)),
|
||||
'langcode' => 'en',
|
||||
),
|
||||
array(
|
||||
'title' => 'Third node en',
|
||||
'type' => 'page',
|
||||
'body' => array(array('value' => $this->randomMachineName(32), 'format' => $default_format)),
|
||||
'langcode' => 'en',
|
||||
),
|
||||
// After the third node, we don't care what the settings are. But we
|
||||
// need to have at least 5 to make sure the throttling is working
|
||||
// correctly. So, let's make 8 total.
|
||||
array(
|
||||
),
|
||||
array(
|
||||
),
|
||||
array(
|
||||
),
|
||||
array(
|
||||
),
|
||||
array(
|
||||
),
|
||||
);
|
||||
$this->searchableNodes = array();
|
||||
foreach ($nodes as $setting) {
|
||||
$this->searchableNodes[] = $this->drupalCreateNode($setting);
|
||||
}
|
||||
|
||||
// Add a single translation to the second node.
|
||||
$translation = $this->searchableNodes[1]->addTranslation('hu', array('title' => 'Second node hu'));
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$this->searchableNodes[1]->save();
|
||||
|
||||
// Add two translations to the third node.
|
||||
$translation = $this->searchableNodes[2]->addTranslation('hu', array('title' => 'Third node this is the Hungarian title'));
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$translation = $this->searchableNodes[2]->addTranslation('sv', array('title' => 'Third node sv'));
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$this->searchableNodes[2]->save();
|
||||
|
||||
// Verify that we have 8 nodes left to do.
|
||||
$this->assertIndexCounts(8, 8, 'before updating the search index');
|
||||
$this->assertDatabaseCounts(0, 0, 'before updating the search index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the indexing throttle and search results with multilingual nodes.
|
||||
*/
|
||||
function testMultilingualSearch() {
|
||||
// Index only 2 nodes per cron run. We cannot do this setting in the UI,
|
||||
// because it doesn't go this low.
|
||||
$this->config('search.settings')->set('index.cron_limit', 2)->save();
|
||||
// Get a new search plugin, to make sure it has this setting.
|
||||
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
|
||||
// Update the index. This does the initial processing.
|
||||
$this->plugin->updateIndex();
|
||||
// Run the shutdown function. Testing is a unique case where indexing
|
||||
// and searching has to happen in the same request, so running the shutdown
|
||||
// function manually is needed to finish the indexing process.
|
||||
search_update_totals();
|
||||
$this->assertIndexCounts(6, 8, 'after updating partially');
|
||||
$this->assertDatabaseCounts(2, 0, 'after updating partially');
|
||||
|
||||
// Now index the rest of the nodes.
|
||||
// Make sure index throttle is high enough, via the UI.
|
||||
$this->drupalPostForm('admin/config/search/pages', array('cron_limit' => 20), t('Save configuration'));
|
||||
$this->assertEqual(20, $this->config('search.settings')->get('index.cron_limit', 100), 'Config setting was saved correctly');
|
||||
// Get a new search plugin, to make sure it has this setting.
|
||||
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
|
||||
$this->plugin->updateIndex();
|
||||
search_update_totals();
|
||||
$this->assertIndexCounts(0, 8, 'after updating fully');
|
||||
$this->assertDatabaseCounts(8, 0, 'after updating fully');
|
||||
|
||||
// Click the reindex button on the admin page, verify counts, and reindex.
|
||||
$this->drupalPostForm('admin/config/search/pages', array(), t('Re-index site'));
|
||||
$this->drupalPostForm(NULL, array(), t('Re-index site'));
|
||||
$this->assertIndexCounts(8, 8, 'after reindex');
|
||||
$this->assertDatabaseCounts(8, 0, 'after reindex');
|
||||
$this->plugin->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Test search results.
|
||||
|
||||
// This should find two results for the second and third node.
|
||||
$this->plugin->setSearch('English OR Hungarian', array(), array());
|
||||
$search_result = $this->plugin->execute();
|
||||
$this->assertEqual(count($search_result), 2, 'Found two results.');
|
||||
// Nodes are saved directly after each other and have the same created time
|
||||
// so testing for the order is not possible.
|
||||
$results = array($search_result[0]['title'], $search_result[1]['title']);
|
||||
$this->assertTrue(in_array('Third node this is the Hungarian title', $results), 'The search finds the correct Hungarian title.');
|
||||
$this->assertTrue(in_array('Second node this is the English title', $results), 'The search finds the correct English title.');
|
||||
|
||||
// Now filter for Hungarian results only.
|
||||
$this->plugin->setSearch('English OR Hungarian', array('f' => array('language:hu')), array());
|
||||
$search_result = $this->plugin->execute();
|
||||
|
||||
$this->assertEqual(count($search_result), 1, 'The search found only one result');
|
||||
$this->assertEqual($search_result[0]['title'], 'Third node this is the Hungarian title', 'The search finds the correct Hungarian title.');
|
||||
|
||||
// Test for search with common key word across multiple languages.
|
||||
$this->plugin->setSearch('node', array(), array());
|
||||
$search_result = $this->plugin->execute();
|
||||
|
||||
$this->assertEqual(count($search_result), 6, 'The search found total six results');
|
||||
|
||||
// Test with language filters and common key word.
|
||||
$this->plugin->setSearch('node', array('f' => array('language:hu')), array());
|
||||
$search_result = $this->plugin->execute();
|
||||
|
||||
$this->assertEqual(count($search_result), 2, 'The search found 2 results');
|
||||
|
||||
// Test to check for the language of result items.
|
||||
foreach($search_result as $result) {
|
||||
$this->assertEqual($result['langcode'], 'hu', 'The search found the correct Hungarian result');
|
||||
}
|
||||
|
||||
// Mark one of the nodes for reindexing, using the API function, and
|
||||
// verify indexing status.
|
||||
search_mark_for_reindex('node_search', $this->searchableNodes[0]->id());
|
||||
$this->assertIndexCounts(1, 8, 'after marking one node to reindex via API function');
|
||||
|
||||
// Update the index and verify the totals again.
|
||||
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
$this->plugin->updateIndex();
|
||||
search_update_totals();
|
||||
$this->assertIndexCounts(0, 8, 'after indexing again');
|
||||
|
||||
// Mark one node for reindexing by saving it, and verify indexing status.
|
||||
$this->searchableNodes[1]->save();
|
||||
$this->assertIndexCounts(1, 8, 'after marking one node to reindex via save');
|
||||
|
||||
// The request time is always the same throughout test runs. Update the
|
||||
// request time to a previous time, to simulate it having been marked
|
||||
// previously.
|
||||
$current = REQUEST_TIME;
|
||||
$old = $current - 10;
|
||||
db_update('search_dataset')
|
||||
->fields(array('reindex' => $old))
|
||||
->condition('reindex', $current, '>=')
|
||||
->execute();
|
||||
|
||||
// Save the node again. Verify that the request time on it is not updated.
|
||||
$this->searchableNodes[1]->save();
|
||||
$result = db_select('search_dataset', 'd')
|
||||
->fields('d', array('reindex'))
|
||||
->condition('type', 'node_search')
|
||||
->condition('sid', $this->searchableNodes[1]->id())
|
||||
->execute()
|
||||
->fetchField();
|
||||
$this->assertEqual($result, $old, 'Reindex time was not updated if node was already marked');
|
||||
|
||||
// Add a bogus entry to the search index table using a different search
|
||||
// type. This will not appear in the index status, because it is not
|
||||
// managed by a plugin.
|
||||
search_index('foo', $this->searchableNodes[0]->id(), 'en', 'some text');
|
||||
$this->assertIndexCounts(1, 8, 'after adding a different index item');
|
||||
|
||||
// Mark just this "foo" index for reindexing.
|
||||
search_mark_for_reindex('foo');
|
||||
$this->assertIndexCounts(1, 8, 'after reindexing the other search type');
|
||||
|
||||
// Mark everything for reindexing.
|
||||
search_mark_for_reindex();
|
||||
$this->assertIndexCounts(8, 8, 'after reindexing everything');
|
||||
|
||||
// Clear one item from the index, but with wrong language.
|
||||
$this->assertDatabaseCounts(8, 1, 'before clear');
|
||||
search_index_clear('node_search', $this->searchableNodes[0]->id(), 'hu');
|
||||
$this->assertDatabaseCounts(8, 1, 'after clear with wrong language');
|
||||
// Clear using correct language.
|
||||
search_index_clear('node_search', $this->searchableNodes[0]->id(), 'en');
|
||||
$this->assertDatabaseCounts(7, 1, 'after clear with right language');
|
||||
// Don't specify language.
|
||||
search_index_clear('node_search', $this->searchableNodes[1]->id());
|
||||
$this->assertDatabaseCounts(6, 1, 'unspecified language clear');
|
||||
// Clear everything in 'foo'.
|
||||
search_index_clear('foo');
|
||||
$this->assertDatabaseCounts(6, 0, 'other index clear');
|
||||
// Clear everything.
|
||||
search_index_clear();
|
||||
$this->assertDatabaseCounts(0, 0, 'complete clear');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the indexing status counts.
|
||||
*
|
||||
* @param int $remaining
|
||||
* Count of remaining items to verify.
|
||||
* @param int $total
|
||||
* Count of total items to verify.
|
||||
* @param string $message
|
||||
* Message to use, something like "after updating the search index".
|
||||
*/
|
||||
protected function assertIndexCounts($remaining, $total, $message) {
|
||||
// Check status via plugin method call.
|
||||
$status = $this->plugin->indexStatus();
|
||||
$this->assertEqual($status['remaining'], $remaining, 'Remaining items ' . $message . ' is ' . $remaining);
|
||||
$this->assertEqual($status['total'], $total, 'Total items ' . $message . ' is ' . $total);
|
||||
|
||||
// Check text in progress section of Search settings page. Note that this
|
||||
// test avoids using
|
||||
// \Drupal\Core\StringTranslation\TranslationInterface::formatPlural(), so
|
||||
// it tests for fragments of text.
|
||||
$indexed = $total - $remaining;
|
||||
$percent = ($total > 0) ? floor(100 * $indexed / $total) : 100;
|
||||
$this->drupalGet('admin/config/search/pages');
|
||||
$this->assertText($percent . '% of the site has been indexed.', 'Progress percent text at top of Search settings page is correct at: ' . $message);
|
||||
$this->assertText($remaining . ' item', 'Remaining text at top of Search settings page is correct at: ' . $message);
|
||||
|
||||
// Check text in pages section of Search settings page.
|
||||
$this->assertText($indexed . ' of ' . $total . ' indexed', 'Progress text in pages section of Search settings page is correct at: ' . $message);
|
||||
|
||||
// Check text on status report page.
|
||||
$this->drupalGet('admin/reports/status');
|
||||
$this->assertText('Search index progress', 'Search status section header is present on status report page');
|
||||
$this->assertText($percent . '%', 'Correct percentage is shown on status report page at: ' . $message);
|
||||
$this->assertText('(' . $remaining . ' remaining)', 'Correct remaining value is shown on status report page at: ' . $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks actual database counts of items in the search index.
|
||||
*
|
||||
* @param int $count_node
|
||||
* Count of node items to assert.
|
||||
* @param int $count_foo
|
||||
* Count of "foo" items to assert.
|
||||
* @param string $message
|
||||
* Message suffix to use.
|
||||
*/
|
||||
protected function assertDatabaseCounts($count_node, $count_foo, $message) {
|
||||
// Count number of distinct nodes by ID.
|
||||
$results = db_select('search_dataset', 'i')
|
||||
->fields('i', array('sid'))
|
||||
->condition('type', 'node_search')
|
||||
->groupBy('sid')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
$this->assertEqual($count_node, count($results), 'Node count was ' . $count_node . ' for ' . $message);
|
||||
|
||||
// Count number of "foo" records.
|
||||
$results = db_select('search_dataset', 'i')
|
||||
->fields('i', array('sid'))
|
||||
->condition('type', 'foo')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
$this->assertEqual($count_foo, count($results), 'Foo count was ' . $count_foo . ' for ' . $message);
|
||||
|
||||
}
|
||||
}
|
84
core/modules/search/src/Tests/SearchNodeDiacriticsTest.php
Normal file
84
core/modules/search/src/Tests/SearchNodeDiacriticsTest.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchNodeDiacriticsTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests search functionality with diacritics.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchNodeDiacriticsTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* A user with permission to use advanced search.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
public $testUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
node_access_rebuild();
|
||||
|
||||
// Create a test user and log in.
|
||||
$this->testUser = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'access user profiles'));
|
||||
$this->drupalLogin($this->testUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that search returns results with diacritics in the search phrase.
|
||||
*/
|
||||
function testPhraseSearchPunctuation() {
|
||||
$body_text = 'The Enricþment Center is cómmīŦŧęđ to the well BɆĬŇĜ of æll påŔťıçȉpǎǹţș. ';
|
||||
$body_text .= 'Also meklēt (see #731298)';
|
||||
$this->drupalCreateNode(array('body' => array(array('value' => $body_text))));
|
||||
|
||||
// Update the search index.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Refresh variables after the treatment.
|
||||
$this->refreshVariables();
|
||||
|
||||
$edit = array('keys' => 'meklet');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>meklēt</strong>');
|
||||
|
||||
$edit = array('keys' => 'meklēt');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>meklēt</strong>');
|
||||
|
||||
$edit = array('keys' => 'cómmīŦŧęđ BɆĬŇĜ påŔťıçȉpǎǹţș');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>cómmīŦŧęđ</strong>');
|
||||
$this->assertRaw('<strong>BɆĬŇĜ</strong>');
|
||||
$this->assertRaw('<strong>påŔťıçȉpǎǹţș</strong>');
|
||||
|
||||
$edit = array('keys' => 'committed being participants');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>cómmīŦŧęđ</strong>');
|
||||
$this->assertRaw('<strong>BɆĬŇĜ</strong>');
|
||||
$this->assertRaw('<strong>påŔťıçȉpǎǹţș</strong>');
|
||||
|
||||
$edit = array('keys' => 'Enricþment');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>Enricþment</strong>');
|
||||
|
||||
$edit = array('keys' => 'Enritchment');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoRaw('<strong>Enricþment</strong>');
|
||||
|
||||
$edit = array('keys' => 'æll');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>æll</strong>');
|
||||
|
||||
$edit = array('keys' => 'all');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoRaw('<strong>æll</strong>');
|
||||
}
|
||||
}
|
67
core/modules/search/src/Tests/SearchNodePunctuationTest.php
Normal file
67
core/modules/search/src/Tests/SearchNodePunctuationTest.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchNodePunctuationTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests search functionality with punctuation and HTML entities.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchNodePunctuationTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* A user with permission to use advanced search.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
public $testUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
node_access_rebuild();
|
||||
|
||||
// Create a test user and log in.
|
||||
$this->testUser = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'access user profiles'));
|
||||
$this->drupalLogin($this->testUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that search works with punctuation and HTML entities.
|
||||
*/
|
||||
function testPhraseSearchPunctuation() {
|
||||
$node = $this->drupalCreateNode(array('body' => array(array('value' => "The bunny's ears were fluffy."))));
|
||||
$node2 = $this->drupalCreateNode(array('body' => array(array('value' => 'Dignissim Aliquam & Quieligo meus natu quae quia te. Damnum© erat— neo pneum. Facilisi feugiat ibidem ratis.'))));
|
||||
|
||||
// Update the search index.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Refresh variables after the treatment.
|
||||
$this->refreshVariables();
|
||||
|
||||
// Submit a phrase wrapped in double quotes to include the punctuation.
|
||||
$edit = array('keys' => '"bunny\'s"');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText($node->label());
|
||||
|
||||
// Check if the author is linked correctly to the user profile page.
|
||||
$username = $node->getOwner()->getUsername();
|
||||
$this->assertLink($username);
|
||||
|
||||
// Search for "&" and verify entities are not broken up in the output.
|
||||
$edit = array('keys' => '&');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoRaw('<strong>&</strong>amp;');
|
||||
$this->assertText('You must include at least one positive keyword');
|
||||
|
||||
$edit = array('keys' => '&');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoRaw('<strong>&</strong>amp;');
|
||||
$this->assertText('You must include at least one positive keyword');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchNodeUpdateAndDeletionTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests search index is updated properly when nodes are removed or updated.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchNodeUpdateAndDeletionTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array();
|
||||
|
||||
/**
|
||||
* A user with permission to access and search content.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
public $testUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a test user and log in.
|
||||
$this->testUser = $this->drupalCreateUser(array('access content', 'search content'));
|
||||
$this->drupalLogin($this->testUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the search index info is properly updated when a node changes.
|
||||
*/
|
||||
function testSearchIndexUpdateOnNodeChange() {
|
||||
// Create a node.
|
||||
$node = $this->drupalCreateNode(array(
|
||||
'title' => 'Someone who says Ni!',
|
||||
'body' => array(array('value' => "We are the knights who say Ni!")),
|
||||
'type' => 'page'));
|
||||
|
||||
$node_search_plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
// Update the search index.
|
||||
$node_search_plugin->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Search the node to verify it appears in search results
|
||||
$edit = array('keys' => 'knights');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText($node->label());
|
||||
|
||||
// Update the node
|
||||
$node->body->value = "We want a shrubbery!";
|
||||
$node->save();
|
||||
|
||||
// Run indexer again
|
||||
$node_search_plugin->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Search again to verify the new text appears in test results.
|
||||
$edit = array('keys' => 'shrubbery');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText($node->label());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the search index info is updated when a node is deleted.
|
||||
*/
|
||||
function testSearchIndexUpdateOnNodeDeletion() {
|
||||
// Create a node.
|
||||
$node = $this->drupalCreateNode(array(
|
||||
'title' => 'No dragons here',
|
||||
'body' => array(array('value' => 'Again: No dragons here')),
|
||||
'type' => 'page'));
|
||||
|
||||
$node_search_plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
// Update the search index.
|
||||
$node_search_plugin->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Search the node to verify it appears in search results
|
||||
$edit = array('keys' => 'dragons');
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText($node->label());
|
||||
|
||||
// Get the node info from the search index tables.
|
||||
$search_index_dataset = db_query("SELECT sid FROM {search_index} WHERE type = 'node_search' AND word = :word", array(':word' => 'dragons'))
|
||||
->fetchField();
|
||||
$this->assertNotEqual($search_index_dataset, FALSE, t('Node info found on the search_index'));
|
||||
|
||||
// Delete the node.
|
||||
$node->delete();
|
||||
|
||||
// Check if the node info is gone from the search table.
|
||||
$search_index_dataset = db_query("SELECT sid FROM {search_index} WHERE type = 'node_search' AND word = :word", array(':word' => 'dragons'))
|
||||
->fetchField();
|
||||
$this->assertFalse($search_index_dataset, t('Node info successfully removed from search_index'));
|
||||
|
||||
// Search again to verify the node doesn't appear anymore.
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoText($node->label());
|
||||
}
|
||||
|
||||
}
|
103
core/modules/search/src/Tests/SearchNumberMatchingTest.php
Normal file
103
core/modules/search/src/Tests/SearchNumberMatchingTest.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchNumberMatchingTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
/**
|
||||
* Tests that numbers can be searched with more complex matching.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchNumberMatchingTest extends SearchTestBase {
|
||||
/**
|
||||
* A user with permission to administer nodes.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $testUser;
|
||||
|
||||
/**
|
||||
* An array of strings containing numbers to use for testing.
|
||||
*
|
||||
* Define a group of numbers that should all match each other --
|
||||
* numbers with internal punctuation should match each other, as well
|
||||
* as numbers with and without leading zeros and leading/trailing
|
||||
* . and -.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $numbers = array(
|
||||
'123456789',
|
||||
'12/34/56789',
|
||||
'12.3456789',
|
||||
'12-34-56789',
|
||||
'123,456,789',
|
||||
'-123456789',
|
||||
'0123456789',
|
||||
);
|
||||
|
||||
/**
|
||||
* An array of nodes created for testing purposes.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $nodes;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->testUser = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports'));
|
||||
$this->drupalLogin($this->testUser);
|
||||
|
||||
foreach ($this->numbers as $num) {
|
||||
$info = array(
|
||||
'body' => array(array('value' => $num)),
|
||||
'type' => 'page',
|
||||
'language' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
);
|
||||
$this->nodes[] = $this->drupalCreateNode($info);
|
||||
}
|
||||
|
||||
// Run cron to ensure the content is indexed.
|
||||
$this->cronRun();
|
||||
$this->drupalGet('admin/reports/dblog');
|
||||
$this->assertText(t('Cron run completed'), 'Log shows cron run completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that all the numbers can be searched.
|
||||
*/
|
||||
function testNumberSearching() {
|
||||
for ($i = 0; $i < count($this->numbers); $i++) {
|
||||
$node = $this->nodes[$i];
|
||||
|
||||
// Verify that the node title does not appear on the search page
|
||||
// with a dummy search.
|
||||
$this->drupalPostForm('search/node',
|
||||
array('keys' => 'foo'),
|
||||
t('Search'));
|
||||
$this->assertNoText($node->label(), format_string('%number: node title not shown in dummy search', array('%number' => $i)));
|
||||
|
||||
// Now verify that we can find node i by searching for any of the
|
||||
// numbers.
|
||||
for ($j = 0; $j < count($this->numbers); $j++) {
|
||||
$number = $this->numbers[$j];
|
||||
// If the number is negative, remove the - sign, because - indicates
|
||||
// "not keyword" when searching.
|
||||
$number = ltrim($number, '-');
|
||||
|
||||
$this->drupalPostForm('search/node',
|
||||
array('keys' => $number),
|
||||
t('Search'));
|
||||
$this->assertText($node->label(), format_string('%i: node title shown (search found the node) in search for number %number', array('%i' => $i, '%number' => $number)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
108
core/modules/search/src/Tests/SearchNumbersTest.php
Normal file
108
core/modules/search/src/Tests/SearchNumbersTest.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchNumbersTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
/**
|
||||
* Tests that numbers can be searched.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchNumbersTest extends SearchTestBase {
|
||||
/**
|
||||
* A user with permission to administer nodes.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $testUser;
|
||||
|
||||
/**
|
||||
* An array containing a series of "numbers" for testing purposes.
|
||||
*
|
||||
* Create content with various numbers in it.
|
||||
* Note: 50 characters is the current limit of the search index's word
|
||||
* field.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $numbers = array(
|
||||
'ISBN' => '978-0446365383',
|
||||
'UPC' => '036000 291452',
|
||||
'EAN bar code' => '5901234123457',
|
||||
'negative' => '-123456.7890',
|
||||
'quoted negative' => '"-123456.7890"',
|
||||
'leading zero' => '0777777777',
|
||||
'tiny' => '111',
|
||||
'small' => '22222222222222',
|
||||
'medium' => '333333333333333333333333333',
|
||||
'large' => '444444444444444444444444444444444444444',
|
||||
'gigantic' => '5555555555555555555555555555555555555555555555555',
|
||||
'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666',
|
||||
'date' => '01/02/2009',
|
||||
'commas' => '987,654,321',
|
||||
);
|
||||
|
||||
/**
|
||||
* An array of nodes created for testing purposes.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $nodes;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->testUser = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports'));
|
||||
$this->drupalLogin($this->testUser);
|
||||
|
||||
foreach ($this->numbers as $doc => $num) {
|
||||
$info = array(
|
||||
'body' => array(array('value' => $num)),
|
||||
'type' => 'page',
|
||||
'language' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
'title' => $doc . ' number',
|
||||
);
|
||||
$this->nodes[$doc] = $this->drupalCreateNode($info);
|
||||
}
|
||||
|
||||
// Run cron to ensure the content is indexed.
|
||||
$this->cronRun();
|
||||
$this->drupalGet('admin/reports/dblog');
|
||||
$this->assertText(t('Cron run completed'), 'Log shows cron run completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that all the numbers can be searched.
|
||||
*/
|
||||
function testNumberSearching() {
|
||||
$types = array_keys($this->numbers);
|
||||
|
||||
foreach ($types as $type) {
|
||||
$number = $this->numbers[$type];
|
||||
// If the number is negative, remove the - sign, because - indicates
|
||||
// "not keyword" when searching.
|
||||
$number = ltrim($number, '-');
|
||||
$node = $this->nodes[$type];
|
||||
|
||||
// Verify that the node title does not appear on the search page
|
||||
// with a dummy search.
|
||||
$this->drupalPostForm('search/node',
|
||||
array('keys' => 'foo'),
|
||||
t('Search'));
|
||||
$this->assertNoText($node->label(), $type . ': node title not shown in dummy search');
|
||||
|
||||
// Verify that the node title does appear as a link on the search page
|
||||
// when searching for the number.
|
||||
$this->drupalPostForm('search/node',
|
||||
array('keys' => $number),
|
||||
t('Search'));
|
||||
$this->assertText($node->label(), format_string('%type: node title shown (search found the node) in search for number %number.', array('%type' => $type, '%number' => $number)));
|
||||
}
|
||||
}
|
||||
}
|
102
core/modules/search/src/Tests/SearchPageCacheTagsTest.php
Normal file
102
core/modules/search/src/Tests/SearchPageCacheTagsTest.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchPageCacheTagsTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests the search_page entity cache tags on the search results pages.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchPageCacheTagsTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $dumpHeaders = TRUE;
|
||||
|
||||
/**
|
||||
* A user with permission to search content.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $searchingUser;
|
||||
|
||||
/**
|
||||
* A node that is indexed by the search module.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create user.
|
||||
$this->searchingUser = $this->drupalCreateUser(array('search content', 'access user profiles'));
|
||||
|
||||
// Create a node and update the search index.
|
||||
$this->node = $this->drupalCreateNode(['title' => 'bike shed shop']);
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the presence of the expected cache tag in various situations.
|
||||
*/
|
||||
function testSearchText() {
|
||||
$this->drupalLogin($this->searchingUser);
|
||||
|
||||
// Initial page for searching nodes.
|
||||
$this->drupalGet('search/node');
|
||||
$this->assertCacheTag('config:search.page.node_search');
|
||||
$this->assertCacheTag('search_index:node_search');
|
||||
|
||||
// Node search results.
|
||||
$edit = array();
|
||||
$edit['keys'] = 'bike shed';
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText('bike shed shop');
|
||||
$this->assertCacheTag('config:search.page.node_search');
|
||||
$this->assertCacheTag('search_index');
|
||||
$this->assertCacheTag('search_index:node_search');
|
||||
|
||||
// Updating a node should invalidate the search plugin's index cache tag.
|
||||
$this->node->title = 'bike shop';
|
||||
$this->node->save();
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText('bike shop');
|
||||
$this->assertCacheTag('config:search.page.node_search');
|
||||
$this->assertCacheTag('search_index');
|
||||
$this->assertCacheTag('search_index:node_search');
|
||||
|
||||
// Deleting a node should invalidate the search plugin's index cache tag.
|
||||
$this->node->delete();
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText('Your search yielded no results.');
|
||||
$this->assertCacheTag('config:search.page.node_search');
|
||||
$this->assertCacheTag('search_index');
|
||||
$this->assertCacheTag('search_index:node_search');
|
||||
|
||||
// Initial page for searching users.
|
||||
$this->drupalGet('search/user');
|
||||
$this->assertCacheTag('config:search.page.user_search');
|
||||
$this->assertNoCacheTag('search_index');
|
||||
$this->assertNoCacheTag('search_index:user_search');
|
||||
|
||||
// User search results.
|
||||
$edit['keys'] = $this->searchingUser->getUsername();
|
||||
$this->drupalPostForm('search/user', $edit, t('Search'));
|
||||
$this->assertCacheTag('config:search.page.user_search');
|
||||
$this->assertNoCacheTag('search_index');
|
||||
$this->assertNoCacheTag('search_index:user_search');
|
||||
}
|
||||
|
||||
}
|
48
core/modules/search/src/Tests/SearchPageOverrideTest.php
Normal file
48
core/modules/search/src/Tests/SearchPageOverrideTest.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchPageOverrideTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests if the result page can be overridden.
|
||||
*
|
||||
* Verifies that a plugin can override the buildResults() method to
|
||||
* control what the search results page looks like.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchPageOverrideTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('search_extra_type');
|
||||
|
||||
/**
|
||||
* A user with permission to administer search.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
public $searchUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Login as a user that can create and search content.
|
||||
$this->searchUser = $this->drupalCreateUser(array('search content', 'administer search'));
|
||||
$this->drupalLogin($this->searchUser);
|
||||
}
|
||||
|
||||
function testSearchPageHook() {
|
||||
$keys = 'bike shed ' . $this->randomMachineName();
|
||||
$this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys)));
|
||||
$this->assertText('Dummy search snippet', 'Dummy search snippet is shown');
|
||||
$this->assertText('Test page text is here', 'Page override is working');
|
||||
}
|
||||
}
|
140
core/modules/search/src/Tests/SearchPageTextTest.php
Normal file
140
core/modules/search/src/Tests/SearchPageTextTest.php
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchPageTextTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
|
||||
/**
|
||||
* Tests the search help text and search page text.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchPageTextTest extends SearchTestBase {
|
||||
/**
|
||||
* A user with permission to use advanced search.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $searchingUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create user.
|
||||
$this->searchingUser = $this->drupalCreateUser(array('search content', 'access user profiles', 'use advanced search'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the failed search text, and various other text on the search page.
|
||||
*/
|
||||
function testSearchText() {
|
||||
$this->drupalLogin($this->searchingUser);
|
||||
$this->drupalGet('search/node');
|
||||
$this->assertText(t('Enter your keywords'));
|
||||
$this->assertText(t('Search'));
|
||||
$this->assertTitle(t('Search') . ' | Drupal', 'Search page title is correct');
|
||||
|
||||
$edit = array();
|
||||
$search_terms = 'bike shed ' . $this->randomMachineName();
|
||||
$edit['keys'] = $search_terms;
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText('search yielded no results');
|
||||
$this->assertText(t('Search'));
|
||||
$title_source = 'Search for @keywords | Drupal';
|
||||
$this->assertTitle(t($title_source, array('@keywords' => Unicode::truncate($search_terms, 60, TRUE, TRUE))), 'Search page title is correct');
|
||||
$this->assertNoText('Node', 'Erroneous tab and breadcrumb text is not present');
|
||||
$this->assertNoText(t('Node'), 'Erroneous translated tab and breadcrumb text is not present');
|
||||
$this->assertText(t('Content'), 'Tab and breadcrumb text is present');
|
||||
|
||||
$this->clickLink('Search help');
|
||||
$this->assertText('Search help', 'Correct title is on search help page');
|
||||
$this->assertText('Use upper-case OR to get more results', 'Correct text is on content search help page');
|
||||
|
||||
// Search for a longer text, and see that it is in the title, truncated.
|
||||
$edit = array();
|
||||
$search_terms = 'Every word is like an unnecessary stain on silence and nothingness.';
|
||||
$edit['keys'] = $search_terms;
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertTitle(t($title_source, array('@keywords' => 'Every word is like an unnecessary stain on silence and…')), 'Search page title is correct');
|
||||
|
||||
// Search for a string with a lot of special characters.
|
||||
$search_terms = 'Hear nothing > "see nothing" `feel' . " '1982.";
|
||||
$edit['keys'] = $search_terms;
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$actual_title = (string) current($this->xpath('//title'));
|
||||
$this->assertEqual($actual_title, Html::decodeEntities(t($title_source, array('@keywords' => Unicode::truncate($search_terms, 60, TRUE, TRUE)))), 'Search page title is correct');
|
||||
|
||||
$edit['keys'] = $this->searchingUser->getUsername();
|
||||
$this->drupalPostForm('search/user', $edit, t('Search'));
|
||||
$this->assertText(t('Search'));
|
||||
$this->assertTitle(t($title_source, array('@keywords' => Unicode::truncate($this->searchingUser->getUsername(), 60, TRUE, TRUE))));
|
||||
|
||||
$this->clickLink('Search help');
|
||||
$this->assertText('Search help', 'Correct title is on search help page');
|
||||
$this->assertText('user names and partial user names', 'Correct text is on user search help page');
|
||||
|
||||
// Test that search keywords containing slashes are correctly loaded
|
||||
// from the GET params and displayed in the search form.
|
||||
$arg = $this->randomMachineName() . '/' . $this->randomMachineName();
|
||||
$this->drupalGet('search/node', array('query' => array('keys' => $arg)));
|
||||
$input = $this->xpath("//input[@id='edit-keys' and @value='{$arg}']");
|
||||
$this->assertFalse(empty($input), 'Search keys with a / are correctly set as the default value in the search box.');
|
||||
|
||||
// Test a search input exceeding the limit of AND/OR combinations to test
|
||||
// the Denial-of-Service protection.
|
||||
$limit = $this->config('search.settings')->get('and_or_limit');
|
||||
$keys = array();
|
||||
for ($i = 0; $i < $limit + 1; $i++) {
|
||||
// Use a key of 4 characters to ensure we never generate 'AND' or 'OR'.
|
||||
$keys[] = $this->randomMachineName(4);
|
||||
if ($i % 2 == 0) {
|
||||
$keys[] = 'OR';
|
||||
}
|
||||
}
|
||||
$edit['keys'] = implode(' ', $keys);
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw(t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => $limit)));
|
||||
|
||||
// Test that a search on Node or User with no keywords entered generates
|
||||
// the "Please enter some keywords" message.
|
||||
$this->drupalPostForm('search/node', array(), t('Search'));
|
||||
$this->assertText(t('Please enter some keywords'), 'With no keywords entered, message is displayed on node page');
|
||||
$this->drupalPostForm('search/user', array(), t('Search'));
|
||||
$this->assertText(t('Please enter some keywords'), 'With no keywords entered, message is displayed on user page');
|
||||
|
||||
// Make sure the "Please enter some keywords" message is NOT displayed if
|
||||
// you use "or" words or phrases in Advanced Search.
|
||||
$this->drupalPostForm('search/node', array('or' => $this->randomMachineName() . ' ' . $this->randomMachineName()), t('Advanced search'));
|
||||
$this->assertNoText(t('Please enter some keywords'), 'With advanced OR keywords entered, no keywords message is not displayed on node page');
|
||||
$this->drupalPostForm('search/node', array('phrase' => '"' . $this->randomMachineName() . '" "' . $this->randomMachineName() . '"'), t('Advanced search'));
|
||||
$this->assertNoText(t('Please enter some keywords'), 'With advanced phrase entered, no keywords message is not displayed on node page');
|
||||
|
||||
// Verify that if you search for a too-short keyword, you get the right
|
||||
// message, and that if after that you search for a longer keyword, you
|
||||
// do not still see the message.
|
||||
$this->drupalPostForm('search/node', array('keys' => $this->randomMachineName(1)), t('Search'));
|
||||
$this->assertText('You must include at least one positive keyword', 'Keyword message is displayed when searching for short word');
|
||||
$this->assertNoText(t('Please enter some keywords'), 'With short word entered, no keywords message is not displayed');
|
||||
$this->drupalPostForm(NULL, array('keys' => $this->randomMachineName()), t('Search'));
|
||||
$this->assertNoText('You must include at least one positive keyword', 'Keyword message is not displayed when searching for long word after short word search');
|
||||
|
||||
// Test that if you search for a URL with .. in it, you still end up at
|
||||
// the search page. See issue https://www.drupal.org/node/890058.
|
||||
$this->drupalPostForm('search/node', array('keys' => '../../admin'), t('Search'));
|
||||
$this->assertResponse(200, 'Searching for ../../admin with non-admin user does not lead to a 403 error');
|
||||
$this->assertText('no results', 'Searching for ../../admin with non-admin user gives you a no search results page');
|
||||
|
||||
// Test that if you search for a URL starting with "./", you still end up
|
||||
// at the search page. See issue https://www.drupal.org/node/1421560.
|
||||
$this->drupalPostForm('search/node', array('keys' => '.something'), t('Search'));
|
||||
$this->assertResponse(200, 'Searching for .something does not lead to a 403 error');
|
||||
$this->assertText('no results', 'Searching for .something gives you a no search results page');
|
||||
|
||||
}
|
||||
}
|
102
core/modules/search/src/Tests/SearchPreprocessLangcodeTest.php
Normal file
102
core/modules/search/src/Tests/SearchPreprocessLangcodeTest.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchPreprocessLangcodeTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests that the search preprocessing uses the correct language code.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchPreprocessLangcodeTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('search_langcode_test');
|
||||
|
||||
/**
|
||||
* Test node for searching.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$web_user = $this->drupalCreateUser(array(
|
||||
'create page content',
|
||||
'edit own page content',
|
||||
'search content',
|
||||
'use advanced search',
|
||||
));
|
||||
$this->drupalLogin($web_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that hook_search_preprocess() returns the correct langcode.
|
||||
*/
|
||||
function testPreprocessLangcode() {
|
||||
// Create a node.
|
||||
$this->node = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'en'));
|
||||
|
||||
// First update the index. This does the initial processing.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
|
||||
// Then, run the shutdown function. Testing is a unique case where indexing
|
||||
// and searching has to happen in the same request, so running the shutdown
|
||||
// function manually is needed to finish the indexing process.
|
||||
search_update_totals();
|
||||
|
||||
// Search for the additional text that is added by the preprocess
|
||||
// function. If you search for text that is in the node, preprocess is
|
||||
// not invoked on the node during the search excerpt generation.
|
||||
$edit = array('or' => 'Additional text');
|
||||
$this->drupalPostForm('search/node', $edit, t('Advanced search'));
|
||||
|
||||
// Checks if the langcode message has been set by hook_search_preprocess().
|
||||
$this->assertText('Langcode Preprocess Test: en');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests stemming for hook_search_preprocess().
|
||||
*/
|
||||
function testPreprocessStemming() {
|
||||
// Create a node.
|
||||
$this->node = $this->drupalCreateNode(array(
|
||||
'title' => 'we are testing',
|
||||
'body' => array(array()),
|
||||
'langcode' => 'en',
|
||||
));
|
||||
|
||||
// First update the index. This does the initial processing.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
|
||||
// Then, run the shutdown function. Testing is a unique case where indexing
|
||||
// and searching has to happen in the same request, so running the shutdown
|
||||
// function manually is needed to finish the indexing process.
|
||||
search_update_totals();
|
||||
|
||||
// Search for the title of the node with a POST query.
|
||||
$edit = array('or' => 'testing');
|
||||
$this->drupalPostForm('search/node', $edit, t('Advanced search'));
|
||||
|
||||
// Check if the node has been found.
|
||||
$this->assertText('Search results');
|
||||
$this->assertText('we are testing');
|
||||
|
||||
// Search for the same node using a different query.
|
||||
$edit = array('or' => 'test');
|
||||
$this->drupalPostForm('search/node', $edit, t('Advanced search'));
|
||||
|
||||
// Check if the node has been found.
|
||||
$this->assertText('Search results');
|
||||
$this->assertText('we are testing');
|
||||
}
|
||||
}
|
53
core/modules/search/src/Tests/SearchQueryAlterTest.php
Normal file
53
core/modules/search/src/Tests/SearchQueryAlterTest.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchQueryAlterTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests that the node search query can be altered via the query alter hook.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchQueryAlterTest extends SearchTestBase {
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('search_query_alter');
|
||||
|
||||
/**
|
||||
* Tests that the query alter works.
|
||||
*/
|
||||
function testQueryAlter() {
|
||||
// Login with sufficient privileges.
|
||||
$this->drupalLogin($this->drupalCreateUser(array('create page content', 'search content')));
|
||||
|
||||
// Create a node and an article with the same keyword. The query alter
|
||||
// test module will alter the query so only articles should be returned.
|
||||
$data = array(
|
||||
'type' => 'page',
|
||||
'title' => 'test page',
|
||||
'body' => array(array('value' => 'pizza')),
|
||||
);
|
||||
$this->drupalCreateNode($data);
|
||||
|
||||
$data['type'] = 'article';
|
||||
$data['title'] = 'test article';
|
||||
$this->drupalCreateNode($data);
|
||||
|
||||
// Update the search index.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Search for the body keyword 'pizza'.
|
||||
$this->drupalPostForm('search/node', array('keys' => 'pizza'), t('Search'));
|
||||
// The article should be there but not the page.
|
||||
$this->assertText('article', 'Article is in search results');
|
||||
$this->assertNoText('page', 'Page is not in search results');
|
||||
}
|
||||
}
|
280
core/modules/search/src/Tests/SearchRankingTest.php
Normal file
280
core/modules/search/src/Tests/SearchRankingTest.php
Normal file
|
@ -0,0 +1,280 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchRankingTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Indexes content and tests ranking factors.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchRankingTest extends SearchTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* The node search page.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageInterface
|
||||
*/
|
||||
protected $nodeSearch;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('statistics', 'comment');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a plugin instance.
|
||||
$this->nodeSearch = entity_load('search_page', 'node_search');
|
||||
|
||||
// Login with sufficient privileges.
|
||||
$this->drupalLogin($this->drupalCreateUser(array('post comments', 'skip comment approval', 'create page content', 'administer search')));
|
||||
}
|
||||
|
||||
public function testRankings() {
|
||||
// Add a comment field.
|
||||
$this->addDefaultCommentField('node', 'page');
|
||||
|
||||
// Build a list of the rankings to test.
|
||||
$node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
|
||||
|
||||
// Create nodes for testing.
|
||||
$nodes = array();
|
||||
foreach ($node_ranks as $node_rank) {
|
||||
$settings = array(
|
||||
'type' => 'page',
|
||||
'comment' => array(array(
|
||||
'status' => CommentItemInterface::HIDDEN,
|
||||
)),
|
||||
'title' => 'Drupal rocks',
|
||||
'body' => array(array('value' => "Drupal's search rocks")),
|
||||
// Node is one day old.
|
||||
'created' => REQUEST_TIME - 24 * 3600,
|
||||
'sticky' => 0,
|
||||
'promote' => 0,
|
||||
);
|
||||
foreach (array(0, 1) as $num) {
|
||||
if ($num == 1) {
|
||||
switch ($node_rank) {
|
||||
case 'sticky':
|
||||
case 'promote':
|
||||
$settings[$node_rank] = 1;
|
||||
break;
|
||||
case 'relevance':
|
||||
$settings['body'][0]['value'] .= " really rocks";
|
||||
break;
|
||||
case 'recent':
|
||||
// Node is 1 hour hold.
|
||||
$settings['created'] = REQUEST_TIME - 3600;
|
||||
break;
|
||||
case 'comments':
|
||||
$settings['comment'][0]['status'] = CommentItemInterface::OPEN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$nodes[$node_rank][$num] = $this->drupalCreateNode($settings);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a comment to one of the nodes.
|
||||
$edit = array();
|
||||
$edit['subject[0][value]'] = 'my comment title';
|
||||
$edit['comment_body[0][value]'] = 'some random comment';
|
||||
$this->drupalGet('comment/reply/node/' . $nodes['comments'][1]->id() . '/comment');
|
||||
$this->drupalPostForm(NULL, $edit, t('Preview'));
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
|
||||
// Enable counting of statistics.
|
||||
$this->config('statistics.settings')->set('count_content_views', 1)->save();
|
||||
|
||||
// Simulating content views is kind of difficult in the test. Leave that
|
||||
// to the Statistics module. So instead go ahead and manually update the
|
||||
// counter for this node.
|
||||
$nid = $nodes['views'][1]->id();
|
||||
db_insert('node_counter')
|
||||
->fields(array('totalcount' => 5, 'daycount' => 5, 'timestamp' => REQUEST_TIME, 'nid' => $nid))
|
||||
->execute();
|
||||
|
||||
// Run cron to update the search index and comment/statistics totals.
|
||||
$this->cronRun();
|
||||
|
||||
// Test that the settings form displays the content ranking section.
|
||||
$this->drupalGet('admin/config/search/pages/manage/node_search');
|
||||
$this->assertText(t('Content ranking'));
|
||||
|
||||
// Check that all rankings are visible and set to 0.
|
||||
foreach ($node_ranks as $node_rank) {
|
||||
$this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '-value"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.');
|
||||
}
|
||||
|
||||
// Test each of the possible rankings.
|
||||
$edit = array();
|
||||
foreach ($node_ranks as $node_rank) {
|
||||
// Enable the ranking we are testing.
|
||||
$edit['rankings[' . $node_rank . '][value]'] = 10;
|
||||
$this->drupalPostForm('admin/config/search/pages/manage/node_search', $edit, t('Save search page'));
|
||||
$this->drupalGet('admin/config/search/pages/manage/node_search');
|
||||
$this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '-value"]//option[@value="10"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 10.');
|
||||
|
||||
// Reload the plugin to get the up-to-date values.
|
||||
$this->nodeSearch = entity_load('search_page', 'node_search');
|
||||
// Do the search and assert the results.
|
||||
$this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
|
||||
$set = $this->nodeSearch->getPlugin()->execute();
|
||||
$this->assertEqual($set[0]['node']->id(), $nodes[$node_rank][1]->id(), 'Search ranking "' . $node_rank . '" order.');
|
||||
|
||||
// Clear this ranking for the next test.
|
||||
$edit['rankings[' . $node_rank . '][value]'] = 0;
|
||||
}
|
||||
|
||||
// Save the final node_rank change then check that all rankings are visible
|
||||
// and have been set back to 0.
|
||||
$this->drupalPostForm('admin/config/search/pages/manage/node_search', $edit, t('Save search page'));
|
||||
$this->drupalGet('admin/config/search/pages/manage/node_search');
|
||||
foreach ($node_ranks as $node_rank) {
|
||||
$this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '-value"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.');
|
||||
}
|
||||
|
||||
// Try with sticky, then promoted. This is a test for issue
|
||||
// https://www.drupal.org/node/771596.
|
||||
$node_ranks = array(
|
||||
'sticky' => 10,
|
||||
'promote' => 1,
|
||||
'relevance' => 0,
|
||||
'recent' => 0,
|
||||
'comments' => 0,
|
||||
'views' => 0,
|
||||
);
|
||||
$configuration = $this->nodeSearch->getPlugin()->getConfiguration();
|
||||
foreach ($node_ranks as $var => $value) {
|
||||
$configuration['rankings'][$var] = $value;
|
||||
}
|
||||
$this->nodeSearch->getPlugin()->setConfiguration($configuration);
|
||||
$this->nodeSearch->save();
|
||||
|
||||
// Do the search and assert the results. The sticky node should show up
|
||||
// first, then the promoted node, then all the rest.
|
||||
$this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
|
||||
$set = $this->nodeSearch->getPlugin()->execute();
|
||||
$this->assertEqual($set[0]['node']->id(), $nodes['sticky'][1]->id(), 'Search ranking for sticky first worked.');
|
||||
$this->assertEqual($set[1]['node']->id(), $nodes['promote'][1]->id(), 'Search ranking for promoted second worked.');
|
||||
|
||||
// Try with recent, then comments. This is a test for issues
|
||||
// https://www.drupal.org/node/771596 and
|
||||
// https://www.drupal.org/node/303574.
|
||||
$node_ranks = array(
|
||||
'sticky' => 0,
|
||||
'promote' => 0,
|
||||
'relevance' => 0,
|
||||
'recent' => 10,
|
||||
'comments' => 1,
|
||||
'views' => 0,
|
||||
);
|
||||
$configuration = $this->nodeSearch->getPlugin()->getConfiguration();
|
||||
foreach ($node_ranks as $var => $value) {
|
||||
$configuration['rankings'][$var] = $value;
|
||||
}
|
||||
$this->nodeSearch->getPlugin()->setConfiguration($configuration);
|
||||
$this->nodeSearch->save();
|
||||
|
||||
// Do the search and assert the results. The recent node should show up
|
||||
// first, then the commented node, then all the rest.
|
||||
$this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
|
||||
$set = $this->nodeSearch->getPlugin()->execute();
|
||||
$this->assertEqual($set[0]['node']->id(), $nodes['recent'][1]->id(), 'Search ranking for recent first worked.');
|
||||
$this->assertEqual($set[1]['node']->id(), $nodes['comments'][1]->id(), 'Search ranking for comments second worked.');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test rankings of HTML tags.
|
||||
*/
|
||||
public function testHTMLRankings() {
|
||||
$full_html_format = entity_create('filter_format', array(
|
||||
'format' => 'full_html',
|
||||
'name' => 'Full HTML',
|
||||
));
|
||||
$full_html_format->save();
|
||||
|
||||
// Test HTML tags with different weights.
|
||||
$sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag');
|
||||
$shuffled_tags = $sorted_tags;
|
||||
|
||||
// Shuffle tags to ensure HTML tags are ranked properly.
|
||||
shuffle($shuffled_tags);
|
||||
$settings = array(
|
||||
'type' => 'page',
|
||||
'title' => 'Simple node',
|
||||
);
|
||||
$nodes = array();
|
||||
foreach ($shuffled_tags as $tag) {
|
||||
switch ($tag) {
|
||||
case 'a':
|
||||
$settings['body'] = array(array('value' => \Drupal::l('Drupal Rocks', new Url('<front>')), 'format' => 'full_html'));
|
||||
break;
|
||||
case 'notag':
|
||||
$settings['body'] = array(array('value' => 'Drupal Rocks'));
|
||||
break;
|
||||
default:
|
||||
$settings['body'] = array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html'));
|
||||
break;
|
||||
}
|
||||
$nodes[$tag] = $this->drupalCreateNode($settings);
|
||||
}
|
||||
|
||||
// Update the search index.
|
||||
$this->nodeSearch->getPlugin()->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
$this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
|
||||
// Do the search and assert the results.
|
||||
$set = $this->nodeSearch->getPlugin()->execute();
|
||||
|
||||
// Test the ranking of each tag.
|
||||
foreach ($sorted_tags as $tag_rank => $tag) {
|
||||
// Assert the results.
|
||||
if ($tag == 'notag') {
|
||||
$this->assertEqual($set[$tag_rank]['node']->id(), $nodes[$tag]->id(), 'Search tag ranking for plain text order.');
|
||||
} else {
|
||||
$this->assertEqual($set[$tag_rank]['node']->id(), $nodes[$tag]->id(), 'Search tag ranking for "<' . $sorted_tags[$tag_rank] . '>" order.');
|
||||
}
|
||||
}
|
||||
|
||||
// Test tags with the same weight against the sorted tags.
|
||||
$unsorted_tags = array('u', 'b', 'i', 'strong', 'em');
|
||||
foreach ($unsorted_tags as $tag) {
|
||||
$settings['body'] = array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html'));
|
||||
$node = $this->drupalCreateNode($settings);
|
||||
|
||||
// Update the search index.
|
||||
$this->nodeSearch->getPlugin()->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
$this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
|
||||
// Do the search and assert the results.
|
||||
$set = $this->nodeSearch->getPlugin()->execute();
|
||||
|
||||
// Ranking should always be second to last.
|
||||
$set = array_slice($set, -2, 1);
|
||||
|
||||
// Assert the results.
|
||||
$this->assertEqual($set[0]['node']->id(), $node->id(), 'Search tag ranking for "<' . $tag . '>" order.');
|
||||
|
||||
// Delete node so it doesn't show up in subsequent search results.
|
||||
$node->delete();
|
||||
}
|
||||
}
|
||||
}
|
55
core/modules/search/src/Tests/SearchSetLocaleTest.php
Normal file
55
core/modules/search/src/Tests/SearchSetLocaleTest.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchSetLocaleTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
/**
|
||||
* Tests that search works with numeric locale settings.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchSetLocaleTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('comment');
|
||||
|
||||
/**
|
||||
* A node search plugin instance.
|
||||
*
|
||||
* @var \Drupal\search\Plugin\SearchInterface
|
||||
*/
|
||||
protected $nodeSearchPlugin;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a plugin instance.
|
||||
$this->nodeSearchPlugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
// Create a node with a very simple body.
|
||||
$this->drupalCreateNode(array('body' => array(array('value' => 'tapir'))));
|
||||
// Update the search index.
|
||||
$this->nodeSearchPlugin->updateIndex();
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that search works with a numeric locale set.
|
||||
*/
|
||||
public function testSearchWithNumericLocale() {
|
||||
// French decimal point is comma.
|
||||
setlocale(LC_NUMERIC, 'fr_FR');
|
||||
$this->nodeSearchPlugin->setSearch('tapir', array(), array());
|
||||
// The call to execute will throw an exception if a float in the wrong
|
||||
// format is passed in the query to the database, so an assertion is not
|
||||
// necessary here.
|
||||
$this->nodeSearchPlugin->execute();
|
||||
}
|
||||
}
|
85
core/modules/search/src/Tests/SearchSimplifyTest.php
Normal file
85
core/modules/search/src/Tests/SearchSimplifyTest.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchSimplifyTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
|
||||
/**
|
||||
* Tests that the search_simply() function works as intended.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchSimplifyTest extends SearchTestBase {
|
||||
/**
|
||||
* Tests that all Unicode characters simplify correctly.
|
||||
*/
|
||||
function testSearchSimplifyUnicode() {
|
||||
// This test uses a file that was constructed so that the even lines are
|
||||
// boundary characters, and the odd lines are valid word characters. (It
|
||||
// was generated as a sequence of all the Unicode characters, and then the
|
||||
// boundary characters (punctuation, spaces, etc.) were split off into
|
||||
// their own lines). So the even-numbered lines should simplify to nothing,
|
||||
// and the odd-numbered lines we need to split into shorter chunks and
|
||||
// verify that simplification doesn't lose any characters.
|
||||
$input = file_get_contents(\Drupal::root() . '/core/modules/search/tests/UnicodeTest.txt');
|
||||
$basestrings = explode(chr(10), $input);
|
||||
$strings = array();
|
||||
foreach ($basestrings as $key => $string) {
|
||||
if ($key %2) {
|
||||
// Even line - should simplify down to a space.
|
||||
$simplified = search_simplify($string);
|
||||
$this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
|
||||
}
|
||||
else {
|
||||
// Odd line, should be word characters.
|
||||
// Split this into 30-character chunks, so we don't run into limits
|
||||
// of truncation in search_simplify().
|
||||
$start = 0;
|
||||
while ($start < Unicode::strlen($string)) {
|
||||
$newstr = Unicode::substr($string, $start, 30);
|
||||
// Special case: leading zeros are removed from numeric strings,
|
||||
// and there's one string in this file that is numbers starting with
|
||||
// zero, so prepend a 1 on that string.
|
||||
if (preg_match('/^[0-9]+$/', $newstr)) {
|
||||
$newstr = '1' . $newstr;
|
||||
}
|
||||
$strings[] = $newstr;
|
||||
$start += 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($strings as $key => $string) {
|
||||
$simplified = search_simplify($string);
|
||||
$this->assertTrue(Unicode::strlen($simplified) >= Unicode::strlen($string), "Nothing is removed from string $key.");
|
||||
}
|
||||
|
||||
// Test the low-numbered ASCII control characters separately. They are not
|
||||
// in the text file because they are problematic for diff, especially \0.
|
||||
$string = '';
|
||||
for ($i = 0; $i < 32; $i++) {
|
||||
$string .= chr($i);
|
||||
}
|
||||
$this->assertIdentical(' ', search_simplify($string), 'Search simplify works for ASCII control characters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that search_simplify() does the right thing with punctuation.
|
||||
*/
|
||||
function testSearchSimplifyPunctuation() {
|
||||
$cases = array(
|
||||
array('20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'),
|
||||
array('great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'),
|
||||
array('very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'),
|
||||
array('regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'),
|
||||
);
|
||||
|
||||
foreach ($cases as $case) {
|
||||
$out = trim(search_simplify($case[0]));
|
||||
$this->assertEqual($out, $case[1], $case[2]);
|
||||
}
|
||||
}
|
||||
}
|
96
core/modules/search/src/Tests/SearchTestBase.php
Normal file
96
core/modules/search/src/Tests/SearchTestBase.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchTestBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Defines the common search test code.
|
||||
*/
|
||||
abstract class SearchTestBase extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('node', 'search', 'dblog');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create Basic page and Article node types.
|
||||
if ($this->profile != 'standard') {
|
||||
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
|
||||
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates submission of a form using GET instead of POST.
|
||||
*
|
||||
* Forms that use the GET method cannot be submitted with
|
||||
* WebTestBase::drupalPostForm(), which explicitly uses POST to submit the
|
||||
* form. So this method finds the form, verifies that it has input fields and
|
||||
* a submit button matching the inputs to this method, and then calls
|
||||
* WebTestBase::drupalGet() to simulate the form submission to the 'action'
|
||||
* URL of the form (if set, or the current URL if not).
|
||||
*
|
||||
* See WebTestBase::drupalPostForm() for more detailed documentation of the
|
||||
* function parameters.
|
||||
*
|
||||
* @param string $path
|
||||
* Location of the form to be submitted: either a Drupal path, absolute
|
||||
* path, or NULL to use the current page.
|
||||
* @param array $edit
|
||||
* Form field data to submit. Unlike drupalPostForm(), this does not support
|
||||
* file uploads.
|
||||
* @param string $submit
|
||||
* Value of the submit button to submit clicking. Unlike drupalPostForm(),
|
||||
* this does not support AJAX.
|
||||
* @param string $form_html_id
|
||||
* (optional) HTML ID of the form, to disambiguate.
|
||||
*/
|
||||
protected function submitGetForm($path, $edit, $submit, $form_html_id = NULL) {
|
||||
if (isset($path)) {
|
||||
$this->drupalGet($path);
|
||||
}
|
||||
|
||||
if ($this->parse()) {
|
||||
// Iterate over forms to find one that matches $edit and $submit.
|
||||
$edit_save = $edit;
|
||||
$xpath = '//form';
|
||||
if (!empty($form_html_id)) {
|
||||
$xpath .= "[@id='" . $form_html_id . "']";
|
||||
}
|
||||
$forms = $this->xpath($xpath);
|
||||
foreach ($forms as $form) {
|
||||
// Try to set the fields of this form as specified in $edit.
|
||||
$edit = $edit_save;
|
||||
$post = array();
|
||||
$upload = array();
|
||||
$submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form);
|
||||
if (!$edit && $submit_matches) {
|
||||
// Everything matched, so "submit" the form.
|
||||
$action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : NULL;
|
||||
$this->drupalGet($action, array('query' => $post));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We have not found a form which contained all fields of $edit and
|
||||
// the submit button.
|
||||
foreach ($edit as $name => $value) {
|
||||
$this->fail(SafeMarkup::format('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
|
||||
}
|
||||
$this->assertTrue($submit_matches, format_string('Found the @submit button', array('@submit' => $submit)));
|
||||
$this->fail(format_string('Found the requested form fields at @path', array('@path' => $path)));
|
||||
}
|
||||
}
|
||||
}
|
155
core/modules/search/src/Tests/SearchTokenizerTest.php
Normal file
155
core/modules/search/src/Tests/SearchTokenizerTest.php
Normal file
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\Tests\SearchTokenizerTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\search\Tests;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
|
||||
/**
|
||||
* Tests that CJK tokenizer works as intended.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchTokenizerTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Verifies that strings of CJK characters are tokenized.
|
||||
*
|
||||
* The search_simplify() function does special things with numbers, symbols,
|
||||
* and punctuation. So we only test that CJK characters that are not in these
|
||||
* character classes are tokenized properly. See PREG_CLASS_CKJ for more
|
||||
* information.
|
||||
*/
|
||||
function testTokenizer() {
|
||||
// Set the minimum word size to 1 (to split all CJK characters) and make
|
||||
// sure CJK tokenizing is turned on.
|
||||
$this->config('search.settings')
|
||||
->set('index.minimum_word_size', 1)
|
||||
->set('index.overlap_cjk', TRUE)
|
||||
->save();
|
||||
$this->refreshVariables();
|
||||
|
||||
// Create a string of CJK characters from various character ranges in
|
||||
// the Unicode tables.
|
||||
|
||||
// Beginnings of the character ranges.
|
||||
$starts = array(
|
||||
'CJK unified' => 0x4e00,
|
||||
'CJK Ext A' => 0x3400,
|
||||
'CJK Compat' => 0xf900,
|
||||
'Hangul Jamo' => 0x1100,
|
||||
'Hangul Ext A' => 0xa960,
|
||||
'Hangul Ext B' => 0xd7b0,
|
||||
'Hangul Compat' => 0x3131,
|
||||
'Half non-punct 1' => 0xff21,
|
||||
'Half non-punct 2' => 0xff41,
|
||||
'Half non-punct 3' => 0xff66,
|
||||
'Hangul Syllables' => 0xac00,
|
||||
'Hiragana' => 0x3040,
|
||||
'Katakana' => 0x30a1,
|
||||
'Katakana Ext' => 0x31f0,
|
||||
'CJK Reserve 1' => 0x20000,
|
||||
'CJK Reserve 2' => 0x30000,
|
||||
'Bomofo' => 0x3100,
|
||||
'Bomofo Ext' => 0x31a0,
|
||||
'Lisu' => 0xa4d0,
|
||||
'Yi' => 0xa000,
|
||||
);
|
||||
|
||||
// Ends of the character ranges.
|
||||
$ends = array(
|
||||
'CJK unified' => 0x9fcf,
|
||||
'CJK Ext A' => 0x4dbf,
|
||||
'CJK Compat' => 0xfaff,
|
||||
'Hangul Jamo' => 0x11ff,
|
||||
'Hangul Ext A' => 0xa97f,
|
||||
'Hangul Ext B' => 0xd7ff,
|
||||
'Hangul Compat' => 0x318e,
|
||||
'Half non-punct 1' => 0xff3a,
|
||||
'Half non-punct 2' => 0xff5a,
|
||||
'Half non-punct 3' => 0xffdc,
|
||||
'Hangul Syllables' => 0xd7af,
|
||||
'Hiragana' => 0x309f,
|
||||
'Katakana' => 0x30ff,
|
||||
'Katakana Ext' => 0x31ff,
|
||||
'CJK Reserve 1' => 0x2fffd,
|
||||
'CJK Reserve 2' => 0x3fffd,
|
||||
'Bomofo' => 0x312f,
|
||||
'Bomofo Ext' => 0x31b7,
|
||||
'Lisu' => 0xa4fd,
|
||||
'Yi' => 0xa48f,
|
||||
);
|
||||
|
||||
// Generate characters consisting of starts, midpoints, and ends.
|
||||
$chars = array();
|
||||
$charcodes = array();
|
||||
foreach ($starts as $key => $value) {
|
||||
$charcodes[] = $starts[$key];
|
||||
$chars[] = $this->code2utf($starts[$key]);
|
||||
$mid = round(0.5 * ($starts[$key] + $ends[$key]));
|
||||
$charcodes[] = $mid;
|
||||
$chars[] = $this->code2utf($mid);
|
||||
$charcodes[] = $ends[$key];
|
||||
$chars[] = $this->code2utf($ends[$key]);
|
||||
}
|
||||
|
||||
// Merge into a string and tokenize.
|
||||
$string = implode('', $chars);
|
||||
$out = trim(search_simplify($string));
|
||||
$expected = Unicode::strtolower(implode(' ', $chars));
|
||||
|
||||
// Verify that the output matches what we expect.
|
||||
$this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that strings of non-CJK characters are not tokenized.
|
||||
*
|
||||
* This is just a sanity check - it verifies that strings of letters are
|
||||
* not tokenized.
|
||||
*/
|
||||
function testNoTokenizer() {
|
||||
// Set the minimum word size to 1 (to split all CJK characters) and make
|
||||
// sure CJK tokenizing is turned on.
|
||||
$this->config('search.settings')
|
||||
->set('index.minimum_word_size', 1)
|
||||
->set('index.overlap_cjk', TRUE)
|
||||
->save();
|
||||
$this->refreshVariables();
|
||||
|
||||
$letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
$out = trim(search_simplify($letters));
|
||||
|
||||
$this->assertEqual($letters, $out, 'Letters are not CJK tokenized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Like PHP chr() function, but for unicode characters.
|
||||
*
|
||||
* chr() only works for ASCII characters up to character 255. This function
|
||||
* converts a number to the corresponding unicode character. Adapted from
|
||||
* functions supplied in comments on several functions on php.net.
|
||||
*/
|
||||
function code2utf($num) {
|
||||
if ($num < 128) {
|
||||
return chr($num);
|
||||
}
|
||||
|
||||
if ($num < 2048) {
|
||||
return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
|
||||
}
|
||||
|
||||
if ($num < 65536) {
|
||||
return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
|
||||
}
|
||||
|
||||
if ($num < 2097152) {
|
||||
return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
91
core/modules/search/src/ViewsSearchQuery.php
Normal file
91
core/modules/search/src/ViewsSearchQuery.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search\ViewsSearchQuery.
|
||||
*/
|
||||
|
||||
namespace Drupal\search;
|
||||
|
||||
use Drupal\Core\Database\Query\Condition;
|
||||
|
||||
/**
|
||||
* Extends the core SearchQuery to be able to gets its protected values.
|
||||
*/
|
||||
class ViewsSearchQuery extends SearchQuery {
|
||||
|
||||
/**
|
||||
* Returns the conditions property.
|
||||
*
|
||||
* @return array
|
||||
* The query conditions.
|
||||
*/
|
||||
public function &conditions() {
|
||||
return $this->conditions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the words property.
|
||||
*
|
||||
* @return array
|
||||
* The positive search keywords.
|
||||
*/
|
||||
public function words() {
|
||||
return $this->words;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the simple property.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if it is a simple query, and FALSE if it is complicated (phrases
|
||||
* or LIKE).
|
||||
*/
|
||||
public function simple() {
|
||||
return $this->simple;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the matches property.
|
||||
*
|
||||
* @return int
|
||||
* The number of matches needed.
|
||||
*/
|
||||
public function matches() {
|
||||
return $this->matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes and returns the protected parseSearchExpression method.
|
||||
*/
|
||||
public function publicParseSearchExpression() {
|
||||
return $this->parseSearchExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the original condition with a custom one from views recursively.
|
||||
*
|
||||
* @param string $search
|
||||
* The searched value.
|
||||
* @param string $replace
|
||||
* The value which replaces the search value.
|
||||
* @param \Drupal\Core\Database\Query\Condition $condition
|
||||
* The query condition in which the string is replaced.
|
||||
*/
|
||||
function conditionReplaceString($search, $replace, &$condition) {
|
||||
if ($condition['field'] instanceof Condition) {
|
||||
$conditions =& $condition['field']->conditions();
|
||||
foreach ($conditions as $key => &$subcondition) {
|
||||
if (is_numeric($key)) {
|
||||
// As conditions can have subconditions, for example db_or(), the
|
||||
// function has to be called recursively.
|
||||
$this->conditionReplaceString($search, $replace, $subcondition);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$condition['field'] = str_replace($search, $replace, $condition['field']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
71
core/modules/search/templates/search-result.html.twig
Normal file
71
core/modules/search/templates/search-result.html.twig
Normal file
|
@ -0,0 +1,71 @@
|
|||
{#
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation for displaying a single search result.
|
||||
*
|
||||
* This template renders a single search result. The list of results is
|
||||
* rendered using '#theme' => 'item_list', with suggestions of:
|
||||
* - item_list__search_results__(plugin_id)
|
||||
* - item_list__search_results
|
||||
*
|
||||
* Available variables:
|
||||
* - url: URL of the result.
|
||||
* - title: Title of the result.
|
||||
* - snippet: A small preview of the result. Does not apply to user searches.
|
||||
* - info: String of all the meta information ready for print. Does not apply
|
||||
* to user searches.
|
||||
* - plugin_id: The machine-readable name of the plugin being executed,such
|
||||
* as "node_search" or "user_search".
|
||||
* - title_prefix: Additional output populated by modules, intended to be
|
||||
* displayed in front of the main title tag that appears in the template.
|
||||
* - title_suffix: Additional output populated by modules, intended to be
|
||||
* displayed after the main title tag that appears in the template.
|
||||
* - info_split: Contains same data as info, but split into separate parts.
|
||||
* - info_split.type: Node type (or item type string supplied by module).
|
||||
* - info_split.user: Author of the node linked to users profile. Depends
|
||||
* on permission.
|
||||
* - info_split.date: Last update of the node. Short formatted.
|
||||
* - info_split.comment: Number of comments output as "% comments", %
|
||||
* being the count. (Depends on comment.module).
|
||||
* @todo The info variable needs to be made drillable and each of these sub
|
||||
* items should instead be within info and renamed info.foo, info.bar, etc.
|
||||
*
|
||||
* Other variables:
|
||||
* - title_attributes: HTML attributes for the title.
|
||||
* - content_attributes: HTML attributes for the content.
|
||||
*
|
||||
* Since info_split is keyed, a direct print of the item is possible.
|
||||
* This array does not apply to user searches so it is recommended to check
|
||||
* for its existence before printing. The default keys of 'type', 'user' and
|
||||
* 'date' always exist for node searches. Modules may provide other data.
|
||||
* @code
|
||||
* {% if (info_split.comment) %}
|
||||
* <span class="info-comment">
|
||||
* {{ info_split.comment }}
|
||||
* </span>
|
||||
* {% endif %}
|
||||
* @endcode
|
||||
*
|
||||
* To check for all available data within info_split, use the code below.
|
||||
* @code
|
||||
* <pre>
|
||||
* {{ dump(info_split) }}
|
||||
* </pre>
|
||||
* @endcode
|
||||
*
|
||||
* @see template_preprocess_search_result()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
#}
|
||||
{{ title_prefix }}
|
||||
<h3{{ title_attributes }}>
|
||||
<a href="{{ url }}">{{ title }}</a>
|
||||
</h3>
|
||||
{{ title_suffix }}
|
||||
{% if snippet %}
|
||||
<p{{ content_attributes }}>{{ snippet }}</p>
|
||||
{% endif %}
|
||||
{% if info %}
|
||||
<p>{{ info }}</p>
|
||||
{% endif %}
|
333
core/modules/search/tests/UnicodeTest.txt
Normal file
333
core/modules/search/tests/UnicodeTest.txt
Normal file
|
@ -0,0 +1,333 @@
|
|||
|
||||
!"#$%&'()*+,-./
|
||||
0123456789
|
||||
:;<=>?@
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
[\]^_`
|
||||
abcdefghijklmnopqrstuvwxyz
|
||||
{|}~
¡¢£¤¥¦§¨©
|
||||
ª
|
||||
«¬®¯°±
|
||||
²³
|
||||
´
|
||||
µ
|
||||
¶·¸
|
||||
¹º
|
||||
»
|
||||
¼½¾
|
||||
¿
|
||||
ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ
|
||||
×
|
||||
ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö
|
||||
÷
|
||||
øùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯʰʱʲʳʴʵʶʷʸʹʺʻʼʽʾʿˀˁ
|
||||
˂˃˄˅
|
||||
ˆˇˈˉˊˋˌˍˎˏːˑ
|
||||
˒˓˔˕˖˗˘˙˚˛˜˝˞˟
|
||||
ˠˡˢˣˤ
|
||||
˥˦˧˨˩˪˫
|
||||
ˬ
|
||||
˭
|
||||
ˮ
|
||||
˯˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿
|
||||
̴̵̶̷̸̡̢̧̨̛̖̗̘̙̜̝̞̟̠̣̤̥̦̩̪̫̬̭̮̯̰̱̲̳̹̺̻̼͇͈͉͍͎̀́̂̃̄̅̆̇̈̉̊̋̌̍̎̏̐̑̒̓̔̽̾̿̀́͂̓̈́͆͊͋͌̕̚ͅ͏͓͔͕͖͙͚͐͑͒͗͛ͣͤͥͦͧͨͩͪͫͬͭͮͯ͘͜͟͢͝͞͠͡ͰͱͲͳʹ
|
||||
͵
|
||||
Ͷͷͺͻͼͽ
|
||||
;΄΅
|
||||
Ά
|
||||
·
|
||||
ΈΉΊΌΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώϏϐϑϒϓϔϕϖϗϘϙϚϛϜϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϲϳϴϵ
|
||||
϶
|
||||
ϷϸϹϺϻϼϽϾϿЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ
|
||||
҂
|
||||
҃҄҅҆҇҈҉ҊҋҌҍҎҏҐґҒғҔҕҖҗҘҙҚқҜҝҞҟҠҡҢңҤҥҦҧҨҩҪҫҬҭҮүҰұҲҳҴҵҶҷҸҹҺһҼҽҾҿӀӁӂӃӄӅӆӇӈӉӊӋӌӍӎӏӐӑӒӓӔӕӖӗӘәӚӛӜӝӞӟӠӡӢӣӤӥӦӧӨөӪӫӬӭӮӯӰӱӲӳӴӵӶӷӸӹӺӻӼӽӾӿԀԁԂԃԄԅԆԇԈԉԊԋԌԍԎԏԐԑԒԓԔԕԖԗԘԙԚԛԜԝԞԟԠԡԢԣԤԥԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖՙ
|
||||
՚՛՜՝՞՟
|
||||
աբգդեզէըթժիլխծկհձղճմյնշոչպջռսվտրցւփքօֆև
|
||||
։֊
|
||||
ְֱֲֳִֵֶַָֹֺֻּֽ֑֖֛֢֣֤֥֦֧֪֚֭֮֒֓֔֕֗֘֙֜֝֞֟֠֡֨֩֫֬֯
|
||||
־
|
||||
ֿ
|
||||
׀
|
||||
ׁׂ
|
||||
׃
|
||||
ׅׄ
|
||||
׆
|
||||
ׇאבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ
|
||||
׳״؆؇؈؉؊؋،؍؎؏
|
||||
ؘؙؚؐؑؒؓؔؕؖؗ
|
||||
؛؞؟
|
||||
ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـفقكلمنهوىيًٌٍَُِّْٕٖٜٓٔٗ٘ٙٚٛٝٞ٠١٢٣٤٥٦٧٨٩
|
||||
٪٫٬٭
|
||||
ٮٯٰٱٲٳٴٵٶٷٸٹٺٻټٽپٿڀځڂڃڄڅچڇڈډڊڋڌڍڎڏڐڑڒړڔڕږڗژڙښڛڜڝڞڟڠڡڢڣڤڥڦڧڨکڪګڬڭڮگڰڱڲڳڴڵڶڷڸڹںڻڼڽھڿۀہۂۃۄۅۆۇۈۉۊۋیۍێۏېۑےۓ
|
||||
۔
|
||||
ەۖۗۘۙۚۛۜ
|
||||
|
||||
۞ۣ۟۠ۡۢۤۥۦۧۨ
|
||||
۩
|
||||
۪ۭ۫۬ۮۯ۰۱۲۳۴۵۶۷۸۹ۺۻۼ
|
||||
۽۾
|
||||
ۿ
|
||||
܀܁܂܃܄܅܆܇܈܉܊܋܌܍
|
||||
ܐܑܒܓܔܕܖܗܘܙܚܛܜܝܞܟܠܡܢܣܤܥܦܧܨܩܪܫܬܭܮܯܱܴܷܸܹܻܼܾ݂݄݆݈ܰܲܳܵܶܺܽܿ݀݁݃݅݇݉݊ݍݎݏݐݑݒݓݔݕݖݗݘݙݚݛݜݝݞݟݠݡݢݣݤݥݦݧݨݩݪݫݬݭݮݯݰݱݲݳݴݵݶݷݸݹݺݻݼݽݾݿހށނރބޅކއވމފދތލގޏސޑޒޓޔޕޖޗޘޙޚޛޜޝޞޟޠޡޢޣޤޥަާިީުޫެޭޮޯްޱ߀߁߂߃߄߅߆߇߈߉ߊߋߌߍߎߏߐߑߒߓߔߕߖߗߘߙߚߛߜߝߞߟߠߡߢߣߤߥߦߧߨߩߪ߲߫߬߭߮߯߰߱߳ߴߵ
|
||||
߶߷߸߹
|
||||
ߺࠀࠁࠂࠃࠄࠅࠆࠇࠈࠉࠊࠋࠌࠍࠎࠏࠐࠑࠒࠓࠔࠕࠖࠗ࠘࠙ࠚࠛࠜࠝࠞࠟࠠࠡࠢࠣࠤࠥࠦࠧࠨࠩࠪࠫࠬ࠭
|
||||
࠰࠱࠲࠳࠴࠵࠶࠷࠸࠹࠺࠻࠼࠽࠾
|
||||
ऀँंःऄअआइईउऊऋऌऍऎएऐऑऒओऔकखगघङचछजझञटठडढणतथदधनऩपफबभमयरऱलळऴवशषसह़ऽािीुूृॄॅॆेैॉॊोौ्ॎॐ॒॑॓॔ॕक़ख़ग़ज़ड़ढ़फ़य़ॠॡॢॣ
|
||||
।॥
|
||||
०१२३४५६७८९
|
||||
॰
|
||||
ॱॲॹॺॻॼॽॾॿঁংঃঅআইঈউঊঋঌএঐওঔকখগঘঙচছজঝঞটঠডঢণতথদধনপফবভমযরলশষসহ়ঽািীুূৃৄেৈোৌ্ৎৗড়ঢ়য়ৠৡৢৣ০১২৩৪৫৬৭৮৯ৰৱ
|
||||
৲৳
|
||||
৴৵৶৷৸৹
|
||||
৺৻
|
||||
ਁਂਃਅਆਇਈਉਊਏਐਓਔਕਖਗਘਙਚਛਜਝਞਟਠਡਢਣਤਥਦਧਨਪਫਬਭਮਯਰਲਲ਼ਵਸ਼ਸਹ਼ਾਿੀੁੂੇੈੋੌ੍ੑਖ਼ਗ਼ਜ਼ੜਫ਼੦੧੨੩੪੫੬੭੮੯ੰੱੲੳੴੵઁંઃઅઆઇઈઉઊઋઌઍએઐઑઓઔકખગઘઙચછજઝઞટઠડઢણતથદધનપફબભમયરલળવશષસહ઼ઽાિીુૂૃૄૅેૈૉોૌ્ૐૠૡૢૣ૦૧૨૩૪૫૬૭૮૯
|
||||
૱
|
||||
ଁଂଃଅଆଇଈଉଊଋଌଏଐଓଔକଖଗଘଙଚଛଜଝଞଟଠଡଢଣତଥଦଧନପଫବଭମଯରଲଳଵଶଷସହ଼ଽାିୀୁୂୃୄେୈୋୌ୍ୖୗଡ଼ଢ଼ୟୠୡୢୣ୦୧୨୩୪୫୬୭୮୯
|
||||
୰
|
||||
ୱஂஃஅஆஇஈஉஊஎஏஐஒஓஔகஙசஜஞடணதநனபமயரறலளழவஶஷஸஹாிீுூெேைொோௌ்ௐௗ௦௧௨௩௪௫௬௭௮௯௰௱௲
|
||||
௳௴௵௶௷௸௹௺
|
||||
ఁంఃఅఆఇఈఉఊఋఌఎఏఐఒఓఔకఖగఘఙచఛజఝఞటఠడఢణతథదధనపఫబభమయరఱలళవశషసహఽాిీుూృౄెేైొోౌ్ౕౖౘౙౠౡౢౣ౦౧౨౩౪౫౬౭౮౯౸౹౺౻౼౽౾
|
||||
౿
|
||||
ಂಃಅಆಇಈಉಊಋಌಎಏಐಒಓಔಕಖಗಘಙಚಛಜಝಞಟಠಡಢಣತಥದಧನಪಫಬಭಮಯರಱಲಳವಶಷಸಹ಼ಽಾಿೀುೂೃೄೆೇೈೊೋೌ್ೕೖೞೠೡೢೣ೦೧೨೩೪೫೬೭೮೯
|
||||
ೱೲ
|
||||
ംഃഅആഇഈഉഊഋഌഎഏഐഒഓഔകഖഗഘങചഛജഝഞടഠഡഢണതഥദധനപഫബഭമയരറലളഴവശഷസഹഽാിീുൂൃൄെേൈൊോൌ്ൗൠൡൢൣ൦൧൨൩൪൫൬൭൮൯൰൱൲൳൴൵
|
||||
൹
|
||||
ൺൻർൽൾൿංඃඅආඇඈඉඊඋඌඍඎඏඐඑඒඓඔඕඖකඛගඝඞඟචඡජඣඤඥඦටඨඩඪණඬතථදධනඳපඵබභමඹයරලවශෂසහළෆ්ාැෑිීුූෘෙේෛොෝෞෟෲෳ
|
||||
෴
|
||||
กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู
|
||||
฿
|
||||
เแโใไๅๆ็่้๊๋์ํ๎
|
||||
๏
|
||||
๐๑๒๓๔๕๖๗๘๙
|
||||
๚๛
|
||||
ກຂຄງຈຊຍດຕຖທນບປຜຝພຟມຢຣລວສຫອຮຯະັາຳິີຶືຸູົຼຽເແໂໃໄໆ່້໊໋໌ໍ໐໑໒໓໔໕໖໗໘໙ໜໝༀ
|
||||
༁༂༃༄༅༆༇༈༉༊་༌།༎༏༐༑༒༓༔༕༖༗
|
||||
༘༙
|
||||
༚༛༜༝༞༟
|
||||
༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳
|
||||
༴
|
||||
༵
|
||||
༶
|
||||
༷
|
||||
༸
|
||||
༹
|
||||
༺༻༼༽
|
||||
༾༿ཀཁགགྷངཅཆཇཉཊཋཌཌྷཎཏཐདདྷནཔཕབབྷམཙཚཛཛྷཝཞཟའཡརལཤཥསཧཨཀྵཪཫཬཱཱཱིིུུྲྀཷླྀཹེཻོཽཾཿ྄ཱྀྀྂྃ
|
||||
྅
|
||||
྆྇ྈྉྊྋྐྑྒྒྷྔྕྖྗྙྚྛྜྜྷྞྟྠྡྡྷྣྤྥྦྦྷྨྩྪྫྫྷྭྮྯྰྱྲླྴྵྶྷྸྐྵྺྻྼ
|
||||
྾྿࿀࿁࿂࿃࿄࿅
|
||||
࿆
|
||||
࿇࿈࿉࿊࿋࿌࿎࿏࿐࿑࿒࿓࿔࿕࿖࿗࿘
|
||||
ကခဂဃငစဆဇဈဉညဋဌဍဎဏတထဒဓနပဖဗဘမယရလဝသဟဠအဢဣဤဥဦဧဨဩဪါာိီုူေဲဳဴဵံ့း္်ျြွှဿ၀၁၂၃၄၅၆၇၈၉
|
||||
၊။၌၍၎၏
|
||||
ၐၑၒၓၔၕၖၗၘၙၚၛၜၝၞၟၠၡၢၣၤၥၦၧၨၩၪၫၬၭၮၯၰၱၲၳၴၵၶၷၸၹၺၻၼၽၾၿႀႁႂႃႄႅႆႇႈႉႊႋႌႍႎႏ႐႑႒႓႔႕႖႗႘႙ႚႛႜႝ
|
||||
႞႟
|
||||
ႠႡႢႣႤႥႦႧႨႩႪႫႬႭႮႯႰႱႲႳႴႵႶႷႸႹႺႻႼႽႾႿჀჁჂჃჄჅაბგდევზთიკლმნოპჟრსტუფქღყშჩცძწჭხჯჰჱჲჳჴჵჶჷჸჹჺ
|
||||
჻
|
||||
ჼᄀᄁᄂᄃᄄᄅᄆᄇᄈᄉᄊᄋᄌᄍᄎᄏᄐᄑᄒᄓᄔᄕᄖᄗᄘᄙᄚᄛᄜᄝᄞᄟᄠᄡᄢᄣᄤᄥᄦᄧᄨᄩᄪᄫᄬᄭᄮᄯᄰᄱᄲᄳᄴᄵᄶᄷᄸᄹᄺᄻᄼᄽᄾᄿᅀᅁᅂᅃᅄᅅᅆᅇᅈᅉᅊᅋᅌᅍᅎᅏᅐᅑᅒᅓᅔᅕᅖᅗᅘᅙᅚᅛᅜᅝᅞᅟᅠᅡᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵᅶᅷᅸᅹᅺᅻᅼᅽᅾᅿᆀᆁᆂᆃᆄᆅᆆᆇᆈᆉᆊᆋᆌᆍᆎᆏᆐᆑᆒᆓᆔᆕᆖᆗᆘᆙᆚᆛᆜᆝᆞᆟᆠᆡᆢᆣᆤᆥᆦᆧᆨᆩᆪᆫᆬᆭᆮᆯᆰᆱᆲᆳᆴᆵᆶᆷᆸᆹᆺᆻᆼᆽᆾᆿᇀᇁᇂᇃᇄᇅᇆᇇᇈᇉᇊᇋᇌᇍᇎᇏᇐᇑᇒᇓᇔᇕᇖᇗᇘᇙᇚᇛᇜᇝᇞᇟᇠᇡᇢᇣᇤᇥᇦᇧᇨᇩᇪᇫᇬᇭᇮᇯᇰᇱᇲᇳᇴᇵᇶᇷᇸᇹᇺᇻᇼᇽᇾᇿሀሁሂሃሄህሆሇለሉሊላሌልሎሏሐሑሒሓሔሕሖሗመሙሚማሜምሞሟሠሡሢሣሤሥሦሧረሩሪራሬርሮሯሰሱሲሳሴስሶሷሸሹሺሻሼሽሾሿቀቁቂቃቄቅቆቇቈቊቋቌቍቐቑቒቓቔቕቖቘቚቛቜቝበቡቢባቤብቦቧቨቩቪቫቬቭቮቯተቱቲታቴትቶቷቸቹቺቻቼችቾቿኀኁኂኃኄኅኆኇኈኊኋኌኍነኑኒናኔንኖኗኘኙኚኛኜኝኞኟአኡኢኣኤእኦኧከኩኪካኬክኮኯኰኲኳኴኵኸኹኺኻኼኽኾዀዂዃዄዅወዉዊዋዌውዎዏዐዑዒዓዔዕዖዘዙዚዛዜዝዞዟዠዡዢዣዤዥዦዧየዩዪያዬይዮዯደዱዲዳዴድዶዷዸዹዺዻዼዽዾዿጀጁጂጃጄጅጆጇገጉጊጋጌግጎጏጐጒጓጔጕጘጙጚጛጜጝጞጟጠጡጢጣጤጥጦጧጨጩጪጫጬጭጮጯጰጱጲጳጴጵጶጷጸጹጺጻጼጽጾጿፀፁፂፃፄፅፆፇፈፉፊፋፌፍፎፏፐፑፒፓፔፕፖፗፘፙፚ፟
|
||||
፠፡።፣፤፥፦፧፨
|
||||
፩፪፫፬፭፮፯፰፱፲፳፴፵፶፷፸፹፺፻፼ᎀᎁᎂᎃᎄᎅᎆᎇᎈᎉᎊᎋᎌᎍᎎᎏ
|
||||
᎐᎑᎒᎓᎔᎕᎖᎗᎘᎙
|
||||
ᎠᎡᎢᎣᎤᎥᎦᎧᎨᎩᎪᎫᎬᎭᎮᎯᎰᎱᎲᎳᎴᎵᎶᎷᎸᎹᎺᎻᎼᎽᎾᎿᏀᏁᏂᏃᏄᏅᏆᏇᏈᏉᏊᏋᏌᏍᏎᏏᏐᏑᏒᏓᏔᏕᏖᏗᏘᏙᏚᏛᏜᏝᏞᏟᏠᏡᏢᏣᏤᏥᏦᏧᏨᏩᏪᏫᏬᏭᏮᏯᏰᏱᏲᏳᏴ
|
||||
᐀
|
||||
ᐁᐂᐃᐄᐅᐆᐇᐈᐉᐊᐋᐌᐍᐎᐏᐐᐑᐒᐓᐔᐕᐖᐗᐘᐙᐚᐛᐜᐝᐞᐟᐠᐡᐢᐣᐤᐥᐦᐧᐨᐩᐪᐫᐬᐭᐮᐯᐰᐱᐲᐳᐴᐵᐶᐷᐸᐹᐺᐻᐼᐽᐾᐿᑀᑁᑂᑃᑄᑅᑆᑇᑈᑉᑊᑋᑌᑍᑎᑏᑐᑑᑒᑓᑔᑕᑖᑗᑘᑙᑚᑛᑜᑝᑞᑟᑠᑡᑢᑣᑤᑥᑦᑧᑨᑩᑪᑫᑬᑭᑮᑯᑰᑱᑲᑳᑴᑵᑶᑷᑸᑹᑺᑻᑼᑽᑾᑿᒀᒁᒂᒃᒄᒅᒆᒇᒈᒉᒊᒋᒌᒍᒎᒏᒐᒑᒒᒓᒔᒕᒖᒗᒘᒙᒚᒛᒜᒝᒞᒟᒠᒡᒢᒣᒤᒥᒦᒧᒨᒩᒪᒫᒬᒭᒮᒯᒰᒱᒲᒳᒴᒵᒶᒷᒸᒹᒺᒻᒼᒽᒾᒿᓀᓁᓂᓃᓄᓅᓆᓇᓈᓉᓊᓋᓌᓍᓎᓏᓐᓑᓒᓓᓔᓕᓖᓗᓘᓙᓚᓛᓜᓝᓞᓟᓠᓡᓢᓣᓤᓥᓦᓧᓨᓩᓪᓫᓬᓭᓮᓯᓰᓱᓲᓳᓴᓵᓶᓷᓸᓹᓺᓻᓼᓽᓾᓿᔀᔁᔂᔃᔄᔅᔆᔇᔈᔉᔊᔋᔌᔍᔎᔏᔐᔑᔒᔓᔔᔕᔖᔗᔘᔙᔚᔛᔜᔝᔞᔟᔠᔡᔢᔣᔤᔥᔦᔧᔨᔩᔪᔫᔬᔭᔮᔯᔰᔱᔲᔳᔴᔵᔶᔷᔸᔹᔺᔻᔼᔽᔾᔿᕀᕁᕂᕃᕄᕅᕆᕇᕈᕉᕊᕋᕌᕍᕎᕏᕐᕑᕒᕓᕔᕕᕖᕗᕘᕙᕚᕛᕜᕝᕞᕟᕠᕡᕢᕣᕤᕥᕦᕧᕨᕩᕪᕫᕬᕭᕮᕯᕰᕱᕲᕳᕴᕵᕶᕷᕸᕹᕺᕻᕼᕽᕾᕿᖀᖁᖂᖃᖄᖅᖆᖇᖈᖉᖊᖋᖌᖍᖎᖏᖐᖑᖒᖓᖔᖕᖖᖗᖘᖙᖚᖛᖜᖝᖞᖟᖠᖡᖢᖣᖤᖥᖦᖧᖨᖩᖪᖫᖬᖭᖮᖯᖰᖱᖲᖳᖴᖵᖶᖷᖸᖹᖺᖻᖼᖽᖾᖿᗀᗁᗂᗃᗄᗅᗆᗇᗈᗉᗊᗋᗌᗍᗎᗏᗐᗑᗒᗓᗔᗕᗖᗗᗘᗙᗚᗛᗜᗝᗞᗟᗠᗡᗢᗣᗤᗥᗦᗧᗨᗩᗪᗫᗬᗭᗮᗯᗰᗱᗲᗳᗴᗵᗶᗷᗸᗹᗺᗻᗼᗽᗾᗿᘀᘁᘂᘃᘄᘅᘆᘇᘈᘉᘊᘋᘌᘍᘎᘏᘐᘑᘒᘓᘔᘕᘖᘗᘘᘙᘚᘛᘜᘝᘞᘟᘠᘡᘢᘣᘤᘥᘦᘧᘨᘩᘪᘫᘬᘭᘮᘯᘰᘱᘲᘳᘴᘵᘶᘷᘸᘹᘺᘻᘼᘽᘾᘿᙀᙁᙂᙃᙄᙅᙆᙇᙈᙉᙊᙋᙌᙍᙎᙏᙐᙑᙒᙓᙔᙕᙖᙗᙘᙙᙚᙛᙜᙝᙞᙟᙠᙡᙢᙣᙤᙥᙦᙧᙨᙩᙪᙫᙬ
|
||||
᙭᙮
|
||||
ᙯᙰᙱᙲᙳᙴᙵᙶᙷᙸᙹᙺᙻᙼᙽᙾᙿ
|
||||
|
||||
ᚁᚂᚃᚄᚅᚆᚇᚈᚉᚊᚋᚌᚍᚎᚏᚐᚑᚒᚓᚔᚕᚖᚗᚘᚙᚚ
|
||||
᚛᚜
|
||||
ᚠᚡᚢᚣᚤᚥᚦᚧᚨᚩᚪᚫᚬᚭᚮᚯᚰᚱᚲᚳᚴᚵᚶᚷᚸᚹᚺᚻᚼᚽᚾᚿᛀᛁᛂᛃᛄᛅᛆᛇᛈᛉᛊᛋᛌᛍᛎᛏᛐᛑᛒᛓᛔᛕᛖᛗᛘᛙᛚᛛᛜᛝᛞᛟᛠᛡᛢᛣᛤᛥᛦᛧᛨᛩᛪ
|
||||
᛫᛬᛭
|
||||
ᛮᛯᛰᜀᜁᜂᜃᜄᜅᜆᜇᜈᜉᜊᜋᜌᜎᜏᜐᜑᜒᜓ᜔ᜠᜡᜢᜣᜤᜥᜦᜧᜨᜩᜪᜫᜬᜭᜮᜯᜰᜱᜲᜳ᜴
|
||||
᜵᜶
|
||||
ᝀᝁᝂᝃᝄᝅᝆᝇᝈᝉᝊᝋᝌᝍᝎᝏᝐᝑᝒᝓᝠᝡᝢᝣᝤᝥᝦᝧᝨᝩᝪᝫᝬᝮᝯᝰᝲᝳកខគឃងចឆជឈញដឋឌឍណតថទធនបផពភមយរលវឝឞសហឡអឣឤឥឦឧឨឩឪឫឬឭឮឯឰឱឲឳ
|
||||
឴឵
|
||||
ាិីឹឺុូួើឿៀេែៃោៅំះៈ៉៊់៌៍៎៏័៑្៓
|
||||
។៕៖
|
||||
ៗ
|
||||
៘៙៚៛
|
||||
ៜ៝០១២៣៤៥៦៧៨៩៰៱៲៳៴៵៶៷៸៹
|
||||
᠀᠁᠂᠃᠄᠅᠆᠇᠈᠉᠊
|
||||
᠋᠌᠍
|
||||
|
||||
᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙ᠠᠡᠢᠣᠤᠥᠦᠧᠨᠩᠪᠫᠬᠭᠮᠯᠰᠱᠲᠳᠴᠵᠶᠷᠸᠹᠺᠻᠼᠽᠾᠿᡀᡁᡂᡃᡄᡅᡆᡇᡈᡉᡊᡋᡌᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡘᡙᡚᡛᡜᡝᡞᡟᡠᡡᡢᡣᡤᡥᡦᡧᡨᡩᡪᡫᡬᡭᡮᡯᡰᡱᡲᡳᡴᡵᡶᡷᢀᢁᢂᢃᢄᢅᢆᢇᢈᢉᢊᢋᢌᢍᢎᢏᢐᢑᢒᢓᢔᢕᢖᢗᢘᢙᢚᢛᢜᢝᢞᢟᢠᢡᢢᢣᢤᢥᢦᢧᢨᢩᢪᢰᢱᢲᢳᢴᢵᢶᢷᢸᢹᢺᢻᢼᢽᢾᢿᣀᣁᣂᣃᣄᣅᣆᣇᣈᣉᣊᣋᣌᣍᣎᣏᣐᣑᣒᣓᣔᣕᣖᣗᣘᣙᣚᣛᣜᣝᣞᣟᣠᣡᣢᣣᣤᣥᣦᣧᣨᣩᣪᣫᣬᣭᣮᣯᣰᣱᣲᣳᣴᣵᤀᤁᤂᤃᤄᤅᤆᤇᤈᤉᤊᤋᤌᤍᤎᤏᤐᤑᤒᤓᤔᤕᤖᤗᤘᤙᤚᤛᤜᤠᤡᤢᤣᤤᤥᤦᤧᤨᤩᤪᤫᤰᤱᤲᤳᤴᤵᤶᤷᤸ᤻᤹᤺
|
||||
᥀᥄᥅
|
||||
᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏ᥐᥑᥒᥓᥔᥕᥖᥗᥘᥙᥚᥛᥜᥝᥞᥟᥠᥡᥢᥣᥤᥥᥦᥧᥨᥩᥪᥫᥬᥭᥰᥱᥲᥳᥴᦀᦁᦂᦃᦄᦅᦆᦇᦈᦉᦊᦋᦌᦍᦎᦏᦐᦑᦒᦓᦔᦕᦖᦗᦘᦙᦚᦛᦜᦝᦞᦟᦠᦡᦢᦣᦤᦥᦦᦧᦨᦩᦪᦫᦰᦱᦲᦳᦴᦵᦶᦷᦸᦹᦺᦻᦼᦽᦾᦿᧀᧁᧂᧃᧄᧅᧆᧇᧈᧉ᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙᧚
|
||||
᧞᧟᧠᧡᧢᧣᧤᧥᧦᧧᧨᧩᧪᧫᧬᧭᧮᧯᧰᧱᧲᧳᧴᧵᧶᧷᧸᧹᧺᧻᧼᧽᧾᧿
|
||||
ᨀᨁᨂᨃᨄᨅᨆᨇᨈᨉᨊᨋᨌᨍᨎᨏᨐᨑᨒᨓᨔᨕᨖᨘᨗᨙᨚᨛ
|
||||
᨞᨟
|
||||
ᨠᨡᨢᨣᨤᨥᨦᨧᨨᨩᨪᨫᨬᨭᨮᨯᨰᨱᨲᨳᨴᨵᨶᨷᨸᨹᨺᨻᨼᨽᨾᨿᩀᩁᩂᩃᩄᩅᩆᩇᩈᩉᩊᩋᩌᩍᩎᩏᩐᩑᩒᩓᩔᩕᩖᩗᩘᩙᩚᩛᩜᩝᩞ᩠ᩡᩢᩣᩤᩥᩦᩧᩨᩩᩪᩫᩬᩭᩮᩯᩰᩱᩲᩳᩴ᩿᩵᩶᩷᩸᩹᩺᩻᩼᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙
|
||||
᪠᪡᪢᪣᪤᪥᪦
|
||||
ᪧ
|
||||
᪨᪩᪪᪫᪬᪭
|
||||
ᬀᬁᬂᬃᬄᬅᬆᬇᬈᬉᬊᬋᬌᬍᬎᬏᬐᬑᬒᬓᬔᬕᬖᬗᬘᬙᬚᬛᬜᬝᬞᬟᬠᬡᬢᬣᬤᬥᬦᬧᬨᬩᬪᬫᬬᬭᬮᬯᬰᬱᬲᬳ᬴ᬵᬶᬷᬸᬹᬺᬻᬼᬽᬾᬿᭀᭁᭂᭃ᭄ᭅᭆᭇᭈᭉᭊᭋ᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙
|
||||
᭚᭛᭜᭝᭞᭟᭠᭡᭢᭣᭤᭥᭦᭧᭨᭩᭪
|
||||
᭬᭫᭭᭮᭯᭰᭱᭲᭳
|
||||
᭴᭵᭶᭷᭸᭹᭺᭻᭼
|
||||
ᮀᮁᮂᮃᮄᮅᮆᮇᮈᮉᮊᮋᮌᮍᮎᮏᮐᮑᮒᮓᮔᮕᮖᮗᮘᮙᮚᮛᮜᮝᮞᮟᮠᮡᮢᮣᮤᮥᮦᮧᮨᮩ᮪ᮮᮯ᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹ᰀᰁᰂᰃᰄᰅᰆᰇᰈᰉᰊᰋᰌᰍᰎᰏᰐᰑᰒᰓᰔᰕᰖᰗᰘᰙᰚᰛᰜᰝᰞᰟᰠᰡᰢᰣᰤᰥᰦᰧᰨᰩᰪᰫᰬᰭᰮᰯᰰᰱᰲᰳᰴᰵᰶ᰷
|
||||
᰻᰼᰽᰾᰿
|
||||
᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉ᱍᱎᱏ᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙ᱚᱛᱜᱝᱞᱟᱠᱡᱢᱣᱤᱥᱦᱧᱨᱩᱪᱫᱬᱭᱮᱯᱰᱱᱲᱳᱴᱵᱶᱷᱸᱹᱺᱻᱼᱽ
|
||||
᱾᱿
|
||||
᳐᳑᳒
|
||||
᳓
|
||||
᳔᳕᳖᳗᳘᳙᳜᳝᳞᳟᳚᳛᳠᳡᳢᳣᳤᳥᳦᳧᳨ᳩᳪᳫᳬ᳭ᳮᳯᳰᳱᳲᴀᴁᴂᴃᴄᴅᴆᴇᴈᴉᴊᴋᴌᴍᴎᴏᴐᴑᴒᴓᴔᴕᴖᴗᴘᴙᴚᴛᴜᴝᴞᴟᴠᴡᴢᴣᴤᴥᴦᴧᴨᴩᴪᴫᴬᴭᴮᴯᴰᴱᴲᴳᴴᴵᴶᴷᴸᴹᴺᴻᴼᴽᴾᴿᵀᵁᵂᵃᵄᵅᵆᵇᵈᵉᵊᵋᵌᵍᵎᵏᵐᵑᵒᵓᵔᵕᵖᵗᵘᵙᵚᵛᵜᵝᵞᵟᵠᵡᵢᵣᵤᵥᵦᵧᵨᵩᵪᵫᵬᵭᵮᵯᵰᵱᵲᵳᵴᵵᵶᵷᵸᵹᵺᵻᵼᵽᵾᵿᶀᶁᶂᶃᶄᶅᶆᶇᶈᶉᶊᶋᶌᶍᶎᶏᶐᶑᶒᶓᶔᶕᶖᶗᶘᶙᶚᶛᶜᶝᶞᶟᶠᶡᶢᶣᶤᶥᶦᶧᶨᶩᶪᶫᶬᶭᶮᶯᶰᶱᶲᶳᶴᶵᶶᶷᶸᶹᶺᶻᶼᶽᶾᶿ᷐᷎᷂᷊᷏᷽᷿᷀᷁᷃᷄᷅᷆᷇᷈᷉᷋᷌᷑᷒ᷓᷔᷕᷖᷗᷘᷙᷚᷛᷜᷝᷞᷟᷠᷡᷢᷣᷤᷥᷦ᷾᷍ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔḕḖḗḘḙḚḛḜḝḞḟḠḡḢḣḤḥḦḧḨḩḪḫḬḭḮḯḰḱḲḳḴḵḶḷḸḹḺḻḼḽḾḿṀṁṂṃṄṅṆṇṈṉṊṋṌṍṎṏṐṑṒṓṔṕṖṗṘṙṚṛṜṝṞṟṠṡṢṣṤṥṦṧṨṩṪṫṬṭṮṯṰṱṲṳṴṵṶṷṸṹṺṻṼṽṾṿẀẁẂẃẄẅẆẇẈẉẊẋẌẍẎẏẐẑẒẓẔẕẖẗẘẙẚẛẜẝẞẟẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹỺỻỼỽỾỿἀἁἂἃἄἅἆἇἈἉἊἋἌἍἎἏἐἑἒἓἔἕἘἙἚἛἜἝἠἡἢἣἤἥἦἧἨἩἪἫἬἭἮἯἰἱἲἳἴἵἶἷἸἹἺἻἼἽἾἿὀὁὂὃὄὅὈὉὊὋὌὍὐὑὒὓὔὕὖὗὙὛὝὟὠὡὢὣὤὥὦὧὨὩὪὫὬὭὮὯὰάὲέὴήὶίὸόὺύὼώᾀᾁᾂᾃᾄᾅᾆᾇᾈᾉᾊᾋᾌᾍᾎᾏᾐᾑᾒᾓᾔᾕᾖᾗᾘᾙᾚᾛᾜᾝᾞᾟᾠᾡᾢᾣᾤᾥᾦᾧᾨᾩᾪᾫᾬᾭᾮᾯᾰᾱᾲᾳᾴᾶᾷᾸᾹᾺΆᾼ
|
||||
᾽
|
||||
ι
|
||||
᾿῀῁
|
||||
ῂῃῄῆῇῈΈῊΉῌ
|
||||
῍῎῏
|
||||
ῐῑῒΐῖῗῘῙῚΊ
|
||||
῝῞῟
|
||||
ῠῡῢΰῤῥῦῧῨῩῪΎῬ
|
||||
῭΅`
|
||||
ῲῳῴῶῷῸΌῺΏῼ
|
||||
´῾ ‐‑‒–—―‖‗‘’‚‛“”„‟†‡•‣․‥…‧
‰‱′″‴‵‶‷‸‹›※‼‽‾‿⁀⁁⁂⁃⁄⁅⁆⁇⁈⁉⁊⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞
|
||||
⁰ⁱ⁴⁵⁶⁷⁸⁹
|
||||
⁺⁻⁼⁽⁾
|
||||
ⁿ₀₁₂₃₄₅₆₇₈₉
|
||||
₊₋₌₍₎
|
||||
ₐₑₒₓₔ
|
||||
₠₡₢₣₤₥₦₧₨₩₪₫€₭₮₯₰₱₲₳₴₵₶₷₸
|
||||
⃒⃓⃘⃙⃚⃐⃑⃔⃕⃖⃗⃛⃜⃝⃞⃟⃠⃡⃢⃣⃤⃥⃦⃪⃫⃨⃬⃭⃮⃯⃧⃩⃰
|
||||
℀℁
|
||||
ℂ
|
||||
℃℄℅℆
|
||||
ℇ
|
||||
℈℉
|
||||
ℊℋℌℍℎℏℐℑℒℓ
|
||||
℔
|
||||
ℕ
|
||||
№℗℘
|
||||
ℙℚℛℜℝ
|
||||
℞℟℠℡™℣
|
||||
ℤ
|
||||
℥
|
||||
Ω
|
||||
℧
|
||||
ℨ
|
||||
℩
|
||||
KÅℬℭ
|
||||
℮
|
||||
ℯℰℱℲℳℴℵℶℷℸℹ
|
||||
℺℻
|
||||
ℼℽℾℿ
|
||||
⅀⅁⅂⅃⅄
|
||||
ⅅⅆⅇⅈⅉ
|
||||
⅊⅋⅌⅍
|
||||
ⅎ
|
||||
⅏
|
||||
⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿↀↁↂↃↄↅↆↇↈ↉
|
||||
←↑→↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥↦↧↨↩↪↫↬↭↮↯↰↱↲↳↴↵↶↷↸↹↺↻↼↽↾↿⇀⇁⇂⇃⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇚⇛⇜⇝⇞⇟⇠⇡⇢⇣⇤⇥⇦⇧⇨⇩⇪⇫⇬⇭⇮⇯⇰⇱⇲⇳⇴⇵⇶⇷⇸⇹⇺⇻⇼⇽⇾⇿∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝∞∟∠∡∢∣∤∥∦∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿≀≁≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩≪≫≬≭≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊌⊍⊎⊏⊐⊑⊒⊓⊔⊕⊖⊗⊘⊙⊚⊛⊜⊝⊞⊟⊠⊡⊢⊣⊤⊥⊦⊧⊨⊩⊪⊫⊬⊭⊮⊯⊰⊱⊲⊳⊴⊵⊶⊷⊸⊹⊺⊻⊼⊽⊾⊿⋀⋁⋂⋃⋄⋅⋆⋇⋈⋉⋊⋋⋌⋍⋎⋏⋐⋑⋒⋓⋔⋕⋖⋗⋘⋙⋚⋛⋜⋝⋞⋟⋠⋡⋢⋣⋤⋥⋦⋧⋨⋩⋪⋫⋬⋭⋮⋯⋰⋱⋲⋳⋴⋵⋶⋷⋸⋹⋺⋻⋼⋽⋾⋿⌀⌁⌂⌃⌄⌅⌆⌇⌈⌉⌊⌋⌌⌍⌎⌏⌐⌑⌒⌓⌔⌕⌖⌗⌘⌙⌚⌛⌜⌝⌞⌟⌠⌡⌢⌣⌤⌥⌦⌧⌨〈〉⌫⌬⌭⌮⌯⌰⌱⌲⌳⌴⌵⌶⌷⌸⌹⌺⌻⌼⌽⌾⌿⍀⍁⍂⍃⍄⍅⍆⍇⍈⍉⍊⍋⍌⍍⍎⍏⍐⍑⍒⍓⍔⍕⍖⍗⍘⍙⍚⍛⍜⍝⍞⍟⍠⍡⍢⍣⍤⍥⍦⍧⍨⍩⍪⍫⍬⍭⍮⍯⍰⍱⍲⍳⍴⍵⍶⍷⍸⍹⍺⍻⍼⍽⍾⍿⎀⎁⎂⎃⎄⎅⎆⎇⎈⎉⎊⎋⎌⎍⎎⎏⎐⎑⎒⎓⎔⎕⎖⎗⎘⎙⎚⎛⎜⎝⎞⎟⎠⎡⎢⎣⎤⎥⎦⎧⎨⎩⎪⎫⎬⎭⎮⎯⎰⎱⎲⎳⎴⎵⎶⎷⎸⎹⎺⎻⎼⎽⎾⎿⏀⏁⏂⏃⏄⏅⏆⏇⏈⏉⏊⏋⏌⏍⏎⏏⏐⏑⏒⏓⏔⏕⏖⏗⏘⏙⏚⏛⏜⏝⏞⏟⏠⏡⏢⏣⏤⏥⏦⏧⏨␀␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␠␡␢␣␥␦⑀⑁⑂⑃⑄⑅⑆⑇⑈⑉⑊
|
||||
①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛
|
||||
⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ
|
||||
⓪⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴⓵⓶⓷⓸⓹⓺⓻⓼⓽⓾⓿
|
||||
─━│┃┄┅┆┇┈┉┊┋┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟■□▢▣▤▥▦▧▨▩▪▫▬▭▮▯▰▱▲△▴▵▶▷▸▹►▻▼▽▾▿◀◁◂◃◄◅◆◇◈◉◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◽◾◿☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♈♉♊♋♌♍♎♏♐♑♒♓♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚇⚈⚉⚊⚋⚌⚍⚎⚏⚐⚑⚒⚓⚔⚕⚖⚗⚘⚙⚚⚛⚜⚝⚞⚟⚠⚡⚢⚣⚤⚥⚦⚧⚨⚩⚪⚫⚬⚭⚮⚯⚰⚱⚲⚳⚴⚵⚶⚷⚸⚹⚺⚻⚼⚽⚾⚿⛀⛁⛂⛃⛄⛅⛆⛇⛈⛉⛊⛋⛌⛍⛏⛐⛑⛒⛓⛔⛕⛖⛗⛘⛙⛚⛛⛜⛝⛞⛟⛠⛡⛣⛨⛩⛪⛫⛬⛭⛮⛯⛰⛱⛲⛳⛴⛵⛶⛷⛸⛹⛺⛻⛼⛽⛾⛿✁✂✃✄✆✇✈✉✌✍✎✏✐✑✒✓✔✕✖✗✘✙✚✛✜✝✞✟✠✡✢✣✤✥✦✧✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀❁❂❃❄❅❆❇❈❉❊❋❍❏❐❑❒❖❗❘❙❚❛❜❝❞❡❢❣❤❥❦❧❨❩❪❫❬❭❮❯❰❱❲❳❴❵
|
||||
❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓
|
||||
➔➘➙➚➛➜➝➞➟➠➡➢➣➤➥➦➧➨➩➪➫➬➭➮➯➱➲➳➴➵➶➷➸➹➺➻➼➽➾⟀⟁⟂⟃⟄⟅⟆⟇⟈⟉⟊⟌⟐⟑⟒⟓⟔⟕⟖⟗⟘⟙⟚⟛⟜⟝⟞⟟⟠⟡⟢⟣⟤⟥⟦⟧⟨⟩⟪⟫⟬⟭⟮⟯⟰⟱⟲⟳⟴⟵⟶⟷⟸⟹⟺⟻⟼⟽⟾⟿⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿⤀⤁⤂⤃⤄⤅⤆⤇⤈⤉⤊⤋⤌⤍⤎⤏⤐⤑⤒⤓⤔⤕⤖⤗⤘⤙⤚⤛⤜⤝⤞⤟⤠⤡⤢⤣⤤⤥⤦⤧⤨⤩⤪⤫⤬⤭⤮⤯⤰⤱⤲⤳⤴⤵⤶⤷⤸⤹⤺⤻⤼⤽⤾⤿⥀⥁⥂⥃⥄⥅⥆⥇⥈⥉⥊⥋⥌⥍⥎⥏⥐⥑⥒⥓⥔⥕⥖⥗⥘⥙⥚⥛⥜⥝⥞⥟⥠⥡⥢⥣⥤⥥⥦⥧⥨⥩⥪⥫⥬⥭⥮⥯⥰⥱⥲⥳⥴⥵⥶⥷⥸⥹⥺⥻⥼⥽⥾⥿⦀⦁⦂⦃⦄⦅⦆⦇⦈⦉⦊⦋⦌⦍⦎⦏⦐⦑⦒⦓⦔⦕⦖⦗⦘⦙⦚⦛⦜⦝⦞⦟⦠⦡⦢⦣⦤⦥⦦⦧⦨⦩⦪⦫⦬⦭⦮⦯⦰⦱⦲⦳⦴⦵⦶⦷⦸⦹⦺⦻⦼⦽⦾⦿⧀⧁⧂⧃⧄⧅⧆⧇⧈⧉⧊⧋⧌⧍⧎⧏⧐⧑⧒⧓⧔⧕⧖⧗⧘⧙⧚⧛⧜⧝⧞⧟⧠⧡⧢⧣⧤⧥⧦⧧⧨⧩⧪⧫⧬⧭⧮⧯⧰⧱⧲⧳⧴⧵⧶⧷⧸⧹⧺⧻⧼⧽⧾⧿⨀⨁⨂⨃⨄⨅⨆⨇⨈⨉⨊⨋⨌⨍⨎⨏⨐⨑⨒⨓⨔⨕⨖⨗⨘⨙⨚⨛⨜⨝⨞⨟⨠⨡⨢⨣⨤⨥⨦⨧⨨⨩⨪⨫⨬⨭⨮⨯⨰⨱⨲⨳⨴⨵⨶⨷⨸⨹⨺⨻⨼⨽⨾⨿⩀⩁⩂⩃⩄⩅⩆⩇⩈⩉⩊⩋⩌⩍⩎⩏⩐⩑⩒⩓⩔⩕⩖⩗⩘⩙⩚⩛⩜⩝⩞⩟⩠⩡⩢⩣⩤⩥⩦⩧⩨⩩⩪⩫⩬⩭⩮⩯⩰⩱⩲⩳⩴⩵⩶⩷⩸⩹⩺⩻⩼⩽⩾⩿⪀⪁⪂⪃⪄⪅⪆⪇⪈⪉⪊⪋⪌⪍⪎⪏⪐⪑⪒⪓⪔⪕⪖⪗⪘⪙⪚⪛⪜⪝⪞⪟⪠⪡⪢⪣⪤⪥⪦⪧⪨⪩⪪⪫⪬⪭⪮⪯⪰⪱⪲⪳⪴⪵⪶⪷⪸⪹⪺⪻⪼⪽⪾⪿⫀⫁⫂⫃⫄⫅⫆⫇⫈⫉⫊⫋⫌⫍⫎⫏⫐⫑⫒⫓⫔⫕⫖⫗⫘⫙⫚⫛⫝̸⫝⫞⫟⫠⫡⫢⫣⫤⫥⫦⫧⫨⫩⫪⫫⫬⫭⫮⫯⫰⫱⫲⫳⫴⫵⫶⫷⫸⫹⫺⫻⫼⫽⫾⫿⬀⬁⬂⬃⬄⬅⬆⬇⬈⬉⬊⬋⬌⬍⬎⬏⬐⬑⬒⬓⬔⬕⬖⬗⬘⬙⬚⬛⬜⬝⬞⬟⬠⬡⬢⬣⬤⬥⬦⬧⬨⬩⬪⬫⬬⬭⬮⬯⬰⬱⬲⬳⬴⬵⬶⬷⬸⬹⬺⬻⬼⬽⬾⬿⭀⭁⭂⭃⭄⭅⭆⭇⭈⭉⭊⭋⭌⭐⭑⭒⭓⭔⭕⭖⭗⭘⭙
|
||||
ⰀⰁⰂⰃⰄⰅⰆⰇⰈⰉⰊⰋⰌⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰙⰚⰛⰜⰝⰞⰟⰠⰡⰢⰣⰤⰥⰦⰧⰨⰩⰪⰫⰬⰭⰮⰰⰱⰲⰳⰴⰵⰶⰷⰸⰹⰺⰻⰼⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱉⱊⱋⱌⱍⱎⱏⱐⱑⱒⱓⱔⱕⱖⱗⱘⱙⱚⱛⱜⱝⱞⱠⱡⱢⱣⱤⱥⱦⱧⱨⱩⱪⱫⱬⱭⱮⱯⱰⱱⱲⱳⱴⱵⱶⱷⱸⱹⱺⱻⱼⱽⱾⱿⲀⲁⲂⲃⲄⲅⲆⲇⲈⲉⲊⲋⲌⲍⲎⲏⲐⲑⲒⲓⲔⲕⲖⲗⲘⲙⲚⲛⲜⲝⲞⲟⲠⲡⲢⲣⲤⲥⲦⲧⲨⲩⲪⲫⲬⲭⲮⲯⲰⲱⲲⲳⲴⲵⲶⲷⲸⲹⲺⲻⲼⲽⲾⲿⳀⳁⳂⳃⳄⳅⳆⳇⳈⳉⳊⳋⳌⳍⳎⳏⳐⳑⳒⳓⳔⳕⳖⳗⳘⳙⳚⳛⳜⳝⳞⳟⳠⳡⳢⳣⳤ
|
||||
⳥⳦⳧⳨⳩⳪
|
||||
ⳫⳬⳭⳮ⳯⳰⳱
|
||||
⳹⳺⳻⳼
|
||||
⳽
|
||||
⳾⳿
|
||||
ⴀⴁⴂⴃⴄⴅⴆⴇⴈⴉⴊⴋⴌⴍⴎⴏⴐⴑⴒⴓⴔⴕⴖⴗⴘⴙⴚⴛⴜⴝⴞⴟⴠⴡⴢⴣⴤⴥⴰⴱⴲⴳⴴⴵⴶⴷⴸⴹⴺⴻⴼⴽⴾⴿⵀⵁⵂⵃⵄⵅⵆⵇⵈⵉⵊⵋⵌⵍⵎⵏⵐⵑⵒⵓⵔⵕⵖⵗⵘⵙⵚⵛⵜⵝⵞⵟⵠⵡⵢⵣⵤⵥⵯⶀⶁⶂⶃⶄⶅⶆⶇⶈⶉⶊⶋⶌⶍⶎⶏⶐⶑⶒⶓⶔⶕⶖⶠⶡⶢⶣⶤⶥⶦⶨⶩⶪⶫⶬⶭⶮⶰⶱⶲⶳⶴⶵⶶⶸⶹⶺⶻⶼⶽⶾⷀⷁⷂⷃⷄⷅⷆⷈⷉⷊⷋⷌⷍⷎⷐⷑⷒⷓⷔⷕⷖⷘⷙⷚⷛⷜⷝⷞⷠⷡⷢⷣⷤⷥⷦⷧⷨⷩⷪⷫⷬⷭⷮⷯⷰⷱⷲⷳⷴⷵⷶⷷⷸⷹⷺⷻⷼⷽⷾⷿ
|
||||
⸀⸁⸂⸃⸄⸅⸆⸇⸈⸉⸊⸋⸌⸍⸎⸏⸐⸑⸒⸓⸔⸕⸖⸗⸘⸙⸚⸛⸜⸝⸞⸟⸠⸡⸢⸣⸤⸥⸦⸧⸨⸩⸪⸫⸬⸭⸮
|
||||
ⸯ
|
||||
⸰⸱⺀⺁⺂⺃⺄⺅⺆⺇⺈⺉⺊⺋⺌⺍⺎⺏⺐⺑⺒⺓⺔⺕⺖⺗⺘⺙⺛⺜⺝⺞⺟⺠⺡⺢⺣⺤⺥⺦⺧⺨⺩⺪⺫⺬⺭⺮⺯⺰⺱⺲⺳⺴⺵⺶⺷⺸⺹⺺⺻⺼⺽⺾⺿⻀⻁⻂⻃⻄⻅⻆⻇⻈⻉⻊⻋⻌⻍⻎⻏⻐⻑⻒⻓⻔⻕⻖⻗⻘⻙⻚⻛⻜⻝⻞⻟⻠⻡⻢⻣⻤⻥⻦⻧⻨⻩⻪⻫⻬⻭⻮⻯⻰⻱⻲⻳⼀⼁⼂⼃⼄⼅⼆⼇⼈⼉⼊⼋⼌⼍⼎⼏⼐⼑⼒⼓⼔⼕⼖⼗⼘⼙⼚⼛⼜⼝⼞⼟⼠⼡⼢⼣⼤⼥⼦⼧⼨⼩⼪⼫⼬⼭⼮⼯⼰⼱⼲⼳⼴⼵⼶⼷⼸⼹⼺⼻⼼⼽⼾⼿⽀⽁⽂⽃⽄⽅⽆⽇⽈⽉⽊⽋⽌⽍⽎⽏⽐⽑⽒⽓⽔⽕⽖⽗⽘⽙⽚⽛⽜⽝⽞⽟⽠⽡⽢⽣⽤⽥⽦⽧⽨⽩⽪⽫⽬⽭⽮⽯⽰⽱⽲⽳⽴⽵⽶⽷⽸⽹⽺⽻⽼⽽⽾⽿⾀⾁⾂⾃⾄⾅⾆⾇⾈⾉⾊⾋⾌⾍⾎⾏⾐⾑⾒⾓⾔⾕⾖⾗⾘⾙⾚⾛⾜⾝⾞⾟⾠⾡⾢⾣⾤⾥⾦⾧⾨⾩⾪⾫⾬⾭⾮⾯⾰⾱⾲⾳⾴⾵⾶⾷⾸⾹⾺⾻⾼⾽⾾⾿⿀⿁⿂⿃⿄⿅⿆⿇⿈⿉⿊⿋⿌⿍⿎⿏⿐⿑⿒⿓⿔⿕⿰⿱⿲⿳⿴⿵⿶⿷⿸⿹⿺⿻ 、。〃〄
|
||||
々〆〇
|
||||
〈〉《》「」『』【】〒〓〔〕〖〗〘〙〚〛〜〝〞〟〠
|
||||
〡〢〣〤〥〦〧〨〩〪〭〮〯〫〬
|
||||
〰
|
||||
〱〲〳〴〵
|
||||
〶〷
|
||||
〸〹〺〻〼
|
||||
〽〾〿
|
||||
ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ゙゚
|
||||
゛゜
|
||||
ゝゞゟ
|
||||
゠
|
||||
ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ
|
||||
・
|
||||
ーヽヾヿㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦㄧㄨㄩㄪㄫㄬㄭㄱㄲㄳㄴㄵㄶㄷㄸㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅃㅄㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣㅤㅥㅦㅧㅨㅩㅪㅫㅬㅭㅮㅯㅰㅱㅲㅳㅴㅵㅶㅷㅸㅹㅺㅻㅼㅽㅾㅿㆀㆁㆂㆃㆄㆅㆆㆇㆈㆉㆊㆋㆌㆍㆎ
|
||||
㆐㆑
|
||||
㆒㆓㆔㆕
|
||||
㆖㆗㆘㆙㆚㆛㆜㆝㆞㆟
|
||||
ㆠㆡㆢㆣㆤㆥㆦㆧㆨㆩㆪㆫㆬㆭㆮㆯㆰㆱㆲㆳㆴㆵㆶㆷ
|
||||
㇀㇁㇂㇃㇄㇅㇆㇇㇈㇉㇊㇋㇌㇍㇎㇏㇐㇑㇒㇓㇔㇕㇖㇗㇘㇙㇚㇛㇜㇝㇞㇟㇠㇡㇢㇣
|
||||
ㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ
|
||||
㈀㈁㈂㈃㈄㈅㈆㈇㈈㈉㈊㈋㈌㈍㈎㈏㈐㈑㈒㈓㈔㈕㈖㈗㈘㈙㈚㈛㈜㈝㈞
|
||||
㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩
|
||||
㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃㉄㉅㉆㉇㉈㉉㉊㉋㉌㉍㉎㉏㉐
|
||||
㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟
|
||||
㉠㉡㉢㉣㉤㉥㉦㉧㉨㉩㉪㉫㉬㉭㉮㉯㉰㉱㉲㉳㉴㉵㉶㉷㉸㉹㉺㉻㉼㉽㉾㉿
|
||||
㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉
|
||||
㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗㊘㊙㊚㊛㊜㊝㊞㊟㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰
|
||||
㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿
|
||||
㋀㋁㋂㋃㋄㋅㋆㋇㋈㋉㋊㋋㋌㋍㋎㋏㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋽㋾㌀㌁㌂㌃㌄㌅㌆㌇㌈㌉㌊㌋㌌㌍㌎㌏㌐㌑㌒㌓㌔㌕㌖㌗㌘㌙㌚㌛㌜㌝㌞㌟㌠㌡㌢㌣㌤㌥㌦㌧㌨㌩㌪㌫㌬㌭㌮㌯㌰㌱㌲㌳㌴㌵㌶㌷㌸㌹㌺㌻㌼㌽㌾㌿㍀㍁㍂㍃㍄㍅㍆㍇㍈㍉㍊㍋㍌㍍㍎㍏㍐㍑㍒㍓㍔㍕㍖㍗㍘㍙㍚㍛㍜㍝㍞㍟㍠㍡㍢㍣㍤㍥㍦㍧㍨㍩㍪㍫㍬㍭㍮㍯㍰㍱㍲㍳㍴㍵㍶㍷㍸㍹㍺㍻㍼㍽㍾㍿㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢㎣㎤㎥㎦㎧㎨㎩㎪㎫㎬㎭㎮㎯㎰㎱㎲㎳㎴㎵㎶㎷㎸㎹㎺㎻㎼㎽㎾㎿㏀㏁㏂㏃㏄㏅㏆㏇㏈㏉㏊㏋㏌㏍㏎㏏㏐㏑㏒㏓㏔㏕㏖㏗㏘㏙㏚㏛㏜㏝㏞㏟㏠㏡㏢㏣㏤㏥㏦㏧㏨㏩㏪㏫㏬㏭㏮㏯㏰㏱㏲㏳㏴㏵㏶㏷㏸㏹㏺㏻㏼㏽㏾㏿
|
||||
㐀䶵
|
||||
䷀䷁䷂䷃䷄䷅䷆䷇䷈䷉䷊䷋䷌䷍䷎䷏䷐䷑䷒䷓䷔䷕䷖䷗䷘䷙䷚䷛䷜䷝䷞䷟䷠䷡䷢䷣䷤䷥䷦䷧䷨䷩䷪䷫䷬䷭䷮䷯䷰䷱䷲䷳䷴䷵䷶䷷䷸䷹䷺䷻䷼䷽䷾䷿
|
||||
一鿋ꀀꀁꀂꀃꀄꀅꀆꀇꀈꀉꀊꀋꀌꀍꀎꀏꀐꀑꀒꀓꀔꀕꀖꀗꀘꀙꀚꀛꀜꀝꀞꀟꀠꀡꀢꀣꀤꀥꀦꀧꀨꀩꀪꀫꀬꀭꀮꀯꀰꀱꀲꀳꀴꀵꀶꀷꀸꀹꀺꀻꀼꀽꀾꀿꁀꁁꁂꁃꁄꁅꁆꁇꁈꁉꁊꁋꁌꁍꁎꁏꁐꁑꁒꁓꁔꁕꁖꁗꁘꁙꁚꁛꁜꁝꁞꁟꁠꁡꁢꁣꁤꁥꁦꁧꁨꁩꁪꁫꁬꁭꁮꁯꁰꁱꁲꁳꁴꁵꁶꁷꁸꁹꁺꁻꁼꁽꁾꁿꂀꂁꂂꂃꂄꂅꂆꂇꂈꂉꂊꂋꂌꂍꂎꂏꂐꂑꂒꂓꂔꂕꂖꂗꂘꂙꂚꂛꂜꂝꂞꂟꂠꂡꂢꂣꂤꂥꂦꂧꂨꂩꂪꂫꂬꂭꂮꂯꂰꂱꂲꂳꂴꂵꂶꂷꂸꂹꂺꂻꂼꂽꂾꂿꃀꃁꃂꃃꃄꃅꃆꃇꃈꃉꃊꃋꃌꃍꃎꃏꃐꃑꃒꃓꃔꃕꃖꃗꃘꃙꃚꃛꃜꃝꃞꃟꃠꃡꃢꃣꃤꃥꃦꃧꃨꃩꃪꃫꃬꃭꃮꃯꃰꃱꃲꃳꃴꃵꃶꃷꃸꃹꃺꃻꃼꃽꃾꃿꄀꄁꄂꄃꄄꄅꄆꄇꄈꄉꄊꄋꄌꄍꄎꄏꄐꄑꄒꄓꄔꄕꄖꄗꄘꄙꄚꄛꄜꄝꄞꄟꄠꄡꄢꄣꄤꄥꄦꄧꄨꄩꄪꄫꄬꄭꄮꄯꄰꄱꄲꄳꄴꄵꄶꄷꄸꄹꄺꄻꄼꄽꄾꄿꅀꅁꅂꅃꅄꅅꅆꅇꅈꅉꅊꅋꅌꅍꅎꅏꅐꅑꅒꅓꅔꅕꅖꅗꅘꅙꅚꅛꅜꅝꅞꅟꅠꅡꅢꅣꅤꅥꅦꅧꅨꅩꅪꅫꅬꅭꅮꅯꅰꅱꅲꅳꅴꅵꅶꅷꅸꅹꅺꅻꅼꅽꅾꅿꆀꆁꆂꆃꆄꆅꆆꆇꆈꆉꆊꆋꆌꆍꆎꆏꆐꆑꆒꆓꆔꆕꆖꆗꆘꆙꆚꆛꆜꆝꆞꆟꆠꆡꆢꆣꆤꆥꆦꆧꆨꆩꆪꆫꆬꆭꆮꆯꆰꆱꆲꆳꆴꆵꆶꆷꆸꆹꆺꆻꆼꆽꆾꆿꇀꇁꇂꇃꇄꇅꇆꇇꇈꇉꇊꇋꇌꇍꇎꇏꇐꇑꇒꇓꇔꇕꇖꇗꇘꇙꇚꇛꇜꇝꇞꇟꇠꇡꇢꇣꇤꇥꇦꇧꇨꇩꇪꇫꇬꇭꇮꇯꇰꇱꇲꇳꇴꇵꇶꇷꇸꇹꇺꇻꇼꇽꇾꇿꈀꈁꈂꈃꈄꈅꈆꈇꈈꈉꈊꈋꈌꈍꈎꈏꈐꈑꈒꈓꈔꈕꈖꈗꈘꈙꈚꈛꈜꈝꈞꈟꈠꈡꈢꈣꈤꈥꈦꈧꈨꈩꈪꈫꈬꈭꈮꈯꈰꈱꈲꈳꈴꈵꈶꈷꈸꈹꈺꈻꈼꈽꈾꈿꉀꉁꉂꉃꉄꉅꉆꉇꉈꉉꉊꉋꉌꉍꉎꉏꉐꉑꉒꉓꉔꉕꉖꉗꉘꉙꉚꉛꉜꉝꉞꉟꉠꉡꉢꉣꉤꉥꉦꉧꉨꉩꉪꉫꉬꉭꉮꉯꉰꉱꉲꉳꉴꉵꉶꉷꉸꉹꉺꉻꉼꉽꉾꉿꊀꊁꊂꊃꊄꊅꊆꊇꊈꊉꊊꊋꊌꊍꊎꊏꊐꊑꊒꊓꊔꊕꊖꊗꊘꊙꊚꊛꊜꊝꊞꊟꊠꊡꊢꊣꊤꊥꊦꊧꊨꊩꊪꊫꊬꊭꊮꊯꊰꊱꊲꊳꊴꊵꊶꊷꊸꊹꊺꊻꊼꊽꊾꊿꋀꋁꋂꋃꋄꋅꋆꋇꋈꋉꋊꋋꋌꋍꋎꋏꋐꋑꋒꋓꋔꋕꋖꋗꋘꋙꋚꋛꋜꋝꋞꋟꋠꋡꋢꋣꋤꋥꋦꋧꋨꋩꋪꋫꋬꋭꋮꋯꋰꋱꋲꋳꋴꋵꋶꋷꋸꋹꋺꋻꋼꋽꋾꋿꌀꌁꌂꌃꌄꌅꌆꌇꌈꌉꌊꌋꌌꌍꌎꌏꌐꌑꌒꌓꌔꌕꌖꌗꌘꌙꌚꌛꌜꌝꌞꌟꌠꌡꌢꌣꌤꌥꌦꌧꌨꌩꌪꌫꌬꌭꌮꌯꌰꌱꌲꌳꌴꌵꌶꌷꌸꌹꌺꌻꌼꌽꌾꌿꍀꍁꍂꍃꍄꍅꍆꍇꍈꍉꍊꍋꍌꍍꍎꍏꍐꍑꍒꍓꍔꍕꍖꍗꍘꍙꍚꍛꍜꍝꍞꍟꍠꍡꍢꍣꍤꍥꍦꍧꍨꍩꍪꍫꍬꍭꍮꍯꍰꍱꍲꍳꍴꍵꍶꍷꍸꍹꍺꍻꍼꍽꍾꍿꎀꎁꎂꎃꎄꎅꎆꎇꎈꎉꎊꎋꎌꎍꎎꎏꎐꎑꎒꎓꎔꎕꎖꎗꎘꎙꎚꎛꎜꎝꎞꎟꎠꎡꎢꎣꎤꎥꎦꎧꎨꎩꎪꎫꎬꎭꎮꎯꎰꎱꎲꎳꎴꎵꎶꎷꎸꎹꎺꎻꎼꎽꎾꎿꏀꏁꏂꏃꏄꏅꏆꏇꏈꏉꏊꏋꏌꏍꏎꏏꏐꏑꏒꏓꏔꏕꏖꏗꏘꏙꏚꏛꏜꏝꏞꏟꏠꏡꏢꏣꏤꏥꏦꏧꏨꏩꏪꏫꏬꏭꏮꏯꏰꏱꏲꏳꏴꏵꏶꏷꏸꏹꏺꏻꏼꏽꏾꏿꐀꐁꐂꐃꐄꐅꐆꐇꐈꐉꐊꐋꐌꐍꐎꐏꐐꐑꐒꐓꐔꐕꐖꐗꐘꐙꐚꐛꐜꐝꐞꐟꐠꐡꐢꐣꐤꐥꐦꐧꐨꐩꐪꐫꐬꐭꐮꐯꐰꐱꐲꐳꐴꐵꐶꐷꐸꐹꐺꐻꐼꐽꐾꐿꑀꑁꑂꑃꑄꑅꑆꑇꑈꑉꑊꑋꑌꑍꑎꑏꑐꑑꑒꑓꑔꑕꑖꑗꑘꑙꑚꑛꑜꑝꑞꑟꑠꑡꑢꑣꑤꑥꑦꑧꑨꑩꑪꑫꑬꑭꑮꑯꑰꑱꑲꑳꑴꑵꑶꑷꑸꑹꑺꑻꑼꑽꑾꑿꒀꒁꒂꒃꒄꒅꒆꒇꒈꒉꒊꒋꒌ
|
||||
꒐꒑꒒꒓꒔꒕꒖꒗꒘꒙꒚꒛꒜꒝꒞꒟꒠꒡꒢꒣꒤꒥꒦꒧꒨꒩꒪꒫꒬꒭꒮꒯꒰꒱꒲꒳꒴꒵꒶꒷꒸꒹꒺꒻꒼꒽꒾꒿꓀꓁꓂꓃꓄꓅꓆
|
||||
ꓐꓑꓒꓓꓔꓕꓖꓗꓘꓙꓚꓛꓜꓝꓞꓟꓠꓡꓢꓣꓤꓥꓦꓧꓨꓩꓪꓫꓬꓭꓮꓯꓰꓱꓲꓳꓴꓵꓶꓷꓸꓹꓺꓻꓼꓽ
|
||||
꓾꓿
|
||||
ꔀꔁꔂꔃꔄꔅꔆꔇꔈꔉꔊꔋꔌꔍꔎꔏꔐꔑꔒꔓꔔꔕꔖꔗꔘꔙꔚꔛꔜꔝꔞꔟꔠꔡꔢꔣꔤꔥꔦꔧꔨꔩꔪꔫꔬꔭꔮꔯꔰꔱꔲꔳꔴꔵꔶꔷꔸꔹꔺꔻꔼꔽꔾꔿꕀꕁꕂꕃꕄꕅꕆꕇꕈꕉꕊꕋꕌꕍꕎꕏꕐꕑꕒꕓꕔꕕꕖꕗꕘꕙꕚꕛꕜꕝꕞꕟꕠꕡꕢꕣꕤꕥꕦꕧꕨꕩꕪꕫꕬꕭꕮꕯꕰꕱꕲꕳꕴꕵꕶꕷꕸꕹꕺꕻꕼꕽꕾꕿꖀꖁꖂꖃꖄꖅꖆꖇꖈꖉꖊꖋꖌꖍꖎꖏꖐꖑꖒꖓꖔꖕꖖꖗꖘꖙꖚꖛꖜꖝꖞꖟꖠꖡꖢꖣꖤꖥꖦꖧꖨꖩꖪꖫꖬꖭꖮꖯꖰꖱꖲꖳꖴꖵꖶꖷꖸꖹꖺꖻꖼꖽꖾꖿꗀꗁꗂꗃꗄꗅꗆꗇꗈꗉꗊꗋꗌꗍꗎꗏꗐꗑꗒꗓꗔꗕꗖꗗꗘꗙꗚꗛꗜꗝꗞꗟꗠꗡꗢꗣꗤꗥꗦꗧꗨꗩꗪꗫꗬꗭꗮꗯꗰꗱꗲꗳꗴꗵꗶꗷꗸꗹꗺꗻꗼꗽꗾꗿꘀꘁꘂꘃꘄꘅꘆꘇꘈꘉꘊꘋꘌ
|
||||
꘍꘎꘏
|
||||
ꘐꘑꘒꘓꘔꘕꘖꘗꘘꘙꘚꘛꘜꘝꘞꘟ꘠꘡꘢꘣꘤꘥꘦꘧꘨꘩ꘪꘫꙀꙁꙂꙃꙄꙅꙆꙇꙈꙉꙊꙋꙌꙍꙎꙏꙐꙑꙒꙓꙔꙕꙖꙗꙘꙙꙚꙛꙜꙝꙞꙟꙢꙣꙤꙥꙦꙧꙨꙩꙪꙫꙬꙭꙮ꙯꙰꙱꙲
|
||||
꙳
|
||||
꙼꙽
|
||||
꙾
|
||||
ꙿꚀꚁꚂꚃꚄꚅꚆꚇꚈꚉꚊꚋꚌꚍꚎꚏꚐꚑꚒꚓꚔꚕꚖꚗꚠꚡꚢꚣꚤꚥꚦꚧꚨꚩꚪꚫꚬꚭꚮꚯꚰꚱꚲꚳꚴꚵꚶꚷꚸꚹꚺꚻꚼꚽꚾꚿꛀꛁꛂꛃꛄꛅꛆꛇꛈꛉꛊꛋꛌꛍꛎꛏꛐꛑꛒꛓꛔꛕꛖꛗꛘꛙꛚꛛꛜꛝꛞꛟꛠꛡꛢꛣꛤꛥꛦꛧꛨꛩꛪꛫꛬꛭꛮꛯ꛰꛱
|
||||
꛲꛳꛴꛵꛶꛷꜀꜁꜂꜃꜄꜅꜆꜇꜈꜉꜊꜋꜌꜍꜎꜏꜐꜑꜒꜓꜔꜕꜖
|
||||
ꜗꜘꜙꜚꜛꜜꜝꜞꜟ
|
||||
꜠꜡
|
||||
ꜢꜣꜤꜥꜦꜧꜨꜩꜪꜫꜬꜭꜮꜯꜰꜱꜲꜳꜴꜵꜶꜷꜸꜹꜺꜻꜼꜽꜾꜿꝀꝁꝂꝃꝄꝅꝆꝇꝈꝉꝊꝋꝌꝍꝎꝏꝐꝑꝒꝓꝔꝕꝖꝗꝘꝙꝚꝛꝜꝝꝞꝟꝠꝡꝢꝣꝤꝥꝦꝧꝨꝩꝪꝫꝬꝭꝮꝯꝰꝱꝲꝳꝴꝵꝶꝷꝸꝹꝺꝻꝼꝽꝾꝿꞀꞁꞂꞃꞄꞅꞆꞇꞈ
|
||||
꞉꞊
|
||||
Ꞌꞌꟻꟼꟽꟾꟿꠀꠁꠂꠃꠄꠅ꠆ꠇꠈꠉꠊꠋꠌꠍꠎꠏꠐꠑꠒꠓꠔꠕꠖꠗꠘꠙꠚꠛꠜꠝꠞꠟꠠꠡꠢꠣꠤꠥꠦꠧ
|
||||
꠨꠩꠪꠫
|
||||
꠰꠱꠲꠳꠴꠵
|
||||
꠶꠷꠸꠹
|
||||
ꡀꡁꡂꡃꡄꡅꡆꡇꡈꡉꡊꡋꡌꡍꡎꡏꡐꡑꡒꡓꡔꡕꡖꡗꡘꡙꡚꡛꡜꡝꡞꡟꡠꡡꡢꡣꡤꡥꡦꡧꡨꡩꡪꡫꡬꡭꡮꡯꡰꡱꡲꡳ
|
||||
꡴꡵꡶꡷
|
||||
ꢀꢁꢂꢃꢄꢅꢆꢇꢈꢉꢊꢋꢌꢍꢎꢏꢐꢑꢒꢓꢔꢕꢖꢗꢘꢙꢚꢛꢜꢝꢞꢟꢠꢡꢢꢣꢤꢥꢦꢧꢨꢩꢪꢫꢬꢭꢮꢯꢰꢱꢲꢳꢴꢵꢶꢷꢸꢹꢺꢻꢼꢽꢾꢿꣀꣁꣂꣃ꣄
|
||||
꣎꣏
|
||||
꣐꣑꣒꣓꣔꣕꣖꣗꣘꣙꣠꣡꣢꣣꣤꣥꣦꣧꣨꣩꣪꣫꣬꣭꣮꣯꣰꣱ꣲꣳꣴꣵꣶꣷ
|
||||
꣸꣹꣺
|
||||
ꣻ꤀꤁꤂꤃꤄꤅꤆꤇꤈꤉ꤊꤋꤌꤍꤎꤏꤐꤑꤒꤓꤔꤕꤖꤗꤘꤙꤚꤛꤜꤝꤞꤟꤠꤡꤢꤣꤤꤥꤦꤧꤨꤩꤪ꤫꤬꤭
|
||||
꤮꤯
|
||||
ꤰꤱꤲꤳꤴꤵꤶꤷꤸꤹꤺꤻꤼꤽꤾꤿꥀꥁꥂꥃꥄꥅꥆꥇꥈꥉꥊꥋꥌꥍꥎꥏꥐꥑꥒ꥓
|
||||
꥟
|
||||
ꥠꥡꥢꥣꥤꥥꥦꥧꥨꥩꥪꥫꥬꥭꥮꥯꥰꥱꥲꥳꥴꥵꥶꥷꥸꥹꥺꥻꥼꦀꦁꦂꦃꦄꦅꦆꦇꦈꦉꦊꦋꦌꦍꦎꦏꦐꦑꦒꦓꦔꦕꦖꦗꦘꦙꦚꦛꦜꦝꦞꦟꦠꦡꦢꦣꦤꦥꦦꦧꦨꦩꦪꦫꦬꦭꦮꦯꦰꦱꦲ꦳ꦴꦵꦶꦷꦸꦹꦺꦻꦼꦽꦾꦿ꧀
|
||||
꧁꧂꧃꧄꧅꧆꧇꧈꧉꧊꧋꧌꧍
|
||||
ꧏ꧐꧑꧒꧓꧔꧕꧖꧗꧘꧙
|
||||
꧞꧟
|
||||
ꨀꨁꨂꨃꨄꨅꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨩꨪꨫꨬꨭꨮꨯꨰꨱꨲꨳꨴꨵꨶꩀꩁꩂꩃꩄꩅꩆꩇꩈꩉꩊꩋꩌꩍ꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙
|
||||
꩜꩝꩞꩟
|
||||
ꩠꩡꩢꩣꩤꩥꩦꩧꩨꩩꩪꩫꩬꩭꩮꩯꩰꩱꩲꩳꩴꩵꩶ
|
||||
꩷꩸꩹
|
||||
ꩺꩻꪀꪁꪂꪃꪄꪅꪆꪇꪈꪉꪊꪋꪌꪍꪎꪏꪐꪑꪒꪓꪔꪕꪖꪗꪘꪙꪚꪛꪜꪝꪞꪟꪠꪡꪢꪣꪤꪥꪦꪧꪨꪩꪪꪫꪬꪭꪮꪯꪰꪱꪴꪲꪳꪵꪶꪷꪸꪹꪺꪻꪼꪽꪾ꪿ꫀ꫁ꫂꫛꫜꫝ
|
||||
꫞꫟
|
||||
ꯀꯁꯂꯃꯄꯅꯆꯇꯈꯉꯊꯋꯌꯍꯎꯏꯐꯑꯒꯓꯔꯕꯖꯗꯘꯙꯚꯛꯜꯝꯞꯟꯠꯡꯢꯣꯤꯥꯦꯧꯨꯩꯪ
|
||||
꯫
|
||||
꯬꯭꯰꯱꯲꯳꯴꯵꯶꯷꯸꯹가힣ힰힱힲힳힴힵힶힷힸힹힺힻힼힽힾힿퟀퟁퟂퟃퟄퟅퟆퟋퟌퟍퟎퟏퟐퟑퟒퟓퟔퟕퟖퟗퟘퟙퟚퟛퟜퟝퟞퟟퟠퟡퟢퟣퟤퟥퟦퟧퟨퟩퟪퟫퟬퟭퟮퟯퟰퟱퟲퟳퟴퟵퟶퟷퟸퟹퟺퟻ
|
||||
|
||||
豈更車賈滑串句龜龜契金喇奈懶癩羅蘿螺裸邏樂洛烙珞落酪駱亂卵欄爛蘭鸞嵐濫藍襤拉臘蠟廊朗浪狼郎來冷勞擄櫓爐盧老蘆虜路露魯鷺碌祿綠菉錄鹿論壟弄籠聾牢磊賂雷壘屢樓淚漏累縷陋勒肋凜凌稜綾菱陵讀拏樂諾丹寧怒率異北磻便復不泌數索參塞省葉說殺辰沈拾若掠略亮兩凉梁糧良諒量勵呂女廬旅濾礪閭驪麗黎力曆歷轢年憐戀撚漣煉璉秊練聯輦蓮連鍊列劣咽烈裂說廉念捻殮簾獵令囹寧嶺怜玲瑩羚聆鈴零靈領例禮醴隸惡了僚寮尿料樂燎療蓼遼龍暈阮劉杻柳流溜琉留硫紐類六戮陸倫崙淪輪律慄栗率隆利吏履易李梨泥理痢罹裏裡里離匿溺吝燐璘藺隣鱗麟林淋臨立笠粒狀炙識什茶刺切度拓糖宅洞暴輻行降見廓兀嗀﨎﨏塚﨑晴﨓﨔凞猪益礼神祥福靖精羽﨟蘒﨡諸﨣﨤逸都﨧﨨﨩飯飼館鶴侮僧免勉勤卑喝嘆器塀墨層屮悔慨憎懲敏既暑梅海渚漢煮爫琢碑社祉祈祐祖祝禍禎穀突節練縉繁署者臭艹艹著褐視謁謹賓贈辶逸難響頻恵𤋮舘並况全侀充冀勇勺喝啕喙嗢塚墳奄奔婢嬨廒廙彩徭惘慎愈憎慠懲戴揄搜摒敖晴朗望杖歹殺流滛滋漢瀞煮瞧爵犯猪瑱甆画瘝瘟益盛直睊着磌窱節类絛練缾者荒華蝹襁覆視調諸請謁諾諭謹變贈輸遲醙鉶陼難靖韛響頋頻鬒龜𢡊𢡄𣏕㮝䀘䀹𥉉𥳐𧻓齃龎fffiflffifflſtstﬓﬔﬕﬖﬗיִﬞײַﬠﬡﬢﬣﬤﬥﬦﬧﬨ
|
||||
﬩
|
||||
שׁשׂשּׁשּׂאַאָאּבּגּדּהּוּזּטּיּךּכּלּמּנּסּףּפּצּקּרּשּתּוֹבֿכֿפֿﭏﭐﭑﭒﭓﭔﭕﭖﭗﭘﭙﭚﭛﭜﭝﭞﭟﭠﭡﭢﭣﭤﭥﭦﭧﭨﭩﭪﭫﭬﭭﭮﭯﭰﭱﭲﭳﭴﭵﭶﭷﭸﭹﭺﭻﭼﭽﭾﭿﮀﮁﮂﮃﮄﮅﮆﮇﮈﮉﮊﮋﮌﮍﮎﮏﮐﮑﮒﮓﮔﮕﮖﮗﮘﮙﮚﮛﮜﮝﮞﮟﮠﮡﮢﮣﮤﮥﮦﮧﮨﮩﮪﮫﮬﮭﮮﮯﮰﮱﯓﯔﯕﯖﯗﯘﯙﯚﯛﯜﯝﯞﯟﯠﯡﯢﯣﯤﯥﯦﯧﯨﯩﯪﯫﯬﯭﯮﯯﯰﯱﯲﯳﯴﯵﯶﯷﯸﯹﯺﯻﯼﯽﯾﯿﰀﰁﰂﰃﰄﰅﰆﰇﰈﰉﰊﰋﰌﰍﰎﰏﰐﰑﰒﰓﰔﰕﰖﰗﰘﰙﰚﰛﰜﰝﰞﰟﰠﰡﰢﰣﰤﰥﰦﰧﰨﰩﰪﰫﰬﰭﰮﰯﰰﰱﰲﰳﰴﰵﰶﰷﰸﰹﰺﰻﰼﰽﰾﰿﱀﱁﱂﱃﱄﱅﱆﱇﱈﱉﱊﱋﱌﱍﱎﱏﱐﱑﱒﱓﱔﱕﱖﱗﱘﱙﱚﱛﱜﱝﱞﱟﱠﱡﱢﱣﱤﱥﱦﱧﱨﱩﱪﱫﱬﱭﱮﱯﱰﱱﱲﱳﱴﱵﱶﱷﱸﱹﱺﱻﱼﱽﱾﱿﲀﲁﲂﲃﲄﲅﲆﲇﲈﲉﲊﲋﲌﲍﲎﲏﲐﲑﲒﲓﲔﲕﲖﲗﲘﲙﲚﲛﲜﲝﲞﲟﲠﲡﲢﲣﲤﲥﲦﲧﲨﲩﲪﲫﲬﲭﲮﲯﲰﲱﲲﲳﲴﲵﲶﲷﲸﲹﲺﲻﲼﲽﲾﲿﳀﳁﳂﳃﳄﳅﳆﳇﳈﳉﳊﳋﳌﳍﳎﳏﳐﳑﳒﳓﳔﳕﳖﳗﳘﳙﳚﳛﳜﳝﳞﳟﳠﳡﳢﳣﳤﳥﳦﳧﳨﳩﳪﳫﳬﳭﳮﳯﳰﳱﳲﳳﳴﳵﳶﳷﳸﳹﳺﳻﳼﳽﳾﳿﴀﴁﴂﴃﴄﴅﴆﴇﴈﴉﴊﴋﴌﴍﴎﴏﴐﴑﴒﴓﴔﴕﴖﴗﴘﴙﴚﴛﴜﴝﴞﴟﴠﴡﴢﴣﴤﴥﴦﴧﴨﴩﴪﴫﴬﴭﴮﴯﴰﴱﴲﴳﴴﴵﴶﴷﴸﴹﴺﴻﴼﴽ
|
||||
﴾﴿
|
||||
ﵐﵑﵒﵓﵔﵕﵖﵗﵘﵙﵚﵛﵜﵝﵞﵟﵠﵡﵢﵣﵤﵥﵦﵧﵨﵩﵪﵫﵬﵭﵮﵯﵰﵱﵲﵳﵴﵵﵶﵷﵸﵹﵺﵻﵼﵽﵾﵿﶀﶁﶂﶃﶄﶅﶆﶇﶈﶉﶊﶋﶌﶍﶎﶏﶒﶓﶔﶕﶖﶗﶘﶙﶚﶛﶜﶝﶞﶟﶠﶡﶢﶣﶤﶥﶦﶧﶨﶩﶪﶫﶬﶭﶮﶯﶰﶱﶲﶳﶴﶵﶶﶷﶸﶹﶺﶻﶼﶽﶾﶿﷀﷁﷂﷃﷄﷅﷆﷇﷰﷱﷲﷳﷴﷵﷶﷷﷸﷹﷺﷻ
|
||||
﷼﷽
|
||||
︀︁︂︃︄︅︆︇︈︉︊︋︌︍︎️
|
||||
︐︑︒︓︔︕︖︗︘︙
|
||||
︠︡︢︣︤︥︦
|
||||
︰︱︲︳︴︵︶︷︸︹︺︻︼︽︾︿﹀﹁﹂﹃﹄﹅﹆﹇﹈﹉﹊﹋﹌﹍﹎﹏﹐﹑﹒﹔﹕﹖﹗﹘﹙﹚﹛﹜﹝﹞﹟﹠﹡﹢﹣﹤﹥﹦﹨﹩﹪﹫
|
||||
ﹰﹱﹲﹳﹴﹶﹷﹸﹹﹺﹻﹼﹽﹾﹿﺀﺁﺂﺃﺄﺅﺆﺇﺈﺉﺊﺋﺌﺍﺎﺏﺐﺑﺒﺓﺔﺕﺖﺗﺘﺙﺚﺛﺜﺝﺞﺟﺠﺡﺢﺣﺤﺥﺦﺧﺨﺩﺪﺫﺬﺭﺮﺯﺰﺱﺲﺳﺴﺵﺶﺷﺸﺹﺺﺻﺼﺽﺾﺿﻀﻁﻂﻃﻄﻅﻆﻇﻈﻉﻊﻋﻌﻍﻎﻏﻐﻑﻒﻓﻔﻕﻖﻗﻘﻙﻚﻛﻜﻝﻞﻟﻠﻡﻢﻣﻤﻥﻦﻧﻨﻩﻪﻫﻬﻭﻮﻯﻰﻱﻲﻳﻴﻵﻶﻷﻸﻹﻺﻻﻼ
|
||||
!"#$%&'()*+,-./
|
||||
0123456789
|
||||
:;<=>?@
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
[\]^_`
|
||||
abcdefghijklmnopqrstuvwxyz
|
||||
{|}~⦅⦆。「」、・
|
||||
ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚ᅠᄀᄁᆪᄂᆬᆭᄃᄄᄅᆰᆱᆲᆳᆴᆵᄚᄆᄇᄈᄡᄉᄊᄋᄌᄍᄎᄏᄐᄑ하ᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵ
|
||||
¢£¬ ̄¦¥₩│←↑→↓■○<EFBFBD>
|
||||
𐀀
|
|
@ -0,0 +1,6 @@
|
|||
name: 'Search Embedded Form'
|
||||
type: module
|
||||
description: 'Support module for Search module testing of embedded forms.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Test module implementing a form that can be embedded in search results.
|
||||
*
|
||||
* A sample use of an embedded form is an e-commerce site where each search
|
||||
* result may include an embedded form with buttons like "Add to cart" for each
|
||||
* individual product (node) listed in the search results.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Adds the test form to search results.
|
||||
*/
|
||||
function search_embedded_form_preprocess_search_result(&$variables) {
|
||||
$form = \Drupal::formBuilder()->getForm('Drupal\search_embedded_form\Form\SearchEmbeddedForm');
|
||||
$variables['snippet'] = ['#markup' => SafeMarkup::escape($variables['snippet']), $form];
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
search_embedded_form.test_embedded_form:
|
||||
path: '/search_embedded_form'
|
||||
defaults:
|
||||
_title: 'Search_Embed_Form'
|
||||
_form: '\Drupal\search_embedded_form\Form\SearchEmbeddedForm'
|
||||
requirements:
|
||||
_permission: 'search content'
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search_embedded_form\Form\SearchEmbeddedForm.
|
||||
*/
|
||||
|
||||
namespace Drupal\search_embedded_form\Form;
|
||||
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Form controller for search_embedded_form form.
|
||||
*/
|
||||
class SearchEmbeddedForm extends FormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'search_embedded_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$count = \Drupal::state()->get('search_embedded_form.submit_count');
|
||||
|
||||
$form['name'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Your name'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => '',
|
||||
'#required' => TRUE,
|
||||
'#description' => $this->t('Times form has been submitted: %count', array('%count' => $count)),
|
||||
);
|
||||
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Send away'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$state = \Drupal::state();
|
||||
$submit_count = (int) $state->get('search_embedded_form.submit_count');
|
||||
$state->set('search_embedded_form.submit_count', $submit_count + 1);
|
||||
drupal_set_message($this->t('Test form was submitted'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
id: dummy_search_type
|
||||
label: 'Dummy search type'
|
||||
status: true
|
||||
langcode: en
|
||||
path: dummy_path
|
||||
plugin: search_extra_type_search
|
||||
configuration: { }
|
|
@ -0,0 +1,18 @@
|
|||
# Schema for the configuration files of the Search Extra Type module.
|
||||
|
||||
search_extra_type.settings:
|
||||
type: mapping
|
||||
label: 'Test search type settings'
|
||||
mapping:
|
||||
boost:
|
||||
type: string
|
||||
label: 'String'
|
||||
|
||||
# Plugin \Drupal\search_extra_type\Plugin\Search\SearchExtraTypeSearch
|
||||
search.plugin.search_extra_type_search:
|
||||
type: mapping
|
||||
label: 'Extra type settings'
|
||||
mapping:
|
||||
boost:
|
||||
type: string
|
||||
label: 'Boost method'
|
|
@ -0,0 +1,9 @@
|
|||
name: 'Test Search Type'
|
||||
type: module
|
||||
description: 'Support module for Search module testing.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- test_page_test
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\search_extra_type\Plugin\Search\SearchExtraTypeSearch.
|
||||
*/
|
||||
|
||||
namespace Drupal\search_extra_type\Plugin\Search;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Routing\UrlGeneratorTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\search\Plugin\ConfigurableSearchPluginBase;
|
||||
|
||||
/**
|
||||
* Executes a dummy keyword search.
|
||||
*
|
||||
* @SearchPlugin(
|
||||
* id = "search_extra_type_search",
|
||||
* title = @Translation("Dummy search type")
|
||||
* )
|
||||
*/
|
||||
class SearchExtraTypeSearch extends ConfigurableSearchPluginBase {
|
||||
|
||||
use UrlGeneratorTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSearch($keywords, array $parameters, array $attributes) {
|
||||
if (empty($parameters['search_conditions'])) {
|
||||
$parameters['search_conditions'] = '';
|
||||
}
|
||||
parent::setSearch($keywords, $parameters, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the given parameters are valid enough to execute a search for.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if there are keywords or search conditions in the query.
|
||||
*/
|
||||
public function isSearchExecutable() {
|
||||
return (bool) ($this->keywords || !empty($this->searchParameters['search_conditions']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the search.
|
||||
*
|
||||
* This is a dummy search, so when search "executes", we just return a dummy
|
||||
* result containing the keywords and a list of conditions.
|
||||
*
|
||||
* @return array
|
||||
* A structured list of search results
|
||||
*/
|
||||
public function execute() {
|
||||
$results = array();
|
||||
if (!$this->isSearchExecutable()) {
|
||||
return $results;
|
||||
}
|
||||
return array(
|
||||
array(
|
||||
'link' => Url::fromRoute('test_page_test.test_page')->toString(),
|
||||
'type' => 'Dummy result type',
|
||||
'title' => 'Dummy title',
|
||||
'snippet' => SafeMarkup::set("Dummy search snippet to display. Keywords: {$this->keywords}\n\nConditions: " . print_r($this->searchParameters, TRUE)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildResults() {
|
||||
$results = $this->execute();
|
||||
$output['prefix']['#markup'] = '<h2>Test page text is here</h2> <ol class="search-results">';
|
||||
|
||||
foreach ($results as $entry) {
|
||||
$output[] = array(
|
||||
'#theme' => 'search_result',
|
||||
'#result' => $entry,
|
||||
'#plugin_id' => 'search_extra_type_search',
|
||||
);
|
||||
}
|
||||
$pager = array(
|
||||
'#type' => 'pager',
|
||||
);
|
||||
$output['suffix']['#markup'] = '</ol>' . drupal_render($pager);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
|
||||
// Output form for defining rank factor weights.
|
||||
$form['extra_type_settings'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Extra type settings'),
|
||||
'#tree' => TRUE,
|
||||
);
|
||||
|
||||
$form['extra_type_settings']['boost'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Boost method'),
|
||||
'#options' => array(
|
||||
'bi' => t('Bistromathic'),
|
||||
'ii' => t('Infinite Improbability'),
|
||||
),
|
||||
'#default_value' => $this->configuration['boost'],
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->configuration['boost'] = $form_state->getValue(array('extra_type_settings', 'boost'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defaultConfiguration() {
|
||||
return array(
|
||||
'boost' => 'bi',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
name: 'Test search entity langcode'
|
||||
type: module
|
||||
description: 'Support module for search module testing.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Test module setting up two tests, one for checking if the entity $langcode is
|
||||
* being passed on and another one sets up the alternate verb forms for the
|
||||
* stemming test.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_search_preprocess().
|
||||
*/
|
||||
function search_langcode_test_search_preprocess($text, $langcode = NULL) {
|
||||
if (isset($langcode) && $langcode == 'en') {
|
||||
// Add the alternate verb forms for the word "testing".
|
||||
if ($text == 'we are testing') {
|
||||
$text .= ' test tested';
|
||||
}
|
||||
// Prints the langcode for testPreprocessLangcode() and adds some
|
||||
// extra text.
|
||||
else {
|
||||
drupal_set_message('Langcode Preprocess Test: ' . $langcode);
|
||||
$text .= 'Additional text';
|
||||
}
|
||||
}
|
||||
// Prints the langcode for testPreprocessLangcode().
|
||||
elseif (isset($langcode)) {
|
||||
drupal_set_message('Langcode Preprocess Test: ' . $langcode);
|
||||
|
||||
// Preprocessing for the excerpt test.
|
||||
if ($langcode == 'ex') {
|
||||
$text = str_replace('finding', 'find', $text);
|
||||
$text = str_replace('finds', 'find', $text);
|
||||
$text = str_replace('dic', ' dependency injection container', $text);
|
||||
$text = str_replace('hypertext markup language', 'html', $text);
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
name: 'Test Search Query Alter'
|
||||
type: module
|
||||
description: 'Support module for Search module testing.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
use Drupal\Core\Database\Query\AlterableInterface;
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Test module that alters search queries.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_query_TAG_alter(): tag search_$type with $type node_search.
|
||||
*/
|
||||
function search_query_alter_query_search_node_search_alter(AlterableInterface $query) {
|
||||
// For testing purposes, restrict the query to node type 'article' only.
|
||||
$query->condition('n.type', 'article');
|
||||
}
|
284
core/modules/search/tests/src/Unit/SearchPageRepositoryTest.php
Normal file
284
core/modules/search/tests/src/Unit/SearchPageRepositoryTest.php
Normal file
|
@ -0,0 +1,284 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Tests\search\Unit\SearchPageRepositoryTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\search\Unit;
|
||||
|
||||
use Drupal\search\Entity\SearchPage;
|
||||
use Drupal\search\SearchPageRepository;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\search\SearchPageRepository
|
||||
* @group search
|
||||
*/
|
||||
class SearchPageRepositoryTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The search page repository.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageRepository
|
||||
*/
|
||||
protected $searchPageRepository;
|
||||
|
||||
/**
|
||||
* The entity query object.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* The search page storage.
|
||||
*
|
||||
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* The config factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
$this->query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface');
|
||||
|
||||
$this->storage = $this->getMock('Drupal\Core\Config\Entity\ConfigEntityStorageInterface');
|
||||
$this->storage->expects($this->any())
|
||||
->method('getQuery')
|
||||
->will($this->returnValue($this->query));
|
||||
|
||||
$entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
|
||||
$entity_manager->expects($this->any())
|
||||
->method('getStorage')
|
||||
->will($this->returnValue($this->storage));
|
||||
|
||||
$this->configFactory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
|
||||
$this->searchPageRepository = new SearchPageRepository($this->configFactory, $entity_manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the getActiveSearchPages() method.
|
||||
*/
|
||||
public function testGetActiveSearchPages() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test')));
|
||||
|
||||
$entities = array();
|
||||
$entities['test'] = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$this->storage->expects($this->once())
|
||||
->method('loadMultiple')
|
||||
->with(array('test' => 'test', 'other_test' => 'other_test'))
|
||||
->will($this->returnValue($entities));
|
||||
|
||||
$result = $this->searchPageRepository->getActiveSearchPages();
|
||||
$this->assertSame($entities, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the isSearchActive() method.
|
||||
*/
|
||||
public function testIsSearchActive() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('range')
|
||||
->with(0, 1)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(array('test' => 'test')));
|
||||
|
||||
$this->assertSame(TRUE, $this->searchPageRepository->isSearchActive());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the getIndexableSearchPages() method.
|
||||
*/
|
||||
public function testGetIndexableSearchPages() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test')));
|
||||
|
||||
$entities = array();
|
||||
$entities['test'] = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$entities['test']->expects($this->once())
|
||||
->method('isIndexable')
|
||||
->will($this->returnValue(TRUE));
|
||||
$entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$entities['other_test']->expects($this->once())
|
||||
->method('isIndexable')
|
||||
->will($this->returnValue(FALSE));
|
||||
$this->storage->expects($this->once())
|
||||
->method('loadMultiple')
|
||||
->with(array('test' => 'test', 'other_test' => 'other_test'))
|
||||
->will($this->returnValue($entities));
|
||||
|
||||
$result = $this->searchPageRepository->getIndexableSearchPages();
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertSame($entities['test'], reset($result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the clearDefaultSearchPage() method.
|
||||
*/
|
||||
public function testClearDefaultSearchPage() {
|
||||
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$config->expects($this->once())
|
||||
->method('clear')
|
||||
->with('default_page')
|
||||
->will($this->returnValue($config));
|
||||
$this->configFactory->expects($this->once())
|
||||
->method('getEditable')
|
||||
->with('search.settings')
|
||||
->will($this->returnValue($config));
|
||||
$this->searchPageRepository->clearDefaultSearchPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the getDefaultSearchPage() method when the default is active.
|
||||
*/
|
||||
public function testGetDefaultSearchPageWithActiveDefault() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test')));
|
||||
|
||||
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$config->expects($this->once())
|
||||
->method('get')
|
||||
->with('default_page')
|
||||
->will($this->returnValue('test'));
|
||||
$this->configFactory->expects($this->once())
|
||||
->method('get')
|
||||
->with('search.settings')
|
||||
->will($this->returnValue($config));
|
||||
|
||||
$this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the getDefaultSearchPage() method when the default is inactive.
|
||||
*/
|
||||
public function testGetDefaultSearchPageWithInactiveDefault() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(array('test' => 'test')));
|
||||
|
||||
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$config->expects($this->once())
|
||||
->method('get')
|
||||
->with('default_page')
|
||||
->will($this->returnValue('other_test'));
|
||||
$this->configFactory->expects($this->once())
|
||||
->method('get')
|
||||
->with('search.settings')
|
||||
->will($this->returnValue($config));
|
||||
|
||||
$this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the setDefaultSearchPage() method.
|
||||
*/
|
||||
public function testSetDefaultSearchPage() {
|
||||
$id = 'bananas';
|
||||
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$config->expects($this->once())
|
||||
->method('set')
|
||||
->with('default_page', $id)
|
||||
->will($this->returnValue($config));
|
||||
$config->expects($this->once())
|
||||
->method('save')
|
||||
->will($this->returnValue($config));
|
||||
$this->configFactory->expects($this->once())
|
||||
->method('getEditable')
|
||||
->with('search.settings')
|
||||
->will($this->returnValue($config));
|
||||
|
||||
$search_page = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$search_page->expects($this->once())
|
||||
->method('id')
|
||||
->will($this->returnValue($id));
|
||||
$search_page->expects($this->once())
|
||||
->method('enable')
|
||||
->will($this->returnValue($search_page));
|
||||
$search_page->expects($this->once())
|
||||
->method('save')
|
||||
->will($this->returnValue($search_page));
|
||||
$this->searchPageRepository->setDefaultSearchPage($search_page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the sortSearchPages() method.
|
||||
*/
|
||||
public function testSortSearchPages() {
|
||||
$entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
|
||||
$entity_type->expects($this->any())
|
||||
->method('getClass')
|
||||
->will($this->returnValue('Drupal\Tests\search\Unit\TestSearchPage'));
|
||||
$this->storage->expects($this->once())
|
||||
->method('getEntityType')
|
||||
->will($this->returnValue($entity_type));
|
||||
|
||||
// Declare entities out of their expected order so we can be sure they were
|
||||
// sorted. We cannot mock these because of uasort(), see
|
||||
// https://bugs.php.net/bug.php?id=50688.
|
||||
$unsorted_entities['test4'] = new TestSearchPage(array('weight' => 0, 'status' => FALSE, 'label' => 'Test4'));
|
||||
$unsorted_entities['test3'] = new TestSearchPage(array('weight' => 10, 'status' => TRUE, 'label' => 'Test3'));
|
||||
$unsorted_entities['test2'] = new TestSearchPage(array('weight' => 0, 'status' => TRUE, 'label' => 'Test2'));
|
||||
$unsorted_entities['test1'] = new TestSearchPage(array('weight' => 0, 'status' => TRUE, 'label' => 'Test1'));
|
||||
$expected = $unsorted_entities;
|
||||
ksort($expected);
|
||||
|
||||
$sorted_entities = $this->searchPageRepository->sortSearchPages($unsorted_entities);
|
||||
$this->assertSame($expected, $sorted_entities);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TestSearchPage extends SearchPage {
|
||||
public function __construct(array $values) {
|
||||
foreach ($values as $key => $value) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
public function label($langcode = NULL) {
|
||||
return $this->label;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Tests\search\Unit\SearchPluginCollectionTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\search\Unit;
|
||||
|
||||
use Drupal\search\Plugin\SearchPluginCollection;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\search\Plugin\SearchPluginCollection
|
||||
* @group search
|
||||
*/
|
||||
class SearchPluginCollectionTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The mocked plugin manager.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $pluginManager;
|
||||
|
||||
/**
|
||||
* The tested plugin collection.
|
||||
*
|
||||
* @var \Drupal\search\Plugin\SearchPluginCollection
|
||||
*/
|
||||
protected $searchPluginCollection;
|
||||
|
||||
/**
|
||||
* Stores all setup plugin instances.
|
||||
*
|
||||
* @var \Drupal\search\Plugin\SearchInterface[]
|
||||
*/
|
||||
protected $pluginInstances;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
$this->pluginManager = $this->getMock('Drupal\Component\Plugin\PluginManagerInterface');
|
||||
$this->searchPluginCollection = new SearchPluginCollection($this->pluginManager, 'banana', array('id' => 'banana', 'color' => 'yellow'), 'fruit_stand');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get() method.
|
||||
*/
|
||||
public function testGet() {
|
||||
$plugin = $this->getMock('Drupal\search\Plugin\SearchInterface');
|
||||
$this->pluginManager->expects($this->once())
|
||||
->method('createInstance')
|
||||
->will($this->returnValue($plugin));
|
||||
$this->assertSame($plugin, $this->searchPluginCollection->get('banana'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get() method with a configurable plugin.
|
||||
*/
|
||||
public function testGetWithConfigurablePlugin() {
|
||||
$plugin = $this->getMock('Drupal\search\Plugin\ConfigurableSearchPluginInterface');
|
||||
$plugin->expects($this->once())
|
||||
->method('setSearchPageId')
|
||||
->with('fruit_stand')
|
||||
->will($this->returnValue($plugin));
|
||||
|
||||
$this->pluginManager->expects($this->once())
|
||||
->method('createInstance')
|
||||
->will($this->returnValue($plugin));
|
||||
|
||||
$this->assertSame($plugin, $this->searchPluginCollection->get('banana'));
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue