diff --git a/core/core.api.php b/core/core.api.php index a8b13ca..e2ff4dd 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -2054,12 +2054,6 @@ function hook_validation_constraint_alter(array &$definitions) { * an array. Here are the details of its elements, all of which are optional: * - callback: The callback to invoke to handle the server side of the * Ajax event. More information on callbacks is below in @ref sub_callback. - * - path: The URL path to use for the request. If omitted, defaults to - * 'system/ajax', which invokes the default Drupal Ajax processing (this will - * call the callback supplied in the 'callback' element). If you supply a - * path, you must set up a routing entry to handle the request yourself and - * return output described in @ref sub_callback below. See the - * @link menu Routing topic @endlink for more information on routing. * - wrapper: The HTML 'id' attribute of the area where the content returned by * the callback should be placed. Note that callbacks have a choice of * returning content or JavaScript commands; 'wrapper' is used for content diff --git a/core/includes/pager.inc b/core/includes/pager.inc index 7ce3407..0dab506 100644 --- a/core/includes/pager.inc +++ b/core/includes/pager.inc @@ -148,7 +148,12 @@ function pager_default_initialize($total, $limit, $element = 0) { function pager_get_query_parameters() { $query = &drupal_static(__FUNCTION__); if (!isset($query)) { - $query = UrlHelper::filterQueryParameters(\Drupal::request()->query->all(), array('page')); + // There is a chance that the pager is rendered in the context of an AJAX + // form request. Therefore exclude AJAX_FORM_REQUEST from from the query + // arguments, as otherwise clicking the link would be interpreted + // potentially as an AJAX form request, especially when done as POST request + // using JS. + $query = UrlHelper::filterQueryParameters(\Drupal::request()->query->all(), array('page', \Drupal\Core\Form\FormBuilderInterface::AJAX_FORM_REQUEST)); } return $query; } diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index d966801..988779f 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -313,8 +313,11 @@ public function buildForm($form_id, FormStateInterface &$form_state) { */ public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL) { $form = $this->retrieveForm($form_id, $form_state); - // All rebuilt forms will be cached. - $form_state->setCached(); + + // We don't allow to set state on GET requests. + if (!$form_state->isMethodType('GET')) { + $form_state->setCached(); + } // If only parts of the form will be returned to the browser (e.g., Ajax or // RIA clients), or if the form already had a new build ID regenerated when diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php index 60ad704..bbfed36 100644 --- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php +++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php @@ -108,9 +108,7 @@ public function buildForm($form_id, FormStateInterface &$form_state); * form workflow, to be returned for rendering. * * Ajax form submissions are almost always multi-step workflows, so that is - * one common use-case during which form rebuilding occurs. See - * Drupal\system\FormAjaxController::content() for more information about - * creating Ajax-enabled forms. + * one common use-case during which form rebuilding occurs. * * @param string $form_id * The unique string identifying the desired form. If a function with that @@ -130,7 +128,6 @@ public function buildForm($form_id, FormStateInterface &$form_state); * The newly built form. * * @see self::processForm() - * @see \Drupal\system\FormAjaxController::content() */ public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL); diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php index 4a17d6b..94a5df1 100644 --- a/core/lib/Drupal/Core/Form/FormState.php +++ b/core/lib/Drupal/Core/Form/FormState.php @@ -141,16 +141,16 @@ class FormState implements FormStateInterface { /** * The HTTP form method to use for finding the input for this form. * - * May be 'post' or 'get'. Defaults to 'post'. Note that 'get' method forms do + * May be 'POST' or 'GET'. Defaults to 'POST'. Note that 'GET' method forms do * not use form ids so are always considered to be submitted, which can have - * unexpected effects. The 'get' method should only be used on forms that do - * not change data, as that is exclusively the domain of 'post.' + * unexpected effects. The 'GET' method should only be used on forms that do + * not change data, as that is exclusively the domain of 'POST.' * * This property is uncacheable. * * @var string */ - protected $method = 'post'; + protected $method = 'POST'; /** * If set to TRUE the original, unprocessed form structure will be cached, @@ -475,6 +475,10 @@ public function getButtons() { * {@inheritdoc} */ public function setCached($cache = TRUE) { + // We don't allow to cache on GET requests. + if ($this->isMethodType('GET')) { + throw new \LogicException('Caching on GET requests is not allowed.'); + } $this->cache = (bool) $cache; return $this; } diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index a3b362c..239916b 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Render\Element; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginBase; @@ -128,14 +129,7 @@ public static function preRenderGroup($element) { * @see self::preRenderAjaxForm() */ public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) { - $element = static::preRenderAjaxForm($element); - - // If the element was processed as an #ajax element, and a custom URL was - // provided, set the form to be cached. - if (!empty($element['#ajax_processed']) && !empty($element['#ajax']['url'])) { - $form_state->setCached(); - } - return $element; + return static::preRenderAjaxForm($element); } /** @@ -149,7 +143,6 @@ public static function processAjaxForm(&$element, FormStateInterface $form_state * Properties used: * - #ajax['event'] * - #ajax['prevent'] - * - #ajax['url'] * - #ajax['callback'] * - #ajax['options'] * - #ajax['wrapper'] @@ -159,6 +152,9 @@ public static function processAjaxForm(&$element, FormStateInterface $form_state * * @return array * The processed element with the necessary JavaScript attached to it. + * + * @throws \InvalidArgumentException + * Thrown when an element provides a custom #ajax URL. */ public static function preRenderAjaxForm($element) { // Skip already processed elements. @@ -173,6 +169,12 @@ public static function preRenderAjaxForm($element) { return $element; } + // If #ajax is TRUE, convert it to an empty array. Depending on the #type, + // it will still be given an event. + if (!is_array($element['#ajax'])) { + $element['#ajax'] = []; + } + // Add a reasonable default event handler if none was specified. if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) { switch ($element['#type']) { @@ -230,31 +232,28 @@ public static function preRenderAjaxForm($element) { $element['#attached']['library'][] = 'core/drupal.ajax'; $settings = $element['#ajax']; + // Do not support custom URLs, $settings['url'] is generated below. + if (isset($settings['url'])) { + throw new \InvalidArgumentException('Custom AJAX URLs are not allowed'); + } - // Assign default settings. When 'url' is set to NULL, ajax.js submits the - // Ajax request to the same URL as the form or link destination is for - // someone with JavaScript disabled. This is generally preferred as a way to - // ensure consistent server processing for js and no-js users, and Drupal's - // content negotiation takes care of formatting the response appropriately. - // However, 'url' and 'options' may be set when wanting server processing - // to be substantially different for a JavaScript triggered submission. - // One such substantial difference is form elements that use - // #ajax['callback'] for determining which part of the form needs - // re-rendering. For that, we have a special 'system.ajax' route which - // must be manually set. + // Assign default settings. $settings += [ - 'url' => NULL, - 'options' => ['query' => []], 'dialogType' => 'ajax', ]; - if (array_key_exists('callback', $settings) && !isset($settings['url'])) { - $settings['url'] = Url::fromRoute(''); + if (array_key_exists('callback', $settings)) { // Add all the current query parameters in order to ensure that we build // the same form on the AJAX POST requests. For example, // \Drupal\user\AccountForm takes query parameters into account in order // to hide the password field dynamically. - $settings['options']['query'] += \Drupal::request()->query->all(); - $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE; + $query = \Drupal::request()->query->all(); + $query[FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE; + + // Add options['query'] => [] as default value. + $settings = NestedArray::mergeDeep($settings, ['options' => ['query' => []]]); + $options['query'] = $settings['options']['query'] + $query; + + $settings['url'] = Url::fromRoute('', [], $options)->toString(); } // @todo Legacy support. Remove in Drupal 8. @@ -262,13 +261,6 @@ public static function preRenderAjaxForm($element) { $settings['method'] = 'replaceWith'; } - // Convert \Drupal\Core\Url object to string. - if (isset($settings['url']) && $settings['url'] instanceof Url) { - $settings['url'] = $settings['url']->setOptions($settings['options'])->toString(); - } - else { - $settings['url'] = NULL; - } unset($settings['options']); // Add special data to $settings['submit'] so that when this element diff --git a/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php index b15ce93..9d904ee 100644 --- a/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php +++ b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php @@ -15,12 +15,12 @@ /** * Defines a theme negotiator that deals with the active theme on ajax requests. * - * Many different pages can invoke an Ajax request to system/ajax or another - * generic Ajax path. It is almost always desired for an Ajax response to be - * rendered using the same theme as the base page, because most themes are built - * with the assumption that they control the entire page, so if the CSS for two - * themes are both loaded for a given page, they may conflict with each other. - * For example, Bartik is Drupal's default theme, and Seven is Drupal's default + * Many different pages can invoke an Ajax request to a generic Ajax path. It is + * almost always desired for an Ajax response to be rendered using the same + * theme as the base page, because most themes are built with the assumption + * that they control the entire page, so if the CSS for two themes are both + * loaded for a given page, they may conflict with each other. For example, + * Bartik is Drupal's default theme, and Seven is Drupal's default * administration theme. Depending on whether the "Use the administration theme * when editing or creating content" checkbox is checked, the node edit form may * be displayed in either theme, but the Ajax response to the Field module's diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php index 1c1b7e3..d05a4dd 100644 --- a/core/modules/file/src/Controller/FileWidgetAjaxController.php +++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php @@ -7,13 +7,12 @@ namespace Drupal\file\Controller; -use Drupal\system\Controller\FormAjaxController; use Symfony\Component\HttpFoundation\JsonResponse; /** * Defines a controller to respond to file widget AJAX requests. */ -class FileWidgetAjaxController extends FormAjaxController { +class FileWidgetAjaxController { /** * Returns the progress status for a file upload process. diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index a006284..6afeedf 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -1593,15 +1593,14 @@ protected function drupalGetAjax($path, array $options = array(), array $headers * case, this value needs to be an array with the following keys: * - path: A path to submit the form values to for Ajax-specific processing, * which is likely different than the $path parameter used for retrieving - * the initial form. Defaults to 'system/ajax'. - * - triggering_element: If the value for the 'path' key is 'system/ajax' or - * another generic Ajax processing path, this needs to be set to the name - * of the element. If the name doesn't identify the element uniquely, then - * this should instead be an array with a single key/value pair, - * corresponding to the element name and value. The callback for the - * generic Ajax processing path uses this to find the #ajax information - * for the element, including which specific callback to use for - * processing the request. + * the initial form. + * - triggering_element: If the value for the 'path' key is a generic Ajax + * processing path, this needs to be set to the name of the element. If + * the name doesn't identify the element uniquely, then this should + * instead be an array with a single key/value pair, corresponding to the + * element name and value. The callback for the generic Ajax processing + * path uses this to find the #ajax information for the element, including + * which specific callback to use for processing the request. * * This can also be set to NULL in order to emulate an Internet Explorer * submission of a form with a single text field, and pressing ENTER in that @@ -1649,7 +1648,7 @@ protected function drupalPostForm($path, $edit, $submit, array $options = array( $submit_matches = $this->handleForm($post, $edit, $upload, $ajax ? NULL : $submit, $form); $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : $this->getUrl(); if ($ajax) { - $action = $this->getAbsoluteUrl(!empty($submit['path']) ? $submit['path'] : 'system/ajax'); + $action = $this->getAbsoluteUrl(!empty($submit['path']) ? $submit['path'] : ''); // Ajax callbacks verify the triggering element if necessary, so while // we may eventually want extra code that verifies it in the // handleForm() function, it's not currently a requirement. @@ -1736,7 +1735,7 @@ protected function drupalPostForm($path, $edit, $submit, array $options = array( * @param $ajax_path * (optional) Override the path set by the Ajax settings of the triggering * element. In the absence of both the triggering element's Ajax path and - * $ajax_path 'system/ajax' will be used. + * $ajax_path '/' will be used. * @param $options * (optional) Options to be forwarded to the url generator. * @param $headers @@ -1807,7 +1806,7 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p $extra_post = '&' . $this->serializePostValues($extra_post); // Unless a particular path is specified, use the one specified by the - // Ajax settings, or else 'system/ajax'. + // Ajax settings, or else '/'. if (!isset($ajax_path)) { if (isset($ajax_settings['url'])) { // In order to allow to set for example the wrapper envelope query @@ -1824,11 +1823,8 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p $parsed_url['path'] ); } - else { - $ajax_path = 'system/ajax'; - } } - $ajax_path = $this->container->get('unrouted_url_assembler')->assemble('base://' . $ajax_path, $options); + $ajax_path = $ajax_path ? $this->container->get('unrouted_url_assembler')->assemble('base://' . $ajax_path, $options) : '/'; // Submit the POST request. $return = Json::decode($this->drupalPostForm(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post)); diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php deleted file mode 100644 index 084847a..0000000 --- a/core/modules/system/src/Controller/FormAjaxController.php +++ /dev/null @@ -1,186 +0,0 @@ -logger = $logger; - $this->formBuilder = $form_builder; - $this->renderer = $renderer; - $this->ajaxRenderer = $ajax_renderer; - $this->routeMatch = $route_match; - $this->formAjaxResponseBuilder = $form_ajax_response_builder; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('logger.factory')->get('ajax'), - $container->get('form_builder'), - $container->get('renderer'), - $container->get('main_content_renderer.ajax'), - $container->get('current_route_match'), - $container->get('form_ajax_response_builder') - ); - } - - /** - * Processes an Ajax form submission. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The current request object. - * - * @return mixed - * Whatever is returned by the triggering element's #ajax['callback'] - * function. One of: - * - A render array containing the new or updated content to return to the - * browser. This is commonly an element within the rebuilt form. - * - A \Drupal\Core\Ajax\AjaxResponse object containing commands for the - * browser to process. - * - * @throws \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface - */ - public function content(Request $request) { - $ajax_form = $this->getForm($request); - $form = $ajax_form->getForm(); - $form_state = $ajax_form->getFormState(); - $commands = $ajax_form->getCommands(); - - $this->formBuilder->processForm($form['#form_id'], $form, $form_state); - - return $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands); - } - - /** - * Gets a form submitted via #ajax during an Ajax callback. - * - * This will load a form from the form cache used during Ajax operations. It - * pulls the form info from the request body. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The current request object. - * - * @return \Drupal\system\FileAjaxForm - * A wrapper object containing the $form, $form_state, $form_id, - * $form_build_id and an initial list of Ajax $commands. - * - * @throws \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface - */ - protected function getForm(Request $request) { - $form_state = new FormState(); - $form_build_id = $request->request->get('form_build_id'); - - // Get the form from the cache. - $form = $this->formBuilder->getCache($form_build_id, $form_state); - if (!$form) { - // If $form cannot be loaded from the cache, the form_build_id must be - // invalid, which means that someone performed a POST request onto - // system/ajax without actually viewing the concerned form in the browser. - // This is likely a hacking attempt as it never happens under normal - // circumstances. - $this->logger->warning('Invalid form POST data.'); - throw new BadRequestHttpException(); - } - - // Since some of the submit handlers are run, redirects need to be disabled. - $form_state->disableRedirect(); - - // When a form is rebuilt after Ajax processing, its #build_id and #action - // should not change. - // @see \Drupal\Core\Form\FormBuilderInterface::rebuildForm() - $form_state->addRebuildInfo('copy', [ - '#build_id' => TRUE, - '#action' => TRUE, - ]); - - // The form needs to be processed; prepare for that by setting a few - // internal variables. - $form_state->setUserInput($request->request->all()); - $form_id = $form['#form_id']; - - return new FileAjaxForm($form, $form_state, $form_id, $form['#build_id'], []); - } - -} diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php index f151c33..4ebeed6 100644 --- a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php +++ b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php @@ -37,15 +37,6 @@ public function testFormCacheUsage() { // The number of cache entries should not have changed. $this->assertEqual(0, count($key_value_expirable->getAll())); - - // Visit a form that is explicitly cached, 3 times. - $cached_form_url = Url::fromRoute('ajax_forms_test.cached_form'); - $this->drupalGet($cached_form_url); - $this->drupalGet($cached_form_url); - $this->drupalGet($cached_form_url); - - // The number of cache entries should be exactly 3. - $this->assertEqual(3, count($key_value_expirable->getAll())); } /** diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php index 03727b6..136ec69 100644 --- a/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php +++ b/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php @@ -35,7 +35,7 @@ protected function getFormBuildId() { } /** - * Create a simple form, then POST to system/ajax to change to it. + * Create a simple form, then POST via AJAX to change to it. */ public function testSimpleAJAXFormValue() { $this->drupalGet('ajax_forms_test_get_form'); diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 12e7d9a..a386196 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -1,12 +1,3 @@ -system.ajax: - path: '/system/ajax' - defaults: - _controller: '\Drupal\system\Controller\FormAjaxController::content' - options: - _theme: ajax_base_page - requirements: - _access: 'TRUE' - system.401: path: '/system/401' defaults: diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml index 3caf5ba..ccca279 100644 --- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml +++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml @@ -30,10 +30,3 @@ ajax_forms_test.lazy_load_form: requirements: _access: 'TRUE' -ajax_forms_test.cached_form: - path: '/ajax_forms_test_cached_form' - defaults: - _title: 'AJAX forms cached form test' - _form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestCachedForm' - requirements: - _access: 'TRUE' diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php deleted file mode 100644 index 3b22783..0000000 --- a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php +++ /dev/null @@ -1,50 +0,0 @@ - 'select', - '#title' => $this->t('Test 1'), - '#options' => [ - 'option1' => $this->t('Option 1'), - 'option2' => $this->t('Option 2'), - ], - '#ajax' => [ - 'url' => Url::fromRoute('system.ajax'), - ], - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - } - -} diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php index 225664d..2fcfd1f 100644 --- a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php +++ b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php @@ -57,7 +57,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { ); // This is for testing invalid callbacks that should return a 500 error in - // \Drupal\system\FormAjaxController::content(). + // \Drupal\Core\Form\FormAjaxResponseBuilderInterface::buildResponse(). $invalid_callbacks = array( 'null' => NULL, 'empty' => '', diff --git a/core/modules/views/src/Plugin/views/field/Field.php b/core/modules/views/src/Plugin/views/field/Field.php index 386456c..4368efe 100644 --- a/core/modules/views/src/Plugin/views/field/Field.php +++ b/core/modules/views/src/Plugin/views/field/Field.php @@ -447,9 +447,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#title' => $this->t('Formatter'), '#options' => $formatters, '#default_value' => $this->options['type'], - '#ajax' => array( - 'url' => views_ui_build_form_url($form_state), - ), + '#ajax' => TRUE, '#submit' => array(array($this, 'submitTemporaryForm')), '#executes_submit_callback' => TRUE, ); diff --git a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php index e290789..84901d0 100644 --- a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php +++ b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php @@ -181,9 +181,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Request $ '#value' => $this->t('Remove'), '#submit' => array(array($this, 'remove')), '#limit_validation_errors' => array(array('override')), - '#ajax' => array( - 'url' => Url::fromRoute(''), - ), + '#ajax' => TRUE, '#button_type' => 'danger', ); } diff --git a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php index 06bbd0a..fc43a28 100644 --- a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php +++ b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php @@ -134,7 +134,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { 'class' => array('views-remove-group'), ), '#group' => $id, - '#ajax' => ['url' => NULL], + '#ajax' => TRUE, ); } $group_options[$id] = $id == 1 ? $this->t('Default group') : $this->t('Group @group', array('@group' => $id)); @@ -218,7 +218,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#attributes' => array( 'class' => array('views-add-group'), ), - '#ajax' => ['url' => NULL], + '#ajax' => TRUE, ); return $form; diff --git a/core/modules/views_ui/src/Tests/PreviewTest.php b/core/modules/views_ui/src/Tests/PreviewTest.php index ff10213..4771b51 100644 --- a/core/modules/views_ui/src/Tests/PreviewTest.php +++ b/core/modules/views_ui/src/Tests/PreviewTest.php @@ -278,7 +278,7 @@ protected function clickPreviewLinkAJAX($url, $row_count) { * @param int $row_count * The expected number of rows in the preview. */ - protected function assertPreviewAJAX($result, $row_count) { + protected function assertPreviewAJAX(array $result, $row_count) { // Has AJAX callback replied with an insert command? If so, we can // assume that the page content was updated with AJAX returned data. $result_commands = array(); diff --git a/core/modules/views_ui/src/ViewPreviewForm.php b/core/modules/views_ui/src/ViewPreviewForm.php index 7ec8f4d..fce89dc 100644 --- a/core/modules/views_ui/src/ViewPreviewForm.php +++ b/core/modules/views_ui/src/ViewPreviewForm.php @@ -76,7 +76,6 @@ public function form(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ protected function actions(array $form, FormStateInterface $form_state) { - $view = $this->entity; return array( '#attributes' => array( 'id' => 'preview-submit-wrapper', @@ -89,7 +88,7 @@ protected function actions(array $form, FormStateInterface $form_state) { '#submit' => array('::submitPreview'), '#id' => 'preview-submit', '#ajax' => array( - 'url' => Url::fromRoute('entity.view.preview_form', ['view' => $view->id(), 'display_id' => $this->displayID]), + 'callback' => [get_called_class(), 'previewSubmit'], 'wrapper' => 'views-preview-wrapper', 'event' => 'click', 'progress' => array('type' => 'fullscreen'), @@ -100,6 +99,19 @@ protected function actions(array $form, FormStateInterface $form_state) { } /** + * AJAX callback: Return the form as-is to let the views UI be rebuilt. + * + * @param array $form + * An associative array containing the structure of the form. + * + * @return array + * The form, with no changes. + */ + public static function previewSubmit(array $form) { + return $form; + } + + /** * Form submission handler for the Preview button. */ public function submitPreview($form, FormStateInterface $form_state) { diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index 05451be..074e612 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -12,7 +12,9 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; +use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Url; use Drupal\views\Views; use Drupal\Core\Entity\EntityStorageInterface; @@ -311,11 +313,6 @@ public function getStandardButtons(&$form, FormStateInterface $form_state, $form $names = array(t('Apply'), t('Apply and continue')); } - // Views provides its own custom handling of AJAX form submissions. Usually - // this happens at the same path, but custom paths may be specified in - // $form_state. - $form_url = $form_state->get('url') ?: Url::fromRouteMatch(\Drupal::routeMatch()); - // Forms that are purely informational set an ok_button flag, so we know not // to create an "Apply" button for them. if (!$form_state->get('ok_button')) { @@ -330,9 +327,7 @@ public function getStandardButtons(&$form, FormStateInterface $form_state, $form // take care of running the regular submit handler as appropriate. '#submit' => array(array($this, 'standardSubmit')), '#button_type' => 'primary', - '#ajax' => array( - 'url' => $form_url, - ), + '#ajax' => TRUE, ); // Form API button click detection requires the button's #value to be the // same between the form build of the initial page request, and the @@ -358,9 +353,7 @@ public function getStandardButtons(&$form, FormStateInterface $form_state, $form '#value' => !$form_state->get('ok_button') ? t('Cancel') : t('Ok'), '#submit' => array($cancel_submit), '#validate' => array(), - '#ajax' => array( - 'path' => $form_url, - ), + '#ajax' => TRUE, '#limit_validation_errors' => array(), ); @@ -550,6 +543,8 @@ public function renderPreview($display_id, $args = array()) { $request_stack = \Drupal::requestStack(); $current_request = $request_stack->getCurrentRequest(); $executable = $this->getExecutable(); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); // Determine where the query and performance statistics should be output. $config = \Drupal::config('views.settings'); @@ -578,7 +573,7 @@ public function renderPreview($display_id, $args = array()) { // have some input in the query parameters, so we merge request() and // query() to ensure we get it all. $exposed_input = array_merge(\Drupal::request()->request->all(), \Drupal::request()->query->all()); - foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { + foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', FormBuilderInterface::AJAX_FORM_REQUEST, AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { if (isset($exposed_input[$key])) { unset($exposed_input[$key]); } @@ -612,10 +607,13 @@ public function renderPreview($display_id, $args = array()) { $raw_parameters->set('display_id', $display_id); $request->attributes->set('_raw_variables', $raw_parameters); + $request->query->remove(FormBuilderInterface::AJAX_FORM_REQUEST); + foreach ($args as $key => $arg) { $request->attributes->set('arg_' . $key, $arg); } $request_stack->push($request); + $executable->setRequest($request_stack->getCurrentRequest()); // Suppress contextual links of entities within the result set during a // Preview. @@ -855,6 +853,14 @@ public function renderPreview($display_id, $args = array()) { ]; } + if (!empty($output['preview'])) { + // We render the preview now, in order to render it in the context of the + // previous setup request, because for example pager links depend on the + // "current" URL. + $renderer->render($output['preview']); + $output['preview']['#printed'] = FALSE; + } + // Ensure that we just remove an additional request we pushed earlier. // This could happen if $errors was not empty. if ($request_stack->getCurrentRequest() != $current_request) { diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php index 9e53645..c02555b 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php @@ -310,11 +310,51 @@ public function testRebuildForm() { $form_state->addRebuildInfo('copy', ['#build_id' => TRUE]); $this->formBuilder->processForm($form_id, $form, $form_state); $this->assertSame($original_build_id, $form['#build_id']); + $this->assertTrue($form_state->isCached()); // Rebuild the form again, and assert that there is a new build ID. $form_state->setRebuildInfo([]); $form = $this->formBuilder->buildForm($form_arg, $form_state); $this->assertNotSame($original_build_id, $form['#build_id']); + $this->assertTrue($form_state->isCached()); + } + + /** + * Tests the rebuildForm() method for a GET submission. + */ + public function testRebuildFormOnGetRequest() { + $form_id = 'test_form_id'; + $expected_form = $form_id(); + + // The form will be built four times. + $form_arg = $this->getMock('Drupal\Core\Form\FormInterface'); + $form_arg->expects($this->exactly(2)) + ->method('getFormId') + ->will($this->returnValue($form_id)); + $form_arg->expects($this->exactly(4)) + ->method('buildForm') + ->will($this->returnValue($expected_form)); + + // Do an initial build of the form and track the build ID. + $form_state = new FormState(); + $form_state->setMethod('GET'); + $form = $this->formBuilder->buildForm($form_arg, $form_state); + $original_build_id = $form['#build_id']; + + // Rebuild the form, and assert that the build ID has not changed. + $form_state->setRebuild(); + $input['form_id'] = $form_id; + $form_state->setUserInput($input); + $form_state->addRebuildInfo('copy', ['#build_id' => TRUE]); + $this->formBuilder->processForm($form_id, $form, $form_state); + $this->assertSame($original_build_id, $form['#build_id']); + $this->assertFalse($form_state->isCached()); + + // Rebuild the form again, and assert that there is a new build ID. + $form_state->setRebuildInfo([]); + $form = $this->formBuilder->buildForm($form_arg, $form_state); + $this->assertNotSame($original_build_id, $form['#build_id']); + $this->assertFalse($form_state->isCached()); } /** diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php index b92c5f7..aac4094 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php @@ -422,6 +422,11 @@ public function testIsCached($cache_key, $no_cache_key, $expected) { 'cache' => $cache_key, 'no_cache' => $no_cache_key, ]); + + $form_state->setMethod('POST'); + $this->assertSame($expected, $form_state->isCached()); + + $form_state->setMethod('GET'); $this->assertSame($expected, $form_state->isCached()); } @@ -464,6 +469,28 @@ public function providerTestIsCached() { } /** + * @covers ::setCached + */ + public function testSetCachedPost() { + $form_state = new FormState(); + $form_state->setMethod('POST'); + $form_state->setCached(); + $this->assertTrue($form_state->isCached()); + } + + /** + * @covers ::setCached + * + * @expectedException \LogicException + * @expectedExceptionMessage Initial form rendering is not allowed to cache the form. + */ + public function testSetCachedGet() { + $form_state = new FormState(); + $form_state->setMethod('GET'); + $form_state->setCached(); + } + + /** * @covers ::isMethodType * @covers ::setMethod * diff --git a/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php new file mode 100644 index 0000000..99abc76 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php @@ -0,0 +1,155 @@ +requestStack = new RequestStack(); + $this->container = new ContainerBuilder(); + $this->container->set('request_stack', $this->requestStack); + \Drupal::setContainer($this->container); + } + + /** + * @covers ::preRenderAjaxForm + */ + public function testPreRenderAjaxForm() { + $request = Request::create('/test'); + $request->query->set('foo', 'bar'); + $this->requestStack->push($request); + + $prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface'); + $url = '/test?foo=bar&ajax_form=1'; + $prophecy->generateFromRoute('', [], ['query' => ['foo' => 'bar', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE) + ->willReturn($url); + + $url_generator = $prophecy->reveal(); + $this->container->set('url_generator', $url_generator); + + $element = [ + '#type' => 'select', + '#id' => 'test', + '#ajax' => [ + 'wrapper' => 'foo', + 'callback' => 'test-callback', + ], + ]; + + $element = RenderElement::preRenderAjaxForm($element); + + $this->assertTrue($element['#ajax_processed']); + $this->assertEquals($url, $element['#attached']['drupalSettings']['ajax']['test']['url']); + } + + /** + * @covers ::preRenderAjaxForm + */ + public function testPreRenderAjaxFormWithQueryOptions() { + $request = Request::create('/test'); + $request->query->set('foo', 'bar'); + $this->requestStack->push($request); + + $prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface'); + $url = '/test?foo=bar&other=query&ajax_form=1'; + $prophecy->generateFromRoute('', [], ['query' => ['foo' => 'bar', 'other' => 'query', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE) + ->willReturn($url); + + $url_generator = $prophecy->reveal(); + $this->container->set('url_generator', $url_generator); + + $element = [ + '#type' => 'select', + '#id' => 'test', + '#ajax' => [ + 'wrapper' => 'foo', + 'callback' => 'test-callback', + 'options' => [ + 'query' => [ + 'other' => 'query', + ], + ], + ], + ]; + + $element = RenderElement::preRenderAjaxForm($element); + + $this->assertTrue($element['#ajax_processed']); + $this->assertEquals($url, $element['#attached']['drupalSettings']['ajax']['test']['url']); + } + + /** + * @covers ::preRenderAjaxForm + * + * @expectedException \InvalidArgumentException + */ + public function testPreRenderAjaxFormWithDisallowedUrl() { + $element = [ + '#type' => 'select', + '#id' => 'test', + '#ajax' => [ + 'url' => '/foo', + ], + ]; + + $element = RenderElement::preRenderAjaxForm($element); + + $this->assertTrue($element['#ajax_processed']); + } + + /** + * @covers ::preRenderAjaxForm + */ + public function testPreRenderAjaxFormAjaxTrue() { + $element = [ + '#type' => 'select', + '#id' => 'test', + '#ajax' => TRUE, + ]; + + $element = RenderElement::preRenderAjaxForm($element); + + $this->assertTrue($element['#ajax_processed']); + $ajax_settings = [ + 'event' => 'change', + 'dialogType' => 'ajax', + ]; + $this->assertSame($ajax_settings, $element['#attached']['drupalSettings']['ajax']['test']); + } + +}