/** * 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 { /** @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('#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'; } 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); drupal_process_attached($form); $js = _drupal_add_js(); $settings = drupal_merge_js_settings($js['settings']['data']); $response = new AjaxResponse(); foreach ($commands as $command) { $response->addCommand($command, TRUE); } return $response->addCommand(new ReplaceCommand(NULL, $output, $settings)); }
/** * If $sticky is FALSE, no tableheader.js should be included. */ function testThemeTableNoStickyHeaders() { $header = array('one', 'two', 'three'); $rows = array(array(1, 2, 3), array(4, 5, 6), array(7, 8, 9)); $attributes = array(); $caption = NULL; $colgroups = array(); $table = array('#type' => 'table', '#header' => $header, '#rows' => $rows, '#attributes' => $attributes, '#caption' => $caption, '#colgroups' => $colgroups, '#sticky' => FALSE); $this->render($table); $js = _drupal_add_js(); $this->assertFalse(isset($js['core/misc/tableheader.js']), 'tableheader.js not found.'); $this->assertNoRaw('sticky-enabled'); drupal_static_reset('_drupal_add_js'); }
/** * Tests AjaxResponse::prepare() AJAX commands ordering. */ public function testOrder() { $path = drupal_get_path('module', 'system'); $expected_commands = array(); // Expected commands, in a very specific order. $expected_commands[0] = new SettingsCommand(array('ajax' => 'test'), TRUE); drupal_static_reset('_drupal_add_css'); $attached = array('#attached' => array('css' => array($path . '/css/system.admin.css' => array(), $path . '/css/system.maintenance.css' => array()))); drupal_render($attached); drupal_process_attached($attached); $expected_commands[1] = new AddCssCommand(drupal_get_css(_drupal_add_css(), TRUE)); drupal_static_reset('_drupal_add_js'); $attached = array('#attached' => array('js' => array($path . '/system.js' => array()))); drupal_render($attached); drupal_process_attached($attached); $expected_commands[2] = new PrependCommand('head', drupal_get_js('header', _drupal_add_js(), TRUE)); drupal_static_reset('_drupal_add_js'); $attached = array('#attached' => array('js' => array($path . '/system.modules.js' => array('scope' => 'footer')))); drupal_render($attached); drupal_process_attached($attached); $expected_commands[3] = new AppendCommand('body', drupal_get_js('footer', _drupal_add_js(), TRUE)); $expected_commands[4] = new HtmlCommand('body', 'Hello, world!'); // Load any page with at least one CSS file, at least one JavaScript file // and at least one #ajax-powered element. The latter is an assumption of // drupalPostAjaxForm(), the two former are assumptions of // AjaxReponse::ajaxRender(). // @todo refactor AJAX Framework + tests to make less assumptions. $this->drupalGet('ajax_forms_test_lazy_load_form'); // Verify AJAX command order — this should always be the order: // 1. JavaScript settings // 2. CSS files // 3. JavaScript files in the header // 4. JavaScript files in the footer // 5. Any other AJAX commands, in whatever order they were added. $commands = $this->drupalPostAjaxForm(NULL, array(), NULL, 'ajax-test/order', array(), array(), NULL, array()); $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[0]->render(), 'Settings command is first.'); $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[1]->render(), 'CSS command is second (and CSS files are ordered correctly).'); $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[2]->render(), 'Header JS command is third.'); $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[3]->render(), 'Footer JS command is fourth.'); $this->assertCommand(array_slice($commands, 4, 1), $expected_commands[4]->render(), 'HTML command is fifth.'); }
/** * {@inheritdoc} */ public function getForm(ViewStorageInterface $view, $display_id, $js) { $form_state = $this->getFormState($view, $display_id, $js); $view = $form_state->get('view'); $key = $form_state->get('form_key'); // @todo Remove the need for this. \Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin'); \Drupal::moduleHandler()->loadInclude('views', 'inc', 'includes/ajax'); // Reset the cache of IDs. Drupal rather aggressively prevents ID // duplication but this causes it to remember IDs that are no longer even // being used. Html::resetSeenIds(); // check to see if this is the top form of the stack. If it is, pop // it off; if it isn't, the user clicked somewhere else and the stack is // now irrelevant. if (!empty($view->stack)) { $identifier = implode('-', array_filter([$key, $view->id(), $display_id, $form_state->get('type'), $form_state->get('id')])); // Retrieve the first form from the stack without changing the integer keys, // as they're being used for the "2 of 3" progress indicator. reset($view->stack); list($key, $top) = each($view->stack); unset($view->stack[$key]); if (array_shift($top) != $identifier) { $view->stack = array(); } } // Automatically remove the form cache if it is set and the key does // not match. This way navigating away from the form without hitting // update will work. if (isset($view->form_cache) && $view->form_cache['key'] != $key) { unset($view->form_cache); } // With the below logic, we may end up rendering a form twice (or two forms // each sharing the same element ids), potentially resulting in // _drupal_add_js() being called twice to add the same setting. drupal_get_js() // is ok with that, but until \Drupal\Core\Ajax\AjaxResponse::ajaxRender() // is (http://drupal.org/node/208611), reset the _drupal_add_js() static // before rendering the second time. $drupal_add_js_original = _drupal_add_js(); $drupal_add_js =& drupal_static('_drupal_add_js'); $form_class = get_class($form_state->getFormObject()); $response = views_ajax_form_wrapper($form_class, $form_state); // If the form has not been submitted, or was not set for rerendering, stop. if (!$form_state->isSubmitted() || $form_state->get('rerender')) { return $response; } // Sometimes we need to re-generate the form for multi-step type operations. if (!empty($view->stack)) { $drupal_add_js = $drupal_add_js_original; $stack = $view->stack; $top = array_shift($stack); // Build the new form state for the next form in the stack. $reflection = new \ReflectionClass($view::$forms[$top[1]]); /** @var $form_state \Drupal\Core\Form\FormStateInterface */ $form_state = $reflection->newInstanceArgs(array_slice($top, 3, 2))->getFormState($view, $top[2], $form_state->get('ajax')); $form_class = get_class($form_state->getFormObject()); $form_state->setUserInput(array()); $form_path = views_ui_build_form_path($form_state); if (!$form_state->get('ajax')) { return new RedirectResponse(_url($form_path, array('absolute' => TRUE))); } $form_state->set('path', $form_path); $response = views_ajax_form_wrapper($form_class, $form_state); } elseif (!$form_state->get('ajax')) { // if nothing on the stack, non-js forms just go back to the main view editor. $display_id = $form_state->get('display_id'); return new RedirectResponse($this->url('entity.view.edit_display_form', ['view' => $view->id(), 'display_id' => $display_id], ['absolute' => TRUE])); } else { $response = new AjaxResponse(); $response->addCommand(new CloseModalDialogCommand()); $response->addCommand(new Ajax\ShowButtonsCommand(!empty($view->changed))); $response->addCommand(new Ajax\TriggerPreviewCommand()); if ($page_title = $form_state->get('page_title')) { $response->addCommand(new Ajax\ReplaceTitleCommand($page_title)); } } // If this form was for view-wide changes, there's no need to regenerate // the display section of the form. if ($display_id !== '') { \Drupal::entityManager()->getFormObject('view', 'edit')->rebuildCurrentTab($view, $response, $display_id); } return $response; }
/** * Tests JavaScript files that have querystrings attached get added right. */ function testAddJsFileWithQueryString() { $js = drupal_get_path('module', 'node') . '/node.js'; _drupal_add_js($js); $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; $scripts = drupal_get_js(); $this->assertTrue(strpos($scripts, $js . '?' . $query_string), 'Query string was appended correctly to JS.'); }
/** * Tests css/js storage and restoring mechanism. */ function testHeaderStorage() { // Create a view with output caching enabled. // Some hook_views_pre_render in views_test_data.module adds the test css/js file. // so they should be added to the css/js storage. $view = Views::getView('test_view'); $view->setDisplay(); $view->storage->set('id', 'test_cache_header_storage'); $view->display_handler->overrideOption('cache', array('type' => 'time', 'options' => array('output_lifespan' => '3600'))); $output = $view->preview(); drupal_render($output); unset($view->pre_render_called); drupal_static_reset('_drupal_add_css'); drupal_static_reset('_drupal_add_js'); $view->destroy(); $view->setDisplay(); $output = $view->preview(); drupal_render($output); $css = _drupal_add_css(); $css_path = drupal_get_path('module', 'views_test_data') . '/views_cache.test.css'; $js_path = drupal_get_path('module', 'views_test_data') . '/views_cache.test.js'; $js = _drupal_add_js(); $this->assertTrue(isset($css[basename($css_path)]), 'Make sure the css is added for cached views.'); $this->assertTrue(isset($js[$js_path]), 'Make sure the js is added for cached views.'); $this->assertFalse(!empty($view->build_info['pre_render_called']), 'Make sure hook_views_pre_render is not called for the cached view.'); // Now add some css/jss before running the view. // Make sure that this css is not added when running the cached view. $view->storage->set('id', 'test_cache_header_storage_2'); $attached = array('#attached' => array('css' => array(drupal_get_path('module', 'system') . '/css/system.maintenance.css' => array()), 'js' => array(drupal_get_path('module', 'user') . '/user.permissions.js' => array()))); drupal_render($attached); $view->destroy(); $output = $view->preview(); drupal_render($output); drupal_static_reset('_drupal_add_css'); drupal_static_reset('_drupal_add_js'); $view->destroy(); $output = $view->preview(); drupal_render($output); $css = _drupal_add_css(); $js = _drupal_add_js(); $this->assertFalse(isset($css['system.maintenance.css']), 'Make sure that unrelated css is not added.'); $this->assertFalse(isset($js[drupal_get_path('module', 'user') . '/user.permissions.js']), 'Make sure that unrelated js is not added.'); }
/** * Prepares the AJAX commands for sending back to the client. * * @param Request $request * The request object that the AJAX is responding to. * * @return array * An array of commands ready to be returned as JSON. */ protected function ajaxRender(Request $request) { // Ajax responses aren't rendered with html.html.twig, so we have to call // drupal_get_css() and drupal_get_js() here, in order to have new files // added during this request to be loaded by the page. We only want to send // back files that the page hasn't already loaded, so we implement simple // diffing logic using array_diff_key(). $ajax_page_state = $request->request->get('ajax_page_state'); foreach (array('css', 'js') as $type) { // It is highly suspicious if // $request->request->get("ajax_page_state[$type]") is empty, since the // base page ought to have at least one JS file and one CSS file loaded. // It probably indicates an error, and rather than making the page reload // all of the files, instead we return no new files. if (empty($ajax_page_state[$type])) { $items[$type] = array(); } else { $function = '_drupal_add_' . $type; $items[$type] = $function(); \Drupal::moduleHandler()->alter($type, $items[$type]); // @todo Inline CSS and JS items are indexed numerically. These can't be // reliably diffed with array_diff_key(), since the number can change // due to factors unrelated to the inline content, so for now, we // strip the inline items from Ajax responses, and can add support for // them when _drupal_add_css() and _drupal_add_js() are changed to use // a hash of the inline content as the array key. foreach ($items[$type] as $key => $item) { if (is_numeric($key)) { unset($items[$type][$key]); } } // Ensure that the page doesn't reload what it already has. $items[$type] = array_diff_key($items[$type], $ajax_page_state[$type]); } } // Render the HTML to load these files, and add AJAX commands to insert this // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the // data from being altered again, as we already altered it above. Settings // are handled separately, afterwards. if (isset($items['js']['settings'])) { unset($items['js']['settings']); } $styles = drupal_get_css($items['css'], TRUE); $scripts_footer = drupal_get_js('footer', $items['js'], TRUE, TRUE); $scripts_header = drupal_get_js('header', $items['js'], TRUE, TRUE); // Prepend commands to add the resources, preserving their relative order. $resource_commands = array(); if (!empty($styles)) { $resource_commands[] = new AddCssCommand($styles); } if (!empty($scripts_header)) { $resource_commands[] = new PrependCommand('head', $scripts_header); } if (!empty($scripts_footer)) { $resource_commands[] = new AppendCommand('body', $scripts_footer); } foreach (array_reverse($resource_commands) as $resource_command) { $this->addCommand($resource_command, TRUE); } // Prepend a command to merge changes and additions to drupalSettings. $scripts = _drupal_add_js(); if (!empty($scripts['settings'])) { $settings = drupal_merge_js_settings($scripts['settings']['data']); // During Ajax requests basic path-specific settings are excluded from // new drupalSettings values. The original page where this request comes // from already has the right values for the keys below. An Ajax request // would update them with values for the Ajax request and incorrectly // override the page's values. // @see _drupal_add_js() foreach (array('basePath', 'currentPath', 'scriptPath', 'pathPrefix') as $item) { unset($settings[$item]); } $this->addCommand(new SettingsCommand($settings, TRUE), TRUE); } $commands = $this->commands; \Drupal::moduleHandler()->alter('ajax_render', $commands); return $commands; }