Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\search\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a SearchPlugin type annotation object.
*
* SearchPlugin classes define search types for the core Search module. Each
* search type can be used to create search pages from the Search settings page.
*
* @see SearchPluginBase
*
* @ingroup search
*
* @Annotation
*/
class SearchPlugin extends Plugin {
/**
* A unique identifier for the search plugin.
*
* @var string
*/
public $id;
/**
* The title for the search page tab.
*
* @todo This will potentially be translated twice or cached with the wrong
* translation until the search tabs are converted to local task plugins.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $title;
}

View file

@ -0,0 +1,236 @@
<?php
namespace Drupal\search\Controller;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\RendererInterface;
use Drupal\search\Form\SearchPageForm;
use Drupal\search\SearchPageInterface;
use Drupal\search\SearchPageRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Route controller for search.
*/
class SearchController extends ControllerBase {
/**
* The search page repository.
*
* @var \Drupal\search\SearchPageRepositoryInterface
*/
protected $searchPageRepository;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new search controller.
*
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
* The search page repository.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(SearchPageRepositoryInterface $search_page_repository, RendererInterface $renderer) {
$this->searchPageRepository = $search_page_repository;
$this->logger = $this->getLogger('search');
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('search.search_page_repository'),
$container->get('renderer')
);
}
/**
* Creates a render array for the search page.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\search\SearchPageInterface $entity
* The search page entity.
*
* @return array
* The search form and search results build array.
*/
public function view(Request $request, SearchPageInterface $entity) {
$build = [];
$plugin = $entity->getPlugin();
// Build the form first, because it may redirect during the submit,
// and we don't want to build the results based on last time's request.
$build['#cache']['contexts'][] = 'url.query_args:keys';
if ($request->query->has('keys')) {
$keys = trim($request->query->get('keys'));
$plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
}
$build['#title'] = $plugin->suggestedTitle();
$build['search_form'] = $this->formBuilder()->getForm(SearchPageForm::class, $entity);
// Build search results, if keywords or other search parameters are in the
// GET parameters. Note that we need to try the search if 'keys' is in
// there at all, vs. being empty, due to advanced search.
$results = [];
if ($request->query->has('keys')) {
if ($plugin->isSearchExecutable()) {
// Log the search.
if ($this->config('search.settings')->get('logging')) {
$this->logger->notice('Searched %type for %keys.', ['%keys' => $keys, '%type' => $entity->label()]);
}
// Collect the search results.
$results = $plugin->buildResults();
}
else {
// The search not being executable means that no keywords or other
// conditions were entered.
$this->messenger()->addError($this->t('Please enter some keywords.'));
}
}
if (count($results)) {
$build['search_results_title'] = [
'#markup' => '<h2>' . $this->t('Search results') . '</h2>',
];
}
$build['search_results'] = [
'#theme' => ['item_list__search_results__' . $plugin->getPluginId(), 'item_list__search_results'],
'#items' => $results,
'#empty' => [
'#markup' => '<h3>' . $this->t('Your search yielded no results.') . '</h3>',
],
'#list_type' => 'ol',
'#context' => [
'plugin' => $plugin->getPluginId(),
],
];
$this->renderer->addCacheableDependency($build, $entity);
if ($plugin instanceof CacheableDependencyInterface) {
$this->renderer->addCacheableDependency($build, $plugin);
}
// If this plugin uses a search index, then also add the cache tag tracking
// that search index, so that cached search result pages are invalidated
// when necessary.
if ($plugin->getType()) {
$build['search_results']['#cache']['tags'][] = 'search_index';
$build['search_results']['#cache']['tags'][] = 'search_index:' . $plugin->getType();
}
$build['pager'] = [
'#type' => 'pager',
];
return $build;
}
/**
* Creates a render array for the search help page.
*
* @param \Drupal\search\SearchPageInterface $entity
* The search page entity.
*
* @return array
* The search help page.
*/
public function searchHelp(SearchPageInterface $entity) {
$build = [];
$build['search_help'] = $entity->getPlugin()->getHelp();
return $build;
}
/**
* Redirects to a search page.
*
* This is used to redirect from /search to the default search page.
*
* @param \Drupal\search\SearchPageInterface $entity
* The search page entity.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect to the search page.
*/
public function redirectSearchPage(SearchPageInterface $entity) {
return $this->redirect('search.view_' . $entity->id());
}
/**
* Route title callback.
*
* @param \Drupal\search\SearchPageInterface $search_page
* The search page entity.
*
* @return string
* The title for the search page edit form.
*/
public function editTitle(SearchPageInterface $search_page) {
return $this->t('Edit %label search page', ['%label' => $search_page->label()]);
}
/**
* Performs an operation on the search page entity.
*
* @param \Drupal\search\SearchPageInterface $search_page
* The search page entity.
* @param string $op
* The operation to perform, usually 'enable' or 'disable'.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect back to the search settings page.
*/
public function performOperation(SearchPageInterface $search_page, $op) {
$search_page->$op()->save();
if ($op == 'enable') {
$this->messenger()->addStatus($this->t('The %label search page has been enabled.', ['%label' => $search_page->label()]));
}
elseif ($op == 'disable') {
$this->messenger()->addStatus($this->t('The %label search page has been disabled.', ['%label' => $search_page->label()]));
}
$url = $search_page->urlInfo('collection');
return $this->redirect($url->getRouteName(), $url->getRouteParameters(), $url->getOptions());
}
/**
* Sets the search page as the default.
*
* @param \Drupal\search\SearchPageInterface $search_page
* The search page entity.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect to the search settings page.
*/
public function setAsDefault(SearchPageInterface $search_page) {
// Set the default page to this search page.
$this->searchPageRepository->setDefaultSearchPage($search_page);
$this->messenger()->addStatus($this->t('The default search page is now %label. Be sure to check the ordering of your search pages.', ['%label' => $search_page->label()]));
return $this->redirect('entity.search_page.collection');
}
}

View file

@ -0,0 +1,264 @@
<?php
namespace Drupal\search\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\search\Plugin\SearchIndexingInterface;
use Drupal\search\Plugin\SearchPluginCollection;
use Drupal\search\SearchPageInterface;
/**
* Defines a configured search page.
*
* @ConfigEntityType(
* id = "search_page",
* label = @Translation("Search page"),
* label_collection = @Translation("Search pages"),
* label_singular = @Translation("search page"),
* label_plural = @Translation("search pages"),
* label_count = @PluralTranslation(
* singular = "@count search page",
* plural = "@count search pages",
* ),
* handlers = {
* "access" = "Drupal\search\SearchPageAccessControlHandler",
* "list_builder" = "Drupal\search\SearchPageListBuilder",
* "form" = {
* "add" = "Drupal\search\Form\SearchPageAddForm",
* "edit" = "Drupal\search\Form\SearchPageEditForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm"
* }
* },
* admin_permission = "administer search",
* links = {
* "edit-form" = "/admin/config/search/pages/manage/{search_page}",
* "delete-form" = "/admin/config/search/pages/manage/{search_page}/delete",
* "enable" = "/admin/config/search/pages/manage/{search_page}/enable",
* "disable" = "/admin/config/search/pages/manage/{search_page}/disable",
* "set-default" = "/admin/config/search/pages/manage/{search_page}/set-default",
* "collection" = "/admin/config/search/pages",
* },
* config_prefix = "page",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "weight" = "weight",
* "status" = "status"
* },
* config_export = {
* "id",
* "label",
* "path",
* "weight",
* "plugin",
* "configuration",
* }
* )
*/
class SearchPage extends ConfigEntityBase implements SearchPageInterface, EntityWithPluginCollectionInterface {
/**
* The name (plugin ID) of the search page entity.
*
* @var string
*/
protected $id;
/**
* The label of the search page entity.
*
* @var string
*/
protected $label;
/**
* The configuration of the search page entity.
*
* @var array
*/
protected $configuration = [];
/**
* The search plugin ID.
*
* @var string
*/
protected $plugin;
/**
* The path this search page will appear upon.
*
* This value is appended to 'search/' when building the path.
*
* @var string
*/
protected $path;
/**
* The weight of the search page.
*
* @var int
*/
protected $weight;
/**
* The plugin collection that stores search plugins.
*
* @var \Drupal\search\Plugin\SearchPluginCollection
*/
protected $pluginCollection;
/**
* {@inheritdoc}
*/
public function getPlugin() {
return $this->getPluginCollection()->get($this->plugin);
}
/**
* Encapsulates the creation of the search page's LazyPluginCollection.
*
* @return \Drupal\Component\Plugin\LazyPluginCollection
* The search page's plugin collection.
*/
protected function getPluginCollection() {
if (!$this->pluginCollection) {
$this->pluginCollection = new SearchPluginCollection($this->searchPluginManager(), $this->plugin, $this->configuration, $this->id());
}
return $this->pluginCollection;
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
return ['configuration' => $this->getPluginCollection()];
}
/**
* {@inheritdoc}
*/
public function setPlugin($plugin_id) {
$this->plugin = $plugin_id;
$this->getPluginCollection()->addInstanceID($plugin_id);
}
/**
* {@inheritdoc}
*/
public function isIndexable() {
return $this->status() && $this->getPlugin() instanceof SearchIndexingInterface;
}
/**
* {@inheritdoc}
*/
public function isDefaultSearch() {
return $this->searchPageRepository()->getDefaultSearchPage() == $this->id();
}
/**
* {@inheritdoc}
*/
public function getPath() {
return $this->path;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->weight;
}
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageInterface $storage) {
parent::postCreate($storage);
// @todo Use self::applyDefaultValue() once
// https://www.drupal.org/node/2004756 is in.
if (!isset($this->weight)) {
$this->weight = $this->isDefaultSearch() ? -10 : 0;
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
$this->routeBuilder()->setRebuildNeeded();
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
$search_page_repository = \Drupal::service('search.search_page_repository');
if (!$search_page_repository->isSearchActive()) {
$search_page_repository->clearDefaultSearchPage();
}
}
/**
* {@inheritdoc}
*/
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
/** @var $a \Drupal\search\SearchPageInterface */
/** @var $b \Drupal\search\SearchPageInterface */
$a_status = (int) $a->status();
$b_status = (int) $b->status();
if ($a_status != $b_status) {
return ($a_status > $b_status) ? -1 : 1;
}
return parent::sort($a, $b);
}
/**
* Wraps the route builder.
*
* @return \Drupal\Core\Routing\RouteBuilderInterface
* An object for state storage.
*/
protected function routeBuilder() {
return \Drupal::service('router.builder');
}
/**
* Wraps the config factory.
*
* @return \Drupal\Core\Config\ConfigFactoryInterface
* A config factory object.
*/
protected function configFactory() {
return \Drupal::service('config.factory');
}
/**
* Wraps the search page repository.
*
* @return \Drupal\search\SearchPageRepositoryInterface
* A search page repository object.
*/
protected function searchPageRepository() {
return \Drupal::service('search.search_page_repository');
}
/**
* Wraps the search plugin manager.
*
* @return \Drupal\Component\Plugin\PluginManagerInterface
* A search plugin manager object.
*/
protected function searchPluginManager() {
return \Drupal::service('plugin.manager.search');
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace Drupal\search\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides the search reindex confirmation form.
*
* @internal
*/
class ReindexConfirm extends ConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'search_reindex_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to re-index the site?');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t("This will re-index content in the search indexes of all active search pages. Searching will continue to work, but new content won't be indexed until all existing content has been re-indexed. This action cannot be undone.");
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Re-index site');
}
/**
* {@inheritdoc}
*/
public function getCancelText() {
return $this->t('Cancel');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.search_page.collection');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form['confirm']) {
// Ask each active search page to mark itself for re-index.
$search_page_repository = \Drupal::service('search.search_page_repository');
foreach ($search_page_repository->getIndexableSearchPages() as $entity) {
$entity->getPlugin()->markForReindex();
}
$this->messenger()->addStatus($this->t('All search indexes will be rebuilt.'));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
}

View file

@ -0,0 +1,126 @@
<?php
namespace Drupal\search\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\search\SearchPageRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Builds the search form for the search block.
*
* @internal
*/
class SearchBlockForm extends FormBase {
/**
* The search page repository.
*
* @var \Drupal\search\SearchPageRepositoryInterface
*/
protected $searchPageRepository;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new SearchBlockForm.
*
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
* The search page repository.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(SearchPageRepositoryInterface $search_page_repository, ConfigFactoryInterface $config_factory, RendererInterface $renderer) {
$this->searchPageRepository = $search_page_repository;
$this->configFactory = $config_factory;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('search.search_page_repository'),
$container->get('config.factory'),
$container->get('renderer')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'search_block_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Set up the form to submit using GET to the correct search page.
$entity_id = $this->searchPageRepository->getDefaultSearchPage();
// SearchPageRepository::getDefaultSearchPage() depends on search.settings.
// The dependency needs to be added before the conditional return, otherwise
// the block would get cached without the necessary cacheablity metadata in
// case there is no default search page and would not be invalidated if that
// changes.
$this->renderer->addCacheableDependency($form, $this->configFactory->get('search.settings'));
if (!$entity_id) {
$form['message'] = [
'#markup' => $this->t('Search is currently disabled'),
];
return $form;
}
$route = 'search.view_' . $entity_id;
$form['#action'] = $this->url($route);
$form['#method'] = 'get';
$form['keys'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#title_display' => 'invisible',
'#size' => 15,
'#default_value' => '',
'#attributes' => ['title' => $this->t('Enter the terms you wish to search for.')],
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Search'),
// Prevent op from showing up in the query string.
'#name' => '',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// This form submits to the search page, so processing happens there.
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\search\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form for adding a search page.
*
* @internal
*/
class SearchPageAddForm extends SearchPageFormBase {
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $search_plugin_id = NULL) {
$this->entity->setPlugin($search_plugin_id);
$definition = $this->entity->getPlugin()->getPluginDefinition();
$this->entity->set('label', $definition['title']);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save');
return $actions;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
// If there is no default search page, make the added search the default.
if (!$this->searchPageRepository->getDefaultSearchPage()) {
$this->searchPageRepository->setDefaultSearchPage($this->entity);
}
parent::save($form, $form_state);
$this->messenger()->addStatus($this->t('The %label search page has been added.', ['%label' => $this->entity->label()]));
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\search\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form for editing a search page.
*
* @internal
*/
class SearchPageEditForm extends SearchPageFormBase {
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save search page');
return $actions;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
parent::save($form, $form_state);
$this->messenger()->addStatus($this->t('The %label search page has been updated.', ['%label' => $this->entity->label()]));
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\search\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\search\SearchPageInterface;
/**
* Provides a search form for site wide search.
*
* Search plugins can define method searchFormAlter() to alter the form. If they
* have additional or substitute fields, they will need to override the form
* submit, making sure to redirect with a GET parameter of 'keys' included, to
* trigger the search being processed by the controller, and adding in any
* additional query parameters they need to execute search.
*
* @internal
*/
class SearchPageForm extends FormBase {
/**
* The search page entity.
*
* @var \Drupal\search\SearchPageInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'search_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, SearchPageInterface $search_page = NULL) {
$this->entity = $search_page;
$plugin = $this->entity->getPlugin();
$form_state->set('search_page_id', $this->entity->id());
$form['basic'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['container-inline'],
],
];
$form['basic']['keys'] = [
'#type' => 'search',
'#title' => $this->t('Enter your keywords'),
'#default_value' => $plugin->getKeywords(),
'#size' => 30,
'#maxlength' => 255,
];
// processed_keys is used to coordinate keyword passing between other forms
// that hook into the basic search form.
$form['basic']['processed_keys'] = [
'#type' => 'value',
'#value' => '',
];
$form['basic']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Search'),
];
$form['help_link'] = [
'#type' => 'link',
'#url' => new Url('search.help_' . $this->entity->id()),
'#title' => $this->t('Search help'),
'#options' => ['attributes' => ['class' => 'search-help-link']],
];
// Allow the plugin to add to or alter the search form.
$plugin->searchFormAlter($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Redirect to the search page with keywords in the GET parameters.
// Plugins with additional search parameters will need to provide their
// own form submit handler to replace this, so they can put their values
// into the GET as well. If so, make sure to put 'keys' into the GET
// parameters so that the search results generation is triggered.
$query = $this->entity->getPlugin()->buildSearchUrlQuery($form_state);
$route = 'search.view_' . $form_state->get('search_page_id');
$form_state->setRedirect(
$route,
[],
['query' => $query]
);
}
}

View file

@ -0,0 +1,169 @@
<?php
namespace Drupal\search\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\search\SearchPageRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base form for search pages.
*/
abstract class SearchPageFormBase extends EntityForm {
/**
* The entity being used by this form.
*
* @var \Drupal\search\SearchPageInterface
*/
protected $entity;
/**
* The search plugin being configured.
*
* @var \Drupal\search\Plugin\SearchInterface
*/
protected $plugin;
/**
* The search page repository.
*
* @var \Drupal\search\SearchPageRepositoryInterface
*/
protected $searchPageRepository;
/**
* Constructs a new search form.
*
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
* The search page repository.
*/
public function __construct(SearchPageRepositoryInterface $search_page_repository) {
$this->searchPageRepository = $search_page_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('search.search_page_repository')
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'search_entity_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$this->plugin = $this->entity->getPlugin();
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#description' => $this->t('The label for this search page.'),
'#default_value' => $this->entity->label(),
'#maxlength' => '255',
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $this->entity->id(),
'#disabled' => !$this->entity->isNew(),
'#maxlength' => 64,
'#machine_name' => [
'exists' => [$this, 'exists'],
],
];
$form['path'] = [
'#type' => 'textfield',
'#title' => $this->t('Path'),
'#field_prefix' => 'search/',
'#default_value' => $this->entity->getPath(),
'#maxlength' => '255',
'#required' => TRUE,
];
$form['plugin'] = [
'#type' => 'value',
'#value' => $this->entity->get('plugin'),
];
if ($this->plugin instanceof PluginFormInterface) {
$form += $this->plugin->buildConfigurationForm($form, $form_state);
}
return parent::form($form, $form_state);
}
/**
* Determines if the search page entity already exists.
*
* @param string $id
* The search configuration ID.
*
* @return bool
* TRUE if the search configuration exists, FALSE otherwise.
*/
public function exists($id) {
$entity = $this->entityTypeManager->getStorage('search_page')->getQuery()
->condition('id', $id)
->execute();
return (bool) $entity;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
// Ensure each path is unique.
$path = $this->entityTypeManager->getStorage('search_page')->getQuery()
->condition('path', $form_state->getValue('path'))
->condition('id', $form_state->getValue('id'), '<>')
->execute();
if ($path) {
$form_state->setErrorByName('path', $this->t('The search page path must be unique.'));
}
if ($this->plugin instanceof PluginFormInterface) {
$this->plugin->validateConfigurationForm($form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
if ($this->plugin instanceof PluginFormInterface) {
$this->plugin->submitConfigurationForm($form, $form_state);
}
return $this->entity;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$this->entity->save();
$form_state->setRedirectUrl($this->entity->urlInfo('collection'));
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\search\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'Search form' block.
*
* @Block(
* id = "search_form_block",
* admin_label = @Translation("Search form"),
* category = @Translation("Forms")
* )
*/
class SearchBlock extends BlockBase {
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return AccessResult::allowedIfHasPermission($account, 'search content');
}
/**
* {@inheritdoc}
*/
public function build() {
return \Drupal::formBuilder()->getForm('Drupal\search\Form\SearchBlockForm');
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\search\Plugin;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a base implementation for a configurable Search plugin.
*/
abstract class ConfigurableSearchPluginBase extends SearchPluginBase implements ConfigurableSearchPluginInterface {
/**
* The unique ID for the search page using this plugin.
*
* @var string
*/
protected $searchPageId;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->setConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [];
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration);
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
/**
* {@inheritdoc}
*/
public function setSearchPageId($search_page_id) {
$this->searchPageId = $search_page_id;
return $this;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\search\Plugin;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Provides an interface for a configurable Search plugin.
*/
interface ConfigurableSearchPluginInterface extends ConfigurablePluginInterface, PluginFormInterface, SearchInterface {
/**
* Sets the ID for the search page using this plugin.
*
* @param string $search_page_id
* The search page ID.
*
* @return static
*/
public function setSearchPageId($search_page_id);
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\search\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\search\SearchPageRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local tasks for each search page.
*/
class SearchLocalTask extends DeriverBase implements ContainerDeriverInterface {
/**
* The search page repository.
*
* @var \Drupal\search\SearchPageRepositoryInterface
*/
protected $searchPageRepository;
/**
* Constructs a new SearchLocalTask.
*
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
* The search page repository.
*/
public function __construct(SearchPageRepositoryInterface $search_page_repository) {
$this->searchPageRepository = $search_page_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('search.search_page_repository')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
if ($default = $this->searchPageRepository->getDefaultSearchPage()) {
$active_search_pages = $this->searchPageRepository->getActiveSearchPages();
foreach ($this->searchPageRepository->sortSearchPages($active_search_pages) as $entity_id => $entity) {
$this->derivatives[$entity_id] = [
'title' => $entity->label(),
'route_name' => 'search.view_' . $entity_id,
'base_route' => 'search.plugins:' . $default,
'weight' => $entity->getWeight(),
];
}
}
return $this->derivatives;
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Drupal\search\Plugin;
/**
* Defines an optional interface for SearchPlugin objects using an index.
*
* Plugins implementing this interface will have these methods invoked during
* search_cron() and via the search module administration form. Plugins not
* implementing this interface are assumed to be using their own methods for
* searching, not involving separate index tables.
*
* The user interface for managing search pages displays the indexing status for
* search pages implementing this interface. It also allows users to configure
* default settings for indexing, and refers to the "default search index". If
* your search page plugin uses its own indexing mechanism instead of the
* default search index, or overrides the default indexing settings, you should
* make this clear on the settings page or other documentation for your plugin.
*
* Multiple search pages can be created for each search plugin, so you will need
* to choose whether these search pages should share an index (in which case
* they must not use any search page-specific configuration while indexing) or
* they will have separate indexes (which will use additional server resources).
*/
interface SearchIndexingInterface {
/**
* Updates the search index for this plugin.
*
* This method is called every cron run if the plugin has been set as
* an active search module on the Search settings page
* (admin/config/search/pages). It allows your module to add items to the
* built-in search index using search_index(), or to add them to your module's
* own indexing mechanism.
*
* When implementing this method, your module should index content items that
* were modified or added since the last run. There is a time limit for cron,
* so it is advisable to limit how many items you index per run using
* config('search.settings')->get('index.cron_limit') or with your own
* setting. And since the cron run could time out and abort in the middle of
* your run, you should update any needed internal bookkeeping on when items
* have last been indexed as you go rather than waiting to the end of
* indexing.
*/
public function updateIndex();
/**
* Clears the search index for this plugin.
*
* When a request is made to clear all items from the search index related to
* this plugin, this method will be called. If this plugin uses the default
* search index, this method can call search_index_clear($type) to remove
* indexed items from the search database.
*
* @see search_index_clear()
*/
public function indexClear();
/**
* Marks the search index for reindexing for this plugin.
*
* When a request is made to mark all items from the search index related to
* this plugin for reindexing, this method will be called. If this plugin uses
* the default search index, this method can call
* search_mark_for_reindex($type) to mark the items in the search database for
* reindexing.
*
* @see search_mark_for_reindex()
*/
public function markForReindex();
/**
* Reports the status of indexing.
*
* The core search module only invokes this method on active module plugins.
* Implementing modules do not need to check whether they are active when
* calculating their return values.
*
* @return array
* An associative array with the key-value pairs:
* - remaining: The number of items left to index.
* - total: The total number of items to index.
*/
public function indexStatus();
}

View file

@ -0,0 +1,148 @@
<?php
namespace Drupal\search\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a common interface for all SearchPlugin objects.
*/
interface SearchInterface extends PluginInspectionInterface {
/**
* Sets the keywords, parameters, and attributes to be used by execute().
*
* @param string $keywords
* The keywords to use in a search.
* @param array $parameters
* Array of parameters as an associative array. This is expected to
* be the query string from the current request.
* @param array $attributes
* Array of attributes, usually from the current request object.
*
* @return \Drupal\search\Plugin\SearchInterface
* A search plugin object for chaining.
*/
public function setSearch($keywords, array $parameters, array $attributes);
/**
* Returns the currently set keywords of the plugin instance.
*
* @return string
* The keywords.
*/
public function getKeywords();
/**
* Returns the current parameters set using setSearch().
*
* @return array
* The parameters.
*/
public function getParameters();
/**
* Returns the currently set attributes (from the request).
*
* @return array
* The attributes.
*/
public function getAttributes();
/**
* Verifies if the values set via setSearch() are valid and sufficient.
*
* @return bool
* TRUE if the search settings are valid and sufficient to execute a search,
* and FALSE if not.
*/
public function isSearchExecutable();
/**
* Returns the search index type this plugin uses.
*
* @return string|null
* The type used by this search plugin in the search index, or NULL if this
* plugin does not use the search index.
*
* @see search_index()
* @see search_index_clear()
*/
public function getType();
/**
* Executes the search.
*
* @return array
* A structured list of search results.
*/
public function execute();
/**
* Executes the search and builds render arrays for the result items.
*
* @return array
* An array of render arrays of search result items (generally each item
* has '#theme' set to 'search_result'), or an empty array if there are no
* results.
*/
public function buildResults();
/**
* Provides a suggested title for a page of search results.
*
* @return string
* The translated suggested page title.
*/
public function suggestedTitle();
/**
* Returns the searching help.
*
* @return array
* Render array for the searching help.
*/
public function getHelp();
/**
* Alters the search form when being built for a given plugin.
*
* The core search module only invokes this method on active module plugins
* when building a form for them in
* \Drupal\search\Form\SearchPageForm::buildForm(). A plugin implementing this
* will also need to implement the buildSearchUrlQuery() method.
*
* @param array $form
* Nested array of form elements that comprise the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form. The arguments that
* \Drupal::formBuilder()->getForm() was originally called with are
* available in the array $form_state->getBuildInfo()['args'].
*
* @see SearchInterface::buildSearchUrlQuery()
*/
public function searchFormAlter(array &$form, FormStateInterface $form_state);
/**
* Builds the URL GET query parameters array for search.
*
* When the search form is submitted, a redirect is generated with the
* search input as GET query parameters. Plugins using the searchFormAlter()
* method to add form elements to the search form will need to override this
* method to gather the form input and add it to the GET query parameters.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state, with submitted form information.
*
* @return array
* An array of GET query parameters containing all relevant form values
* to process the search. The 'keys' element must be present in order to
* trigger generation of search results, even if it is empty or unused by
* the search plugin.
*
* @see SearchInterface::searchFormAlter()
*/
public function buildSearchUrlQuery(FormStateInterface $form_state);
}

View file

@ -0,0 +1,165 @@
<?php
namespace Drupal\search\Plugin;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Component\Utility\Unicode;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a base class for plugins wishing to support search.
*/
abstract class SearchPluginBase extends PluginBase implements ContainerFactoryPluginInterface, SearchInterface, RefinableCacheableDependencyInterface {
use RefinableCacheableDependencyTrait;
/**
* The keywords to use in a search.
*
* @var string
*/
protected $keywords;
/**
* Array of parameters from the query string from the request.
*
* @var array
*/
protected $searchParameters;
/**
* Array of attributes - usually from the request object.
*
* @var array
*/
protected $searchAttributes;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function setSearch($keywords, array $parameters, array $attributes) {
$this->keywords = (string) $keywords;
$this->searchParameters = $parameters;
$this->searchAttributes = $attributes;
return $this;
}
/**
* {@inheritdoc}
*/
public function getKeywords() {
return $this->keywords;
}
/**
* {@inheritdoc}
*/
public function getParameters() {
return $this->searchParameters;
}
/**
* {@inheritdoc}
*/
public function getAttributes() {
return $this->searchAttributes;
}
/**
* {@inheritdoc}
*/
public function isSearchExecutable() {
// Default implementation suitable for plugins that only use keywords.
return !empty($this->keywords);
}
/**
* {@inheritdoc}
*/
public function getType() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function buildResults() {
$results = $this->execute();
$built = [];
foreach ($results as $result) {
$built[] = [
'#theme' => 'search_result',
'#result' => $result,
'#plugin_id' => $this->getPluginId(),
];
}
return $built;
}
/**
* {@inheritdoc}
*/
public function searchFormAlter(array &$form, FormStateInterface $form_state) {
// Empty default implementation.
}
/**
* {@inheritdoc}
*/
public function suggestedTitle() {
// If the user entered a search string, truncate it and append it to the
// title.
if (!empty($this->keywords)) {
return $this->t('Search for @keywords', ['@keywords' => Unicode::truncate($this->keywords, 60, TRUE, TRUE)]);
}
// Use the default 'Search' title.
return $this->t('Search');
}
/**
* {@inheritdoc}
*/
public function buildSearchUrlQuery(FormStateInterface $form_state) {
// Grab the keywords entered in the form and put them as 'keys' in the GET.
$keys = trim($form_state->getValue('keys'));
$query = ['keys' => $keys];
return $query;
}
/**
* {@inheritdoc}
*/
public function getHelp() {
// This default search help is appropriate for plugins like NodeSearch
// that use the SearchQuery class.
$help = [
'list' => [
'#theme' => 'item_list',
'#items' => [
$this->t('Search looks for exact, case-insensitive keywords; keywords shorter than a minimum length are ignored.'),
$this->t('Use upper-case OR to get more results. Example: cat OR dog (content contains either "cat" or "dog").'),
$this->t('You can use upper-case AND to require all words, but this is the same as the default behavior. Example: cat AND dog (same as cat dog, content must contain both "cat" and "dog").'),
$this->t('Use quotes to search for a phrase. Example: "the cat eats mice".'),
$this->t('You can precede keywords by - to exclude them; you must still have at least one "positive" keyword. Example: cat -dog (content must contain cat and cannot contain dog).'),
],
],
];
return $help;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\search\Plugin;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Provides a container for lazily loading search plugins.
*/
class SearchPluginCollection extends DefaultSingleLazyPluginCollection {
/**
* The unique ID for the search page using this plugin collection.
*
* @var string
*/
protected $searchPageId;
/**
* Constructs a new SearchPluginCollection.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The manager to be used for instantiating plugins.
* @param string $instance_id
* The ID of the plugin instance.
* @param array $configuration
* An array of configuration.
* @param string $search_page_id
* The unique ID of the search page using this plugin.
*/
public function __construct(PluginManagerInterface $manager, $instance_id, array $configuration, $search_page_id) {
parent::__construct($manager, $instance_id, $configuration);
$this->searchPageId = $search_page_id;
}
/**
* {@inheritdoc}
*
* @return \Drupal\search\Plugin\SearchInterface
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
/**
* {@inheritdoc}
*/
protected function initializePlugin($instance_id) {
parent::initializePlugin($instance_id);
$plugin_instance = $this->pluginInstances[$instance_id];
if ($plugin_instance instanceof ConfigurableSearchPluginInterface) {
$plugin_instance->setSearchPageId($this->searchPageId);
}
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\search\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity:search_page"
* )
*/
class EntitySearchPage extends EntityConfigBase {
/**
* Updates the entity with the contents of a row.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The search page entity.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
$entity->setPlugin($row->getDestinationProperty('plugin'));
$entity->getPlugin()->setConfiguration($row->getDestinationProperty('configuration'));
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\search\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* Generate configuration rankings.
*
* @MigrateProcessPlugin(
* id = "search_configuration_rankings"
* )
*/
class SearchConfigurationRankings extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Generate the configuration rankings.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$return = [];
foreach ($row->getSource() as $name => $rank) {
if (substr($name, 0, 10) == 'node_rank_' && is_numeric($rank)) {
$return[substr($name, 10)] = $rank;
}
}
return $return;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\search\Plugin\migrate\process\d6;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* Generate configuration rankings.
*
* @MigrateProcessPlugin(
* id = "d6_search_configuration_rankings"
* )
*/
class SearchConfigurationRankings extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Generate the configuration rankings.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$return = [];
foreach ($row->getSource() as $name => $rank) {
if (substr($name, 0, 10) == 'node_rank_' && $rank) {
$return[substr($name, 10)] = $rank;
}
}
return $return;
}
}

View file

@ -0,0 +1,132 @@
<?php
namespace Drupal\search\Plugin\views\argument;
use Drupal\Core\Database\Query\Condition;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
/**
* Argument handler for search keywords.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("search")
*/
class Search extends ArgumentPluginBase {
/**
* A search query to use for parsing search keywords.
*
* @var \Drupal\search\ViewsSearchQuery
*/
protected $searchQuery = NULL;
/**
* The search type name (value of {search_index}.type in the database).
*
* @var string
*/
protected $searchType;
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
$this->searchType = $this->definition['search_type'];
}
/**
* Sets up and parses the search query.
*
* @param string $input
* The search keywords entered by the user.
*/
protected function queryParseSearchExpression($input) {
if (!isset($this->searchQuery)) {
$this->searchQuery = db_select('search_index', 'i', ['target' => 'replica'])->extend('Drupal\search\ViewsSearchQuery');
$this->searchQuery->searchExpression($input, $this->searchType);
$this->searchQuery->publicParseSearchExpression();
}
}
/**
* {@inheritdoc}
*/
public function query($group_by = FALSE) {
$required = FALSE;
$this->queryParseSearchExpression($this->argument);
if (!isset($this->searchQuery)) {
$required = TRUE;
}
else {
$words = $this->searchQuery->words();
if (empty($words)) {
$required = TRUE;
}
}
if ($required) {
if ($this->operator == 'required') {
$this->query->addWhere(0, 'FALSE');
}
}
else {
$search_index = $this->ensureMyTable();
$search_condition = new Condition('AND');
// Create a new join to relate the 'search_total' table to our current 'search_index' table.
$definition = [
'table' => 'search_total',
'field' => 'word',
'left_table' => $search_index,
'left_field' => 'word',
];
$join = Views::pluginManager('join')->createInstance('standard', $definition);
$search_total = $this->query->addRelationship('search_total', $join, $search_index);
// Add the search score field to the query.
$this->search_score = $this->query->addField('', "$search_index.score * $search_total.count", 'score', ['function' => 'sum']);
// Add the conditions set up by the search query to the views query.
$search_condition->condition("$search_index.type", $this->searchType);
$search_dataset = $this->query->addTable('node_search_dataset');
$conditions = $this->searchQuery->conditions();
$condition_conditions =& $conditions->conditions();
foreach ($condition_conditions as $key => &$condition) {
// Make sure we just look at real conditions.
if (is_numeric($key)) {
// Replace the conditions with the table alias of views.
$this->searchQuery->conditionReplaceString('d.', "$search_dataset.", $condition);
}
}
$search_conditions =& $search_condition->conditions();
$search_conditions = array_merge($search_conditions, $condition_conditions);
// Add the keyword conditions, as is done in
// SearchQuery::prepareAndNormalize(), but simplified because we are
// only concerned with relevance ranking so we do not need to normalize.
$or = new Condition('OR');
foreach ($words as $word) {
$or->condition("$search_index.word", $word);
}
$search_condition->condition($or);
// Add the GROUP BY and HAVING expressions to the query.
$this->query->addWhere(0, $search_condition);
$this->query->addGroupBy("$search_index.sid");
$matches = $this->searchQuery->matches();
$placeholder = $this->placeholder();
$this->query->addHavingExpression(0, "COUNT(*) >= $placeholder", [$placeholder => $matches]);
}
// Set to NULL to prevent PDO exception when views object is cached
// and to clear out memory.
$this->searchQuery = NULL;
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\search\Plugin\views\field;
use Drupal\views\Plugin\views\field\NumericField;
use Drupal\views\ResultRow;
/**
* Field handler for search score.
*
* @ingroup views_field_handlers
*
* @ViewsField("search_score")
*/
class Score extends NumericField {
/**
* {@inheritdoc}
*/
public function query() {
// Check to see if the search filter added 'score' to the table.
// Our filter stores it as $handler->search_score -- and we also
// need to check its relationship to make sure that we're using the same
// one or obviously this won't work.
foreach ($this->view->filter as $handler) {
if (isset($handler->search_score) && ($handler->relationship == $this->relationship)) {
$this->field_alias = $handler->search_score;
$this->tableAlias = $handler->tableAlias;
return;
}
}
// Hide this field if no search filter is in place.
$this->options['exclude'] = TRUE;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
// Only render if we exist.
if (isset($this->tableAlias)) {
return parent::render($values);
}
}
}

View file

@ -0,0 +1,208 @@
<?php
namespace Drupal\search\Plugin\views\filter;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
/**
* Filter handler for search keywords.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("search_keywords")
*/
class Search extends FilterPluginBase {
/**
* This filter is always considered multiple-valued.
*
* @var bool
*/
protected $alwaysMultiple = TRUE;
/**
* A search query to use for parsing search keywords.
*
* @var \Drupal\search\ViewsSearchQuery
*/
protected $searchQuery = NULL;
/**
* TRUE if the search query has been parsed.
*
* @var bool
*/
protected $parsed = FALSE;
/**
* The search type name (value of {search_index}.type in the database).
*
* @var string
*/
protected $searchType;
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
$this->searchType = $this->definition['search_type'];
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['operator']['default'] = 'optional';
return $options;
}
/**
* {@inheritdoc}
*/
protected function operatorForm(&$form, FormStateInterface $form_state) {
$form['operator'] = [
'#type' => 'radios',
'#title' => $this->t('On empty input'),
'#default_value' => $this->operator,
'#options' => [
'optional' => $this->t('Show All'),
'required' => $this->t('Show None'),
],
];
}
/**
* {@inheritdoc}
*/
protected function valueForm(&$form, FormStateInterface $form_state) {
$form['value'] = [
'#type' => 'textfield',
'#size' => 15,
'#default_value' => $this->value,
'#attributes' => ['title' => $this->t('Search keywords')],
'#title' => !$form_state->get('exposed') ? $this->t('Keywords') : '',
];
}
/**
* {@inheritdoc}
*/
public function validateExposed(&$form, FormStateInterface $form_state) {
if (!isset($this->options['expose']['identifier'])) {
return;
}
$key = $this->options['expose']['identifier'];
if (!$form_state->isValueEmpty($key)) {
$this->queryParseSearchExpression($form_state->getValue($key));
if (count($this->searchQuery->words()) == 0) {
$form_state->setErrorByName($key, $this->formatPlural(\Drupal::config('search.settings')->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
}
}
}
/**
* Sets up and parses the search query.
*
* @param string $input
* The search keywords entered by the user.
*/
protected function queryParseSearchExpression($input) {
if (!isset($this->searchQuery)) {
$this->parsed = TRUE;
$this->searchQuery = db_select('search_index', 'i', ['target' => 'replica'])->extend('Drupal\search\ViewsSearchQuery');
$this->searchQuery->searchExpression($input, $this->searchType);
$this->searchQuery->publicParseSearchExpression();
}
}
/**
* {@inheritdoc}
*/
public function query() {
// Since attachment views don't validate the exposed input, parse the search
// expression if required.
if (!$this->parsed) {
$this->queryParseSearchExpression($this->value);
}
$required = FALSE;
if (!isset($this->searchQuery)) {
$required = TRUE;
}
else {
$words = $this->searchQuery->words();
if (empty($words)) {
$required = TRUE;
}
}
if ($required) {
if ($this->operator == 'required') {
$this->query->addWhere($this->options['group'], 'FALSE');
}
}
else {
$search_index = $this->ensureMyTable();
$search_condition = new Condition('AND');
// Create a new join to relate the 'search_total' table to our current
// 'search_index' table.
$definition = [
'table' => 'search_total',
'field' => 'word',
'left_table' => $search_index,
'left_field' => 'word',
];
$join = Views::pluginManager('join')->createInstance('standard', $definition);
$search_total = $this->query->addRelationship('search_total', $join, $search_index);
// Add the search score field to the query.
$this->search_score = $this->query->addField('', "$search_index.score * $search_total.count", 'score', ['function' => 'sum']);
// Add the conditions set up by the search query to the views query.
$search_condition->condition("$search_index.type", $this->searchType);
$search_dataset = $this->query->addTable('node_search_dataset');
$conditions = $this->searchQuery->conditions();
$condition_conditions =& $conditions->conditions();
foreach ($condition_conditions as $key => &$condition) {
// Make sure we just look at real conditions.
if (is_numeric($key)) {
// Replace the conditions with the table alias of views.
$this->searchQuery->conditionReplaceString('d.', "$search_dataset.", $condition);
}
}
$search_conditions =& $search_condition->conditions();
$search_conditions = array_merge($search_conditions, $condition_conditions);
// Add the keyword conditions, as is done in
// SearchQuery::prepareAndNormalize(), but simplified because we are
// only concerned with relevance ranking so we do not need to normalize.
$or = new Condition('OR');
foreach ($words as $word) {
$or->condition("$search_index.word", $word);
}
$search_condition->condition($or);
$this->query->addWhere($this->options['group'], $search_condition);
// Add the GROUP BY and HAVING expressions to the query.
$this->query->addGroupBy("$search_index.sid");
$matches = $this->searchQuery->matches();
$placeholder = $this->placeholder();
$this->query->addHavingExpression($this->options['group'], "COUNT(*) >= $placeholder", [$placeholder => $matches]);
}
// Set to NULL to prevent PDO exception when views object is cached.
$this->searchQuery = NULL;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\search\Plugin\views\row;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\row\RowPluginBase;
/**
* Row handler plugin for displaying search results.
*
* @ViewsRow(
* id = "search_view",
* title = @Translation("Search results"),
* help = @Translation("Provides a row plugin to display search results.")
* )
*/
class SearchRow extends RowPluginBase {
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['score'] = ['default' => TRUE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['score'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display score'),
'#default_value' => $this->options['score'],
];
}
/**
* {@inheritdoc}
*/
public function render($row) {
return [
'#theme' => $this->themeFunctions(),
'#view' => $this->view,
'#options' => $this->options,
'#row' => $row,
];
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Drupal\search\Plugin\views\sort;
use Drupal\views\Plugin\views\sort\SortPluginBase;
/**
* Sort handler for sorting by search score.
*
* @ingroup views_sort_handlers
*
* @ViewsSort("search_score")
*/
class Score extends SortPluginBase {
/**
* {@inheritdoc}
*/
public function query() {
// Check to see if the search filter/argument added 'score' to the table.
// Our filter stores it as $handler->search_score -- and we also
// need to check its relationship to make sure that we're using the same
// one or obviously this won't work.
foreach (['filter', 'argument'] as $type) {
foreach ($this->view->{$type} as $handler) {
if (isset($handler->search_score) && $handler->relationship == $this->relationship) {
$this->query->addOrderBy(NULL, NULL, $this->options['order'], $handler->search_score);
$this->tableAlias = $handler->tableAlias;
return;
}
}
}
// Do nothing if there is no filter/argument in place. There is no way
// to sort on scores.
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace Drupal\search\Routing;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\search\SearchPageRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
/**
* Provides dynamic routes for search.
*/
class SearchPageRoutes implements ContainerInjectionInterface {
/**
* The search page repository.
*
* @var \Drupal\search\SearchPageRepositoryInterface
*/
protected $searchPageRepository;
/**
* Constructs a new search route subscriber.
*
* @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
* The search page repository.
*/
public function __construct(SearchPageRepositoryInterface $search_page_repository) {
$this->searchPageRepository = $search_page_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('search.search_page_repository')
);
}
/**
* Returns an array of route objects.
*
* @return \Symfony\Component\Routing\Route[]
* An array of route objects.
*/
public function routes() {
$routes = [];
// @todo Decide if /search should continue to redirect to /search/$default,
// or just perform the appropriate search.
if ($default_page = $this->searchPageRepository->getDefaultSearchPage()) {
$routes['search.view'] = new Route(
'/search',
[
'_controller' => 'Drupal\search\Controller\SearchController::redirectSearchPage',
'_title' => 'Search',
'entity' => $default_page,
],
[
'_entity_access' => 'entity.view',
'_permission' => 'search content',
],
[
'parameters' => [
'entity' => [
'type' => 'entity:search_page',
],
],
]
);
}
$active_pages = $this->searchPageRepository->getActiveSearchPages();
foreach ($active_pages as $entity_id => $entity) {
$routes["search.view_$entity_id"] = new Route(
'/search/' . $entity->getPath(),
[
'_controller' => 'Drupal\search\Controller\SearchController::view',
'_title' => 'Search',
'entity' => $entity_id,
],
[
'_entity_access' => 'entity.view',
'_permission' => 'search content',
],
[
'parameters' => [
'entity' => [
'type' => 'entity:search_page',
],
],
]
);
$routes["search.help_$entity_id"] = new Route(
'/search/' . $entity->getPath() . '/help',
[
'_controller' => 'Drupal\search\Controller\SearchController::searchHelp',
'_title' => 'Search help',
'entity' => $entity_id,
],
[
'_entity_access' => 'entity.view',
'_permission' => 'search content',
],
[
'parameters' => [
'entity' => [
'type' => 'entity:search_page',
],
],
]
);
}
return $routes;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\search;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the search page entity type.
*
* @see \Drupal\search\Entity\SearchPage
*/
class SearchPageAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var $entity \Drupal\search\SearchPageInterface */
if (in_array($operation, ['delete', 'disable'])) {
if ($entity->isDefaultSearch()) {
return AccessResult::forbidden()->addCacheableDependency($entity);
}
else {
return parent::checkAccess($entity, $operation, $account)->addCacheableDependency($entity);
}
}
if ($operation == 'view') {
if (!$entity->status()) {
return AccessResult::forbidden()->addCacheableDependency($entity);
}
$plugin = $entity->getPlugin();
if ($plugin instanceof AccessibleInterface) {
return $plugin->access($operation, $account, TRUE)->addCacheableDependency($entity);
}
return AccessResult::allowed()->addCacheableDependency($entity);
}
return parent::checkAccess($entity, $operation, $account);
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\search;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a search page entity.
*/
interface SearchPageInterface extends ConfigEntityInterface {
/**
* Returns the search plugin.
*
* @return \Drupal\search\Plugin\SearchInterface
* The search plugin used by this search page entity.
*/
public function getPlugin();
/**
* Sets the search plugin.
*
* @param string $plugin_id
* The search plugin ID.
*/
public function setPlugin($plugin_id);
/**
* Determines if this search page entity is currently the default search.
*
* @return bool
* TRUE if this search page entity is the default search, FALSE otherwise.
*/
public function isDefaultSearch();
/**
* Determines if this search page entity is indexable.
*
* @return bool
* TRUE if this search page entity is indexable, FALSE otherwise.
*/
public function isIndexable();
/**
* Returns the path for the search.
*
* @return string
* The part of the path for this search page that comes after 'search'.
*/
public function getPath();
/**
* Returns the weight for the page.
*
* @return int
* The page weight.
*/
public function getWeight();
}

View file

@ -0,0 +1,388 @@
<?php
namespace Drupal\search;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\ConfigFormBaseTrait;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of search page entities.
*
* @see \Drupal\search\Entity\SearchPage
*/
class SearchPageListBuilder extends DraggableListBuilder implements FormInterface {
use ConfigFormBaseTrait;
/**
* The entities being listed.
*
* @var \Drupal\search\SearchPageInterface[]
*/
protected $entities = [];
/**
* Stores the configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The search manager.
*
* @var \Drupal\search\SearchPluginManager
*/
protected $searchManager;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new SearchPageListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\search\SearchPluginManager $search_manager
* The search plugin manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, SearchPluginManager $search_manager, ConfigFactoryInterface $config_factory, MessengerInterface $messenger) {
parent::__construct($entity_type, $storage);
$this->configFactory = $config_factory;
$this->searchManager = $search_manager;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager')->getStorage($entity_type->id()),
$container->get('plugin.manager.search'),
$container->get('config.factory'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'search_admin_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['search.settings'];
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = [
'data' => $this->t('Label'),
];
$header['url'] = [
'data' => $this->t('URL'),
'class' => [RESPONSIVE_PRIORITY_LOW],
];
$header['plugin'] = [
'data' => $this->t('Type'),
'class' => [RESPONSIVE_PRIORITY_LOW],
];
$header['status'] = [
'data' => $this->t('Status'),
];
$header['progress'] = [
'data' => $this->t('Indexing progress'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
];
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var $entity \Drupal\search\SearchPageInterface */
$row['label'] = $entity->label();
$row['url']['#markup'] = 'search/' . $entity->getPath();
// If the search page is active, link to it.
if ($entity->status()) {
$row['url'] = [
'#type' => 'link',
'#title' => $row['url'],
'#url' => Url::fromRoute('search.view_' . $entity->id()),
];
}
$definition = $entity->getPlugin()->getPluginDefinition();
$row['plugin']['#markup'] = $definition['title'];
if ($entity->isDefaultSearch()) {
$status = $this->t('Default');
}
elseif ($entity->status()) {
$status = $this->t('Enabled');
}
else {
$status = $this->t('Disabled');
}
$row['status']['#markup'] = $status;
if ($entity->isIndexable()) {
$status = $entity->getPlugin()->indexStatus();
$row['progress']['#markup'] = $this->t('%num_indexed of %num_total indexed', [
'%num_indexed' => $status['total'] - $status['remaining'],
'%num_total' => $status['total'],
]);
}
else {
$row['progress']['#markup'] = $this->t('Does not use index');
}
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$search_settings = $this->config('search.settings');
// Collect some stats.
$remaining = 0;
$total = 0;
foreach ($this->entities as $entity) {
if ($entity->isIndexable() && $status = $entity->getPlugin()->indexStatus()) {
$remaining += $status['remaining'];
$total += $status['total'];
}
}
$this->moduleHandler->loadAllIncludes('admin.inc');
$count = $this->formatPlural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.');
$done = $total - $remaining;
// Use floor() to calculate the percentage, so if it is not quite 100%, it
// will show as 99%, to indicate "almost done".
$percentage = $total > 0 ? floor(100 * $done / $total) : 100;
$percentage .= '%';
$status = '<p><strong>' . $this->t('%percentage of the site has been indexed.', ['%percentage' => $percentage]) . ' ' . $count . '</strong></p>';
$form['status'] = [
'#type' => 'details',
'#title' => $this->t('Indexing progress'),
'#open' => TRUE,
'#description' => $this->t('Only items in the index will appear in search results. To build and maintain the index, a correctly configured <a href=":cron">cron maintenance task</a> is required.', [':cron' => \Drupal::url('system.cron_settings')]),
];
$form['status']['status'] = ['#markup' => $status];
$form['status']['wipe'] = [
'#type' => 'submit',
'#value' => $this->t('Re-index site'),
'#submit' => ['::searchAdminReindexSubmit'],
];
$items = [10, 20, 50, 100, 200, 500];
$items = array_combine($items, $items);
// Indexing throttle:
$form['indexing_throttle'] = [
'#type' => 'details',
'#title' => $this->t('Indexing throttle'),
'#open' => TRUE,
];
$form['indexing_throttle']['cron_limit'] = [
'#type' => 'select',
'#title' => $this->t('Number of items to index per cron run'),
'#default_value' => $search_settings->get('index.cron_limit'),
'#options' => $items,
'#description' => $this->t('The maximum number of items indexed in each run of the <a href=":cron">cron maintenance task</a>. If necessary, reduce the number of items to prevent timeouts and memory errors while indexing. Some search page types may have their own setting for this.', [':cron' => \Drupal::url('system.cron_settings')]),
];
// Indexing settings:
$form['indexing_settings'] = [
'#type' => 'details',
'#title' => $this->t('Default indexing settings'),
'#open' => TRUE,
];
$form['indexing_settings']['info'] = [
'#markup' => $this->t("<p>Search pages that use an index may use the default index provided by the Search module, or they may use a different indexing mechanism. These settings are for the default index. <em>Changing these settings will cause the default search index to be rebuilt to reflect the new settings. Searching will continue to work, based on the existing index, but new content won't be indexed until all existing content has been re-indexed.</em></p><p><em>The default settings should be appropriate for the majority of sites.</em></p>"),
];
$form['indexing_settings']['minimum_word_size'] = [
'#type' => 'number',
'#title' => $this->t('Minimum word length to index'),
'#default_value' => $search_settings->get('index.minimum_word_size'),
'#min' => 1,
'#max' => 1000,
'#description' => $this->t('The minimum character length for a word to be added to the index. Searches must include a keyword of at least this length.'),
];
$form['indexing_settings']['overlap_cjk'] = [
'#type' => 'checkbox',
'#title' => $this->t('Simple CJK handling'),
'#default_value' => $search_settings->get('index.overlap_cjk'),
'#description' => $this->t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.'),
];
// Indexing settings:
$form['logging'] = [
'#type' => 'details',
'#title' => $this->t('Logging'),
'#open' => TRUE,
];
$form['logging']['logging'] = [
'#type' => 'checkbox',
'#title' => $this->t('Log searches'),
'#default_value' => $search_settings->get('logging'),
'#description' => $this->t('If checked, all searches will be logged. Uncheck to skip logging. Logging may affect performance.'),
];
$form['search_pages'] = [
'#type' => 'details',
'#title' => $this->t('Search pages'),
'#open' => TRUE,
];
$form['search_pages']['add_page'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['container-inline'],
],
];
// In order to prevent validation errors for the parent form, this cannot be
// required, see self::validateAddSearchPage().
$form['search_pages']['add_page']['search_type'] = [
'#type' => 'select',
'#title' => $this->t('Search page type'),
'#empty_option' => $this->t('- Choose page type -'),
'#options' => array_map(function ($definition) {
return $definition['title'];
}, $this->searchManager->getDefinitions()),
];
$form['search_pages']['add_page']['add_search_submit'] = [
'#type' => 'submit',
'#value' => $this->t('Add search page'),
'#validate' => ['::validateAddSearchPage'],
'#submit' => ['::submitAddSearchPage'],
'#limit_validation_errors' => [['search_type']],
];
// Move the listing into the search_pages element.
$form['search_pages'][$this->entitiesKey] = $form[$this->entitiesKey];
$form['search_pages'][$this->entitiesKey]['#empty'] = $this->t('No search pages have been configured.');
unset($form[$this->entitiesKey]);
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save configuration'),
'#button_type' => 'primary',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
/** @var $entity \Drupal\search\SearchPageInterface */
$operations = parent::getDefaultOperations($entity);
// Prevent the default search from being disabled or deleted.
if ($entity->isDefaultSearch()) {
unset($operations['disable'], $operations['delete']);
}
else {
$operations['default'] = [
'title' => $this->t('Set as default'),
'url' => Url::fromRoute('entity.search_page.set_default', [
'search_page' => $entity->id(),
]),
'weight' => 50,
];
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$search_settings = $this->config('search.settings');
// If these settings change, the default index needs to be rebuilt.
if (($search_settings->get('index.minimum_word_size') != $form_state->getValue('minimum_word_size')) || ($search_settings->get('index.overlap_cjk') != $form_state->getValue('overlap_cjk'))) {
$search_settings->set('index.minimum_word_size', $form_state->getValue('minimum_word_size'));
$search_settings->set('index.overlap_cjk', $form_state->getValue('overlap_cjk'));
// Specifically mark items in the default index for reindexing, since
// these settings are used in the search_index() function.
$this->messenger->addStatus($this->t('The default search index will be rebuilt.'));
search_mark_for_reindex();
}
$search_settings
->set('index.cron_limit', $form_state->getValue('cron_limit'))
->set('logging', $form_state->getValue('logging'))
->save();
$this->messenger->addStatus($this->t('The configuration options have been saved.'));
}
/**
* Form submission handler for the reindex button on the search admin settings
* form.
*/
public function searchAdminReindexSubmit(array &$form, FormStateInterface $form_state) {
// Send the user to the confirmation page.
$form_state->setRedirect('search.reindex_confirm');
}
/**
* Form validation handler for adding a new search page.
*/
public function validateAddSearchPage(array &$form, FormStateInterface $form_state) {
if ($form_state->isValueEmpty('search_type')) {
$form_state->setErrorByName('search_type', $this->t('You must select the new search page type.'));
}
}
/**
* Form submission handler for adding a new search page.
*/
public function submitAddSearchPage(array &$form, FormStateInterface $form_state) {
$form_state->setRedirect(
'search.add_type',
['search_plugin_id' => $form_state->getValue('search_type')]
);
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Drupal\search;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
/**
* Provides a repository for Search Page config entities.
*/
class SearchPageRepository implements SearchPageRepositoryInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The search page storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* Constructs a new SearchPageRepository.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityManagerInterface $entity_manager) {
$this->configFactory = $config_factory;
$this->storage = $entity_manager->getStorage('search_page');
}
/**
* {@inheritdoc}
*/
public function getActiveSearchPages() {
$ids = $this->getQuery()
->condition('status', TRUE)
->execute();
return $this->storage->loadMultiple($ids);
}
/**
* {@inheritdoc}
*/
public function isSearchActive() {
return (bool) $this->getQuery()
->condition('status', TRUE)
->range(0, 1)
->execute();
}
/**
* {@inheritdoc}
*/
public function getIndexableSearchPages() {
return array_filter($this->getActiveSearchPages(), function (SearchPageInterface $search) {
return $search->isIndexable();
});
}
/**
* {@inheritdoc}
*/
public function getDefaultSearchPage() {
// Find all active search pages (without loading them).
$search_pages = $this->getQuery()
->condition('status', TRUE)
->execute();
// If the default page is active, return it.
$default = $this->configFactory->get('search.settings')->get('default_page');
if (isset($search_pages[$default])) {
return $default;
}
// Otherwise, use the first active search page.
return is_array($search_pages) ? reset($search_pages) : FALSE;
}
/**
* {@inheritdoc}
*/
public function clearDefaultSearchPage() {
$this->configFactory->getEditable('search.settings')->clear('default_page')->save();
}
/**
* {@inheritdoc}
*/
public function setDefaultSearchPage(SearchPageInterface $search_page) {
$this->configFactory->getEditable('search.settings')->set('default_page', $search_page->id())->save();
$search_page->enable()->save();
}
/**
* {@inheritdoc}
*/
public function sortSearchPages($search_pages) {
$entity_type = $this->storage->getEntityType();
uasort($search_pages, [$entity_type->getClass(), 'sort']);
return $search_pages;
}
/**
* Returns an entity query instance.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* The query instance.
*/
protected function getQuery() {
return $this->storage->getQuery();
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Drupal\search;
/**
* Provides the interface for a repository Search Page entities.
*/
interface SearchPageRepositoryInterface {
/**
* Returns all active search page entities.
*
* @return \Drupal\search\SearchPageInterface[]
* An array of active search page entities.
*/
public function getActiveSearchPages();
/**
* Returns whether search is active.
*
* @return bool
* TRUE if at least one search is active, FALSE otherwise.
*/
public function isSearchActive();
/**
* Returns all active, indexable search page entities.
*
* @return \Drupal\search\SearchPageInterface[]
* An array of indexable search page entities.
*/
public function getIndexableSearchPages();
/**
* Returns the default search page.
*
* @return \Drupal\search\SearchPageInterface|bool
* The search page entity, or FALSE if no pages are active.
*/
public function getDefaultSearchPage();
/**
* Sets a given search page as the default.
*
* @param \Drupal\search\SearchPageInterface $search_page
* The search page entity.
*
* @return static
*/
public function setDefaultSearchPage(SearchPageInterface $search_page);
/**
* Clears the default search page.
*/
public function clearDefaultSearchPage();
/**
* Sorts a list of search pages.
*
* @param \Drupal\search\SearchPageInterface[] $search_pages
* The unsorted list of search pages.
*
* @return \Drupal\search\SearchPageInterface[]
* The sorted list of search pages.
*/
public function sortSearchPages($search_pages);
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\search;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
/**
* SearchExecute plugin manager.
*/
class SearchPluginManager extends DefaultPluginManager {
/**
* Constructs SearchPluginManager
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Search', $namespaces, $module_handler, 'Drupal\search\Plugin\SearchInterface', 'Drupal\search\Annotation\SearchPlugin');
$this->setCacheBackend($cache_backend, 'search_plugins');
$this->alterInfo('search_plugin');
}
}

View file

@ -0,0 +1,647 @@
<?php
namespace Drupal\search;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\Query\SelectExtender;
use Drupal\Core\Database\Query\SelectInterface;
/**
* Search query extender and helper functions.
*
* Performs a query on the full-text search index for a word or words.
*
* This query is used by search plugins that use the search index (not all
* search plugins do, as some use a different searching mechanism). It
* assumes you have set up a query on the {search_index} table with alias 'i',
* and will only work if the user is searching for at least one "positive"
* keyword or phrase.
*
* For efficiency, users of this query can run the prepareAndNormalize()
* method to figure out if there are any search results, before fully setting
* up and calling execute() to execute the query. The scoring expressions are
* not needed until the execute() step. However, it's not really necessary
* to do this, because this class's execute() method does that anyway.
*
* During both the prepareAndNormalize() and execute() steps, there can be
* problems. Call getStatus() to figure out if the query is OK or not.
*
* The query object is given the tag 'search_$type' and can be further
* extended with hook_query_alter().
*/
class SearchQuery extends SelectExtender {
/**
* Indicates no positive keywords were in the search expression.
*
* Positive keywords are words that are searched for, as opposed to negative
* keywords, which are words that are excluded. To count as a keyword, a
* word must be at least
* \Drupal::config('search.settings')->get('index.minimum_word_size')
* characters.
*
* @see SearchQuery::getStatus()
*/
const NO_POSITIVE_KEYWORDS = 1;
/**
* Indicates that part of the search expression was ignored.
*
* To prevent Denial of Service attacks, only
* \Drupal::config('search.settings')->get('and_or_limit') expressions
* (positive keywords, phrases, negative keywords) are allowed; this flag
* indicates that expressions existed past that limit and they were removed.
*
* @see SearchQuery::getStatus()
*/
const EXPRESSIONS_IGNORED = 2;
/**
* Indicates that lower-case "or" was in the search expression.
*
* The word "or" in lower case was found in the search expression. This
* probably means someone was trying to do an OR search but used lower-case
* instead of upper-case.
*
* @see SearchQuery::getStatus()
*/
const LOWER_CASE_OR = 4;
/**
* Indicates that no positive keyword matches were found.
*
* @see SearchQuery::getStatus()
*/
const NO_KEYWORD_MATCHES = 8;
/**
* The keywords and advanced search options that are entered by the user.
*
* @var string
*/
protected $searchExpression;
/**
* The type of search (search type).
*
* This maps to the value of the type column in search_index, and is usually
* equal to the machine-readable name of the plugin or the search page.
*
* @var string
*/
protected $type;
/**
* Parsed-out positive and negative search keys.
*
* @var array
*/
protected $keys = ['positive' => [], 'negative' => []];
/**
* Indicates whether the query conditions are simple or complex (LIKE).
*
* @var bool
*/
protected $simple = TRUE;
/**
* Conditions that are used for exact searches.
*
* This is always used for the second step in the query, but is not part of
* the preparation step unless $this->simple is FALSE.
*
* @var DatabaseCondition
*/
protected $conditions;
/**
* Indicates how many matches for a search query are necessary.
*
* @var int
*/
protected $matches = 0;
/**
* Array of positive search words.
*
* These words have to match against {search_index}.word.
*
* @var array
*/
protected $words = [];
/**
* Multiplier to normalize the keyword score.
*
* This value is calculated by the preparation step, and is used as a
* multiplier of the word scores to make sure they are between 0 and 1.
*
* @var float
*/
protected $normalize = 0;
/**
* Indicates whether the preparation step has been executed.
*
* @var bool
*/
protected $executedPrepare = FALSE;
/**
* A bitmap of status conditions, described in getStatus().
*
* @var int
*
* @see SearchQuery::getStatus()
*/
protected $status = 0;
/**
* The word score expressions.
*
* @var array
*
* @see SearchQuery::addScore()
*/
protected $scores = [];
/**
* Arguments for the score expressions.
*
* @var array
*/
protected $scoresArguments = [];
/**
* The number of 'i.relevance' occurrences in score expressions.
*
* @var int
*/
protected $relevance_count = 0;
/**
* Multipliers for score expressions.
*
* @var array
*/
protected $multiply = [];
/**
* Sets the search query expression.
*
* @param string $expression
* A search string, which can contain keywords and options.
* @param string $type
* The search type. This maps to {search_index}.type in the database.
*
* @return $this
*/
public function searchExpression($expression, $type) {
$this->searchExpression = $expression;
$this->type = $type;
// Add query tag.
$this->addTag('search_' . $type);
// Initialize conditions and status.
$this->conditions = new Condition('AND');
$this->status = 0;
return $this;
}
/**
* Parses the search query into SQL conditions.
*
* Sets up the following variables:
* - $this->keys
* - $this->words
* - $this->conditions
* - $this->simple
* - $this->matches
*/
protected function parseSearchExpression() {
// Matches words optionally prefixed by a - sign. A word in this case is
// something between two spaces, optionally quoted.
preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' ' . $this->searchExpression, $keywords, PREG_SET_ORDER);
if (count($keywords) == 0) {
return;
}
// Classify tokens.
$in_or = FALSE;
$limit_combinations = \Drupal::config('search.settings')->get('and_or_limit');
// The first search expression does not count as AND.
$and_count = -1;
$or_count = 0;
foreach ($keywords as $match) {
if ($or_count && $and_count + $or_count >= $limit_combinations) {
// Ignore all further search expressions to prevent Denial-of-Service
// attacks using a high number of AND/OR combinations.
$this->status |= SearchQuery::EXPRESSIONS_IGNORED;
break;
}
// Strip off phrase quotes.
$phrase = FALSE;
if ($match[2]{0} == '"') {
$match[2] = substr($match[2], 1, -1);
$phrase = TRUE;
$this->simple = FALSE;
}
// Simplify keyword according to indexing rules and external
// preprocessors. Use same process as during search indexing, so it
// will match search index.
$words = search_simplify($match[2]);
// Re-explode in case simplification added more words, except when
// matching a phrase.
$words = $phrase ? [$words] : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY);
// Negative matches.
if ($match[1] == '-') {
$this->keys['negative'] = array_merge($this->keys['negative'], $words);
}
// OR operator: instead of a single keyword, we store an array of all
// OR'd keywords.
elseif ($match[2] == 'OR' && count($this->keys['positive'])) {
$last = array_pop($this->keys['positive']);
// Starting a new OR?
if (!is_array($last)) {
$last = [$last];
}
$this->keys['positive'][] = $last;
$in_or = TRUE;
$or_count++;
continue;
}
// AND operator: implied, so just ignore it.
elseif ($match[2] == 'AND' || $match[2] == 'and') {
continue;
}
// Plain keyword.
else {
if ($match[2] == 'or') {
// Lower-case "or" instead of "OR" is a warning condition.
$this->status |= SearchQuery::LOWER_CASE_OR;
}
if ($in_or) {
// Add to last element (which is an array).
$this->keys['positive'][count($this->keys['positive']) - 1] = array_merge($this->keys['positive'][count($this->keys['positive']) - 1], $words);
}
else {
$this->keys['positive'] = array_merge($this->keys['positive'], $words);
$and_count++;
}
}
$in_or = FALSE;
}
// Convert keywords into SQL statements.
$has_and = FALSE;
$has_or = FALSE;
// Positive matches.
foreach ($this->keys['positive'] as $key) {
// Group of ORed terms.
if (is_array($key) && count($key)) {
// If we had already found one OR, this is another one AND-ed with the
// first, meaning it is not a simple query.
if ($has_or) {
$this->simple = FALSE;
}
$has_or = TRUE;
$has_new_scores = FALSE;
$queryor = new Condition('OR');
foreach ($key as $or) {
list($num_new_scores) = $this->parseWord($or);
$has_new_scores |= $num_new_scores;
$queryor->condition('d.data', "% $or %", 'LIKE');
}
if (count($queryor)) {
$this->conditions->condition($queryor);
// A group of OR keywords only needs to match once.
$this->matches += ($has_new_scores > 0);
}
}
// Single ANDed term.
else {
$has_and = TRUE;
list($num_new_scores, $num_valid_words) = $this->parseWord($key);
$this->conditions->condition('d.data', "% $key %", 'LIKE');
if (!$num_valid_words) {
$this->simple = FALSE;
}
// Each AND keyword needs to match at least once.
$this->matches += $num_new_scores;
}
}
if ($has_and && $has_or) {
$this->simple = FALSE;
}
// Negative matches.
foreach ($this->keys['negative'] as $key) {
$this->conditions->condition('d.data', "% $key %", 'NOT LIKE');
$this->simple = FALSE;
}
}
/**
* Parses a word or phrase for parseQuery().
*
* Splits a phrase into words. Adds its words to $this->words, if it is not
* already there. Returns a list containing the number of new words found,
* and the total number of words in the phrase.
*/
protected function parseWord($word) {
$num_new_scores = 0;
$num_valid_words = 0;
// Determine the scorewords of this word/phrase.
$split = explode(' ', $word);
foreach ($split as $s) {
$num = is_numeric($s);
if ($num || mb_strlen($s) >= \Drupal::config('search.settings')->get('index.minimum_word_size')) {
if (!isset($this->words[$s])) {
$this->words[$s] = $s;
$num_new_scores++;
}
$num_valid_words++;
}
}
// Return matching snippet and number of added words.
return [$num_new_scores, $num_valid_words];
}
/**
* Prepares the query and calculates the normalization factor.
*
* After the query is normalized the keywords are weighted to give the results
* a relevancy score. The query is ready for execution after this.
*
* Error and warning conditions can apply. Call getStatus() after calling
* this method to retrieve them.
*
* @return bool
* TRUE if at least one keyword matched the search index; FALSE if not.
*/
public function prepareAndNormalize() {
$this->parseSearchExpression();
$this->executedPrepare = TRUE;
if (count($this->words) == 0) {
// Although the query could proceed, there is no point in joining
// with other tables and attempting to normalize if there are no
// keywords present.
$this->status |= SearchQuery::NO_POSITIVE_KEYWORDS;
return FALSE;
}
// Build the basic search query: match the entered keywords.
$or = new Condition('OR');
foreach ($this->words as $word) {
$or->condition('i.word', $word);
}
$this->condition($or);
// Add keyword normalization information to the query.
$this->join('search_total', 't', 'i.word = t.word');
$this
->condition('i.type', $this->type)
->groupBy('i.type')
->groupBy('i.sid');
// If the query is simple, we should have calculated the number of
// matching words we need to find, so impose that criterion. For non-
// simple queries, this condition could lead to incorrectly deciding not
// to continue with the full query.
if ($this->simple) {
$this->having('COUNT(*) >= :matches', [':matches' => $this->matches]);
}
// Clone the query object to calculate normalization.
$normalize_query = clone $this->query;
// For complex search queries, add the LIKE conditions; if the query is
// simple, we do not need them for normalization.
if (!$this->simple) {
$normalize_query->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type AND i.langcode = d.langcode');
if (count($this->conditions)) {
$normalize_query->condition($this->conditions);
}
}
// Calculate normalization, which is the max of all the search scores for
// positive keywords in the query. And note that the query could have other
// fields added to it by the user of this extension.
$normalize_query->addExpression('SUM(i.score * t.count)', 'calculated_score');
$result = $normalize_query
->range(0, 1)
->orderBy('calculated_score', 'DESC')
->execute()
->fetchObject();
if (isset($result->calculated_score)) {
$this->normalize = (float) $result->calculated_score;
}
if ($this->normalize) {
return TRUE;
}
// If the normalization value was zero, that indicates there were no
// matches to the supplied positive keywords.
$this->status |= SearchQuery::NO_KEYWORD_MATCHES;
return FALSE;
}
/**
* {@inheritdoc}
*/
public function preExecute(SelectInterface $query = NULL) {
if (!$this->executedPrepare) {
$this->prepareAndNormalize();
}
if (!$this->normalize) {
return FALSE;
}
return parent::preExecute($query);
}
/**
* Adds a custom score expression to the search query.
*
* Score expressions are used to order search results. If no calls to
* addScore() have taken place, a default keyword relevance score will be
* used. However, if at least one call to addScore() has taken place, the
* keyword relevance score is not automatically added.
*
* Note that you must use this method to add ordering to your searches, and
* not call orderBy() directly, when using the SearchQuery extender. This is
* because of the two-pass system the SearchQuery class uses to normalize
* scores.
*
* @param string $score
* The score expression, which should evaluate to a number between 0 and 1.
* The string 'i.relevance' in a score expression will be replaced by a
* measure of keyword relevance between 0 and 1.
* @param array $arguments
* Query arguments needed to provide values to the score expression.
* @param float $multiply
* If set, the score is multiplied with this value. However, all scores
* with multipliers are then divided by the total of all multipliers, so
* that overall, the normalization is maintained.
*
* @return $this
*/
public function addScore($score, $arguments = [], $multiply = FALSE) {
if ($multiply) {
$i = count($this->multiply);
// Modify the score expression so it is multiplied by the multiplier,
// with a divisor to renormalize. Note that the ROUND here is necessary
// for PostgreSQL and SQLite in order to ensure that the :multiply_* and
// :total_* arguments are treated as a numeric type, because the
// PostgreSQL PDO driver sometimes puts values in as strings instead of
// numbers in complex expressions like this.
$score = "(ROUND(:multiply_$i, 4)) * COALESCE(($score), 0) / (ROUND(:total_$i, 4))";
// Add an argument for the multiplier. The :total_$i argument is taken
// care of in the execute() method, which is when the total divisor is
// calculated.
$arguments[':multiply_' . $i] = $multiply;
$this->multiply[] = $multiply;
}
// Search scoring needs a way to include a keyword relevance in the score.
// For historical reasons, this is done by putting 'i.relevance' into the
// search expression. So, use string replacement to change this to a
// calculated query expression, counting the number of occurrences so
// in the execute() method we can add arguments.
while (($pos = strpos($score, 'i.relevance')) !== FALSE) {
$pieces = explode('i.relevance', $score, 2);
$score = implode('((ROUND(:normalization_' . $this->relevance_count . ', 4)) * i.score * t.count)', $pieces);
$this->relevance_count++;
}
$this->scores[] = $score;
$this->scoresArguments += $arguments;
return $this;
}
/**
* Executes the search.
*
* The complex conditions are applied to the query including score
* expressions and ordering.
*
* Error and warning conditions can apply. Call getStatus() after calling
* this method to retrieve them.
*
* @return \Drupal\Core\Database\StatementInterface|null
* A query result set containing the results of the query.
*/
public function execute() {
if (!$this->preExecute($this)) {
return NULL;
}
// Add conditions to the query.
$this->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type AND i.langcode = d.langcode');
if (count($this->conditions)) {
$this->condition($this->conditions);
}
// Add default score (keyword relevance) if there are not any defined.
if (empty($this->scores)) {
$this->addScore('i.relevance');
}
if (count($this->multiply)) {
// Re-normalize scores with multipliers by dividing by the total of all
// multipliers. The expressions were altered in addScore(), so here just
// add the arguments for the total.
$sum = array_sum($this->multiply);
for ($i = 0; $i < count($this->multiply); $i++) {
$this->scoresArguments[':total_' . $i] = $sum;
}
}
// Add arguments for the keyword relevance normalization number.
$normalization = 1.0 / $this->normalize;
for ($i = 0; $i < $this->relevance_count; $i++) {
$this->scoresArguments[':normalization_' . $i] = $normalization;
}
// Add all scores together to form a query field.
$this->addExpression('SUM(' . implode(' + ', $this->scores) . ')', 'calculated_score', $this->scoresArguments);
// If an order has not yet been set for this query, add a default order
// that sorts by the calculated sum of scores.
if (count($this->getOrderBy()) == 0) {
$this->orderBy('calculated_score', 'DESC');
}
// Add query metadata.
$this
->addMetaData('normalize', $this->normalize)
->fields('i', ['type', 'sid']);
return $this->query->execute();
}
/**
* Builds the default count query for SearchQuery.
*
* Since SearchQuery always uses GROUP BY, we can default to a subquery. We
* also add the same conditions as execute() because countQuery() is called
* first.
*/
public function countQuery() {
if (!$this->executedPrepare) {
$this->prepareAndNormalize();
}
// Clone the inner query.
$inner = clone $this->query;
// Add conditions to query.
$inner->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type');
if (count($this->conditions)) {
$inner->condition($this->conditions);
}
// Remove existing fields and expressions, they are not needed for a count
// query.
$fields =& $inner->getFields();
$fields = [];
$expressions =& $inner->getExpressions();
$expressions = [];
// Add sid as the only field and count them as a subquery.
$count = db_select($inner->fields('i', ['sid']), NULL, ['target' => 'replica']);
// Add the COUNT() expression.
$count->addExpression('COUNT(*)');
return $count;
}
/**
* Returns the query status bitmap.
*
* @return int
* A bitmap indicating query status. Zero indicates there were no problems.
* A non-zero value is a combination of one or more of the following flags:
* - SearchQuery::NO_POSITIVE_KEYWORDS
* - SearchQuery::EXPRESSIONS_IGNORED
* - SearchQuery::LOWER_CASE_OR
* - SearchQuery::NO_KEYWORD_MATCHES
*/
public function getStatus() {
return $this->status;
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Drupal\search\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\Component\Render\FormattableMarkup;
/**
* Defines the common search test code.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use \Drupal\Tests\search\Functional\SearchTestBase instead.
*/
abstract class SearchTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'search', 'dblog'];
protected function setUp() {
parent::setUp();
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
}
/**
* Simulates submission of a form using GET instead of POST.
*
* Forms that use the GET method cannot be submitted with
* WebTestBase::drupalPostForm(), which explicitly uses POST to submit the
* form. So this method finds the form, verifies that it has input fields and
* a submit button matching the inputs to this method, and then calls
* WebTestBase::drupalGet() to simulate the form submission to the 'action'
* URL of the form (if set, or the current URL if not).
*
* See WebTestBase::drupalPostForm() for more detailed documentation of the
* function parameters.
*
* @param string $path
* Location of the form to be submitted: either a Drupal path, absolute
* path, or NULL to use the current page.
* @param array $edit
* Form field data to submit. Unlike drupalPostForm(), this does not support
* file uploads.
* @param string $submit
* Value of the submit button to submit clicking. Unlike drupalPostForm(),
* this does not support AJAX.
* @param string $form_html_id
* (optional) HTML ID of the form, to disambiguate.
*/
protected function submitGetForm($path, $edit, $submit, $form_html_id = NULL) {
if (isset($path)) {
$this->drupalGet($path);
}
if ($this->parse()) {
// Iterate over forms to find one that matches $edit and $submit.
$edit_save = $edit;
$xpath = '//form';
if (!empty($form_html_id)) {
$xpath .= "[@id='" . $form_html_id . "']";
}
$forms = $this->xpath($xpath);
foreach ($forms as $form) {
// Try to set the fields of this form as specified in $edit.
$edit = $edit_save;
$post = [];
$upload = [];
$submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form);
if (!$edit && $submit_matches) {
// Everything matched, so "submit" the form.
$action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : NULL;
$this->drupalGet($action, ['query' => $post]);
return;
}
}
// We have not found a form which contained all fields of $edit and
// the submit button.
foreach ($edit as $name => $value) {
$this->fail(new FormattableMarkup('Failed to set field @name to @value', ['@name' => $name, '@value' => $value]));
}
$this->assertTrue($submit_matches, format_string('Found the @submit button', ['@submit' => $submit]));
$this->fail(format_string('Found the requested form fields at @path', ['@path' => $path]));
}
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Drupal\search;
use Drupal\Core\Database\Query\Condition;
/**
* Extends the core SearchQuery to be able to gets its protected values.
*/
class ViewsSearchQuery extends SearchQuery {
/**
* Returns the conditions property.
*
* @return array
* The query conditions.
*/
public function &conditions() {
return $this->conditions;
}
/**
* Returns the words property.
*
* @return array
* The positive search keywords.
*/
public function words() {
return $this->words;
}
/**
* Returns the simple property.
*
* @return bool
* TRUE if it is a simple query, and FALSE if it is complicated (phrases
* or LIKE).
*/
public function simple() {
return $this->simple;
}
/**
* Returns the matches property.
*
* @return int
* The number of matches needed.
*/
public function matches() {
return $this->matches;
}
/**
* Executes and returns the protected parseSearchExpression method.
*/
public function publicParseSearchExpression() {
return $this->parseSearchExpression();
}
/**
* Replaces the original condition with a custom one from views recursively.
*
* @param string $search
* The searched value.
* @param string $replace
* The value which replaces the search value.
* @param array $condition
* The query conditions array in which the string is replaced. This is an
* item from a \Drupal\Core\Database\Query\Condition::conditions array,
* which must have a 'field' element.
*/
public function conditionReplaceString($search, $replace, &$condition) {
if ($condition['field'] instanceof Condition) {
$conditions =& $condition['field']->conditions();
foreach ($conditions as $key => &$subcondition) {
if (is_numeric($key)) {
// As conditions can be nested, the function has to be called
// recursively.
$this->conditionReplaceString($search, $replace, $subcondition);
}
}
}
else {
$condition['field'] = str_replace($search, $replace, $condition['field']);
}
}
}