FormAjaxController.php 7.6 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
 * Contains \Drupal\system\Controller\FormAjaxController.
6
7
8
9
 */

namespace Drupal\system\Controller;

10
use Drupal\Core\Ajax\AjaxResponse;
11
use Drupal\Core\Ajax\UpdateBuildIdCommand;
12
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
13
14
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormBuilderInterface;
15
use Drupal\Core\Render\MainContent\MainContentRendererInterface;
16
use Drupal\Core\Render\RendererInterface;
17
use Drupal\Core\Routing\RouteMatchInterface;
18
use Drupal\system\FileAjaxForm;
19
20
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
21
22
23
24
25
26
27
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;

/**
 * Defines a controller to respond to form Ajax requests.
 */
28
29
30
31
32
33
34
35
36
class FormAjaxController implements ContainerInjectionInterface {

  /**
   * A logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

37
38
39
  /**
   * The form builder.
   *
40
   * @var \Drupal\Core\Form\FormBuilderInterface|\Drupal\Core\Form\FormCacheInterface
41
42
43
   */
  protected $formBuilder;

44
45
46
47
48
49
50
  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

51
52
53
54
55
56
57
58
59
60
61
62
63
64
  /**
   * The main content to AJAX Response renderer.
   *
   * @var \Drupal\Core\Render\MainContent\MainContentRendererInterface
   */
  protected $ajaxRenderer;

  /**
   * The current route match.
   *
   * @var \Drupal\Core\Routing\RouteMatchInterface
   */
  protected $routeMatch;

65
66
67
68
69
  /**
   * Constructs a FormAjaxController object.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
70
71
   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
   *   The form builder.
72
73
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
74
75
76
77
   * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
   *   The main content to AJAX Response renderer.
   * @param \Drupal\Core\Routing\RouteMatchInterface
   *   The current route match.
78
   */
79
  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
80
    $this->logger = $logger;
81
    $this->formBuilder = $form_builder;
82
    $this->renderer = $renderer;
83
84
    $this->ajaxRenderer = $ajax_renderer;
    $this->routeMatch = $route_match;
85
86
87
88
89
90
91
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
92
      $container->get('logger.factory')->get('ajax'),
93
      $container->get('form_builder'),
94
95
96
      $container->get('renderer'),
      $container->get('main_content_renderer.ajax'),
      $container->get('current_route_match')
97
98
    );
  }
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

  /**
   * 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) {
117
118
119
120
    $ajax_form = $this->getForm($request);
    $form = $ajax_form->getForm();
    $form_state = $ajax_form->getFormState();
    $commands = $ajax_form->getCommands();
121

122
    $this->formBuilder->processForm($form['#form_id'], $form, $form_state);
123
124
125
126
127
128
129

    // We need to return the part of the form (or some other content) that needs
    // to be re-rendered so the browser can update the page with changed content.
    // Since this is the generic menu callback used by many Ajax elements, it is
    // up to the #ajax['callback'] function of the element (may or may not be a
    // button) that triggered the Ajax request to determine what needs to be
    // rendered.
130
    $callback = NULL;
131
132
    if ($triggering_element = $form_state->getTriggeringElement()) {
      $callback = $triggering_element['#ajax']['callback'];
133
    }
134
    $callback = $form_state->prepareCallback($callback);
135
    if (empty($callback) || !is_callable($callback)) {
136
      throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
137
    }
138
139
140
141
142
143
144
145
146
147
148
149
150
151
    $result = call_user_func_array($callback, [&$form, &$form_state]);

    // If the callback is an #ajax callback, the result is a render array, and
    // we need to turn it into an AJAX response, so that we can add any commands
    // we got earlier; typically the UpdateBuildIdCommand when handling an AJAX
    // submit from a cached page.
    if ($result instanceof AjaxResponse) {
      $response = $result;
    }
    else {
      /** @var \Drupal\Core\Ajax\AjaxResponse $response */
      $response = $this->ajaxRenderer->renderResponse($result, $request, $this->routeMatch);
    }

152
153
154
155
    foreach ($commands as $command) {
      $response->addCommand($command, TRUE);
    }
    return $response;
156
157
158
159
160
161
162
163
164
165
166
  }

  /**
   * 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.
   *
167
168
169
   * @return \Drupal\system\FileAjaxForm
   *   A wrapper object containing the $form, $form_state, $form_id,
   *   $form_build_id and an initial list of Ajax $commands.
170
   *
171
   * @throws \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
172
173
   */
  protected function getForm(Request $request) {
174
    $form_state = new FormState();
175
176
177
    $form_build_id = $request->request->get('form_build_id');

    // Get the form from the cache.
178
    $form = $this->formBuilder->getCache($form_build_id, $form_state);
179
180
181
182
183
184
    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.
185
      $this->logger->warning('Invalid form POST data.');
186
187
188
      throw new BadRequestHttpException();
    }

189
    // When a page level cache is enabled, the form-build id might have been
190
191
192
    // replaced from within \Drupal::formBuilder()->getCache(). If this is the
    // case, it is also necessary to update it in the browser by issuing an
    // appropriate Ajax command.
193
194
195
196
197
198
199
    $commands = [];
    if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) {
      // If the form build ID has changed, issue an Ajax command to update it.
      $commands[] = new UpdateBuildIdCommand($form['#build_id_old'], $form['#build_id']);
      $form_build_id = $form['#build_id'];
    }

200
    // Since some of the submit handlers are run, redirects need to be disabled.
201
    $form_state->disableRedirect();
202
203
204

    // When a form is rebuilt after Ajax processing, its #build_id and #action
    // should not change.
205
    // @see \Drupal\Core\Form\FormBuilderInterface::rebuildForm()
206
207
208
209
    $form_state->addRebuildInfo('copy', [
      '#build_id' => TRUE,
      '#action' => TRUE,
    ]);
210

211
212
    // The form needs to be processed; prepare for that by setting a few
    // internal variables.
213
    $form_state->setUserInput($request->request->all());
214
215
    $form_id = $form['#form_id'];

216
    return new FileAjaxForm($form, $form_state, $form_id, $form_build_id, $commands);
217
218
219
  }

}