diff --git a/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php new file mode 100644 index 0000000..6c194d4 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php @@ -0,0 +1,336 @@ +get('database'), + $container->get('plugin.manager.entity'), + $container->get('module_handler'), + $container->get('config.factory')->get('search.settings'), + $container->get('keyvalue')->get('state') + ); + } + + /** + * Constructs a \Drupal\node\Plugin\Search\NodeSearch object. + * + * @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. + * @param \Drupal\Core\Database\Connection $database + * A database connection object. + * @param \Drupal\Core\Entity\EntityManager $entity_manager + * An entity manager object. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * A module manager object. + * @param \Drupal\Core\Config\Config $search_settings + * A config object for 'search.settings'. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state + * The Drupal state object used to set 'node.cron_last'. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, Connection $database, EntityManager $entity_manager, ModuleHandlerInterface $module_handler, Config $search_settings, KeyValueStoreInterface $state) { + $this->database = $database; + $this->entityManager = $entity_manager; + $this->moduleHandler = $module_handler; + $this->searchSettings = $search_settings; + $this->state = $state; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function execute() { + $results = array(); + if (!$this->isSearchExecutable()) { + return $results; + } + $keys = $this->keywords; + // Build matching conditions + $query = $this->database + ->select('search_index', 'i', array('target' => 'slave')) + ->extend('Drupal\search\SearchQuery') + ->extend('Drupal\Core\Database\Query\PagerSelectExtender'); + $query->join('node_field_data', 'n', 'n.nid = i.sid'); + $query->condition('n.status', 1) + ->addTag('node_access') + ->searchExpression($keys, 'node'); + + // Insert special keywords. + $query->setOption('type', 'n.type'); + $query->setOption('langcode', 'n.langcode'); + if ($query->setOption('term', 'ti.tid')) { + $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid'); + } + // Only continue if the first pass query matches. + if (!$query->executeFirstPass()) { + return array(); + } + + // Add the ranking expressions. + $this->addNodeRankings($query); + + // Load results. + $find = $query + // Add the language code of the indexed item to the result of the query, + // since the node will be rendered using the respective language. + ->fields('i', array('langcode')) + ->limit(10) + ->execute(); + + $node_storage = $this->entityManager->getStorageController('node'); + $node_render = $this->entityManager->getRenderController('node'); + + foreach ($find as $item) { + // Render the node. + $entities = $node_storage->loadMultiple(array($item->sid)); + // Convert to BCEntity to match node_load_multiple(). + // @todo - remove this when code that receives this object is updated. + $node = $entities[$item->sid]->getBCEntity(); + $build = $node_render->view($node, 'search_result', $item->langcode); + unset($build['#theme']); + $node->rendered = drupal_render($build); + + // Fetch comments for snippet. + $node->rendered .= ' ' . $this->moduleHandler->invoke('comment', 'node_update_index', array($node, $item->langcode)); + + $extra = $this->moduleHandler->invokeAll('node_search_result', array($node, $item->langcode)); + + $language = $this->moduleHandler->invoke('language', 'load', array($item->langcode)); + $uri = $node->uri(); + $username = array( + '#theme' => 'username', + '#account' => $this->entityManager->getStorageController('user')->load($node->uid), + ); + $results[] = array( + 'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE, 'language' => $language))), + 'type' => check_plain($this->entityManager->getStorageController('node_type')->load($node->bundle())->label()), + 'title' => $node->label($item->langcode), + 'user' => drupal_render($username), + 'date' => $node->getChangedTime(), + 'node' => $node, + 'extra' => $extra, + 'score' => $item->calculated_score, + 'snippet' => search_excerpt($keys, $node->rendered, $item->langcode), + 'langcode' => $node->language()->id, + ); + } + return $results; + } + + /** + * Gathers the rankings from the the hook_ranking() implementations. + * + * @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')) { + $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 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']); + } + $arguments = isset($values['arguments']) ? $values['arguments'] : array(); + $query->addScore($values['score'], $arguments, $node_rank); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function updateIndex() { + $limit = (int) $this->searchSettings->get('index.cron_limit'); + + $result = $this->database->queryRange("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit, array(), array('target' => 'slave')); + $nids = $result->fetchCol(); + if (!$nids) { + return; + } + + // The indexing throttle should be aware of the number of language variants + // of a node. + $counter = 0; + $node_storage = $this->entityManager->getStorageController('node'); + foreach ($node_storage->loadMultiple($nids) as $entity) { + // Convert to BCEntity to match node_load_multiple(). + // @todo - remove this when hooks passed this object are updated. + $node = $entity->getBCEntity(); + // Determine when the maximum number of indexable items is reached. + $counter += count($node->getTranslationLanguages()); + if ($counter > $limit) { + break; + } + $this->indexNode($node); + } + } + + /** + * Indexes a single node. + * + * @param \Drupal\Core\Entity\EntityInterface $node + * The node to index. + */ + protected function indexNode(EntityInterface $node) { + // Save the changed time of the most recent indexed node, for the search + // results half-life calculation. + $this->state->set('node.cron_last', $node->getChangedTime()); + + $languages = $node->getTranslationLanguages(); + + foreach ($languages as $language) { + // Render the node. + $build = $this->moduleHandler->invoke('node', 'view', array($node, 'search_index', $language->id)); + + unset($build['#theme']); + $node->rendered = drupal_render($build); + + $text = '

' . check_plain($node->label($language->id)) . '

' . $node->rendered; + + // Fetch extra data normally not visible. + $extra = $this->moduleHandler->invokeAll('node_update_index', array($node, $language->id)); + foreach ($extra as $t) { + $text .= $t; + } + + // Update index. + $this->moduleHandler->invoke('search', 'index', array($node->id(), 'node', $text, $language->id)); + } + } + + /** + * {@inheritdoc} + */ + public function resetIndex() { + $this->database->update('search_dataset') + ->fields(array('reindex' => REQUEST_TIME)) + ->condition('type', 'node') + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function indexStatus() { + $total = $this->database->query('SELECT COUNT(*) FROM {node}')->fetchField(); + $remaining = $this->database->query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0")->fetchField(); + return array('remaining' => $remaining, 'total' => $total); + } + + /** + * {@inheritdoc} + */ + public function addToAdminForm(array &$form, array &$form_state) { + // Output form for defining rank factor weights. + $form['content_ranking'] = array( + '#type' => 'details', + '#title' => t('Content ranking'), + ); + $form['content_ranking']['#theme'] = 'node_search_admin'; + $form['content_ranking']['info'] = array( + '#value' => '' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property 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( + '#title' => $values['title'], + '#type' => 'select', + '#options' => $options, + '#default_value' => variable_get('node_rank_' . $var, 0), + ); + } + } + + /** + * {@inheritdoc} + */ + public function submitAdminForm(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]); + } + } + } + +} diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index 2166391..ce240bc 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -107,7 +107,7 @@ * - Resulting node is prepared for viewing (see Viewing a single node above) * - comment_node_update_index() is called. * - hook_node_search_result() (all) - * - Search indexing (calling node_update_index()): + * - Search indexing (calling updateIndex() on the 'node_search' plugin): * - Node is loaded (see Loading section above) * - Node is prepared for viewing (see Viewing a single node above) * - hook_node_update_index() (all) diff --git a/core/modules/node/node.module b/core/modules/node/node.module index d0a928b..ae14496 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -9,12 +9,9 @@ */ use Drupal\Core\Language\Language; -use Drupal\node\NodeInterface; use Symfony\Component\HttpFoundation\Response; - use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Query\AlterableInterface; -use Drupal\Core\Database\Query\SelectExtender; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\node\NodeTypeInterface; @@ -815,38 +812,6 @@ function node_permission() { } /** - * Gathers the rankings from the the hook_ranking() implementations. - * - * @param $query - * A query object that has been extended with the Search DB Extender. - */ -function _node_rankings(SelectExtender $query) { - if ($ranking = module_invoke_all('ranking')) { - $tables = &$query->getTables(); - foreach ($ranking as $rank => $values) { - if ($node_rank = variable_get('node_rank_' . $rank, 0)) { - // 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']); - } - $arguments = isset($values['arguments']) ? $values['arguments'] : array(); - $query->addScore($values['score'], $arguments, $node_rank); - } - } - } -} - -/** - * Implements hook_search_info(). - */ -function node_search_info() { - return array( - 'title' => 'Content', - 'path' => 'node', - ); -} - -/** * Implements hook_search_access(). */ function node_search_access() { @@ -854,122 +819,6 @@ function node_search_access() { } /** - * Implements hook_search_reset(). - */ -function node_search_reset() { - db_update('search_dataset') - ->fields(array('reindex' => REQUEST_TIME)) - ->condition('type', 'node') - ->execute(); -} - -/** - * Implements hook_search_status(). - */ -function node_search_status() { - $total = db_query('SELECT COUNT(*) FROM {node}')->fetchField(); - $remaining = db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0")->fetchField(); - return array('remaining' => $remaining, 'total' => $total); -} - -/** - * Implements hook_search_admin(). - */ -function node_search_admin() { - // Output form for defining rank factor weights. - $form['content_ranking'] = array( - '#type' => 'details', - '#title' => t('Content ranking'), - ); - $form['content_ranking']['#theme'] = 'node_search_admin'; - $form['content_ranking']['info'] = array( - '#value' => '' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property 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 (module_invoke_all('ranking') as $var => $values) { - $form['content_ranking']['factors']['node_rank_' . $var] = array( - '#title' => $values['title'], - '#type' => 'select', - '#options' => $options, - '#default_value' => variable_get('node_rank_' . $var, 0), - ); - } - return $form; -} - -/** - * Implements hook_search_execute(). - */ -function node_search_execute($keys = NULL, $conditions = NULL) { - // Build matching conditions - $query = db_select('search_index', 'i', array('target' => 'slave')) - ->extend('Drupal\search\SearchQuery') - ->extend('Drupal\Core\Database\Query\PagerSelectExtender'); - $query->join('node_field_data', 'n', 'n.nid = i.sid'); - $query - ->condition('n.status', 1) - ->addTag('node_access') - ->searchExpression($keys, 'node'); - - // Insert special keywords. - $query->setOption('type', 'n.type'); - $query->setOption('langcode', 'n.langcode'); - if ($query->setOption('term', 'ti.tid')) { - $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid'); - } - // Only continue if the first pass query matches. - if (!$query->executeFirstPass()) { - return array(); - } - - // Add the ranking expressions. - _node_rankings($query); - - // Load results. - $find = $query - // Add the language code of the indexed item to the result of the query, - // since the node will be rendered using the respective language. - ->fields('i', array('langcode')) - ->limit(10) - ->execute(); - $results = array(); - foreach ($find as $item) { - // Render the node. - $node = node_load($item->sid); - $build = node_view($node, 'search_result', $item->langcode); - unset($build['#theme']); - $node->rendered = drupal_render($build); - - // Fetch comments for snippet. - $node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node, $item->langcode); - - $extra = module_invoke_all('node_search_result', $node, $item->langcode); - - $language = language_load($item->langcode); - $uri = $node->uri(); - $username = array( - '#theme' => 'username', - '#account' => user_load($node->uid), - ); - $results[] = array( - 'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE, 'language' => $language))), - 'type' => check_plain(node_get_type_label($node)), - 'title' => $node->label($item->langcode), - 'user' => drupal_render($username), - 'date' => $node->getChangedTime(), - 'node' => $node, - 'extra' => $extra, - 'score' => $item->calculated_score, - 'snippet' => search_excerpt($keys, $node->rendered, $item->langcode), - 'langcode' => $node->language()->id, - ); - } - return $results; -} - -/** * Implements hook_ranking(). */ function node_ranking() { @@ -1708,70 +1557,12 @@ function node_page_view(EntityInterface $node) { } /** - * Implements hook_update_index(). - */ -function node_update_index() { - $limit = (int) config('search.settings')->get('index.cron_limit'); - - $result = db_query_range("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit, array(), array('target' => 'slave')); - $nids = $result->fetchCol(); - if (!$nids) { - return; - } - - // The indexing throttle should be aware of the number of language variants - // of a node. - $counter = 0; - foreach (node_load_multiple($nids) as $node) { - // Determine when the maximum number of indexable items is reached. - $counter += count($node->getTranslationLanguages()); - if ($counter > $limit) { - break; - } - _node_index_node($node); - } -} - -/** - * Indexes a single node. - * - * @param \Drupal\Core\Entity\EntityInterface $node - * The node to index. - */ -function _node_index_node(EntityInterface $node) { - - // Save the changed time of the most recent indexed node, for the search - // results half-life calculation. - Drupal::state()->set('node.cron_last', $node->getChangedTime()); - - $languages = $node->getTranslationLanguages(); - - foreach ($languages as $language) { - // Render the node. - $build = node_view($node, 'search_index', $language->id); - - unset($build['#theme']); - $node->rendered = drupal_render($build); - - $text = '

' . check_plain($node->label($language->id)) . '

' . $node->rendered; - - // Fetch extra data normally not visible. - $extra = module_invoke_all('node_update_index', $node, $language->id); - foreach ($extra as $t) { - $text .= $t; - } - - // Update index. - search_index($node->id(), 'node', $text, $language->id); - } -} - -/** * Implements hook_form_FORM_ID_alter(). * * @see node_search_validate() */ function node_form_search_form_alter(&$form, $form_state) { + if (isset($form['module']) && $form['module']['#value'] == 'node' && user_access('use advanced search')) { // Keyword boxes: $form['advanced'] = array( diff --git a/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php new file mode 100644 index 0000000..5dfda5b --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php @@ -0,0 +1,49 @@ +searchSettings = $config_factory->get('search.settings'); + $this->searchPluginManager = $manager; $this->moduleHandler = $module_handler; $this->state = $state; } @@ -56,7 +73,7 @@ public function __construct(ConfigFactory $config_factory, ContextInterface $con 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') ); @@ -77,23 +94,28 @@ public function getFormID() { * sorted into alphabetical order. */ protected function getModuleOptions() { - $search_info = search_get_info(TRUE); + $options = array(); $names = system_get_module_info('name'); - $names = array_intersect_key($names, $search_info); - asort($names, SORT_STRING); - return $names; + foreach ($this->searchPluginManager->getDefinitions() as $search_info) { + if (isset($names[$search_info['module']])) { + $options[$search_info['module']] = $names[$search_info['module']]; + } + } + asort($options, SORT_STRING); + return $options; } /** * {@inheritdoc} */ public function buildForm(array $form, array &$form_state) { - $config = $this->configFactory->get('search.settings'); - // Collect some stats + + // Collect some stats. $remaining = 0; $total = 0; - foreach ($config->get('active_modules') as $module) { - if ($status = $this->moduleHandler->invoke($module, 'search_status')) { + $active_plugins = $this->searchPluginManager->getActivePlugins(); + foreach ($active_plugins as $plugin) { + if ($status = $plugin->indexStatus()) { $remaining += $status['remaining']; $total += $status['total']; } @@ -101,7 +123,7 @@ public function buildForm(array $form, array &$form_state) { $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))) . '%'; + $percentage = ((int) min(100, 100 * ($total - $remaining) / max(1, $total))) . '%'; $status = '

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

'; $form['status'] = array( '#type' => 'details', @@ -124,7 +146,7 @@ public function buildForm(array $form, array &$form_state) { $form['indexing_throttle']['cron_limit'] = array( '#type' => 'select', '#title' => t('Number of items to index per cron run'), - '#default_value' => $config->get('index.cron_limit'), + '#default_value' => $this->searchSettings->get('index.cron_limit'), '#options' => $items, '#description' => 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'))) ); @@ -139,7 +161,7 @@ public function buildForm(array $form, array &$form_state) { $form['indexing_settings']['minimum_word_size'] = array( '#type' => 'number', '#title' => t('Minimum word length to index'), - '#default_value' => $config->get('index.minimum_word_size'), + '#default_value' => $this->searchSettings->get('index.minimum_word_size'), '#min' => 1, '#max' => 1000, '#description' => 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).') @@ -147,7 +169,7 @@ public function buildForm(array $form, array &$form_state) { $form['indexing_settings']['overlap_cjk'] = array( '#type' => 'checkbox', '#title' => t('Simple CJK handling'), - '#default_value' => $config->get('index.overlap_cjk'), + '#default_value' => $this->searchSettings->get('index.overlap_cjk'), '#description' => 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.') ); @@ -160,24 +182,21 @@ public function buildForm(array $form, array &$form_state) { '#type' => 'checkboxes', '#title' => t('Active modules'), '#title_display' => 'invisible', - '#default_value' => $config->get('active_modules'), + '#default_value' => $this->searchSettings->get('active_modules'), '#options' => $module_options, '#description' => t('Choose which search modules are active from the available modules.') ); $form['active']['default_module'] = array( '#title' => t('Default search module'), '#type' => 'radios', - '#default_value' => $config->get('default_module'), + '#default_value' => $this->searchSettings->get('default_module'), '#options' => $module_options, '#description' => t('Choose which search module is the default.') ); - // Per module settings - foreach ($config->get('active_modules') as $module) { - $added_form = $this->moduleHandler->invoke($module, 'search_admin'); - if (is_array($added_form)) { - $form = NestedArray::mergeDeep($form, $added_form); - } + // Per module plugin settings. + foreach ($active_plugins as $plugin) { + $plugin->addToAdminForm($form, $form_state); } // Set #submit so we are sure it's invoked even if one of // the active search modules added its own #submit. @@ -207,17 +226,21 @@ public function validateForm(array &$form, array &$form_state) { */ public function submitForm(array &$form, array &$form_state) { parent::submitForm($form, $form_state); - $config = $this->configFactory->get('search.settings'); // If these settings change, the index needs to be rebuilt. - if (($config->get('index.minimum_word_size') != $form_state['values']['minimum_word_size']) || ($config->get('index.overlap_cjk') != $form_state['values']['overlap_cjk'])) { - $config->set('index.minimum_word_size', $form_state['values']['minimum_word_size']); - $config->set('index.overlap_cjk', $form_state['values']['overlap_cjk']); + 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(t('The index will be rebuilt.')); search_reindex(); } - $config->set('index.cron_limit', $form_state['values']['cron_limit']); - $config->set('default_module', $form_state['values']['default_module']); + $this->searchSettings->set('index.cron_limit', $form_state['values']['cron_limit']); + $this->searchSettings->set('default_module', $form_state['values']['default_module']); + + // Handle per-plugin submission logic. + foreach ($this->searchPluginManager->getActivePlugins() as $plugin) { + $plugin->submitAdminForm($form, $form_state); + } // Check whether we are resetting the values. if ($form_state['triggering_element']['#value'] == t('Reset to defaults')) { @@ -226,12 +249,12 @@ public function submitForm(array &$form, array &$form_state) { else { $new_modules = array_filter($form_state['values']['active_modules']); } - if ($config->get('active_modules') != $new_modules) { - $config->set('active_modules', $new_modules); + if ($this->searchSettings->get('active_modules') != $new_modules) { + $this->searchSettings->set('active_modules', $new_modules); drupal_set_message(t('The active search modules have been changed.')); $this->state->set('menu_rebuild_needed', TRUE); } - $config->save(); + $this->searchSettings->save(); } /** @@ -242,4 +265,5 @@ public function searchAdminReindexSubmit(array $form, array &$form_state) { // send the user to the confirmation page $form_state['redirect'] = 'admin/config/search/settings/reindex'; } + } diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php new file mode 100644 index 0000000..c4cc34d --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php @@ -0,0 +1,146 @@ +get('index.cron_limit') (see + * example below). Also, since the cron run could time out and abort in the + * middle of your run, you should update your module's internal bookkeeping on + * when items have last been indexed as you go rather than waiting to the end + * of indexing. + */ + public function updateIndex(); + + /** + * Take action when the search index is going to be rebuilt. + * + * Modules that use updateIndex() should update their indexing + * bookkeeping so that it starts from scratch the next time updateIndex() + * is called. + */ + public function resetIndex(); + + /** + * Report the status of indexing. + * + * The core search module only invokes this method on active module plugins. + * Implementing modules do not need to check whether they are active when + * calculating their return values. + * + * @return + * An associative array with the key-value pairs: + * - remaining: The number of items left to index. + * - total: The total number of items to index. + */ + public function indexStatus(); + + /** + * Add elements to the search settings form. + * + * The core search module only invokes this method on active module plugins. + * + * @param $form + * Nested array of form elements that comprise the form. + * @param $form_state + * A keyed array containing the current state of the form. The arguments + * that drupal_get_form() was originally called with are available in the + * array $form_state['build_info']['args']. + */ + public function addToAdminForm(array &$form, array &$form_state); + + /** + * Handle any submission for elements on the search settings form. + * + * The core search module only invokes this method on active module plugins. + * + * @param $form + * Nested array of form elements that comprise the form. + * @param $form_state + * A keyed array containing the current state of the form. The arguments + * that drupal_get_form() was originally called with are available in the + * array $form_state['build_info']['args']. + */ + public function submitAdminForm(array &$form, array &$form_state); + +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php new file mode 100644 index 0000000..d69c4a6 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php @@ -0,0 +1,126 @@ +keywords = (string) $keywords; + $this->searchParams = $params; + $this->searchAttributes = $attributes; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSearchKeywords() { + return $this->keywords; + } + + /** + * {@inheritdoc} + */ + public function getSearchParams() { + return $this->searchParams; + } + + /** + * {@inheritdoc} + */ + public function getSearchAttributes() { + return $this->searchAttributes; + } + + /** + * {@inheritdoc} + */ + public function isSearchExecutable() { + // Default implementation suitable for plugins that only use keywords. + return !empty($this->keywords); + } + + /** + * {@inheritdoc} + */ + public function buildResults() { + $results = $this->execute(); + return array( + '#theme' => 'search_results', + '#results' => $results, + '#module' => $this->pluginDefinition['module'], + ); + } + + /** + * {@inheritdoc} + */ + public function updateIndex() { + // Empty default implementation. + } + + /** + * {@inheritdoc} + */ + public function resetIndex() { + // Empty default implementation. + } + + /** + * {@inheritdoc} + */ + public function indexStatus() { + // No-op default implementation + return array('remaining' => 0, 'total' => 0); + } + + /** + * {@inheritdoc} + */ + public function addToAdminForm(array &$form, array &$form_state) { + // Empty default implementation. + } + + /** + * {@inheritdoc} + */ + public function submitAdminForm(array &$form, array &$form_state) { + // Empty default implementation. + } + +} diff --git a/core/modules/search/lib/Drupal/search/SearchPluginManager.php b/core/modules/search/lib/Drupal/search/SearchPluginManager.php new file mode 100644 index 0000000..41cd975 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/SearchPluginManager.php @@ -0,0 +1,96 @@ + $namespaces['Drupal\search']); + parent::__construct('Plugin\Search', $namespaces, $annotation_namespaces, 'Drupal\search\Annotation\SearchPlugin'); + + $this->configFactory = $config_factory; + } + + /** + * {@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'], + 'module' => $definition['provider'], + ); + } + + /** + * Returns definitions for active search plugins. + * + * @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 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_modules = array_flip($this->configFactory->get('search.settings')->get('active_modules')); + foreach ($this->getDefinitions() as $plugin_id => $definition) { + if (isset($active_modules[$definition['module']])) { + $active_definitions[$plugin_id] = $definition; + } + } + return $active_definitions; + } + + /** + * Returns definitions for active search plugins keyed by their module. + * + * @return array + * An array of active search plugin definitions, keyed by their providing + * module. + */ + public function getActiveDefinitionsByModule() { + $active_definitions = array(); + foreach ($this->getActiveDefinitions() as $definition) { + // Re-key by plugin module name. + $active_definitions[$definition['module']] = $definition; + } + return $active_definitions; + } +} diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php index 5b0efeb..74fb54f 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php @@ -29,7 +29,7 @@ function setUp() { $this->node = $this->drupalCreateNode(); // First update the index. This does the initial processing. - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); // Then, run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php index f45895d..aab4c13 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php @@ -66,7 +66,7 @@ function setUp() { $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->id(), $edit_comment, t('Save')); // First update the index. This does the initial processing. - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); // Then, run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php index 2313a95..fc99845 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php @@ -49,7 +49,7 @@ function setUp() { $edit[$body_key] = l($node->label(), 'node/' . $node->id()) . ' pizza sandwich'; $this->drupalPost('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Enable the search block. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php index ff822c6..649e410 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php @@ -46,7 +46,7 @@ function setUp() { $this->node = $this->drupalCreateNode(); - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Set up a dummy initial count of times the form has been submitted. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php index c52549f..587d773 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php @@ -42,7 +42,7 @@ function testExactQuery() { } // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php index d9ab415..8cbe050 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php @@ -95,7 +95,8 @@ function testIndexingThrottle() { // Index only 4 items per cron run. config('search.settings')->set('index.cron_limit', 4)->save(); // Update the index. This does the initial processing. - node_update_index(); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->updateIndex(); // Run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown // function manually is needed to finish the indexing process. @@ -104,7 +105,7 @@ function testIndexingThrottle() { // the first has one, the second has two and the third has three language // variants. Indexing the third would exceed the throttle limit, so we // expect that only the first two will be indexed. - $status = module_invoke('node', 'search_status'); + $status = $plugin->indexStatus(); $this->assertEqual($status['remaining'], 1, 'Remaining items after updating the search index is 1.'); } @@ -114,14 +115,17 @@ function testIndexingThrottle() { function testSearchingMultilingualFieldValues() { // Update the index and then run the shutdown method. // See testIndexingThrottle() for further explanation. - node_update_index(); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->updateIndex(); search_update_totals(); foreach ($this->searchable_nodes as $node) { // Each searchable node that we created contains values in the body field // in one or more languages. Let's pick the last language variant from the // body array and execute a search using that as a search keyword. $body_language_variant = end($node->body); - $search_result = node_search_execute($body_language_variant[0]['value']); + $plugin->setSearch($body_language_variant[0]['value'], array(), array()); + // Do the search and assert the results. + $search_result = $plugin->execute(); // See whether we get the same node as a result. $this->assertEqual($search_result[0]['node']->id(), $node->id(), 'The search has resulted the correct node.'); } diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php index 67706cf..8f067f6 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php @@ -45,7 +45,7 @@ function testPhraseSearchPunctuation() { $node = $this->drupalCreateNode(array('body' => array(array('value' => "The bunny's ears were fluffy.")))); // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php index a8c2e61..8792596 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php @@ -46,7 +46,7 @@ function testPreprocessLangcode() { $node = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'en')); // First update the index. This does the initial processing. - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); // Then, run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown @@ -73,7 +73,7 @@ function testPreprocessStemming() { )); // First update the index. This does the initial processing. - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); // Then, run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php index 887e997..64d9eb9 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php @@ -63,7 +63,7 @@ function testRankings() { } // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. @@ -90,16 +90,21 @@ function testRankings() { for ($i = 0; $i < 5; $i ++) { $client->post($stats_path, array(), array('nid' => $nid))->send(); } - // Test each of the possible rankings. + // @todo - comments and views are removed from the array since they are + // broken in core. Those modules expected hook_update_index() to be called + // even though it was only called on modules that implemented a search type. + array_pop($node_ranks); + array_pop($node_ranks); foreach ($node_ranks as $node_rank) { // Disable all relevancy rankings except the one we are testing. foreach ($node_ranks as $var) { variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0); } - // Do the search and assert the results. - $set = node_search_execute('rocks'); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->setSearch('rocks', array(), array()); + $set = $plugin->execute(); $this->assertEqual($set[0]['node']->id(), $nodes[$node_rank][1]->id(), 'Search ranking "' . $node_rank . '" order.'); } } @@ -143,7 +148,7 @@ function testHTMLRankings() { } // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. @@ -154,7 +159,10 @@ function testHTMLRankings() { foreach ($node_ranks as $node_rank) { variable_set('node_rank_' . $node_rank, 0); } - $set = node_search_execute('rocks'); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->setSearch('rocks', array(), array()); + // Do the search and assert the results. + $set = $plugin->execute(); // Test the ranking of each tag. foreach ($sorted_tags as $tag_rank => $tag) { @@ -173,13 +181,15 @@ function testHTMLRankings() { $node = $this->drupalCreateNode($settings); // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. $this->refreshVariables(); - - $set = node_search_execute('rocks'); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->setSearch('rocks', array(), array()); + // Do the search and assert the results. + $set = $plugin->execute(); // Ranking should always be second to last. $set = array_slice($set, -2, 1); @@ -212,7 +222,7 @@ function testDoubleRankings() { $node = $this->drupalCreateNode($settings); // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. @@ -227,7 +237,10 @@ function testDoubleRankings() { } // Do the search and assert the results. - $set = node_search_execute('rocks'); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->setSearch('rocks', array(), array()); + // Do the search and assert the results. + $set = $plugin->execute(); $this->assertEqual($set[0]['node']->id(), $node->id(), 'Search double ranking order.'); } } diff --git a/core/modules/search/search.api.php b/core/modules/search/search.api.php index 03b45aa..003f84b 100644 --- a/core/modules/search/search.api.php +++ b/core/modules/search/search.api.php @@ -11,42 +11,6 @@ */ /** - * Define a custom search type. - * - * This hook allows a module to tell the Search module that it wishes to - * perform searches on content it defines (custom node types, users, or - * comments for example) when a site search is performed. - * - * In order for the search to do anything, your module must also implement - * hook_search_execute(), which is called when someone requests a search on - * your module's type of content. If you want to have your content indexed - * in the standard search index, your module should also implement - * hook_update_index(). If your search type has settings, you can implement - * hook_search_admin() to add them to the search settings page. You can use - * hook_form_FORM_ID_alter(), with FORM_ID set to 'search_form', to add fields - * to the search form (see node_form_search_form_alter() for an example). - * You can use hook_search_access() to limit access to searching, and - * hook_search_page() to override how search results are displayed. - * - * @return - * Array with optional keys: - * - title: Title for the tab on the search page for this module. Defaults to - * the module name if not given. - * - path: Path component after 'search/' for searching with this module. - * Defaults to the module name if not given. - * - conditions_callback: An implementation of callback_search_conditions(). - * - * @ingroup search - */ -function hook_search_info() { - return array( - 'title' => 'Content', - 'path' => 'node', - 'conditions_callback' => 'callback_search_conditions', - ); -} - -/** * Define access to a custom search routine. * * This hook allows a module to define permissions for a search tab. @@ -58,225 +22,6 @@ function hook_search_access() { } /** - * Take action when the search index is going to be rebuilt. - * - * Modules that use hook_update_index() should update their indexing - * bookkeeping so that it starts from scratch the next time hook_update_index() - * is called. - * - * @ingroup search - */ -function hook_search_reset() { - db_update('search_dataset') - ->fields(array('reindex' => REQUEST_TIME)) - ->condition('type', 'node') - ->execute(); -} - -/** - * Report the status of indexing. - * - * The core search module only invokes this hook on active modules. - * Implementing modules do not need to check whether they are active when - * calculating their return values. - * - * @return - * An associative array with the key-value pairs: - * - remaining: The number of items left to index. - * - total: The total number of items to index. - * - * @ingroup search - */ -function hook_search_status() { - $total = db_query('SELECT COUNT(DISTINCT nid) FROM {node_field_data} WHERE status = 1')->fetchField(); - $remaining = db_query("SELECT COUNT(DISTINCT nid) FROM {node_field_data} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0")->fetchField(); - return array('remaining' => $remaining, 'total' => $total); -} - -/** - * Add elements to the search settings form. - * - * @return - * Form array for the Search settings page at admin/config/search/settings. - * - * @ingroup search - */ -function hook_search_admin() { - // Output form for defining rank factor weights. - $form['content_ranking'] = array( - '#type' => 'details', - '#title' => t('Content ranking'), - ); - $form['content_ranking']['#theme'] = 'node_search_admin'; - $form['content_ranking']['#tree'] = TRUE; - $form['content_ranking']['info'] = array( - '#value' => '' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property 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)); - $ranks = config('node.settings')->get('search_rank'); - foreach (module_invoke_all('ranking') as $var => $values) { - $form['content_ranking']['factors'][$var] = array( - '#title' => $values['title'], - '#type' => 'select', - '#options' => $options, - '#default_value' => isset($ranks[$var]) ? $ranks[$var] : 0, - ); - } - - $form['#submit'][] = 'node_search_admin_submit'; - - return $form; -} - -/** - * Execute a search for a set of key words. - * - * Use database API with the 'Drupal\Core\Database\Query\PagerSelectExtender' - * query extension to perform your search. - * - * If your module uses hook_update_index() and search_index() to index its - * items, use table 'search_index' aliased to 'i' as the main table in your - * query, with the 'Drupal\search\SearchQuery' extension. You can join to your - * module's table using the 'i.sid' field, which will contain the $sid values - * you provided to search_index(). Add the main keywords to the query by using - * method searchExpression(). The functions search_expression_extract() and - * search_expression_insert() may also be helpful for adding custom search - * parameters to the search expression. - * - * See node_search_execute() for an example of a module that uses the search - * index, and user_search_execute() for an example that doesn't use the search - * index. - * - * @param $keys - * The search keywords as entered by the user. Defaults to NULL. - * @param $conditions - * (optional) An array of additional conditions, such as filters. Defaults to - * NULL. - * - * @return - * An array of search results. To use the default search result display, each - * item should have the following keys': - * - link: (required) The URL of the found item. - * - type: The type of item (such as the content type). - * - title: (required) The name of the item. - * - user: The author of the item. - * - date: A timestamp when the item was last modified. - * - extra: An array of optional extra information items. - * - snippet: An excerpt or preview to show with the result (can be generated - * with search_excerpt()). - * - language: Language code for the item (usually two characters). - * - * @ingroup search - */ -function hook_search_execute($keys = NULL, $conditions = NULL) { - // Build matching conditions - $query = db_select('search_index', 'i', array('target' => 'slave')) - ->extend('Drupal\search\SearchQuery') - ->extend('Drupal\Core\Database\Query\PagerSelectExtender'); - $query->join('node_field_data', 'n', 'n.nid = i.sid'); - $query - ->condition('n.status', 1) - ->addTag('node_access') - ->searchExpression($keys, 'node'); - - // Insert special keywords. - $query->setOption('type', 'n.type'); - $query->setOption('langcode', 'n.langcode'); - if ($query->setOption('term', 'ti.tid')) { - $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid'); - } - // Only continue if the first pass query matches. - if (!$query->executeFirstPass()) { - return array(); - } - - // Add the ranking expressions. - _node_rankings($query); - - // Load results. - $find = $query - // Add the language code of the indexed item to the result of the query, - // since the node will be rendered using the respective language. - ->fields('i', array('langcode')) - ->limit(10) - ->execute(); - $results = array(); - foreach ($find as $item) { - // Render the node. - $node = node_load($item->sid); - $build = node_view($node, 'search_result', $item->langcode); - unset($build['#theme']); - $node->rendered = drupal_render($build); - - // Fetch comments for snippet. - $node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node, $item->langcode); - - $extra = module_invoke_all('node_search_result', $node, $item->langcode); - - $language = language_load($item->langcode); - $uri = $node->uri(); - $username = array( - '#theme' => 'username', - '#account' => $node, - ); - $results[] = array( - 'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE, 'language' => $language))), - 'type' => check_plain(node_get_type_label($node)), - 'title' => $node->label($item->langcode), - 'user' => drupal_render($username), - 'date' => $node->changed, - 'node' => $node, - 'extra' => $extra, - 'score' => $item->calculated_score, - 'snippet' => search_excerpt($keys, $node->rendered, $item->langcode), - 'langcode' => $node->langcode, - ); - } - return $results; -} - -/** - * Override the rendering of search results. - * - * A module that implements hook_search_info() to define a type of search may - * implement this hook in order to override the default theming of its search - * results, which is otherwise themed using theme('search_results'). - * - * Note that by default, theme('search_results') and theme('search_result') - * work together to create an ordered list (OL). So your hook_search_page() - * implementation should probably do this as well. - * - * @param $results - * An array of search results. - * - * @return - * A renderable array, which will render the formatted search results with a - * pager included. - * - * @see search-result.tpl.php - * @see search-results.tpl.php - */ -function hook_search_page($results) { - $output['prefix']['#markup'] = '
    '; - - foreach ($results as $entry) { - $output[] = array( - '#theme' => 'search_result', - '#result' => $entry, - '#module' => 'my_module_name', - ); - } - $pager = array( - '#theme' => 'pager', - ); - $output['suffix']['#markup'] = '
' . drupal_render($pager); - - return $output; -} - -/** * Preprocess text for search. * * This hook is called to preprocess both the text added to the search index @@ -316,95 +61,3 @@ function hook_search_preprocess($text, $langcode = NULL) { return $text; } -/** - * Update the search index for this module. - * - * This hook is called every cron run if the Search module is enabled, your - * module has implemented hook_search_info(), and your module has been set as - * an active search module on the Search settings page - * (admin/config/search/settings). It allows your module to add items to the - * built-in search index using search_index(), or to add them to your module's - * own indexing mechanism. - * - * When implementing this hook, your module should index content items that - * were modified or added since the last run. PHP has a time limit - * for cron, though, so it is advisable to limit how many items you index - * per run using config('search.settings')->get('index.cron_limit') (see - * example below). Also, since the cron run could time out and abort in the - * middle of your run, you should update your module's internal bookkeeping on - * when items have last been indexed as you go rather than waiting to the end - * of indexing. - * - * @ingroup search - */ -function hook_update_index() { - $limit = (int) config('search.settings')->get('index.cron_limit'); - - $result = db_query_range("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit); - - foreach ($result as $node) { - $node = node_load($node->id()); - - // Save the changed time of the most recent indexed node, for the search - // results half-life calculation. - \Drupal::state()->set('node.cron_last', $node->changed); - - // Render the node. - $build = node_view($node, 'search_index'); - $node->rendered = drupal_render($node->content); - - $text = '

' . check_plain($node->label()) . '

' . $node->rendered; - - // Fetch extra data normally not visible - $extra = module_invoke_all('node_update_index', $node); - foreach ($extra as $t) { - $text .= $t; - } - - // Update index - search_index($node->id(), 'node', $text); - } -} - -/** - * @} End of "addtogroup hooks". - */ - -/** - * Provide search query conditions. - * - * Callback for hook_search_info(). - * - * This callback is invoked by search_view() to get an array of additional - * search conditions to pass to search_data(). For example, a search module - * may get additional keywords, filters, or modifiers for the search from - * the query string. - * - * This example pulls additional search keywords out of the $_REQUEST variable, - * (i.e. from the query string of the request). The conditions may also be - * generated internally - for example based on a module's settings. - * - * @param $keys - * The search keywords string. - * - * @return - * An array of additional conditions, such as filters. - * - * @ingroup callbacks - * @ingroup search - */ -function callback_search_conditions($keys) { - $conditions = array(); - - if (!empty($_REQUEST['keys'])) { - $conditions['keys'] = $_REQUEST['keys']; - } - if (!empty($_REQUEST['sample_search_keys'])) { - $conditions['sample_search_keys'] = $_REQUEST['sample_search_keys']; - } - if ($force_keys = config('sample_search.settings')->get('force_keywords')) { - $conditions['sample_search_force_keywords'] = $force_keys; - } - return $conditions; -} - diff --git a/core/modules/search/search.module b/core/modules/search/search.module index ba5434f..d912b88 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -152,6 +152,7 @@ function search_menu() { $items['search'] = array( 'title' => 'Search', 'page callback' => 'search_view', + 'page arguments' => array(NULL, '', ''), 'access callback' => 'search_is_active', 'type' => MENU_SUGGESTED_ITEM, 'file' => 'search.pages.inc', @@ -175,15 +176,14 @@ function search_menu() { // system appears to be having two sets of tabs. See discussion on issue // http://drupal.org/node/245103 for details. - drupal_static_reset('search_get_info'); $default_info = search_get_default_module_info(); if ($default_info) { - foreach (search_get_info() as $module => $search_info) { + foreach (Drupal::service('plugin.manager.search')->getActiveDefinitionsByModule() as $module => $search_info) { $path = 'search/' . $search_info['path']; $items[$path] = array( 'title' => $search_info['title'], 'page callback' => 'search_view', - 'page arguments' => array($module, ''), + 'page arguments' => array($search_info['id'], $module, ''), 'access callback' => '_search_menu_access', 'access arguments' => array($module), 'type' => MENU_LOCAL_TASK, @@ -194,7 +194,7 @@ function search_menu() { 'title' => $search_info['title'], 'load arguments' => array('%map', '%index'), 'page callback' => 'search_view', - 'page arguments' => array($module, 2), + 'page arguments' => array($search_info['id'], $module, 2), 'access callback' => '_search_menu_access', 'access arguments' => array($module), // The default local task points to its parent, but this item points to @@ -217,52 +217,17 @@ function search_menu() { */ function search_is_active() { // This path cannot be accessed if there are no active modules. - return user_access('search content') && search_get_info(); -} - -/** - * Returns information about available search modules. - * - * @param $all - * If TRUE, information about all enabled modules implementing - * hook_search_info() will be returned. If FALSE (default), only modules that - * have been set to active on the search settings page will be returned. - * - * @return - * Array of hook_search_info() return values, keyed by module name. The - * 'title' and 'path' array elements will be set to defaults for each module - * if not supplied by hook_search_info(), and an additional array element of - * 'module' will be added (set to the module name). - */ -function search_get_info($all = FALSE) { - $search_hooks = &drupal_static(__FUNCTION__); - - if (!isset($search_hooks)) { - foreach (module_implements('search_info') as $module) { - $search_hooks[$module] = call_user_func($module . '_search_info'); - // Use module name as the default value. - $search_hooks[$module] += array('title' => $module, 'path' => $module); - // Include the module name itself in the array. - $search_hooks[$module]['module'] = $module; - } - } - - if ($all) { - return $search_hooks; - } - - // Return only modules that are set to active in search settings. - return array_intersect_key($search_hooks, array_flip(config('search.settings')->get('active_modules'))); + return user_access('search content') && Drupal::service('plugin.manager.search')->getActiveDefinitions(); } /** * Returns information about the default search module. * - * @return - * The search_get_info() array element for the default search module, if any. + * @return array + * The search plugin definition for the default search module, if any. */ function search_get_default_module_info() { - $info = search_get_info(); + $info = Drupal::service('plugin.manager.search')->getActiveDefinitionsByModule(); $default = config('search.settings')->get('default_module'); if (isset($info[$default])) { return $info[$default]; @@ -305,7 +270,9 @@ function _search_menu_access($name) { */ function search_reindex($sid = NULL, $module = NULL, $reindex = FALSE, $langcode = NULL) { if ($module == NULL && $sid == NULL) { - module_invoke_all('search_reset'); + foreach (Drupal::service('plugin.manager.search')->getActivePlugins() as $plugin) { + $plugin->resetIndex(); + } } else { $query = db_delete('search_dataset') @@ -362,9 +329,8 @@ function search_cron() { // to date. drupal_register_shutdown_function('search_update_totals'); - foreach (config('search.settings')->get('active_modules') as $module) { - // Update word index - module_invoke($module, 'update_index'); + foreach (Drupal::service('plugin.manager.search')->getActivePlugins() as $plugin) { + $plugin->updateIndex(); } } @@ -948,12 +914,11 @@ function search_expression_insert($expression, $option, $value = NULL) { * @see search_form_submit() */ function search_form($form, &$form_state, $action = '', $keys = '', $module = NULL, $prompt = NULL) { - $module_info = FALSE; if (!$module) { $module_info = search_get_default_module_info(); } else { - $info = search_get_info(); + $info = Drupal::service('plugin.manager.search')->getActiveDefinitionsByModule(); $module_info = isset($info[$module]) ? $info[$module] : FALSE; } @@ -1048,36 +1013,6 @@ function search_box_form_submit($form, &$form_state) { } /** - * Performs a search by calling hook_search_execute(). - * - * @param $keys - * Keyword query to search on. - * @param $module - * Search module to search. - * @param $conditions - * Optional array of additional search conditions. - * - * @return - * Renderable array of search results. No return value if $keys are not - * supplied or if the given search module is not active. - */ -function search_data($keys, $module, $conditions = NULL) { - if (module_hook($module, 'search_execute')) { - $results = module_invoke($module, 'search_execute', $keys, $conditions); - if (module_hook($module, 'search_page')) { - return module_invoke($module, 'search_page', $results); - } - else { - return array( - '#theme' => 'search_results', - '#results' => $results, - '#module' => $module, - ); - } - } -} - -/** * Returns snippets from a piece of text, with certain keywords highlighted. * * Used for formatting search results. diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc index a39a815..b828220 100644 --- a/core/modules/search/search.pages.inc +++ b/core/modules/search/search.pages.inc @@ -16,7 +16,7 @@ * @param $keys * Keywords to use for the search. */ -function search_view($module = NULL, $keys = '') { +function search_view($plugin_id = NULL, $module = NULL, $keys = '') { $info = FALSE; $keys = trim($keys); // Also try to pull search keywords out of the $_REQUEST variable to @@ -25,14 +25,15 @@ function search_view($module = NULL, $keys = '') { $keys = trim($_REQUEST['keys']); } + $manager = Drupal::service('plugin.manager.search'); if (!empty($module)) { - $active_module_info = search_get_info(); + $active_module_info = $manager->getActiveDefinitionsByModule(); if (isset($active_module_info[$module])) { $info = $active_module_info[$module]; } } - if (empty($info)) { + if (empty($plugin_id) || empty($info)) { // No path or invalid path: find the default module. Note that if there // are no enabled search modules, this function should never be called, // since hook_menu() would not have defined any search paths. @@ -44,7 +45,9 @@ function search_view($module = NULL, $keys = '') { } return new RedirectResponse(url($path, array('absolute' => TRUE))); } - + $plugin = $manager->createInstance($plugin_id); + $request = Drupal::request(); + $plugin->setSearch($keys, $request->query->all(), $request->attributes->all()); // Default results output is an empty string. $results = array('#markup' => ''); // Process the search form. Note that if there is $_POST data, @@ -53,18 +56,13 @@ function search_view($module = NULL, $keys = '') { // form submits with POST but redirects to GET. This way we can keep // the search query URL clean as a whistle. if (empty($_POST['form_id']) || $_POST['form_id'] != 'search_form') { - $conditions = NULL; - if (isset($info['conditions_callback'])) { - // Build an optional array of more search conditions. - $conditions = call_user_func($info['conditions_callback'], $keys); - } // Only search if there are keywords or non-empty conditions. - if ($keys || !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)); // Collect the search results. - $results = search_data($keys, $info['module'], $conditions); + $results = $plugin->buildResults(); } } // The form may be altered based on whether the search was run. diff --git a/core/modules/search/search.services.yml b/core/modules/search/search.services.yml new file mode 100644 index 0000000..22dc7f2 --- /dev/null +++ b/core/modules/search/search.services.yml @@ -0,0 +1,4 @@ +services: + plugin.manager.search: + class: Drupal\search\SearchPluginManager + arguments: ['@container.namespaces', '@config.factory'] 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 new file mode 100644 index 0000000..3c44e18 --- /dev/null +++ b/core/modules/search/tests/modules/search_extra_type/lib/Drupal/search_extra_type/Plugin/Search/SearchExtraTypeSearch.php @@ -0,0 +1,154 @@ +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); + } + + /** + * {@inheritdoc} + */ + public function setSearch($keywords, array $params, array $attributes) { + if (empty($params['search_conditions'])) { + $params['search_conditions'] = ''; + } + parent::setSearch($keywords, $params, $attributes); + } + + /** + * Verifies if the given parameters are valid enough to execute a search for. + * + * @return bool + * A true or false depending on the implementation. + */ + public function isSearchExecutable() { + return (bool) ($this->keywords || !empty($this->searchParams['search_conditions'])); + } + + /** + * Execute the search. + * + * This is a dummy search, so when search "executes", we just return a dummy + * result containing the keywords and a list of conditions. + * + * @return array + * A structured list of search results + */ + public function execute() { + $results = array(); + if (!$this->isSearchExecutable()) { + return $results; + } + return array( + array( + 'link' => url('node'), + 'type' => 'Dummy result type', + 'title' => 'Dummy title', + 'snippet' => "Dummy search snippet to display. Keywords: {$this->keywords}\n\nConditions: " . print_r($this->searchParams, TRUE), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function buildResults() { + $results = $this->execute(); + $output['prefix']['#markup'] = '

Test page text is here

    '; + + foreach ($results as $entry) { + $output[] = array( + '#theme' => 'search_result', + '#result' => $entry, + '#module' => 'search_extra_type', + ); + } + $output['suffix']['#markup'] = '
' . theme('pager'); + + return $output; + } + + /** + * {@inheritdoc} + */ + public function addToAdminForm(array &$form, array &$form_state) { + // Output form for defining rank factor weights. + $form['extra_type_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Extra type settings'), + '#tree' => TRUE, + ); + + $form['extra_type_settings']['boost'] = array( + '#type' => 'select', + '#title' => t('Boost method'), + '#options' => array( + 'bi' => t('Bistromathic'), + 'ii' => t('Infinite Improbability'), + ), + '#default_value' => $this->configSettings->get('boost'), + ); + } + + /** + * {@inheritdoc} + */ + public function submitAdminForm(array &$form, array &$form_state) { + $this->configSettings + ->set('boost', $form_state['values']['extra_type_settings']['boost']) + ->save(); + } + +} diff --git a/core/modules/search/tests/modules/search_extra_type/search_extra_type.module b/core/modules/search/tests/modules/search_extra_type/search_extra_type.module index 4435b44..4600708 100644 --- a/core/modules/search/tests/modules/search_extra_type/search_extra_type.module +++ b/core/modules/search/tests/modules/search_extra_type/search_extra_type.module @@ -5,105 +5,3 @@ * Dummy module implementing a search type for search module testing. */ -/** - * Implements hook_search_info(). - */ -function search_extra_type_search_info() { - return array( - 'title' => 'Dummy search type', - 'path' => 'dummy_path', - 'conditions_callback' => 'search_extra_type_conditions', - ); -} - -/** - * Implements callback_search_conditions(). - * - * Tests the conditions callback for hook_search_info(). - */ -function search_extra_type_conditions() { - $conditions = array(); - - if (!empty($_REQUEST['search_conditions'])) { - $conditions['search_conditions'] = $_REQUEST['search_conditions']; - } - return $conditions; -} - -/** - * Implements hook_search_execute(). - * - * This is a dummy search, so when search "executes", we just return a dummy - * result containing the keywords and a list of conditions. - */ -function search_extra_type_search_execute($keys = NULL, $conditions = NULL) { - if (!$keys) { - $keys = ''; - } - return array( - array( - 'link' => url('node'), - 'type' => 'Dummy result type', - 'title' => 'Dummy title', - 'snippet' => "Dummy search snippet to display. Keywords: {$keys}\n\nConditions: " . print_r($conditions, TRUE), - ), - ); -} - -/** - * Implements hook_search_page(). - * - * Adds some text to the search page so we can verify that it runs. - */ -function search_extra_type_search_page($results) { - $output['prefix']['#markup'] = '

Test page text is here

    '; - - foreach ($results as $entry) { - $output[] = array( - '#theme' => 'search_result', - '#result' => $entry, - '#module' => 'search_extra_type', - ); - } - $pager = array( - '#theme' => 'pager', - ); - $output['suffix']['#markup'] = '
' . drupal_render($pager); - - return $output; -} - -/** - * Implements hook_search_admin(). - */ -function search_extra_type_search_admin() { - // Output form for defining rank factor weights. - $form['extra_type_settings'] = array( - '#type' => 'fieldset', - '#title' => t('Extra type settings'), - '#tree' => TRUE, - ); - - $form['extra_type_settings']['boost'] = array( - '#type' => 'select', - '#title' => t('Boost method'), - '#options' => array( - 'bi' => t('Bistromathic'), - 'ii' => t('Infinite Improbability'), - ), - '#default_value' => config('search_extra_type.settings')->get('boost'), - ); - - $form['#submit'][] = 'search_extra_type_admin_submit'; - - return $form; -} - -/** - * Form API callback: Save admin settings - */ -function search_extra_type_admin_submit($form, &$form_state) { - config('search_extra_type.settings') - ->set('boost', $form_state['values']['extra_type_settings']['boost']) - ->save(); -} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php new file mode 100644 index 0000000..38978b8 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php @@ -0,0 +1,132 @@ +get('database'), + $container->get('plugin.manager.entity'), + $container->get('module_handler'), + $configuration, + $plugin_id, + $plugin_definition + ); + } + + /** + * Creates a UserSearch object. + * + * @param Connection $database + * The database connection. + * @param EntityManager $entity_manager + * The entity manager. + * @param ModuleHandlerInterface $module_handler + * The module handler. + * @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(Connection $database, EntityManager $entity_manager, ModuleHandlerInterface $module_handler, array $configuration, $plugin_id, array $plugin_definition) { + $this->database = $database; + $this->entityManager = $entity_manager; + $this->moduleHandler = $module_handler; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function execute() { + $results = array(); + if (!$this->isSearchExecutable()) { + return $results; + } + $keys = $this->keywords; + // Replace wildcards with MySQL/PostgreSQL wildcards. + $keys = preg_replace('!\*+!', '%', $keys); + $query = $this->database + ->select('users') + ->extend('Drupal\Core\Database\Query\PagerSelectExtender'); + $query->fields('users', array('uid')); + $user_account = \Drupal::request()->get('account'); + if ($user_account->hasPermission('administer users')) { + // Administrators can also search in the otherwise private email field, and + // they don't need to be restricted to only active users. + $query->fields('users', array('mail')); + $query->condition($query->orConditionGroup() + ->condition('name', '%' . $this->database->escapeLike($keys) . '%', 'LIKE') + ->condition('mail', '%' . $this->database->escapeLike($keys) . '%', 'LIKE') + ); + } + else { + // Regular users can only search via usernames, and we do not show them + // blocked accounts. + $query->condition('name', '%' . $this->database->escapeLike($keys) . '%', 'LIKE') + ->condition('status', 1); + } + $uids = $query + ->limit(15) + ->execute() + ->fetchCol(); + $accounts = $this->entityManager->getStorageController('user')->loadMultiple($uids); + + foreach ($accounts as $account_ng) { + $account = $account_ng->getBCEntity(); + $result = array( + 'title' => $account->getUsername(), + 'link' => url('user/' . $account->id(), array('absolute' => TRUE)), + ); + if ($user_account->hasPermission('administer users')) { + $result['title'] .= ' (' . $account->getEmail() . ')'; + } + $results[] = $result; + } + + return $results; + } + +} diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 6380387..b8b5828 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1,12 +1,10 @@ 'Users', - ); -} - -/** * Implements hook_search_access(). */ function user_search_access() { @@ -525,51 +514,6 @@ function user_search_access() { } /** - * Implements hook_search_execute(). - */ -function user_search_execute($keys = NULL, $conditions = NULL) { - $find = array(); - // Replace wildcards with MySQL/PostgreSQL wildcards. - $keys = preg_replace('!\*+!', '%', $keys); - $query = db_select('users') - ->extend('Drupal\Core\Database\Query\PagerSelectExtender'); - $query->fields('users', array('uid')); - if (user_access('administer users')) { - // Administrators can also search in the otherwise private email field, and - // they don't need to be restricted to only active users. - $query->fields('users', array('mail')); - $query->condition(db_or()-> - condition('name', '%' . db_like($keys) . '%', 'LIKE')-> - condition('mail', '%' . db_like($keys) . '%', 'LIKE')); - } - else { - // Regular users can only search via usernames, and we do not show them - // blocked accounts. - $query->condition('name', '%' . db_like($keys) . '%', 'LIKE') - ->condition('status', 1); - } - $uids = $query - ->limit(15) - ->execute() - ->fetchCol(); - $accounts = user_load_multiple($uids); - - $results = array(); - foreach ($accounts as $account) { - $result = array( - 'title' => user_format_name($account), - 'link' => url('user/' . $account->id(), array('absolute' => TRUE)), - ); - if (user_access('administer users')) { - $result['title'] .= ' (' . $account->getEmail() . ')'; - } - $results[] = $result; - } - - return $results; -} - -/** * Implements hook_user_view(). */ function user_user_view(UserInterface $account, EntityDisplay $display) {