contrib modules updates

This commit is contained in:
2019-02-27 10:39:59 +01:00
parent 04a4b8895d
commit e3cf889820
579 changed files with 18343 additions and 4076 deletions
@@ -1,3 +1,74 @@
Search API 1.11 (2018-11-18):
-----------------------------
- #3012778 by Shashwat Purav, pamatt, drunken monkey: Fixed several doc comment
typos.
- #3011340 by pfrenssen, idimopoulos, drunken monkey: Fixed problem when
updating multiple indexes' datasource settings via "drush cim".
- #2986623 by drunken monkey, valic: Added the "Reverse entity references"
processor.
- #3009136 by drunken monkey, mkalkbrenner, borisson_: Added the
"search_api_retrieved_field_values" query option.
- #3006267 by idebr, drunken monkey: Removed conditional code for Drupal 8.5.
- #3007311 by drunken monkey: Fixed catch clause in
PostRequestIndexing::destruct().
- #2994022 by drunken monkey, Watergate, borisson_: Fixed facets when there are
no search results in Database backend.
- #2898917 by Lennard Westerveld, RoSk0, drunken monkey: Fixed Views filters
with checkboxes and "is all of" operator.
- #3001259 by klausi, drunken monkey, borisson_: Fixed HTML escaping throughout
the module.
- #3001030 by mkalkbrenner, drunken monkey, mvantuch: Fixed excerpt not taking
Solr filters into account.
- #3004017 by idebr, drunken monkey: Fixed server/index config schemas to use
UUID field from Core definition.
- #2971619 by idebr, drunken monkey: Refactored the access check for the
"Execute pending tasks" local action.
- #2995342 by AndrewsizZ, drunken monkey, borisson_: Added stripping of
script/style HTML tags in excerpts to Highlight processor.
Search API 1.10 (2018-09-17):
-----------------------------
- #2993787 by drunken monkey, borisson_, mkalkbrenner: Added the possibility to
boost fields in MLT queries.
- #2936631 by drunken monkey, borisson_: Added support for the "no_ui"
definition key for plugins.
- #2996936 by drunken monkey, borisson_: Added suggested packages to
composer.json.
- #2987156 by hchonov, drunken monkey, borisson_: Fixed missing dependency on
modules providing fields.
- #2994638 by drunken monkey, Grayle, borisson_: Fixed warning in aggregated
fields without values.
- #2987237 by quicksketch, drunken monkey, borisson_: Fixed requirements hook
for the Taxonomy Term Handlers module preventing updates from running.
- #2992662 by capysara, drunken monkey, borisson_: Fixed the reindexing link
when saving index processors or fields.
- #2989689 by drunken monkey, valthebald, borisson_: Fixed bug in Views cache
plugin alter hook.
- #2919676 by drunken monkey, borisson_: Fixed server entity link definitions.
- #2996278 by drunken monkey: Removed one pointless variable.
- #2987245 by drunken monkey: Fixed use of account_switcher service for
switching the currently active account.
- #2996907 by zerolab, drunken monkey: Fixed entity tracking using access
checks.
- #2993434 by jasonawant, drunken monkey: Fixed handling of comma-separated
values in Views contextual filters.
- #2983843 by drunken monkey, borisson_: Fixed suggestion of installing default
module when dependencies aren't met.
- #2979316 by drunken monkey, Sutharsan: Fixed highlighting problems with
multi-byte characters.
- #2986310 by capysara, drunken monkey: Added link to reindexing when saving
fields.
- #2986013 by tim-diels, drunken monkey, borisson_: Fixed casting of
nun-numeric strings in the database backend.
- #2983630 by chipway, borisson_, drunken monkey: Fixed format of System
dependency in search_api.info.yml.
- #2982660 by jhedstrom, drunken monkey, borisson_: Fixed indexed time of
date-only fields.
- #2986216 by drunken monkey: Added the "legacy" group to
SearchApiDbUpdate8102Test.
- #2920943 by drunken monkey: Fixed error handling for "Rendered item"
processor.
Search API 1.9 (2018-07-19):
----------------------------
- #2956012 by drunken monkey, DuaelFr: Added uncustomized view modes as options
@@ -84,7 +84,7 @@ framework.
- Datasources: src/Datasource
- Data types: src/DataType
- Displays: src/Display
- ParseModes: src/ParseMode
- Parse modes: src/ParseMode
- Processors: src/Processor
- Trackers: src/Tracker
The display plugins are a bit of a special case there, because they aren't
@@ -106,6 +106,32 @@ module's issue regarding potential module-specific changes to that policy [7].
[6] https://www.drupal.org/core/d8-bc-policy
[7] https://www.drupal.org/node/2871549
SERVER BACKEND FEATURES
-----------------------
Server backend features are a way for other contrib modules to cleanly define
ways in which the Search API can be extended. For more information, see [8].
[8] https://www.drupal.org/docs/8/modules/search-api/getting-started/server-backends-and-features
The Search API module itself currently defines one feature:
- More Like This (search_api_mlt)
This feature can be used to retrieve a list of search results that are similar
to a given indexed item. A backend that supports this plugin has to recognize
the "search_api_mlt" query option. If present, it contains an associative
array with the following keys:
- id: The Search API item ID (consisting of the datasource ID and the
datasource-specific item ID passing a plain entity ID will NOT work!) of
the item for which similar results should be found.
- fields: A simple array of fields which should be used for determining
similarity. Backends can choose to ignore this field.
- "field boosts": (optional) An associative array mapping fields to a numeric
"boost" value that determines how important they should be considered when
determining similarity. Backends can choose to ignore this field.
The feature can be used in the UI via the "More like this" Views contextual
filter.
MAINTAINERS
-----------
Current maintainers:
@@ -26,6 +26,11 @@
"require-dev": {
"drupal/search_api_autocomplete": "@dev"
},
"suggest": {
"drupal/facets": "Adds the ability to create faceted searches.",
"drupal/search_api_autocomplete": "Allows adding autocomplete suggestions to search fields.",
"drupal/search_api_solr": "Adds support for using Apache Solr as a backend."
},
"extra": {
"drush": {
"services": {
@@ -8,9 +8,6 @@ search_api.index.*:
name:
type: label
label: Name'
uuid:
type: string
label: 'UUID'
description:
type: text
label: 'Description'
@@ -8,9 +8,6 @@ search_api.server.*:
name:
type: label
label: 'Name'
uuid:
type: string
label: 'UUID'
description:
type: text
label: 'Description'
@@ -6,8 +6,8 @@ package: Search
dependencies:
- search_api:search_api
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -13,8 +13,8 @@ dependencies:
- drupal:views
- search_api:search_api_db
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -5,7 +5,7 @@
* Install, update and uninstall functions for the DB Search Defaults module.
*/
use Drupal\node\Entity\NodeType;
use Drupal\search_api\IndexListBuilder;
/**
* Implements hook_requirements().
@@ -14,60 +14,12 @@ function search_api_db_defaults_requirements($phase) {
$requirements = [];
if ($phase == 'install') {
$node_types = NodeType::loadMultiple();
$required_types = [
'article' => ['body', 'comment', 'field_tags', 'field_image'],
'page' => ['body'],
];
/** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */
$entity_field_manager = \Drupal::service('entity_field.manager');
foreach ($required_types as $required_type_id => $required_fields) {
if (!isset($node_types[$required_type_id])) {
$requirements['search_api_db_defaults:' . $required_type_id] = [
'severity' => REQUIREMENT_ERROR,
'description' => t('Content type @content_type not found. Database Search Defaults module could not be installed.', ['@content_type' => $required_type_id]),
];
}
else {
// Check if all the fields are here.
$fields = $entity_field_manager->getFieldDefinitions('node', $required_type_id);
foreach ($required_fields as $required_field) {
if (!isset($fields[$required_field])) {
$requirements['search_api_db_defaults:' . $required_type_id . ':' . $required_field] = [
'severity' => REQUIREMENT_ERROR,
'description' => t('Field @field in content type @node_type not found. Database Search Defaults module could not be installed', ['@node_type' => $required_type_id, '@field' => $required_field]),
];
}
}
}
}
if (\Drupal::moduleHandler()->moduleExists('search_api_db')) {
$entities_to_check = [
'search_api_index' => 'default_index',
'search_api_server' => 'default_server',
'view' => 'search_content',
$errors = IndexListBuilder::checkDefaultsModuleCanBeInstalled();
foreach ($errors as $type => $error) {
$requirements["search_api_db_defaults:$type"] = [
'severity' => REQUIREMENT_ERROR,
'description' => $error,
];
/** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
$entity_type_manager = \Drupal::service('entity_type.manager');
foreach ($entities_to_check as $entity_type => $entity_id) {
// Find out if the entity is already in place. If so, fail to install the
// module.
$entity_storage = $entity_type_manager->getStorage($entity_type);
$entity_storage->resetCache();
$entity = $entity_storage->load($entity_id);
if ($entity) {
$requirements['search_api_db_defaults:defaults_exist'] = [
'severity' => REQUIREMENT_ERROR,
'description' => t('It looks like the default setup provided by this module already exists on your site. Cannot re-install module.'),
];
break;
}
}
}
}
@@ -3,7 +3,6 @@
namespace Drupal\search_api_db\Plugin\search_api\backend;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Database as CoreDatabase;
@@ -1166,7 +1165,7 @@ class Database extends BackendPluginBase implements PluginFormInterface {
}
catch (\Exception $e) {
// We just log the error, hoping we can index the other items.
$this->getLogger()->warning(Html::escape($e->getMessage()));
$this->getLogger()->warning($e->getMessage());
}
}
return $indexed;
@@ -1405,8 +1404,8 @@ class Database extends BackendPluginBase implements PluginFormInterface {
* @param mixed $value
* The value to convert.
* @param string $type
* The type to convert to. One of the keys from
* search_api_default_field_types().
* The Search API type to convert to. (Has to be a type supported by this
* backend.)
* @param string $original_type
* The value's original type.
* @param \Drupal\search_api\IndexInterface $index
@@ -1471,7 +1470,6 @@ class Database extends BackendPluginBase implements PluginFormInterface {
return $tokens;
case 'string':
case 'uri':
// For non-dates, PHP can handle this well enough.
if ($original_type == 'date') {
return date('c', $value);
@@ -1484,9 +1482,10 @@ class Database extends BackendPluginBase implements PluginFormInterface {
case 'integer':
case 'date':
case 'duration':
return (int) $value;
case 'decimal':
return 0 + $value;
return (float) $value;
case 'boolean':
return $value ? 1 : 0;
@@ -1602,16 +1601,21 @@ class Database extends BackendPluginBase implements PluginFormInterface {
$results = $query->getResults();
$skip_count = $query->getOption('skip result count');
$count = NULL;
if (!$skip_count) {
$count_query = $db_query->countQuery();
$results->setResultCount($count_query->execute()->fetchField());
$count = $count_query->execute()->fetchField();
$results->setResultCount($count);
}
if ($skip_count || $results->getResultCount()) {
if ($query->getOption('search_api_facets')) {
$results->setExtraData('search_api_facets', $this->getFacets($query, clone $db_query));
}
// With a "min_count" of 0, some facets can even be available if there are
// no results.
if ($query->getOption('search_api_facets')) {
$facets = $this->getFacets($query, clone $db_query, $count);
$results->setExtraData('search_api_facets', $facets);
}
// Everything else can be skipped if the count is 0.
if ($skip_count || $count) {
$query_options = $query->getOptions();
if (isset($query_options['offset']) || isset($query_options['limit'])) {
$offset = isset($query_options['offset']) ? $query_options['offset'] : 0;
@@ -2353,16 +2357,13 @@ class Database extends BackendPluginBase implements PluginFormInterface {
* The search query for which facets should be computed.
* @param \Drupal\Core\Database\Query\SelectInterface $db_query
* A database select query which returns all results of that search query.
* @param int|null $result_count
* (optional) The total number of results of the search query, if known.
*
* @return array
* An array of facets, as specified by the search_api_facets feature.
*/
protected function getFacets(QueryInterface $query, SelectInterface $db_query) {
$table = $this->getTemporaryResultsTable($db_query);
if (!$table) {
return [];
}
protected function getFacets(QueryInterface $query, SelectInterface $db_query, $result_count = NULL) {
$fields = $this->getFieldInfo($query->getIndex());
$ret = [];
foreach ($query->getOption('search_api_facets') as $key => $facet) {
@@ -2374,8 +2375,33 @@ class Database extends BackendPluginBase implements PluginFormInterface {
$field = $fields[$facet['field']];
if (empty($facet['operator']) || $facet['operator'] != 'or') {
// All the AND facets can use the main query.
$select = $this->database->select($table, 't');
// First, check whether this can even possibly have any results.
if ($result_count !== NULL && $result_count < $facet['min_count']) {
continue;
}
// All the AND facets can use the main query. If we didn't yet create a
// temporary table for them yet, do so now.
if (!isset($table)) {
$table = $this->getTemporaryResultsTable($db_query);
}
if ($table) {
$select = $this->database->select($table, 't');
}
else {
// If no temporary table could be created (most likely due to a
// missing permission), use a nested query instead.
$select = $this->database->select(clone $db_query, 't');
}
// In case we didn't get the result count passed to the method, we can
// get it now. (This allows us to skip AND facets with a min_count
// higher than the result count.)
if ($result_count === NULL) {
$result_count = $select->countQuery()->execute()->fetchField();
if ($result_count < $facet['min_count']) {
continue;
}
}
}
else {
// For OR facets, we need to build a different base query that excludes
@@ -2388,7 +2414,13 @@ class Database extends BackendPluginBase implements PluginFormInterface {
unset($conditions[$i]);
}
}
$or_db_query = $this->createDbQuery($or_query, $fields);
try {
$or_db_query = $this->createDbQuery($or_query, $fields);
}
catch (SearchApiException $e) {
$this->logException($e, '%type while trying to create a facets query: @message in %function (line %line of %file).');
continue;
}
$select = $this->database->select($or_db_query, 't');
}
@@ -9,6 +9,7 @@ use Drupal\search_api_db\Tests\DatabaseTestsTrait;
* Tests whether search_api_db_update_8102() works correctly.
*
* @group search_api
* @group legacy
*
* @see https://www.drupal.org/node/2884451
*/
@@ -8,8 +8,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -4,6 +4,7 @@ namespace Drupal\Tests\search_api_db\Kernel;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\Database as CoreDatabase;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Entity\Server;
use Drupal\search_api\IndexInterface;
@@ -93,6 +94,7 @@ class BackendTest extends BackendTestBase {
$this->regressionTest2926733();
$this->regressionTest2938646();
$this->regressionTest2925464();
$this->regressionTest2994022();
}
/**
@@ -637,6 +639,54 @@ class BackendTest extends BackendTestBase {
$this->indexItems($index->id());
}
/**
* Tests changing of field types.
*
* @see https://www.drupal.org/project/search_api/issues/2994022
*/
protected function regressionTest2994022() {
$query = $this->buildSearch('nonexistent_search_term');
$facets['category'] = [
'field' => 'category',
'limit' => 0,
'min_count' => 0,
'missing' => FALSE,
'operator' => 'and',
];
$query->setOption('search_api_facets', $facets);
$results = $query->execute();
$this->assertResults([], $results, 'Non-existent keyword');
$expected = [
['count' => 0, 'filter' => '"article_category"'],
['count' => 0, 'filter' => '"item_category"'],
];
$category_facets = $results->getExtraData('search_api_facets')['category'];
usort($category_facets, [$this, 'facetCompare']);
$this->assertEquals($expected, $category_facets, 'Correct facets were returned for minimum count 0');
$query = $this->buildSearch('nonexistent_search_term');
$conditions = $query->createConditionGroup('AND', ['facet:category']);
$conditions->addCondition('category', 'article_category');
$query->addConditionGroup($conditions);
$facets['category'] = [
'field' => 'category',
'limit' => 0,
'min_count' => 0,
'missing' => FALSE,
'operator' => 'and',
];
$query->setOption('search_api_facets', $facets);
$results = $query->execute();
$this->assertResults([], $results, 'Non-existent keyword with filter');
$expected = [
['count' => 0, 'filter' => '"article_category"'],
['count' => 0, 'filter' => '"item_category"'],
];
$category_facets = $results->getExtraData('search_api_facets')['category'];
usort($category_facets, [$this, 'facetCompare']);
$this->assertEquals($expected, $category_facets, 'Correct facets were returned for minimum count 0');
}
/**
* {@inheritdoc}
*/
@@ -723,7 +773,7 @@ class BackendTest extends BackendTestBase {
$expected = [
'search_api_db_database_search_index' => 'search_api_db_database_search_index',
];
$this->assertEquals($expected, $tables, 'All the tables of the the Database Search module have been removed.');
$this->assertEquals($expected, $tables, 'All the tables of the Database Search module have been removed.');
}
/**
@@ -850,14 +900,20 @@ class BackendTest extends BackendTestBase {
// Test different input values, similar to @dataProvider (but with less
// overhead).
$t = 1400000000;
$f = 'Y-m-d H:i:s';
$date_time_format = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
$date_format = DateTimeItemInterface::DATE_STORAGE_FORMAT;
$test_values = [
'null' => [NULL, NULL],
'timestamp' => [$t, $t],
'string timestamp' => ["$t", $t],
'float timestamp' => [$t + 0.12, $t],
'date string' => [gmdate($f, $t), $t],
'date string with timezone' => [date($f . 'P', $t), $t],
'date string' => [gmdate($date_time_format, $t), $t],
'date string with timezone' => [date($date_time_format . 'P', $t), $t],
'date only' => [
date($date_format, $t),
// Date-only fields are stored with the default time (12:00:00).
strtotime(date($date_format, $t) . 'T12:00:00+00:00'),
],
];
// Get storage information for quickly checking the indexed value.
@@ -11,8 +11,8 @@ dependencies:
- drupal:taxonomy
- drupal:views
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -10,15 +10,20 @@ use Drupal\Core\Url;
/**
* Implements hook_requirements().
*/
function search_api_views_taxonomy_requirements() {
$requirements['search_api_views_taxonomy'] = [
'title' => t('Search API Taxonomy Term Handlers'),
'value' => t('Out-dated'),
'description' => t('All functionality of the "Search API Taxonomy Term Handlers" module has been moved into the Search API module. You should <a href=":uninstall">uninstall this module</a>.', [
':uninstall' => Url::fromRoute('system.modules_uninstall')->toString(),
]),
'severity' => REQUIREMENT_WARNING,
];
function search_api_views_taxonomy_requirements($phase) {
$requirements = [];
// We don't want to prevent update.php from running.
if ($phase !== 'update') {
$requirements['search_api_views_taxonomy'] = [
'title' => t('Search API Taxonomy Term Handlers'),
'value' => t('Out-dated'),
'description' => t('All functionality of the "Search API Taxonomy Term Handlers" module has been moved into the Search API module. You should <a href=":uninstall">uninstall this module</a>.', [
':uninstall' => Url::fromRoute('system.modules_uninstall')->toString(),
]),
'severity' => REQUIREMENT_WARNING,
];
}
return $requirements;
}
@@ -5,10 +5,10 @@ package: Search
# core: 8.x
configure: search_api.overview
dependencies:
- system (>=8.5)
- drupal:system (>=8.5)
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -12,8 +12,7 @@ search_api.execute_tasks:
_controller: '\Drupal\search_api\Controller\TaskController::executeTasks'
_title: 'Execute pending tasks'
requirements:
_permission: 'administer search_api'
_search_api_tasks: 'TRUE'
_custom_access: '\Drupal\search_api\Controller\TaskController::executeTasksAccess'
entity.search_api_server.add_form:
path: '/admin/config/search/search-api/add-server'
@@ -1,10 +1,4 @@
services:
access_check.search_api_tasks:
class: Drupal\search_api\Controller\ExecuteTasksAccessCheck
arguments: ['@search_api.task_manager']
tags:
- { name: access_check, applies_to: _search_api_tasks }
logger.channel.search_api:
parent: logger.channel_base
arguments: ['search_api']
@@ -119,7 +119,8 @@ function search_api_views_plugins_cache_alter(array &$plugins) {
foreach (Index::loadMultiple() as $index) {
$bases[] = 'search_api_index_' . $index->id();
}
$plugins['search_api']['base'] = $bases;
$plugins['search_api_tag']['base'] = $bases;
$plugins['search_api_time']['base'] = $bases;
}
/**
@@ -1,46 +0,0 @@
<?php
namespace Drupal\search_api\Controller;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\search_api\Task\TaskManagerInterface;
/**
* Provides an access check for the "Execute pending tasks" route.
*/
class ExecuteTasksAccessCheck implements AccessInterface {
/**
* The tasks manager service.
*
* @var \Drupal\search_api\Task\TaskManagerInterface
*/
protected $tasksManager;
/**
* Creates an ExecuteTasksAccessCheck object.
*
* @param \Drupal\search_api\Task\TaskManagerInterface $tasksManager
* The tasks manager service.
*/
public function __construct(TaskManagerInterface $tasksManager) {
$this->tasksManager = $tasksManager;
}
/**
* Checks access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access() {
// @todo Once #2722237 is fixed, see whether this can't just use the
// "search_api_task_list" cache tag instead.
if ($this->tasksManager->getTasksCount()) {
return AccessResult::allowed()->setCacheMaxAge(0);
}
return AccessResult::forbidden()->setCacheMaxAge(0);
}
}
@@ -2,7 +2,9 @@
namespace Drupal\search_api\Controller;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\search_api\Task\TaskManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -62,4 +64,23 @@ class TaskController extends ControllerBase {
return batch_process(Url::fromRoute('search_api.overview'));
}
/**
* Checks access for executing pending tasks.
*
* @param \Drupal\Core\Session\AccountInterface $account
* Run access checks for this account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function executeTasksAccess(AccountInterface $account) {
// @todo Once we depend on Drupal 8.7+, check whether this can't just use
// the "search_api_task_list" cache tag instead. (See #2722237.)
if ($this->taskManager->getTasksCount()) {
return AccessResult::allowedIfHasPermission($account, 'administer search_api')
->setCacheMaxAge(0);
}
return AccessResult::forbidden()->setCacheMaxAge(0);
}
}
@@ -5,6 +5,7 @@ namespace Drupal\search_api\DataType;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\search_api\Plugin\HideablePluginInterface;
/**
* Defines an interface for data type plugins.
@@ -14,7 +15,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
* @see \Drupal\search_api\DataType\DataTypePluginBase
* @see plugin_api
*/
interface DataTypeInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ContainerFactoryPluginInterface {
interface DataTypeInterface extends HideablePluginInterface, PluginInspectionInterface, DerivativeInspectionInterface, ContainerFactoryPluginInterface {
/**
* Returns the label of the data type.
@@ -2,7 +2,7 @@
namespace Drupal\search_api\DataType;
use Drupal\Core\Plugin\PluginBase;
use Drupal\search_api\Plugin\HideablePluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -40,7 +40,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @see \Drupal\search_api\DataType\DataTypeInterface
* @see plugin_api
*/
abstract class DataTypePluginBase extends PluginBase implements DataTypeInterface {
abstract class DataTypePluginBase extends HideablePluginBase implements DataTypeInterface {
/**
* {@inheritdoc}
@@ -6,6 +6,7 @@ use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\search_api\Plugin\HideablePluginInterface;
/**
* Defines an interface for display plugins.
@@ -15,7 +16,7 @@ use Drupal\Component\Plugin\DependentPluginInterface;
* @see \Drupal\search_api\Display\DisplayPluginBase
* @see plugin_api
*/
interface DisplayInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ContainerFactoryPluginInterface, DependentPluginInterface {
interface DisplayInterface extends HideablePluginInterface, PluginInspectionInterface, DerivativeInspectionInterface, ContainerFactoryPluginInterface, DependentPluginInterface {
/**
* Returns the display label.
@@ -4,8 +4,8 @@ namespace Drupal\search_api\Display;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Url;
use Drupal\search_api\Plugin\HideablePluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -33,7 +33,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @see \Drupal\search_api\Display\DisplayInterface
* @see plugin_api
*/
abstract class DisplayPluginBase extends PluginBase implements DisplayInterface {
abstract class DisplayPluginBase extends HideablePluginBase implements DisplayInterface {
/**
* The current path service.
@@ -51,12 +51,12 @@ use Drupal\search_api\Utility\Utility;
* "backend_config",
* },
* links = {
* "canonical" = "/admin/config/search/search-api/index/{search_api_server}",
* "canonical" = "/admin/config/search/search-api/server/{search_api_server}",
* "add-form" = "/admin/config/search/search-api/add-server",
* "edit-form" = "/admin/config/search/search-api/index/{search_api_server}/edit",
* "delete-form" = "/admin/config/search/search-api/index/{search_api_server}/delete",
* "disable" = "/admin/config/search/search-api/index/{search_api_server}/disable",
* "enable" = "/admin/config/search/search-api/index/{search_api_server}/enable",
* "edit-form" = "/admin/config/search/search-api/server/{search_api_server}/edit",
* "delete-form" = "/admin/config/search/search-api/server/{search_api_server}/delete",
* "disable" = "/admin/config/search/search-api/server/{search_api_server}/disable",
* "enable" = "/admin/config/search/search-api/server/{search_api_server}/enable",
* }
* )
*/
@@ -187,6 +187,9 @@ class IndexFieldsForm extends EntityForm {
$data_types = [];
foreach ($instances as $name => $type) {
if ($type->isHidden()) {
continue;
}
$data_types[$name] = [
'label' => $type->label(),
'description' => $type->getDescription(),
@@ -219,7 +222,6 @@ class IndexFieldsForm extends EntityForm {
* The build structure.
*/
protected function buildFieldsTable(array $fields) {
$types = $this->dataTypePluginManager->getInstancesOptions();
$fallback_types = $this->dataTypeHelper
->getDataTypeFallbackMapping($this->entity);
@@ -236,6 +238,7 @@ class IndexFieldsForm extends EntityForm {
}
}
$types = [];
$fulltext_types = [
[
'value' => 'text',
@@ -243,6 +246,9 @@ class IndexFieldsForm extends EntityForm {
];
// Add all data types with fallback "text" to fulltext types as well.
foreach ($this->dataTypePluginManager->getInstances() as $type_id => $type) {
if (!$type->isHidden()) {
$types[$type_id] = $type->label();
}
if ($type->getFallbackType() == 'text') {
$fulltext_types[] = [
'value' => $type_id,
@@ -488,7 +494,8 @@ class IndexFieldsForm extends EntityForm {
$this->messenger->addStatus($this->t('The changes were successfully saved.'));
if ($this->entity->isReindexing()) {
$this->messenger->addStatus($this->t('All content was scheduled for reindexing so the new settings can take effect.'));
$url = $this->entity->toUrl('canonical');
$this->messenger->addStatus($this->t('All content was scheduled for <a href=":url">reindexing</a> so the new settings can take effect.', [':url' => $url->toString()]));
}
return SAVED_UPDATED;
@@ -2,7 +2,6 @@
namespace Drupal\search_api\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -13,6 +12,7 @@ use Drupal\Core\Form\SubformState;
use Drupal\Core\Utility\Error;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Utility\PluginHelperInterface;
use Drupal\search_api\Utility\Utility;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -96,7 +96,7 @@ class IndexForm extends EntityForm {
->loadMultiple();
foreach ($servers as $server_id => $server) {
// @todo Special formatting for disabled servers.
$options[$server_id] = Html::escape($server->label());
$options[$server_id] = Utility::escapeHtml($server->label());
}
return $options;
}
@@ -185,8 +185,11 @@ class IndexForm extends EntityForm {
];
$datasource_options = [];
foreach ($this->pluginHelper->createDatasourcePlugins($index) as $datasource_id => $datasource) {
$datasource_options[$datasource_id] = $datasource->label();
$form['datasources'][$datasource_id]['#description'] = $datasource->getDescription();
if ($datasource->isHidden()) {
continue;
}
$datasource_options[$datasource_id] = Utility::escapeHtml($datasource->label());
$form['datasources'][$datasource_id]['#description'] = Utility::escapeHtml($datasource->getDescription());
}
asort($datasource_options, SORT_NATURAL | SORT_FLAG_CASE);
$form['datasources']['#options'] = $datasource_options;
@@ -230,8 +233,11 @@ class IndexForm extends EntityForm {
];
$tracker_options = [];
foreach ($this->pluginHelper->createTrackerPlugins($index) as $tracker_id => $tracker) {
$tracker_options[$tracker_id] = $tracker->label();
$form['tracker'][$tracker_id]['#description'] = $tracker->getDescription();
if ($tracker->isHidden()) {
continue;
}
$tracker_options[$tracker_id] = Utility::escapeHtml($tracker->label());
$form['tracker'][$tracker_id]['#description'] = Utility::escapeHtml($tracker->getDescription());
}
asort($tracker_options, SORT_NATURAL | SORT_FLAG_CASE);
$form['tracker']['#options'] = $tracker_options;
@@ -419,7 +425,7 @@ class IndexForm extends EntityForm {
$form['tracker_config']['#type'] = 'details';
$form['tracker_config']['#title'] = $this->t('Configure the %plugin tracker', ['%plugin' => $tracker->label()]);
$form['tracker_config']['#description'] = Html::escape($tracker->getDescription());
$form['tracker_config']['#description'] = Utility::escapeHtml($tracker->getDescription());
$form['tracker_config']['#open'] = $index->isNew();
// If the user changed the tracker and the new one has a config form, show
@@ -336,7 +336,7 @@ class IndexProcessorsForm extends EntityForm {
$save_status = parent::save($form, $form_state);
$this->messenger->addStatus($this->t('The indexing workflow was successfully edited.'));
if ($this->entity->isReindexing()) {
$url = $this->entity->toUrl();
$url = $this->entity->toUrl('canonical');
$this->messenger->addStatus($this->t('All content was scheduled for <a href=":url">reindexing</a> so the new settings can take effect.', [':url' => $url->toString()]));
}
}
@@ -2,6 +2,7 @@
namespace Drupal\search_api\Form;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Form\FormStateInterface;
@@ -12,6 +13,7 @@ use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use Drupal\search_api\Backend\BackendPluginManager;
use Drupal\search_api\ServerInterface;
use Drupal\search_api\Utility\Utility;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -140,10 +142,19 @@ class ServerForm extends EntityForm {
foreach ($backends as $backend_id => $definition) {
$config = $backend_id === $server->getBackendId() ? $server->getBackendConfig() : [];
$config['#server'] = $server;
$backend = $this->backendPluginManager
->createInstance($backend_id, $config);
$backend_options[$backend_id] = $backend->label();
$descriptions[$backend_id]['#description'] = $backend->getDescription();
try {
/** @var \Drupal\search_api\Backend\BackendInterface $backend */
$backend = $this->backendPluginManager
->createInstance($backend_id, $config);
}
catch (PluginException $e) {
continue;
}
if ($backend->isHidden()) {
continue;
}
$backend_options[$backend_id] = Utility::escapeHtml($backend->label());
$descriptions[$backend_id]['#description'] = Utility::escapeHtml($backend->getDescription());
}
asort($backend_options, SORT_NATURAL | SORT_FLAG_CASE);
if ($backend_options) {
@@ -2,6 +2,7 @@
namespace Drupal\search_api;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
@@ -9,6 +10,7 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -50,6 +52,75 @@ class IndexListBuilder extends ConfigEntityListBuilder {
);
}
/**
* Determines whether the "Database Search Defaults" module can be installed.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
* An array of error messages describing why the module cannot be installed,
* keyed by a short, machine name-like identifier for the kind of error. If
* the array is empty, the module can be installed.
*/
public static function checkDefaultsModuleCanBeInstalled() {
$errors = [];
$node_types = NodeType::loadMultiple();
$required_types = [
'article' => ['body', 'comment', 'field_tags', 'field_image'],
'page' => ['body'],
];
/** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */
$entity_field_manager = \Drupal::service('entity_field.manager');
foreach ($required_types as $required_type_id => $required_fields) {
if (!isset($node_types[$required_type_id])) {
$errors[$required_type_id] = t('Content type @content_type not found. Database Search Defaults module could not be installed.', ['@content_type' => $required_type_id]);
}
else {
// Check if all the fields are here.
$fields = $entity_field_manager->getFieldDefinitions('node', $required_type_id);
foreach ($required_fields as $required_field) {
if (!isset($fields[$required_field])) {
$errors[$required_type_id . ':' . $required_field] = t('Field @field in content type @node_type not found. Database Search Defaults module could not be installed', [
'@node_type' => $required_type_id,
'@field' => $required_field
]);
}
}
}
}
if (\Drupal::moduleHandler()->moduleExists('search_api_db')) {
$entities_to_check = [
'search_api_index' => 'default_index',
'search_api_server' => 'default_server',
'view' => 'search_content',
];
/** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
$entity_type_manager = \Drupal::service('entity_type.manager');
foreach ($entities_to_check as $entity_type => $entity_id) {
try {
// Find out if the entity is already in place. If so, fail to install
// the module.
$entity_storage = $entity_type_manager->getStorage($entity_type);
$entity_storage->resetCache();
$entity = $entity_storage->load($entity_id);
if ($entity) {
$errors['defaults_exist'] = t('It looks like the default setup provided by this module already exists on your site. Cannot re-install module.');
break;
}
}
catch (PluginException $e) {
// This can only happen for the view, if the Views module isn't
// installed. Ignore.
}
}
}
return $errors;
}
/**
* {@inheritdoc}
*/
@@ -160,7 +231,7 @@ class IndexListBuilder extends ConfigEntityListBuilder {
'#type' => 'table',
'#header' => $this->buildHeader(),
'#rows' => [],
'#empty' => $entity_groups['lone_indexes'] ? '' : $this->t('There are no servers or indexes defined. For a quick start, we suggest you install the Database Search Defaults module.'),
'#empty' => '',
'#attributes' => [
'id' => 'search-api-entity-list',
'class' => ['search-api-entity-list'],
@@ -186,6 +257,14 @@ class IndexListBuilder extends ConfigEntityListBuilder {
$list['lone_indexes']['table']['#rows'][$entity->id()] = $this->buildRow($entity);
}
}
elseif (!$list['servers']['#rows']) {
if (static::checkDefaultsModuleCanBeInstalled() === []) {
$list['servers']['#empty'] = $this->t('There are no servers or indexes defined. For a quick start, we suggest you install the Database Search Defaults module.');
}
else {
$list['servers']['#empty'] = $this->t('There are no servers or indexes defined.');
}
}
return $list;
}
@@ -253,6 +253,8 @@ interface ItemInterface extends \Traversable {
* highlighting data should also include any non-highlighted field values,
* to avoid having to determine which values are included and which
* aren't.
* - highlighted_keys: The exact tokens that matched keys in this item's
* text values. The value is an array of strings.
* However, contrib modules can define arbitrary other keys. (Usually they
* should be prefixed with the module name, though.)
* @param mixed $default
@@ -5,6 +5,7 @@ namespace Drupal\search_api\ParseMode;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\search_api\Plugin\HideablePluginInterface;
/**
* Defines an interface for parse mode plugins.
@@ -14,7 +15,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
* @see \Drupal\search_api\ParseMode\ParseModePluginBase
* @see plugin_api
*/
interface ParseModeInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ContainerFactoryPluginInterface {
interface ParseModeInterface extends HideablePluginInterface, PluginInspectionInterface, DerivativeInspectionInterface, ContainerFactoryPluginInterface {
/**
* Returns the label of the parse mode.
@@ -2,7 +2,7 @@
namespace Drupal\search_api\ParseMode;
use Drupal\Core\Plugin\PluginBase;
use Drupal\search_api\Plugin\HideablePluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -31,7 +31,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @see \Drupal\search_api\ParseMode\ParseModeInterface
* @see plugin_api
*/
abstract class ParseModePluginBase extends PluginBase implements ParseModeInterface {
abstract class ParseModePluginBase extends HideablePluginBase implements ParseModeInterface {
/**
* The default conjunction to use when parsing keywords.
@@ -2,14 +2,13 @@
namespace Drupal\search_api\Plugin;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Plugin\PluginDependencyTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for all configurable Search API plugins.
*/
abstract class ConfigurablePluginBase extends PluginBase implements ConfigurablePluginInterface {
abstract class ConfigurablePluginBase extends HideablePluginBase implements ConfigurablePluginInterface {
// Normally, we'd just need \Drupal\Core\Entity\DependencyTrait here for
// plugins. However, in a few cases, plugins use plugins themselves, and then
@@ -16,7 +16,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
* @see \Drupal\Core\Plugin\PluginFormInterface
* @see \Drupal\search_api\Plugin\PluginFormTrait
*/
interface ConfigurablePluginInterface extends PluginInspectionInterface, DerivativeInspectionInterface, DrupalConfigurablePluginInterface, ContainerFactoryPluginInterface {
interface ConfigurablePluginInterface extends HideablePluginInterface, PluginInspectionInterface, DerivativeInspectionInterface, DrupalConfigurablePluginInterface, ContainerFactoryPluginInterface {
/**
* Returns the label for use on the administration pages.
@@ -0,0 +1,19 @@
<?php
namespace Drupal\search_api\Plugin;
use Drupal\Core\Plugin\PluginBase;
/**
* Provides a base class for plugins that can be hidden.
*/
class HideablePluginBase extends PluginBase implements HideablePluginInterface {
/**
* {@inheritdoc}
*/
public function isHidden() {
return !empty($this->pluginDefinition['no_ui']);
}
}
@@ -0,0 +1,18 @@
<?php
namespace Drupal\search_api\Plugin;
/**
* Defines an interface for plugins that can be hidden.
*/
interface HideablePluginInterface {
/**
* Determines whether this plugin should be hidden in the UI.
*
* @return bool
* TRUE if this processor should be hidden from the user; FALSE otherwise.
*/
public function isHidden();
}
@@ -2,6 +2,8 @@
namespace Drupal\search_api\Plugin\search_api\data_type;
use Drupal\Component\Datetime\DateTimePlus;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\search_api\DataType\DataTypePluginBase;
/**
@@ -26,8 +28,13 @@ class DateDataType extends DataTypePluginBase {
if (is_numeric($value)) {
return (int) $value;
}
$timezone = new \DateTimezone('UTC');
$date = new \Datetime($value, $timezone);
$timezone = new \DateTimezone(DateTimeItemInterface::STORAGE_TIMEZONE);
$date = new DateTimePlus($value, $timezone);
// Add in time component if this is a date-only field.
if (strpos($value, ':') === FALSE) {
$date->setDefaultDateTime();
}
return $date->getTimestamp();
}
@@ -11,15 +11,15 @@ interface TextTokenInterface {
* Retrieves the text value of this token.
*
* @return string
* The the text value of this token.
* The text value of this token.
*/
public function getText();
/**
* Sets the the text value of this token.
* Sets the text value of this token.
*
* @param string $text
* The new the text value of this token.
* The new text value of this token.
*
* @return $this
*/
@@ -2,7 +2,6 @@
namespace Drupal\search_api\Plugin\search_api\datasource;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
@@ -11,6 +10,7 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
@@ -507,7 +507,7 @@ class ContentEntity extends DatasourcePluginBase implements EntityDatasourceInte
$options = [];
if (($bundles = $this->getEntityBundles())) {
foreach ($bundles as $bundle => $bundle_info) {
$options[$bundle] = Html::escape($bundle_info['label']);
$options[$bundle] = Utility::escapeHtml($bundle_info['label']);
}
}
return $options;
@@ -677,6 +677,9 @@ class ContentEntity extends DatasourcePluginBase implements EntityDatasourceInte
->getStorage($this->getEntityTypeId())
->getQuery();
// When tracking items, we never want access checks.
$select->accessCheck(FALSE);
// We want to determine all entities of either one of the given bundles OR
// one of the given languages. That means we can't just filter for $bundles
// if $languages is given. Instead, we have to filter for all bundles we
@@ -930,6 +933,13 @@ class ContentEntity extends DatasourcePluginBase implements EntityDatasourceInte
}
}
// The field might be provided by a module which is not the provider of the
// entity type, therefore we need to add a dependency on that module.
if ($property instanceof FieldStorageDefinitionInterface) {
$provider = $property->getProvider();
$dependencies['module'][$provider] = $provider;
}
$property = $this->getFieldsHelper()->getInnerProperty($property);
if ($property instanceof EntityDataDefinitionInterface) {
@@ -2,7 +2,6 @@
namespace Drupal\search_api\Plugin\search_api\processor;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
@@ -13,6 +12,7 @@ use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\FieldInterface;
use Drupal\search_api\Plugin\PluginFormTrait;
use Drupal\search_api\Processor\ProcessorPluginBase;
use Drupal\search_api\Utility\Utility;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -151,6 +151,7 @@ class AddHierarchy extends ProcessorPluginBase implements PluginFormInterface {
*/
protected function findHierarchicalProperties(EntityDataDefinitionInterface $property, $property_label) {
$entity_type_id = $property->getEntityTypeId();
$property_label = Utility::escapeHtml($property_label);
$options = [];
// Check properties for potential hierarchy. Check two levels down, since
@@ -159,9 +160,10 @@ class AddHierarchy extends ProcessorPluginBase implements PluginFormInterface {
foreach ($this->getFieldsHelper()->getNestedProperties($property) as $name_2 => $property_2) {
$property_2_label = $property_2->getLabel();
$property_2 = $this->getFieldsHelper()->getInnerProperty($property_2);
$is_reference = FALSE;
if ($property_2 instanceof EntityDataDefinitionInterface) {
if ($property_2->getEntityTypeId() == $entity_type_id) {
$options["$entity_type_id-$name_2"] = Html::escape($property_label . ' » ' . $property_2_label);
$is_reference = TRUE;
}
}
elseif ($property_2 instanceof ComplexDataDefinitionInterface) {
@@ -169,12 +171,16 @@ class AddHierarchy extends ProcessorPluginBase implements PluginFormInterface {
$property_3 = $this->getFieldsHelper()->getInnerProperty($property_3);
if ($property_3 instanceof EntityDataDefinitionInterface) {
if ($property_3->getEntityTypeId() == $entity_type_id) {
$options["$entity_type_id-$name_2"] = Html::escape($property_label . ' » ' . $property_2_label);
$is_reference = TRUE;
break;
}
}
}
}
if ($is_reference) {
$property_2_label = Utility::escapeHtml($property_2_label);
$options["$entity_type_id-$name_2"] = $property_label . ' » ' . $property_2_label;
}
}
return $options;
}
@@ -94,11 +94,15 @@ class AggregatedFields extends ProcessorPluginBase {
break;
case 'max':
$values = [max($values)];
if ($values) {
$values = [max($values)];
}
break;
case 'min':
$values = [min($values)];
if ($values) {
$values = [min($values)];
}
break;
case 'first':
@@ -279,11 +279,22 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface {
foreach ($item as $values) {
$text = array_merge($text, $values);
}
$item_keys = $keys;
// If the backend already did highlighting and told us the exact keys it
// found in the item's text values, we can use those for our own
// highlighting. This will help us take stemming, transliteration, etc.
// into account properly.
$highlighted_keys = $results[$item_id]->getExtraData('highlighted_keys');
if ($highlighted_keys) {
$item_keys = array_unique(array_merge($keys, $highlighted_keys));
}
// @todo This is pretty poor handling for the borders between different
// values/fields. Better would be to pass an array and have proper
// handling of this in createExcerpt(), ensuring that no snippet goes
// across multiple values/fields.
$results[$item_id]->setExcerpt($this->createExcerpt(implode($this->getEllipses()[1], $text), $keys));
$results[$item_id]->setExcerpt($this->createExcerpt(implode($this->getEllipses()[1], $text), $item_keys));
}
}
@@ -438,12 +449,15 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface {
* created.
*/
protected function createExcerpt($text, array $keys) {
// Remove HTML tags <script> and <style> with all of their contents.
$text = preg_replace('#<(style|script).*?>.*?</\1>#is', ' ', $text);
// Prepare text by stripping HTML tags and decoding HTML entities.
$text = strip_tags(str_replace(['<', '>'], [' <', '> '], $text));
$text = Html::decodeEntities($text);
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text, ' ');
$text_length = strlen($text);
$text_length = mb_strlen($text);
// Try to reach the requested excerpt length with about two fragments (each
// with a keyword and some context).
@@ -483,13 +497,12 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface {
$regex = '/' . static::$boundary . preg_quote($key, '/') . static::$boundary . '/iu';
if (preg_match($regex, ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) {
$found_position = $matches[0][1];
// Convert the byte position into a multi-byte character position.
$found_position = mb_strlen(substr(" $text", 0, $found_position));
}
}
elseif (function_exists('mb_stripos')) {
$found_position = mb_stripos($text, $key, $look_start[$key], 'UTF-8');
}
else {
$found_position = stripos($text, $key, $look_start[$key]);
$found_position = mb_stripos($text, $key, $look_start[$key], 'UTF-8');
}
if ($found_position !== FALSE) {
$look_start[$key] = $found_position + 1;
@@ -500,7 +513,7 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface {
// Locate a space before and after this match, leaving some context on
// each end.
if ($found_position > $context_length) {
$before = strpos($text, ' ', $found_position - $context_length);
$before = mb_strpos($text, ' ', $found_position - $context_length);
if ($before !== FALSE) {
++$before;
}
@@ -510,7 +523,7 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface {
}
if ($before !== FALSE && $before <= $found_position) {
if ($text_length > $found_position + $context_length) {
$after = strrpos(substr($text, 0, $found_position + $context_length), ' ', $found_position);
$after = mb_strrpos(mb_substr($text, 0, $found_position + $context_length), ' ', $found_position);
}
else {
$after = $text_length;
@@ -540,10 +553,8 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface {
// Collapse overlapping text ranges into one. The sorting makes it O(n).
$new_ranges = [];
$max_end = 0;
$working_from = $working_to = NULL;
foreach ($ranges as $this_from => $this_to) {
$max_end = max($max_end, $this_to);
if ($working_from === NULL) {
// This is the first time through this loop: initialize.
$working_from = $this_from;
@@ -568,7 +579,7 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface {
// Fetch text within the combined ranges we found.
$out = [];
foreach ($new_ranges as $from => $to) {
$out[] = Html::escape(substr($text, $from, $to - $from));
$out[] = Html::escape(mb_substr($text, $from, $to - $from));
}
if (!$out) {
return NULL;
@@ -2,7 +2,6 @@
namespace Drupal\search_api\Plugin\search_api\processor\Property;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\search_api\IndexInterface;
@@ -70,7 +69,7 @@ class AggregatedFieldProperty extends ConfigurablePropertyBase {
continue;
}
$label = $datasource_labels[$datasource_id] . $property->getLabel();
$field_options[$combined_id] = Html::escape($label);
$field_options[$combined_id] = Utility::escapeHtml($label);
if ($property instanceof ConfigurablePropertyInterface) {
$description = $property->getFieldDescription($field);
}
@@ -5,7 +5,7 @@ namespace Drupal\search_api\Plugin\search_api\processor;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Theme\ThemeInitializationInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
@@ -39,9 +39,9 @@ class RenderedItem extends ProcessorPluginBase {
/**
* The current_user service used by this plugin.
*
* @var \Drupal\Core\Session\AccountProxyInterface|null
* @var \Drupal\Core\Session\AccountSwitcherInterface|null
*/
protected $currentUser;
protected $accountSwitcher;
/**
* The renderer to use.
@@ -78,7 +78,7 @@ class RenderedItem extends ProcessorPluginBase {
/** @var static $plugin */
$plugin = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$plugin->setCurrentUser($container->get('current_user'));
$plugin->setAccountSwitcher($container->get('account_switcher'));
$plugin->setRenderer($container->get('renderer'));
$plugin->setLogger($container->get('logger.channel.search_api'));
$plugin->setThemeManager($container->get('theme.manager'));
@@ -89,25 +89,25 @@ class RenderedItem extends ProcessorPluginBase {
}
/**
* Retrieves the current user.
* Retrieves the account switcher service.
*
* @return \Drupal\Core\Session\AccountProxyInterface
* The current user.
* @return \Drupal\Core\Session\AccountSwitcherInterface
* The account switcher service.
*/
public function getCurrentUser() {
return $this->currentUser ?: \Drupal::currentUser();
public function getAccountSwitcher() {
return $this->accountSwitcher ?: \Drupal::service('account_switcher');
}
/**
* Sets the current user.
* Sets the account switcher service.
*
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
* @param \Drupal\Core\Session\AccountSwitcherInterface $current_user
* The account switcher service.
*
* @return $this
*/
public function setCurrentUser(AccountProxyInterface $current_user) {
$this->currentUser = $current_user;
public function setAccountSwitcher(AccountSwitcherInterface $current_user) {
$this->accountSwitcher = $current_user;
return $this;
}
@@ -229,8 +229,6 @@ class RenderedItem extends ProcessorPluginBase {
* {@inheritdoc}
*/
public function addFieldValues(ItemInterface $item) {
$original_user = $this->currentUser->getAccount();
// Switch to the default theme in case the admin theme is enabled.
$active_theme = $this->getThemeManager()->getActiveTheme();
$default_theme = $this->getConfigFactory()
@@ -250,7 +248,8 @@ class RenderedItem extends ProcessorPluginBase {
// Change the current user to our dummy implementation to ensure we are
// using the configured roles.
$this->currentUser->setAccount(new UserSession(['roles' => $configuration['roles']]));
$this->getAccountSwitcher()
->switchTo(new UserSession(['roles' => $configuration['roles']]));
$datasource_id = $item->getDatasourceId();
$datasource = $item->getDatasource();
@@ -268,15 +267,29 @@ class RenderedItem extends ProcessorPluginBase {
$view_mode = (string) $configuration['view_mode'][$datasource_id][$bundle];
}
$build = $datasource->viewItem($item->getOriginalObject(), $view_mode);
$value = (string) $this->getRenderer()->renderPlain($build);
if ($value) {
$field->addValue($value);
try {
$build = $datasource->viewItem($item->getOriginalObject(), $view_mode);
$value = (string) $this->getRenderer()->renderPlain($build);
if ($value) {
$field->addValue($value);
}
}
catch (\Exception $e) {
// This could throw all kinds of exceptions in specific scenarios, so we
// just catch all of them here. Not having a field value for this field
// probably makes sense in that case, so we just log an error and
// continue.
$variables = [
'%item_id' => $item->getId(),
'%view_mode' => $view_mode,
'%index' => $this->index->label(),
];
$this->logException($e, '%type while trying to render item %item_id with view mode %view_mode for search index %index: @message in %function (line %line of %file).', $variables);
}
}
// Restore the original user.
$this->currentUser->setAccount($original_user);
$this->getAccountSwitcher()->switchBack();
// Restore the original theme.
$this->getThemeManager()->setActiveTheme($active_theme);
@@ -0,0 +1,417 @@
<?php
namespace Drupal\search_api\Plugin\search_api\processor;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManager;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\Processor\EntityProcessorProperty;
use Drupal\search_api\Processor\ProcessorPluginBase;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\Utility\Utility;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Allows indexing of reverse entity references.
*
* @SearchApiProcessor(
* id = "reverse_entity_references",
* label = @Translation("Reverse entity references"),
* description = @Translation("Allows indexing of entities that link to the indexed entity."),
* stages = {
* "add_properties" = 0,
* },
* )
*/
class ReverseEntityReferences extends ProcessorPluginBase {
/**
* Static cache for all entity references.
*
* @var array[][]|null
*
* @see \Drupal\search_api\Plugin\search_api\processor\ReverseEntityReferences::getEntityReferences()
*/
protected $references;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManager|null
*/
protected $entityFieldManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|null
*/
protected $entityTypeBundleInfo;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|null
*/
protected $languageManager;
/**
* The cache.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|null
*/
protected $cache;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
/** @var static $processor */
$processor = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$processor->setEntityTypeManager($container->get('entity_type.manager'));
$processor->setEntityFieldManager($container->get('entity_field.manager'));
$processor->setEntityTypeBundleInfo($container->get('entity_type.bundle.info'));
$processor->setLanguageManager($container->get('language_manager'));
$processor->setCache($container->get('cache.default'));
return $processor;
}
/**
* Retrieves the entity type manager.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
* The entity type manager.
*/
public function getEntityTypeManager() {
return $this->entityTypeManager ?: \Drupal::entityTypeManager();
}
/**
* Sets the entity type manager.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The new entity type manager.
*
* @return $this
*/
public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
return $this;
}
/**
* Retrieves the entity field manager.
*
* @return \Drupal\Core\Entity\EntityFieldManager
* The entity field manager.
*/
public function getEntityFieldManager() {
return $this->entityFieldManager ?: \Drupal::service('entity_field.manager');
}
/**
* Sets the entity field manager.
*
* @param \Drupal\Core\Entity\EntityFieldManager $entity_field_manager
* The new entity field manager.
*
* @return $this
*/
public function setEntityFieldManager(EntityFieldManager $entity_field_manager) {
$this->entityFieldManager = $entity_field_manager;
return $this;
}
/**
* Retrieves the entity type bundle info.
*
* @return \Drupal\Core\Entity\EntityTypeBundleInfoInterface
* The entity type bundle info.
*/
public function getEntityTypeBundleInfo() {
return $this->entityTypeBundleInfo ?: \Drupal::service('entity_type.bundle.info');
}
/**
* Sets the entity type bundle info.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The new entity type bundle info.
*
* @return $this
*/
public function setEntityTypeBundleInfo(EntityTypeBundleInfoInterface $entity_type_bundle_info) {
$this->entityTypeBundleInfo = $entity_type_bundle_info;
return $this;
}
/**
* Retrieves the language manager.
*
* @return \Drupal\Core\Language\LanguageManagerInterface
* The language manager.
*/
public function getLanguageManager() {
return $this->languageManager ?: \Drupal::service('language_manager');
}
/**
* Sets the language manager.
*
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The new language manager.
*
* @return $this
*/
public function setLanguageManager(LanguageManagerInterface $language_manager) {
$this->languageManager = $language_manager;
return $this;
}
/**
* Retrieves the cache.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* The cache.
*/
public function getCache() {
return $this->cache ?: \Drupal::service('cache.default');
}
/**
* Sets the cache.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The new cache.
*
* @return $this
*/
public function setCache(CacheBackendInterface $cache) {
$this->cache = $cache;
return $this;
}
/**
* {@inheritdoc}
*/
public static function supportsIndex(IndexInterface $index) {
foreach ($index->getDatasources() as $datasource) {
if ($datasource->getEntityTypeId()) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
$properties = [];
if (!$datasource || !$datasource->getEntityTypeId()) {
return $properties;
}
$references = $this->getEntityReferences();
$entity_type_id = $datasource->getEntityTypeId();
if (isset($references[$entity_type_id])) {
foreach ($references[$entity_type_id] as $key => $reference) {
$entity_type_id = $reference['entity_type'];
try {
$entity_type = $this->getEntityTypeManager()
->getDefinition($entity_type_id);
}
catch (PluginNotFoundException $e) {
continue;
}
$args = [
'%entity_type' => $entity_type->getLabel(),
'%property' => $reference['label'],
];
$definition = [
'label' => $this->t('Reverse reference: %entity_type using %property', $args),
'description' => $this->t("All %entity_type entities that reference this item via the %property field."),
'type' => "entity:$entity_type_id",
'processor_id' => $this->getPluginId(),
// We can't really know whether this will end up being multi-valued, so
// we err on the side of caution.
'is_list' => TRUE,
];
$property = new EntityProcessorProperty($definition);
$property->setEntityTypeId($entity_type_id);
$properties["search_api_reverse_entity_references_$key"] = $property;
}
}
return $properties;
}
/**
* {@inheritdoc}
*/
public function addFieldValues(ItemInterface $item) {
try {
$entity = $item->getOriginalObject()->getValue();
}
catch (SearchApiException $e) {
return;
}
if (!($entity instanceof EntityInterface)) {
return;
}
$entity_type_id = $entity->getEntityTypeId();
$entity_id = $entity->id();
$langcode = $entity->language()->getId();
$datasource_id = $item->getDatasourceId();
/** @var \Drupal\search_api\Item\FieldInterface[][][] $to_extract */
$to_extract = [];
$prefix = 'search_api_reverse_entity_references_';
$prefix_length = strlen($prefix);
foreach ($item->getFields() as $field) {
$property_path = $field->getPropertyPath();
list($direct, $nested) = Utility::splitPropertyPath($property_path, FALSE);
if ($field->getDatasourceId() === $datasource_id
&& substr($direct, 0, $prefix_length) === $prefix) {
$property_name = substr($direct, $prefix_length);
$to_extract[$property_name][$nested][] = $field;
}
}
$references = $this->getEntityReferences();
foreach ($to_extract as $property_name => $fields_to_extract) {
if (!isset($references[$entity_type_id][$property_name])) {
continue;
}
$property_info = $references[$entity_type_id][$property_name];
try {
$storage = $this->getEntityTypeManager()
->getStorage($property_info['entity_type']);
}
// @todo Replace with multi-catch once we depend on PHP 7.1+.
catch (InvalidPluginDefinitionException $e) {
continue;
}
catch (PluginNotFoundException $e) {
continue;
}
$entity_ids = $storage->getQuery()
->accessCheck(FALSE)
->condition($property_info['property'], $entity_id)
->execute();
$entities = $storage->loadMultiple($entity_ids);
if (!$entities) {
continue;
}
// This is a pretty hack-y work-around to make property extraction work for
// Views fields, too. In general, adding entities as field values is a
// pretty bad idea, so this might blow up in some use cases. Just do it for
// now and hope for the best.
if (isset($fields_to_extract[''])) {
foreach ($fields_to_extract[''] as $field) {
$field->setValues(array_values($entities));
}
unset($fields_to_extract['']);
}
foreach ($entities as $referencing_entity) {
$typed_data = $referencing_entity->getTypedData();
$this->getFieldsHelper()
->extractFields($typed_data, $fields_to_extract, $langcode);
}
}
}
/**
* Collects all entity references.
*
* @return array[][]
* An associative array of entity reference information keyed by the
* referenced entity type's ID and a custom identifier for the property
* (consisting of referencing entity type and property name), with values
* being associative arrays with the following keys:
* - label: The property label.
* - entity_type: The referencing entity type.
* - property: The property name.
*/
public function getEntityReferences() {
if ($this->references !== NULL) {
return $this->references;
}
// Property labels differ by language, so we need to vary the cache
// according to the current language.
$langcode = $this->getLanguageManager()->getCurrentLanguage()->getId();
$cid = "search_api:reverse_entity_references:$langcode";
$cache = $this->getCache()->get($cid);
if (isset($cache->data)) {
$this->references = $cache->data;
}
else {
$this->references = [];
$entity_types = $this->getEntityTypeManager()->getDefinitions();
$field_manager = $this->getEntityFieldManager();
$entity_type_bundle_info = $this->getEntityTypeBundleInfo();
foreach ($entity_types as $entity_type_id => $entity_type) {
if (!($entity_type instanceof ContentEntityTypeInterface)) {
continue;
}
/** @var \Drupal\Core\Field\FieldDefinitionInterface[] $properties */
$properties = $field_manager->getBaseFieldDefinitions($entity_type_id);
$bundles = $entity_type_bundle_info->getBundleInfo($entity_type_id);
foreach ($bundles as $bundle => $info) {
$properties += $field_manager->getFieldDefinitions($entity_type_id, $bundle);
}
foreach ($properties as $name => $property) {
if ($property->getType() !== 'entity_reference') {
continue;
}
$settings = $property->getSettings();
if (empty($settings['target_type'])) {
continue;
}
$this->references[$settings['target_type']]["{$entity_type_id}__$name"] = [
'label' => $property->getLabel(),
'entity_type' => $entity_type_id,
'property' => $name,
];
}
}
$tags = [
'entity_types',
'entity_bundles',
'entity_field_info',
];
$this->getCache()->set($cid, $this->references, Cache::PERMANENT, $tags);
}
return $this->references;
}
}
@@ -2,12 +2,12 @@
namespace Drupal\search_api\Plugin\search_api\processor;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Plugin\PluginFormTrait;
use Drupal\search_api\Processor\ProcessorPluginBase;
use Drupal\search_api\Utility\Utility;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
@@ -56,7 +56,7 @@ class RoleFilter extends ProcessorPluginBase implements PluginFormInterface {
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$options = array_map(function (RoleInterface $role) {
return Html::escape($role->label());
return Utility::escapeHtml($role->label());
}, user_roles());
$form['default'] = [
@@ -3,7 +3,6 @@
namespace Drupal\search_api\Plugin\views\argument;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Datetime\DateFormatterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -130,10 +129,10 @@ class SearchApiDate extends SearchApiStandard {
$dates[] = $date_string;
}
}
return $dates ? implode(', ', $dates) : Html::escape($this->argument);
return $dates ? implode(', ', $dates) : $this->argument;
}
return Html::escape($this->argument);
return $this->argument;
}
/**
@@ -81,10 +81,14 @@ class SearchApiFulltext extends SearchApiStandard {
'#type' => 'select',
'#title' => $this->t('Parse mode'),
'#description' => $this->t('Choose how the search keys will be parsed.'),
'#options' => $this->getParseModeManager()->getInstancesOptions(),
'#options' => [],
'#default_value' => $this->options['parse_mode'],
];
foreach ($this->getParseModeManager()->getInstances() as $key => $mode) {
if ($mode->isHidden()) {
continue;
}
$form['parse_mode']['#options'][$key] = $mode->label();
if ($mode->getDescription()) {
$states['visible'][':input[name="options[parse_mode]"]']['value'] = $key;
$form["parse_mode_{$key}_description"] = [
@@ -168,9 +168,19 @@ class SearchApiStandard extends ArgumentPluginBase {
public function query($group_by = FALSE) {
$this->fillValue();
// If there are multiple arguments, add query conditions accordingly.
// E.g 1+2+3 (for OR) or 1,2,3 (for AND).
if (count($this->value) > 1) {
$operator = empty($this->options['not']) ? 'IN' : 'NOT IN';
$this->query->addCondition($this->realField, $this->value, $operator);
if ($this->operator === 'or') {
$operator = empty($this->options['not']) ? 'IN' : 'NOT IN';
$this->query->addCondition($this->realField, $this->value, $operator);
}
else {
foreach ($this->value as $value) {
$operator = empty($this->options['not']) ? '=' : '<>';
$this->query->addCondition($this->realField, $value, $operator);
}
}
}
elseif ($this->value) {
$operator = empty($this->options['not']) ? '=' : '<>';
@@ -2,7 +2,6 @@
namespace Drupal\search_api\Plugin\views\argument;
use Drupal\Component\Utility\Html;
use Drupal\taxonomy\Entity\Term;
/**
@@ -30,14 +29,14 @@ class SearchApiTerm extends SearchApiStandard {
foreach ($this->value as $tid) {
$taxonomy_term = Term::load($tid);
if ($taxonomy_term) {
$terms[] = Html::escape($taxonomy_term->label());
$terms[] = $taxonomy_term->label();
}
}
return $terms ? implode(', ', $terms) : Html::escape($this->argument);
return $terms ? implode(', ', $terms) : $this->argument;
}
else {
return Html::escape($this->argument);
return $this->argument;
}
}
@@ -337,7 +337,11 @@ trait SearchApiFieldTrait {
*/
public function query() {
$combined_property_path = $this->getCombinedPropertyPath();
$this->addRetrievedProperty($combined_property_path);
$field_id = NULL;
if (!empty($this->definition['search_api field'])) {
$field_id = $this->definition['search_api field'];
}
$this->addRetrievedProperty($combined_property_path, $field_id);
if ($this->options['link_to_item']) {
$this->addRetrievedProperty("$combined_property_path:_object");
}
@@ -350,11 +354,15 @@ trait SearchApiFieldTrait {
* The combined property path of the property that should be retrieved.
* "_object" can be used as a property name to indicate the loaded object is
* required.
* @param string|null $field_id
* (optional) The ID of the field corresponding to this property, if any.
*
* @return $this
*/
protected function addRetrievedProperty($combined_property_path) {
$this->getQuery()->addRetrievedProperty($combined_property_path);
protected function addRetrievedProperty($combined_property_path, $field_id = NULL) {
if ($field_id) {
$this->getQuery()->addRetrievedFieldValue($field_id);
}
list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
$this->retrievedProperties[$datasource_id][$property_path] = $combined_property_path;
@@ -467,7 +475,7 @@ trait SearchApiFieldTrait {
/**
* Expands the properties to retrieve for this field.
*
* The properties are taken from this object's $retrievedProperties property,
* The properties are taken from this object's $retrievedFieldValues property,
* with all their ancestors also added to the array, with the ancestor
* properties always ordered before their descendants.
*
@@ -39,6 +39,10 @@ trait SearchApiFilterTrait {
* @see \Drupal\views\Plugin\views\filter\ManyToOne::opHelper()
*/
protected function opHelper() {
// Form API returns unchecked options in the form of option_id => 0. This
// breaks the generated query for "is all of" filters so we remove them.
$this->value = array_filter($this->value, 'static::arrayFilterZero');
if (empty($this->value)) {
return;
}
@@ -141,10 +141,14 @@ class SearchApiFulltext extends FilterPluginBase {
'#type' => 'select',
'#title' => $this->t('Parse mode'),
'#description' => $this->t('Choose how the search keys will be parsed.'),
'#options' => $this->getParseModeManager()->getInstancesOptions(),
'#options' => [],
'#default_value' => $this->options['parse_mode'],
];
foreach ($this->getParseModeManager()->getInstances() as $key => $mode) {
if ($mode->isHidden()) {
continue;
}
$form['parse_mode']['#options'][$key] = $mode->label();
if ($mode->getDescription()) {
$states['visible'][':input[name="options[parse_mode]"]']['value'] = $key;
$form["parse_mode_{$key}_description"] = [
@@ -3,7 +3,6 @@
namespace Drupal\search_api\Plugin\views\query;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Database\Query\ConditionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -19,7 +18,6 @@ use Drupal\search_api\Processor\ConfigurablePropertyInterface;
use Drupal\search_api\Query\ConditionGroupInterface;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\Utility\Utility;
use Drupal\user\Entity\User;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\query\QueryPluginBase;
@@ -86,14 +84,11 @@ class SearchApiQuery extends QueryPluginBase {
protected $abort = FALSE;
/**
* The properties that should be retrieved from result items.
* The IDs of fields whose values should be retrieved by the backend.
*
* The array is keyed by datasource ID (which might be NULL) and property
* path, the values are the associated combined property paths.
*
* @var string[][]
* @var string[]
*/
protected $retrievedProperties = [];
protected $retrievedFieldValues = [];
/**
* The query's conditions representing the different Views filter groups.
@@ -250,8 +245,6 @@ class SearchApiQuery extends QueryPluginBase {
if (!$this->index) {
$this->abort(new FormattableMarkup('View %view is not based on Search API but tries to use its query plugin.', ['%view' => $view->storage->label()]));
}
$this->retrievedProperties = array_fill_keys($this->index->getDatasourceIds(), []);
$this->retrievedProperties[NULL] = [];
$this->query = $this->index->query();
$this->query->addTag('views');
$this->query->addTag('views_' . $view->id());
@@ -278,10 +271,28 @@ class SearchApiQuery extends QueryPluginBase {
* The combined property path of the property that should be retrieved.
*
* @return $this
*
* @deprecated in 8.x-1.11. Use ::addRetrievedFieldValue() instead.
*/
public function addRetrievedProperty($combined_property_path) {
list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
$this->retrievedProperties[$datasource_id][$property_path] = $combined_property_path;
@trigger_error('\Drupal\search_api\Plugin\views\query\SearchApiQuery::addRetrievedProperty() is deprecated in Search API 8.x-1.11. Use addRetrievedFieldValue() instead. See https://www.drupal.org/project/search_api/issues/3009136', E_USER_DEPRECATED);
$this->addField(NULL, $combined_property_path);
return $this;
}
/**
* Adds a field value to be retrieved.
*
* Helps backends that support returning fields to determine which of the
* fields should actually be returned.
*
* @param string $field_id
* The ID of the field whose value should be retrieved.
*
* @return $this
*/
public function addRetrievedFieldValue($field_id) {
$this->retrievedFieldValues[$field_id] = $field_id;
return $this;
}
@@ -290,7 +301,7 @@ class SearchApiQuery extends QueryPluginBase {
*
* This replicates the interface of Views' default SQL backend to simplify
* the Views integration of the Search API. If you are writing Search
* API-specific Views code, you should better use the addRetrievedProperty()
* API-specific Views code, you should better use the addRetrievedFieldValue()
* method.
*
* @param string|null $table
@@ -306,10 +317,15 @@ class SearchApiQuery extends QueryPluginBase {
* The name that this field can be referred to as (always $field).
*
* @see \Drupal\views\Plugin\views\query\Sql::addField()
* @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::addField()
* @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::addRetrievedFieldValue()
*/
public function addField($table, $field, $alias = '', array $params = []) {
$this->addRetrievedProperty($field);
foreach ($this->getIndex()->getFields(TRUE) as $field_id => $field_object) {
if ($field_object->getCombinedPropertyPath() === $field) {
$this->addRetrievedFieldValue($field_id);
break;
}
}
return $field;
}
@@ -440,9 +456,9 @@ class SearchApiQuery extends QueryPluginBase {
// Save query information for Views UI.
$view->build_info['query'] = (string) $this->query;
// Add the properties to be retrieved to the query, as information for the
// Add the fields to be retrieved to the query, as information for the
// backend.
$this->query->setOption('search_api_retrieved_properties', $this->retrievedProperties);
$this->query->setOption('search_api_retrieved_field_values', array_values($this->retrievedFieldValues));
}
/**
@@ -459,7 +475,7 @@ class SearchApiQuery extends QueryPluginBase {
if ($this->shouldAbort()) {
if (error_displayable()) {
foreach ($this->errors as $msg) {
$this->getMessenger()->addError(Html::escape($msg));
$this->getMessenger()->addError($msg);
}
}
$view->result = [];
@@ -132,14 +132,6 @@ interface ProcessorInterface extends IndexPluginInterface {
*/
public function isLocked();
/**
* Determines whether this processor should be hidden from the user.
*
* @return bool
* TRUE if this processor should be hidden from the user; FALSE otherwise.
*/
public function isHidden();
/**
* Retrieves the properties this processor defines for the given datasource.
*
@@ -141,7 +141,8 @@ abstract class ProcessorPluginBase extends IndexPluginBase implements ProcessorI
* {@inheritdoc}
*/
public function isHidden() {
return !empty($this->pluginDefinition['hidden']);
return !empty($this->pluginDefinition['hidden'])
|| !empty($this->pluginDefinition['no_ui']);
}
/**
@@ -432,12 +432,10 @@ interface QueryInterface extends ConditionSetInterface {
* access checks, if available and enabled for the index.
* - search_api_bypass_access: If set to TRUE, entity access checks will be
* skipped, even if enabled for the index.
* - search_api_retrieved_properties: A list of properties that will be
* required from results by the code displaying the results list. This
* option, if present, should be an array keyed by datasource ID and
* (datasource-internal) property path, with combined property paths as
* the values. (The "_object" pseudo-property can be used where a whole
* object (entity or other) is required.)
* - search_api_retrieved_field_values: A list of IDs of fields whose values
* should be returned along with the results by the backend, if possible.
* For backends that support retrieving fields values, this allows them to
* only retrieve the values that are actually needed.
* However, contrib modules might introduce arbitrary other keys with their
* own, special meaning. (Usually they should be prefixed with the module
* name, though, to avoid conflicts.)
@@ -174,9 +174,6 @@ interface ResultSetInterface extends \Traversable {
* key instead.
*
* @return $this
*
* @todo Add unsetExtraData() instead of special NULL handling? And/or
* just have to use &getAllExtraData()?
*/
public function setExtraData($key, $data = NULL);
@@ -279,7 +279,16 @@ class TaskManager implements TaskManagerInterface {
// Schedule the batch.
batch_set($batch_definition);
if (function_exists('drush_backend_batch_process')) {
drush_backend_batch_process();
$result = drush_backend_batch_process();
// Drush performs batch processing in a separate PHP request. When the
// last batch is processed the batch list is cleared, but this only takes
// effect in the other request. Take the same action here to ensure that
// we are not requeueing stale batches when there are multiple tasks being
// handled in a single request.
if ($result['context']['drush_batch_process_finished'] === TRUE) {
$batch = &batch_get();
$batch = NULL;
}
}
}
@@ -2,10 +2,10 @@
namespace Drupal\search_api\Tracker;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\search_api\Utility\Utility;
/**
* Manages tracker plugins.
@@ -45,7 +45,7 @@ class TrackerPluginManager extends DefaultPluginManager {
public function getOptionsList() {
$options = [];
foreach ($this->getDefinitions() as $plugin_id => $plugin_definition) {
$options[$plugin_id] = Html::escape($plugin_definition['label']);
$options[$plugin_id] = Utility::escapeHtml($plugin_definition['label']);
}
return $options;
}
@@ -2,7 +2,6 @@
namespace Drupal\search_api\Utility;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\search_api\LoggerTrait;
@@ -50,7 +49,10 @@ class PostRequestIndexing implements PostRequestIndexingInterface, DestructableI
try {
$storage = $this->entityTypeManager->getStorage('search_api_index');
}
catch (InvalidPluginDefinitionException $e) {
// @todo @todo Replace with multi-catch for
// InvalidPluginDefinitionException and PluginNotFoundException once we
// depend on PHP 7.1+.
catch (\Exception $e) {
// It might be possible that the module got uninstalled during the rest
// of the page request, or something else happened. To be on the safe
// side, catch the exception in case the entity type isn't found.
@@ -2,10 +2,13 @@
namespace Drupal\search_api\Utility;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Render\Markup;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Plugin\search_api\data_type\value\TextToken;
@@ -248,4 +251,22 @@ class Utility {
return in_array($value, $settings['selected']) != $settings['default'];
}
/**
* Escapes HTML special characters in plain text, if necessary.
*
* @param string|\Drupal\Component\Render\MarkupInterface $text
* The text to escape.
*
* @return \Drupal\Component\Render\MarkupInterface
* If a markup object was passed as $text, it is returned as-is. Otherwise,
* the text is escaped and returned
*/
public static function escapeHtml($text) {
if ($text instanceof MarkupInterface) {
return $text;
}
return Markup::create(Html::escape((string) $text));
}
}
@@ -7,8 +7,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -9,8 +9,8 @@ use Drupal\search_api\DataType\DataTypePluginBase;
*
* @SearchApiDataType(
* id = "search_api_test",
* label = @Translation("Test data type"),
* description = @Translation("Dummy data type implementation")
* label = @Translation("&quot;Test&quot; data type"),
* description = @Translation("Dummy <em>data type</em> implementation")
* )
*/
class TestDataType extends DataTypePluginBase {
@@ -11,7 +11,8 @@ use Drupal\search_api_test\TestPluginTrait;
*
* @SearchApiDatasource(
* id = "search_api_test",
* label = @Translation("Test datasource"),
* label = @Translation("&quot;Test&quot; datasource"),
* description = @Translation("This is the <em>test datasource</em> plugin description."),
* )
*/
class TestDatasource extends DatasourcePluginBase {
@@ -13,8 +13,8 @@ use Drupal\search_api_test\TestPluginTrait;
*
* @SearchApiTracker(
* id = "search_api_test",
* label = @Translation("Test tracker"),
* description = @Translation("This is the test tracker plugin description."),
* label = @Translation("&quot;Test&quot; tracker"),
* description = @Translation("This is the <em>test tracker</em> plugin description."),
* )
*/
class TestTracker extends TrackerPluginBase implements PluginFormInterface {
@@ -0,0 +1,15 @@
<?php
namespace Drupal\search_api_test\Plugin\search_api\tracker;
/**
* Provides a tracker implementation which uses a FIFO-like processing order.
*
* @SearchApiTracker(
* id = "search_api_test_string_label",
* label = "&quot;String label&quot; test tracker",
* description = "This is the <em>test tracker with string label</em> plugin description.",
* )
*/
class TestTrackerStringLabel extends TestTracker {
}
@@ -8,8 +8,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -7,8 +7,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -7,8 +7,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -7,8 +7,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -8,8 +8,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -0,0 +1,14 @@
name: 'Search API Test No UI'
type: module
description: 'Support module for Search API tests ("No UI" plugins)'
package: Search
dependencies:
- search_api:search_api
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1542564486
@@ -0,0 +1,45 @@
<?php
namespace Drupal\search_api_test_no_ui\Plugin\search_api\backend;
use Drupal\search_api\Backend\BackendPluginBase;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Query\QueryInterface;
/**
* Provides a test backend that should be hidden from the UI.
*
* @SearchApiBackend(
* id = "search_api_test_no_ui",
* label = @Translation("No UI backend"),
* no_ui = true,
* )
*/
class NoUi extends BackendPluginBase {
/**
* {@inheritdoc}
*/
public function indexItems(IndexInterface $index, array $items) {
return [];
}
/**
* {@inheritdoc}
*/
public function deleteItems(IndexInterface $index, array $item_ids) {
}
/**
* {@inheritdoc}
*/
public function deleteAllIndexItems(IndexInterface $index, $datasource_id = NULL) {
}
/**
* {@inheritdoc}
*/
public function search(QueryInterface $query) {
}
}
@@ -0,0 +1,17 @@
<?php
namespace Drupal\search_api_test_no_ui\Plugin\search_api\data_type;
use Drupal\search_api\DataType\DataTypePluginBase;
/**
* Provides a test data type that should be hidden from the UI.
*
* @SearchApiDataType(
* id = "search_api_test_no_ui",
* label = @Translation("No UI data type"),
* no_ui = true,
* )
*/
class NoUi extends DataTypePluginBase {
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\search_api_test_no_ui\Plugin\search_api\datasource;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\search_api\Datasource\DatasourcePluginBase;
/**
* Provides a test datasource that should be hidden from the UI.
*
* @SearchApiDatasource(
* id = "search_api_test_no_ui",
* label = @Translation("No UI datasource"),
* no_ui = true,
* )
*/
class NoUi extends DatasourcePluginBase {
/**
* {@inheritdoc}
*/
public function getItemId(ComplexDataInterface $item) {
return NULL;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Drupal\search_api_test_no_ui\Plugin\search_api\parse_mode;
use Drupal\search_api\ParseMode\ParseModePluginBase;
/**
* Provides a parse mode that should be hidden from the UI.
*
* @SearchApiParseMode(
* id = "search_api_test_no_ui",
* label = @Translation("No UI parse mode"),
* no_ui = true,
* )
*/
class NoUi extends ParseModePluginBase {
/**
* {@inheritdoc}
*/
public function parseInput($keys) {
return $keys;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Drupal\search_api_test_no_ui\Plugin\search_api\processor;
use Drupal\search_api\Processor\ProcessorPluginBase;
/**
* Provides a test processor that should be hidden from the UI.
*
* @SearchApiProcessor(
* id = "search_api_test_no_ui",
* label = @Translation("No UI processor"),
* no_ui = true,
* )
*/
class NoUi extends ProcessorPluginBase {
/**
* {@inheritdoc}
*/
public function supportsStage($stage_identifier) {
return TRUE;
}
}
@@ -0,0 +1,18 @@
<?php
namespace Drupal\search_api_test_no_ui\Plugin\search_api\tracker;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\search_api\Plugin\search_api\tracker\Basic;
/**
* Provides a test tracker that should be hidden from the UI.
*
* @SearchApiTracker(
* id = "search_api_test_no_ui",
* label = @Translation("No UI tracker"),
* no_ui = true,
* )
*/
class NoUi extends Basic implements PluginFormInterface {
}
@@ -7,8 +7,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -11,8 +11,8 @@ dependencies:
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-07-19
version: '8.x-1.9'
# Information added by Drupal.org packaging script on 2018-11-18
version: '8.x-1.11'
core: '8.x'
project: 'search_api'
datestamp: 1531985591
datestamp: 1542564486
@@ -10,18 +10,12 @@ use Drupal\search_api\Query\QueryInterface;
/**
* Implements hook_search_api_query_alter().
*
* Prints the contents of the "search_api_retrieved_properties" query option to
* the page (if present) so it can be checked by the testing code.
* Prints the contents of the "search_api_retrieved_field_values" query option
* to the page (if present) so it can be checked by the testing code.
*/
function search_api_test_views_search_api_query_alter(QueryInterface $query) {
$properties = $query->getOption('search_api_retrieved_properties');
if ($properties) {
$message = [];
foreach ($properties as $datasource_properties) {
foreach ($datasource_properties as $combined_property_path) {
$message[] = "'$combined_property_path'";
}
}
\Drupal::messenger()->addStatus(implode(' ', $message));
$fields = $query->getOption('search_api_retrieved_field_values');
if ($fields) {
\Drupal::messenger()->addStatus("'" . implode("' '", $fields) . "'");
}
}
@@ -63,6 +63,7 @@ class IntegrationTest extends SearchApiBrowserTestBase {
'node',
'search_api',
'search_api_test',
'search_api_test_no_ui',
'field_ui',
'link',
'image',
@@ -204,6 +205,8 @@ class IntegrationTest extends SearchApiBrowserTestBase {
$this->drupalGet($settings_path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('No UI backend');
$edit = [
'name' => '',
'status' => 1,
@@ -280,6 +283,14 @@ class IntegrationTest extends SearchApiBrowserTestBase {
$this->drupalGet($settings_path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('No UI datasource');
$this->assertSession()->pageTextNotContains('No UI tracker');
// Make sure plugin labels are only escaped when necessary.
$this->assertHtmlEscaped('"Test" tracker');
$this->assertHtmlEscaped('&quot;String label&quot; test tracker');
$this->assertHtmlEscaped('"Test" datasource');
// Make sure datasource and tracker plugin descriptions are displayed.
$dummy_index = Index::create();
foreach (['createDatasourcePlugins', 'createTrackerPlugins'] as $method) {
@@ -288,9 +299,8 @@ class IntegrationTest extends SearchApiBrowserTestBase {
->get('search_api.plugin_helper')
->$method($dummy_index);
foreach ($plugins as $plugin) {
$description = strip_tags($plugin->getDescription());
$description = Html::decodeEntities($description);
$this->assertSession()->pageTextContains($description);
$description = Utility::escapeHtml($plugin->getDescription());
$this->assertSession()->responseContains($description);
}
}
@@ -767,6 +777,9 @@ class IntegrationTest extends SearchApiBrowserTestBase {
$this->drupalGet($this->getIndexPath('fields'));
$this->assertHtmlEscaped($field_name);
// Also check data type labels/descriptions.
$this->assertHtmlEscaped('"Test" data type');
$this->assertSession()->responseContains('Dummy <em>data type</em> implementation');
$edit = [
'datasource_configs[entity:node][bundles][default]' => 1,
@@ -807,6 +820,8 @@ class IntegrationTest extends SearchApiBrowserTestBase {
$this->addField('entity:node', $property_path, $label);
}
$this->assertSession()->pageTextNotContains('No UI data type');
$index = $this->getIndex(TRUE);
$fields = $index->getFields();
@@ -1206,7 +1221,7 @@ class IntegrationTest extends SearchApiBrowserTestBase {
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('All content was scheduled for reindexing so the new settings can take effect.');
$this->assertSession()->responseContains($this->getIndex()->toUrl()->toString());
$this->assertSession()->responseContains($this->getIndex()->toUrl('canonical')->toString());
}
/**
@@ -210,13 +210,7 @@ class OverviewPageTest extends SearchApiBrowserTestBase {
$this->drupalGet($this->overviewPageUrl);
$basic_url = $this->urlGenerator->generateFromRoute('entity.search_api_server.canonical', ['search_api_server' => $server->id()]);
$destination = '';
// Drupal 8.5.x introduced "destination" parameters to all operations links
// by default, so we now need to take that into account.
// @todo Remove once we depend on 8.5.
if (version_compare(\Drupal::VERSION, '8.5.x-dev', '>=')) {
$destination = "?destination=" . $this->urlGenerator->generateFromRoute('search_api.overview');
}
$destination = "?destination=" . $this->urlGenerator->generateFromRoute('search_api.overview');
$this->assertSession()->responseContains("<a href=\"$basic_url/edit$destination\">Edit</a>");
$this->assertSession()->responseContains("<a href=\"$basic_url/disable$destination\">Disable</a>");
$this->assertSession()->responseContains("<a href=\"$basic_url/delete$destination\">Delete</a>");
@@ -30,6 +30,7 @@ class ProcessorIntegrationTest extends SearchApiBrowserTestBase {
public static $modules = [
'filter',
'taxonomy',
'search_api_test_no_ui',
];
/**
@@ -160,6 +161,13 @@ class ProcessorIntegrationTest extends SearchApiBrowserTestBase {
sort($actual_processors);
$this->assertEquals($enabled, $actual_processors);
$this->checkNoUiIntegration();
$enabled[] = 'search_api_test_no_ui';
sort($enabled);
$actual_processors = array_keys($this->loadIndex()->getProcessors());
sort($actual_processors);
$this->assertEquals($enabled, $actual_processors);
$this->checkRoleFilterIntegration();
$enabled[] = 'role_filter';
sort($enabled);
@@ -506,6 +514,21 @@ TAGS
$this->enableProcessor('entity_status');
}
/**
* Tests the "No UI" test processor.
*/
public function checkNoUiIntegration() {
$this->loadProcessorsTab();
$this->assertSession()->pageTextNotContains('No UI processor');
// Ensure that the processor can still be enabled programmatically and
// stays enabled when submitting the processors form.
$index = $this->loadIndex();
$processor = \Drupal::getContainer()->get('search_api.plugin_helper')
->createProcessorPlugin($index, 'search_api_test_no_ui');
$index->addProcessor($processor)->save();
}
/**
* Tests the UI for the "Role filter" processor.
*/
@@ -252,6 +252,9 @@ class ViewsTest extends SearchApiBrowserTestBase {
// have any effect.
$this->checkResults([], [2, 4, 5], 'Search with arguments', 'all/item+article/strawberry+apple');
// Check "OR" contextual filters (using commas).
$this->checkResults([], [4], 'Search with OR arguments', 'all/item,article/strawberry,apple');
$this->checkResults([], [], 'Search with unknown datasource argument', 'entity:foobar/all/all');
$query = [
@@ -663,6 +666,19 @@ class ViewsTest extends SearchApiBrowserTestBase {
$this->submitForm($edit, 'Expose filter');
$this->submitPluginForm([]);
// Add a "Search: Fulltext search" filter.
$this->clickLink('Add filter criteria');
$edit = [
'name[search_api_index_database_search_index.search_api_fulltext]' => 'search_api_index_database_search_index.search_api_fulltext',
];
$this->submitForm($edit, 'Add and configure filter criteria');
$this->assertSession()->pageTextNotContains('No UI parse mode');
$edit = [
'options[expose_button][checkbox][checkbox]' => 1,
];
$this->submitForm($edit, 'Expose filter');
$this->submitPluginForm([]);
// Save the view.
$this->submitForm([], 'Save');
$this->assertSession()->statusCodeEquals(200);
@@ -723,21 +739,18 @@ class ViewsTest extends SearchApiBrowserTestBase {
}
}
// Check whether the expected retrieved properties were listed on the page.
// Since the fields with the "field_rendering" option enabled will need the
// complete loaded entity, these are only present as "_object" here.
// Check whether the expected retrieved fields were listed on the page.
// These are only "keywords" and "rendered_item", since only fields that
// correspond to an indexed field are included (not when a field is added
// via the datasource table), and only if "Use entity field rendering" is
// disabled.
// @see search_api_test_views_search_api_query_alter()
$retrieved_properties = [
Utility::createCombinedId($datasource_id, 'id'),
Utility::createCombinedId($datasource_id, '_object'),
Utility::createCombinedId($datasource_id, 'keywords'),
Utility::createCombinedId($datasource_id, 'user_id'),
Utility::createCombinedId($datasource_id, 'user_id:entity:_object'),
Utility::createCombinedId($datasource_id, 'user_id:entity:roles'),
Utility::createCombinedId(NULL, 'rendered_item'),
$retrieved_fields = [
'keywords',
'rendered_item',
];
foreach ($retrieved_properties as $combined_property_path) {
$this->assertSession()->pageTextContains("'$combined_property_path'");
foreach ($retrieved_fields as $field_id) {
$this->assertSession()->pageTextContains("'$field_id'");
}
// Check that click-sorting works correctly.
@@ -88,7 +88,9 @@ abstract class BackendTestBase extends KernelTestBase {
$this->searchNoResults();
$this->indexItems($this->indexId);
$this->searchSuccess();
$this->checkFacets();
if ($this->getServer()->supportsFeature('search_api_facets')) {
$this->checkFacets();
}
$this->checkSecondServer();
$this->regressionTests();
$this->clearIndex();
@@ -0,0 +1,158 @@
<?php
namespace Drupal\Tests\search_api\Kernel\Processor;
use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\Item\Field;
use Drupal\search_api\Processor\ProcessorPropertyInterface;
use Drupal\user\Entity\User;
/**
* Tests the "Reverse entity references" processor.
*
* @group search_api
*
* @coversDefaultClass \Drupal\search_api\Plugin\search_api\processor\ReverseEntityReferences
*/
class ReverseEntityReferencesTest extends ProcessorTestBase {
/**
* The nodes created for testing.
*
* @var \Drupal\node\Entity\Node[]
*/
protected $nodes;
/**
* The author UIDs of the created nodes, keyed by index in $this->nodes.
*
* @var array
*/
protected $nodeUids = [
1,
0,
1,
2,
0,
1,
];
/**
* {@inheritdoc}
*/
public function setUp($processor = NULL) {
parent::setUp('reverse_entity_references');
// Create a node type for testing.
$type = NodeType::create(['type' => 'page', 'name' => 'page']);
$type->save();
// Insert the anonymous user into the database, as well as some other users.
User::create([
'uid' => 0,
'name' => '',
])->save();
User::create([
'uid' => 1,
'name' => 'admin',
])->save();
User::create([
'uid' => 2,
'name' => 'user',
])->save();
User::create([
'uid' => 3,
'name' => 'other user',
])->save();
// Create nodes.
foreach ($this->nodeUids as $i => $uid) {
$values = [
'type' => 'page',
'title' => 'test title',
'uid' => $uid,
];
$this->nodes[$i] = Node::create($values);
$this->nodes[$i]->save();
}
// Switch the index to index users and add a reverse reference to the nodes
// authored by the indexed user.
$datasources = \Drupal::getContainer()
->get('search_api.plugin_helper')
->createDatasourcePlugins($this->index, [
'entity:user',
]);
$this->index->setDatasources($datasources);
$field = new Field($this->index, 'nid');
$field->setType('integer');
$field->setPropertyPath('search_api_reverse_entity_references_node__uid:nid');
$field->setDatasourceId('entity:user');
$field->setLabel('Authored nodes');
$this->index->addField($field);
$this->index->save();
\Drupal::getContainer()
->get('search_api.index_task_manager')
->addItemsAll($this->index);
$index_storage = \Drupal::entityTypeManager()
->getStorage('search_api_index');
$index_storage->resetCache([$this->index->id()]);
$this->index = $index_storage->load($this->index->id());
}
/**
* Tests that property definitions are created correctly.
*
* @covers ::getPropertyDefinitions
*/
public function testGetPropertyDefinitions() {
$properties = $this->processor->getPropertyDefinitions(NULL);
$this->assertEmpty($properties);
$datasource = $this->createMock(DatasourceInterface::class);
$datasource->method('getEntityTypeId')
->willReturn(NULL);
$properties = $this->processor->getPropertyDefinitions($datasource);
$this->assertEmpty($properties);
$datasource = $this->index->getDatasource('entity:user');
$properties = $this->processor->getPropertyDefinitions($datasource);
$this->assertArrayHasKey('search_api_reverse_entity_references_node__uid', $properties);
/** @var \Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface|\Drupal\search_api\Processor\ProcessorPropertyInterface $property */
$property = $properties['search_api_reverse_entity_references_node__uid'];
$this->assertInstanceOf(EntityDataDefinitionInterface::class, $property);
$this->assertInstanceOf(ProcessorPropertyInterface::class, $property);
$this->assertEquals('node', $property->getEntityTypeId());
$this->assertEquals('reverse_entity_references', $property->getProcessorId());
}
/**
* Tests that field value extraction works correctly.
*
* @covers ::addFieldValues
*/
public function testAddFieldValues() {
$nids_by_user = [];
foreach ($this->nodeUids as $i => $uid) {
$nids_by_user[$uid][] = $this->nodes[$i]->id();
}
$fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
foreach ([0, 1, 2, 3] as $uid) {
$item = $fields_helper->createItem($this->index, "entity:user/$uid:en");
// This will automatically trigger field extraction.
$nids = $item->getField('nid')->getValues();
sort($nids);
$nids_by_user += [$uid => []];
$this->assertEquals($nids_by_user[$uid], $nids, "Unexpected field values extracted for user #$uid.");
}
}
}
@@ -71,6 +71,7 @@ class AggregatedFieldsTest extends UnitTestCase {
'datasourceInstances' => [
'entity:test1' => $datasource,
'entity:test2' => $datasource,
'entity:test3' => $datasource,
],
'processorInstances' => [],
'field_settings' => [
@@ -89,6 +90,11 @@ class AggregatedFieldsTest extends UnitTestCase {
'datasource_id' => 'entity:test2',
'property_path' => 'foobaz:bla',
],
'always_empty' => [
'type' => 'string',
'datasource_id' => 'entity:test3',
'property_path' => 'always_empty',
],
'aggregated_field' => [
'type' => 'text',
'property_path' => 'aggregated_field',
@@ -146,6 +152,7 @@ class AggregatedFieldsTest extends UnitTestCase {
'entity:test1/foo',
'entity:test1/foo:bar',
'entity:test2/foobaz:bla',
'entity:test3/always_empty',
],
];
$this->index->getField($this->fieldId)->setConfiguration($configuration);
@@ -166,7 +173,7 @@ class AggregatedFieldsTest extends UnitTestCase {
}
$items = [];
$i = 0;
foreach (['entity:test1', 'entity:test2'] as $datasource_id) {
foreach (['entity:test1', 'entity:test2', 'entity:test3'] as $datasource_id) {
$this->itemIds[$i++] = $item_id = Utility::createCombinedId($datasource_id, '1:en');
$item = \Drupal::getContainer()
->get('search_api.fields_helper')
@@ -191,6 +198,7 @@ class AggregatedFieldsTest extends UnitTestCase {
$this->assertEquals(array_map($this->valueCallback, $expected[0]), $items[$this->itemIds[0]]->getField($this->fieldId)->getValues(), 'Correct aggregation for item 1.');
$this->assertEquals(array_map($this->valueCallback, $expected[1]), $items[$this->itemIds[1]]->getField($this->fieldId)->getValues(), 'Correct aggregation for item 2.');
$this->assertEquals(array_map($this->valueCallback, $expected[2]), $items[$this->itemIds[2]]->getField($this->fieldId)->getValues(), 'Correct aggregation for item 3.');
}
/**
@@ -209,6 +217,7 @@ class AggregatedFieldsTest extends UnitTestCase {
[
['foo', 'bar', 'baz'],
['foobar'],
[],
],
],
'"Concatenation" aggregation' => [
@@ -216,6 +225,7 @@ class AggregatedFieldsTest extends UnitTestCase {
[
["foo\n\nbar\n\nbaz"],
['foobar'],
[''],
],
],
'"Sum" aggregation' => [
@@ -223,6 +233,7 @@ class AggregatedFieldsTest extends UnitTestCase {
[
[22],
[7],
[0],
],
TRUE,
],
@@ -231,6 +242,7 @@ class AggregatedFieldsTest extends UnitTestCase {
[
[3],
[1],
[0],
],
],
'"Maximum" aggregation' => [
@@ -238,6 +250,7 @@ class AggregatedFieldsTest extends UnitTestCase {
[
[16],
[7],
[],
],
TRUE,
],
@@ -246,6 +259,7 @@ class AggregatedFieldsTest extends UnitTestCase {
[
[2],
[7],
[],
],
TRUE,
],
@@ -254,6 +268,7 @@ class AggregatedFieldsTest extends UnitTestCase {
[
['foo'],
['foobar'],
[],
],
],
'"Last" aggregation' => [
@@ -261,6 +276,7 @@ class AggregatedFieldsTest extends UnitTestCase {
[
['baz'],
['foobar'],
[],
],
],
];
@@ -468,6 +468,60 @@ class HighlightTest extends UnitTestCase {
$this->assertEquals($correct_output, $excerpt, 'Excerpt was added.');
}
/**
* Tests whether excerpt creation correctly handles HTML tags.
*/
public function testPostprocessSearchResultsExcerptStripTags() {
$query = $this->createMock(QueryInterface::class);
$query->expects($this->once())
->method('getProcessingLevel')
->willReturn(QueryInterface::PROCESSING_FULL);
$query->expects($this->atLeastOnce())
->method('getOriginalKeys')
->will($this->returnValue(['#conjunction' => 'AND', 'foo']));
/** @var \Drupal\search_api\Query\QueryInterface $query */
$field = $this->createTestField('body', 'entity:node/body');
$this->index->expects($this->atLeastOnce())
->method('getFields')
->will($this->returnValue(['body' => $field]));
$this->processor->setIndex($this->index);
$body_value = <<<'END'
Sentence with foo keyword.
<script>
var foo = 1;
</script>
<style>
a.foo {
font-weight: bold;
}
</style>
And another foo!
END;
$fields = [
'entity:node/body' => [
'type' => 'text',
'values' => [$body_value],
],
];
$items = $this->createItems($this->index, 1, $fields);
$results = new ResultSet($query);
$results->setResultItems($items);
$results->setResultCount(1);
$this->processor->postprocessSearchResults($results);
$output = $results->getResultItems();
$excerpt = $output[$this->itemIds[0]]->getExcerpt();
$correct_output = '… Sentence with <strong>foo</strong> keyword. And another <strong>foo</strong>! …';
$this->assertEquals($correct_output, $excerpt, 'Excerpt was added.');
}
/**
* Tests whether highlighting works on a longer text matching near the end.
*/
@@ -600,6 +654,50 @@ class HighlightTest extends UnitTestCase {
$this->assertEmpty($excerpt, 'No excerpt added when disabled.');
}
/**
* Tests whether excerpt creation uses the "highlighted_keys" extra data.
*/
public function testPostprocessSearchResultsExcerptWithKeysFromBackend() {
$query = $this->createMock(QueryInterface::class);
$query->expects($this->once())
->method('getProcessingLevel')
->willReturn(QueryInterface::PROCESSING_FULL);
$query->expects($this->atLeastOnce())
->method('getOriginalKeys')
->will($this->returnValue(['#conjunction' => 'AND', 'congues']));
/** @var \Drupal\search_api\Query\QueryInterface $query */
$field = $this->createTestField('body', 'entity:node/body');
$this->index->expects($this->atLeastOnce())
->method('getFields')
->will($this->returnValue(['body' => $field]));
$this->processor->setIndex($this->index);
$body_values = [$this->getFieldBody()];
$fields = [
'entity:node/body' => [
'type' => 'text',
'values' => $body_values,
],
];
$items = $this->createItems($this->index, 1, $fields);
$items[$this->itemIds[0]]->setExtraData('highlighted_keys', ['congue']);
$results = new ResultSet($query);
$results->setResultItems($items);
$results->setResultCount(1);
$this->processor->postprocessSearchResults($results);
$output = $results->getResultItems();
$excerpt = $output[$this->itemIds[0]]->getExcerpt();
$correct_output = '… tristique, ligula sit amet condimentum dapibus, lorem nunc <strong>congue</strong> velit, et dictum augue leo sodales augue. Maecenas …';
$this->assertEquals($correct_output, $excerpt, 'Excerpt was added.');
}
/**
* Tests whether highlighting works on a longer text.
*/