From 2141a64e341b9880b7b32ec7456554170dd21ec6 Mon Sep 17 00:00:00 2001 From: Dries Buytaert Date: Wed, 11 Aug 2010 14:21:39 +0000 Subject: - Patch #497206 by jhodgdon, pwolanin, Island Usurper, YannickD: avoid search conflicts with other forms, use menu API instead of search_get_keys(). --- modules/search/search.api.php | 43 +++++++- modules/search/search.module | 25 ++--- modules/search/search.pages.inc | 58 +++++----- modules/search/search.test | 128 +++++++++++++++++++++++ modules/search/tests/search_embedded_form.info | 8 ++ modules/search/tests/search_embedded_form.module | 70 +++++++++++++ modules/search/tests/search_extra_type.module | 19 +++- 7 files changed, 300 insertions(+), 51 deletions(-) create mode 100644 modules/search/tests/search_embedded_form.info create mode 100644 modules/search/tests/search_embedded_form.module (limited to 'modules/search') diff --git a/modules/search/search.api.php b/modules/search/search.api.php index a33bfaf83..da3b7b839 100644 --- a/modules/search/search.api.php +++ b/modules/search/search.api.php @@ -30,9 +30,16 @@ * node_form_search_form_alter() for an example. * * @return - * Array with the optional keys 'title' for the tab title and 'path' for - * the path component after 'search/'. Both will default to the module - * name. + * 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': Name of a callback function that 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. Sample + * callback function: sample_search_conditions_callback(). * * @ingroup search */ @@ -40,9 +47,35 @@ function hook_search_info() { return array( 'title' => 'Content', 'path' => 'node', + 'conditions_callback' => 'sample_search_conditions_callback', ); } +/** + * An example conditions callback function for search. + * + * 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. + * + * @see hook_search_info() + * @ingroup search + */ +function sample_search_conditions_callback($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 = variable_get('sample_search_force_keywords', '')) { + $conditions['sample_search_force_keywords'] = $force_keys; + } + return $conditions; +} + /** * Define access to a custom search routine. * @@ -139,6 +172,8 @@ function hook_search_admin() { * * @param $keys * The search keywords as entered by the user. + * @param $conditions + * An optional array of additional conditions, such as filters. * * @return * An array of search results. To use the default search result @@ -154,7 +189,7 @@ function hook_search_admin() { * * @ingroup search */ -function hook_search_execute($keys = NULL) { +function hook_search_execute($keys = NULL, $conditions = NULL) { // Build matching conditions $query = db_select('search_index', 'i', array('target' => 'slave'))->extend('SearchQuery')->extend('PagerDefault'); $query->join('node', 'n', 'n.nid = i.sid'); diff --git a/modules/search/search.module b/modules/search/search.module index 82d6a1b12..981256dc4 100644 --- a/modules/search/search.module +++ b/modules/search/search.module @@ -208,7 +208,7 @@ function search_menu() { $items[$path] = array( 'title' => $search_info['title'], 'page callback' => 'search_view', - 'page arguments' => array($module), + 'page arguments' => array($module, ''), 'access callback' => '_search_menu_access', 'access arguments' => array($module), 'type' => MENU_LOCAL_TASK, @@ -218,7 +218,7 @@ function search_menu() { $items["$path/%menu_tail"] = array( 'title' => $search_info['title'], 'page callback' => 'search_view', - 'page arguments' => array($module), + 'page arguments' => array($module, 2), 'access callback' => '_search_menu_access', 'access arguments' => array($module), // The default local task points to its parent, but this item points to @@ -868,21 +868,6 @@ function search_expression_insert($keys, $option, $value = '') { return $keys; } -/** - * Helper function for grabbing search keys. - */ -function search_get_keys() { - $return = &drupal_static(__FUNCTION__); - if (!isset($return)) { - // Extract keys as remainder of path - // Note: support old GET format of searches for existing links. - $path = explode('/', $_GET['q'], 3); - $keys = empty($_REQUEST['keys']) ? '' : $_REQUEST['keys']; - $return = count($path) == 3 ? $path[2] : $keys; - } - return $return; -} - /** * @defgroup search Search interface * @{ @@ -1052,14 +1037,16 @@ function template_preprocess_search_block_form(&$variables) { * Keyword query to search on. * @param $module * Search module to search. + * @param $conditions + * Optional array of additional search conditions. * * @return * Formatted search results. No return value if $keys are not supplied or * if the given search module is not active. */ -function search_data($keys, $module) { +function search_data($keys, $module, $conditions = NULL) { if (module_hook($module, 'search_execute')) { - $results = module_invoke($module, 'search_execute', $keys); + $results = module_invoke($module, 'search_execute', $keys, $conditions); if (module_hook($module, 'search_page')) { return module_invoke($module, 'search_page', $results); } diff --git a/modules/search/search.pages.inc b/modules/search/search.pages.inc index 0d9fb4bc3..a5eca2d21 100644 --- a/modules/search/search.pages.inc +++ b/modules/search/search.pages.inc @@ -11,10 +11,18 @@ * * @param $module * Search module to use for the search. + * @param $keys + * Keywords to use for the search. */ -function search_view($module = NULL) { +function search_view($module = NULL, $keys = '') { $info = FALSE; $redirect = FALSE; + $keys = trim($keys); + // Also try to pull search keywords out of the $_REQUEST variable to + // support old GET format of searches for existing links. + if (!$keys && !empty($_REQUEST['keys'])) { + $keys = trim($_REQUEST['keys']); + } if (!empty($module)) { $active_module_info = search_get_info(); @@ -28,40 +36,40 @@ function search_view($module = NULL) { // are no enabled search modules, this function should never be called, // since hook_menu() would not have defined any search paths. $info = search_get_default_module_info(); - $redirect = TRUE; + // Redirect from bare /search or an invalid path to the default search path. + $path = 'search/' . $info['path']; + if ($keys) { + $path .= '/' . $keys; + } + drupal_goto($path); } - // Search form submits with POST but redirects to GET. This way we can keep - // the search query URL clean as a whistle: - // search/type_path/keyword+keyword - if (!isset($_POST['form_id'])) { - $keys = search_get_keys(); - if ($redirect) { - // Redirect from bare /search or an invalid path to the default search path. - $path = 'search/' . $info['path']; - if ($keys) { - $path .= '/' . $keys; - } - drupal_goto($path); + $results = ''; + // Process the search form. Note that if there is $_POST data, + // search_form_submit() will cause a redirect to search/[module 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']) && function_exists($info['conditions_callback'])) { + // Build an optional array of more search conditions. + $conditions = call_user_func($info['conditions_callback'], $keys); } - // Only perform search if there is a non-whitespace search term. - $results = ''; - if (trim($keys)) { + // Only search if there are keywords or non-empty conditions. + if ($keys || !empty($conditions)) { // 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']); - - // Construct the search form. - $build['search_form'] = drupal_get_form('search_form', NULL, $keys, $info['module']); - $build['search_results'] = array('#markup' => $results); - - return $build; + $results = search_data($keys, $info['module'], $conditions); } } + // 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_results'] = array('#markup' => $results); - return drupal_get_form('search_form', NULL, empty($keys) ? '' : $keys, $info['module']); + return $build; } /** diff --git a/modules/search/search.test b/modules/search/search.test index c43f8d7d2..ded36a993 100644 --- a/modules/search/search.test +++ b/modules/search/search.test @@ -988,6 +988,58 @@ class SearchSimplifyTestCase extends DrupalWebTestCase { } } + +/** + * Test config page. + */ +class SearchKeywordsConditions extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Keywords and conditions', + 'description' => 'Verify the search pulls in keywords and extra conditions.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'search_extra_type'); + // Create searching user. + $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'post comments without approval')); + // Login with sufficient privileges. + $this->drupalLogin($this->searching_user); + // Test with all search modules enabled. + variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type')); + menu_rebuild(); + } + + /** + * Verify the kewords are captured and conditions respected. + */ + function testSearchKeyswordsConditions() { + // No keys, not conditions - no results. + $this->drupalGet('search/dummy_path'); + $this->assertNoText('Dummy search snippet to display'); + // With keys - get results. + $keys = 'bike shed ' . $this->randomName(); + $this->drupalGet("search/dummy_path/{$keys}"); + $this->assertText("Dummy search snippet to display. Keywords: {$keys}"); + $keys = 'blue drop ' . $this->randomName(); + $this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys))); + $this->assertText("Dummy search snippet to display. Keywords: {$keys}"); + // Add some conditions and keys. + $keys = 'moving drop ' . $this->randomName(); + $this->drupalGet("search/dummy_path/bike", array('query' => array('search_conditions' => $keys))); + $this->assertText("Dummy search snippet to display."); + $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE)); + // Add some conditions and no keys. + $keys = 'drop kick ' . $this->randomName(); + $this->drupalGet("search/dummy_path", array('query' => array('search_conditions' => $keys))); + $this->assertText("Dummy search snippet to display."); + $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE)); + } +} + /** * Test config page. */ @@ -1335,3 +1387,79 @@ class SearchTokenizerTestCase extends DrupalWebTestCase { return ''; } } + +/** + * Tests that we can embed a form in search results and submit it. + */ +class SearchEmbedForm extends DrupalWebTestCase { + /** + * Node used for testing. + */ + public $node; + + /** + * Count of how many times the form has been submitted. + */ + public $submit_count = 0; + + public static function getInfo() { + return array( + 'name' => 'Embedded forms', + 'description' => 'Verifies that a form embedded in search results works', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'search_embedded_form'); + + // Create a user and a node, and update the search index. + $test_user = $this->drupalCreateUser(array('access content', 'search content', 'administer nodes')); + $this->drupalLogin($test_user); + + $this->node = $this->drupalCreateNode(); + + node_update_index(); + search_update_totals(); + + // Set up a dummy initial count of times the form has been submitted. + $this->submit_count = 12; + variable_set('search_embedded_form_submitted', $this->submit_count); + $this->refreshVariables(); + } + + /** + * Tests that the embedded form appears and can be submitted. + */ + function testEmbeddedForm() { + // First verify we can submit the form from the module's page. + $this->drupalPost('search_embedded_form', + array('name' => 'John'), + t('Send away')); + $this->assertText(t('Test form was submitted'), 'Form message appears'); + $count = variable_get('search_embedded_form_submitted', 0); + $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct'); + $this->submit_count = $count; + + // Now verify that we can see and submit the form from the search results. + $this->drupalGet('search/node/' . $this->node->title); + $this->assertText(t('Your name'), 'Form is visible'); + $this->drupalPost('search/node/' . $this->node->title, + array('name' => 'John'), + t('Send away')); + $this->assertText(t('Test form was submitted'), 'Form message appears'); + $count = variable_get('search_embedded_form_submitted', 0); + $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct'); + $this->submit_count = $count; + + // Now verify that if we submit the search form, it doesn't count as + // our form being submitted. + $this->drupalPost('search', + array('keys' => 'foo'), + t('Search')); + $this->assertNoText(t('Test form was submitted'), 'Form message does not appear'); + $count = variable_get('search_embedded_form_submitted', 0); + $this->assertEqual($this->submit_count, $count, 'Form submission count is correct'); + $this->submit_count = $count; + } +} diff --git a/modules/search/tests/search_embedded_form.info b/modules/search/tests/search_embedded_form.info new file mode 100644 index 000000000..54455134b --- /dev/null +++ b/modules/search/tests/search_embedded_form.info @@ -0,0 +1,8 @@ +; $Id$ +name = "Search embedded form" +description = "Support module for search module testing of embedded forms." +package = Testing +version = VERSION +core = 7.x +files[] = search_embedded_form.module +hidden = TRUE diff --git a/modules/search/tests/search_embedded_form.module b/modules/search/tests/search_embedded_form.module new file mode 100644 index 000000000..d80fa170a --- /dev/null +++ b/modules/search/tests/search_embedded_form.module @@ -0,0 +1,70 @@ + 'Search_Embed_Form', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('search_embedded_form_form'), + 'access arguments' => array('search content'), + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Builds a form for embedding in search results for testing. + * + * @see search_embedded_form_form_submit(). + */ +function search_embedded_form_form($form, &$form_state) { + $count = variable_get('search_embedded_form_submitted', 0); + + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Your name'), + '#maxlength' => 255, + '#default_value' => '', + '#required' => TRUE, + '#description' => t('Times form has been submitted: %count', array('%count' => $count)), + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Send away'), + ); + + $form['#submit'][] = 'search_embedded_form_form_submit'; + + return $form; +} + +/** + * Submit handler for search_embedded_form_form(). + */ +function search_embedded_form_form_submit($form, &$form_state) { + $count = variable_get('search_embedded_form_submitted', 0) + 1; + variable_set('search_embedded_form_submitted', $count); + drupal_set_message(t('Test form was submitted')); +} + +/** + * Adds the test form to search results. + */ +function search_embedded_form_preprocess_search_result(&$variables) { + $variables['snippet'] .= drupal_render(drupal_get_form('search_embedded_form_form')); +} diff --git a/modules/search/tests/search_extra_type.module b/modules/search/tests/search_extra_type.module index 0aaa9260a..1d22c46c8 100644 --- a/modules/search/tests/search_extra_type.module +++ b/modules/search/tests/search_extra_type.module @@ -13,22 +13,35 @@ function search_extra_type_search_info() { return array( 'title' => 'Dummy search type', 'path' => 'dummy_path', + 'conditions_callback' => 'search_extra_type_conditions', ); } +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. + * result containing the keywords and a list of conditions. */ -function search_extra_type_search_execute() { +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', + 'snippet' => "Dummy search snippet to display. Keywords: {$keys}\n\nConditions: " . print_r($conditions, TRUE), ), ); } -- cgit v1.2.3