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 }
|
- { name: event_subscriber }
|
||||||
form_ajax_subscriber:
|
form_ajax_subscriber:
|
||||||
class: Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber
|
class: Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber
|
||||||
arguments: ['@form_ajax_response_builder']
|
arguments: ['@form_ajax_response_builder', '@renderer', '@string_translation']
|
||||||
tags:
|
tags:
|
||||||
- { name: event_subscriber }
|
- { name: event_subscriber }
|
||||||
route_enhancer.lazy_collector:
|
route_enhancer.lazy_collector:
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,16 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Form\EventSubscriber;
|
namespace Drupal\Core\Form\EventSubscriber;
|
||||||
|
|
||||||
|
use Drupal\Core\Ajax\AjaxResponse;
|
||||||
|
use Drupal\Core\Ajax\ReplaceCommand;
|
||||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
||||||
|
use Drupal\Core\Form\Exception\BrokenPostRequestException;
|
||||||
use Drupal\Core\Form\FormAjaxException;
|
use Drupal\Core\Form\FormAjaxException;
|
||||||
use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
|
use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
|
||||||
use Drupal\Core\Form\FormBuilderInterface;
|
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\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
|
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
|
||||||
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
|
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
|
||||||
|
|
@ -21,6 +27,8 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
*/
|
*/
|
||||||
class FormAjaxSubscriber implements EventSubscriberInterface {
|
class FormAjaxSubscriber implements EventSubscriberInterface {
|
||||||
|
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The form AJAX response builder.
|
* The form AJAX response builder.
|
||||||
*
|
*
|
||||||
|
|
@ -28,14 +36,27 @@ class FormAjaxSubscriber implements EventSubscriberInterface {
|
||||||
*/
|
*/
|
||||||
protected $formAjaxResponseBuilder;
|
protected $formAjaxResponseBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The renderer service.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Render\RendererInterface
|
||||||
|
*/
|
||||||
|
protected $renderer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new FormAjaxSubscriber.
|
* Constructs a new FormAjaxSubscriber.
|
||||||
*
|
*
|
||||||
* @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
|
* @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
|
||||||
* The 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->formAjaxResponseBuilder = $form_ajax_response_builder;
|
||||||
|
$this->renderer = $renderer;
|
||||||
|
$this->stringTranslation = $string_translation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,9 +85,24 @@ class FormAjaxSubscriber implements EventSubscriberInterface {
|
||||||
* The event to process.
|
* The event to process.
|
||||||
*/
|
*/
|
||||||
public function onException(GetResponseForExceptionEvent $event) {
|
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
|
// Extract the form AJAX exception (it may have been passed to another
|
||||||
// exception before reaching here).
|
// exception before reaching here).
|
||||||
if ($exception = $this->getFormAjaxException($event->getException())) {
|
if ($exception = $this->getFormAjaxException($exception)) {
|
||||||
$request = $event->getRequest();
|
$request = $event->getRequest();
|
||||||
$form = $exception->getForm();
|
$form = $exception->getForm();
|
||||||
$form_state = $exception->getFormState();
|
$form_state = $exception->getFormState();
|
||||||
|
|
@ -111,6 +147,16 @@ class FormAjaxSubscriber implements EventSubscriberInterface {
|
||||||
return $exception;
|
return $exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps format_size()
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The formatted size.
|
||||||
|
*/
|
||||||
|
protected function formatSize($size) {
|
||||||
|
return format_size($size);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
|
@ -123,4 +169,13 @@ class FormAjaxSubscriber implements EventSubscriberInterface {
|
||||||
return $events;
|
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)) {
|
if (empty($callback) || !is_callable($callback)) {
|
||||||
throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
|
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
|
// 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 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\DependencyInjection\ClassResolverInterface;
|
||||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
||||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||||
|
use Drupal\Core\Form\Exception\BrokenPostRequestException;
|
||||||
use Drupal\Core\Render\Element;
|
use Drupal\Core\Render\Element;
|
||||||
use Drupal\Core\Render\ElementInfoManagerInterface;
|
use Drupal\Core\Render\ElementInfoManagerInterface;
|
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||||
|
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||||
use Drupal\Core\Theme\ThemeManagerInterface;
|
use Drupal\Core\Theme\ThemeManagerInterface;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
@ -30,6 +33,8 @@ use Symfony\Component\HttpFoundation\Response;
|
||||||
*/
|
*/
|
||||||
class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface {
|
class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface {
|
||||||
|
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The module handler.
|
* 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.
|
// can use it to know or update information about the state of the form.
|
||||||
$response = $this->processForm($form_id, $form, $form_state);
|
$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
|
// After processing the form, if this is an AJAX form request, interrupt
|
||||||
// form rendering and return by throwing an exception that contains the
|
// form rendering and return by throwing an exception that contains the
|
||||||
// processed form and form state. This exception will be caught by
|
// processed form and form state. This exception will be caught by
|
||||||
|
|
@ -1151,6 +1163,17 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
|
||||||
return FALSE;
|
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.
|
* Gets the current active user.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -328,20 +328,6 @@
|
||||||
}
|
}
|
||||||
else if (this.element && element.form) {
|
else if (this.element && element.form) {
|
||||||
this.url = this.$form.attr('action');
|
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:
|
file.ajax_progress:
|
||||||
path: '/file/progress'
|
path: '/file/progress'
|
||||||
defaults:
|
defaults:
|
||||||
|
|
|
||||||
|
|
@ -7,87 +7,14 @@
|
||||||
|
|
||||||
namespace Drupal\file\Controller;
|
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 Drupal\system\Controller\FormAjaxController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
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.
|
* Defines a controller to respond to file widget AJAX requests.
|
||||||
*/
|
*/
|
||||||
class FileWidgetAjaxController extends FormAjaxController {
|
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.
|
* Returns the progress status for a file upload process.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,15 @@
|
||||||
|
|
||||||
namespace Drupal\file\Element;
|
namespace Drupal\file\Element;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\NestedArray;
|
||||||
use Drupal\Component\Utility\Html;
|
use Drupal\Component\Utility\Html;
|
||||||
|
use Drupal\Core\Ajax\AjaxResponse;
|
||||||
|
use Drupal\Core\Ajax\ReplaceCommand;
|
||||||
use Drupal\Core\Form\FormStateInterface;
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
use Drupal\Core\Render\Element\FormElement;
|
use Drupal\Core\Render\Element\FormElement;
|
||||||
use Drupal\Core\Url;
|
use Drupal\Core\Url;
|
||||||
use Drupal\file\Entity\File;
|
use Drupal\file\Entity\File;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an AJAX/progress aware widget for uploading and saving a file.
|
* Provides an AJAX/progress aware widget for uploading and saving a file.
|
||||||
|
|
@ -124,6 +128,53 @@ class ManagedFile extends FormElement {
|
||||||
return $return;
|
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.
|
* 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_wrapper_id = Html::getUniqueId('ajax-wrapper');
|
||||||
|
|
||||||
$ajax_settings = [
|
$ajax_settings = [
|
||||||
// @todo Remove this in https://www.drupal.org/node/2500527.
|
'callback' => [get_called_class(), 'uploadAjaxCallback'],
|
||||||
'url' => Url::fromRoute('file.ajax_upload'),
|
|
||||||
'options' => [
|
'options' => [
|
||||||
'query' => [
|
'query' => [
|
||||||
'element_parents' => implode('/', $element['#array_parents']),
|
'element_parents' => implode('/', $element['#array_parents']),
|
||||||
'form_build_id' => $complete_form['form_build_id']['#value'],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'wrapper' => $ajax_wrapper_id,
|
'wrapper' => $ajax_wrapper_id,
|
||||||
|
|
|
||||||
|
|
@ -407,18 +407,15 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
|
||||||
// file, the entire group of file fields is updated together.
|
// file, the entire group of file fields is updated together.
|
||||||
if ($element['#cardinality'] != 1) {
|
if ($element['#cardinality'] != 1) {
|
||||||
$parents = array_slice($element['#array_parents'], 0, -1);
|
$parents = array_slice($element['#array_parents'], 0, -1);
|
||||||
$new_url = Url::fromRoute('file.ajax_upload');
|
|
||||||
$new_options = array(
|
$new_options = array(
|
||||||
'query' => array(
|
'query' => array(
|
||||||
'element_parents' => implode('/', $parents),
|
'element_parents' => implode('/', $parents),
|
||||||
'form_build_id' => $form['form_build_id']['#value'],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
$field_element = NestedArray::getValue($form, $parents);
|
$field_element = NestedArray::getValue($form, $parents);
|
||||||
$new_wrapper = $field_element['#id'] . '-ajax-wrapper';
|
$new_wrapper = $field_element['#id'] . '-ajax-wrapper';
|
||||||
foreach (Element::children($element) as $key) {
|
foreach (Element::children($element) as $key) {
|
||||||
if (isset($element[$key]['#ajax'])) {
|
if (isset($element[$key]['#ajax'])) {
|
||||||
$element[$key]['#ajax']['url'] = $new_url->setOptions($new_options);
|
|
||||||
$element[$key]['#ajax']['options'] = $new_options;
|
$element[$key]['#ajax']['options'] = $new_options;
|
||||||
$element[$key]['#ajax']['wrapper'] = $new_wrapper;
|
$element[$key]['#ajax']['wrapper'] = $new_wrapper;
|
||||||
}
|
}
|
||||||
|
|
@ -451,6 +448,19 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
|
||||||
$element_children = Element::children($element, TRUE);
|
$element_children = Element::children($element, TRUE);
|
||||||
$count = count($element_children);
|
$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) {
|
foreach ($element_children as $delta => $key) {
|
||||||
if ($key != $element['#file_upload_delta']) {
|
if ($key != $element['#file_upload_delta']) {
|
||||||
$description = static::getDescriptionFromElement($element[$key]);
|
$description = static::getDescriptionFromElement($element[$key]);
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class AjaxFormCacheTest extends AjaxTestBase {
|
||||||
$this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
|
$this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
|
||||||
$this->assertOptionSelectedWithDrupalSelector('edit-test1', 'option1');
|
$this->assertOptionSelectedWithDrupalSelector('edit-test1', 'option1');
|
||||||
$this->assertOptionWithDrupalSelector('edit-test1', 'option3');
|
$this->assertOptionWithDrupalSelector('edit-test1', 'option3');
|
||||||
$this->drupalPostForm($this->getUrl(), ['test1' => 'option1'], 'Submit');
|
$this->drupalPostForm(NULL, ['test1' => 'option1'], 'Submit');
|
||||||
$this->assertText('Submission successful.');
|
$this->assertText('Submission successful.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,17 @@
|
||||||
|
|
||||||
namespace Drupal\Tests\Core\Form\EventSubscriber;
|
namespace Drupal\Tests\Core\Form\EventSubscriber;
|
||||||
|
|
||||||
|
use Drupal\Core\Ajax\AjaxResponse;
|
||||||
use Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber;
|
use Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber;
|
||||||
|
use Drupal\Core\Form\Exception\BrokenPostRequestException;
|
||||||
use Drupal\Core\Form\FormAjaxException;
|
use Drupal\Core\Form\FormAjaxException;
|
||||||
|
use Drupal\Core\Form\FormBuilderInterface;
|
||||||
use Drupal\Core\Form\FormState;
|
use Drupal\Core\Form\FormState;
|
||||||
use Drupal\Tests\UnitTestCase;
|
use Drupal\Tests\UnitTestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
|
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||||
|
|
||||||
|
|
@ -38,6 +42,20 @@ class FormAjaxSubscriberTest extends UnitTestCase {
|
||||||
*/
|
*/
|
||||||
protected $httpKernel;
|
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}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
|
@ -46,7 +64,9 @@ class FormAjaxSubscriberTest extends UnitTestCase {
|
||||||
|
|
||||||
$this->httpKernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
|
$this->httpKernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
|
||||||
$this->formAjaxResponseBuilder = $this->getMock('Drupal\Core\Form\FormAjaxResponseBuilderInterface');
|
$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());
|
$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 ::onException
|
||||||
* @covers ::getFormAjaxException
|
* @covers ::getFormAjaxException
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@ namespace Drupal\Tests\Core\Form;
|
||||||
|
|
||||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||||
use Drupal\Core\Form\EnforcedResponseException;
|
use Drupal\Core\Form\EnforcedResponseException;
|
||||||
|
use Drupal\Core\Form\FormBuilderInterface;
|
||||||
use Drupal\Core\Form\FormInterface;
|
use Drupal\Core\Form\FormInterface;
|
||||||
use Drupal\Core\Form\FormState;
|
use Drupal\Core\Form\FormState;
|
||||||
use Drupal\Core\Form\FormStateInterface;
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @coversDefaultClass \Drupal\Core\Form\FormBuilder
|
* @coversDefaultClass \Drupal\Core\Form\FormBuilder
|
||||||
|
|
@ -423,6 +426,29 @@ class FormBuilderTest extends FormTestBase {
|
||||||
$this->simulateFormSubmission($form_id, $form_arg, $form_state);
|
$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 {
|
class TestForm implements FormInterface {
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,12 @@ abstract class FormTestBase extends UnitTestCase {
|
||||||
*/
|
*/
|
||||||
protected $themeManager;
|
protected $themeManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
protected function setUp() {
|
protected function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
|
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
|
||||||
|
|
||||||
$this->formCache = $this->getMock('Drupal\Core\Form\FormCacheInterface');
|
$this->formCache = $this->getMock('Drupal\Core\Form\FormCacheInterface');
|
||||||
|
|
@ -189,7 +194,7 @@ abstract class FormTestBase extends UnitTestCase {
|
||||||
->getMock();
|
->getMock();
|
||||||
$this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
|
$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