diff --git a/core/includes/form.inc b/core/includes/form.inc index 3c88b1e..0ade219 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -3890,36 +3890,13 @@ function theme_vertical_tabs($variables) { /** * Adds autocomplete functionality to elements with a valid #autocomplete_path. * - * Note that autocomplete callbacks should include special handling as the - * user's input may contain forward slashes. If the user-submitted string has a - * '/' in the text that is sent in the autocomplete request, the menu system - * will split the text and pass it to the callback as multiple arguments. - * * Suppose your autocomplete path in the menu system is 'mymodule_autocomplete.' * In your form you have: * @code * '#autocomplete_path' => 'mymodule_autocomplete/' . $some_key . '/' . $some_id, * @endcode * The user types in "keywords" so the full path called is: - * 'mymodule_autocomplete/$some_key/$some_id/keywords' - * - * You should include code similar to the following to handle slashes in the - * input: - * @code - * function mymodule_autocomplete_callback($arg1, $arg2, $keywords) { - * $args = func_get_args(); - * // We need to remove $arg1 and $arg2 from the beginning of the array so we - * // are left with the keywords. - * array_shift($args); - * array_shift($args); - * // We store the user's original input in $keywords, including any slashes. - * // Note: A prepended or trailing slash will be removed. For example, if the - * // user enters '/a/few/words/' then $keywords will contain 'a/few/words'. - * $keywords = implode('/', $args); - * - * // Your code here. - * } - * @endcode + * 'mymodule_autocomplete/$some_key/$some_id?q=keywords' * * @param $element * The form element to process. Properties used: diff --git a/core/misc/autocomplete.js b/core/misc/autocomplete.js index dc58b19..d272c6c 100644 --- a/core/misc/autocomplete.js +++ b/core/misc/autocomplete.js @@ -297,11 +297,13 @@ Drupal.ACDB.prototype.search = function (searchString) { this.timer = setTimeout(function () { db.owner.setStatus('begin'); - // Ajax GET request for autocompletion. We use Drupal.encodePath instead of - // encodeURIComponent to allow autocomplete search terms to contain slashes. + // Ajax GET request for autocompletion. $.ajax({ type: 'GET', - url: db.uri + '/' + Drupal.encodePath(searchString), + url: db.uri, + data: { + q: searchString + }, dataType: 'json', success: function (matches) { if (typeof matches.status === 'undefined' || matches.status !== 0) { diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/FastTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/FastTest.php index 91ed65c..843106e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/FastTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/FastTest.php @@ -39,7 +39,7 @@ function setUp() { */ function testUserAutocomplete() { $this->drupalLogin($this->account); - $this->drupalGet('user/autocomplete/' . $this->account->name); + $this->drupalGet('user/autocomplete', array('query' => array('q' => $this->account->name))); $this->assertRaw($this->account->name); $this->assertNoText('registry initialized', 'The registry was not initialized'); } diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Taxonomy.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Taxonomy.php new file mode 100644 index 0000000..345ad3e --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Taxonomy.php @@ -0,0 +1,118 @@ +container = $container; + } + + /** + * Page callback: Outputs JSON for taxonomy autocomplete suggestions. + * + * This callback outputs term name suggestions in response to Ajax requests + * made by the taxonomy autocomplete widget for taxonomy term reference + * fields. The output is a JSON object of plain-text term suggestions, keyed + * by the user-entered value with the completed term name appended. Term + * names containing commas are wrapped in quotes. + * + * For example, suppose the user has entered the string 'red fish, blue' in + * the field, and there are two taxonomy terms, 'blue fish' and 'blue moon'. + * The JSON output would have the following structure: + * @code + * { + * "red fish, blue fish": "blue fish", + * "red fish, blue moon": "blue moon", + * }; + * @endcode + * + * @param string $field_name + * The name of the term reference field. + * + * @see taxonomy_menu() + * @see taxonomy_field_widget_info() + */ + public function autocomplete($field_name) { + $tags_typed = drupal_container()->get('request')->query->get('q'); + + // Make sure the field exists and is a taxonomy field. + if (!($field = field_info_field($field_name)) || $field['type'] !== 'taxonomy_term_reference') { + // Error string. The JavaScript handler will realize this is not JSON and + // will display it as debugging information. + return new Response(t('Taxonomy field @field_name not found.', array('@field_name' => $field_name))); + } + + // The user enters a comma-separated list of tags. We only autocomplete + // the last tag. + $tags_typed = drupal_explode_tags($tags_typed); + $tag_last = drupal_strtolower(array_pop($tags_typed)); + + $matches = array(); + if ($tag_last != '') { + + // Part of the criteria for the query come from the field's own settings. + $vids = array(); + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($field['settings']['allowed_values'] as $tree) { + $vids[] = $vocabularies[$tree['vocabulary']]->vid; + } + + $query = db_select('taxonomy_term_data', 't'); + $query->addTag('translatable'); + $query->addTag('term_access'); + + // Do not select already entered terms. + if (!empty($tags_typed)) { + $query->condition('t.name', $tags_typed, 'NOT IN'); + } + // Select rows that match by term name. + $tags_return = $query + ->fields('t', array('tid', 'name')) + ->condition('t.vid', $vids) + ->condition('t.name', '%' . db_like($tag_last) . '%', 'LIKE') + ->range(0, 10) + ->execute() + ->fetchAllKeyed(); + + $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : ''; + + $term_matches = array(); + foreach ($tags_return as $tid => $name) { + $n = $name; + // Term names containing commas or quotes must be wrapped in quotes. + if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) { + $n = '"' . str_replace('"', '""', $name) . '"'; + } + $term_matches[$prefix . $n] = check_plain($name); + } + } + return new JsonResponse($term_matches); + } +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyRouteController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyRouteController.php new file mode 100644 index 0000000..922f930 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyRouteController.php @@ -0,0 +1,27 @@ +autocomplete($field_name, $tags_typed); + } +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php index 4abbf62..3c47f15 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php @@ -214,13 +214,13 @@ function testNodeTermCreationAndDeletion() { // Test autocomplete on term 3, which contains a comma. // The term will be quoted, and the " will be encoded in unicode (\u0022). $input = substr($term_objects['term3']->name, 0, 3); - $json = $this->drupalGet('taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name . '/' . $input); + $json = $this->drupalGet('taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name, array('query' => array('q' => $input))); $this->assertEqual($json, '{"\u0022' . $term_objects['term3']->name . '\u0022":"' . $term_objects['term3']->name . '"}', format_string('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term_objects['term3']->name))); // Test autocomplete on term 4 - it is alphanumeric only, so no extra // quoting. $input = substr($term_objects['term4']->name, 0, 3); - $this->drupalGet('taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name . '/' . $input); + $this->drupalGet('taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name, array('query' => array('q' => $input))); $this->assertRaw('{"' . $term_objects['term4']->name . '":"' . $term_objects['term4']->name . '"}', format_string('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term_objects['term4']->name))); // Test taxonomy autocomplete with a nonexistent field. @@ -228,7 +228,7 @@ function testNodeTermCreationAndDeletion() { $tag = $this->randomName(); $message = t("Taxonomy field @field_name not found.", array('@field_name' => $field_name)); $this->assertFalse(field_info_field($field_name), format_string('Field %field_name does not exist.', array('%field_name' => $field_name))); - $this->drupalGet('taxonomy/autocomplete/' . $field_name . '/' . $tag); + $this->drupalGet('taxonomy/autocomplete/' . $field_name, array('query' => array('q' => $tag))); $this->assertRaw($message, 'Autocomplete returns correct error message when the taxonomy field does not exist.'); } @@ -252,10 +252,9 @@ function testTermAutocompletion() { // Try to autocomplete a term name that matches both terms. // We should get both term in a json encoded string. $input = '10/'; - $path = 'taxonomy/autocomplete/taxonomy_'; - $path .= $this->vocabulary->machine_name . '/' . $input; + $path = 'taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name; // The result order is not guaranteed, so check each term separately. - $result = $this->drupalGet($path); + $result = $this->drupalGet($path, array('query' => array('q' => $input))); $data = drupal_json_decode($result); $this->assertEqual($data[$first_term->name], check_plain($first_term->name), 'Autocomplete returned the first matching term'); $this->assertEqual($data[$second_term->name], check_plain($second_term->name), 'Autocomplete returned the second matching term'); @@ -263,17 +262,15 @@ function testTermAutocompletion() { // Try to autocomplete a term name that matches first term. // We should only get the first term in a json encoded string. $input = '10/16'; - $url = 'taxonomy/autocomplete/taxonomy_'; - $url .= $this->vocabulary->machine_name . '/' . $input; - $this->drupalGet($url); + $path = 'taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name; + $this->drupalGet($path, array('query' => array('q' => $input))); $target = array($first_term->name => check_plain($first_term->name)); $this->assertRaw(drupal_json_encode($target), 'Autocomplete returns only the expected matching term.'); // Try to autocomplete a term name with both a comma and a slash. $input = '"term with, comma and / a'; - $url = 'taxonomy/autocomplete/taxonomy_'; - $url .= $this->vocabulary->machine_name . '/' . $input; - $this->drupalGet($url); + $path = 'taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name; + $this->drupalGet($path, array('query' => array('q' => $input))); $n = $third_term->name; // Term names containing commas or quotes must be wrapped in quotes. if (strpos($third_term->name, ',') !== FALSE || strpos($third_term->name, '"') !== FALSE) { diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 0158e7e..d336a4b 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -10,6 +10,10 @@ use Drupal\taxonomy\Plugin\Core\Entity\Vocabulary; use Drupal\Core\Entity\EntityInterface; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\Route; + /** * Denotes that no term in the vocabulary has a parent. */ @@ -307,14 +311,6 @@ function taxonomy_menu() { 'type' => MENU_CALLBACK, 'file' => 'taxonomy.pages.inc', ); - $items['taxonomy/autocomplete'] = array( - 'title' => 'Autocomplete taxonomy', - 'page callback' => 'taxonomy_autocomplete', - 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, - 'file' => 'taxonomy.pages.inc', - ); - $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name'] = array( 'title callback' => 'entity_page_label', 'title arguments' => array(3), diff --git a/core/modules/taxonomy/taxonomy.pages.inc b/core/modules/taxonomy/taxonomy.pages.inc index 042263c..0506645 100644 --- a/core/modules/taxonomy/taxonomy.pages.inc +++ b/core/modules/taxonomy/taxonomy.pages.inc @@ -81,94 +81,3 @@ function taxonomy_term_feed(Term $term) { return node_feed($nids, $channel); } -/** - * Page callback: Outputs JSON for taxonomy autocomplete suggestions. - * - * This callback outputs term name suggestions in response to Ajax requests - * made by the taxonomy autocomplete widget for taxonomy term reference - * fields. The output is a JSON object of plain-text term suggestions, keyed by - * the user-entered value with the completed term name appended. Term names - * containing commas are wrapped in quotes. - * - * For example, suppose the user has entered the string 'red fish, blue' in the - * field, and there are two taxonomy terms, 'blue fish' and 'blue moon'. The - * JSON output would have the following structure: - * @code - * { - * "red fish, blue fish": "blue fish", - * "red fish, blue moon": "blue moon", - * }; - * @endcode - * - * @param $field_name - * The name of the term reference field. - * @param $tags_typed - * (optional) A comma-separated list of term names entered in the - * autocomplete form element. Only the last term is used for autocompletion. - * Defaults to '' (an empty string). - * - * @see taxonomy_menu() - * @see taxonomy_field_widget_info() - */ -function taxonomy_autocomplete($field_name, $tags_typed = '') { - // If the request has a '/' in the search text, then the menu system will have - // split it into multiple arguments, recover the intended $tags_typed. - $args = func_get_args(); - // Shift off the $field_name argument. - array_shift($args); - $tags_typed = implode('/', $args); - - // Make sure the field exists and is a taxonomy field. - if (!($field = field_info_field($field_name)) || $field['type'] !== 'taxonomy_term_reference') { - // Error string. The JavaScript handler will realize this is not JSON and - // will display it as debugging information. - print t('Taxonomy field @field_name not found.', array('@field_name' => $field_name)); - exit; - } - - // The user enters a comma-separated list of tags. We only autocomplete the last tag. - $tags_typed = drupal_explode_tags($tags_typed); - $tag_last = drupal_strtolower(array_pop($tags_typed)); - - $matches = array(); - if ($tag_last != '') { - - // Part of the criteria for the query come from the field's own settings. - $vids = array(); - $vocabularies = taxonomy_vocabulary_get_names(); - foreach ($field['settings']['allowed_values'] as $tree) { - $vids[] = $vocabularies[$tree['vocabulary']]->vid; - } - - $query = db_select('taxonomy_term_data', 't'); - $query->addTag('translatable'); - $query->addTag('term_access'); - - // Do not select already entered terms. - if (!empty($tags_typed)) { - $query->condition('t.name', $tags_typed, 'NOT IN'); - } - // Select rows that match by term name. - $tags_return = $query - ->fields('t', array('tid', 'name')) - ->condition('t.vid', $vids) - ->condition('t.name', '%' . db_like($tag_last) . '%', 'LIKE') - ->range(0, 10) - ->execute() - ->fetchAllKeyed(); - - $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : ''; - - $term_matches = array(); - foreach ($tags_return as $tid => $name) { - $n = $name; - // Term names containing commas or quotes must be wrapped in quotes. - if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) { - $n = '"' . str_replace('"', '""', $name) . '"'; - } - $term_matches[$prefix . $n] = check_plain($name); - } - } - - return new JsonResponse($term_matches); -} diff --git a/core/modules/taxonomy/taxonomy.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml new file mode 100644 index 0000000..5cca0f5 --- /dev/null +++ b/core/modules/taxonomy/taxonomy.routing.yml @@ -0,0 +1,6 @@ +taxonomy_autocomplete: + pattern: '/taxonomy/autocomplete/{field_name}' + defaults: + _controller: '\Drupal\taxonomy\TaxonomyRouteController::autocomplete' + requirements: + _permission: 'access content' diff --git a/core/modules/user/lib/Drupal/user/Tests/UserAutocompleteTest.php b/core/modules/user/lib/Drupal/user/Tests/UserAutocompleteTest.php index 8f058f3..d225150 100644 --- a/core/modules/user/lib/Drupal/user/Tests/UserAutocompleteTest.php +++ b/core/modules/user/lib/Drupal/user/Tests/UserAutocompleteTest.php @@ -35,13 +35,13 @@ function setUp() { function testUserAutocomplete() { // Check access from unprivileged user, should be denied. $this->drupalLogin($this->unprivileged_user); - $this->drupalGet('user/autocomplete/' . $this->unprivileged_user->name[0]); + $this->drupalGet('user/autocomplete', array('query' => array('q' => $this->unprivileged_user->name[0]))); $this->assertResponse(403, 'Autocompletion access denied to user without permission.'); // Check access from privileged user. $this->drupalLogout(); $this->drupalLogin($this->privileged_user); - $this->drupalGet('user/autocomplete/' . $this->unprivileged_user->name[0]); + $this->drupalGet('user/autocomplete', array('query' => array('q' => $this->unprivileged_user->name[0]))); $this->assertResponse(200, 'Autocompletion access allowed.'); // Using first letter of the user's name, make sure the user's full name is in the results. diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc index d45112d..a2a2450 100644 --- a/core/modules/user/user.pages.inc +++ b/core/modules/user/user.pages.inc @@ -14,9 +14,9 @@ /** * Menu callback; Retrieve a JSON object containing autocomplete suggestions for existing users. */ -function user_autocomplete($string = '') { +function user_autocomplete() { $matches = array(); - if ($string) { + if ($string = drupal_container()->get('request')->query->get('q')) { $result = db_select('users')->fields('users', array('name'))->condition('name', db_like($string) . '%', 'LIKE')->range(0, 10)->execute(); foreach ($result as $account) { $matches[$account->name] = check_plain($account->name); diff --git a/core/modules/views/includes/ajax.inc b/core/modules/views/includes/ajax.inc index 1dff833..ee769a3 100644 --- a/core/modules/views/includes/ajax.inc +++ b/core/modules/views/includes/ajax.inc @@ -283,12 +283,15 @@ function views_ajax_form_wrapper($form_id, &$form_state) { * * @param string $string * (optional) A comma-separated list of user names entered in the - * autocomplete form element. Only the last user is used for autocompletion. - * Defaults to '' (an empty string). + * autocomplete form element. If not passed, it is taken from the 'q' query + * string parameter. * * @return Symfony\Component\HttpFoundation\JsonResponse */ -function views_ajax_autocomplete_user($string = '') { +function views_ajax_autocomplete_user($string = NULL) { + if (!isset($string)) { + $string = drupal_container()->get('request')->query->get('q'); + } // The user enters a comma-separated list of user name. We only autocomplete the last name. $array = drupal_explode_tags($string); @@ -328,14 +331,11 @@ function views_ajax_autocomplete_user($string = '') { * @param $vid * The vocabulary id of the tags which should be returned. * - * @param $tags_typed - * The typed string of the user. - * * @see taxonomy_autocomplete() */ -function views_ajax_autocomplete_taxonomy($vid, $tags_typed = '') { +function views_ajax_autocomplete_taxonomy($vid) { // The user enters a comma-separated list of tags. We only autocomplete the last tag. - $tags_typed = drupal_explode_tags($tags_typed); + $tags_typed = drupal_explode_tags(drupal_container()->get('request')->query->get('q')); $tag_last = drupal_strtolower(array_pop($tags_typed)); $matches = array(); diff --git a/core/modules/views/views.module b/core/modules/views/views.module index ac75dd7..2227e31 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -417,7 +417,7 @@ function views_menu() { ); // Define another taxonomy autocomplete because the default one of drupal // does not support a vid a argument anymore - $items['admin/views/ajax/autocomplete/taxonomy'] = array( + $items['admin/views/ajax/autocomplete/taxonomy/%'] = array( 'page callback' => 'views_ajax_autocomplete_taxonomy', 'theme callback' => 'ajax_base_page_theme', 'access callback' => 'user_access', diff --git a/core/modules/views/views_ui/admin.inc b/core/modules/views/views_ui/admin.inc index 7f08067..67ff2c2 100644 --- a/core/modules/views/views_ui/admin.inc +++ b/core/modules/views/views_ui/admin.inc @@ -2096,8 +2096,11 @@ function views_ui_edit_display_form_change_theme($form, &$form_state) { /** * Page callback for views tag autocomplete */ -function views_ui_autocomplete_tag($string = '') { +function views_ui_autocomplete_tag($string = NULL) { $matches = array(); + if (!isset($string)) { + $string = drupal_container()->get('request')->query->get('q'); + } // get matches from default views: $views = views_get_all_views(); foreach ($views as $view) {