Issue #2500527 by dawehner, tim.plunkett, effulgentsia: Rewrite \Drupal\file\Controller\FileWidgetAjaxController::upload() to not rely on form cache
							parent
							
								
									acf91933f9
								
							
						
					
					
						commit
						8b4bc7df8f
					
				| 
						 | 
				
			
			@ -821,7 +821,7 @@ services:
 | 
			
		|||
      - { name: event_subscriber }
 | 
			
		||||
  form_ajax_subscriber:
 | 
			
		||||
    class: Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber
 | 
			
		||||
    arguments: ['@form_ajax_response_builder']
 | 
			
		||||
    arguments: ['@form_ajax_response_builder', '@renderer', '@string_translation']
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: event_subscriber }
 | 
			
		||||
  route_enhancer.lazy_collector:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,16 @@
 | 
			
		|||
 | 
			
		||||
namespace Drupal\Core\Form\EventSubscriber;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Ajax\AjaxResponse;
 | 
			
		||||
use Drupal\Core\Ajax\ReplaceCommand;
 | 
			
		||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 | 
			
		||||
use Drupal\Core\Form\Exception\BrokenPostRequestException;
 | 
			
		||||
use Drupal\Core\Form\FormAjaxException;
 | 
			
		||||
use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
 | 
			
		||||
use Drupal\Core\Form\FormBuilderInterface;
 | 
			
		||||
use Drupal\Core\Render\RendererInterface;
 | 
			
		||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslationInterface;
 | 
			
		||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 | 
			
		||||
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
 | 
			
		||||
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +27,8 @@ use Symfony\Component\HttpKernel\KernelEvents;
 | 
			
		|||
 */
 | 
			
		||||
class FormAjaxSubscriber implements EventSubscriberInterface {
 | 
			
		||||
 | 
			
		||||
  use StringTranslationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The form AJAX response builder.
 | 
			
		||||
   *
 | 
			
		||||
| 
						 | 
				
			
			@ -28,14 +36,27 @@ class FormAjaxSubscriber implements EventSubscriberInterface {
 | 
			
		|||
   */
 | 
			
		||||
  protected $formAjaxResponseBuilder;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The renderer service.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Render\RendererInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $renderer;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructs a new FormAjaxSubscriber.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
 | 
			
		||||
   *   The form AJAX response builder.
 | 
			
		||||
   * @param \Drupal\Core\Render\RendererInterface $renderer
 | 
			
		||||
   *   The renderer service.
 | 
			
		||||
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
 | 
			
		||||
   *   The string translation.
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct(FormAjaxResponseBuilderInterface $form_ajax_response_builder) {
 | 
			
		||||
  public function __construct(FormAjaxResponseBuilderInterface $form_ajax_response_builder, RendererInterface $renderer, TranslationInterface $string_translation) {
 | 
			
		||||
    $this->formAjaxResponseBuilder = $form_ajax_response_builder;
 | 
			
		||||
    $this->renderer = $renderer;
 | 
			
		||||
    $this->stringTranslation = $string_translation;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -64,9 +85,24 @@ class FormAjaxSubscriber implements EventSubscriberInterface {
 | 
			
		|||
   *   The event to process.
 | 
			
		||||
   */
 | 
			
		||||
  public function onException(GetResponseForExceptionEvent $event) {
 | 
			
		||||
    $exception = $event->getException();
 | 
			
		||||
    $request = $event->getRequest();
 | 
			
		||||
 | 
			
		||||
    // Render a nice error message in case we have a file upload which exceeds
 | 
			
		||||
    // the configured upload limit.
 | 
			
		||||
    if ($exception instanceof BrokenPostRequestException && $request->query->has(FormBuilderInterface::AJAX_FORM_REQUEST)) {
 | 
			
		||||
      $this->drupalSetMessage($this->t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', ['@size' => $this->formatSize($exception->getSize())]), 'error');
 | 
			
		||||
      $response = new AjaxResponse();
 | 
			
		||||
      $status_messages = ['#type' => 'status_messages'];
 | 
			
		||||
      $response->addCommand(new ReplaceCommand(NULL, $this->renderer->renderRoot($status_messages)));
 | 
			
		||||
      $response->headers->set('X-Status-Code', 200);
 | 
			
		||||
      $event->setResponse($response);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Extract the form AJAX exception (it may have been passed to another
 | 
			
		||||
    // exception before reaching here).
 | 
			
		||||
    if ($exception = $this->getFormAjaxException($event->getException())) {
 | 
			
		||||
    if ($exception = $this->getFormAjaxException($exception)) {
 | 
			
		||||
      $request = $event->getRequest();
 | 
			
		||||
      $form = $exception->getForm();
 | 
			
		||||
      $form_state = $exception->getFormState();
 | 
			
		||||
| 
						 | 
				
			
			@ -111,6 +147,16 @@ class FormAjaxSubscriber implements EventSubscriberInterface {
 | 
			
		|||
    return $exception;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Wraps format_size()
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The formatted size.
 | 
			
		||||
   */
 | 
			
		||||
  protected function formatSize($size) {
 | 
			
		||||
    return format_size($size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
| 
						 | 
				
			
			@ -123,4 +169,13 @@ class FormAjaxSubscriber implements EventSubscriberInterface {
 | 
			
		|||
    return $events;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Wraps drupal_set_message().
 | 
			
		||||
   *
 | 
			
		||||
   * @codeCoverageIgnore
 | 
			
		||||
   */
 | 
			
		||||
  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
 | 
			
		||||
    drupal_set_message($message, $type, $repeat);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Contains \Drupal\Core\Form\BrokenPostRequestException.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Core\Form\Exception;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Defines an exception used, when the POST HTTP body is broken.
 | 
			
		||||
 */
 | 
			
		||||
class BrokenPostRequestException extends BadRequestHttpException {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The maximum upload size.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $size;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructs a new BrokenPostRequestException.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $max_upload_size
 | 
			
		||||
   *   The size of the maximum upload size.
 | 
			
		||||
   * @param string $message
 | 
			
		||||
   *   The internal exception message.
 | 
			
		||||
   * @param \Exception $previous
 | 
			
		||||
   *   The previous exception.
 | 
			
		||||
   * @param int $code
 | 
			
		||||
   *   The internal exception code.
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct($max_upload_size, $message = NULL, \Exception $previous = NULL, $code = 0) {
 | 
			
		||||
    parent::__construct($message, $previous, $code);
 | 
			
		||||
 | 
			
		||||
    $this->size = $max_upload_size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the maximum upload size.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   A translated string representation of the size of the file size limit
 | 
			
		||||
   *   based on the PHP upload_max_filesize and post_max_size.
 | 
			
		||||
   */
 | 
			
		||||
  public function getSize() {
 | 
			
		||||
    return $this->size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +71,7 @@ class FormAjaxResponseBuilder implements FormAjaxResponseBuilderInterface {
 | 
			
		|||
    if (empty($callback) || !is_callable($callback)) {
 | 
			
		||||
      throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
 | 
			
		||||
    }
 | 
			
		||||
    $result = call_user_func_array($callback, [&$form, &$form_state]);
 | 
			
		||||
    $result = call_user_func_array($callback, [&$form, &$form_state, $request]);
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,8 +16,11 @@ use Drupal\Core\Access\CsrfTokenGenerator;
 | 
			
		|||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
 | 
			
		||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 | 
			
		||||
use Drupal\Core\Extension\ModuleHandlerInterface;
 | 
			
		||||
use Drupal\Core\Form\Exception\BrokenPostRequestException;
 | 
			
		||||
use Drupal\Core\Render\Element;
 | 
			
		||||
use Drupal\Core\Render\ElementInfoManagerInterface;
 | 
			
		||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslationInterface;
 | 
			
		||||
use Drupal\Core\Theme\ThemeManagerInterface;
 | 
			
		||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\RequestStack;
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +33,8 @@ use Symfony\Component\HttpFoundation\Response;
 | 
			
		|||
 */
 | 
			
		||||
class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface {
 | 
			
		||||
 | 
			
		||||
  use StringTranslationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The module handler.
 | 
			
		||||
   *
 | 
			
		||||
| 
						 | 
				
			
			@ -264,6 +269,13 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
 | 
			
		|||
    // can use it to know or update information about the state of the form.
 | 
			
		||||
    $response = $this->processForm($form_id, $form, $form_state);
 | 
			
		||||
 | 
			
		||||
    // In case the post request exceeds the configured allowed size
 | 
			
		||||
    // (post_max_size), the post request is potentially broken. Add some
 | 
			
		||||
    // protection against that and at the same time have a nice error message.
 | 
			
		||||
    if ($ajax_form_request && !isset($form_state->getUserInput()['form_id'])) {
 | 
			
		||||
      throw new BrokenPostRequestException($this->getFileUploadMaxSize());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // After processing the form, if this is an AJAX form request, interrupt
 | 
			
		||||
    // form rendering and return by throwing an exception that contains the
 | 
			
		||||
    // processed form and form state. This exception will be caught by
 | 
			
		||||
| 
						 | 
				
			
			@ -1151,6 +1163,17 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
 | 
			
		|||
    return FALSE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Wraps file_upload_max_size().
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   A translated string representation of the size of the file size limit
 | 
			
		||||
   *   based on the PHP upload_max_filesize and post_max_size.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getFileUploadMaxSize() {
 | 
			
		||||
    return file_upload_max_size();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the current active user.
 | 
			
		||||
   *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -328,20 +328,6 @@
 | 
			
		|||
      }
 | 
			
		||||
      else if (this.element && element.form) {
 | 
			
		||||
        this.url = this.$form.attr('action');
 | 
			
		||||
 | 
			
		||||
        // @todo If there's a file input on this form, then jQuery will submit
 | 
			
		||||
        //   the AJAX response with a hidden Iframe rather than the XHR object.
 | 
			
		||||
        //   If the response to the submission is an HTTP redirect, then the
 | 
			
		||||
        //   Iframe will follow it, but the server won't content negotiate it
 | 
			
		||||
        //   correctly, because there won't be an ajax_iframe_upload POST
 | 
			
		||||
        //   variable. Until we figure out a work around to this problem, we
 | 
			
		||||
        //   prevent AJAX-enabling elements that submit to the same URL as the
 | 
			
		||||
        //   form when there's a file input. For example, this means the Delete
 | 
			
		||||
        //   button on the edit form of an Article node doesn't open its
 | 
			
		||||
        //   confirmation form in a dialog.
 | 
			
		||||
        if (this.$form.find(':file').length) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,3 @@
 | 
			
		|||
file.ajax_upload:
 | 
			
		||||
  path: '/file/ajax'
 | 
			
		||||
  defaults:
 | 
			
		||||
    _controller: '\Drupal\file\Controller\FileWidgetAjaxController::upload'
 | 
			
		||||
  options:
 | 
			
		||||
    _theme: ajax_base_page
 | 
			
		||||
  requirements:
 | 
			
		||||
    _permission: 'access content'
 | 
			
		||||
 | 
			
		||||
file.ajax_progress:
 | 
			
		||||
  path: '/file/progress'
 | 
			
		||||
  defaults:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,87 +7,14 @@
 | 
			
		|||
 | 
			
		||||
namespace Drupal\file\Controller;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Utility\NestedArray;
 | 
			
		||||
use Drupal\Core\Ajax\AjaxResponse;
 | 
			
		||||
use Drupal\Core\Ajax\ReplaceCommand;
 | 
			
		||||
use Drupal\system\Controller\FormAjaxController;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Defines a controller to respond to file widget AJAX requests.
 | 
			
		||||
 */
 | 
			
		||||
class FileWidgetAjaxController extends FormAjaxController {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Processes AJAX file uploads and deletions.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Symfony\Component\HttpFoundation\Request $request
 | 
			
		||||
   *   The current request object.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Ajax\AjaxResponse
 | 
			
		||||
   *   An AjaxResponse object.
 | 
			
		||||
   */
 | 
			
		||||
  public function upload(Request $request) {
 | 
			
		||||
    $form_parents = explode('/', $request->query->get('element_parents'));
 | 
			
		||||
    $form_build_id = $request->query->get('form_build_id');
 | 
			
		||||
    $request_form_build_id = $request->request->get('form_build_id');
 | 
			
		||||
 | 
			
		||||
    if (empty($request_form_build_id) || $form_build_id !== $request_form_build_id) {
 | 
			
		||||
      // Invalid request.
 | 
			
		||||
      drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
 | 
			
		||||
      $response = new AjaxResponse();
 | 
			
		||||
      $status_messages = array('#type' => 'status_messages');
 | 
			
		||||
      return $response->addCommand(new ReplaceCommand(NULL, $this->renderer->renderRoot($status_messages)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      /** @var $ajaxForm \Drupal\system\FileAjaxForm */
 | 
			
		||||
      $ajaxForm = $this->getForm($request);
 | 
			
		||||
      $form = $ajaxForm->getForm();
 | 
			
		||||
      $form_state = $ajaxForm->getFormState();
 | 
			
		||||
      $commands = $ajaxForm->getCommands();
 | 
			
		||||
    }
 | 
			
		||||
    catch (HttpExceptionInterface $e) {
 | 
			
		||||
      // Invalid form_build_id.
 | 
			
		||||
      drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
 | 
			
		||||
      $response = new AjaxResponse();
 | 
			
		||||
      $status_messages = array('#type' => 'status_messages');
 | 
			
		||||
      return $response->addCommand(new ReplaceCommand(NULL, $this->renderer->renderRoot($status_messages)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get the current element and count the number of files.
 | 
			
		||||
    $current_element = NestedArray::getValue($form, $form_parents);
 | 
			
		||||
    $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
 | 
			
		||||
 | 
			
		||||
    // Process user input. $form and $form_state are modified in the process.
 | 
			
		||||
    $this->formBuilder->processForm($form['#form_id'], $form, $form_state);
 | 
			
		||||
 | 
			
		||||
    // Retrieve the element to be rendered.
 | 
			
		||||
    $form = NestedArray::getValue($form, $form_parents);
 | 
			
		||||
 | 
			
		||||
    // Add the special Ajax class if a new file was added.
 | 
			
		||||
    if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
 | 
			
		||||
      $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
 | 
			
		||||
    }
 | 
			
		||||
    // Otherwise just add the new content class on a placeholder.
 | 
			
		||||
    else {
 | 
			
		||||
      $form['#suffix'] .= '<span class="ajax-new-content"></span>';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $status_messages = array('#type' => 'status_messages');
 | 
			
		||||
    $form['#prefix'] .= $this->renderer->renderRoot($status_messages);
 | 
			
		||||
    $output = $this->renderer->renderRoot($form);
 | 
			
		||||
 | 
			
		||||
    $response = new AjaxResponse();
 | 
			
		||||
    $response->setAttachments($form['#attached']);
 | 
			
		||||
    foreach ($commands as $command) {
 | 
			
		||||
      $response->addCommand($command, TRUE);
 | 
			
		||||
    }
 | 
			
		||||
    return $response->addCommand(new ReplaceCommand(NULL, $output));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the progress status for a file upload process.
 | 
			
		||||
   *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,11 +7,15 @@
 | 
			
		|||
 | 
			
		||||
namespace Drupal\file\Element;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Utility\NestedArray;
 | 
			
		||||
use Drupal\Component\Utility\Html;
 | 
			
		||||
use Drupal\Core\Ajax\AjaxResponse;
 | 
			
		||||
use Drupal\Core\Ajax\ReplaceCommand;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\Core\Render\Element\FormElement;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\file\Entity\File;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides an AJAX/progress aware widget for uploading and saving a file.
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +128,53 @@ class ManagedFile extends FormElement {
 | 
			
		|||
    return $return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * #ajax callback for managed_file upload forms.
 | 
			
		||||
   *
 | 
			
		||||
   * This ajax callback takes care of the following things:
 | 
			
		||||
   *   - Ensures that broken requests due to too big files are caught.
 | 
			
		||||
   *   - Adds a class to the response to be able to highlight in the UI, that a
 | 
			
		||||
   *     new file got uploaded.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $form
 | 
			
		||||
   *   The build form.
 | 
			
		||||
   * @param \Drupal\Core\Form\FormStateInterface $form_state
 | 
			
		||||
   *   The form state.
 | 
			
		||||
   * @param \Symfony\Component\HttpFoundation\Request $request
 | 
			
		||||
   *   The current request.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Ajax\AjaxResponse
 | 
			
		||||
   *   The ajax response of the ajax upload.
 | 
			
		||||
   */
 | 
			
		||||
  public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) {
 | 
			
		||||
    /** @var \Drupal\Core\Render\RendererInterface $renderer */
 | 
			
		||||
    $renderer = \Drupal::service('renderer');
 | 
			
		||||
 | 
			
		||||
    $form_parents = explode('/', $request->query->get('element_parents'));
 | 
			
		||||
 | 
			
		||||
    // Retrieve the element to be rendered.
 | 
			
		||||
    $form = NestedArray::getValue($form, $form_parents);
 | 
			
		||||
 | 
			
		||||
    // Add the special AJAX class if a new file was added.
 | 
			
		||||
    $current_file_count = $form_state->get('file_upload_delta_initial');
 | 
			
		||||
    if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
 | 
			
		||||
      $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
 | 
			
		||||
    }
 | 
			
		||||
    // Otherwise just add the new content class on a placeholder.
 | 
			
		||||
    else {
 | 
			
		||||
      $form['#suffix'] .= '<span class="ajax-new-content"></span>';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $status_messages = ['#type' => 'status_messages'];
 | 
			
		||||
    $form['#prefix'] .= $renderer->renderRoot($status_messages);
 | 
			
		||||
    $output = $renderer->renderRoot($form);
 | 
			
		||||
 | 
			
		||||
    $response = new AjaxResponse();
 | 
			
		||||
    $response->setAttachments($form['#attached']);
 | 
			
		||||
 | 
			
		||||
    return $response->addCommand(new ReplaceCommand(NULL, $output));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Render API callback: Expands the managed_file element type.
 | 
			
		||||
   *
 | 
			
		||||
| 
						 | 
				
			
			@ -146,12 +197,10 @@ class ManagedFile extends FormElement {
 | 
			
		|||
    $ajax_wrapper_id = Html::getUniqueId('ajax-wrapper');
 | 
			
		||||
 | 
			
		||||
    $ajax_settings = [
 | 
			
		||||
      // @todo Remove this in https://www.drupal.org/node/2500527.
 | 
			
		||||
      'url' => Url::fromRoute('file.ajax_upload'),
 | 
			
		||||
      'callback' => [get_called_class(), 'uploadAjaxCallback'],
 | 
			
		||||
      'options' => [
 | 
			
		||||
        'query' => [
 | 
			
		||||
          'element_parents' => implode('/', $element['#array_parents']),
 | 
			
		||||
          'form_build_id' => $complete_form['form_build_id']['#value'],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'wrapper' => $ajax_wrapper_id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -407,18 +407,15 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
 | 
			
		|||
    // file, the entire group of file fields is updated together.
 | 
			
		||||
    if ($element['#cardinality'] != 1) {
 | 
			
		||||
      $parents = array_slice($element['#array_parents'], 0, -1);
 | 
			
		||||
      $new_url = Url::fromRoute('file.ajax_upload');
 | 
			
		||||
      $new_options = array(
 | 
			
		||||
        'query' => array(
 | 
			
		||||
          'element_parents' => implode('/', $parents),
 | 
			
		||||
          'form_build_id' => $form['form_build_id']['#value'],
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      $field_element = NestedArray::getValue($form, $parents);
 | 
			
		||||
      $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
 | 
			
		||||
      foreach (Element::children($element) as $key) {
 | 
			
		||||
        if (isset($element[$key]['#ajax'])) {
 | 
			
		||||
          $element[$key]['#ajax']['url'] = $new_url->setOptions($new_options);
 | 
			
		||||
          $element[$key]['#ajax']['options'] = $new_options;
 | 
			
		||||
          $element[$key]['#ajax']['wrapper'] = $new_wrapper;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -451,6 +448,19 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
 | 
			
		|||
    $element_children = Element::children($element, TRUE);
 | 
			
		||||
    $count = count($element_children);
 | 
			
		||||
 | 
			
		||||
    // Count the number of already uploaded files, in order to display new
 | 
			
		||||
    // items in \Drupal\file\Element\ManagedFile::uploadAjaxCallback().
 | 
			
		||||
    if (!$form_state->isRebuilding()) {
 | 
			
		||||
      $count_items_before = 0;
 | 
			
		||||
      foreach ($element_children as $children) {
 | 
			
		||||
        if (!empty($element[$children]['#default_value']['fids'])) {
 | 
			
		||||
          $count_items_before++;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      $form_state->set('file_upload_delta_initial', $count_items_before);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    foreach ($element_children as $delta => $key) {
 | 
			
		||||
      if ($key != $element['#file_upload_delta']) {
 | 
			
		||||
        $description = static::getDescriptionFromElement($element[$key]);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,7 +64,7 @@ class AjaxFormCacheTest extends AjaxTestBase {
 | 
			
		|||
    $this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
 | 
			
		||||
    $this->assertOptionSelectedWithDrupalSelector('edit-test1', 'option1');
 | 
			
		||||
    $this->assertOptionWithDrupalSelector('edit-test1', 'option3');
 | 
			
		||||
    $this->drupalPostForm($this->getUrl(), ['test1' => 'option1'], 'Submit');
 | 
			
		||||
    $this->drupalPostForm(NULL, ['test1' => 'option1'], 'Submit');
 | 
			
		||||
    $this->assertText('Submission successful.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,13 +7,17 @@
 | 
			
		|||
 | 
			
		||||
namespace Drupal\Tests\Core\Form\EventSubscriber;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Ajax\AjaxResponse;
 | 
			
		||||
use Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber;
 | 
			
		||||
use Drupal\Core\Form\Exception\BrokenPostRequestException;
 | 
			
		||||
use Drupal\Core\Form\FormAjaxException;
 | 
			
		||||
use Drupal\Core\Form\FormBuilderInterface;
 | 
			
		||||
use Drupal\Core\Form\FormState;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\HttpException;
 | 
			
		||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +42,20 @@ class FormAjaxSubscriberTest extends UnitTestCase {
 | 
			
		|||
   */
 | 
			
		||||
  protected $httpKernel;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The renderer service.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
 | 
			
		||||
   */
 | 
			
		||||
  protected $renderer;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The mocked string translation.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\StringTranslation\TranslationInterface|\PHPUnit_Framework_MockObject_MockObject
 | 
			
		||||
   */
 | 
			
		||||
  protected $stringTranslation;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +64,9 @@ class FormAjaxSubscriberTest extends UnitTestCase {
 | 
			
		|||
 | 
			
		||||
    $this->httpKernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
 | 
			
		||||
    $this->formAjaxResponseBuilder = $this->getMock('Drupal\Core\Form\FormAjaxResponseBuilderInterface');
 | 
			
		||||
    $this->subscriber = new FormAjaxSubscriber($this->formAjaxResponseBuilder);
 | 
			
		||||
    $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
 | 
			
		||||
    $this->stringTranslation = $this->getStringTranslationStub();
 | 
			
		||||
    $this->subscriber = new FormAjaxSubscriber($this->formAjaxResponseBuilder, $this->renderer, $this->stringTranslation);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -133,6 +153,46 @@ class FormAjaxSubscriberTest extends UnitTestCase {
 | 
			
		|||
    $this->assertSame($expected_exception, $event->getException());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::onException
 | 
			
		||||
   */
 | 
			
		||||
  public function testOnExceptionBrokenPostRequest() {
 | 
			
		||||
    $this->formAjaxResponseBuilder->expects($this->never())
 | 
			
		||||
      ->method('buildResponse');
 | 
			
		||||
    $this->subscriber = $this->getMockBuilder('\Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber')
 | 
			
		||||
      ->setConstructorArgs([$this->formAjaxResponseBuilder, $this->renderer, $this->getStringTranslationStub()])
 | 
			
		||||
      ->setMethods(['drupalSetMessage', 'formatSize'])
 | 
			
		||||
      ->getMock();
 | 
			
		||||
    $this->subscriber->expects($this->once())
 | 
			
		||||
      ->method('drupalSetMessage')
 | 
			
		||||
      ->willReturn('asdf');
 | 
			
		||||
    $this->subscriber->expects($this->once())
 | 
			
		||||
      ->method('formatSize')
 | 
			
		||||
      ->with(32 * 1e6)
 | 
			
		||||
      ->willReturn('32M');
 | 
			
		||||
    $rendered_output = 'the rendered output';
 | 
			
		||||
    $this->renderer->expects($this->once())
 | 
			
		||||
      ->method('renderRoot')
 | 
			
		||||
      ->willReturn($rendered_output);
 | 
			
		||||
 | 
			
		||||
    $exception = new BrokenPostRequestException(32 * 1e6);
 | 
			
		||||
    $request = new Request([FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]);
 | 
			
		||||
 | 
			
		||||
    $event = new GetResponseForExceptionEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST, $exception);
 | 
			
		||||
    $this->subscriber->onException($event);
 | 
			
		||||
    $actual_response = $event->getResponse();
 | 
			
		||||
    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $actual_response);
 | 
			
		||||
    $this->assertSame(200, $actual_response->headers->get('X-Status-Code'));
 | 
			
		||||
    $expected_commands[] = [
 | 
			
		||||
      'command' => 'insert',
 | 
			
		||||
      'method' => 'replaceWith',
 | 
			
		||||
      'selector' => NULL,
 | 
			
		||||
      'data' => $rendered_output,
 | 
			
		||||
      'settings' => NULL,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertSame($expected_commands, $actual_response->getCommands());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::onException
 | 
			
		||||
   * @covers ::getFormAjaxException
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,10 +9,13 @@ namespace Drupal\Tests\Core\Form;
 | 
			
		|||
 | 
			
		||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 | 
			
		||||
use Drupal\Core\Form\EnforcedResponseException;
 | 
			
		||||
use Drupal\Core\Form\FormBuilderInterface;
 | 
			
		||||
use Drupal\Core\Form\FormInterface;
 | 
			
		||||
use Drupal\Core\Form\FormState;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\RequestStack;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Form\FormBuilder
 | 
			
		||||
| 
						 | 
				
			
			@ -423,6 +426,29 @@ class FormBuilderTest extends FormTestBase {
 | 
			
		|||
    $this->simulateFormSubmission($form_id, $form_arg, $form_state);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::buildForm
 | 
			
		||||
   *
 | 
			
		||||
   * @expectedException \Drupal\Core\Form\Exception\BrokenPostRequestException
 | 
			
		||||
   */
 | 
			
		||||
  public function testExceededFileSize() {
 | 
			
		||||
    $request = new Request([FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]);
 | 
			
		||||
    $request_stack = new RequestStack();
 | 
			
		||||
    $request_stack->push($request);
 | 
			
		||||
    $this->formBuilder = $this->getMockBuilder('\Drupal\Core\Form\FormBuilder')
 | 
			
		||||
      ->setConstructorArgs([$this->formValidator, $this->formSubmitter, $this->formCache, $this->moduleHandler, $this->eventDispatcher, $request_stack, $this->classResolver, $this->elementInfo, $this->themeManager, $this->csrfToken])
 | 
			
		||||
      ->setMethods(['getFileUploadMaxSize'])
 | 
			
		||||
      ->getMock();
 | 
			
		||||
    $this->formBuilder->expects($this->once())
 | 
			
		||||
      ->method('getFileUploadMaxSize')
 | 
			
		||||
      ->willReturn(33554432);
 | 
			
		||||
 | 
			
		||||
    $form_arg = $this->getMockForm('test_form_id');
 | 
			
		||||
    $form_state = new FormState();
 | 
			
		||||
 | 
			
		||||
    $this->formBuilder->buildForm($form_arg, $form_state);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class TestForm implements FormInterface {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -149,7 +149,12 @@ abstract class FormTestBase extends UnitTestCase {
 | 
			
		|||
   */
 | 
			
		||||
  protected $themeManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp() {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
 | 
			
		||||
 | 
			
		||||
    $this->formCache = $this->getMock('Drupal\Core\Form\FormCacheInterface');
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +194,7 @@ abstract class FormTestBase extends UnitTestCase {
 | 
			
		|||
      ->getMock();
 | 
			
		||||
    $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
 | 
			
		||||
 | 
			
		||||
    $this->formBuilder = new FormBuilder($this->formValidator, $this->formSubmitter, $this->formCache, $this->moduleHandler, $this->eventDispatcher, $this->requestStack, $this->classResolver, $this->elementInfo, $this->themeManager, $this->csrfToken, $this->kernel);
 | 
			
		||||
    $this->formBuilder = new FormBuilder($this->formValidator, $this->formSubmitter, $this->formCache, $this->moduleHandler, $this->eventDispatcher, $this->requestStack, $this->classResolver, $this->elementInfo, $this->themeManager, $this->csrfToken);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue