Commit 06e1dd8b authored by webchick's avatar webchick
Browse files

Issue #1987602 by glennpratt, juampy, pwieck, vijaycs85, dawehner,...

Issue #1987602 by glennpratt, juampy, pwieck, vijaycs85, dawehner, tim.plunkett, effulgentsia: Convert ajax_form_callback() to a new style controller.
parent 74cbf956
......@@ -23,9 +23,9 @@
* forms, it can be used with the #ajax property.
* The #ajax property can be used to bind events to the Ajax framework. By
* default, #ajax uses 'system/ajax' as its path for submission and thus calls
* ajax_form_callback() and a defined #ajax['callback'] function.
* However, you may optionally specify a different path to request or a
* different callback function to invoke, which can return updated HTML or can
* \Drupal\system\FormAjaxController::content() and a defined #ajax['callback']
* function. However, you may optionally specify a different path to request or
* a different callback function to invoke, which can return updated HTML or can
* also return a richer set of
* @link ajax_commands Ajax framework commands @endlink.
*
......@@ -38,17 +38,18 @@
* that element.
* - The browser submits an HTTP POST request to the 'system/ajax' Drupal
* path.
* - The menu page callback for 'system/ajax', ajax_form_callback(), calls
* drupal_process_form() to process the form submission and rebuild the
* form if necessary. The form is processed in much the same way as if it
* were submitted without Ajax, with the same #process functions and
* validation and submission handlers called in either case, making it easy
* to create Ajax-enabled forms that degrade gracefully when JavaScript is
* disabled.
* - After form processing is complete, ajax_form_callback() calls the
* function named by #ajax['callback'], which returns the form element that
* has been updated and needs to be returned to the browser, or
* alternatively, an array of custom Ajax commands.
* - The controller for the route '/system/ajax',
* \Drupal\system\FormAjaxController::content(), calls drupal_process_form()
* to process the form submission and rebuild the form if necessary. The
* form is processed in much the same way as if it were submitted without
* Ajax, with the same #process functions and validation and submission
* handlers called in either case, making it easy to create Ajax-enabled
* forms that degrade gracefully when JavaScript is disabled.
* - After form processing is complete,
* \Drupal\system\FormAjaxController::content() calls the function named by
* #ajax['callback'], which returns the form element that has been updated
* and needs to be returned to the browser, or alternatively, an array of
* custom Ajax commands.
* - The array is serialized using ajax_render() and sent to the browser.
* - The browser unserializes the returned JSON string into an array of
* command objects and executes each command, resulting in the old page
......@@ -125,10 +126,10 @@
* - #ajax['path']: The menu path to use for the request. This is often omitted
* and the default is used. This path should map
* to a menu page callback that returns data using ajax_render(). Defaults to
* 'system/ajax', which invokes ajax_form_callback(), eventually calling
* the function named in #ajax['callback']. If you use a custom
* path, you must set up the menu entry and handle the entire callback in your
* own code.
* 'system/ajax', which invokes \Drupal\system\FormAjaxController::content(),
* eventually calling the function named in #ajax['callback']. If you use a
* custom path, you must set up the menu entry and handle the entire callback
* in your own code.
* - #ajax['wrapper']: The CSS ID of the area to be replaced by the content
* returned by the #ajax['callback'] function. The content returned from
* the callback will replace the entire element named by #ajax['wrapper'].
......@@ -301,88 +302,6 @@ function ajax_render($commands = array()) {
return drupal_json_encode($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 $_POST.
*
* @return
* An array containing the $form and $form_state. Use the list() function
* to break these apart:
* @code
* list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
* @endcode
*/
function ajax_get_form() {
$form_state = form_state_defaults();
$form_build_id = $_POST['form_build_id'];
// Get the form from the cache.
$form = form_get_cache($form_build_id, $form_state);
if (!$form) {
// If $form cannot be loaded from the cache, the form_build_id in $_POST
// 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, so we just do nothing.
watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING);
throw new BadRequestHttpException();
}
// Since some of the submit handlers are run, redirects need to be disabled.
$form_state['no_redirect'] = TRUE;
// When a form is rebuilt after Ajax processing, its #build_id and #action
// should not change.
// @see drupal_rebuild_form()
$form_state['rebuild_info']['copy']['#build_id'] = TRUE;
$form_state['rebuild_info']['copy']['#action'] = TRUE;
// The form needs to be processed; prepare for that by setting a few internal
// variables.
$form_state['input'] = $_POST;
$form_id = $form['#form_id'];
return array($form, $form_state, $form_id, $form_build_id);
}
/**
* Page callback: Handles Ajax requests for the #ajax Form API property.
*
* This rebuilds the form from cache and invokes the defined #ajax['callback']
* to return an Ajax command structure for JavaScript. In case no 'callback' has
* been defined, nothing will happen.
*
* The Form API #ajax property can be set both for buttons and other input
* elements.
*
* This function is also the canonical example of how to implement
* #ajax['path']. If processing is required that cannot be accomplished with
* a callback, re-implement this function and set #ajax['path'] to the
* enhanced function.
*
* @see system_menu()
*/
function ajax_form_callback() {
list($form, $form_state) = ajax_get_form();
drupal_process_form($form['#form_id'], $form, $form_state);
// 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.
if (!empty($form_state['triggering_element'])) {
$callback = $form_state['triggering_element']['#ajax']['callback'];
}
if (!empty($callback) && is_callable($callback)) {
return call_user_func_array($callback, array(&$form, &$form_state));
}
}
/**
* Theme callback: Returns the correct theme for an Ajax request.
*
......
......@@ -471,8 +471,9 @@ function form_state_defaults() {
* 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 ajax_form_callback()
* for more information about creating Ajax-enabled forms.
* common use-case during which form rebuilding occurs. See
* Drupal\system\FormAjaxController::content() for more information about
* creating Ajax-enabled forms.
*
* @param $form_id
* The unique string identifying the desired form. If a function
......@@ -496,7 +497,7 @@ function form_state_defaults() {
* The newly built form.
*
* @see drupal_process_form()
* @see ajax_form_callback()
* @see \Drupal\system\FormAjaxController::content()
*/
function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
$form = drupal_retrieve_form($form_id, $form_state);
......@@ -1292,9 +1293,10 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
* - If $form_state['rebuild'] is TRUE, the form is being rebuilt, and no
* redirection is done.
* - If $form_state['no_redirect'] is TRUE, redirection is disabled. This is
* set, for instance, by ajax_get_form() to prevent redirection in Ajax
* callbacks. $form_state['no_redirect'] should never be set or altered by
* form builder functions or form validation/submit handlers.
* set, for instance, by \Drupal\system\FormAjaxController::getForm() to
* prevent redirection in Ajax callbacks. $form_state['no_redirect'] should
* never be set or altered by form builder functions or form validation/submit
* handlers.
* - If $form_state['redirect'] is set to FALSE, redirection is disabled.
* - If none of the above conditions has prevented redirection, then the
* redirect is accomplished by returning a RedirectResponse, passing in the
......
......@@ -129,8 +129,8 @@ function field_form_element_after_build($element, &$form_state) {
* This handler is run regardless of whether JS is enabled or not. It makes
* changes to the form state. If the button was clicked with JS disabled, then
* the page is reloaded with the complete rebuilt form. If the button was
* clicked with JS enabled, then ajax_form_callback() calls field_add_more_js()
* to return just the changed part of the form.
* clicked with JS enabled, then Drupal\system\FormAjaxController::content()
* calls field_add_more_js() to return just the changed part of the form.
*/
function field_add_more_submit($form, &$form_state) {
$button = $form_state['triggering_element'];
......
......@@ -428,12 +428,19 @@ function file_field_widget_process($element, &$form_state, $form) {
// file, the entire group of file fields is updated together.
if ($element['#cardinality'] != 1) {
$parents = array_slice($element['#array_parents'], 0, -1);
$new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
$new_path = 'file/ajax';
$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']['path'] = $new_path;
$element[$key]['#ajax']['options'] = $new_options;
$element[$key]['#ajax']['wrapper'] = $new_wrapper;
}
}
......
......@@ -46,14 +46,12 @@ function file_menu() {
$items = array();
$items['file/ajax'] = array(
'page callback' => 'file_ajax_upload',
'access arguments' => array('access content'),
'route_name' => 'file.ajax.upload',
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
);
$items['file/progress'] = array(
'page callback' => 'file_ajax_progress',
'access arguments' => array('access content'),
'route name' => 'file.ajax.progress',
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
);
......@@ -959,104 +957,6 @@ function file_save_upload($form_field_name, $validators = array(), $destination
return isset($delta) ? $files[$delta] : $files;
}
/**
* Ajax callback: Processes file uploads and deletions.
*
* This rebuilds the form element for a particular field item. As long as the
* form processing is properly encapsulated in the widget element the form
* should rebuild correctly using FAPI without the need for additional callbacks
* or processing.
*
* @see file_menu()
*/
function file_ajax_upload() {
$form_parents = func_get_args();
$form_build_id = (string) array_pop($form_parents);
$request = \Drupal::request();
if (!$request->request->has('form_build_id') || $form_build_id != $request->request->get('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('#theme' => 'status_messages');
return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
}
list($form, $form_state) = ajax_get_form();
if (!$form) {
// 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('#theme' => 'status_messages');
return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
}
// Get the current element and count the number of files.
$current_element = $form;
foreach ($form_parents as $parent) {
$current_element = $current_element[$parent];
}
$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.
drupal_process_form($form['#form_id'], $form, $form_state);
// Retrieve the element to be rendered.
foreach ($form_parents as $parent) {
$form = $form[$parent];
}
// 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('#theme' => 'status_messages');
$form['#prefix'] .= drupal_render($status_messages);
$output = drupal_render($form);
$js = drupal_add_js();
$settings = drupal_merge_js_settings($js['settings']['data']);
$response = new AjaxResponse();
return $response->addCommand(new ReplaceCommand(NULL, $output, $settings));
}
/**
* Ajax callback: Retrieves upload progress.
*
* @param $key
* The unique key for this upload process.
*/
function file_ajax_progress($key) {
$progress = array(
'message' => t('Starting upload...'),
'percentage' => -1,
);
$implementation = file_progress_implementation();
if ($implementation == 'uploadprogress') {
$status = uploadprogress_get_info($key);
if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) {
$progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total'])));
$progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']);
}
}
elseif ($implementation == 'apc') {
$status = apc_fetch('upload_' . $key);
if (isset($status['current']) && !empty($status['total'])) {
$progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['current']), '@total' => format_size($status['total'])));
$progress['percentage'] = round(100 * $status['current'] / $status['total']);
}
}
return new JsonResponse($progress);
}
/**
* Determines the preferred upload progress implementation.
*
......@@ -1239,7 +1139,13 @@ function file_managed_file_process($element, &$form_state, $form) {
$element['#tree'] = TRUE;
$ajax_settings = array(
'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'],
'path' => 'file/ajax',
'options' => array(
'query' => array(
'element_parents' => implode('/', $element['#array_parents']),
'form_build_id' => $form['form_build_id']['#value'],
),
),
'wrapper' => $element['#id'] . '-ajax-wrapper',
'effect' => 'fade',
'progress' => array(
......
file.ajax.upload:
pattern: '/file/ajax'
defaults:
_controller: '\Drupal\file\Controller\FileWidgetAjaxController::upload'
requirements:
_permission: 'access content'
file.ajax.progress:
pattern: '/file/progress'
defaults:
_controller: '\Drupal\file\Controller\FileWidgetAjaxController::progress'
requirements:
_permission: 'access content'
<?php
/**
* @file
* Contains \Drupal\file\FileWidgetAjaxController.
*/
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('#theme' => 'status_messages');
return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
}
try {
list($form, $form_state) = $this->getForm($request);
}
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('#theme' => 'status_messages');
return $response->addCommand(new ReplaceCommand(NULL, drupal_render($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.
drupal_process_form($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('#theme' => 'status_messages');
$form['#prefix'] .= drupal_render($status_messages);
$output = drupal_render($form);
$js = drupal_add_js();
$settings = drupal_merge_js_settings($js['settings']['data']);
$response = new AjaxResponse();
return $response->addCommand(new ReplaceCommand(NULL, $output, $settings));
}
/**
* Returns the progress status for a file upload process.
*
* @param string $key
* The unique key for this upload process.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JsonResponse object.
*/
public function progress($key) {
$progress = array(
'message' => t('Starting upload...'),
'percentage' => -1,
);
$implementation = file_progress_implementation();
if ($implementation == 'uploadprogress') {
$status = uploadprogress_get_info($key);
if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) {
$progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total'])));
$progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']);
}
}
elseif ($implementation == 'apc') {
$status = apc_fetch('upload_' . $key);
if (isset($status['current']) && !empty($status['total'])) {
$progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['current']), '@total' => format_size($status['total'])));
$progress['percentage'] = round(100 * $status['current'] / $status['total']);
}
}
return new JsonResponse($progress);
}
}
<?php
/**
* @file
* Contains \Drupal\system\FormAjaxController.
*/
namespace Drupal\system\Controller;
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.
*/
class FormAjaxController {
/**
* 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) {
list($form, $form_state) = $this->getForm($request);
drupal_process_form($form['#form_id'], $form, $form_state);
// 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.
if (!empty($form_state['triggering_element'])) {
$callback = $form_state['triggering_element']['#ajax']['callback'];
}
if (empty($callback) || !is_callable($callback)) {
throw new HttpException(500, t('Internal Server Error'));
}
return call_user_func_array($callback, array(&$form, &$form_state));
}
/**
* 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 array
* An array containing the $form and $form_state. Use the list() function
* to break these apart:
* @code
* list($form, $form_state, $form_id, $form_build_id) = $this->getForm();
* @endcode
*
* @throws Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
*/
protected function getForm(Request $request) {
$form_state = form_state_defaults();
$form_build_id = $request->request->get('form_build_id');
// Get the form from the cache.
$form = form_get_cache($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.
watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING);
throw new BadRequestHttpException();
}
// Since some of the submit handlers are run, redirects need to be disabled.
$form_state['no_redirect'] = TRUE;
// When a form is rebuilt after Ajax processing, its #build_id and #action
// should not change.
// @see drupal_rebuild_form()
$form_state['rebuild_info']['copy']['#build_id'] = TRUE;
$form_state['rebuild_info']['copy']['#action'] = TRUE;
// The form needs to be processed; prepare for that by setting a few internal
// variables.
$form_state['input'] = $request->request->all();
$form_id = $form['#form_id'];
return array($form, $form_state, $form_id, $form_build_id);
}
}
......@@ -51,5 +51,15 @@ function testSimpleAjaxFormValue() {
$expected = new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $item);
$this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a checkbox issued with a correct value.');
}
// Verify that AJAX elements with invalid callbacks return error code 500.
foreach (array('null', 'empty', 'nonexistent') as $key) {
$element_name = 'select_' . $key . '_callback';
$edit = array(
$element_name => 'red',
);