diff --git a/core/modules/comment/comment.api.php b/core/modules/comment/comment.api.php index ec1707f..43dd29f 100644 --- a/core/modules/comment/comment.api.php +++ b/core/modules/comment/comment.api.php @@ -34,7 +34,7 @@ function hook_comment_presave(Drupal\comment\Comment $comment) { */ function hook_comment_insert(Drupal\comment\Comment $comment) { // Reindex the node when comments are added. - search_touch_node($comment->nid->target_id); + search_mark_for_reindex('node_search', $comment->nid->target_id); } /** @@ -45,7 +45,7 @@ function hook_comment_insert(Drupal\comment\Comment $comment) { */ function hook_comment_update(Drupal\comment\Comment $comment) { // Reindex the node when comments are updated. - search_touch_node($comment->nid->target_id); + node_reindex_node_search($comment->nid->target_id); } /** 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..a2064a5 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php @@ -0,0 +1,550 @@ + array('column' => 'n.type'), + 'langcode' => array('column' => 'n.langcode'), + 'author' => array('column' => 'n.uid'), + 'term' => array('column' => 'ti.tid', 'join' => array('table' => 'taxonomy_index', 'alias' => 'ti', 'condition' => 'n.nid = ti.nid')), + ); + + /** + * {@inheritdoc} + */ + static public function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('database'), + $container->get('plugin.manager.entity'), + $container->get('module_handler'), + $container->get('config.factory')->get('search.settings'), + $container->get('keyvalue')->get('state'), + $container->get('request')->attributes->get('_account') + ); + } + + /** + * 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'. + * @param \Drupal\Core\Session\AccountInterface $account + * The $account object to use for checking for access to advanced search. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, Connection $database, EntityManager $entity_manager, ModuleHandlerInterface $module_handler, Config $search_settings, KeyValueStoreInterface $state, AccountInterface $account = NULL) { + $this->database = $database; + $this->entityManager = $entity_manager; + $this->moduleHandler = $module_handler; + $this->searchSettings = $search_settings; + $this->state = $state; + $this->account = $account; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function access($operation = 'view', AccountInterface $account = NULL) { + return !empty($account) && $account->hasPermission('access content'); + } + + /** + * {@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, $this->getPluginId()); + + // Handle advanced search filters in the f query string. + $parameters = $this->getParameters(); + if (!empty($parameters['f']) && is_array($parameters['f'])) { + $filters = array(); + // Match any query value that is an expected option and a value + // separated by ':' like 'term:27'. + $pattern = '/(' . implode('|', array_keys($this->advanced)) . '):([^ ]*)/i'; + foreach ($parameters['f'] as $item) { + if (preg_match($pattern, $item, $m)) { + // Use the matched value as the array key to eliminate duplicates. + $filters[$m[1]][$m[2]] = $m[2]; + } + } + foreach ($filters as $option => $matched) { + $info = $this->advanced[$option]; + // Insert additional conditions. By default, all use the OR operator. + $operator = empty($info['operator']) ? 'OR' : $info['operator']; + $where = new Condition($operator); + foreach ($matched as $value) { + $where->condition($info['column'], $value); + } + $query->condition($where); + if (!empty($info['join'])) { + $query->join($info['join']['table'], $info['join']['alias'], $info['join']['condition']); + } + } + } + // 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 = :type 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(':type' => $this->getPluginId()), 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(), $this->getPluginId(), $text, $language->id)); + } + } + + /** + * {@inheritdoc} + */ + public function resetIndex() { + $this->database->update('search_dataset') + ->fields(array('reindex' => REQUEST_TIME)) + ->condition('type', $this->getPluginId()) + ->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 = :type AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0", array(':type' => $this->getPluginId()))->fetchField(); + return array('remaining' => $remaining, 'total' => $total); + } + + /** + * {@inheritdoc} + */ + public function searchFormAlter(array &$form, array &$form_state) { + // Keyword boxes: + $form['advanced'] = array( + '#type' => 'details', + '#title' => t('Advanced search'), + '#collapsed' => TRUE, + '#attributes' => array('class' => array('search-advanced')), + '#access' => $this->account && $this->account->hasPermission('use advanced search'), + ); + $form['advanced']['keywords-fieldset'] = array( + '#type' => 'fieldset', + '#title' => t('Keywords'), + '#collapsible' => FALSE, + ); + $form['advanced']['keywords'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + $form['advanced']['keywords-fieldset']['keywords']['or'] = array( + '#type' => 'textfield', + '#title' => t('Containing any of the words'), + '#size' => 30, + '#maxlength' => 255, + ); + $form['advanced']['keywords-fieldset']['keywords']['phrase'] = array( + '#type' => 'textfield', + '#title' => t('Containing the phrase'), + '#size' => 30, + '#maxlength' => 255, + ); + $form['advanced']['keywords-fieldset']['keywords']['negative'] = array( + '#type' => 'textfield', + '#title' => t('Containing none of the words'), + '#size' => 30, + '#maxlength' => 255, + ); + + // 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', + '#title' => t('Types'), + '#collapsible' => FALSE, + ); + $form['advanced']['types-fieldset']['type'] = array( + '#type' => 'checkboxes', + '#title' => t('Only of the type(s)'), + '#prefix' => '
', + '#suffix' => '
', + '#options' => $types, + ); + $form['advanced']['submit'] = array( + '#type' => 'submit', + '#value' => t('Advanced search'), + '#prefix' => '
', + '#suffix' => '
', + '#weight' => 100, + ); + + // Languages: + $language_options = array(); + foreach (language_list(Language::STATE_ALL) as $langcode => $language) { + // Make locked languages appear special in the list. + $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name; + } + if (count($language_options) > 1) { + $form['advanced']['lang-fieldset'] = array( + '#type' => 'fieldset', + '#title' => t('Languages'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); + $form['advanced']['lang-fieldset']['language'] = array( + '#type' => 'checkboxes', + '#title' => t('Languages'), + '#prefix' => '
', + '#suffix' => '
', + '#options' => $language_options, + ); + } + + // Add a submit handler. + $form['#submit'][] = array($this, 'searchFormSubmit'); + } + + /** + * Handle submission of elements added in searchFormAlter(). + * + * @param array $form + * Nested array of form elements that comprise the form. + * @param array $form_state + * A keyed array containing the current state of the form. + */ + public function searchFormSubmit(array &$form, array &$form_state) { + // Initialize using any existing basic search keywords. + $keys = $form_state['values']['processed_keys']; + $filters = array(); + + // Collect extra restrictions. + if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) { + // Retrieve selected types - Form API sets the value of unselected + // checkboxes to 0. + foreach ($form_state['values']['type'] as $type) { + if ($type) { + $filters[] = 'type:' . $type; + } + } + } + + if (isset($form_state['values']['term']) && is_array($form_state['values']['term'])) { + foreach ($form_state['values']['term'] as $term) { + $filters[] = 'term:' . $term; + } + } + if (isset($form_state['values']['language']) && is_array($form_state['values']['language'])) { + foreach ($form_state['values']['language'] as $language) { + if ($language) { + $filters[] = 'language:' . $language; + } + } + } + if ($form_state['values']['or'] != '') { + if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['or'], $matches)) { + $keys .= ' ' . implode(' OR ', $matches[1]); + } + } + if ($form_state['values']['negative'] != '') { + if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['negative'], $matches)) { + $keys .= ' -' . implode(' -', $matches[1]); + } + } + if ($form_state['values']['phrase'] != '') { + $keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"'; + } + 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); + } + + /** + * {@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 26f11a2..a4f1a6d 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,161 +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 = Drupal::moduleHandler()->invokeAll('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() { - return user_access('access content'); -} - -/** - * 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 (Drupal::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), - ); - } - 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 = Drupal::moduleHandler()->invokeAll('node_search_result', array($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,199 +1550,6 @@ 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 = Drupal::moduleHandler()->invokeAll('node_update_index', array($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( - '#type' => 'details', - '#title' => t('Advanced search'), - '#collapsed' => TRUE, - '#attributes' => array('class' => array('search-advanced')), - ); - $form['advanced']['keywords-fieldset'] = array( - '#type' => 'fieldset', - '#title' => t('Keywords'), - '#collapsible' => FALSE, - ); - $form['advanced']['keywords'] = array( - '#prefix' => '
', - '#suffix' => '
', - ); - $form['advanced']['keywords-fieldset']['keywords']['or'] = array( - '#type' => 'textfield', - '#title' => t('Containing any of the words'), - '#size' => 30, - '#maxlength' => 255, - ); - $form['advanced']['keywords-fieldset']['keywords']['phrase'] = array( - '#type' => 'textfield', - '#title' => t('Containing the phrase'), - '#size' => 30, - '#maxlength' => 255, - ); - $form['advanced']['keywords-fieldset']['keywords']['negative'] = array( - '#type' => 'textfield', - '#title' => t('Containing none of the words'), - '#size' => 30, - '#maxlength' => 255, - ); - - // Node types: - $types = array_map('check_plain', node_type_get_names()); - $form['advanced']['types-fieldset'] = array( - '#type' => 'fieldset', - '#title' => t('Types'), - '#collapsible' => FALSE, - ); - $form['advanced']['types-fieldset']['type'] = array( - '#type' => 'checkboxes', - '#title' => t('Only of the type(s)'), - '#prefix' => '
', - '#suffix' => '
', - '#options' => $types, - ); - $form['advanced']['submit'] = array( - '#type' => 'submit', - '#value' => t('Advanced search'), - '#prefix' => '
', - '#suffix' => '
', - '#weight' => 100, - ); - - // Languages: - $language_options = array(); - foreach (language_list(Language::STATE_ALL) as $langcode => $language) { - // Make locked languages appear special in the list. - $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name; - } - if (count($language_options) > 1) { - $form['advanced']['lang-fieldset'] = array( - '#type' => 'fieldset', - '#title' => t('Languages'), - '#collapsible' => FALSE, - '#collapsed' => FALSE, - ); - $form['advanced']['lang-fieldset']['language'] = array( - '#type' => 'checkboxes', - '#title' => t('Languages'), - '#prefix' => '
', - '#suffix' => '
', - '#options' => $language_options, - ); - } - - $form['#validate'][] = 'node_search_validate'; - } -} - -/** - * Form validation handler for node_form_search_form_alter(). - */ -function node_search_validate($form, &$form_state) { - // Initialize using any existing basic search keywords. - $keys = $form_state['values']['processed_keys']; - - // Insert extra restrictions into the search keywords string. - if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) { - // Retrieve selected types - Form API sets the value of unselected - // checkboxes to 0. - $form_state['values']['type'] = array_filter($form_state['values']['type']); - if (count($form_state['values']['type'])) { - $keys = search_expression_insert($keys, 'type', implode(',', array_keys($form_state['values']['type']))); - } - } - - if (isset($form_state['values']['term']) && is_array($form_state['values']['term']) && count($form_state['values']['term'])) { - $keys = search_expression_insert($keys, 'term', implode(',', $form_state['values']['term'])); - } - if (isset($form_state['values']['language']) && is_array($form_state['values']['language'])) { - $languages = array_filter($form_state['values']['language']); - if (count($languages)) { - $keys = search_expression_insert($keys, 'language', implode(',', $languages)); - } - } - if ($form_state['values']['or'] != '') { - if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['or'], $matches)) { - $keys .= ' ' . implode(' OR ', $matches[1]); - } - } - if ($form_state['values']['negative'] != '') { - if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['negative'], $matches)) { - $keys .= ' -' . implode(' -', $matches[1]); - } - } - if ($form_state['values']['phrase'] != '') { - $keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"'; - } - if (!empty($keys)) { - form_set_value($form['basic']['processed_keys'], trim($keys), $form_state); - } -} - -/** * Implements hook_form_FORM_ID_alter(). * * Alters the System module's site information settings form to add a global @@ -2612,3 +2261,65 @@ function node_system_info_alter(&$info, $file, $type) { $info['hidden'] = !module_exists('translation') && config('system.module.disabled')->get('translation') === NULL; } } + +/** + * Mark a node to be re-indexed by the node_search plugin. + * + * @param int $nid + * The Node ID. + */ +function node_reindex_node_search($nid) { + if (Drupal::moduleHandler()->moduleExists('search')) { + // Reindex node context indexed by the node module search plugin. + search_mark_for_reindex('node_search', $nid); + } +} + +/** + * Implements hook_node_update(). + */ +function node_node_update(EntityInterface $node) { + // Reindex the node when it is updated. The node is automatically indexed + // when it is added, simply by being added to the node table. + node_reindex_node_search($node->id()); +} + +/** + * Implements hook_comment_insert(). + */ +function node_comment_insert($comment) { + // Reindex the node when comments are added. + node_reindex_node_search($comment->nid->target_id); +} + +/** + * Implements hook_comment_update(). + */ +function node_comment_update($comment) { + // Reindex the node when comments are changed. + node_reindex_node_search($comment->nid->target_id); +} + +/** + * Implements hook_comment_delete(). + */ +function node_comment_delete($comment) { + // Reindex the node when comments are deleted. + node_reindex_node_search($comment->nid->target_id); +} + +/** + * Implements hook_comment_publish(). + */ +function node_comment_publish($comment) { + // Reindex the node when comments are published. + node_reindex_node_search($comment->nid->target_id); +} + +/** + * Implements hook_comment_unpublish(). + */ +function node_comment_unpublish($comment) { + // Reindex the node when comments are unpublished. + node_reindex_node_search($comment->nid->target_id); +} diff --git a/core/modules/search/config/search.settings.yml b/core/modules/search/config/search.settings.yml index a1a88b4..b09ce57 100644 --- a/core/modules/search/config/search.settings.yml +++ b/core/modules/search/config/search.settings.yml @@ -1,8 +1,8 @@ -active_modules: - - node - - user +active_plugins: + - node_search + - user_search and_or_limit: '7' -default_module: node +default_plugin: node_search index: cron_limit: '100' overlap_cjk: '1' 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..b8ea0ad --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php @@ -0,0 +1,47 @@ +searchSettings = $config_factory->get('search.settings'); + $this->searchPluginManager = $manager; $this->moduleHandler = $module_handler; $this->state = $state; } @@ -57,6 +77,7 @@ 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') ); @@ -70,38 +91,39 @@ public function getFormID() { } /** - * Returns names of available search modules. + * Returns names of available search plugins. * * @return array - * An array of the names of enabled modules that call hook_search_info - * sorted into alphabetical order. + * An array of the names of available search plugins. */ - protected function getModuleOptions() { - $search_info = search_get_info(TRUE); - $names = system_get_module_info('name'); - $names = array_intersect_key($names, $search_info); - asort($names, SORT_STRING); - return $names; + protected function getOptions() { + $options = array(); + foreach ($this->searchPluginManager->getDefinitions() as $plugin_id => $search_info) { + $options[$plugin_id] = t($search_info['title']) . ' (' . $plugin_id . ')'; + } + 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')) { + + 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))) . '%'; + $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,40 +169,37 @@ 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.') ); $form['active'] = array( '#type' => 'details', - '#title' => t('Active search modules') + '#title' => t('Active search plugins') ); - $module_options = $this->getModuleOptions(); - $form['active']['active_modules'] = array( + $options = $this->getOptions(); + $form['active']['active_plugins'] = array( '#type' => 'checkboxes', - '#title' => t('Active modules'), + '#title' => t('Active plugins'), '#title_display' => 'invisible', - '#default_value' => $config->get('active_modules'), - '#options' => $module_options, - '#description' => t('Choose which search modules are active from the available modules.') + '#default_value' => $this->searchSettings->get('active_plugins'), + '#options' => $options, + '#description' => t('Choose which search plugins are active from the available plugins.') ); - $form['active']['default_module'] = array( - '#title' => t('Default search module'), + $form['active']['default_plugin'] = array( + '#title' => t('Default search plugin'), '#type' => 'radios', - '#default_value' => $config->get('default_module'), - '#options' => $module_options, - '#description' => t('Choose which search module is the default.') + '#default_value' => $this->searchSettings->get('default_plugin'), + '#options' => $options, + '#description' => t('Choose which search plugin 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 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. + // the active search plugins added its own #submit. $form['#submit'][] = array($this, 'submitForm'); return parent::buildForm($form, $form_state); @@ -194,10 +213,10 @@ public function validateForm(array &$form, array &$form_state) { // Check whether we selected a valid default. if ($form_state['triggering_element']['#value'] != t('Reset to defaults')) { - $new_modules = array_filter($form_state['values']['active_modules']); - $default = $form_state['values']['default_module']; - if (!in_array($default, $new_modules, TRUE)) { - form_set_error('default_module', t('Your default search module is not selected as an active module.')); + $new_plugins = array_filter($form_state['values']['active_plugins']); + $default = $form_state['values']['default_plugin']; + if (!in_array($default, $new_plugins, TRUE)) { + form_set_error('default_plugin', t('Your default search plugin is not selected as an active plugin.')); } } } @@ -207,31 +226,35 @@ 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_plugin', $form_state['values']['default_plugin']); + + // 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')) { - $new_modules = array('node', 'user'); + $new_plugins = array('node_search', 'user_search'); } else { - $new_modules = array_filter($form_state['values']['active_modules']); + $new_plugins = array_filter($form_state['values']['active_plugins']); } - if ($config->get('active_modules') != $new_modules) { - $config->set('active_modules', $new_modules); - drupal_set_message(t('The active search modules have been changed.')); + if ($this->searchSettings->get('active_plugins') != $new_plugins) { + $this->searchSettings->set('active_plugins', $new_plugins); + drupal_set_message(t('The active search plugins 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/SearchIndexingInterface.php b/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php new file mode 100644 index 0000000..d06cc62 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php @@ -0,0 +1,64 @@ +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(); + + /** + * Takes 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(); + + /** + * Reports the status of indexing. + * + * The core search module only invokes this method on active module plugins. + * Implementing modules do not need to check whether they are active when + * calculating their return values. + * + * @return array + * An associative array with the key-value pairs: + * - remaining: The number of items left to index. + * - total: The total number of items to index. + */ + public function indexStatus(); + +} 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..0d49a09 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php @@ -0,0 +1,129 @@ +keywords = (string) $keywords; + $this->searchParameters = $parameters; + $this->searchAttributes = $attributes; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getKeywords() { + return $this->keywords; + } + + /** + * {@inheritdoc} + */ + public function getParameters() { + return $this->searchParameters; + } + + /** + * {@inheritdoc} + */ + public function getAttributes() { + return $this->searchAttributes; + } + + /** + * {@inheritdoc} + */ + public function isSearchExecutable() { + // Default implementation suitable for plugins that only use keywords. + return !empty($this->keywords); + } + + /** + * {@inheritdoc} + */ + public function buildResults() { + $results = $this->execute(); + return array( + '#theme' => 'search_results', + '#results' => $results, + '#plugin_id' => $this->getPluginId(), + ); + } + + /** + * {@inheritdoc} + */ + public function searchFormAlter(array &$form, array &$form_state) { + // Empty default implementation. + } + + /** + * {@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/SearchExpression.php b/core/modules/search/lib/Drupal/search/SearchExpression.php index cd5e5dc..5662389 100644 --- a/core/modules/search/lib/Drupal/search/SearchExpression.php +++ b/core/modules/search/lib/Drupal/search/SearchExpression.php @@ -39,7 +39,7 @@ public function getExpression() { } /** - * Extracts a module-specific search option from a search expression. + * Extracts a type-specific search option from a search expression. * * Search options are added using SearchExpression::insert() and retrieved * using SearchExpression::extract(). They take the form option:value, and @@ -59,7 +59,7 @@ public function extract($option) { } /** - * Adds a module-specific search option to a search expression. + * Adds a type-specific search option to a search expression. * * Search options are added using SearchExpression::insert() and retrieved * using SearchExpression::extract(). They take the form option:value, and 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..c8d5ef3 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/SearchPluginManager.php @@ -0,0 +1,119 @@ + $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'], + ); + } + + /** + * 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_plugins = array_flip($this->configFactory->get('search.settings')->get('active_plugins')); + 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\TypedData\AccessibleInterface')) { + return $this->createInstance($plugin_id)->access('view', $account); + } + return TRUE; + } +} diff --git a/core/modules/search/lib/Drupal/search/SearchQuery.php b/core/modules/search/lib/Drupal/search/SearchQuery.php index 86887b7..13c02f3 100644 --- a/core/modules/search/lib/Drupal/search/SearchQuery.php +++ b/core/modules/search/lib/Drupal/search/SearchQuery.php @@ -15,8 +15,8 @@ /** * Performs a query on the full-text search index for a word or words. * - * This function is normally only called by each module that supports the - * indexed search (and thus, implements hook_update_index()). + * This function is normally only called by each plugin that supports the + * indexed search. * * Results are retrieved in two logical passes. However, the two passes are * joined together into a single query, and in the case of most simple queries @@ -28,7 +28,7 @@ * The second portion of the query further refines this set by verifying * advanced text conditions (such as negative or phrase matches). * - * The used query object has the tag 'search_$module' and can be further + * The used query object has the tag 'search_$type' and can be further * extended with hook_query_alter(). */ class SearchQuery extends SelectExtender { @@ -40,11 +40,11 @@ class SearchQuery extends SelectExtender { protected $searchExpression; /** - * The type of search (search module). + * The type of search (search type). * * This maps to the value of the type column in search_index, and is equal - * to the machine-readable name of the module that implements - * hook_search_info(). + * to the machine-readable name of the entity type being indexed, or other + * identifier provided by a search plugin. * * @var string */ @@ -144,21 +144,21 @@ class SearchQuery extends SelectExtender { * * @param $query * A search query string, which can contain options. - * @param $module - * The search module. This maps to {search_index}.type in the database. + * @param $type + * The search type. This maps to {search_index}.type in the database. * * @return * The SearchQuery object. */ - public function searchExpression($expression, $module) { + public function searchExpression($expression, $type) { $this->searchExpression = $expression; - $this->type = $module; + $this->type = $type; return $this; } /** - * Applies a search option and removes it from the search query string. + * Applies a search option and removes it from the search expression string. * * These options are in the form option:value,value2,value3. * 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/SearchCommentTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchCommentTest.php index e2d0d9f..d5c339b 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchCommentTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchCommentTest.php @@ -194,7 +194,7 @@ function setRolePermissions($rid, $access_comments = FALSE, $search_content = TR */ function assertCommentAccess($assume_access, $message) { // Invoke search index update. - search_touch_node($this->node->id()); + search_mark_for_reindex('node_search', $this->node->id()); $this->cronRun(); // Search for the comment subject. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php index 2313a95..7142b08 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. @@ -57,7 +57,7 @@ function setUp() { } /** - * Verify the search settings form. + * Verifies the search settings form. */ function testSearchSettingsPage() { @@ -86,29 +86,29 @@ function testSearchSettingsPage() { } /** - * Verify module-supplied settings form. + * 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 module is not present on + // 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 module is listed as an option - $this->assertTrue($this->xpath('//input[@id="edit-active-modules-search-extra-type"]'), 'Checkbox for activating search for an extra module is visible'); - $this->assertTrue($this->xpath('//input[@id="edit-default-module-search-extra-type"]'), 'Radio button for setting extra module as default search module is visible'); + // 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 module - $edit['active_modules[search_extra_type]'] = 'search_extra_type'; - $edit['default_module'] = 'search_extra_type'; + // 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->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); // Ensure that the settings fieldset is visible after enabling search for - // the test module + // the test plugin $this->assertText(t('Extra type settings')); $this->assertText(t('Boost method')); @@ -125,47 +125,47 @@ function testSearchModuleSettingsPage() { // Ensure that the modifications took effect. $this->assertText(t('The configuration options have been saved.')); $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 module-specific form is active'); + $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'); } /** - * Verify that you can disable individual search modules. + * Verifies that you can disable individual search plugins. */ function testSearchModuleDisabling() { - // Array of search modules to test: 'path' is the search path, 'title' is + // 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. - $module_info = array( - 'node' => array( + $plugin_info = array( + 'node_search' => array( 'path' => 'node', 'title' => 'Content', 'keys' => 'pizza', 'text' => $this->search_node->label(), ), - 'user' => array( + 'user_search' => array( 'path' => 'user', 'title' => 'User', 'keys' => $this->search_user->getUsername(), 'text' => $this->search_user->getEmail(), ), - 'search_extra_type' => array( + 'search_extra_type_search' => array( 'path' => 'dummy_path', 'title' => 'Dummy search type', 'keys' => 'foo', 'text' => 'Dummy search snippet to display', ), ); - $modules = array_keys($module_info); + $plugins = array_keys($plugin_info); - // Test each module if it's enabled as the only search module. - foreach ($modules as $module) { - // Enable the one module and disable other ones. - $info = $module_info[$module]; + // 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 ($modules as $other) { - $edit['active_modules[' . $other . ']'] = (($other == $module) ? $module : FALSE); + foreach ($plugins as $other) { + $edit['active_plugins[' . $other . ']'] = (($other == $plugin) ? $plugin : FALSE); } - $edit['default_module'] = $module; + $edit['default_plugin'] = $plugin; $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); // Run a search from the correct search URL. @@ -173,16 +173,16 @@ function testSearchModuleDisabling() { $this->assertNoText('no results', $info['title'] . ' search found results'); $this->assertText($info['text'], 'Correct search text found'); - // Verify that other module search tab titles are not visible. - foreach ($modules as $other) { - if ($other != $module) { - $title = $module_info[$other]['title']; + // Verify that other plugin search tab titles are not visible. + foreach ($plugins as $other) { + if ($other != $plugin) { + $title = $plugin_info[$other]['title']; $this->assertNoText($title, $title . ' search tab is not shown'); } } // Run a search from the search block on the node page. Verify you get - // to this module's search results page. + // to this plugin's search results page. $terms = array('search_block_form' => $info['keys']); $this->drupalPost('node', $terms, t('Search')); $this->assertEqual( @@ -190,28 +190,28 @@ function testSearchModuleDisabling() { url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)), 'Block redirected to right search page'); - // Try an invalid search path. Should redirect to our active module. - $this->drupalGet('search/not_a_module_path'); + // Try an invalid search path. Should redirect to our active plugin. + $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'); } - // Test with all search modules enabled. When you go to the search - // page or run search, all modules should be shown. + // Test with all search plugins enabled. When you go to the search + // page or run search, all plugins should be shown. $edit = array(); - foreach ($modules as $module) { - $edit['active_modules[' . $module . ']'] = $module; + foreach ($plugins as $plugin) { + $edit['active_plugins[' . $plugin . ']'] = $plugin; } - $edit['default_module'] = 'node'; + $edit['default_plugin'] = 'node_search'; $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); foreach (array('search/node/pizza', 'search/node') as $path) { $this->drupalGet($path); - foreach ($modules as $module) { - $title = $module_info[$module]['title']; + foreach ($plugins as $plugin) { + $title = $plugin_info[$plugin]['title']; $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title))); } } 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/SearchKeywordsConditionsTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchKeywordsConditionsTest.php index 87414fa..86c9b77 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchKeywordsConditionsTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchKeywordsConditionsTest.php @@ -35,7 +35,7 @@ function setUp() { // Login with sufficient privileges. $this->drupalLogin($this->searching_user); // Test with all search modules enabled. - config('search.settings')->set('active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'))->save(); + config('search.settings')->set('active_plugins', array('node_search', 'user_search', 'search_extra_type_search'))->save(); menu_router_rebuild(); } diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchLanguageTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchLanguageTest.php index d8b5f31..19bbb37 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchLanguageTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchLanguageTest.php @@ -54,7 +54,11 @@ function testLanguages() { // Pick French and ensure it is selected. $edit = array('language[fr]' => TRUE); $this->drupalPost('search/node', $edit, t('Advanced search')); - $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', 'Language filter added to query.'); + // Get the redirected URL. + $url = $this->getUrl(); + $parts = parse_url($url); + $query_string = isset($parts['query']) ? rawurldecode($parts['query']) : ''; + $this->assertTrue(strpos($query_string, '=language:fr') !== FALSE, 'Language filter language:fr add to the query string.'); // Change the default language and delete English. $path = 'admin/config/regional/settings'; 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/SearchPageOverrideTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php index df72d2b..62cd629 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php @@ -37,7 +37,7 @@ function setUp() { $this->drupalLogin($this->search_user); // Enable the extra type module for searching. - config('search.settings')->set('active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'))->save(); + config('search.settings')->set('active_plugins', array('node_search', 'user_search', 'search_extra_type_search'))->save(); menu_router_rebuild(); } 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 1a343d2..0200411 100644 --- a/core/modules/search/search.api.php +++ b/core/modules/search/search.api.php @@ -11,272 +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. - * - * @ingroup search - */ -function hook_search_access() { - return user_access('access content'); -} - -/** - * 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 (Drupal::moduleHandler()->invokeAll('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 = Drupal::moduleHandler()->invokeAll('node_search_result', array($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 @@ -315,96 +49,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 = Drupal::moduleHandler()->invokeAll('node_update_index', array($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.install b/core/modules/search/search.install index 40a569c..0cb52d3 100644 --- a/core/modules/search/search.install +++ b/core/modules/search/search.install @@ -120,43 +120,6 @@ function search_schema() { 'primary key' => array('word'), ); - $schema['search_node_links'] = array( - 'description' => 'Stores items (like nodes) that link to other nodes, used to improve search scores for nodes that are frequently linked to.', - 'fields' => array( - 'sid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The {search_dataset}.sid of the searchable item containing the link to the node.', - ), - 'type' => array( - 'type' => 'varchar', - 'length' => 16, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The {search_dataset}.type of the searchable item containing the link to the node.', - ), - 'nid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The {node}.nid that this item links to.', - ), - 'caption' => array( - 'type' => 'text', - 'size' => 'big', - 'not null' => FALSE, - 'description' => 'The text used to link to the {node}.nid.', - ), - ), - 'primary key' => array('sid', 'type', 'nid'), - 'indexes' => array( - 'nid' => array('nid'), - ), - ); - return $schema; } @@ -171,13 +134,38 @@ function search_update_8000() { 'overlap_cjk' => 'index.overlap_cjk', 'search_cron_limit' => 'index.cron_limit', 'search_tag_weights' => 'index.tag_weights', - 'search_active_modules' => 'active_modules', 'search_and_or_limit' => 'and_or_limit', - 'search_default_module' => 'default_module', )); + _search_update_8000_modules_mapto_plugins(array('user' => 'user_search', 'node' => 'node_search')); } /** + * Update search module variables to plugin IDs. + */ +function _search_update_8000_modules_mapto_plugins(array $map) { + $active_modules = update_variable_get('search_active_modules', array('node', 'user')); + $config = config('search.settings'); + $active_plugins = $config->get('active_plugins'); + foreach($active_modules as $idx => $module) { + if (isset($map[$module])) { + $active_plugins[] = $map[$module]; + unset($active_modules[$idx]); + } + } + $config->set('active_plugins', array_unique($active_plugins)); + if ($active_modules) { + update_variable_set('search_active_modules', $active_modules); + } + else { + update_variable_del('search_active_modules'); + } + $default_module = update_variable_get('search_default_module', 'node'); + if (isset($map[$default_module])) { + $config->set('default_plugin', $map[$default_module]); + update_variable_del('search_default_module'); + } +} +/** * Adds the langcode field and indexes to {search_dataset} and {search_index}. */ function search_update_8001() { @@ -185,7 +173,8 @@ function search_update_8001() { // need to recreate search data through running cron. db_truncate('search_dataset'); db_truncate('search_index'); - db_truncate('search_node_links'); + // This table is no longer used. + db_drop_table('search_node_links'); // Add the fields and indexes. db_drop_primary_key('search_dataset'); diff --git a/core/modules/search/search.module b/core/modules/search/search.module index b3f381c..7314463 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -8,6 +8,7 @@ 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) @@ -82,7 +83,7 @@ function search_help($path, $arg) { $output .= '
' . t('Content reindexing') . '
'; $output .= '
' . t('Content-related actions on your site (creating, editing, or deleting content and comments) automatically cause affected content items to be marked for indexing or reindexing at the next cron run. When content is marked for reindexing, the previous content remains in the index until cron runs, at which time it is replaced by the new content. Unlike content-related actions, actions related to the structure of your site do not cause affected content to be marked for reindexing. Examples of structure-related actions that affect content include deleting or editing taxonomy terms, enabling or disabling modules that add text to content (such as Taxonomy, Comment, and field-providing modules), and modifying the fields or display parameters of your content types. If you take one of these actions and you want to ensure that the search index is updated to reflect your changed site structure, you can mark all content for reindexing by clicking the "Re-index site" button on the Search settings page. If you have a lot of content on your site, it may take several cron runs for the content to be reindexed.', array('@searchsettings' => url('admin/config/search/settings'))) . '
'; $output .= '
' . t('Configuring search settings') . '
'; - $output .= '
' . t('Indexing behavior can be adjusted using the Search settings page. Users with Administer search permission can control settings such as the Number of items to index per cron run, Indexing settings (word length), Active search modules, and Content ranking, which lets you adjust the priority in which indexed content is returned in results.', array('@searchsettings' => url('admin/config/search/settings'))) . '
'; + $output .= '
' . t('Indexing behavior can be adjusted using the Search settings page. Users with Administer search permission can control settings such as the Number of items to index per cron run, Indexing settings (word length), Active search plugins, and Content ranking, which lets you adjust the priority in which indexed content is returned in results.', array('@searchsettings' => url('admin/config/search/settings'))) . '
'; $output .= '
' . t('Search block') . '
'; $output .= '
' . t('The Search module includes a default Search form block, which can be enabled and configured on the Blocks administration page. The block is available to users with the Search content permission.', array('@blocks' => url('admin/structure/block'))) . '
'; $output .= '
' . t('Extending Search module') . '
'; @@ -106,12 +107,12 @@ function search_help($path, $arg) { function search_theme() { return array( 'search_result' => array( - 'variables' => array('result' => NULL, 'module' => NULL), + 'variables' => array('result' => NULL, 'plugin_id' => NULL), 'file' => 'search.pages.inc', 'template' => 'search-result', ), 'search_results' => array( - 'variables' => array('results' => NULL, 'module' => NULL), + 'variables' => array('results' => NULL, 'plugin_id' => NULL), 'file' => 'search.pages.inc', 'template' => 'search-results', ), @@ -152,6 +153,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', @@ -168,35 +170,34 @@ function search_menu() { 'type' => MENU_VISIBLE_IN_BREADCRUMB, ); - // Add paths for searching. We add each module search path twice: once without + // Add paths for searching. We add each plugin search path twice: once without // and once with %menu_tail appended. The reason for this is that we want to // preserve keywords when switching tabs, and also to have search tabs // highlighted properly. The only way to do that within the Drupal 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(); + $default_info = search_get_default_plugin_info(); if ($default_info) { - foreach (search_get_info() as $module => $search_info) { + foreach (Drupal::service('plugin.manager.search')->getActiveDefinitions() as $plugin_id => $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($plugin_id, ''), 'access callback' => '_search_menu_access', - 'access arguments' => array($module), + 'access arguments' => array($plugin_id), 'type' => MENU_LOCAL_TASK, 'file' => 'search.pages.inc', - 'weight' => $module == $default_info['module'] ? -10 : 0, + 'weight' => $plugin_id == $default_info['id'] ? -10 : 0, ); $items["$path/%menu_tail"] = array( 'title' => $search_info['title'], 'load arguments' => array('%map', '%index'), 'page callback' => 'search_view', - 'page arguments' => array($module, 2), + 'page arguments' => array($plugin_id, 2), 'access callback' => '_search_menu_access', - 'access arguments' => array($module), + 'access arguments' => array($plugin_id), // The default local task points to its parent, but this item points to // where it should so it should not be changed. 'type' => MENU_LOCAL_TASK, @@ -216,75 +217,46 @@ function search_menu() { * Determines access for the 'search' path. */ function search_is_active() { - // This path cannot be accessed if there are no active modules. - return user_access('search content') && search_get_info(); + // This path cannot be accessed if there are no active plugins. + $account = Drupal::request()->attributes->get('_account'); + return !empty($account) && $account->hasPermission('search content') && Drupal::service('plugin.manager.search')->getActiveDefinitions(); } /** - * Returns information about available search modules. + * Returns information about the default search plugin. * - * @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). + * @return array + * The search plugin definition for the default search plugin, if any. */ -function search_get_info($all = FALSE) { - $search_hooks = &drupal_static(__FUNCTION__); - - if (!isset($search_hooks)) { - foreach (Drupal::moduleHandler()->getImplementations('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'))); -} - -/** - * Returns information about the default search module. - * - * @return - * The search_get_info() array element for the default search module, if any. - */ -function search_get_default_module_info() { - $info = search_get_info(); - $default = config('search.settings')->get('default_module'); +function search_get_default_plugin_info() { + $info = Drupal::service('plugin.manager.search')->getActiveDefinitions(); + $default = config('search.settings')->get('default_plugin'); if (isset($info[$default])) { return $info[$default]; } - // The variable setting does not match any active module, so just return - // the info for the first active module (if any). + // The config setting does not match any active plugin, so just return + // the info for the first active plugin (if any). return reset($info); } /** * Access callback: Determines access for a search page. * - * @param int $name - * The name of a search module (e.g., 'node') + * @param string $plugin_id + * The name of a search plugin (e.g., 'node_search'). * * @return bool * TRUE if a user has access to the search page; FALSE otherwise. * * @see search_menu() */ -function _search_menu_access($name) { - return user_access('search content') && (!function_exists($name . '_search_access') || module_invoke($name, 'search_access')); +function _search_menu_access($plugin_id) { + $account = Drupal::request()->attributes->get('_account'); + // @todo - remove the empty() check once we are more confident + // that the account will be populated, especially during tests. + // @see https://drupal.org/node/2032553 + $access = !empty($account) && $account->hasPermission('search content'); + return $access && Drupal::service('plugin.manager.search')->pluginAccess($plugin_id, $account); } /** @@ -292,25 +264,27 @@ function _search_menu_access($name) { * * @param $sid * (optional) The ID of the item to remove from the search index. If - * specified, $module must also be given. Omit both $sid and $module to clear + * specified, $type must also be given. Omit both $sid and $type to clear * the entire search index. - * @param $module - * (optional) The machine-readable name of the module for the item to remove - * from the search index. + * @param $type + * (optional) The plugin ID other machine-readable type for the item to + * remove from the search index. * @param $reindex * (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. */ -function search_reindex($sid = NULL, $module = NULL, $reindex = FALSE, $langcode = NULL) { - if ($module == NULL && $sid == NULL) { - Drupal::moduleHandler()->invokeAll('search_reset'); +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(); + } } else { $query = db_delete('search_dataset') ->condition('sid', $sid) - ->condition('type', $module); + ->condition('type', $type); if (!empty($langcode)) { $query->condition('langcode', $langcode); } @@ -318,19 +292,11 @@ function search_reindex($sid = NULL, $module = NULL, $reindex = FALSE, $langcode $query = db_delete('search_index') ->condition('sid', $sid) - ->condition('type', $module); + ->condition('type', $type); if (!empty($langcode)) { $query->condition('langcode', $langcode); } $query->execute(); - - // Don't remove links if re-indexing. - if (!$reindex) { - db_delete('search_node_links') - ->condition('sid', $sid) - ->condition('type', $module) - ->execute(); - } } } @@ -353,7 +319,7 @@ function search_dirty($word = NULL) { /** * Implements hook_cron(). * - * Fires hook_update_index() in all modules and cleans up dirty words. + * Fires updateIndex() in all plugins and cleans up dirty words. * * @see search_dirty() */ @@ -362,9 +328,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')->getActiveIndexingPlugins() as $plugin) { + $plugin->updateIndex(); } } @@ -547,9 +512,8 @@ function search_invoke_preprocess(&$text, $langcode = NULL) { * * @param $sid * An ID number identifying this particular item (e.g., node ID). - * @param $module - * The machine-readable name of the module that this item comes from (a - * module that implements hook_search_info()). + * @param $type + * The plugin ID or other machine-readable type of this item. * @param $text * The content of this item. Must be a piece of HTML or plain text. * @param $langcode @@ -557,20 +521,16 @@ function search_invoke_preprocess(&$text, $langcode = NULL) { * * @ingroup search */ -function search_index($sid, $module, $text, $langcode) { +function search_index($sid, $type, $text, $langcode) { $minimum_word_size = config('search.settings')->get('index.minimum_word_size'); - // Link matching - global $base_url; - $node_regexp = '@href=[\'"]?(?:' . preg_quote($base_url, '@') . '/|' . preg_quote(base_path(), '@') . ')(?:\?q=)?/?((?![a-z]+:)[^\'">]+)[\'">]@i'; - - // Multipliers for scores of words inside certain HTML tags. The weights are stored - // in a variable so that modules can overwrite the default weights. + // Multipliers for scores of words inside certain HTML tags. The weights are + // stored in config so that modules can overwrite the default weights. // Note: 'a' must be included for link ranking to work. $tags = config('search.settings')->get('index.tag_weights'); - // Strip off all ignored tags to speed up processing, but insert space before/after - // them to keep word boundaries. + // Strip off all ignored tags to speed up processing, but insert space before + // and after them to keep word boundaries. $text = str_replace(array('<', '>'), array(' <', '> '), $text); $text = strip_tags($text, '<' . implode('><', array_keys($tags)) . '>'); @@ -580,14 +540,13 @@ function search_index($sid, $module, $text, $langcode) { // and begins and ends with a literal (inserting $null as required). $tag = FALSE; // Odd/even counter. Tag or no tag. - $link = FALSE; // State variable for link analyzer $score = 1; // Starting score per word $accum = ' '; // Accumulator for cleaned up data $tagstack = array(); // Stack with open tags $tagwords = 0; // Counter for consecutive words $focus = 1; // Focus state - $results = array(0 => array()); // Accumulator for words for index + $scored_words = array(); // Accumulator for words for index foreach ($split as $value) { if ($tag) { @@ -606,9 +565,6 @@ function search_index($sid, $module, $text, $langcode) { // Remove from tag stack and decrement score $score = max(1, $score - $tags[array_shift($tagstack)]); } - if ($tagname == 'a') { - $link = FALSE; - } } else { if (isset($tagstack[0]) && $tagstack[0] == $tagname) { @@ -622,20 +578,6 @@ function search_index($sid, $module, $text, $langcode) { array_unshift($tagstack, $tagname); $score += $tags[$tagname]; } - if ($tagname == 'a') { - // Check if link points to a node on this site - if (preg_match($node_regexp, $value, $match)) { - $path = Drupal::service('path.alias_manager')->getSystemPath($match[1]); - if (preg_match('!(?:node|book)/(?:view/)?([0-9]+)!i', $path, $match)) { - $linknid = $match[1]; - if ($linknid > 0) { - $link = TRUE; - $node = node_load($linknid); - $linktitle = $node->label(); - } - } - } - } } // A tag change occurred, reset counter. $tagwords = 0; @@ -643,36 +585,19 @@ function search_index($sid, $module, $text, $langcode) { else { // Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty values if ($value != '') { - if ($link) { - // Check to see if the node link text is its URL. If so, we use the target node title instead. - if (preg_match('!^https?://!i', $value)) { - $value = $linktitle; - } - } $words = search_index_split($value, $langcode); foreach ($words as $word) { // Add word to accumulator $accum .= $word . ' '; // Check wordlength if (is_numeric($word) || drupal_strlen($word) >= $minimum_word_size) { - // Links score mainly for the target. - if ($link) { - if (!isset($results[$linknid])) { - $results[$linknid] = array(); - } - $results[$linknid][] = $word; - // Reduce score of the link caption in the source. - $focus *= 0.2; - } - // Fall-through - if (!isset($results[0][$word])) { - $results[0][$word] = 0; + if (!isset($scored_words[$word])) { + $scored_words[$word] = 0; } - $results[0][$word] += $score * $focus; - + $scored_words[$word] += $score * $focus; // Focus is a decaying value in terms of the amount of unique words up to this point. // From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words. - $focus = min(1, .01 + 3.5 / (2 + count($results[0]) * .015)); + $focus = min(1, .01 + 3.5 / (2 + count($scored_words) * .015)); } $tagwords++; // Too many words inside a single tag probably mean a tag was accidentally left open. @@ -686,21 +611,21 @@ function search_index($sid, $module, $text, $langcode) { $tag = !$tag; } - search_reindex($sid, $module, TRUE, $langcode); + search_reindex($sid, $type, TRUE, $langcode); // Insert cleaned up data into dataset db_insert('search_dataset') ->fields(array( 'sid' => $sid, 'langcode' => $langcode, - 'type' => $module, + 'type' => $type, 'data' => $accum, 'reindex' => 0, )) ->execute(); // Insert results into search index - foreach ($results[0] as $word => $score) { + foreach ($scored_words as $word => $score) { // If a word already exists in the database, its score gets increased // appropriately. If not, we create a new record with the appropriate // starting score. @@ -709,147 +634,33 @@ function search_index($sid, $module, $text, $langcode) { 'word' => $word, 'sid' => $sid, 'langcode' => $langcode, - 'type' => $module, + 'type' => $type, )) ->fields(array('score' => $score)) ->expression('score', 'score + :score', array(':score' => $score)) ->execute(); search_dirty($word); } - unset($results[0]); - - // Get all previous links from this item. - $result = db_query("SELECT nid, caption FROM {search_node_links} WHERE sid = :sid AND type = :type", array( - ':sid' => $sid, - ':type' => $module - ), array('target' => 'slave')); - $links = array(); - foreach ($result as $link) { - $links[$link->nid] = $link->caption; - } - - // Now store links to nodes. - foreach ($results as $nid => $words) { - $caption = implode(' ', $words); - if (isset($links[$nid])) { - if ($links[$nid] != $caption) { - // Update the existing link and mark the node for reindexing. - db_update('search_node_links') - ->fields(array('caption' => $caption)) - ->condition('sid', $sid) - ->condition('type', $module) - ->condition('nid', $nid) - ->execute(); - search_touch_node($nid); - } - // Unset the link to mark it as processed. - unset($links[$nid]); - } - elseif ($sid != $nid || $module != 'node') { - // Insert the existing link and mark the node for reindexing, but don't - // reindex if this is a link in a node pointing to itself. - db_insert('search_node_links') - ->fields(array( - 'caption' => $caption, - 'sid' => $sid, - 'type' => $module, - 'nid' => $nid, - )) - ->execute(); - search_touch_node($nid); - } - } - // Any left-over links in $links no longer exist. Delete them and mark the nodes for reindexing. - foreach ($links as $nid => $caption) { - db_delete('search_node_links') - ->condition('sid', $sid) - ->condition('type', $module) - ->condition('nid', $nid) - ->execute(); - search_touch_node($nid); - } } /** - * Changes a node's changed timestamp to 'now' to force reindexing. + * Changes the timestamp on an indexed item to 'now' to force reindexing. * - * @param $nid - * The node ID of the node that needs reindexing. + * @param $type + * The plugin ID or other machine-readable type of this item. + * @param $sid + * An ID number identifying this particular item (e.g., node ID). */ -function search_touch_node($nid) { +function search_mark_for_reindex($type, $sid) { db_update('search_dataset') ->fields(array('reindex' => REQUEST_TIME)) - ->condition('type', 'node') - ->condition('sid', $nid) + ->condition('type', $type) + ->condition('sid', $sid) ->execute(); } /** - * Implements hook_node_update_index(). - */ -function search_node_update_index(EntityInterface $node) { - // Transplant links to a node into the target node. - $result = db_query("SELECT caption FROM {search_node_links} WHERE nid = :nid", array(':nid' => $node->id()), array('target' => 'slave')); - $output = array(); - foreach ($result as $link) { - $output[] = $link->caption; - } - if (count($output)) { - return '(' . implode(', ', $output) . ')'; - } -} - -/** - * Implements hook_node_update(). - */ -function search_node_update(EntityInterface $node) { - // Reindex the node when it is updated. The node is automatically indexed - // when it is added, simply by being added to the node table. - search_touch_node($node->id()); -} - -/** - * Implements hook_comment_insert(). - */ -function search_comment_insert($comment) { - // Reindex the node when comments are added. - search_touch_node($comment->nid->target_id); -} - -/** - * Implements hook_comment_update(). - */ -function search_comment_update($comment) { - // Reindex the node when comments are changed. - search_touch_node($comment->nid->target_id); -} - -/** - * Implements hook_comment_delete(). - */ -function search_comment_delete($comment) { - // Reindex the node when comments are deleted. - search_touch_node($comment->nid->target_id); -} - -/** - * Implements hook_comment_publish(). - */ -function search_comment_publish($comment) { - // Reindex the node when comments are published. - search_touch_node($comment->nid->target_id); -} - -/** - * Implements hook_comment_unpublish(). - */ -function search_comment_unpublish($comment) { - // Reindex the node when comments are unpublished. - search_touch_node($comment->nid->target_id); -} - -/** - * Extracts a module-specific search option from a search expression. + * Extracts a type-specific search option from a search expression. * * Search options are added using search_expression_insert(), and retrieved * using search_expression_extract(). They take the form option:value, and @@ -906,6 +717,8 @@ function search_expression_insert($expression, $option, $value = NULL) { * data. Most of the system is handled by the Search module, so this must be * enabled for all of the search features to work. * + * To be discovered, the plugins must implement \Drupal\search\Plugin\SearchInterface. + * * There are three ways to interact with the search system: * - Specifically for searching nodes, you can implement * hook_node_update_index() and hook_node_search_result(). However, note that @@ -913,30 +726,27 @@ function search_expression_insert($expression, $option, $value = NULL) { * everything displayed normally by hook_node_view(). This is * usually sufficient. You should only use this mechanism if you want * additional, non-visible data to be indexed. - * - Implement hook_search_info(). This will create a search tab for your - * module on the /search page with a simple keyword search form. You will - * also need to implement hook_search_execute() to perform the search. - * - Implement hook_update_index(). This allows your module to use Drupal's - * HTML indexing mechanism for searching full text efficiently. + * - Define a plugin implementing \Drupal\search\Plugin\SearchInterface. 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. * * If your module needs to provide a more complicated search form, then you - * need to implement it yourself without hook_search_info(). In that case, you - * should define it as a local task (tab) under the /search page (e.g. - * /search/mymodule) so that users can easily find it. + * need to implement it yourself. In that case, you may wish to define it as a + * local task (tab) under the /search page (e.g. /search/mymodule) so that users + * can easily find it. */ /** * Form constructor for the search form. * + * @param Drupal\search\Plugin\SearchInterface $plugin + * A search plugin instance to render the form for. * @param $action * Form action. Defaults to "search/$path", where $path is the search path - * associated with the module in its hook_search_info(). This will be - * run through url(). - * @param $keys - * The search string entered by the user, containing keywords for the search. - * @param $module - * The search module to render the form for: a module that implements - * hook_search_info(). If not supplied, the default search module is used. + * associated with the plugin in its definition. This will be run through + * url(). * @param $prompt * Label for the keywords field. Defaults to t('Enter your keywords') if * NULL. Supply '' to omit. @@ -947,24 +757,12 @@ function search_expression_insert($expression, $option, $value = NULL) { * @see search_form_validate() * @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(); - $module_info = isset($info[$module]) ? $info[$module] : FALSE; - } +function search_form($form, &$form_state, SearchInterface $plugin, $action = '', $prompt = NULL) { - // Sanity check. - if (!$module_info) { - form_set_error(NULL, t('Search is currently disabled.'), 'error'); - return $form; - } + $plugin_info = $plugin->getPluginDefinition(); if (!$action) { - $action = 'search/' . $module_info['path']; + $action = 'search/' . $plugin_info['path']; } if (!isset($prompt)) { $prompt = t('Enter your keywords'); @@ -973,12 +771,12 @@ function search_form($form, &$form_state, $action = '', $keys = '', $module = NU $form['#action'] = url($action); // Record the $action for later use in redirecting. $form_state['action'] = $action; - $form['module'] = array('#type' => 'value', '#value' => $module); + $form['plugin_id'] = array('#type' => 'value', '#value' => $plugin->getPluginId()); $form['basic'] = array('#type' => 'container', '#attributes' => array('class' => array('container-inline'))); $form['basic']['keys'] = array( '#type' => 'search', '#title' => $prompt, - '#default_value' => $keys, + '#default_value' => $plugin->getKeywords(), '#size' => $prompt ? 40 : 20, '#maxlength' => 255, ); @@ -986,6 +784,11 @@ function search_form($form, &$form_state, $action = '', $keys = '', $module = NU // that hook into the basic search form. $form['basic']['processed_keys'] = array('#type' => 'value', '#value' => ''); $form['basic']['submit'] = array('#type' => 'submit', '#value' => t('Search')); + // Make sure the default validate and submit handlers are added. + $form['#validate'][] = 'search_form_validate'; + $form['#submit'][] = 'search_form_submit'; + // Allow the plugin to add to or alter the search form. + $plugin->searchFormAlter($form, $form_state); return $form; } @@ -1038,7 +841,7 @@ function search_box_form_submit($form, &$form_state) { } $form_id = $form['form_id']['#value']; - $info = search_get_default_module_info(); + $info = search_get_default_plugin_info(); if ($info) { $form_state['redirect'] = 'search/' . $info['path'] . '/' . trim($form_state['values'][$form_id]); } @@ -1048,34 +851,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) { - $results = module_invoke($module, 'search_execute', $keys, $conditions); - if (Drupal::moduleHandler()->implementsHook($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..804e4be 100644 --- a/core/modules/search/search.pages.inc +++ b/core/modules/search/search.pages.inc @@ -11,12 +11,12 @@ /** * Page callback: Presents the search form and/or search results. * - * @param $module - * Search module to use for the search. + * @param $plugin_id + * Search plugin_id to use for the search. * @param $keys * Keywords to use for the search. */ -function search_view($module = NULL, $keys = '') { +function search_view($plugin_id = NULL, $keys = '') { $info = FALSE; $keys = trim($keys); // Also try to pull search keywords out of the $_REQUEST variable to @@ -25,18 +25,19 @@ function search_view($module = NULL, $keys = '') { $keys = trim($_REQUEST['keys']); } - if (!empty($module)) { - $active_module_info = search_get_info(); - if (isset($active_module_info[$module])) { - $info = $active_module_info[$module]; + $manager = Drupal::service('plugin.manager.search'); + if (!empty($plugin_id)) { + $active_plugin_info = $manager->getActiveDefinitions(); + if (isset($active_plugin_info[$plugin_id])) { + $info = $active_plugin_info[$plugin_id]; } } - if (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, + 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_module_info(); + $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) { @@ -44,31 +45,28 @@ 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, - // search_form_submit() will cause a redirect to search/[module path]/[keys], + // search_form_submit() will cause a redirect to search/[path]/[keys], // which will get us back to this page callback. In other words, the search // 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. - $build['search_form'] = drupal_get_form('search_form', NULL, $keys, $info['module']); + $build['search_form'] = drupal_get_form('search_form', $plugin); $build['search_results'] = $results; return $build; @@ -82,26 +80,25 @@ function search_view($module = NULL, $keys = '') { * @param array $variables * An array with the following elements: * - results: Search results array. - * - module: Module the search results came from (module implementing - * hook_search_info()). + * - plugin_id: Plugin the search results came from. */ function template_preprocess_search_results(&$variables) { $variables['search_results'] = ''; - if (!empty($variables['module'])) { - $variables['module'] = check_plain($variables['module']); + if (!empty($variables['plugin_id'])) { + $variables['plugin_id'] = check_plain($variables['plugin_id']); } foreach ($variables['results'] as $result) { $variables['search_results'][] = array( '#theme' => 'search_result', '#result' => $result, - '#module' => $variables['module'], + '#plugin_id' => $variables['plugin_id'], ); } $variables['pager'] = array('#theme' => 'pager'); // @todo Revisit where this help text is added, see also // http://drupal.org/node/1918856. $variables['help'] = search_help('search#noresults', drupal_help_arg()); - $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module']; + $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['plugin_id']; } /** @@ -112,8 +109,7 @@ function template_preprocess_search_results(&$variables) { * @param array $variables * An array with the following elements: * - result: Individual search result. - * - module: Module the search results came from (module implementing - * hook_search_info()). + * - plugin_id: Plugin the search results came from. * - title_prefix: Additional output populated by modules, intended to be * displayed in front of the main title tag that appears in the template. * - title_suffix: Additional output populated by modules, intended to be @@ -133,8 +129,8 @@ function template_preprocess_search_result(&$variables) { } $info = array(); - if (!empty($result['module'])) { - $info['module'] = check_plain($result['module']); + if (!empty($result['plugin_id'])) { + $info['plugin_id'] = check_plain($result['plugin_id']); } if (!empty($result['user'])) { $info['user'] = $result['user']; @@ -150,7 +146,7 @@ function template_preprocess_search_result(&$variables) { // Provide separated and grouped meta information.. $variables['info_split'] = $info; $variables['info'] = implode(' - ', $info); - $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['module']; + $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['plugin_id']; } /** 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/templates/search-result.html.twig b/core/modules/search/templates/search-result.html.twig index 42cd6d4..281f8b1 100644 --- a/core/modules/search/templates/search-result.html.twig +++ b/core/modules/search/templates/search-result.html.twig @@ -13,8 +13,8 @@ * - snippet: A small preview of the result. Does not apply to user searches. * - info: String of all the meta information ready for print. Does not apply * to user searches. - * - module: The machine-readable name of the module (tab) being searched, such - * as "node" or "user". + * - plugin_id: The machine-readable name of the plugin being executed,such + * as "node_search" or "user_search". * - title_prefix: Additional output populated by modules, intended to be * displayed in front of the main title tag that appears in the template. * - title_suffix: Additional output populated by modules, intended to be diff --git a/core/modules/search/templates/search-results.html.twig b/core/modules/search/templates/search-results.html.twig index a43d162..4c14be7 100644 --- a/core/modules/search/templates/search-results.html.twig +++ b/core/modules/search/templates/search-results.html.twig @@ -13,8 +13,8 @@ * Available variables: * - search_results: All results as it is rendered through * search-result.html.twig. - * - module: The machine-readable name of the module (tab) being searched, such - * as 'node' or 'user'. + * - plugin_id: The machine-readable name of the plugin being executed, such + * as 'node_search' or 'user_search'. * - pager: The pager next/prev links to display, if any. * - help: HTML for help text to display when no results are found. * @@ -25,7 +25,7 @@ #} {% if search_results %}

{{ 'Search results'|t }}

-
    +
      {{ search_results }}
    {{ pager }} 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..673b2e4 --- /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 $parameters, array $attributes) { + if (empty($parameters['search_conditions'])) { + $parameters['search_conditions'] = ''; + } + parent::setSearch($keywords, $parameters, $attributes); + } + + /** + * Verifies if the given parameters are valid enough to execute a search for. + * + * @return bool + * A true or false depending on the implementation. + */ + public function isSearchExecutable() { + return (bool) ($this->keywords || !empty($this->searchParameters['search_conditions'])); + } + + /** + * Execute the search. + * + * This is a dummy search, so when search "executes", we just return a dummy + * result containing the keywords and a list of conditions. + * + * @return array + * A structured list of search results + */ + public function execute() { + $results = array(); + if (!$this->isSearchExecutable()) { + return $results; + } + return array( + array( + 'link' => url('node'), + 'type' => 'Dummy result type', + 'title' => 'Dummy title', + 'snippet' => "Dummy search snippet to display. Keywords: {$this->keywords}\n\nConditions: " . print_r($this->searchParameters, TRUE), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function buildResults() { + $results = $this->execute(); + $output['prefix']['#markup'] = '

    Test page text is here

      '; + + foreach ($results as $entry) { + $output[] = array( + '#theme' => 'search_result', + '#result' => $entry, + '#plugin_id' => 'search_extra_type_search', + ); + } + $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..ecc612d --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php @@ -0,0 +1,158 @@ +get('database'), + $container->get('plugin.manager.entity'), + $container->get('module_handler'), + $container->get('request'), + $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 \Symfony\Component\HttpFoundation\Request $request + * The current request. + * @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, Request $request, array $configuration, $plugin_id, array $plugin_definition) { + $this->database = $database; + $this->entityManager = $entity_manager; + $this->moduleHandler = $module_handler; + $this->request = $request; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function access($operation = 'view', AccountInterface $account = NULL) { + return !empty($account) && $account->hasPermission('access user profiles'); + } + + /** + * {@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 = $this->request->attributes->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) { + $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 0a8d4ab..48fcd7f 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() { - return user_access('access user profiles'); -} - -/** - * Implements hook_search_execute(). - */ -function user_search_execute($keys = NULL, $conditions = NULL) { - // 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) {