/** * Displays details about a specific database log message. * * @param int $event_id * Unique ID of the database log message. * * @return array * If the ID is located in the Database Logging table, a build array in the * format expected by drupal_render(); * */ public function eventDetails($event_id) { $build = array(); if ($dblog = $this->database->query('SELECT w.*, u.uid FROM {watchdog} w LEFT JOIN {users} u ON u.uid = w.uid WHERE w.wid = :id', array(':id' => $event_id))->fetchObject()) { $severity = RfcLogLevel::getLevels(); $message = $this->formatMessage($dblog); $username = array('#theme' => 'username', '#account' => $dblog->uid ? $this->userStorage->load($dblog->uid) : User::getAnonymousUser()); $rows = array(array(array('data' => $this->t('Type'), 'header' => TRUE), $this->t($dblog->type)), array(array('data' => $this->t('Date'), 'header' => TRUE), $this->dateFormatter->format($dblog->timestamp, 'long')), array(array('data' => $this->t('User'), 'header' => TRUE), array('data' => $username)), array(array('data' => $this->t('Location'), 'header' => TRUE), $this->l($dblog->location, $dblog->location ? Url::fromUri($dblog->location) : Url::fromRoute('<none>'))), array(array('data' => $this->t('Referrer'), 'header' => TRUE), $this->l($dblog->referer, $dblog->referer ? Url::fromUri($dblog->referer) : Url::fromRoute('<none>'))), array(array('data' => $this->t('Message'), 'header' => TRUE), $message), array(array('data' => $this->t('Severity'), 'header' => TRUE), $severity[$dblog->severity]), array(array('data' => $this->t('Hostname'), 'header' => TRUE), SafeMarkup::checkPlain($dblog->hostname)), array(array('data' => $this->t('Operations'), 'header' => TRUE), SafeMarkup::checkAdminXss($dblog->link))); $build['dblog_table'] = array('#type' => 'table', '#rows' => $rows, '#attributes' => array('class' => array('dblog-event')), '#attached' => array('library' => array('dblog/drupal.dblog'))); } return $build; }
/** * See the docs for ::render(). */ protected function doRender(&$elements, $is_root_call = FALSE) { if (!isset($elements['#access']) && isset($elements['#access_callback'])) { if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); } $elements['#access'] = call_user_func($elements['#access_callback'], $elements); } // Early-return nothing if user does not have access. if (empty($elements) || isset($elements['#access']) && !$elements['#access']) { return ''; } // Do not print elements twice. if (!empty($elements['#printed'])) { return ''; } if (!isset(static::$stack)) { static::$stack = new \SplStack(); } static::$stack->push(new BubbleableMetadata()); // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely // has these configurable defaults, even when no subtree is render cached. // - this is a render cacheable subtree, to ensure that the cached data has // the configurable defaults (which may affect the ID and invalidation). if ($is_root_call || isset($elements['#cache']['keys'])) { $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; if (isset($elements['#cache']['contexts'])) { $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); } else { $elements['#cache']['contexts'] = $required_cache_contexts; } } // Try to fetch the prerendered element from cache, run any // #post_render_cache callbacks and return the final markup. if (isset($elements['#cache']['keys'])) { $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; // Only when we're not in a root (non-recursive) drupal_render() call, // #post_render_cache callbacks must be executed, to prevent breaking // the render cache in case of nested elements with #cache set. if ($is_root_call) { $this->processPostRenderCache($elements); } // Mark the element markup as safe. If we have cached children, we need // to mark them as safe too. The parent markup contains the child // markup, so if the parent markup is safe, then the markup of the // individual children must be safe as well. $elements['#markup'] = SafeMarkup::set($elements['#markup']); if (!empty($elements['#cache_properties'])) { foreach (Element::children($cached_element) as $key) { SafeMarkup::set($cached_element[$key]['#markup']); } } // The render cache item contains all the bubbleable rendering metadata // for the subtree. $this->updateStack($elements); // Render cache hit, so rendering is finished, all necessary info // collected! $this->bubbleStack(); return $elements['#markup']; } } // Two-tier caching: track pre-bubbling elements' #cache for later // comparison. // @see \Drupal\Core\Render\RenderCacheInterface::get() // @see \Drupal\Core\Render\RenderCacheInterface::set() $pre_bubbling_elements = []; $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : []; // If the default values for this element have not been loaded yet, populate // them. if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { $elements += $this->elementInfo->getInfo($elements['#type']); } // Make any final changes to the element before it is rendered. This means // that the $element or the children can be altered or corrected before the // element is rendered into the final text. if (isset($elements['#pre_render'])) { foreach ($elements['#pre_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements = call_user_func($callable, $elements); } } // Defaults for bubbleable rendering metadata. $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); // Allow #pre_render to abort rendering. if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. $this->updateStack($elements); // #printed, so rendering is finished, all necessary info collected! $this->bubbleStack(); return ''; } // Add any JavaScript state information associated with the element. if (!empty($elements['#states'])) { drupal_process_states($elements); } // Get the children of the element, sorted by weight. $children = Element::children($elements, TRUE); // Initialize this element's #children, unless a #pre_render callback // already preset #children. if (!isset($elements['#children'])) { $elements['#children'] = ''; } // @todo Simplify after https://www.drupal.org/node/2273925. if (isset($elements['#markup'])) { $elements['#markup'] = SafeMarkup::set($elements['#markup']); } // Assume that if #theme is set it represents an implemented hook. $theme_is_implemented = isset($elements['#theme']); // Check the elements for insecure HTML and pass through sanitization. if (isset($elements)) { $markup_keys = array('#description', '#field_prefix', '#field_suffix'); foreach ($markup_keys as $key) { if (!empty($elements[$key]) && is_scalar($elements[$key])) { $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]); } } } // Call the element's #theme function if it is set. Then any children of the // element have to be rendered there. If the internal #render_children // property is set, do not call the #theme function to prevent infinite // recursion. if ($theme_is_implemented && !isset($elements['#render_children'])) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the // hook in #theme was not found in the registry and so we need to update // our flag accordingly. This is common for theme suggestions. $theme_is_implemented = $elements['#children'] !== FALSE; } // If #theme is not implemented or #render_children is set and the element // has an empty #children attribute, render the children now. This is the // same process as Renderer::render() but is inlined for speed. if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } $elements['#children'] = SafeMarkup::set($elements['#children']); } // If #theme is not implemented and the element has raw #markup as a // fallback, prepend the content in #markup to #children. In this case // #children will contain whatever is provided by #pre_render prepended to // what is rendered recursively above. If #theme is implemented then it is // the responsibility of that theme implementation to render #markup if // required. Eventually #theme_wrappers will expect both #markup and // #children to be a single string as #children. if (!$theme_is_implemented && isset($elements['#markup'])) { $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']); } // Let the theme functions in #theme_wrappers add markup around the rendered // children. // #states and #attached have to be processed before #theme_wrappers, // because the #type 'page' render array from drupal_prepare_page() would // render the $page and wrap it into the html.html.twig template without the // attached assets otherwise. // If the internal #render_children property is set, do not call the // #theme_wrappers function(s) to prevent infinite recursion. if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { foreach ($elements['#theme_wrappers'] as $key => $value) { // If the value of a #theme_wrappers item is an array then the theme // hook is found in the key of the item and the value contains attribute // overrides. Attribute overrides replace key/value pairs in $elements // for only this ThemeManagerInterface::render() call. This allows // #theme hooks and #theme_wrappers hooks to share variable names // without conflict or ambiguity. $wrapper_elements = $elements; if (is_string($key)) { $wrapper_hook = $key; foreach ($value as $attribute => $override) { $wrapper_elements[$attribute] = $override; } } else { $wrapper_hook = $value; } $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); } } // Filter the outputted content and make any last changes before the content // is sent to the browser. The changes are made on $content which allows the // outputted text to be filtered. if (isset($elements['#post_render'])) { foreach ($elements['#post_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); } } // We store the resulting output in $elements['#markup'], to be consistent // with how render cached output gets stored. This ensures that // #post_render_cache callbacks get the same data to work with, no matter if // #cache is disabled, #cache is enabled, there is a cache hit or miss. $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : ''; $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : ''; $elements['#markup'] = $prefix . $elements['#children'] . $suffix; // We've rendered this element (and its subtree!), now update the stack. $this->updateStack($elements); // Cache the processed element if both $pre_bubbling_elements and $elements // have the metadata necessary to generate a cache ID. if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); } $this->renderCache->set($elements, $pre_bubbling_elements); } // Only when we're in a root (non-recursive) drupal_render() call, // #post_render_cache callbacks must be executed, to prevent breaking the // render cache in case of nested elements with #cache set. // // By running them here, we ensure that: // - they run when #cache is disabled, // - they run when #cache is enabled and there is a cache miss. // Only the case of a cache hit when #cache is enabled, is not handled here, // that is handled earlier in Renderer::render(). if ($is_root_call) { // We've already called ::updateStack() earlier, which updated both the // element and current stack frame. However, // Renderer::processPostRenderCache() can both change the element // further and create and render new child elements, so provide a fresh // stack frame to collect those additions, merge them back to the element, // and then update the current frame to match the modified element state. do { static::$stack->push(new BubbleableMetadata()); $this->processPostRenderCache($elements); $post_render_additions = static::$stack->pop(); $elements['#post_render_cache'] = NULL; BubbleableMetadata::createFromRenderArray($elements)->merge($post_render_additions)->applyTo($elements); } while (!empty($elements['#post_render_cache'])); if (static::$stack->count() !== 1) { throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); } } // Rendering is finished, all necessary info collected! $this->bubbleStack(); $elements['#printed'] = TRUE; $elements['#markup'] = SafeMarkup::set($elements['#markup']); return $elements['#markup']; }
/** * Adds the result form to a $form. * * This is a static method so that run-tests.sh can use it to generate a * results page completely external to Drupal. This is why the UI strings are * not wrapped in t(). * * @param array $form * The form to attach the results to. * @param array $test_results * The simpletest results. * * @return array * A list of tests the passed and failed. The array has two keys, 'pass' and * 'fail'. Each contains a list of test classes. * * @see simpletest_script_open_browser() * @see run-tests.sh */ public static function addResultForm(array &$form, array $results) { // Transform the test results to be grouped by test class. $test_results = array(); foreach ($results as $result) { if (!isset($test_results[$result->test_class])) { $test_results[$result->test_class] = array(); } $test_results[$result->test_class][] = $result; } $image_status_map = static::buildStatusImageMap(); // Keep track of which test cases passed or failed. $filter = array('pass' => array(), 'fail' => array()); // Summary result widget. $form['result'] = array('#type' => 'fieldset', '#title' => 'Results', '#attributes' => array()); $form['result']['summary'] = $summary = array('#theme' => 'simpletest_result_summary', '#pass' => 0, '#fail' => 0, '#exception' => 0, '#debug' => 0); \Drupal::service('test_discovery')->registerTestNamespaces(); // Cycle through each test group. $header = array('Message', 'Group', 'Filename', 'Line', 'Function', array('colspan' => 2, 'data' => 'Status')); $form['result']['results'] = array(); foreach ($test_results as $group => $assertions) { // Create group details with summary information. $info = TestDiscovery::getTestInfo($group); $form['result']['results'][$group] = array('#type' => 'details', '#title' => $info['name'], '#open' => TRUE, '#description' => $info['description']); $form['result']['results'][$group]['summary'] = $summary; $group_summary =& $form['result']['results'][$group]['summary']; // Create table of assertions for the group. $rows = array(); foreach ($assertions as $assertion) { $row = array(); $row[] = SafeMarkup::checkAdminXss($assertion->message); $row[] = $assertion->message_group; $row[] = \Drupal::service('file_system')->basename($assertion->file); $row[] = $assertion->line; $row[] = $assertion->function; $row[] = ['data' => $image_status_map[$assertion->status]]; $class = 'simpletest-' . $assertion->status; if ($assertion->message_group == 'Debug') { $class = 'simpletest-debug'; } $rows[] = array('data' => $row, 'class' => array($class)); $group_summary['#' . $assertion->status]++; $form['result']['summary']['#' . $assertion->status]++; } $form['result']['results'][$group]['table'] = array('#type' => 'table', '#header' => $header, '#rows' => $rows); // Set summary information. $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0; $form['result']['results'][$group]['#open'] = !$group_summary['#ok']; // Store test group (class) as for use in filter. $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group; } // Overall summary status. $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0; return $filter; }
/** * Pre-render callback: Renders #browsers into #prefix and #suffix. * * @param array $element * A render array with a '#browsers' property. The '#browsers' property can * contain any or all of the following keys: * - 'IE': If FALSE, the element is not rendered by Internet Explorer. If * TRUE, the element is rendered by Internet Explorer. Can also be a string * containing an expression for Internet Explorer to evaluate as part of a * conditional comment. For example, this can be set to 'lt IE 7' for the * element to be rendered in Internet Explorer 6, but not in Internet * Explorer 7 or higher. Defaults to TRUE. * - '!IE': If FALSE, the element is not rendered by browsers other than * Internet Explorer. If TRUE, the element is rendered by those browsers. * Defaults to TRUE. * Examples: * - To render an element in all browsers, '#browsers' can be left out or set * to array('IE' => TRUE, '!IE' => TRUE). * - To render an element in Internet Explorer only, '#browsers' can be set * to array('!IE' => FALSE). * - To render an element in Internet Explorer 6 only, '#browsers' can be set * to array('IE' => 'lt IE 7', '!IE' => FALSE). * - To render an element in Internet Explorer 8 and higher and in all other * browsers, '#browsers' can be set to array('IE' => 'gte IE 8'). * * @return array * The passed-in element with markup for conditional comments potentially * added to '#prefix' and '#suffix'. */ public static function preRenderConditionalComments($element) { $browsers = isset($element['#browsers']) ? $element['#browsers'] : array(); $browsers += array('IE' => TRUE, '!IE' => TRUE); // If rendering in all browsers, no need for conditional comments. if ($browsers['IE'] === TRUE && $browsers['!IE']) { return $element; } // Determine the conditional comment expression for Internet Explorer to // evaluate. if ($browsers['IE'] === TRUE) { $expression = 'IE'; } elseif ($browsers['IE'] === FALSE) { $expression = '!IE'; } else { // The IE expression might contain some user input data. $expression = SafeMarkup::checkAdminXss($browsers['IE']); } // If the #prefix and #suffix properties are used, wrap them with // conditional comment markup. The conditional comment expression is // evaluated by Internet Explorer only. To control the rendering by other // browsers, use either the "downlevel-hidden" or "downlevel-revealed" // technique. See http://en.wikipedia.org/wiki/Conditional_comment // for details. // Ensure what we are dealing with is safe. // This would be done later anyway in drupal_render(). $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : ''; $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : ''; // Now calling SafeMarkup::set is safe, because we ensured the // data coming in was at least admin escaped. if (!$browsers['!IE']) { // "downlevel-hidden". $element['#prefix'] = SafeMarkup::set("\n<!--[if {$expression}]>\n" . $prefix); $element['#suffix'] = SafeMarkup::set($suffix . "<![endif]-->\n"); } else { // "downlevel-revealed". $element['#prefix'] = SafeMarkup::set("\n<!--[if {$expression}]><!-->\n" . $prefix); $element['#suffix'] = SafeMarkup::set($suffix . "<!--<![endif]-->\n"); } return $element; }
/** * Render all items in this field together. * * When using advanced render, each possible item in the list is rendered * individually. Then the items are all pasted together. */ public function renderItems($items) { if (!empty($items)) { if ($this->options['multi_type'] == 'separator' || !$this->options['group_rows']) { $separator = $this->options['multi_type'] == 'separator' ? SafeMarkup::checkAdminXss($this->options['separator']) : ''; $build = ['#type' => 'inline_template', '#template' => '{{ items | safe_join(separator) }}', '#context' => ['separator' => $separator, 'items' => $items]]; } else { $build = array('#theme' => 'item_list', '#items' => $items, '#title' => NULL, '#list_type' => $this->options['multi_type']); } return $this->renderer->render($build); } }
/** * See the docs for ::render(). */ protected function doRender(&$elements, $is_root_call = FALSE) { if (empty($elements)) { return ''; } if (!isset($elements['#access']) && isset($elements['#access_callback'])) { if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); } $elements['#access'] = call_user_func($elements['#access_callback'], $elements); } // Early-return nothing if user does not have access. if (isset($elements['#access'])) { // If #access is an AccessResultInterface object, we must apply it's // cacheability metadata to the render array. if ($elements['#access'] instanceof AccessResultInterface) { $this->addCacheableDependency($elements, $elements['#access']); if (!$elements['#access']->isAllowed()) { return ''; } } elseif ($elements['#access'] === FALSE) { return ''; } } // Do not print elements twice. if (!empty($elements['#printed'])) { return ''; } if (!isset(static::$context)) { throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."); } static::$context->push(new BubbleableMetadata()); // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely // has these configurable defaults, even when no subtree is render cached. // - this is a render cacheable subtree, to ensure that the cached data has // the configurable defaults (which may affect the ID and invalidation). if ($is_root_call || isset($elements['#cache']['keys'])) { $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; if (isset($elements['#cache']['contexts'])) { $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); } else { $elements['#cache']['contexts'] = $required_cache_contexts; } } // Try to fetch the prerendered element from cache, replace any placeholders // and return the final markup. if (isset($elements['#cache']['keys'])) { $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache // in case of nested elements with #cache set. if ($is_root_call) { $this->replacePlaceholders($elements); } // Mark the element markup as safe. If we have cached children, we need // to mark them as safe too. The parent markup contains the child // markup, so if the parent markup is safe, then the markup of the // individual children must be safe as well. $elements['#markup'] = SafeMarkup::set($elements['#markup']); if (!empty($elements['#cache_properties'])) { foreach (Element::children($cached_element) as $key) { SafeMarkup::set($cached_element[$key]['#markup']); } } // The render cache item contains all the bubbleable rendering metadata // for the subtree. static::$context->update($elements); // Render cache hit, so rendering is finished, all necessary info // collected! static::$context->bubble(); return $elements['#markup']; } } // Two-tier caching: track pre-bubbling elements' #cache for later // comparison. // @see \Drupal\Core\Render\RenderCacheInterface::get() // @see \Drupal\Core\Render\RenderCacheInterface::set() $pre_bubbling_elements = []; $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : []; // If the default values for this element have not been loaded yet, populate // them. if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { $elements += $this->elementInfo->getInfo($elements['#type']); } // First validate the usage of #lazy_builder; both of the next if-statements // use it if available. if (isset($elements['#lazy_builder'])) { // @todo Convert to assertions once https://www.drupal.org/node/2408013 // lands. if (!is_array($elements['#lazy_builder'])) { throw new \DomainException('The #lazy_builder property must have an array as a value.'); } if (count($elements['#lazy_builder']) !== 2) { throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.'); } if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function ($v) { return is_null($v) || is_scalar($v); }))) { throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL."); } $children = Element::children($elements); if ($children) { throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children))); } $supported_keys = ['#lazy_builder', '#cache', '#create_placeholder', '#weight', '#printed']; $unsupported_keys = array_diff(array_keys($elements), $supported_keys); if (count($unsupported_keys)) { throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys))); } } // If instructed to create a placeholder, and a #lazy_builder callback is // present (without such a callback, it would be impossible to replace the // placeholder), replace the current element with a placeholder. if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) { if (!isset($elements['#lazy_builder'])) { throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.'); } $elements = $this->createPlaceholder($elements); } // Build the element if it is still empty. if (isset($elements['#lazy_builder'])) { $callable = $elements['#lazy_builder'][0]; $args = $elements['#lazy_builder'][1]; if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $new_elements = call_user_func_array($callable, $args); // Retain the original cacheability metadata, plus cache keys. CacheableMetadata::createFromRenderArray($elements)->merge(CacheableMetadata::createFromRenderArray($new_elements))->applyTo($new_elements); if (isset($elements['#cache']['keys'])) { $new_elements['#cache']['keys'] = $elements['#cache']['keys']; } $elements = $new_elements; $elements['#lazy_builder_built'] = TRUE; } // Make any final changes to the element before it is rendered. This means // that the $element or the children can be altered or corrected before the // element is rendered into the final text. if (isset($elements['#pre_render'])) { foreach ($elements['#pre_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements = call_user_func($callable, $elements); } } // Defaults for bubbleable rendering metadata. $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); // Allow #pre_render to abort rendering. if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. static::$context->update($elements); // #printed, so rendering is finished, all necessary info collected! static::$context->bubble(); return ''; } // Add any JavaScript state information associated with the element. if (!empty($elements['#states'])) { drupal_process_states($elements); } // Get the children of the element, sorted by weight. $children = Element::children($elements, TRUE); // Initialize this element's #children, unless a #pre_render callback // already preset #children. if (!isset($elements['#children'])) { $elements['#children'] = ''; } if (isset($elements['#markup'])) { // @todo Decide how to support non-HTML in the render API in // https://www.drupal.org/node/2501313. $elements['#markup'] = SafeMarkup::checkAdminXss($elements['#markup']); } // Assume that if #theme is set it represents an implemented hook. $theme_is_implemented = isset($elements['#theme']); // Check the elements for insecure HTML and pass through sanitization. if (isset($elements)) { $markup_keys = array('#description', '#field_prefix', '#field_suffix'); foreach ($markup_keys as $key) { if (!empty($elements[$key]) && is_scalar($elements[$key])) { $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]); } } } // Call the element's #theme function if it is set. Then any children of the // element have to be rendered there. If the internal #render_children // property is set, do not call the #theme function to prevent infinite // recursion. if ($theme_is_implemented && !isset($elements['#render_children'])) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the // hook in #theme was not found in the registry and so we need to update // our flag accordingly. This is common for theme suggestions. $theme_is_implemented = $elements['#children'] !== FALSE; } // If #theme is not implemented or #render_children is set and the element // has an empty #children attribute, render the children now. This is the // same process as Renderer::render() but is inlined for speed. if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } $elements['#children'] = SafeMarkup::set($elements['#children']); } // If #theme is not implemented and the element has raw #markup as a // fallback, prepend the content in #markup to #children. In this case // #children will contain whatever is provided by #pre_render prepended to // what is rendered recursively above. If #theme is implemented then it is // the responsibility of that theme implementation to render #markup if // required. Eventually #theme_wrappers will expect both #markup and // #children to be a single string as #children. if (!$theme_is_implemented && isset($elements['#markup'])) { $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']); } // Let the theme functions in #theme_wrappers add markup around the rendered // children. // #states and #attached have to be processed before #theme_wrappers, // because the #type 'page' render array from drupal_prepare_page() would // render the $page and wrap it into the html.html.twig template without the // attached assets otherwise. // If the internal #render_children property is set, do not call the // #theme_wrappers function(s) to prevent infinite recursion. if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { foreach ($elements['#theme_wrappers'] as $key => $value) { // If the value of a #theme_wrappers item is an array then the theme // hook is found in the key of the item and the value contains attribute // overrides. Attribute overrides replace key/value pairs in $elements // for only this ThemeManagerInterface::render() call. This allows // #theme hooks and #theme_wrappers hooks to share variable names // without conflict or ambiguity. $wrapper_elements = $elements; if (is_string($key)) { $wrapper_hook = $key; foreach ($value as $attribute => $override) { $wrapper_elements[$attribute] = $override; } } else { $wrapper_hook = $value; } $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); } } // Filter the outputted content and make any last changes before the content // is sent to the browser. The changes are made on $content which allows the // outputted text to be filtered. if (isset($elements['#post_render'])) { foreach ($elements['#post_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); } } // We store the resulting output in $elements['#markup'], to be consistent // with how render cached output gets stored. This ensures that placeholder // replacement logic gets the same data to work with, no matter if #cache is // disabled, #cache is enabled, there is a cache hit or miss. $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : ''; $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : ''; $elements['#markup'] = $prefix . $elements['#children'] . $suffix; // We've rendered this element (and its subtree!), now update the context. static::$context->update($elements); // Cache the processed element if both $pre_bubbling_elements and $elements // have the metadata necessary to generate a cache ID. if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); } $this->renderCache->set($elements, $pre_bubbling_elements); } // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache in // case of nested elements with #cache set. // // By running them here, we ensure that: // - they run when #cache is disabled, // - they run when #cache is enabled and there is a cache miss. // Only the case of a cache hit when #cache is enabled, is not handled here, // that is handled earlier in Renderer::render(). if ($is_root_call) { $this->replacePlaceholders($elements); // @todo remove as part of https://www.drupal.org/node/2511330. if (static::$context->count() !== 1) { throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); } } // Rendering is finished, all necessary info collected! static::$context->bubble(); $elements['#printed'] = TRUE; $elements['#markup'] = SafeMarkup::set($elements['#markup']); return $elements['#markup']; }