Commit 4e871329 authored by webchick's avatar webchick
Browse files

Issue #1366020 by pwolanin, dawehner, rcaracaus, jhodgdon | tstoeckler:...

Issue #1366020 by pwolanin, dawehner, rcaracaus, jhodgdon | tstoeckler: Overhaul SearchQuery; make search redirects use GET query params for keywords.
parent d4ff24a6
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
use Drupal\node\NodeInterface; use Drupal\node\NodeInterface;
use Drupal\search\Plugin\ConfigurableSearchPluginBase; use Drupal\search\Plugin\ConfigurableSearchPluginBase;
use Drupal\search\Plugin\SearchIndexingInterface; use Drupal\search\Plugin\SearchIndexingInterface;
use Drupal\Search\SearchQuery;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
...@@ -157,6 +158,17 @@ public function access($operation = 'view', AccountInterface $account = NULL) { ...@@ -157,6 +158,17 @@ public function access($operation = 'view', AccountInterface $account = NULL) {
return !empty($account) && $account->hasPermission('access content'); return !empty($account) && $account->hasPermission('access content');
} }
/**
* {@inheritdoc}
*/
public function isSearchExecutable() {
// Node search is executable if we have keywords or an advanced parameter.
// At least, we should parse out the parameters and see if there are any
// keyword matches in that case, rather than just printing out the
// "Please enter keywords" message.
return !empty($this->keywords) || (isset($this->searchParameters['f']) && count($this->searchParameters['f']));
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -182,7 +194,8 @@ public function execute() { ...@@ -182,7 +194,8 @@ public function execute() {
// the URL: ?f[]=type:page&f[]=term:27&f[]=term:13&f[]=langcode:en // the URL: ?f[]=type:page&f[]=term:27&f[]=term:13&f[]=langcode:en
// So $parameters['f'] looks like: // So $parameters['f'] looks like:
// array('type:page', 'term:27', 'term:13', 'langcode:en'); // array('type:page', 'term:27', 'term:13', 'langcode:en');
// We need to parse this out into query conditions. // We need to parse this out into query conditions, some of which go into
// the keywords string, and some of which are separate conditions.
$parameters = $this->getParameters(); $parameters = $this->getParameters();
if (!empty($parameters['f']) && is_array($parameters['f'])) { if (!empty($parameters['f']) && is_array($parameters['f'])) {
$filters = array(); $filters = array();
...@@ -195,6 +208,7 @@ public function execute() { ...@@ -195,6 +208,7 @@ public function execute() {
$filters[$m[1]][$m[2]] = $m[2]; $filters[$m[1]][$m[2]] = $m[2];
} }
} }
// Now turn these into query conditions. This assumes that everything in // Now turn these into query conditions. This assumes that everything in
// $filters is a known type of advanced search. // $filters is a known type of advanced search.
foreach ($filters as $option => $matched) { foreach ($filters as $option => $matched) {
...@@ -211,15 +225,11 @@ public function execute() { ...@@ -211,15 +225,11 @@ public function execute() {
} }
} }
} }
// Only continue if the first pass query matches.
if (!$query->executeFirstPass()) {
return array();
}
// Add the ranking expressions. // Add the ranking expressions.
$this->addNodeRankings($query); $this->addNodeRankings($query);
// Load results. // Run the query and load results.
$find = $query $find = $query
// Add the language code of the indexed item to the result of the 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. // since the node will be rendered using the respective language.
...@@ -231,6 +241,21 @@ public function execute() { ...@@ -231,6 +241,21 @@ public function execute() {
->limit(10) ->limit(10)
->execute(); ->execute();
// Check query status and set messages if needed.
$status = $query->getStatus();
if ($status & SearchQuery::EXPRESSIONS_IGNORED) {
drupal_set_message($this->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => $this->searchSettings->get('and_or_limit'))), 'warning');
}
if ($status & SearchQuery::LOWER_CASE_OR) {
drupal_set_message($this->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'), 'warning');
}
if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) {
drupal_set_message(\Drupal::translation()->formatPlural($this->searchSettings->get('index.minimum_word_size'), 'You must include at least one positive keyword with 1 character or more.', 'You must include at least one positive keyword with @count characters or more.'), 'warning');
}
$node_storage = $this->entityManager->getStorage('node'); $node_storage = $this->entityManager->getStorage('node');
$node_render = $this->entityManager->getViewBuilder('node'); $node_render = $this->entityManager->getViewBuilder('node');
...@@ -370,7 +395,7 @@ public function indexStatus() { ...@@ -370,7 +395,7 @@ public function indexStatus() {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function searchFormAlter(array &$form, array &$form_state) { public function searchFormAlter(array &$form, array &$form_state) {
// Add keyword boxes. // Add advanced search keyword-related boxes.
$form['advanced'] = array( $form['advanced'] = array(
'#type' => 'details', '#type' => 'details',
'#title' => t('Advanced search'), '#title' => t('Advanced search'),
...@@ -445,25 +470,18 @@ public function searchFormAlter(array &$form, array &$form_state) { ...@@ -445,25 +470,18 @@ public function searchFormAlter(array &$form, array &$form_state) {
'#options' => $language_options, '#options' => $language_options,
); );
} }
// Add a submit handler.
$form['#submit'][] = array($this, 'searchFormSubmit');
} }
/** /*
* Handles submission of elements added in searchFormAlter(). * {@inheritdoc}
*
* @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) { public function buildSearchUrlQuery($form_state) {
// Initialize using any existing basic search keywords. // Read keyword and advanced search information from the form values,
$keys = $form_state['values']['processed_keys']; // and put these into the GET parameters.
$filters = array(); $keys = trim($form_state['values']['keys']);
// Collect extra restrictions. // Collect extra filters.
$filters = array();
if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) { if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) {
// Retrieve selected types - Form API sets the value of unselected // Retrieve selected types - Form API sets the value of unselected
// checkboxes to 0. // checkboxes to 0.
...@@ -499,21 +517,17 @@ public function searchFormSubmit(array &$form, array &$form_state) { ...@@ -499,21 +517,17 @@ public function searchFormSubmit(array &$form, array &$form_state) {
if ($form_state['values']['phrase'] != '') { if ($form_state['values']['phrase'] != '') {
$keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"'; $keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"';
} }
if (!empty($keys)) { $keys = trim($keys);
form_set_value($form['basic']['processed_keys'], trim($keys), $form_state);
} // Put the keywords and advanced parameters into GET parameters. Make sure
$options = array(); // to put keywords into the query even if it is empty, because the page
// controller uses that to decide it's time to check for search results.
$query = array('keys' => $keys);
if ($filters) { if ($filters) {
$options['query'] = array('f' => $filters); $query['f'] = $filters;
} }
$form_state['redirect_route'] = array( return $query;
'route_name' => 'search.view_' . $form_state['search_page_id'],
'route_parameters' => array(
'keys' => $keys,
),
'options' => $options,
);
} }
/** /**
......
...@@ -51,42 +51,42 @@ public static function create(ContainerInterface $container) { ...@@ -51,42 +51,42 @@ public static function create(ContainerInterface $container) {
* The request object. * The request object.
* @param \Drupal\search\SearchPageInterface $entity * @param \Drupal\search\SearchPageInterface $entity
* The search page entity. * The search page entity.
* @param string $keys
* (optional) Search keywords, defaults to an empty string.
* *
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse * @return array
* The search form and search results or redirect response. * The search form and search results build array.
*/ */
public function view(Request $request, SearchPageInterface $entity, $keys = '') { public function view(Request $request, SearchPageInterface $entity) {
// Also try to pull search keywords from the request to support old GET $build = array();
// format of searches for existing links. $plugin = $entity->getPlugin();
if (!$keys && $request->query->has('keys')) {
$keys = $request->query->get('keys'); // Build the form first, because it may redirect during the submit,
// and we don't want to build the results based on last time's request.
if ($request->query->has('keys')) {
$keys = trim($request->get('keys'));
$plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
} }
$keys = trim($keys);
$build['#title'] = $this->t('Search');
$plugin = $entity->getPlugin(); $build['search_form'] = $this->entityFormBuilder()->getForm($entity, 'search');
$plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
$results = array();
// Process the search form. Note that if there is // Build search results, if keywords or other search parameters are in the
// \Drupal::request()->request data, search_form_submit() will cause a // GET parameters. Note that we need to try the search if 'keys' is in
// redirect to search/[path]/[keys], which will get us back to this page // there at all, vs. being empty, due to advanced search.
// callback. In other words, the search form submits with POST but redirects $results = array();
// to GET. This way we can keep the search query URL clean as a whistle. if ($request->query->has('keys')) {
if ($request->request->has('form_id') || $request->request->get('form_id') != 'search_form') {
// Only search if there are keywords or non-empty conditions.
if ($plugin->isSearchExecutable()) { if ($plugin->isSearchExecutable()) {
// Log the search keys. // Log the search.
watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $entity->label()), WATCHDOG_NOTICE, $this->l(t('results'), 'search.view_' . $entity->id(), array('keys' => $keys))); watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $entity->label()), WATCHDOG_NOTICE);
// Collect the search results. // Collect the search results.
$results = $plugin->buildResults(); $results = $plugin->buildResults();
} }
else {
// The search not being executable means that no keywords or other
// conditions were entered.
drupal_set_message($this->t('Please enter some keywords.'), 'error');
}
} }
// The form may be altered based on whether the search was run.
$build['search_form'] = $this->entityFormBuilder()->getForm($entity, 'search');
if (count($results)) { if (count($results)) {
$build['search_results_title'] = array( $build['search_results_title'] = array(
'#markup' => '<h2>' . $this->t('Search results') . '</h2>', '#markup' => '<h2>' . $this->t('Search results') . '</h2>',
......
...@@ -53,7 +53,21 @@ public function getFormId() { ...@@ -53,7 +53,21 @@ public function getFormId() {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function buildForm(array $form, array &$form_state) { public function buildForm(array $form, array &$form_state) {
$form['search_block_form'] = array( // Set up the form to submit using GET to the correct search page.
$entity_id = $this->searchPageRepository->getDefaultSearchPage();
if (!$entity_id) {
$form['message'] = array(
'#markup' => $this->t('Search is currently disabled'),
);
return $form;
}
$route = 'search.view_' . $entity_id;
$form['#action'] = $this->url($route);
$form['#token'] = FALSE;
$form['#method'] = 'get';
$form['keys'] = array(
'#type' => 'search', '#type' => 'search',
'#title' => $this->t('Search'), '#title' => $this->t('Search'),
'#title_display' => 'invisible', '#title_display' => 'invisible',
...@@ -61,8 +75,14 @@ public function buildForm(array $form, array &$form_state) { ...@@ -61,8 +75,14 @@ public function buildForm(array $form, array &$form_state) {
'#default_value' => '', '#default_value' => '',
'#attributes' => array('title' => $this->t('Enter the terms you wish to search for.')), '#attributes' => array('title' => $this->t('Enter the terms you wish to search for.')),
); );
$form['actions'] = array('#type' => 'actions'); $form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => $this->t('Search')); $form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Search'),
// Prevent op from showing up in the query string.
'#name' => '',
);
return $form; return $form;
} }
...@@ -71,36 +91,6 @@ public function buildForm(array $form, array &$form_state) { ...@@ -71,36 +91,6 @@ public function buildForm(array $form, array &$form_state) {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function submitForm(array &$form, array &$form_state) { public function submitForm(array &$form, array &$form_state) {
// The search form relies on control of the redirect destination for its // This form submits to the search page, so processing happens there.
// functionality, so we override any static destination set in the request.
// See http://drupal.org/node/292565.
$request = $this->getRequest();
if ($request->query->has('destination')) {
$request->query->remove('destination');
}
// Check to see if the form was submitted empty.
// If it is empty, display an error message.
// (This method is used instead of setting #required to TRUE for this field
// because that results in a confusing error message. It would say a plain
// "field is required" because the search keywords field has no title.
// The error message would also complain about a missing #title field.)
if ($form_state['values']['search_block_form'] == '') {
drupal_set_message($this->t('Please enter some keywords.'), 'error');
}
$form_id = $form['form_id']['#value'];
if ($entity_id = $this->searchPageRepository->getDefaultSearchPage()) {
$form_state['redirect_route'] = array(
'route_name' => 'search.view_' . $entity_id,
'route_parameters' => array(
'keys' => trim($form_state['values'][$form_id]),
),
);
}
else {
drupal_set_message($this->t('Search is currently disabled.'), 'error');
}
} }
} }
...@@ -11,6 +11,12 @@ ...@@ -11,6 +11,12 @@
/** /**
* Provides a search form for site wide search. * Provides a search form for site wide search.
*
* Search plugins can define method searchFormAlter() to alter the form. If they
* have additional or substitute fields, they will need to override the form
* submit, making sure to redirect with a GET parameter of 'keys' included, to
* trigger the search being processed by the controller, and adding in any
* additional query parameters they need to execute search.
*/ */
class SearchPageForm extends EntityFormController { class SearchPageForm extends EntityFormController {
...@@ -33,8 +39,8 @@ public function getFormID() { ...@@ -33,8 +39,8 @@ public function getFormID() {
*/ */
public function form(array $form, array &$form_state) { public function form(array $form, array &$form_state) {
$plugin = $this->entity->getPlugin(); $plugin = $this->entity->getPlugin();
$form_state['search_page_id'] = $this->entity->id(); $form_state['search_page_id'] = $this->entity->id();
$form['basic'] = array( $form['basic'] = array(
'#type' => 'container', '#type' => 'container',
'#attributes' => array( '#attributes' => array(
...@@ -58,6 +64,7 @@ public function form(array $form, array &$form_state) { ...@@ -58,6 +64,7 @@ public function form(array $form, array &$form_state) {
'#type' => 'submit', '#type' => 'submit',
'#value' => $this->t('Search'), '#value' => $this->t('Search'),
); );
// Allow the plugin to add to or alter the search form. // Allow the plugin to add to or alter the search form.
$plugin->searchFormAlter($form, $form_state); $plugin->searchFormAlter($form, $form_state);
...@@ -72,29 +79,20 @@ protected function actions(array $form, array &$form_state) { ...@@ -72,29 +79,20 @@ protected function actions(array $form, array &$form_state) {
return array(); return array();
} }
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, array &$form_state) {
form_set_value($form['basic']['processed_keys'], trim($form_state['values']['keys']), $form_state);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function submitForm(array &$form, array &$form_state) { public function submitForm(array &$form, array &$form_state) {
$keys = $form_state['values']['processed_keys']; // Redirect to the search page with keywords in the GET parameters.
if ($keys == '') { // Plugins with additional search parameters will need to provide their
$this->setFormError('keys', $form_state, $this->t('Please enter some keywords.')); // own form submit handler to replace this, so they can put their values
// Fall through to the form redirect. // into the GET as well. If so, make sure to put 'keys' into the GET
} // parameters so that the search results generation is triggered.
$query = $this->entity->getPlugin()->buildSearchUrlQuery($form_state);
$route = 'search.view_' . $form_state['search_page_id'];
$form_state['redirect_route'] = array( $form_state['redirect_route'] = array(
'route_name' => 'search.view_' . $this->entity->id(), 'route_name' => $route,
'route_parameters' => array( 'options' => array('query' => $query),
'keys' => $keys,
),
); );
} }
} }
...@@ -20,7 +20,7 @@ interface SearchInterface extends PluginInspectionInterface { ...@@ -20,7 +20,7 @@ interface SearchInterface extends PluginInspectionInterface {
* @param string $keywords * @param string $keywords
* The keywords to use in a search. * The keywords to use in a search.
* @param array $parameters * @param array $parameters
* Array of parameters as am associative array. This is expected to * Array of parameters as an associative array. This is expected to
* be the query string from the current request. * be the query string from the current request.
* @param array $attributes * @param array $attributes
* Array of attributes, usually from the current request object. * Array of attributes, usually from the current request object.
...@@ -85,9 +85,9 @@ public function buildResults(); ...@@ -85,9 +85,9 @@ public function buildResults();
* Alters the search form when being built for a given plugin. * Alters the search form when being built for a given plugin.
* *
* The core search module only invokes this method on active module plugins * The core search module only invokes this method on active module plugins
* when building a form for them in search_form(). A plugin implementing * when building a form for them in
* this needs to add validate and submit callbacks to the form if it needs * \Drupal\search\Form\SearchPageForm::form(). A plugin implementing this
* to act after form submission. * will also need to implement the buildSearchUrlQuery() method.
* *
* @param array $form * @param array $form
* Nested array of form elements that comprise the form. * Nested array of form elements that comprise the form.
...@@ -95,7 +95,30 @@ public function buildResults(); ...@@ -95,7 +95,30 @@ public function buildResults();
* A keyed array containing the current state of the form. The arguments * A keyed array containing the current state of the form. The arguments
* that \Drupal::formBuilder()->getForm() was originally called with are * that \Drupal::formBuilder()->getForm() was originally called with are
* available in the array $form_state['build_info']['args']. * available in the array $form_state['build_info']['args'].
*
* @see SearchInterface::buildSearchUrlQuery()
*/ */
public function searchFormAlter(array &$form, array &$form_state); public function searchFormAlter(array &$form, array &$form_state);
/**
* Builds the URL GET query parameters array for search.
*
* When the search form is submitted, a redirect is generated with the
* search input as GET query parameters. Plugins using the searchFormAlter()
* method to add form elements to the search form will need to override this
* method to gather the form input and add it to the GET query parameters.
*
* @param array $form_state
* The form state, with submitted form information.
*
* @return array
* An array of GET query parameters containing all relevant form values
* to process the search. The 'keys' element must be present in order to
* trigger generation of search results, even if it is empty or unused by
* the search plugin.
*
* @see SearchInterface::searchFormAlter()
*/
public function buildSearchUrlQuery($form_state);
} }
...@@ -101,11 +101,21 @@ public function buildResults() { ...@@ -101,11 +101,21 @@ public function buildResults() {
return $built; return $built;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function searchFormAlter(array &$form, array &$form_state) { public function searchFormAlter(array &$form, array &$form_state) {
// Empty default implementation. // Empty default implementation.
} }
/*
* {@inheritdoc}
*/
public function buildSearchUrlQuery($form_state) {
// Grab the keywords entered in the form and put them as 'keys' in the GET.
$keys = trim($form_state['values']['keys']);
$query = array('keys' => $keys);
return $query;
}
} }
...@@ -77,15 +77,13 @@ public function routes() { ...@@ -77,15 +77,13 @@ public function routes() {
$active_pages = $this->searchPageRepository->getActiveSearchPages(); $active_pages = $this->searchPageRepository->getActiveSearchPages();
foreach ($active_pages as $entity_id => $entity) { foreach ($active_pages as $entity_id => $entity) {
$routes["search.view_$entity_id"] = new Route( $routes["search.view_$entity_id"] = new Route(
'/search/' . $entity->getPath() . '/{keys}', '/search/' . $entity->getPath(),
array( array(
'_content' => 'Drupal\search\Controller\SearchController::view', '_content' => 'Drupal\search\Controller\SearchController::view',
'_title' => $entity->label(), '_title' => 'Search',
'entity' => $entity_id, 'entity' => $entity_id,
'keys' => '',
), ),
array( array(
'keys' => '.+',
'_entity_access' => 'entity.view',