diff -u b/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php --- b/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -185,9 +185,12 @@ // Ensure the form ID is prepared. $form_id = $this->getFormId($form_id, $form_state); - // Inform the form about the request that's building it. $request = $this->requestStack->getCurrentRequest(); - $form_state->setRequest($request); + + // Inform $form_state about the request method that's building it, so that + // it can prevent persisting state changes during HTTP methods for which + // that is disallowed by HTTP. + $form_state->setRequestMethod($request->getMethod()); // Initialize the form's user input. The user input should include only the // input meant to be treated as part of what is submitted to the form, so @@ -328,7 +331,14 @@ // via POST, then $form_state must be persisted when it is rebuilt between // submissions. If the form receives its input via GET, then persisting // state is forbidden by $form_state->setCached(), and the form must use - // the URL itself to transfer its state across steps. + // the URL itself to transfer its state across steps. Although $form_state + // throws an exception based on the request method rather than the form's + // method, we base the decision to cache on the form method, because: + // - It's the form method that defines what the form needs to do to manage + // its state. + // - rebuildForm() should only be called after successful input processing, + // which means the request method matches the form method, and if not, + // there's some other error, so it's ok if an exception is thrown. if ($form_state->isMethodType('POST')) { $form_state->setCached(); } diff -u b/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php --- b/core/lib/Drupal/Core/Form/FormState.php +++ b/core/lib/Drupal/Core/Form/FormState.php @@ -9,7 +9,6 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Url; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** @@ -106,24 +105,6 @@ protected $rebuild = FALSE; /** - * The request for which the form is being built of processed. - * - * This is used to determine information about the request, such as its - * method, which can be different from $method, because $method represents - * what method to use for submitting the form, whereas $request->getMethod() - * is the method of the current request, which is usually GET for the initial - * build of the form. - * - * This should not be used for determining the form's input, because that is - * provided in $input. - * - * This property is uncacheable. - * - * @var \Symfony\Component\HttpFoundation\Request - */ - protected $request; - - /** * Used when a form needs to return some kind of a * \Symfony\Component\HttpFoundation\Response object, e.g., a * \Symfony\Component\HttpFoundation\BinaryFileResponse when triggering a @@ -174,2 +155,15 @@ /** + * The HTTP method used by the request building or processing this form. + * + * May be any valid HTTP method. Defaults to 'GET', because even though + * $method is 'POST' for most forms, the form's initial build is usually + * performed as part of a GET request. + * + * This property is uncacheable. + * + * @var string + */ + protected $request_method = 'GET'; + + /** * If set to TRUE the original, unprocessed form structure will be cached, @@ -495,8 +489,8 @@ */ public function setCached($cache = TRUE) { // Persisting $form_state is a side-effect disallowed during a "safe" HTTP - // method (http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1). - if ($cache && $this->request->isMethodSafe()) { + // method. + if ($cache && $this->isRequestMethodSafe()) { throw new \LogicException(sprintf('Form state caching on %s requests is not allowed.', $this->request->getMethod())); } @@ -597,6 +591,29 @@ /** * {@inheritdoc} */ + public function setRequestMethod($method) { + $this->request_method = strtoupper($method); + return $this; + } + + /** + * Checks whether the request method is a "safe" HTTP method. + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 defines + * GET and HEAD as "safe" methods, meaning they SHOULD NOT have side-effects, + * such as persisting $form_state changes. + * + * @return bool + * + * @see \Symfony\Component\HttpFoundation\Request::isMethodSafe() + */ + protected function isRequestMethodSafe() { + return in_array($this->request_method, array('GET', 'HEAD')); + } + + /** + * {@inheritdoc} + */ public function setValidationEnforced($must_validate = TRUE) { $this->must_validate = (bool) $must_validate; return $this; @@ -1017,14 +1034,6 @@ /** * {@inheritdoc} */ - public function setRequest(Request $request) { - $this->request = $request; - return $this; - } - - /** - * {@inheritdoc} - */ public function setResponse(Response $response) { $this->response = $response; return $this; diff -u b/core/lib/Drupal/Core/Form/FormStateInterface.php b/core/lib/Drupal/Core/Form/FormStateInterface.php --- b/core/lib/Drupal/Core/Form/FormStateInterface.php +++ b/core/lib/Drupal/Core/Form/FormStateInterface.php @@ -8,7 +8,6 @@ namespace Drupal\Core\Form; use Drupal\Core\Url; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** @@ -97,16 +96,6 @@ public function setFormState(array $form_state_additions); /** - * Sets the request for which the form is being built or processed. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request for which the form is being built or processed. - * - * @return $this - */ - public function setRequest(Request $request); - - /** * Sets a response for this form. * * If a response is set, it will be used during processing and returned @@ -746,18 +735,35 @@ public function getLimitValidationErrors(); /** - * Sets the HTTP form method. + * Sets the HTTP method to use for the form's submission. + * + * This is what the form's "method" attribute should be, not necessarily what + * the current request's HTTP method is. For example, a form can have a + * method attribute of POST, but the request that initially builds it uses + * GET. * * @param string $method - * The HTTP form method. + * Either "GET" or "POST". Other HTTP methods are not valid form submission + * methods. * * @see \Drupal\Core\Form\FormState::$method + * @see self::setRequestMethod(). * * @return $this */ public function setMethod($method); /** + * Sets the HTTP method used by the request that is building the form. + * + * @param string $method + * Can be any valid HTTP method, such as GET, POST, HEAD, etc. + * + * @return $this + */ + public function setRequestMethod($method); + + /** * Returns the HTTP form method. * * @param string