/** * {@inheritdoc} */ public function render(array $js_assets) { $elements = array(); // A dummy query-string is added to filenames, to gain control over // browser-caching. The string changes on every update or full cache // flush, forcing browsers to load a new copy of the files, as the // URL changed. Files that should not be cached (see _drupal_add_js()) // get REQUEST_TIME as query-string instead, to enforce reload on every // page request. $default_query_string = $this->state->get('system.css_js_query_string') ?: '0'; // For inline JavaScript to validate as XHTML, all JavaScript containing // XHTML needs to be wrapped in CDATA. To make that backwards compatible // with HTML 4, we need to comment out the CDATA-tag. $embed_prefix = "\n<!--//--><![CDATA[//><!--\n"; $embed_suffix = "\n//--><!]]>\n"; // Defaults for each SCRIPT element. $element_defaults = array('#type' => 'html_tag', '#tag' => 'script', '#value' => ''); // Loop through all JS assets. foreach ($js_assets as $js_asset) { // Element properties that do not depend on JS asset type. $element = $element_defaults; $element['#browsers'] = $js_asset['browsers']; // Element properties that depend on item type. switch ($js_asset['type']) { case 'setting': $element['#value_prefix'] = $embed_prefix; $element['#value'] = 'var drupalSettings = ' . Json::encode(drupal_merge_js_settings($js_asset['data'])) . ";"; $element['#value_suffix'] = $embed_suffix; break; case 'inline': $element['#value_prefix'] = $embed_prefix; $element['#value'] = $js_asset['data']; $element['#value_suffix'] = $embed_suffix; break; case 'file': $query_string = empty($js_asset['version']) ? $default_query_string : 'v=' . $js_asset['version']; $query_string_separator = strpos($js_asset['data'], '?') !== FALSE ? '&' : '?'; $element['#attributes']['src'] = file_create_url($js_asset['data']); // Only add the cache-busting query string if this isn't an aggregate // file. if (!isset($js_asset['preprocessed'])) { $element['#attributes']['src'] .= $query_string_separator . ($js_asset['cache'] ? $query_string : REQUEST_TIME); } break; case 'external': $element['#attributes']['src'] = $js_asset['data']; break; default: throw new \Exception('Invalid JS asset type.'); } // Attributes may only be set if this script is output independently. if (!empty($element['#attributes']['src']) && !empty($js_asset['attributes'])) { $element['#attributes'] += $js_asset['attributes']; } $elements[] = $element; } return $elements; }
/** * 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)); }
/** * Processes an AJAX response into current content. * * This processes the AJAX response as ajax.js does. It uses the response's * JSON data, an array of commands, to update $this->content using equivalent * DOM manipulation as is used by ajax.js. * It does not apply custom AJAX commands though, because emulation is only * implemented for the AJAX commands that ship with Drupal core. * * @param string $content * The current HTML content. * @param array $ajax_response * An array of AJAX commands. * @param array $ajax_settings * An array of AJAX settings which will be used to process the response. * @param array $drupal_settings * An array of settings to update the value of drupalSettings for the * currently-loaded page. * * @see drupalPostAjaxForm() * @see ajax.js */ protected function drupalProcessAjaxResponse($content, array $ajax_response, array $ajax_settings, array $drupal_settings) { // ajax.js applies some defaults to the settings object, so do the same // for what's used by this function. $ajax_settings += array('method' => 'replaceWith'); // DOM can load HTML soup. But, HTML soup can throw warnings, suppress // them. $dom = new \DOMDocument(); @$dom->loadHTML($content); // XPath allows for finding wrapper nodes better than DOM does. $xpath = new \DOMXPath($dom); foreach ($ajax_response as $command) { // Error messages might be not commands. if (!is_array($command)) { continue; } switch ($command['command']) { case 'settings': $drupal_settings = drupal_merge_js_settings(array($drupal_settings, $command['settings'])); break; case 'insert': $wrapperNode = NULL; // When a command doesn't specify a selector, use the // #ajax['wrapper'] which is always an HTML ID. if (!isset($command['selector'])) { $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); } elseif (in_array($command['selector'], array('head', 'body'))) { $wrapperNode = $xpath->query('//' . $command['selector'])->item(0); } if ($wrapperNode) { // ajax.js adds an enclosing DIV to work around a Safari bug. $newDom = new \DOMDocument(); @$newDom->loadHTML('<div>' . $command['data'] . '</div>'); $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; // The "method" is a jQuery DOM manipulation function. Emulate // each one using PHP's DOMNode API. switch ($method) { case 'replaceWith': $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); break; case 'append': $wrapperNode->appendChild($newNode); break; case 'prepend': // If no firstChild, insertBefore() falls back to // appendChild(). $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); break; case 'before': $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); break; case 'after': // If no nextSibling, insertBefore() falls back to // appendChild(). $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); break; case 'html': foreach ($wrapperNode->childNodes as $childNode) { $wrapperNode->removeChild($childNode); } $wrapperNode->appendChild($newNode); break; } } break; // @todo Add suitable implementations for these commands in order to // have full test coverage of what ajax.js can do. // @todo Add suitable implementations for these commands in order to // have full test coverage of what ajax.js can do. case 'remove': break; case 'changed': break; case 'css': break; case 'data': break; case 'restripe': break; case 'add_css': break; case 'update_build_id': $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0); if ($buildId) { $buildId->setAttribute('value', $command['new']); } break; } } $content = $dom->saveHTML(); $this->setRawContent($content); $this->setDrupalSettings($drupal_settings); }
/** * 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; }