Commit ad871286 authored by alexpott's avatar alexpott

Issue #2472323 by dawehner, neclimdul, Crell, kim.pepper, nod_, Wim Leers,...

Issue #2472323 by dawehner, neclimdul, Crell, kim.pepper, nod_, Wim Leers, larowlan, jibran, pwolanin, catch: Move modal / dialog to query parameters
parent 47f69d86
......@@ -850,6 +850,10 @@ services:
arguments: ['@class_resolver', '@current_route_match', '%main_content_renderers%']
tags:
- { name: event_subscriber }
accept_negotiation_406:
class: Drupal\Core\EventSubscriber\AcceptNegotiation406
tags:
- { name: event_subscriber }
main_content_renderer.html:
class: Drupal\Core\Render\MainContent\HtmlRenderer
arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@element_info', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager']
......@@ -882,11 +886,6 @@ services:
tags:
- { name: event_subscriber }
arguments: ['@router', '@router.request_context', NULL, '@request_stack']
view_subscriber:
class: Drupal\Core\EventSubscriber\ViewSubscriber
arguments: ['@title_resolver']
tags:
- { name: event_subscriber }
bare_html_page_renderer:
class: Drupal\Core\Render\BareHtmlPageRenderer
arguments: ['@renderer']
......
......@@ -38,10 +38,9 @@ public function getContentType(Request $request) {
// Check all formats, if priority format is found return it.
$first_found_format = FALSE;
$priority = array('html', 'drupal_ajax', 'drupal_modal', 'drupal_dialog');
foreach ($request->getAcceptableContentTypes() as $mime_type) {
$format = $request->getFormat($mime_type);
if (in_array($format, $priority, TRUE)) {
if ($format === 'html') {
return $format;
}
if (!is_null($format) && !$first_found_format) {
......
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\AcceptNegotiation406.
*/
namespace Drupal\Core\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* View subscriber rendering a 406 if we could not route or render a request.
*
* @todo fix or replace this in https://www.drupal.org/node/2364011
*/
class AcceptNegotiation406 implements EventSubscriberInterface {
/**
* Throws an HTTP 406 error if we get this far, which we normally shouldn't.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
* The event to process.
*/
public function onViewDetect406(GetResponseForControllerResultEvent $event) {
$request = $event->getRequest();
$result = $event->getControllerResult();
// If this is a render array then we assume that the router went with the
// generic controller and not one with a format. If the format requested is
// not HTML though we can also assume that the requested format is invalid
// so we provide a 406 response.
if (is_array($result) && $request->getRequestFormat() !== 'html') {
throw new NotAcceptableHttpException('Not acceptable');
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[KernelEvents::VIEW][] = ['onViewDetect406', -10];
return $events;
}
}
......@@ -65,4 +65,15 @@ public function on405(GetResponseForExceptionEvent $event) {
$event->setResponse($response);
}
/**
* Handles a 406 error for JSON.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
* The event to process.
*/
public function on406(GetResponseForExceptionEvent $event) {
$response = new JsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE);
$event->setResponse($response);
}
}
......@@ -10,8 +10,6 @@
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
......@@ -50,6 +48,14 @@ class MainContentViewSubscriber implements EventSubscriberInterface {
*/
protected $mainContentRenderers;
/**
* URL query attribute to indicate the wrapper used to render a request.
*
* The wrapper format determines how the HTML is wrapped, for example in a
* modal dialog.
*/
const WRAPPER_FORMAT = '_wrapper_format';
/**
* Constructs a new MainContentViewSubscriber object.
*
......@@ -76,22 +82,15 @@ public function onViewRenderArray(GetResponseForControllerResultEvent $event) {
$request = $event->getRequest();
$result = $event->getControllerResult();
$format = $request->getRequestFormat();
// Render the controller result into a response if it's a render array.
if (is_array($result)) {
if (isset($this->mainContentRenderers[$format])) {
$renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$format]);
$event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch));
}
else {
$supported_formats = array_keys($this->mainContentRenderers);
$supported_mimetypes = array_map([$request, 'getMimeType'], $supported_formats);
$event->setResponse(new JsonResponse([
'message' => 'Not Acceptable.',
'supported_mime_types' => $supported_mimetypes,
], 406));
}
if (is_array($result) && ($request->query->has(static::WRAPPER_FORMAT) || $request->getRequestFormat() == 'html')) {
$wrapper = $request->query->get(static::WRAPPER_FORMAT, 'html');
// Fall back to HTML if the requested wrapper envelope is not available.
$wrapper = isset($this->mainContentRenderers[$wrapper]) ? $wrapper : 'html';
$renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$wrapper]);
$event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch));
}
}
......
<?php
/**
* @file
* Definition of Drupal\Core\EventSubscriber\ViewSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Controller\TitleResolverInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Main subscriber for VIEW HTTP responses.
*
* @todo This needs to get refactored to be extensible so that we can handle
* more than just Html and Drupal-specific JSON requests. See
* http://drupal.org/node/1594870
*/
class ViewSubscriber implements EventSubscriberInterface {
/**
* The title resolver.
*
* @var \Drupal\Core\Controller\TitleResolverInterface
*/
protected $titleResolver;
/**
* Constructs a new ViewSubscriber.
*
* @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
* The title resolver.
*/
public function __construct(TitleResolverInterface $title_resolver) {
$this->titleResolver = $title_resolver;
}
/**
* Processes a successful controller into an HTTP 200 response.
*
* Some controllers may not return a response object but simply the body of
* one. The VIEW event is called in that case, to allow us to mutate that
* body into a Response object. In particular we assume that the return
* from an JSON-type response is a JSON string, so just wrap it into a
* Response object.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
* The Event to process.
*/
public function onView(GetResponseForControllerResultEvent $event) {
$request = $event->getRequest();
if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
$method = 'on' . $request->getRequestFormat();
if (method_exists($this, $method)) {
$event->setResponse($this->$method($event));
}
else {
$event->setResponse(new Response('Not Acceptable', 406));
}
}
}
public function onJson(GetResponseForControllerResultEvent $event) {
$page_callback_result = $event->getControllerResult();
$response = new JsonResponse();
$response->setData($page_callback_result);
return $response;
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[KernelEvents::VIEW][] = array('onView');
return $events;
}
}
......@@ -240,7 +240,7 @@ public static function preRenderAjaxForm($element) {
$settings += array(
'url' => isset($settings['callback']) ? Url::fromRoute('system.ajax') : NULL,
'options' => array(),
'accepts' => 'application/vnd.drupal-ajax'
'dialogType' => 'ajax',
);
// @todo Legacy support. Remove in Drupal 8.
......
......@@ -52,7 +52,7 @@
element_settings.url = $(this).attr('href');
element_settings.event = 'click';
}
element_settings.accepts = $(this).data('accepts');
element_settings.dialogType = $(this).data('dialog-type');
element_settings.dialog = $(this).data('dialog-options');
var baseUseAjax = $(this).attr('id');
Drupal.ajax[baseUseAjax] = new Drupal.ajax(baseUseAjax, this, element_settings);
......@@ -256,9 +256,6 @@
}
},
dataType: 'json',
accepts: {
json: element_settings.accepts || 'application/vnd.drupal-ajax'
},
type: 'POST'
};
......@@ -266,6 +263,16 @@
ajax.options.data.dialogOptions = element_settings.dialog;
}
// Ensure that we have a valid URL by adding ? when no query parameter is
// yet available, otherwise append using &.
if (ajax.options.url.indexOf('?') === -1) {
ajax.options.url += '?';
}
else {
ajax.options.url += '&';
}
ajax.options.url += Drupal.ajax.WRAPPER_FORMAT + '=drupal_' + (element_settings.dialogType || 'ajax');
// Bind the ajaxSubmit function to the element event.
$(ajax.element).on(element_settings.event, function (event) {
return ajax.eventResponse(this, event);
......@@ -288,6 +295,14 @@
}
};
/**
* URL query attribute to indicate the wrapper used to render a request.
*
* The wrapper format determines how the HTML is wrapped, for example in a
* modal dialog.
*/
Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format';
/**
* Handle a key press.
*
......
......@@ -362,7 +362,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
]),
'attributes' => array(
'class' => array('use-ajax', 'block-filter-text-source'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode(array(
'width' => 700,
)),
......
......@@ -160,9 +160,10 @@
// a Drupal.ajax instance to load the dialog and trigger it.
var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link"><a>' + Drupal.t('Loading...') + '</a></span></div>');
$content.appendTo($target);
new Drupal.ajax('ckeditor-dialog', $content.find('a').get(0), {
accepts: 'application/vnd.drupal-modal',
dialog: dialogSettings,
dialogType: 'modal',
selector: '.ckeditor-dialog-loading-link',
url: url,
event: 'ckeditor-internal.ckeditor',
......
......@@ -281,7 +281,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'url' => Url::fromRoute($route_name, $route_options),
'attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode(array(
'width' => 700
)),
......
......@@ -26,3 +26,7 @@ services:
class: Drupal\hal\Encoder\JsonEncoder
tags:
- { name: encoder, priority: 10, format: hal_json }
exception.default_json:
class: Drupal\hal\EventSubscriber\ExceptionHalJsonSubscriber
tags:
- { name: event_subscriber }
<?php
/**
* @file
* Contains \Drupal\hal\EventSubscriber\ExceptionHalJsonSubscriber.
*/
namespace Drupal\hal\EventSubscriber;
use Drupal\Core\EventSubscriber\ExceptionJsonSubscriber;
/**
* Handle HAL JSON exceptions the same as JSON exceptions.
*/
class ExceptionHalJsonSubscriber extends ExceptionJsonSubscriber {
/**
* {@inheritdoc}
*/
protected function getHandledFormats() {
return ['hal_json'];
}
}
......@@ -19,7 +19,7 @@
/**
* Subscriber for REST-style routes.
*/
class ResourceRoutes extends RouteSubscriberBase{
class ResourceRoutes extends RouteSubscriberBase {
/**
* The plugin manager for REST plugins.
......
......@@ -61,7 +61,7 @@ public function testRead() {
$response = $this->httpRequest($entity_type . '/9999', 'GET', NULL, $this->defaultMimeType);
$this->assertResponse(404);
$path = $entity_type == 'node' ? '/node/{node}' : '/entity_test/{entity_test}';
$expected_message = Json::encode(['error' => 'A fatal error occurred: The "' . $entity_type . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . $entity_type . '.GET.hal_json")']);
$expected_message = Json::encode(['message' => 'The "' . $entity_type . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . $entity_type . '.GET.hal_json")']);
$this->assertIdentical($expected_message, $response, 'Response message is correct.');
// Make sure that field level access works and that the according field is
......@@ -81,7 +81,7 @@ public function testRead() {
$this->drupalLogout();
$response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType);
$this->assertResponse(403);
$this->assertIdentical('{}', $response);
$this->assertIdentical('{"message":""}', $response);
}
// Try to read a resource which is not REST API enabled.
$account = $this->drupalCreateUser();
......@@ -92,7 +92,9 @@ public function testRead() {
// and hence when there is no matching REST route, the non-REST route is
// used, but it can't render into application/hal+json, so it returns a 406.
$this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
$this->assertTrue(strpos($response, '{"message":"Not Acceptable.","supported_mime_types":') !== FALSE);
$this->assertEqual($response, Json::encode([
'message' => 'Not acceptable',
]));
}
/**
......
......@@ -54,7 +54,7 @@ public function testFormats() {
$this->config->save();
$this->rebuildCache();
// Verify that accessing the resource returns 401.
// Verify that accessing the resource returns 406.
$response = $this->httpRequest($this->entity->urlInfo(), 'GET', NULL, $this->defaultMimeType);
// AcceptHeaderMatcher considers the canonical, non-REST route a match, but
// a lower quality one: no format restrictions means there's always a match,
......
......@@ -13,11 +13,13 @@
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
......@@ -1421,8 +1423,8 @@ protected function drupalGetJSON($path, array $options = array(), array $headers
* Requests a Drupal path in drupal_ajax format and JSON-decodes the response.
*/
protected function drupalGetAjax($path, array $options = array(), array $headers = array()) {
if (!preg_grep('/^Accept:/', $headers)) {
$headers[] = 'Accept: application/vnd.drupal-ajax';
if (!isset($options['query'][MainContentViewSubscriber::WRAPPER_FORMAT])) {
$options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax';
}
return Json::decode($this->drupalGet($path, $options, $headers));
}
......@@ -1650,17 +1652,24 @@ protected function drupalPostForm($path, $edit, $submit, array $options = array(
* @see ajax.js
*/
protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) {
// Get the content of the initial page prior to calling drupalPostForm(),
// since drupalPostForm() replaces $this->content.
if (isset($path)) {
$this->drupalGet($path, $options);
// Avoid sending the wrapper query argument to drupalGet so we can fetch
// the form and populate the internal WebTest values.
$get_options = $options;
unset($get_options['query'][MainContentViewSubscriber::WRAPPER_FORMAT]);
$this->drupalGet($path, $get_options);
}
$content = $this->content;
$drupal_settings = $this->drupalSettings;
if (!preg_grep('/^Accept:/', $headers)) {
$headers[] = 'Accept: application/vnd.drupal-ajax';
}
// Provide a default value for the wrapper envelope.
$options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] =
isset($options['query'][MainContentViewSubscriber::WRAPPER_FORMAT]) ?
$options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] :
'drupal_ajax';
// Get the Ajax settings bound to the triggering element.
if (!isset($ajax_settings)) {
......@@ -1699,8 +1708,26 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p
// Unless a particular path is specified, use the one specified by the
// Ajax settings, or else 'system/ajax'.
if (!isset($ajax_path)) {
$ajax_path = isset($ajax_settings['url']) ? $ajax_settings['url'] : 'system/ajax';
if (isset($ajax_settings['url'])) {
// In order to allow to set for example the wrapper envelope query
// parameter we need to get the system path again.
$parsed_url = UrlHelper::parse($ajax_settings['url']);
$options['query'] = $parsed_url['query'] + $options['query'];
$options += ['fragment' => $parsed_url['fragment']];
// We know that $parsed_url['path'] is already with the base path
// attached.
$ajax_path = preg_replace(
'/^' . preg_quote(base_path(), '/') . '/',
'',
$parsed_url['path']
);
}
else {
$ajax_path = 'system/ajax';
}
}
$ajax_path = $this->container->get('unrouted_url_assembler')->assemble('base://' . $ajax_path, $options);
// Submit the POST request.
$return = Json::decode($this->drupalPostForm(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post));
......
......@@ -7,6 +7,7 @@
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Url;
/**
......@@ -93,7 +94,7 @@ public function testDialog() {
$this->assertRaw($dialog_contents, 'Non-JS modal dialog page present.');
// Emulate going to the JS version of the page and check the JSON response.
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-contents', array(), array('Accept: application/vnd.drupal-modal'));
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$this->assertEqual($modal_expected_response, $ajax_result[3], 'Modal dialog JSON response matches.');
// Check that requesting a "normal" dialog without JS goes to a page.
......@@ -106,7 +107,7 @@ public function testDialog() {
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(
// We have to mock a form element to make drupalPost submit from a link.
'textfield' => 'test',
), array(), 'ajax-test/dialog-contents', array(), array('Accept: application/vnd.drupal-dialog'), NULL, array(
), array(), 'ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog')), array(), NULL, array(
'submit' => array(
'dialogOptions[target]' => 'ajax-test-dialog-wrapper-1',
)
......@@ -119,7 +120,7 @@ public function testDialog() {
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(
// We have to mock a form element to make drupalPost submit from a link.
'textfield' => 'test',
), array(), 'ajax-test/dialog-contents', array(), array('Accept: application/vnd.drupal-dialog'), NULL, array(
), array(), 'ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog')), array(), NULL, array(
// Don't send a target.
'submit' => array()
));
......@@ -159,13 +160,13 @@ public function testDialog() {
$this->assertTrue(!empty($form), 'Non-JS form page present.');
// Emulate going to the JS version of the form and check the JSON response.
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-form', array(), array('Accept: application/vnd.drupal-modal'));
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-form', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$expected_ajax_settings = [
'edit-preview' => [
'callback' => '::preview',
'event' => 'click',
'url' => Url::fromRoute('system.ajax')->toString(),
'accepts' => 'application/vnd.drupal-ajax',
'dialogType' => 'ajax',
'submit' => [
'_triggering_element_name' => 'op',
'_triggering_element_value' => 'Preview',
......@@ -188,7 +189,7 @@ public function testDialog() {
$this->assertTrue(!empty($form), 'Non-JS entity form page present.');
// Emulate going to the JS version of the form and check the JSON response.
$ajax_result = $this->drupalGetAjax('admin/structure/contact/add', array(), array('Accept: application/vnd.drupal-modal'));
$ajax_result = $this->drupalGetAjax('admin/structure/contact/add', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$this->setRawContent($ajax_result[3]['data']);
// Remove the data, the form build id and token will never match.
unset($ajax_result[3]['data']);
......
......@@ -120,7 +120,7 @@ public function dialog() {
'#url' => Url::fromRoute('ajax_test.dialog_contents'),
'#attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-type' => 'modal',
),
);
......@@ -133,7 +133,7 @@ public function dialog() {
'url' => Url::fromRoute('ajax_test.dialog_contents'),
'attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode(array(
'width' => 400,
))
......@@ -144,7 +144,7 @@ public function dialog() {
'url' => Url::fromRoute('ajax_test.dialog_contents'),
'attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-dialog',
'data-dialog-type' => 'dialog',
'data-dialog-options' => json_encode(array(
'target' => 'ajax-test-dialog-wrapper-1',
'width' => 800,
......@@ -156,6 +156,7 @@ public function dialog() {
'url' => Url::fromRoute('ajax_test.dialog_close'),
'attributes' => array(
'class' => array('use-ajax'),
'data-dialog-type' => 'modal',
),
),
'link5' => array(
......@@ -163,7 +164,7 @@ public function dialog() {
'url' => Url::fromRoute('ajax_test.dialog_form'),
'attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-type' => 'modal',
),
),
'link6' => array(
......@@ -171,7 +172,7 @@ public function dialog() {
'url' => Url::fromRoute('contact.form_add'),
'attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode(array(
'width' => 800,
'height' => 500,
......@@ -183,7 +184,7 @@ public function dialog() {
'url' => Url::fromRoute('ajax_test.dialog_contents'),
'attributes' => array(