diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 208ef9b..85fa01e 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -480,6 +480,7 @@ function menu_get_item($path = NULL, $router_item = NULL) { if (\Drupal::state()->get('menu_rebuild_needed') || !\Drupal::state()->get('menu.masks')) { menu_router_rebuild(); \Drupal::service('router.builder')->rebuild(); + \Drupal::cache()->deleteTags(array('local_task' => 1)); } $original_map = arg(NULL, $path); diff --git a/core/lib/Drupal/Core/Config/Entity/DraggableListController.php b/core/lib/Drupal/Core/Config/Entity/DraggableListController.php index 8e61eb4..91a1dce 100644 --- a/core/lib/Drupal/Core/Config/Entity/DraggableListController.php +++ b/core/lib/Drupal/Core/Config/Entity/DraggableListController.php @@ -40,6 +40,13 @@ protected $weightKey = FALSE; /** + * The form builder. + * + * @var \Drupal\Core\Form\FormBuilderInterface + */ + protected $formBuilder; + + /** * {@inheritdoc} */ public function __construct(EntityTypeInterface $entity_info, EntityStorageControllerInterface $storage, ModuleHandlerInterface $module_handler) { @@ -88,7 +95,7 @@ public function buildRow(EntityInterface $entity) { */ public function render() { if (!empty($this->weightKey)) { - return drupal_get_form($this); + return $this->formBuilder()->getForm($this); } return parent::render(); } @@ -149,4 +156,17 @@ public function submitForm(array &$form, array &$form_state) { } } + /** + * Returns the form builder. + * + * @return \Drupal\Core\Form\FormBuilderInterface + * The form builder. + */ + protected function formBuilder() { + if (!$this->formBuilder) { + $this->formBuilder = \Drupal::formBuilder(); + } + return $this->formBuilder; + } + } diff --git a/core/modules/node/config/schema/node.schema.yml b/core/modules/node/config/schema/node.schema.yml index f2a8296..52f88c8 100644 --- a/core/modules/node/config/schema/node.schema.yml +++ b/core/modules/node/config/schema/node.schema.yml @@ -75,3 +75,25 @@ node.settings.node: submitted: type: boolean label: 'Display setting for author and date Submitted by post information' + +# Plugin \Drupal\node\Plugin\Search\NodeSearch +search.plugin.node_search: + type: mapping + label: 'Content search' + mapping: + rankings: + type: mapping + label: 'Content Ranking' + mapping: + comments: + type: integer + label: 'Number of comments' + relevance: + type: integer + label: 'Keyword relevance' + sticky: + type: integer + label: 'Content is sticky at top of lists' + promote: + type: integer + label: 'Content is promoted to the front page' diff --git a/core/modules/node/config/search.page.node_search.yml b/core/modules/node/config/search.page.node_search.yml new file mode 100644 index 0000000..cf0de1f --- /dev/null +++ b/core/modules/node/config/search.page.node_search.yml @@ -0,0 +1,9 @@ +id: node_search +label: 'Content' +uuid: 25687eeb-4bb5-469c-ad05-5eb24cd7012c +status: true +langcode: en +path: node +weight: -10 +plugin: node_search +configuration: { } diff --git a/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php index 573447d..89fc3bd 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php +++ b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php @@ -15,13 +15,11 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\KeyValueStore\StateInterface; use Drupal\Core\Language\Language; -use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessibleInterface; use Drupal\Core\Database\Query\Condition; -use Drupal\search\Plugin\SearchPluginBase; +use Drupal\search\Plugin\ConfigurableSearchPluginBase; use Drupal\search\Plugin\SearchIndexingInterface; - use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -29,11 +27,10 @@ * * @SearchPlugin( * id = "node_search", - * title = @Translation("Content"), - * path = "node" + * title = @Translation("Content") * ) */ -class NodeSearch extends SearchPluginBase implements AccessibleInterface, SearchIndexingInterface, PluginFormInterface { +class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInterface, SearchIndexingInterface { /** * A database connection object. @@ -78,6 +75,13 @@ class NodeSearch extends SearchPluginBase implements AccessibleInterface, Search protected $account; /** + * An array of additional rankings from hook_ranking(). + * + * @var array + */ + protected $rankings; + + /** * The list of options and info for advanced search filters. * * Each entry in the array has the option as the key and and for its value, an @@ -261,17 +265,16 @@ public function execute() { } /** - * Gathers the rankings from the the hook_ranking() implementations. + * Adds the configured rankings to the search query. * * @param $query * A query object that has been extended with the Search DB Extender. */ protected function addNodeRankings(SelectExtender $query) { - if ($ranking = $this->moduleHandler->invokeAll('ranking')) { + if ($ranking = $this->getRankings()) { $tables = &$query->getTables(); foreach ($ranking as $rank => $values) { - // @todo - move rank out of drupal variables. - if ($node_rank = variable_get('node_rank_' . $rank, 0)) { + if ($node_rank = $this->configuration['rankings'][$rank]) { // If the table defined in the ranking isn't already joined, then add it. if (isset($values['join']) && !isset($tables[$values['join']['alias']])) { $query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']); @@ -404,7 +407,6 @@ public function searchFormAlter(array &$form, array &$form_state) { ); // Add node types. - $node_types = $this->entityManager->getStorageController('node_type')->loadMultiple(); $types = array_map('check_plain', node_type_get_names()); $form['advanced']['types-fieldset'] = array( '#type' => 'fieldset', @@ -504,13 +506,44 @@ public function searchFormSubmit(array &$form, array &$form_state) { if (!empty($keys)) { form_set_value($form['basic']['processed_keys'], trim($keys), $form_state); } - $path = $form_state['action'] . '/' . $keys; $options = array(); if ($filters) { $options['query'] = array('f' => $filters); } - $form_state['redirect'] = array($path, $options); + $form_state['redirect_route'] = array( + 'route_name' => 'search.view_' . $form_state['search_page_id'], + 'route_parameters' => array( + 'keys' => $keys, + ), + 'options' => $options, + ); + } + + /** + * Gathers ranking definitions from hook_ranking(). + * + * @return array + * An array of ranking definitions. + */ + protected function getRankings() { + if (!$this->rankings) { + $this->rankings = $this->moduleHandler->invokeAll('ranking'); + } + return $this->rankings; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + $configuration = array( + 'rankings' => array(), + ); + foreach ($this->getRankings() as $var => $value) { + $configuration['rankings'][$var] = 0; + } + return $configuration; } /** @@ -524,17 +557,17 @@ public function buildConfigurationForm(array $form, array &$form_state) { ); $form['content_ranking']['#theme'] = 'node_search_admin'; $form['content_ranking']['info'] = array( - '#value' => '' . t('Influence is a numeric multiplier used in ordering search results. A higher number means the corresponding factor has more influence on search results; zero means the factor is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '' + '#value' => '' . $this->t('Influence is a numeric multiplier used in ordering search results. A higher number means the corresponding factor has more influence on search results; zero means the factor is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '' ); // Note: reversed to reflect that higher number = higher ranking. $options = drupal_map_assoc(range(0, 10)); - foreach ($this->moduleHandler->invokeAll('ranking') as $var => $values) { - $form['content_ranking']['factors']['node_rank_' . $var] = array( + foreach ($this->getRankings() as $var => $values) { + $form['content_ranking']['factors']['rankings_' . $var] = array( '#title' => $values['title'], '#type' => 'select', '#options' => $options, - '#default_value' => variable_get('node_rank_' . $var, 0), + '#default_value' => $this->configuration['rankings'][$var], ); } return $form; @@ -543,18 +576,12 @@ public function buildConfigurationForm(array $form, array &$form_state) { /** * {@inheritdoc} */ - public function validateConfigurationForm(array &$form, array &$form_state) { - } - - /** - * {@inheritdoc} - */ public function submitConfigurationForm(array &$form, array &$form_state) { - foreach ($this->moduleHandler->invokeAll('ranking') as $var => $values) { - if (isset($form_state['values']['node_rank_' . $var])) { - // @todo Fix when https://drupal.org/node/1831632 is in. - variable_set('node_rank_' . $var, $form_state['values']['node_rank_' . $var]); + foreach ($this->getRankings() as $var => $values) { + if (isset($form_state['values']['rankings_'.$var])) { + $this->configuration['rankings'][$var] = $form_state['values']['rankings_' . $var]; } } } + } diff --git a/core/modules/node/node.install b/core/modules/node/node.install index 8c104dd..91589a0 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -450,12 +450,6 @@ function node_uninstall() { \Drupal::config('language.settings')->clear('node. ' . $type . '.language.default_configuration')->save(); } - // Delete node search ranking variables. - variable_del('node_rank_relevance'); - variable_del('node_rank_sticky'); - variable_del('node_rank_promote'); - variable_del('node_rank_recent'); - // Delete remaining general module variables. \Drupal::state()->delete('node.node_access_needs_rebuild'); diff --git a/core/modules/search/config/schema/search.schema.yml b/core/modules/search/config/schema/search.schema.yml index 9fdd63f..68e6877 100644 --- a/core/modules/search/config/schema/search.schema.yml +++ b/core/modules/search/config/schema/search.schema.yml @@ -4,18 +4,12 @@ search.settings: type: mapping label: 'Search settings' mapping: - active_plugins: - type: sequence - label: 'Active search plugins' - sequence: - - type: string - label: 'Plugin' and_or_limit: type: integer label: 'AND/OR combination limit' - default_plugin: + default_page: type: string - label: 'Default search plugin' + label: 'Default search page' index: type: mapping label: 'Indexing settings' @@ -69,3 +63,34 @@ search.settings: a: type: integer label: 'Tag a weight' + +search.page.*: + type: mapping + label: 'Search page' + mapping: + id: + type: string + label: 'Machine-readable name' + label: + type: label + label: 'Label' + uuid: + type: string + label: 'UUID' + status: + type: boolean + label: 'Enabled status of the configuration entity' + langcode: + type: string + label: 'Default language' + path: + type: string + label: 'Search page path' + weight: + type: integer + label: 'Weight' + plugin: + type: string + label: 'Plugin' + configuration: + type: search.plugin.[%parent.plugin] diff --git a/core/modules/search/config/search.settings.yml b/core/modules/search/config/search.settings.yml index 78e0767..09c09cb 100644 --- a/core/modules/search/config/search.settings.yml +++ b/core/modules/search/config/search.settings.yml @@ -1,8 +1,5 @@ -active_plugins: - node_search: node_search - user_search: user_search and_or_limit: 7 -default_plugin: node_search +default_page: node_search index: cron_limit: 100 overlap_cjk: true diff --git a/core/modules/search/css/search.admin.css b/core/modules/search/css/search.admin.css new file mode 100644 index 0000000..c049ebd --- /dev/null +++ b/core/modules/search/css/search.admin.css @@ -0,0 +1,14 @@ +/** + * @file + * Styles for administration pages. + */ + +/** + * Add search page select/submit. + */ +#search-admin-settings #edit-add-page { + margin-bottom: 1em; +} +#search-admin-settings #edit-add-page label { + display: block; +} diff --git a/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php b/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php deleted file mode 100644 index 2ad2400..0000000 --- a/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php +++ /dev/null @@ -1,45 +0,0 @@ -searchManager = $search_plugin_manager; - } - - /** - * {@inheritdoc} - */ - public function access(Route $route, Request $request, AccountInterface $account) { - return $this->searchManager->getActiveDefinitions() ? static::ALLOW : static::DENY; - } - -} diff --git a/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php b/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php deleted file mode 100644 index 86e8166..0000000 --- a/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php +++ /dev/null @@ -1,27 +0,0 @@ -getRequirement('_search_plugin_view_access'); - return $this->searchManager->pluginAccess($plugin_id, $account) ? static::ALLOW : static::DENY; - } - -} diff --git a/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php index 720f2b2..b31ffe4 100644 --- a/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php +++ b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php @@ -13,8 +13,7 @@ * Defines a SearchPlugin type annotation object. * * SearchPlugin classes define search types for the core Search module. Each - * active search type is displayed in a tab on the Search page, and each has a - * path suffix after "search/". + * search type can be used to create search pages from the Search settings page. * * @see SearchPluginBase * @@ -30,13 +29,6 @@ class SearchPlugin extends Plugin { public $id; /** - * The path fragment to be added to search/ for the search page. - * - * @var string - */ - public $path; - - /** * The title for the search page tab. * * @todo This will potentially be translated twice or cached with the wrong @@ -47,4 +39,5 @@ class SearchPlugin extends Plugin { * @var \Drupal\Core\Annotation\Translation */ public $title; + } diff --git a/core/modules/search/lib/Drupal/search/Controller/SearchController.php b/core/modules/search/lib/Drupal/search/Controller/SearchController.php index 98dae2b..83ca3b5 100644 --- a/core/modules/search/lib/Drupal/search/Controller/SearchController.php +++ b/core/modules/search/lib/Drupal/search/Controller/SearchController.php @@ -9,8 +9,8 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Form\FormBuilderInterface; -use Drupal\search\SearchPluginManager; +use Drupal\search\SearchPageInterface; +use Drupal\search\SearchPageRepositoryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -20,30 +20,20 @@ class SearchController extends ControllerBase implements ContainerInjectionInterface { /** - * The search plugin manager. + * The search page repository. * - * @var \Drupal\search\SearchPluginManager + * @var \Drupal\search\SearchPageRepositoryInterface */ - protected $searchManager; - - /** - * The form builder. - * - * @var \Drupal\Core\Form\FormBuilderInterface - */ - protected $formBuilder; + protected $searchPageRepository; /** * Constructs a new search controller. * - * @param \Drupal\search\SearchPluginManager $search_plugin_manager - * The search plugin manager. - * @param \Drupal\Core\Form\FormBuilderInterface $form_builder - * The form builder. + * @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository + * The search page repository. */ - public function __construct(SearchPluginManager $search_plugin_manager, FormBuilderInterface $form_builder) { - $this->searchManager = $search_plugin_manager; - $this->formBuilder = $form_builder; + public function __construct(SearchPageRepositoryInterface $search_page_repository) { + $this->searchPageRepository = $search_page_repository; } /** @@ -51,8 +41,7 @@ public function __construct(SearchPluginManager $search_plugin_manager, FormBuil */ public static function create(ContainerInterface $container) { return new static( - $container->get('plugin.manager.search'), - $container->get('form_builder') + $container->get('search.search_page_repository') ); } @@ -61,46 +50,24 @@ public static function create(ContainerInterface $container) { * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. - * @param string $plugin_id - * The ID of a search plugin. + * @param \Drupal\search\SearchPageInterface $entity + * The search page entity. * @param string $keys - * Search keywords. + * (optional) Search keywords, defaults to an empty string. * * @return array|\Symfony\Component\HttpFoundation\RedirectResponse * The search form and search results or redirect response. */ - public function view(Request $request, $plugin_id = NULL, $keys = NULL) { - $info = FALSE; - $keys = trim($keys); + public function view(Request $request, SearchPageInterface $entity, $keys = '') { // Also try to pull search keywords from the request to support old GET // format of searches for existing links. if (!$keys && $request->query->has('keys')) { - $keys = trim($request->query->get('keys')); + $keys = $request->query->get('keys'); } + $keys = trim($keys); $build['#title'] = $this->t('Search'); - if (!empty($plugin_id)) { - $active_plugin_info = $this->searchManager->getActiveDefinitions(); - if (isset($active_plugin_info[$plugin_id])) { - $info = $active_plugin_info[$plugin_id]; - } - } - - if (empty($plugin_id) || empty($info)) { - // No path or invalid path: find the default plugin. Note that if there - // are no enabled search plugins, this function should never be called, - // since hook_menu() would not have defined any search paths. - $info = search_get_default_plugin_info(); - // Redirect from bare /search or an invalid path to the default search - // path. - $path = 'search/' . $info['path']; - if ($keys) { - $path .= '/' . $keys; - } - - return $this->redirect('search.view_' . $info['id']); - } - $plugin = $this->searchManager->createInstance($plugin_id); + $plugin = $entity->getPlugin(); $plugin->setSearch($keys, $request->query->all(), $request->attributes->all()); // Default results output is an empty string. $results = array('#markup' => ''); @@ -114,16 +81,85 @@ public function view(Request $request, $plugin_id = NULL, $keys = NULL) { // Only search if there are keywords or non-empty conditions. if ($plugin->isSearchExecutable()) { // Log the search keys. - watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys)); + watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $entity->label()), WATCHDOG_NOTICE, $this->l(t('results'), 'search.view_' . $entity->id(), array('keys' => $keys))); // Collect the search results. $results = $plugin->buildResults(); } } // The form may be altered based on whether the search was run. - $build['search_form'] = $this->formBuilder->getForm('\Drupal\search\Form\SearchForm', $plugin); + $build['search_form'] = $this->entityManager()->getForm($entity, 'search'); $build['search_results'] = $results; return $build; } + /** + * Redirects to a search page. + * + * This is used to redirect from /search to the default search page. + * + * @param \Drupal\search\SearchPageInterface $entity + * The search page entity. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * A redirect to the search page. + */ + public function redirectSearchPage(SearchPageInterface $entity) { + return $this->redirect('search.view_' . $entity->id()); + } + + /** + * Route title callback. + * + * @param \Drupal\search\SearchPageInterface $search_page + * The search page entity. + * + * @return string + * The title for the search page edit form. + */ + public function editTitle(SearchPageInterface $search_page) { + return $this->t('Edit %label search page', array('%label' => $search_page->label())); + } + + /** + * Performs an operation on the search page entity. + * + * @param \Drupal\search\SearchPageInterface $search_page + * The search page entity. + * @param string $op + * The operation to perform, usually 'enable' or 'disable'. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * A redirect back to the search settings page. + */ + public function performOperation(SearchPageInterface $search_page, $op) { + $search_page->$op()->save(); + + if ($op == 'enable') { + drupal_set_message($this->t('The %label search page has been enabled.', array('%label' => $search_page->label()))); + } + elseif ($op == 'disable') { + drupal_set_message($this->t('The %label search page has been disabled.', array('%label' => $search_page->label()))); + } + + return $this->redirect('search.settings'); + } + + /** + * Sets the search page as the default. + * + * @param \Drupal\search\SearchPageInterface $search_page + * The search page entity. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * A redirect to the search settings page. + */ + public function setAsDefault(SearchPageInterface $search_page) { + // Set the default page to this search page. + $this->searchPageRepository->setDefaultSearchPage($search_page); + + drupal_set_message($this->t('The default search page is now %label. Be sure to check the ordering of your search pages.', array('%label' => $search_page->label()))); + return $this->redirect('search.settings'); + } + } diff --git a/core/modules/search/lib/Drupal/search/Entity/SearchPage.php b/core/modules/search/lib/Drupal/search/Entity/SearchPage.php new file mode 100644 index 0000000..d0419d4 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Entity/SearchPage.php @@ -0,0 +1,281 @@ +pluginBag = new SearchPluginBag($this->searchPluginManager(), array($this->plugin), $this->configuration, $this->id()); + } + + /** + * {@inheritdoc} + */ + public function getPlugin() { + return $this->pluginBag->get($this->plugin); + } + + /** + * {@inheritdoc} + */ + public function setPlugin($plugin_id) { + $this->plugin = $plugin_id; + $this->pluginBag->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 getExportProperties() { + $properties = parent::getExportProperties(); + $names = array( + 'path', + 'weight', + 'plugin', + 'configuration', + ); + foreach ($names as $name) { + $properties[$name] = $this->get($name); + } + return $properties; + } + + /** + * {@inheritdoc} + */ + public function postCreate(EntityStorageControllerInterface $storage_controller) { + parent::postCreate($storage_controller); + + // @todo Use self::applyDefaultValue() once https://drupal.org/node/2004756 + // is in. + if (!isset($this->weight)) { + $this->weight = $this->isDefaultSearch() ? -10 : 0; + } + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageControllerInterface $storage_controller) { + parent::preSave($storage_controller); + + $plugin = $this->getPlugin(); + // If this plugin has any configuration, ensure that it is set. + if ($plugin instanceof ConfigurablePluginInterface) { + $this->set('configuration', $plugin->getConfiguration()); + } + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) { + parent::postSave($storage_controller, $update); + + $this->state()->set('menu_rebuild_needed', TRUE); + // @todo The above call should be sufficient, but it is not until + // https://drupal.org/node/2167323 is fixed. + \Drupal::service('router.builder')->rebuild(); + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) { + parent::postDelete($storage_controller, $entities); + + $search_page_repository = \Drupal::service('search.search_page_repository'); + if (!$search_page_repository->isSearchActive()) { + $search_page_repository->clearDefaultSearchPage(); + } + } + + /** + * {@inheritdoc} + */ + public static function sort($a, $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 state storage. + * + * @return \Drupal\Core\KeyValueStore\StateInterface + * An object for state storage. + */ + protected function state() { + return \Drupal::state(); + } + + /** + * Wraps the config factory. + * + * @return \Drupal\Core\Config\ConfigFactory + * 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'); + } + +} diff --git a/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php b/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php index c943c93..ad96735 100644 --- a/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php +++ b/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php @@ -8,6 +8,8 @@ namespace Drupal\search\Form; use Drupal\Core\Form\FormBase; +use Drupal\search\SearchPageRepositoryInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Builds the search form for the search block. @@ -15,6 +17,32 @@ class SearchBlockForm extends FormBase { /** + * The search page repository. + * + * @var \Drupal\search\SearchPageRepositoryInterface + */ + protected $searchPageRepository; + + /** + * Constructs a new SearchBlockForm. + * + * @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 getFormId() { @@ -62,17 +90,17 @@ public function submitForm(array &$form, array &$form_state) { } $form_id = $form['form_id']['#value']; - $info = search_get_default_plugin_info(); - if ($info) { + if ($entity_id = $this->searchPageRepository->getDefaultSearchPage()) { $form_state['redirect_route'] = array( - 'route_name' => 'search.view_' . $info['id'], + 'route_name' => 'search.view_' . $entity_id, 'route_parameters' => array( 'keys' => trim($form_state['values'][$form_id]), ), ); } else { - $this->setFormError('', $form_state, $this->t('Search is currently disabled.'), 'error'); + $this->setFormError('', $form_state, $this->t('Search is currently disabled.')); } } + } diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageAddForm.php b/core/modules/search/lib/Drupal/search/Form/SearchPageAddForm.php new file mode 100644 index 0000000..79ff609 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Form/SearchPageAddForm.php @@ -0,0 +1,48 @@ +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, array &$form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Add search page'); + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, array &$form_state) { + // If there is no default search page, make the added search the default. + if (!$this->searchPageRepository->getDefaultSearchPage()) { + $this->searchPageRepository->setDefaultSearchPage($this->entity); + } + + parent::save($form, $form_state); + + drupal_set_message($this->t('The %label search page has been added.', array('%label' => $this->entity->label()))); + } + +} diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageDeleteForm.php b/core/modules/search/lib/Drupal/search/Form/SearchPageDeleteForm.php new file mode 100644 index 0000000..c9a5752 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Form/SearchPageDeleteForm.php @@ -0,0 +1,49 @@ +t('Are you sure you want to delete the %label search page?', array('%label' => $this->entity->label())); + } + + /** + * {@inheritdoc} + */ + public function getCancelRoute() { + return array( + 'route_name' => 'search.settings', + ); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submit(array $form, array &$form_state) { + $this->entity->delete(); + $form_state['redirect_route']['route_name'] = 'search.settings'; + drupal_set_message($this->t('The %label search page has been deleted.', array('%label' => $this->entity->label()))); + } + +} diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageEditForm.php b/core/modules/search/lib/Drupal/search/Form/SearchPageEditForm.php new file mode 100644 index 0000000..de9b1e4 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Form/SearchPageEditForm.php @@ -0,0 +1,33 @@ +t('Save search page'); + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, array &$form_state) { + parent::save($form, $form_state); + + drupal_set_message($this->t('The %label search page has been updated.', array('%label' => $this->entity->label()))); + } + +} diff --git a/core/modules/search/lib/Drupal/search/Form/SearchForm.php b/core/modules/search/lib/Drupal/search/Form/SearchPageForm.php similarity index 51% rename from core/modules/search/lib/Drupal/search/Form/SearchForm.php rename to core/modules/search/lib/Drupal/search/Form/SearchPageForm.php index 35b0ff9..d817f7c 100644 --- a/core/modules/search/lib/Drupal/search/Form/SearchForm.php +++ b/core/modules/search/lib/Drupal/search/Form/SearchPageForm.php @@ -2,46 +2,24 @@ /** * @file - * Contains \Drupal\search\Form\SearchForm. + * Contains \Drupal\search\Form\SearchPageForm. */ namespace Drupal\search\Form; -use Drupal\Core\Form\FormBase; -use Drupal\search\Plugin\SearchInterface; -use Drupal\search\SearchPluginManager; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Entity\EntityFormController; /** * Provides a search form for site wide search. */ -class SearchForm extends FormBase { - - /** - * The search plugin manager. - * - * @var \Drupal\search\SearchPluginManager - */ - protected $searchManager; +class SearchPageForm extends EntityFormController { /** * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('plugin.manager.search') - ); - } - - /** - * Constructs a search form. * - * @param \Drupal\search\SearchPluginManager $search_plugin - * The search plugin manager. + * @var \Drupal\search\SearchPageInterface */ - public function __construct(SearchPluginManager $search_plugin) { - $this->searchManager = $search_plugin; - } + protected $entity; /** * {@inheritdoc} @@ -53,23 +31,10 @@ public function getFormID() { /** * {@inheritdoc} */ - public function buildForm(array $form, array &$form_state, SearchInterface $plugin = NULL, $action = '', $prompt = NULL) { - $plugin_info = $plugin->getPluginDefinition(); + public function form(array $form, array &$form_state) { + $plugin = $this->entity->getPlugin(); - if (!$action) { - $action = 'search/' . $plugin_info['path']; - } - if (!isset($prompt)) { - $prompt = $this->t('Enter your keywords'); - } - - $form['#action'] = $this->urlGenerator()->generateFromPath($action); - // Record the $action for later use in redirecting. - $form_state['action'] = $action; - $form['plugin_id'] = array( - '#type' => 'value', - '#value' => $plugin->getPluginId(), - ); + $form_state['search_page_id'] = $this->entity->id(); $form['basic'] = array( '#type' => 'container', '#attributes' => array( @@ -78,9 +43,9 @@ public function buildForm(array $form, array &$form_state, SearchInterface $plug ); $form['basic']['keys'] = array( '#type' => 'search', - '#title' => $prompt, + '#title' => $this->t('Enter your keywords'), '#default_value' => $plugin->getKeywords(), - '#size' => $prompt ? 30 : 20, + '#size' => 30, '#maxlength' => 255, ); // processed_keys is used to coordinate keyword passing between other forms @@ -96,7 +61,15 @@ public function buildForm(array $form, array &$form_state, SearchInterface $plug // Allow the plugin to add to or alter the search form. $plugin->searchFormAlter($form, $form_state); - return $form; + return parent::form($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, array &$form_state) { + // The submit button is added in the form directly. + return array(); } /** @@ -116,6 +89,12 @@ public function submitForm(array &$form, array &$form_state) { // Fall through to the form redirect. } - $form_state['redirect'] = $form_state['action'] . '/' . $keys; + $form_state['redirect_route'] = array( + 'route_name' => 'search.view_' . $this->entity->id(), + 'route_parameters' => array( + 'keys' => $keys, + ), + ); } + } diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageFormBase.php b/core/modules/search/lib/Drupal/search/Form/SearchPageFormBase.php new file mode 100644 index 0000000..1e574c3 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Form/SearchPageFormBase.php @@ -0,0 +1,207 @@ +entityQuery = $entity_query; + $this->searchPageRepository = $search_page_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.query'), + $container->get('search.search_page_repository') + ); + } + + /** + * {@inheritdoc} + */ + public function getBaseFormID() { + return 'search_entity_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state) { + $this->plugin = $this->entity->getPlugin(); + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, array &$form_state) { + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#description' => $this->t('The label for this search page.'), + '#default_value' => $this->entity->label(), + '#maxlength' => '255', + ); + + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $this->entity->id(), + '#disabled' => !$this->entity->isNew(), + '#maxlength' => 64, + '#machine_name' => array( + 'exists' => array($this, 'exists'), + ), + ); + $form['path'] = array( + '#type' => 'textfield', + '#title' => $this->t('Path'), + '#field_prefix' => 'search/', + '#default_value' => $this->entity->getPath(), + '#maxlength' => '255', + ); + $form['plugin'] = array( + '#type' => 'value', + '#value' => $this->entity->get('plugin'), + ); + + if ($this->plugin instanceof PluginFormInterface) { + $form += $this->plugin->buildConfigurationForm($form, $form_state); + } + + return parent::form($form, $form_state); + } + + /** + * Determines if the search page entity already exists. + * + * @param string $id + * The search configuration ID. + * + * @return bool + * TRUE if the search configuration exists, FALSE otherwise. + */ + public function exists($id) { + $entity = $this->entityQuery->get('search_page') + ->condition('id', $id) + ->execute(); + return (bool) $entity; + } + + /** + * {@inheritdoc} + */ + public function validate(array $form, array &$form_state) { + parent::validate($form, $form_state); + + // Ensure each path is unique. + $path = $this->entityQuery->get('search_page') + ->condition('path', $form_state['values']['path']) + ->condition('id', $form_state['values']['id'], '<>') + ->execute(); + if ($path) { + $this->setFormError('path', $form_state, $this->t('The search page path must be unique.')); + } + + if ($this->plugin instanceof PluginFormInterface) { + $this->plugin->validateConfigurationForm($form, $form_state); + } + } + + /** + * {@inheritdoc} + */ + public function submit(array $form, array &$form_state) { + parent::submit($form, $form_state); + + if ($this->plugin instanceof PluginFormInterface) { + $this->plugin->submitConfigurationForm($form, $form_state); + } + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, array &$form_state) { + $this->entity->save(); + + $form_state['redirect_route']['route_name'] = 'search.settings'; + } + + /** + * {@inheritdoc} + */ + public function delete(array $form, array &$form_state) { + $form_state['redirect_route'] = array( + 'route_name' => 'search.delete', + 'route_parameters' => array( + 'search_page' => $this->entity->id(), + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, array &$form_state) { + $actions = parent::actions($form, $form_state); + if ($this->entity->isDefaultSearch()) { + unset($actions['delete']); + } + return $actions; + } + +} diff --git a/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php b/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php deleted file mode 100644 index 0d3e115..0000000 --- a/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php +++ /dev/null @@ -1,281 +0,0 @@ -searchSettings = $config_factory->get('search.settings'); - $this->searchPluginManager = $manager; - $this->moduleHandler = $module_handler; - $this->state = $state; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('config.factory'), - $container->get('config.context.free'), - $container->get('plugin.manager.search'), - $container->get('module_handler'), - $container->get('state') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'search_admin_settings'; - } - - /** - * Returns names of available search plugins. - * - * @return array - * An array of the names of available search plugins. - */ - protected function getOptions() { - $options = array(); - foreach ($this->searchPluginManager->getDefinitions() as $plugin_id => $search_info) { - $options[$plugin_id] = $search_info['title'] . ' (' . $plugin_id . ')'; - } - asort($options, SORT_STRING); - return $options; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, array &$form_state) { - - // Collect some stats. - $remaining = 0; - $total = 0; - - foreach ($this->searchPluginManager->getActiveIndexingPlugins() as $plugin) { - if ($status = $plugin->indexStatus()) { - $remaining += $status['remaining']; - $total += $status['total']; - } - } - $active_plugins = $this->searchPluginManager->getActivePlugins(); - $this->moduleHandler->loadAllIncludes('admin.inc'); - $count = format_plural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.'); - $percentage = ((int) min(100, 100 * ($total - $remaining) / max(1, $total))) . '%'; - $status = '

' . $this->t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '

'; - $form['status'] = array( - '#type' => 'details', - '#title' => $this->t('Indexing status'), - ); - $form['status']['status'] = array('#markup' => $status); - $form['status']['wipe'] = array( - '#type' => 'submit', - '#value' => $this->t('Re-index site'), - '#submit' => array(array($this, 'searchAdminReindexSubmit')), - ); - - $items = drupal_map_assoc(array(10, 20, 50, 100, 200, 500)); - - // Indexing throttle: - $form['indexing_throttle'] = array( - '#type' => 'details', - '#title' => $this->t('Indexing throttle') - ); - $form['indexing_throttle']['cron_limit'] = array( - '#type' => 'select', - '#title' => $this->t('Number of items to index per cron run'), - '#default_value' => $this->searchSettings->get('index.cron_limit'), - '#options' => $items, - '#description' => $this->t('The maximum number of items indexed in each pass of a cron maintenance task. If necessary, reduce the number of items to prevent timeouts and memory errors while indexing.', array('@cron' => $this->url('system.status'))) - ); - // Indexing settings: - $form['indexing_settings'] = array( - '#type' => 'details', - '#title' => $this->t('Indexing settings') - ); - $form['indexing_settings']['info'] = array( - '#markup' => $this->t('

Changing the settings below will cause the site index to be rebuilt. The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed.

The default settings should be appropriate for the majority of sites.

') - ); - $form['indexing_settings']['minimum_word_size'] = array( - '#type' => 'number', - '#title' => $this->t('Minimum word length to index'), - '#default_value' => $this->searchSettings->get('index.minimum_word_size'), - '#min' => 1, - '#max' => 1000, - '#description' => $this->t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).') - ); - $form['indexing_settings']['overlap_cjk'] = array( - '#type' => 'checkbox', - '#title' => $this->t('Simple CJK handling'), - '#default_value' => $this->searchSettings->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.') - ); - - $form['active'] = array( - '#type' => 'details', - '#title' => $this->t('Active search plugins') - ); - $options = $this->getOptions(); - $form['active']['active_plugins'] = array( - '#type' => 'checkboxes', - '#title' => $this->t('Active plugins'), - '#title_display' => 'invisible', - '#default_value' => $this->searchSettings->get('active_plugins'), - '#options' => $options, - '#description' => $this->t('Choose which search plugins are active from the available plugins.') - ); - $form['active']['default_plugin'] = array( - '#title' => $this->t('Default search plugin'), - '#type' => 'radios', - '#default_value' => $this->searchSettings->get('default_plugin'), - '#options' => $options, - '#description' => $this->t('Choose which search plugin is the default.') - ); - - // Per plugin settings. - foreach ($active_plugins as $plugin) { - if ($plugin instanceof PluginFormInterface) { - $form = $plugin->buildConfigurationForm($form, $form_state); - } - } - // Set #submit so we are sure it's invoked even if one of - // the active search plugins added its own #submit. - $form['#submit'][] = array($this, 'submitForm'); - - return parent::buildForm($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, array &$form_state) { - parent::validateForm($form, $form_state); - - // Check whether we selected a valid default. - if ($form_state['triggering_element']['#value'] != $this->t('Reset to defaults')) { - $new_plugins = array_filter($form_state['values']['active_plugins']); - $default = $form_state['values']['default_plugin']; - if (!in_array($default, $new_plugins, TRUE)) { - $this->setFormError('default_plugin', $form_state, $this->t('Your default search plugin is not selected as an active plugin.')); - } - } - // Handle per-plugin validation logic. - foreach ($this->searchPluginManager->getActivePlugins() as $plugin) { - if ($plugin instanceof PluginFormInterface) { - $plugin->validateConfigurationForm($form, $form_state); - } - } - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, array &$form_state) { - parent::submitForm($form, $form_state); - - // If these settings change, the index needs to be rebuilt. - if (($this->searchSettings->get('index.minimum_word_size') != $form_state['values']['minimum_word_size']) || ($this->searchSettings->get('index.overlap_cjk') != $form_state['values']['overlap_cjk'])) { - $this->searchSettings->set('index.minimum_word_size', $form_state['values']['minimum_word_size']); - $this->searchSettings->set('index.overlap_cjk', $form_state['values']['overlap_cjk']); - drupal_set_message($this->t('The index will be rebuilt.')); - search_reindex(); - } - $this->searchSettings->set('index.cron_limit', $form_state['values']['cron_limit']); - $this->searchSettings->set('default_plugin', $form_state['values']['default_plugin']); - - // Handle per-plugin submission logic. - foreach ($this->searchPluginManager->getActivePlugins() as $plugin) { - if ($plugin instanceof PluginFormInterface) { - $plugin->submitConfigurationForm($form, $form_state); - } - } - - // Check whether we are resetting the values. - if ($form_state['triggering_element']['#value'] == $this->t('Reset to defaults')) { - $new_plugins = array('node_search', 'user_search'); - } - else { - $new_plugins = array_filter($form_state['values']['active_plugins']); - } - if ($this->searchSettings->get('active_plugins') != $new_plugins) { - $this->searchSettings->set('active_plugins', $new_plugins); - drupal_set_message($this->t('The active search plugins have been changed.')); - $this->state->set('menu_rebuild_needed', TRUE); - Cache::deleteTags(array('local_task' => TRUE)); - } - $this->searchSettings->save(); - } - - /** - * Form submission handler for the reindex button on the search admin settings - * form. - */ - public function searchAdminReindexSubmit(array $form, array &$form_state) { - // send the user to the confirmation page - $form_state['redirect_route']['route_name'] = 'search.reindex_confirm'; - } - -} diff --git a/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginBase.php b/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginBase.php new file mode 100644 index 0000000..0328915 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginBase.php @@ -0,0 +1,68 @@ +configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $this->configuration); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function setSearchPageId($search_page_id) { + $this->searchPageId = $search_page_id; + return $this; + } + +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginInterface.php b/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginInterface.php new file mode 100644 index 0000000..62899c6 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginInterface.php @@ -0,0 +1,28 @@ +searchPageRepository = $search_page_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('search.search_page_repository') + ); + } /** * {@inheritdoc} @@ -20,20 +49,15 @@ class SearchLocalTask extends DerivativeBase { public function getDerivativeDefinitions(array $base_plugin_definition) { $this->derivatives = array(); - $default_info = search_get_default_plugin_info(); - if ($default_info) { - foreach (\Drupal::service('plugin.manager.search')->getActiveDefinitions() as $plugin_id => $search_info) { - $this->derivatives[$plugin_id] = array( - 'title' => $search_info['title'], - 'route_name' => 'search.view_' . $plugin_id, - 'base_route' => 'search.view_' . $default_info['id'], + if ($default = $this->searchPageRepository->getDefaultSearchPage()) { + $active_search_pages = $this->searchPageRepository->getActiveSearchPages(); + foreach ($this->searchPageRepository->sortSearchPages($active_search_pages) as $entity_id => $entity) { + $this->derivatives[$entity_id] = array( + 'title' => $entity->label(), + 'route_name' => 'search.view_' . $entity_id, + 'base_route' => 'search.plugins:' . $default, + 'weight' => $entity->getWeight(), ); - if ($plugin_id == $default_info['id']) { - $this->derivatives[$plugin_id]['weight'] = -10; - } - else { - $this->derivatives[$plugin_id]['weight'] = 0; - } } } return $this->derivatives; diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php b/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php index a2ca973..26edef5 100644 --- a/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php @@ -14,6 +14,11 @@ * search_cron() and via the search module administration form. Plugins not * implementing this interface are assumed to use alternate mechanisms for * indexing the data used to provide search results. + * + * 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 { diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBag.php b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBag.php new file mode 100644 index 0000000..0f8596e --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBag.php @@ -0,0 +1,55 @@ +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); + } + } + +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php index 7f534d5..ec808ad 100644 --- a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a base class for plugins wishing to support search. @@ -39,6 +40,13 @@ /** * {@inheritdoc} */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $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; diff --git a/core/modules/search/lib/Drupal/search/Routing/SearchPageRoutes.php b/core/modules/search/lib/Drupal/search/Routing/SearchPageRoutes.php new file mode 100644 index 0000000..9a5f505 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Routing/SearchPageRoutes.php @@ -0,0 +1,104 @@ +searchPageRepository = $search_page_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('search.search_page_repository') + ); + } + + /** + * Returns an array of route objects. + * + * @return \Symfony\Component\Routing\Route[] + * An array of route objects. + */ + public function routes() { + $routes = array(); + // @todo Decide if /search should continue to redirect to /search/$default, + // or just perform the appropriate search. + if ($default_page = $this->searchPageRepository->getDefaultSearchPage()) { + $routes['search.view'] = new Route( + '/search', + array( + '_content' => 'Drupal\search\Controller\SearchController::redirectSearchPage', + '_title' => 'Search', + 'entity' => $default_page, + ), + array( + '_entity_access' => 'entity.view', + '_permission' => 'search content', + ), + array( + 'parameters' => array( + 'entity' => array( + 'type' => 'entity:search_page', + ), + ), + ) + ); + } + $active_pages = $this->searchPageRepository->getActiveSearchPages(); + foreach ($active_pages as $entity_id => $entity) { + $routes["search.view_$entity_id"] = new Route( + '/search/' . $entity->getPath() . '/{keys}', + array( + '_content' => 'Drupal\search\Controller\SearchController::view', + '_title' => $entity->label(), + 'entity' => $entity_id, + 'keys' => '', + ), + array( + 'keys' => '.+', + '_entity_access' => 'entity.view', + '_permission' => 'search content', + ), + array( + 'parameters' => array( + 'entity' => array( + 'type' => 'entity:search_page', + ), + ), + ) + ); + } + return $routes; + } + +} diff --git a/core/modules/search/lib/Drupal/search/Routing/SearchPluginRoutes.php b/core/modules/search/lib/Drupal/search/Routing/SearchPluginRoutes.php deleted file mode 100644 index bfc37ea..0000000 --- a/core/modules/search/lib/Drupal/search/Routing/SearchPluginRoutes.php +++ /dev/null @@ -1,73 +0,0 @@ -searchManager = $search_plugin_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('plugin.manager.search') - ); - } - - /** - * Returns an array of route objects. - * - * @return \Symfony\Component\Routing\Route[] - * An array of route objects. - */ - public function routes() { - $routes = array(); - foreach ($this->searchManager->getActiveDefinitions() as $plugin_id => $search_info) { - $routes["search.view_$plugin_id"] = new Route( - 'search/' . $search_info['path'] . '/{keys}', - array( - '_content' => 'Drupal\search\Controller\SearchController::view', - '_title' => $search_info['title'], - 'plugin_id' => $plugin_id, - 'keys' => '', - ), - array( - 'keys' => '.+', - '_search_plugin_view_access' => $plugin_id, - '_permission' => 'search content', - ) - ); - } - return $routes; - } - -} diff --git a/core/modules/search/lib/Drupal/search/SearchPageAccessController.php b/core/modules/search/lib/Drupal/search/SearchPageAccessController.php new file mode 100644 index 0000000..cd51278 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/SearchPageAccessController.php @@ -0,0 +1,41 @@ +isDefaultSearch()) { + return FALSE; + } + if ($operation == 'view') { + if (!$entity->status()) { + return FALSE; + } + $plugin = $entity->getPlugin(); + if ($plugin instanceof AccessibleInterface) { + return $plugin->access($operation, $account); + } + return TRUE; + } + return parent::checkAccess($entity, $operation, $langcode, $account); + } + +} diff --git a/core/modules/search/lib/Drupal/search/SearchPageInterface.php b/core/modules/search/lib/Drupal/search/SearchPageInterface.php new file mode 100644 index 0000000..dae91f9 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/SearchPageInterface.php @@ -0,0 +1,65 @@ +configFactory = $config_factory; + $this->configFactory->enterContext($context); + + $this->searchManager = $search_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_info) { + return new static( + $entity_info, + $container->get('entity.manager')->getStorageController($entity_info->id()), + $container->get('plugin.manager.search'), + $container->get('module_handler'), + $container->get('config.factory'), + $container->get('config.context.free') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'search_admin_settings'; + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = array( + 'data' => $this->t('Label'), + ); + $header['url'] = array( + 'data' => $this->t('URL'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ); + $header['plugin'] = array( + 'data' => $this->t('Type'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ); + $header['status'] = array( + 'data' => $this->t('Status'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /** @var $entity \Drupal\search\SearchPageInterface */ + $row['label'] = $this->getLabel($entity); + $row['url']['#markup'] = 'search/' . $entity->getPath(); + // If the search page is active, link to it. + if ($entity->status()) { + $row['url'] = array( + '#type' => 'link', + '#title' => $row['url'], + '#route_name' => '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; + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state) { + $form = parent::buildForm($form, $form_state); + $search_settings = $this->configFactory->get('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 = format_plural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.'); + $percentage = ((int) min(100, 100 * ($total - $remaining) / max(1, $total))) . '%'; + $status = '

' . $this->t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '

'; + $form['status'] = array( + '#type' => 'details', + '#title' => $this->t('Indexing status'), + ); + $form['status']['status'] = array('#markup' => $status); + $form['status']['wipe'] = array( + '#type' => 'submit', + '#value' => $this->t('Re-index site'), + '#submit' => array(array($this, 'searchAdminReindexSubmit')), + ); + + $items = MapArray::copyValuesToKeys(array(10, 20, 50, 100, 200, 500)); + + // Indexing throttle: + $form['indexing_throttle'] = array( + '#type' => 'details', + '#title' => $this->t('Indexing throttle') + ); + $form['indexing_throttle']['cron_limit'] = array( + '#type' => 'select', + '#title' => $this->t('Number of items to index per cron run'), + '#default_value' => $search_settings->get('index.cron_limit'), + '#options' => $items, + '#description' => $this->t('The maximum number of items indexed in each pass of a cron maintenance task. If necessary, reduce the number of items to prevent timeouts and memory errors while indexing.', array('@cron' => url('admin/reports/status'))), + ); + // Indexing settings: + $form['indexing_settings'] = array( + '#type' => 'details', + '#title' => $this->t('Indexing settings') + ); + $form['indexing_settings']['info'] = array( + '#markup' => $this->t('

Changing the settings below will cause the site index to be rebuilt. The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed.

The default settings should be appropriate for the majority of sites.

') + ); + $form['indexing_settings']['minimum_word_size'] = array( + '#type' => 'number', + '#title' => $this->t('Minimum word length to index'), + '#default_value' => $search_settings->get('index.minimum_word_size'), + '#min' => 1, + '#max' => 1000, + '#description' => $this->t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).') + ); + $form['indexing_settings']['overlap_cjk'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Simple CJK handling'), + '#default_value' => $search_settings->get('index.overlap_cjk'), + '#description' => $this->t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.') + ); + + $form['search_pages'] = array( + '#type' => 'details', + '#title' => $this->t('Search pages'), + ); + $form['search_pages']['add_page'] = array( + '#type' => 'container', + '#attributes' => array( + 'class' => array('container-inline'), + ), + '#attached' => array( + 'css' => array( + drupal_get_path('module', 'search') . '/css/search.admin.css', + ), + ), + ); + // In order to prevent validation errors for the parent form, this cannot be + // required, see self::validateAddSearchPage(). + $form['search_pages']['add_page']['search_type'] = array( + '#type' => 'select', + '#title' => $this->t('Search page type'), + '#empty_option' => $this->t('- Choose page type -'), + '#options' => array_map(function ($definition) { + return $definition['title']; + }, $this->searchManager->getDefinitions()), + ); + $form['search_pages']['add_page']['add_search_submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Add new page'), + '#validate' => array(array($this, 'validateAddSearchPage')), + '#submit' => array(array($this, 'submitAddSearchPage')), + '#limit_validation_errors' => array(array('search_type')), + ); + + // Move the listing into the search_pages element. + $form['search_pages'][$this->entitiesKey] = $form[$this->entitiesKey]; + $form['search_pages'][$this->entitiesKey]['#empty'] = $this->t('No search pages have been configured.'); + unset($form[$this->entitiesKey]); + + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Save configuration'), + '#button_type' => 'primary', + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getOperations(EntityInterface $entity) { + /** @var $entity \Drupal\search\SearchPageInterface */ + $operations = parent::getOperations($entity); + + // Prevent the default search from being disabled or deleted. + if ($entity->isDefaultSearch()) { + unset($operations['disable'], $operations['delete']); + } + else { + $operations['default'] = array( + 'title' => $this->t('Set as default'), + 'route_name' => 'search.set_default', + 'route_parameters' => array( + 'search_page' => $entity->id(), + ), + 'weight' => 50, + ); + } + + return $operations; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + parent::submitForm($form, $form_state); + + $search_settings = $this->configFactory->get('search.settings'); + // If these settings change, the index needs to be rebuilt. + if (($search_settings->get('index.minimum_word_size') != $form_state['values']['minimum_word_size']) || ($search_settings->get('index.overlap_cjk') != $form_state['values']['overlap_cjk'])) { + $search_settings->set('index.minimum_word_size', $form_state['values']['minimum_word_size']); + $search_settings->set('index.overlap_cjk', $form_state['values']['overlap_cjk']); + drupal_set_message($this->t('The index will be rebuilt.')); + search_reindex(); + } + + $search_settings + ->set('index.cron_limit', $form_state['values']['cron_limit']) + ->save(); + + drupal_set_message($this->t('The configuration options have been saved.')); + } + + /** + * Form submission handler for the reindex button on the search admin settings + * form. + */ + public function searchAdminReindexSubmit(array &$form, array &$form_state) { + // Send the user to the confirmation page. + $form_state['redirect_route']['route_name'] = 'search.reindex_confirm'; + } + + /** + * Form validation handler for adding a new search page. + */ + public function validateAddSearchPage(array &$form, array &$form_state) { + if (empty($form_state['values']['search_type'])) { + $this->formBuilder()->setErrorByName('search_type', $form_state, $this->t('You must select the new search page type.')); + } + } + + /** + * Form submission handler for adding a new search page. + */ + public function submitAddSearchPage(array &$form, array &$form_state) { + $form_state['redirect_route'] = array( + 'route_name' => 'search.add_type', + 'route_parameters' => array( + 'search_plugin_id' => $form_state['values']['search_type'], + ), + ); + } + +} diff --git a/core/modules/search/lib/Drupal/search/SearchPageRepository.php b/core/modules/search/lib/Drupal/search/SearchPageRepository.php new file mode 100644 index 0000000..e2af5ff --- /dev/null +++ b/core/modules/search/lib/Drupal/search/SearchPageRepository.php @@ -0,0 +1,127 @@ +configFactory = $config_factory; + $this->storage = $entity_manager->getStorageController('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 reset($search_pages); + } + + /** + * {@inheritdoc} + */ + public function clearDefaultSearchPage() { + $this->configFactory->get('search.settings')->clear('default_page')->save(); + } + + /** + * {@inheritdoc} + */ + public function setDefaultSearchPage(SearchPageInterface $search_page) { + $this->configFactory->get('search.settings')->set('default_page', $search_page->id())->save(); + $search_page->enable()->save(); + } + + /** + * {@inheritdoc} + */ + public function sortSearchPages($search_pages) { + $entity_info = $this->storage->entityInfo(); + uasort($search_pages, array($entity_info->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(); + } + +} diff --git a/core/modules/search/lib/Drupal/search/SearchPageRepositoryInterface.php b/core/modules/search/lib/Drupal/search/SearchPageRepositoryInterface.php new file mode 100644 index 0000000..b75202f --- /dev/null +++ b/core/modules/search/lib/Drupal/search/SearchPageRepositoryInterface.php @@ -0,0 +1,73 @@ +configFactory = $config_factory; $this->setCacheBackend($cache_backend, $language_manager, 'search_plugins'); + // @todo Set an alter hook. } - /** - * {@inheritdoc} - */ - public function processDefinition(&$definition, $plugin_id) { - parent::processDefinition($definition, $plugin_id); - - // Fill in the provider as default values for missing keys. - $definition += array( - 'title' => $definition['provider'], - 'path' => $definition['provider'], - ); - } - - /** - * Returns an instance for each active search plugin. - * - * @return \Drupal\search\Plugin\SearchInterface[] - * An array of active search plugins, keyed by their ID. - */ - public function getActivePlugins() { - $plugins = array(); - foreach ($this->getActiveDefinitions() as $plugin_id => $definition) { - $plugins[$plugin_id] = $this->createInstance($plugin_id); - } - return $plugins; - } - - /** - * Returns an instance for each active plugin that implements \Drupal\search\Plugin\SearchIndexingInterface. - * - * @return \Drupal\search\Plugin\SearchInterface[] - * An array of active search plugins, keyed by their ID. - */ - public function getActiveIndexingPlugins() { - $plugins = array(); - foreach ($this->getActiveDefinitions() as $plugin_id => $definition) { - if (is_subclass_of($definition['class'], '\Drupal\search\Plugin\SearchIndexingInterface')) { - $plugins[$plugin_id] = $this->createInstance($plugin_id); - } - } - return $plugins; - } - - /** - * Returns definitions for active search plugins keyed by their ID. - * - * @return array - * An array of active search plugin definitions, keyed by their ID. - */ - public function getActiveDefinitions() { - $active_definitions = array(); - $active_config = $this->configFactory->get('search.settings')->get('active_plugins'); - $active_plugins = $active_config ? array_flip($active_config) : array(); - foreach ($this->getDefinitions() as $plugin_id => $definition) { - if (isset($active_plugins[$plugin_id])) { - $active_definitions[$plugin_id] = $definition; - } - } - return $active_definitions; - } - - /** - * Check whether access is allowed to search results from a given plugin. - * - * @param string $plugin_id - * The id of the plugin being checked. - * @param \Drupal\Core\Session\AccountInterface $account - * The account being checked for access - * - * @return bool - * TRUE if access is allowed, FALSE otherwise. - */ - public function pluginAccess($plugin_id, AccountInterface $account) { - $definition = $this->getDefinition($plugin_id); - if (empty($definition['class'])) { - return FALSE; - } - // Plugins that implement AccessibleInterface can deny access. - if (is_subclass_of($definition['class'], '\Drupal\Core\Access\AccessibleInterface')) { - return $this->createInstance($plugin_id)->access('view', $account); - } - return TRUE; - } } diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchBlockTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchBlockTest.php index 1a52877..2fa66a7 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchBlockTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchBlockTest.php @@ -52,7 +52,8 @@ protected function testSearchFormBlock() { // Test a normal search via the block form, from the front page. $terms = array('search_block_form' => 'test'); - $this->drupalPostForm('node', $terms, t('Search')); + $this->drupalPostForm('', $terms, t('Search')); + $this->assertResponse(200); $this->assertText('Your search yielded no results'); // Test a search from the block on a 404 page. @@ -66,7 +67,8 @@ protected function testSearchFormBlock() { $visibility['path']['pages'] = 'search'; $block->set('visibility', $visibility); - $this->drupalPostForm('node', $terms, t('Search')); + $this->drupalPostForm('', $terms, t('Search')); + $this->assertResponse(200); $this->assertText('Your search yielded no results'); // Confirm that the user is redirected to the search page. @@ -78,7 +80,8 @@ protected function testSearchFormBlock() { // Test an empty search via the block form, from the front page. $terms = array('search_block_form' => ''); - $this->drupalPostForm('node', $terms, t('Search')); + $this->drupalPostForm('', $terms, t('Search')); + $this->assertResponse(200); $this->assertText('Please enter some keywords'); // Confirm that the user is redirected to the search page, when form is diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php index 205071d..0cff49d 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php @@ -19,7 +19,18 @@ class SearchConfigSettingsFormTest extends SearchTestBase { */ public static $modules = array('block', 'search_extra_type'); + /** + * User who can search and administer search. + * + * @var \Drupal\user\UserInterface + */ public $search_user; + + /** + * Node indexed for searching. + * + * @var \Drupal\node\NodeInterface + */ public $search_node; public static function getInfo() { @@ -86,28 +97,8 @@ function testSearchSettingsPage() { * Verifies plugin-supplied settings form. */ function testSearchModuleSettingsPage() { - - // Test that the settings form displays the correct count of items left to index. $this->drupalGet('admin/config/search/settings'); - - // Ensure that the settings fieldset for the test plugin is not present on - // the page - $this->assertNoText(t('Extra type settings')); - $this->assertNoText(t('Boost method')); - - // Ensure that the test plugin is listed as an option - $this->assertTrue($this->xpath('//input[@id="edit-active-plugins-search-extra-type-search"]'), 'Checkbox for activating search for an extra plugin is visible'); - $this->assertTrue($this->xpath('//input[@id="edit-default-plugin-search-extra-type-search"]'), 'Radio button for setting extra plugin as default search plugin is visible'); - - // Enable search for the test plugin - $edit['active_plugins[search_extra_type_search]'] = 'search_extra_type_search'; - $edit['default_plugin'] = 'search_extra_type_search'; - $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration')); - - // Ensure that the settings fieldset is visible after enabling search for - // the test plugin - $this->assertText(t('Extra type settings')); - $this->assertText(t('Boost method')); + $this->clickLink(t('Edit'), 1); // Ensure that the default setting was picked up from the default config $this->assertTrue($this->xpath('//select[@id="edit-extra-type-settings-boost"]//option[@value="bi" and @selected="selected"]'), 'Module specific settings are picked up from the default config'); @@ -115,66 +106,60 @@ function testSearchModuleSettingsPage() { // Change extra type setting and also modify a common search setting. $edit = array( 'extra_type_settings[boost]' => 'ii', - 'minimum_word_size' => 5, ); - $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration')); + $this->drupalPostForm(NULL, $edit, t('Save search page')); // Ensure that the modifications took effect. - $this->assertText(t('The configuration options have been saved.')); + $this->assertRaw(t('The %label search page has been updated.', array('%label' => 'Dummy search type'))); + $this->drupalGet('admin/config/search/settings/manage/dummy_search_type'); $this->assertTrue($this->xpath('//select[@id="edit-extra-type-settings-boost"]//option[@value="ii" and @selected="selected"]'), 'Module specific settings can be changed'); - $this->assertTrue($this->xpath('//input[@id="edit-minimum-word-size" and @value="5"]'), 'Common search settings can be modified if a plugin-specific form is active'); } /** * Verifies that you can disable individual search plugins. */ function testSearchModuleDisabling() { - // Array of search plugins to test: 'path' is the search path, 'title' is - // the tab title, 'keys' are the keywords to search for, and 'text' is - // the text to assert is on the results page. + // Array of search plugins to test: 'keys' are the keywords to search for, + // and 'text' is the text to assert is on the results page. $plugin_info = array( 'node_search' => array( - 'path' => 'node', - 'title' => 'Content', 'keys' => 'pizza', 'text' => $this->search_node->label(), ), 'user_search' => array( - 'path' => 'user', - 'title' => 'User', 'keys' => $this->search_user->getUsername(), 'text' => $this->search_user->getEmail(), ), - 'search_extra_type_search' => array( - 'path' => 'dummy_path', - 'title' => 'Dummy search type', + 'dummy_search_type' => array( 'keys' => 'foo', 'text' => 'Dummy search snippet to display', ), ); $plugins = array_keys($plugin_info); + /** @var $entities \Drupal\search\SearchPageInterface[] */ + $entities = entity_load_multiple('search_page'); + // Disable all of the search pages. + foreach ($entities as $entity) { + $entity->disable()->save(); + } // Test each plugin if it's enabled as the only search plugin. - foreach ($plugins as $plugin) { - // Enable the one plugin and disable other ones. - $info = $plugin_info[$plugin]; - $edit = array(); - foreach ($plugins as $other) { - $edit['active_plugins[' . $other . ']'] = (($other == $plugin) ? $plugin : FALSE); - } - $edit['default_plugin'] = $plugin; - $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration')); + foreach ($entities as $entity_id => $entity) { + // Set this as default. + $this->drupalGet("admin/config/search/settings/manage/$entity_id/set-default"); // Run a search from the correct search URL. - $this->drupalGet('search/' . $info['path'] . '/' . $info['keys']); - $this->assertNoText('no results', $info['title'] . ' search found results'); + $info = $plugin_info[$entity_id]; + $this->drupalGet('search/' . $entity->getPath() . '/' . $info['keys']); + $this->assertResponse(200); + $this->assertNoText('no results', $entity->label() . ' search found results'); $this->assertText($info['text'], 'Correct search text found'); - // Verify that other plugin search tab titles are not visible. + // Verify that other plugin search tab labels are not visible. foreach ($plugins as $other) { - if ($other != $plugin) { - $title = $plugin_info[$other]['title']; - $this->assertNoText($title, $title . ' search tab is not shown'); + if ($other != $entity_id) { + $label = $entities[$other]->label(); + $this->assertNoText($label, $label . ' search tab is not shown'); } } @@ -184,33 +169,194 @@ function testSearchModuleDisabling() { $this->drupalPostForm('node', $terms, t('Search')); $this->assertEqual( $this->getURL(), - url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)), + \Drupal::url('search.view_' . $entity->id(), array('keys' => $info['keys']), array('absolute' => TRUE)), 'Block redirected to right search page'); - // Try an invalid search path. Should redirect to our active plugin. + // Try an invalid search path, which should 404. $this->drupalGet('search/not_a_plugin_path'); - $this->assertEqual( - $this->getURL(), - url('search/' . $info['path'], array('absolute' => TRUE)), - 'Invalid search path redirected to default search page'); + $this->assertResponse(404); + + $entity->disable()->save(); } // Test with all search plugins enabled. When you go to the search // page or run search, all plugins should be shown. - $edit = array(); - foreach ($plugins as $plugin) { - $edit['active_plugins[' . $plugin . ']'] = $plugin; + foreach ($entities as $entity) { + $entity->enable()->save(); } - $edit['default_plugin'] = 'node_search'; - - $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration')); + // Set the node search as default. + $this->drupalGet('admin/config/search/settings/manage/node_search/set-default'); foreach (array('search/node/pizza', 'search/node') as $path) { $this->drupalGet($path); - foreach ($plugins as $plugin) { - $title = $plugin_info[$plugin]['title']; - $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title))); + foreach ($plugins as $entity_id) { + $label = $entities[$entity_id]->label(); + $this->assertText($label, format_string('%label search tab is shown', array('%label' => $label))); } } } + + /** + * Tests the ordering of search pages on a clean install. + */ + public function testDefaultSearchPageOrdering() { + $this->drupalGet('search'); + $elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary')); + $this->assertIdentical((string) $elements[0]['href'], url('search/node')); + $this->assertIdentical((string) $elements[1]['href'], url('search/user')); + } + + /** + * Tests multiple search pages of the same type. + */ + public function testMultipleSearchPages() { + $this->assertDefaultSearch('node_search', 'The default page is set to the installer default.'); + $search_storage = \Drupal::entityManager()->getStorageController('search_page'); + $entities = $search_storage->loadMultiple(); + $search_storage->delete($entities); + $this->assertDefaultSearch(FALSE); + + // Ensure that no search pages are configured. + $this->drupalGet('admin/config/search/settings'); + $this->assertText(t('No search pages have been configured.')); + + // Add a search page. + $edit = array(); + $edit['search_type'] = 'search_extra_type_search'; + $this->drupalPostForm(NULL, $edit, t('Add new page')); + $this->assertTitle('Add new search page | Drupal'); + + $first = array(); + $first['label'] = $this->randomString(); + $first_id = $first['id'] = strtolower($this->randomName(8)); + $first['path'] = strtolower($this->randomName(8)); + $this->drupalPostForm(NULL, $first, t('Add search page')); + $this->assertDefaultSearch($first_id, 'The default page matches the only search page.'); + $this->assertRaw(t('The %label search page has been added.', array('%label' => $first['label']))); + + // Attempt to add a search page with an existing path. + $edit = array(); + $edit['search_type'] = 'search_extra_type_search'; + $this->drupalPostForm(NULL, $edit, t('Add new page')); + $edit = array(); + $edit['label'] = $this->randomString(); + $edit['id'] = strtolower($this->randomName(8)); + $edit['path'] = $first['path']; + $this->drupalPostForm(NULL, $edit, t('Add search page')); + $this->assertText(t('The search page path must be unique.')); + + // Add a second search page. + $second = array(); + $second['label'] = $this->randomString(); + $second_id = $second['id'] = strtolower($this->randomName(8)); + $second['path'] = strtolower($this->randomName(8)); + $this->drupalPostForm(NULL, $second, t('Add search page')); + $this->assertDefaultSearch($first_id, 'The default page matches the only search page.'); + + // Ensure both search pages have their tabs displayed. + $this->drupalGet('search'); + $elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary')); + $this->assertIdentical((string) $elements[0]['href'], url('search/' . $first['path'])); + $this->assertIdentical((string) $elements[1]['href'], url('search/' . $second['path'])); + + // Switch the weight of the search pages and check the order of the tabs. + $edit = array( + 'entities[' . $first_id . '][weight]' => 10, + 'entities[' . $second_id . '][weight]' => -10, + ); + $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration')); + $this->drupalGet('search'); + $elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary')); + $this->assertIdentical((string) $elements[0]['href'], url('search/' . $second['path'])); + $this->assertIdentical((string) $elements[1]['href'], url('search/' . $first['path'])); + + // Check the initial state of the search pages. + $this->drupalGet('admin/config/search/settings'); + $this->verifySearchPageOperations($first_id, TRUE, FALSE, FALSE, FALSE); + $this->verifySearchPageOperations($second_id, TRUE, TRUE, TRUE, FALSE); + + // Change the default search page. + $this->clickLink(t('Set as default')); + $this->assertRaw(t('The default search page is now %label. Be sure to check the ordering of your search pages.', array('%label' => $second['label']))); + $this->verifySearchPageOperations($first_id, TRUE, TRUE, TRUE, FALSE); + $this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE); + + // Disable the first search page. + $this->clickLink(t('Disable')); + $this->assertResponse(200); + $this->assertNoLink(t('Disable')); + $this->verifySearchPageOperations($first_id, TRUE, TRUE, FALSE, TRUE); + $this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE); + + // Enable the first search page. + $this->clickLink(t('Enable')); + $this->assertResponse(200); + $this->verifySearchPageOperations($first_id, TRUE, TRUE, TRUE, FALSE); + $this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE); + + // Test deleting. + $this->clickLink(t('Delete')); + $this->assertRaw(t('Are you sure you want to delete the %label search page?', array('%label' => $first['label']))); + $this->drupalPostForm(NULL, array(), t('Delete')); + $this->assertRaw(t('The %label search page has been deleted.', array('%label' => $first['label']))); + $this->verifySearchPageOperations($first_id, FALSE, FALSE, FALSE, FALSE); + } + + /** + * Checks that the search page operations match expectations. + * + * @param string $id + * The search page ID to check. + * @param bool $edit + * Whether the edit link is expected. + * @param bool $delete + * Whether the delete link is expected. + * @param bool $disable + * Whether the disable link is expected. + * @param bool $enable + * Whether the enable link is expected. + */ + protected function verifySearchPageOperations($id, $edit, $delete, $disable, $enable) { + if ($edit) { + $this->assertLinkByHref("admin/config/search/settings/manage/$id"); + } + else { + $this->assertNoLinkByHref("admin/config/search/settings/manage/$id"); + } + if ($delete) { + $this->assertLinkByHref("admin/config/search/settings/manage/$id/delete"); + } + else { + $this->assertNoLinkByHref("admin/config/search/settings/manage/$id/delete"); + } + if ($disable) { + $this->assertLinkByHref("admin/config/search/settings/manage/$id/disable"); + } + else { + $this->assertNoLinkByHref("admin/config/search/settings/manage/$id/disable"); + } + if ($enable) { + $this->assertLinkByHref("admin/config/search/settings/manage/$id/enable"); + } + else { + $this->assertNoLinkByHref("admin/config/search/settings/manage/$id/enable"); + } + } + + /** + * Checks that the default search page matches expectations. + * + * @param string $expected + * The expected search page. + * @param string $message + * (optional) A message to display with the assertion. + * @param string $group + * (optional) The group this message is in. + */ + protected function assertDefaultSearch($expected, $message = '', $group = 'Other') { + /** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */ + $search_page_repository = \Drupal::service('search.search_page_repository'); + $this->assertIdentical($search_page_repository->getDefaultSearchPage(), $expected, $message, $group); + } + } diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php index 67deae3..cc9da27 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php @@ -10,11 +10,11 @@ class SearchRankingTest extends SearchTestBase { /** - * A node search plugin instance. + * The node search page. * - * @var \Drupal\search\Plugin\SearchInterface + * @var \Drupal\search\SearchPageInterface */ - protected $nodeSearchPlugin; + protected $nodeSearch; /** * Modules to enable. @@ -35,7 +35,7 @@ public function setUp() { parent::setUp(); // Create a plugin instance. - $this->nodeSearchPlugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $this->nodeSearch = entity_load('search_page', 'node_search'); } public function testRankings() { @@ -48,6 +48,7 @@ public function testRankings() { $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views'); // Create nodes for testing. + $nodes = array(); foreach ($node_ranks as $node_rank) { $settings = array( 'type' => 'page', @@ -80,12 +81,9 @@ public function testRankings() { } // Update the search index. - $this->nodeSearchPlugin->updateIndex(); + $this->nodeSearch->getPlugin()->updateIndex(); search_update_totals(); - // Refresh variables after the treatment. - $this->refreshVariables(); - // Add a comment to one of the nodes. $edit = array(); $edit['subject'] = 'my comment title'; @@ -115,35 +113,39 @@ public function testRankings() { array_pop($node_ranks); // Test that the settings form displays the context ranking section. - $this->drupalGet('admin/config/search/settings'); + $this->drupalGet('admin/config/search/settings/manage/node_search'); $this->assertText(t('Content ranking')); // Check that all rankings are visible and set to 0. foreach ($node_ranks as $node_rank) { - $this->assertTrue($this->xpath('//select[@id="edit-node-rank-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.'); + $this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.'); } // Test each of the possible rankings. $edit = array(); foreach ($node_ranks as $node_rank) { // Enable the ranking we are testing. - $edit['node_rank_' . $node_rank] = 10; - $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration')); - $this->assertTrue($this->xpath('//select[@id="edit-node-rank-' . $node_rank . '"]//option[@value="10"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 10.'); + $edit['rankings_' . $node_rank] = 10; + $this->drupalPostForm('admin/config/search/settings/manage/node_search', $edit, t('Save search page')); + $this->drupalGet('admin/config/search/settings/manage/node_search'); + $this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '"]//option[@value="10"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 10.'); + // Reload the plugin to get the up-to-date values. + $this->nodeSearch = entity_load('search_page', 'node_search'); // Do the search and assert the results. - $this->nodeSearchPlugin->setSearch('rocks', array(), array()); - $set = $this->nodeSearchPlugin->execute(); + $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array()); + $set = $this->nodeSearch->getPlugin()->execute(); $this->assertEqual($set[0]['node']->id(), $nodes[$node_rank][1]->id(), 'Search ranking "' . $node_rank . '" order.'); // Clear this ranking for the next test. - $edit['node_rank_' . $node_rank] = 0; + $edit['rankings_' . $node_rank] = 0; } // Save the final node_rank change then check that all rankings are visible // and have been set back to 0. - $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration')); + $this->drupalPostForm('admin/config/search/settings/manage/node_search', $edit, t('Save search page')); + $this->drupalGet('admin/config/search/settings/manage/node_search'); foreach ($node_ranks as $node_rank) { - $this->assertTrue($this->xpath('//select[@id="edit-node-rank-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.'); + $this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.'); } } @@ -170,6 +172,7 @@ public function testHTMLRankings() { 'type' => 'page', 'title' => 'Simple node', ); + $nodes = array(); foreach ($shuffled_tags as $tag) { switch ($tag) { case 'a': @@ -186,20 +189,12 @@ public function testHTMLRankings() { } // Update the search index. - $this->nodeSearchPlugin->updateIndex(); + $this->nodeSearch->getPlugin()->updateIndex(); search_update_totals(); - // Refresh variables after the treatment. - $this->refreshVariables(); - - // Disable all other rankings. - $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views'); - foreach ($node_ranks as $node_rank) { - variable_set('node_rank_' . $node_rank, 0); - } - $this->nodeSearchPlugin->setSearch('rocks', array(), array()); + $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array()); // Do the search and assert the results. - $set = $this->nodeSearchPlugin->execute(); + $set = $this->nodeSearch->getPlugin()->execute(); // Test the ranking of each tag. foreach ($sorted_tags as $tag_rank => $tag) { @@ -218,14 +213,12 @@ public function testHTMLRankings() { $node = $this->drupalCreateNode($settings); // Update the search index. - $this->nodeSearchPlugin->updateIndex(); + $this->nodeSearch->getPlugin()->updateIndex(); search_update_totals(); - // Refresh variables after the treatment. - $this->refreshVariables(); - $this->nodeSearchPlugin->setSearch('rocks', array(), array()); + $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array()); // Do the search and assert the results. - $set = $this->nodeSearchPlugin->execute(); + $set = $this->nodeSearch->getPlugin()->execute(); // Ranking should always be second to last. $set = array_slice($set, -2, 1); @@ -247,35 +240,36 @@ function testDoubleRankings() { // Login with sufficient privileges. $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content'))); - // See testRankings() above - build a node that will rank high for sticky. + // Create two nodes that will match the search, one that is sticky. $settings = array( 'type' => 'page', 'title' => 'Drupal rocks', 'body' => array(array('value' => "Drupal's search rocks")), - 'sticky' => 1, ); - + $this->drupalCreateNode($settings); + $settings['sticky'] = 1; $node = $this->drupalCreateNode($settings); // Update the search index. - $this->nodeSearchPlugin->updateIndex(); + $this->nodeSearch->getPlugin()->updateIndex(); search_update_totals(); - // Refresh variables after the treatment. - $this->refreshVariables(); - // Set up for ranking sticky and lots of comments; make sure others are // disabled. $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views'); + $configuration = $this->nodeSearch->getPlugin()->getConfiguration(); foreach ($node_ranks as $var) { $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0; - variable_set('node_rank_' . $var, $value); + $configuration['rankings'][$var] = $value; } + $this->nodeSearch->getPlugin()->setConfiguration($configuration); + $this->nodeSearch->save(); // Do the search and assert the results. - $this->nodeSearchPlugin->setSearch('rocks', array(), array()); + $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array()); // Do the search and assert the results. - $set = $this->nodeSearchPlugin->execute(); + $set = $this->nodeSearch->getPlugin()->execute(); $this->assertEqual($set[0]['node']->id(), $node->id(), 'Search double ranking order.'); } + } diff --git a/core/modules/search/search.install b/core/modules/search/search.install index 824a624..955bfed 100644 --- a/core/modules/search/search.install +++ b/core/modules/search/search.install @@ -170,7 +170,7 @@ function _search_update_8000_modules_mapto_plugins(array $map) { } $default_module = update_variable_get('search_default_module', 'node'); if (isset($map[$default_module])) { - $config->set('default_plugin', $map[$default_module]); + $config->set('default_page', $map[$default_module]); update_variable_del('search_default_module'); } $config->save(); diff --git a/core/modules/search/search.module b/core/modules/search/search.module index ca160bf..551f435 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -5,10 +5,7 @@ * Enables site-wide keyword searching. */ -use Drupal\Core\Entity\EntityInterface; use Drupal\Component\Utility\Unicode; -use Drupal\search\SearchExpression; -use Drupal\search\Plugin\SearchInterface; /** * Matches all 'N' Unicode character classes (numbers) @@ -171,23 +168,6 @@ function search_menu() { } /** - * Returns information about the default search plugin. - * - * @return array - * The search plugin definition for the default search plugin, if any. - */ -function search_get_default_plugin_info() { - $info = \Drupal::service('plugin.manager.search')->getActiveDefinitions(); - $default = \Drupal::config('search.settings')->get('default_plugin'); - if (isset($info[$default])) { - return $info[$default]; - } - // The config setting does not match any active plugin, so just return - // the info for the first active plugin (if any). - return reset($info); -} - -/** * Clears either a part of, or the entire search index. * * @param $sid @@ -201,12 +181,14 @@ function search_get_default_plugin_info() { * (optional) Boolean to specify whether reindexing happens. * @param $langcode * (optional) Language code for the operation. If not provided, all - * index records for the $sid will be deleted. + * index records for the $sid and $type will be deleted. */ function search_reindex($sid = NULL, $type = NULL, $reindex = FALSE, $langcode = NULL) { if ($type == NULL && $sid == NULL) { - foreach (\Drupal::service('plugin.manager.search')->getActiveIndexingPlugins() as $plugin) { - $plugin->resetIndex(); + /** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */ + $search_page_repository = \Drupal::service('search.search_page_repository'); + foreach ($search_page_repository->getIndexableSearchPages() as $entity) { + $entity->getPlugin()->resetIndex(); } } else { @@ -247,7 +229,8 @@ function search_dirty($word = NULL) { /** * Implements hook_cron(). * - * Fires updateIndex() in all plugins and cleans up dirty words. + * Fires updateIndex() in the plugins for all indexable active search pages, + * and cleans up dirty words. * * @see search_dirty() */ @@ -256,8 +239,10 @@ function search_cron() { // to date. drupal_register_shutdown_function('search_update_totals'); - foreach (\Drupal::service('plugin.manager.search')->getActiveIndexingPlugins() as $plugin) { - $plugin->updateIndex(); + /** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */ + $search_page_repository = \Drupal::service('search.search_page_repository'); + foreach ($search_page_repository->getIndexableSearchPages() as $entity) { + $entity->getPlugin()->updateIndex(); } } @@ -598,7 +583,8 @@ function search_mark_for_reindex($type, $sid) { * * To be discovered, the plugins must implement * \Drupal\search\Plugin\SearchInterface and be annotated as - * \Drupal\search\Annotation\SearchPlugin plugins. + * \Drupal\search\Annotation\SearchPlugin plugins. Defining a plugin will allow + * administrators to set up one or more search pages using this plugin. * * There are three ways to interact with the search system: * - Specifically for searching nodes, you can implement @@ -609,10 +595,11 @@ function search_mark_for_reindex($type, $sid) { * additional, non-visible data to be indexed. * - Define a plugin implementing \Drupal\search\Plugin\SearchInterface and * annotated as \Drupal\search\Annotation\SearchPlugin. This will create a - * search tab for your plugin on the /search page with a simple keyword - * search form. You will also need to implement the execute() method - * from the interface to perform the search. A base class is provided in - * \Drupal\search\Plugin\SearchPluginBase. + * search page type that users can use to set up one or more search pages. + * Each of these corresponds to a tab on the /search page, which can be + * used to perform searches. You will also need to implement the execute() + * method from the interface to perform the search. A base class is provided + * in \Drupal\search\Plugin\SearchPluginBase. * * If your module needs to provide a more complicated search form, then you * need to implement it yourself. In that case, you may wish to define it as a @@ -851,3 +838,11 @@ function _search_find_match_with_simplify($key, $text, $boundary, $langcode = NU // If we get here, we couldn't find a match. return NULL; } + +/** + * Implements hook_module_preinstall(). + */ +function search_module_preinstall() { + // @todo Remove in https://drupal.org/node/2155635. + \Drupal::service('plugin.manager.search')->clearCachedDefinitions(); +} diff --git a/core/modules/search/search.routing.yml b/core/modules/search/search.routing.yml index a3247bc..8cdec79 100644 --- a/core/modules/search/search.routing.yml +++ b/core/modules/search/search.routing.yml @@ -1,7 +1,7 @@ search.settings: path: '/admin/config/search/settings' defaults: - _form: '\Drupal\search\Form\SearchSettingsForm' + _entity_list: 'search_page' _title: 'Search settings' requirements: _permission: 'administer search' @@ -14,17 +14,51 @@ search.reindex_confirm: requirements: _permission: 'administer search' -search.view: - path: '/search/{plugin_id}' +search.add_type: + path: '/admin/config/search/settings/add/{search_plugin_id}' defaults: - _title: 'Search' - _content: '\Drupal\search\Controller\SearchController::view' - plugin_id: NULL - keys: '' + _entity_form: 'search_page.add' + _title: 'Add new search page' requirements: - keys: '.+' - _permission: 'search content' - _search_access: 'TRUE' + _entity_create_access: 'search_page' + +search.edit: + path: '/admin/config/search/settings/manage/{search_page}' + defaults: + _entity_form: 'search_page.edit' + _title_callback: '\Drupal\search\Controller\SearchController::editTitle' + requirements: + _entity_access: 'search_page.update' + +search.enable: + path: '/admin/config/search/settings/manage/{search_page}/enable' + defaults: + _controller: '\Drupal\search\Controller\SearchController::performOperation' + op: 'enable' + requirements: + _entity_access: 'search_page.update' + +search.disable: + path: '/admin/config/search/settings/manage/{search_page}/disable' + defaults: + _controller: '\Drupal\search\Controller\SearchController::performOperation' + op: 'disable' + requirements: + _entity_access: 'search_page.disable' + +search.set_default: + path: '/admin/config/search/settings/manage/{search_page}/set-default' + defaults: + _controller: '\Drupal\search\Controller\SearchController::setAsDefault' + requirements: + _entity_access: 'search_page.update' + +search.delete: + path: '/admin/config/search/settings/manage/{search_page}/delete' + defaults: + _entity_form: 'search_page.delete' + requirements: + _entity_access: 'search_page.delete' route_callbacks: - - '\Drupal\search\Routing\SearchPluginRoutes::routes' + - '\Drupal\search\Routing\SearchPageRoutes::routes' diff --git a/core/modules/search/search.services.yml b/core/modules/search/search.services.yml index 543bd09..a92d161 100644 --- a/core/modules/search/search.services.yml +++ b/core/modules/search/search.services.yml @@ -1,16 +1,8 @@ services: plugin.manager.search: class: Drupal\search\SearchPluginManager - arguments: ['@container.namespaces', '@config.factory', '@cache.cache', '@language_manager'] + parent: default_plugin_manager - access_check.search: - class: Drupal\search\Access\SearchAccessCheck - arguments: ['@plugin.manager.search'] - tags: - - { name: access_check, applies_to: _search_access } - - access_check.search_plugin: - class: Drupal\search\Access\SearchPluginAccessCheck - arguments: ['@plugin.manager.search'] - tags: - - { name: access_check, applies_to: _search_plugin_view_access } + search.search_page_repository: + class: Drupal\search\SearchPageRepository + arguments: ['@config.factory', '@entity.manager'] diff --git a/core/modules/search/tests/Drupal/search/Tests/SearchPageRepositoryTest.php b/core/modules/search/tests/Drupal/search/Tests/SearchPageRepositoryTest.php new file mode 100644 index 0000000..b054b71 --- /dev/null +++ b/core/modules/search/tests/Drupal/search/Tests/SearchPageRepositoryTest.php @@ -0,0 +1,303 @@ + 'Search page repository test', + 'description' => 'Tests methods on the \Drupal\search\SearchPageRepository class', + 'group' => 'Search', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + $this->query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface'); + + $this->storage = $this->getMockBuilder('Drupal\Core\Config\Entity\ConfigStorageController') + ->disableOriginalConstructor() + ->getMock(); + $this->storage->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($this->query)); + + $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $entity_manager->expects($this->any()) + ->method('getStorageController') + ->will($this->returnValue($this->storage)); + + $this->configFactory = $this->getMockBuilder('Drupal\Core\Config\ConfigFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->searchPageRepository = new SearchPageRepository($this->configFactory, $entity_manager); + } + + /** + * Tests the getActiveSearchPages() method. + */ + public function testGetActiveSearchPages() { + $this->query->expects($this->once()) + ->method('condition') + ->with('status', TRUE) + ->will($this->returnValue($this->query)); + $this->query->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test'))); + + $entities = array(); + $entities['test'] = $this->getMock('Drupal\search\SearchPageInterface'); + $entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface'); + $this->storage->expects($this->once()) + ->method('loadMultiple') + ->with(array('test' => 'test', 'other_test' => 'other_test')) + ->will($this->returnValue($entities)); + + $result = $this->searchPageRepository->getActiveSearchPages(); + $this->assertSame($entities, $result); + } + + /** + * Tests the isSearchActive() method. + */ + public function testIsSearchActive() { + $this->query->expects($this->once()) + ->method('condition') + ->with('status', TRUE) + ->will($this->returnValue($this->query)); + $this->query->expects($this->once()) + ->method('range') + ->with(0, 1) + ->will($this->returnValue($this->query)); + $this->query->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array('test' => 'test'))); + + $this->assertSame(TRUE, $this->searchPageRepository->isSearchActive()); + } + + /** + * Tests the getIndexableSearchPages() method. + */ + public function testGetIndexableSearchPages() { + $this->query->expects($this->once()) + ->method('condition') + ->with('status', TRUE) + ->will($this->returnValue($this->query)); + $this->query->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test'))); + + $entities = array(); + $entities['test'] = $this->getMock('Drupal\search\SearchPageInterface'); + $entities['test']->expects($this->once()) + ->method('isIndexable') + ->will($this->returnValue(TRUE)); + $entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface'); + $entities['other_test']->expects($this->once()) + ->method('isIndexable') + ->will($this->returnValue(FALSE)); + $this->storage->expects($this->once()) + ->method('loadMultiple') + ->with(array('test' => 'test', 'other_test' => 'other_test')) + ->will($this->returnValue($entities)); + + $result = $this->searchPageRepository->getIndexableSearchPages(); + $this->assertCount(1, $result); + $this->assertSame($entities['test'], reset($result)); + } + + /** + * Tests the clearDefaultSearchPage() method. + */ + public function testClearDefaultSearchPage() { + $config = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config->expects($this->once()) + ->method('clear') + ->with('default_page') + ->will($this->returnValue($config)); + $this->configFactory->expects($this->once()) + ->method('get') + ->with('search.settings') + ->will($this->returnValue($config)); + $this->searchPageRepository->clearDefaultSearchPage(); + } + + /** + * Tests the getDefaultSearchPage() method when the default is active. + */ + public function testGetDefaultSearchPageWithActiveDefault() { + $this->query->expects($this->once()) + ->method('condition') + ->with('status', TRUE) + ->will($this->returnValue($this->query)); + $this->query->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test'))); + + $config = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config->expects($this->once()) + ->method('get') + ->with('default_page') + ->will($this->returnValue('test')); + $this->configFactory->expects($this->once()) + ->method('get') + ->with('search.settings') + ->will($this->returnValue($config)); + + $this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage()); + } + + /** + * Tests the getDefaultSearchPage() method when the default is inactive. + */ + public function testGetDefaultSearchPageWithInactiveDefault() { + $this->query->expects($this->once()) + ->method('condition') + ->with('status', TRUE) + ->will($this->returnValue($this->query)); + $this->query->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array('test' => 'test'))); + + $config = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config->expects($this->once()) + ->method('get') + ->with('default_page') + ->will($this->returnValue('other_test')); + $this->configFactory->expects($this->once()) + ->method('get') + ->with('search.settings') + ->will($this->returnValue($config)); + + $this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage()); + } + + /** + * Tests the setDefaultSearchPage() method. + */ + public function testSetDefaultSearchPage() { + $id = 'bananas'; + $config = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config->expects($this->once()) + ->method('set') + ->with('default_page', $id) + ->will($this->returnValue($config)); + $config->expects($this->once()) + ->method('save') + ->will($this->returnValue($config)); + $this->configFactory->expects($this->once()) + ->method('get') + ->with('search.settings') + ->will($this->returnValue($config)); + + $search_page = $this->getMock('Drupal\search\SearchPageInterface'); + $search_page->expects($this->once()) + ->method('id') + ->will($this->returnValue($id)); + $search_page->expects($this->once()) + ->method('enable') + ->will($this->returnValue($search_page)); + $search_page->expects($this->once()) + ->method('save') + ->will($this->returnValue($search_page)); + $this->searchPageRepository->setDefaultSearchPage($search_page); + } + + /** + * Tests the sortSearchPages() method. + */ + public function testSortSearchPages() { + $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface'); + $entity_type->expects($this->any()) + ->method('getClass') + ->will($this->returnValue('Drupal\search\Tests\TestSearchPage')); + $this->storage->expects($this->once()) + ->method('entityInfo') + ->will($this->returnValue($entity_type)); + + // Declare entities out of their expected order so we can be sure they were + // sorted. We cannot mock these because of uasort(), see + // https://bugs.php.net/bug.php?id=50688. + $unsorted_entities['test4'] = new TestSearchPage(array('weight' => 0, 'status' => FALSE, 'label' => 'Test4')); + $unsorted_entities['test3'] = new TestSearchPage(array('weight' => 10, 'status' => TRUE, 'label' => 'Test3')); + $unsorted_entities['test2'] = new TestSearchPage(array('weight' => 0, 'status' => TRUE, 'label' => 'Test2')); + $unsorted_entities['test1'] = new TestSearchPage(array('weight' => 0, 'status' => TRUE, 'label' => 'Test1')); + $expected = $unsorted_entities; + ksort($expected); + + $sorted_entities = $this->searchPageRepository->sortSearchPages($unsorted_entities); + $this->assertSame($expected, $sorted_entities); + } + +} + +class TestSearchPage extends SearchPage { + public function __construct(array $values) { + foreach ($values as $key => $value) { + $this->$key = $value; + } + } + public function label($langcode = NULL) { + return $this->label; + } +} diff --git a/core/modules/search/tests/Drupal/search/Tests/SearchPluginBagTest.php b/core/modules/search/tests/Drupal/search/Tests/SearchPluginBagTest.php new file mode 100644 index 0000000..5ee70f0 --- /dev/null +++ b/core/modules/search/tests/Drupal/search/Tests/SearchPluginBagTest.php @@ -0,0 +1,90 @@ + 'Search plugin bag test', + 'description' => 'Tests the \Drupal\search\Plugin\SearchPluginBag class', + 'group' => 'Search', + ); + } + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->pluginManager = $this->getMock('Drupal\Component\Plugin\PluginManagerInterface'); + $this->searchPluginBag = new SearchPluginBag($this->pluginManager, array('banana'), array('id' => 'banana', 'color' => 'yellow'), 'fruit_stand'); + } + + /** + * Tests the get() method. + */ + public function testGet() { + $plugin = $this->getMock('Drupal\search\Plugin\SearchInterface'); + $this->pluginManager->expects($this->once()) + ->method('createInstance') + ->will($this->returnValue($plugin)); + $this->assertSame($plugin, $this->searchPluginBag->get('banana')); + } + + /** + * Tests the get() method with a configurable plugin. + */ + public function testGetWithConfigurablePlugin() { + $plugin = $this->getMock('Drupal\search\Plugin\ConfigurableSearchPluginInterface'); + $plugin->expects($this->once()) + ->method('setSearchPageId') + ->with('fruit_stand') + ->will($this->returnValue($plugin)); + + $this->pluginManager->expects($this->once()) + ->method('createInstance') + ->will($this->returnValue($plugin)); + + $this->assertSame($plugin, $this->searchPluginBag->get('banana')); + } + +} diff --git a/core/modules/search/tests/modules/search_extra_type/config/schema/search_extra_type.schema.yml b/core/modules/search/tests/modules/search_extra_type/config/schema/search_extra_type.schema.yml new file mode 100644 index 0000000..9a98361 --- /dev/null +++ b/core/modules/search/tests/modules/search_extra_type/config/schema/search_extra_type.schema.yml @@ -0,0 +1,10 @@ +# Schema for the configuration files of the Search Extra Type module. + +# Plugin \Drupal\search_extra_type\Plugin\Search\SearchExtraTypeSearch +search.plugin.search_extra_type_search: + type: mapping + label: 'Extra type settings' + mapping: + boost: + type: string + label: 'Boost method' diff --git a/core/modules/search/tests/modules/search_extra_type/config/search.page.dummy_search_type.yml b/core/modules/search/tests/modules/search_extra_type/config/search.page.dummy_search_type.yml new file mode 100644 index 0000000..e60478c --- /dev/null +++ b/core/modules/search/tests/modules/search_extra_type/config/search.page.dummy_search_type.yml @@ -0,0 +1,8 @@ +id: dummy_search_type +label: 'Dummy search type' +uuid: b55858d4-f428-474c-8200-ef47a4597aef +status: true +langcode: en +path: dummy_path +plugin: search_extra_type_search +configuration: { } diff --git a/core/modules/search/tests/modules/search_extra_type/config/search_extra_type.settings.yml b/core/modules/search/tests/modules/search_extra_type/config/search_extra_type.settings.yml deleted file mode 100644 index db64246..0000000 --- a/core/modules/search/tests/modules/search_extra_type/config/search_extra_type.settings.yml +++ /dev/null @@ -1 +0,0 @@ -boost: bi diff --git a/core/modules/search/tests/modules/search_extra_type/lib/Drupal/search_extra_type/Plugin/Search/SearchExtraTypeSearch.php b/core/modules/search/tests/modules/search_extra_type/lib/Drupal/search_extra_type/Plugin/Search/SearchExtraTypeSearch.php index e30b10e..920f14b 100644 --- a/core/modules/search/tests/modules/search_extra_type/lib/Drupal/search_extra_type/Plugin/Search/SearchExtraTypeSearch.php +++ b/core/modules/search/tests/modules/search_extra_type/lib/Drupal/search_extra_type/Plugin/Search/SearchExtraTypeSearch.php @@ -7,55 +7,17 @@ namespace Drupal\search_extra_type\Plugin\Search; -use Drupal\Core\Config\Config; -use Drupal\Core\Plugin\PluginFormInterface; -use Drupal\search\Plugin\SearchPluginBase; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\search\Plugin\ConfigurableSearchPluginBase; /** * Executes a keyword search against the search index. * * @SearchPlugin( * id = "search_extra_type_search", - * title = @Translation("Dummy search type"), - * path = "dummy_path" + * title = @Translation("Dummy search type") * ) */ -class SearchExtraTypeSearch extends SearchPluginBase implements PluginFormInterface { - - /** - * @var \Drupal\Core\Config\Config - */ - protected $configSettings; - - /** - * {@inheritdoc} - */ - static public function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { - return new static( - $container->get('config.factory')->get('search_extra_type.settings'), - $configuration, - $plugin_id, - $plugin_definition - ); - } - - /** - * Creates a SearchExtraTypeSearch object. - * - * @param Config $config_settings - * The extra config settings. - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param array $plugin_definition - * The plugin implementation definition. - */ - public function __construct(Config $config_settings, array $configuration, $plugin_id, array $plugin_definition) { - $this->configSettings = $config_settings; - parent::__construct($configuration, $plugin_id, $plugin_definition); - } +class SearchExtraTypeSearch extends ConfigurableSearchPluginBase { /** * {@inheritdoc} @@ -138,7 +100,7 @@ public function buildConfigurationForm(array $form, array &$form_state) { 'bi' => t('Bistromathic'), 'ii' => t('Infinite Improbability'), ), - '#default_value' => $this->configSettings->get('boost'), + '#default_value' => $this->configuration['boost'], ); return $form; } @@ -146,16 +108,17 @@ public function buildConfigurationForm(array $form, array &$form_state) { /** * {@inheritdoc} */ - public function validateConfigurationForm(array &$form, array &$form_state) { + public function submitConfigurationForm(array &$form, array &$form_state) { + $this->configuration['boost'] = $form_state['values']['extra_type_settings']['boost']; } /** * {@inheritdoc} */ - public function submitConfigurationForm(array &$form, array &$form_state) { - $this->configSettings - ->set('boost', $form_state['values']['extra_type_settings']['boost']) - ->save(); + public function defaultConfiguration() { + return array( + 'boost' => 'bi', + ); } } diff --git a/core/modules/user/config/search.page.user_search.yml b/core/modules/user/config/search.page.user_search.yml new file mode 100644 index 0000000..4823ec4 --- /dev/null +++ b/core/modules/user/config/search.page.user_search.yml @@ -0,0 +1,8 @@ +id: user_search +label: 'Users' +uuid: c0d6b9a7-09a7-415f-b71a-26957bef635c +status: true +langcode: en +path: user +plugin: user_search +configuration: { } diff --git a/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php index ea5f417..518d9c0 100644 --- a/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php +++ b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php @@ -14,15 +14,13 @@ use Drupal\Core\Access\AccessibleInterface; use Drupal\search\Plugin\SearchPluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; /** * Executes a keyword search for users against the {users} database table. * * @SearchPlugin( * id = "user_search", - * title = @Translation("Users"), - * path = "user" + * title = @Translation("Users") * ) */ class UserSearch extends SearchPluginBase implements AccessibleInterface {