Update to Drupal 8.0.0-beta15. For more information, see: https://www.drupal.org/node/2563023

This commit is contained in:
Pantheon Automation 2015-09-04 13:20:09 -07:00 committed by Greg Anderson
parent 2720a9ec4b
commit f3791f1da3
1898 changed files with 54300 additions and 11481 deletions

View file

@ -40,6 +40,8 @@ class Date extends FormElement {
'#process' => [[$class, 'processDate']],
'#pre_render' => [[$class, 'preRenderDate']],
'#theme_wrappers' => ['form_element'],
'#attributes' => ['type' => 'date'],
'#date_date_format' => 'Y-m-d',
];
}

View file

@ -7,7 +7,9 @@
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html as HtmlUtility;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Render\SafeString;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Template\Attribute;
@ -46,30 +48,17 @@ class HtmlTag extends RenderElement {
/**
* Pre-render callback: Renders a generic HTML tag with attributes into #markup.
*
* Note: It is the caller's responsibility to sanitize any input parameters.
* This callback does not perform sanitization. Despite the result of this
* pre-render callback being a #markup element, it is not passed through
* \Drupal\Component\Utility\Xss::filterAdmin(). This is because it is marked
* safe here, which causes
* \Drupal\Core\Render\Renderer::xssFilterAdminIfUnsafe() to regard it as safe
* and bypass the call to \Drupal\Component\Utility\Xss::filterAdmin().
*
* @param array $element
* An associative array containing:
* - #tag: The tag name to output. Typical tags added to the HTML HEAD:
* - meta: To provide meta information, such as a page refresh.
* - link: To refer to stylesheets and other contextual information.
* - script: To load JavaScript.
* The value of #tag is not escaped or sanitized, so do not pass in user
* input.
* The value of #tag is escaped.
* - #attributes: (optional) An array of HTML attributes to apply to the
* tag.
* tag. The attributes are escaped, see \Drupal\Core\Template\Attribute.
* - #value: (optional) A string containing tag content, such as inline
* CSS.
* - #value_prefix: (optional) A string to prepend to #value, e.g. a CDATA
* wrapper prefix.
* - #value_suffix: (optional) A string to append to #value, e.g. a CDATA
* wrapper suffix.
* CSS. The value of #value will be XSS admin filtered if it is not safe.
* - #noscript: (optional) If TRUE, the markup (including any prefix or
* suffix) will be wrapped in a <noscript> element. (Note that passing
* any non-empty value here will add the <noscript> tag.)
@ -79,35 +68,24 @@ class HtmlTag extends RenderElement {
public static function preRenderHtmlTag($element) {
$attributes = isset($element['#attributes']) ? new Attribute($element['#attributes']) : '';
// An HTML tag should not contain any special characters. Escape them to
// ensure this cannot be abused.
$escaped_tag = HtmlUtility::escape($element['#tag']);
$markup = '<' . $escaped_tag . $attributes;
// Construct a void element.
if (in_array($element['#tag'], self::$voidElements)) {
// This function is intended for internal use, so we assume that no unsafe
// values are passed in #tag. The attributes are already safe because
// Attribute output is already automatically sanitized.
// @todo Escape this properly instead? https://www.drupal.org/node/2296101
$markup = SafeMarkup::set('<' . $element['#tag'] . $attributes . " />\n");
$markup .= " />\n";
}
// Construct all other elements.
else {
$markup = '<' . $element['#tag'] . $attributes . '>';
if (isset($element['#value_prefix'])) {
$markup .= $element['#value_prefix'];
}
$markup .= $element['#value'];
if (isset($element['#value_suffix'])) {
$markup .= $element['#value_suffix'];
}
$markup .= '</' . $element['#tag'] . ">\n";
// @todo We cannot actually guarantee this markup is safe. Consider a fix
// in: https://www.drupal.org/node/2296101
$markup = SafeMarkup::set($markup);
$markup .= '>';
$markup .= SafeMarkup::isSafe($element['#value']) ? $element['#value'] : Xss::filterAdmin($element['#value']);
$markup .= '</' . $escaped_tag . ">\n";
}
if (!empty($element['#noscript'])) {
$element['#markup'] = SafeMarkup::format('<noscript>@markup</noscript>', ['@markup' => $markup]);
}
else {
$element['#markup'] = $markup;
$markup = "<noscript>$markup</noscript>";
}
$element['#markup'] = SafeString::create($markup);
return $element;
}
@ -174,20 +152,27 @@ class HtmlTag extends RenderElement {
// Ensure what we are dealing with is safe.
// This would be done later anyway in drupal_render().
$prefix = isset($elements['#prefix']) ? Xss::filterAdmin($elements['#prefix']) : '';
$suffix = isset($elements['#suffix']) ? Xss::filterAdmin($elements['#suffix']) : '';
$prefix = isset($element['#prefix']) ? $element['#prefix'] : '';
if ($prefix && !SafeMarkup::isSafe($prefix)) {
$prefix = Xss::filterAdmin($prefix);
}
$suffix = isset($element['#suffix']) ? $element['#suffix'] : '';
if ($suffix && !SafeMarkup::isSafe($suffix)) {
$suffix = Xss::filterAdmin($suffix);
}
// Now calling SafeMarkup::set is safe, because we ensured the
// data coming in was at least admin escaped.
// We ensured above that $expression is either a string we created or is
// admin XSS filtered, and that $prefix and $suffix are also admin XSS
// filtered if they are unsafe. Thus, all these strings are safe.
if (!$browsers['!IE']) {
// "downlevel-hidden".
$element['#prefix'] = SafeMarkup::set("\n<!--[if $expression]>\n" . $prefix);
$element['#suffix'] = SafeMarkup::set($suffix . "<![endif]-->\n");
$element['#prefix'] = SafeString::create("\n<!--[if $expression]>\n" . $prefix);
$element['#suffix'] = SafeString::create($suffix . "<![endif]-->\n");
}
else {
// "downlevel-revealed".
$element['#prefix'] = SafeMarkup::set("\n<!--[if $expression]><!-->\n" . $prefix);
$element['#suffix'] = SafeMarkup::set($suffix . "<!--<![endif]-->\n");
$element['#prefix'] = SafeString::create("\n<!--[if $expression]><!-->\n" . $prefix);
$element['#suffix'] = SafeString::create($suffix . "<!--<![endif]-->\n");
}
return $element;

View file

@ -104,6 +104,11 @@ class MachineName extends Textfield {
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE && $input !== NULL) {
// This should be a string, but allow other scalars since they might be
// valid input in programmatic form submissions.
return is_scalar($input) ? (string) $input : '';
}
return NULL;
}
@ -135,7 +140,7 @@ class MachineName extends Textfield {
);
// A form element that only wants to set one #machine_name property (usually
// 'source' only) would leave all other properties undefined, if the defaults
// were defined in hook_element_info(). Therefore, we apply the defaults here.
// were defined by an element plugin. Therefore, we apply the defaults here.
$element['#machine_name'] += array(
'source' => array('label'),
'target' => '#' . $element['#id'],

View file

@ -7,6 +7,7 @@
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
@ -68,4 +69,16 @@ class Password extends FormElement {
return $element;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE && $input !== NULL) {
// This should be a string, but allow other scalars since they might be
// valid input in programmatic form submissions.
return is_scalar($input) ? (string) $input : '';
}
return NULL;
}
}

View file

@ -50,9 +50,20 @@ class PasswordConfirm extends FormElement {
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
$element += array('#default_value' => array());
return $element['#default_value'] + array('pass1' => '', 'pass2' => '');
$element += ['#default_value' => []];
return $element['#default_value'] + ['pass1' => '', 'pass2' => ''];
}
$value = ['pass1' => '', 'pass2' => ''];
// Throw out all invalid array keys; we only allow pass1 and pass2.
foreach ($value as $allowed_key => $default) {
// These should be strings, but allow other scalars since they might be
// valid input in programmatic form submissions. Any nested array values
// are ignored.
if (isset($input[$allowed_key]) && is_scalar($input[$allowed_key])) {
$value[$allowed_key] = (string) $input[$allowed_key];
}
}
return $value;
}
/**

View file

@ -304,6 +304,7 @@ abstract class RenderElement extends PluginBase implements ElementInterface {
}
$element['#attached']['drupalSettings']['ajax'][$element['#id']] = $settings;
$element['#attached']['drupalSettings']['ajaxTrustedUrl'][$settings['url']] = TRUE;
// Indicate that Ajax processing was successful.
$element['#ajax_processed'] = TRUE;

View file

@ -7,6 +7,7 @@
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
@ -55,4 +56,15 @@ class Textarea extends FormElement {
);
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE && $input !== NULL) {
// This should be a string, but allow other scalars since they might be
// valid input in programmatic form submissions.
return is_scalar($input) ? (string) $input : '';
}
return NULL;
}
}

View file

@ -77,10 +77,14 @@ class Textfield extends FormElement {
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE && $input !== NULL) {
// Equate $input to the form value to ensure it's marked for
// validation.
// This should be a string, but allow other scalars since they might be
// valid input in programmatic form submissions.
if (!is_scalar($input)) {
$input = '';
}
return str_replace(array("\r", "\n"), '', $input);
}
return NULL;
}
/**

View file

@ -39,9 +39,12 @@ class Token extends Hidden {
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE) {
return (string) $input;
if ($input !== FALSE && $input !== NULL) {
// This should be a string, but allow other scalars since they might be
// valid input in programmatic form submissions.
return is_scalar($input) ? (string) $input : '';
}
return NULL;
}
}

View file

@ -25,7 +25,7 @@ use Drupal\Core\Render\Element;
* '#size' => 30,
* ...
* );
* @end_code
* @endcode
*
* @see \Drupal\Core\Render\Element\Textfield
*

View file

@ -71,6 +71,10 @@ class VerticalTabs extends RenderElement {
* The processed element.
*/
public static function processVerticalTabs(&$element, FormStateInterface $form_state, &$complete_form) {
if (isset($element['#access']) && !$element['#access']) {
return $element;
}
// Inject a new details as child, so that form_process_details() processes
// this details element like any other details.
$element['group'] = array(

View file

@ -110,10 +110,7 @@ class ElementInfoManager extends DefaultPluginManager implements ElementInfoMana
}
// Otherwise, rebuild and cache.
// @todo Remove this hook once all elements are converted to plugins in
// https://www.drupal.org/node/2311393.
$info = $this->moduleHandler->invokeAll('element_info');
$info = [];
foreach ($this->getDefinitions() as $element_type => $definition) {
$element = $this->createInstance($element_type);
$element_info = $element->getInfo();

View file

@ -44,8 +44,6 @@ interface ElementInfoManagerInterface {
* - #title_display: optional string indicating if and how #title should be
* displayed (see form-element.html.twig).
*
* @see hook_element_info()
* @see hook_element_info_alter()
* @see \Drupal\Core\Render\Element\ElementInterface
* @see \Drupal\Core\Render\Element\ElementInterface::getInfo()
*/
@ -55,7 +53,7 @@ interface ElementInfoManagerInterface {
* Retrieves a single property for the defined element type.
*
* @param string $type
* An element type as defined by hook_element_info().
* An element type as defined by an element plugin.
* @param string $property_name
* The property within the element type that should be returned.
* @param $default

View file

@ -36,12 +36,16 @@ class HtmlResponse extends Response implements CacheableResponseInterface, Attac
// A render array can automatically be converted to a string and set the
// necessary metadata.
if (is_array($content) && (isset($content['#markup']))) {
$content += ['#attached' => ['html_response_placeholders' => []]];
$content += ['#attached' => [
'html_response_attachment_placeholders' => [],
'placeholders' => []],
];
$this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
$this->setAttachments($content['#attached']);
$content = $content['#markup'];
}
parent::setContent($content);
return parent::setContent($content);
}
}

View file

@ -10,6 +10,7 @@ use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\EnforcedResponseException;
use Symfony\Component\HttpFoundation\RequestStack;
/**
@ -99,30 +100,96 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.');
}
// First, render the actual placeholders; this may cause additional
// attachments to be added to the response, which the attachment
// placeholders rendered by renderHtmlResponseAttachmentPlaceholders() will
// need to include.
//
// @todo Exceptions should not be used for code flow control. However, the
// Form API does not integrate with the HTTP Kernel based architecture of
// Drupal 8. In order to resolve this issue properly it is necessary to
// completely separate form submission from rendering.
// @see https://www.drupal.org/node/2367555
try {
$response = $this->renderPlaceholders($response);
}
catch (EnforcedResponseException $e) {
return $e->getResponse();
}
$attached = $response->getAttachments();
// Get the placeholders from attached and then remove them.
$placeholders = $attached['html_response_placeholders'];
unset($attached['html_response_placeholders']);
$attachment_placeholders = $attached['html_response_attachment_placeholders'];
unset($attached['html_response_attachment_placeholders']);
$variables = $this->processAssetLibraries($attached, $placeholders);
$variables = $this->processAssetLibraries($attached, $attachment_placeholders);
// Handle all non-asset attachments. This populates drupal_get_html_head()
// and drupal_get_http_header().
// Handle all non-asset attachments. This populates drupal_get_html_head().
$all_attached = ['#attached' => $attached];
drupal_process_attached($all_attached);
// Get HTML head elements - if present.
if (isset($placeholders['head'])) {
if (isset($attachment_placeholders['head'])) {
$variables['head'] = drupal_get_html_head(FALSE);
}
// Now replace the placeholders in the response content with the real data.
$this->renderPlaceholders($response, $placeholders, $variables);
// Now replace the attachment placeholders.
$this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables);
// Finally set the headers on the response.
$headers = drupal_get_http_header();
$this->setHeaders($response, $headers);
// Finally set the headers on the response if any bubbled.
if (!empty($attached['http_header'])) {
$this->setHeaders($response, $attached['http_header']);
}
return $response;
}
/**
* Renders placeholders (#attached['placeholders']).
*
* First, the HTML response object is converted to an equivalent render array,
* with #markup being set to the response's content and #attached being set to
* the response's attachments. Among these attachments, there may be
* placeholders that need to be rendered (replaced).
*
* Next, RendererInterface::renderRoot() is called, which renders the
* placeholders into their final markup.
*
* The markup that results from RendererInterface::renderRoot() is now the
* original HTML response's content, but with the placeholders rendered. We
* overwrite the existing content in the original HTML response object with
* this markup. The markup that was rendered for the placeholders may also
* have attachments (e.g. for CSS/JS assets) itself, and cacheability metadata
* that indicates what that markup depends on. That metadata is also added to
* the HTML response object.
*
* @param \Drupal\Core\Render\HtmlResponse $response
* The HTML response whose placeholders are being replaced.
*
* @return \Drupal\Core\Render\HtmlResponse
* The updated HTML response, with replaced placeholders.
*
* @see \Drupal\Core\Render\Renderer::replacePlaceholders()
* @see \Drupal\Core\Render\Renderer::renderPlaceholder()
*/
protected function renderPlaceholders(HtmlResponse $response) {
$build = [
'#markup' => SafeString::create($response->getContent()),
'#attached' => $response->getAttachments(),
];
// RendererInterface::renderRoot() renders the $build render array and
// updates it in place. We don't care about the return value (which is just
// $build['#markup']), but about the resulting render array.
// @todo Simplify this when https://www.drupal.org/node/2495001 lands.
$this->renderer->renderRoot($build);
// Update the Response object now that the placeholders have been rendered.
$placeholders_bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build);
$response
->setContent($build['#markup'])
->addCacheableDependency($placeholders_bubbleable_metadata)
->setAttachments($placeholders_bubbleable_metadata->getAttachments());
return $response;
}
@ -164,7 +231,7 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
// Print scripts - if any are present.
if (isset($placeholders['scripts']) || isset($placeholders['scripts_bottom'])) {
// Optimize JS if necessary, but only during normal site operation.
$optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess');
$optimize_js = !defined('MAINTENANCE_MODE') && !\Drupal::state()->get('system.maintenance_mode') && $this->config->get('js.preprocess');
list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js);
$variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header);
$variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer);
@ -174,8 +241,7 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
}
/**
* Renders variables into HTML markup and replaces placeholders in the
* response content.
* Renders HTML response attachment placeholders.
*
* @param \Drupal\Core\Render\HtmlResponse $response
* The HTML response to update.
@ -186,7 +252,7 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
* The variables to render and replace, keyed by type with renderable
* arrays as values.
*/
protected function renderPlaceholders(HtmlResponse $response, array $placeholders, array $variables) {
protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $response, array $placeholders, array $variables) {
$content = $response->getContent();
foreach ($placeholders as $type => $placeholder) {
if (isset($variables[$type])) {
@ -205,13 +271,17 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
* The headers to set.
*/
protected function setHeaders(HtmlResponse $response, array $headers) {
foreach ($headers as $name => $value) {
foreach ($headers as $values) {
$name = $values[0];
$value = $values[1];
$replace = !empty($values[2]);
// Drupal treats the HTTP response status code like a header, even though
// it really is not.
if ($name === 'status') {
if (strtolower($name) === 'status') {
$response->setStatusCode($value);
}
$response->headers->set($name, $value, FALSE);
$response->headers->set($name, $value, $replace);
}
}

View file

@ -64,7 +64,7 @@ class AjaxRenderer implements MainContentRendererInterface {
}
}
$html = (string) $this->drupalRenderRoot($main_content);
$html = $this->drupalRenderRoot($main_content);
$response->setAttachments($main_content['#attached']);
// The selector for the insert command is NULL as the new content will
@ -72,7 +72,7 @@ class AjaxRenderer implements MainContentRendererInterface {
// behavior can be changed with #ajax['method'].
$response->addCommand(new InsertCommand(NULL, $html));
$status_messages = array('#type' => 'status_messages');
$output = (string) $this->drupalRenderRoot($status_messages);
$output = $this->drupalRenderRoot($status_messages);
if (!empty($output)) {
$response->addCommand(new PrependCommand(NULL, $output));
}

View file

@ -8,9 +8,11 @@
namespace Drupal\Core\Render\MainContent;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
use Drupal\Core\Render\RenderCacheInterface;
@ -74,6 +76,15 @@ class HtmlRenderer implements MainContentRendererInterface {
*/
protected $renderCache;
/**
* The renderer configuration array.
*
* @see sites/default/default.services.yml
*
* @var array
*/
protected $rendererConfig;
/**
* Constructs a new HtmlRenderer.
*
@ -89,14 +100,17 @@ class HtmlRenderer implements MainContentRendererInterface {
* The renderer service.
* @param \Drupal\Core\Render\RenderCacheInterface $render_cache
* The render cache service.
* @param array $renderer_config
* The renderer configuration array.
*/
public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache) {
public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, array $renderer_config) {
$this->titleResolver = $title_resolver;
$this->displayVariantManager = $display_variant_manager;
$this->eventDispatcher = $event_dispatcher;
$this->moduleHandler = $module_handler;
$this->renderer = $renderer;
$this->renderCache = $render_cache;
$this->rendererConfig = $renderer_config;
}
/**
@ -125,11 +139,29 @@ class HtmlRenderer implements MainContentRendererInterface {
// page.html.twig, hence add them here, just before rendering html.html.twig.
$this->buildPageTopAndBottom($html);
// @todo https://www.drupal.org/node/2495001 Make renderRoot return a
// cacheable render array directly.
$this->renderer->renderRoot($html);
// Render, but don't replace placeholders yet, because that happens later in
// the render pipeline. To not replace placeholders yet, we use
// RendererInterface::render() instead of RendererInterface::renderRoot().
// @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
$render_context = new RenderContext();
$this->renderer->executeInRenderContext($render_context, function() use (&$html) {
// RendererInterface::render() renders the $html render array and updates
// it in place. We don't care about the return value (which is just
// $html['#markup']), but about the resulting render array.
// @todo Simplify this when https://www.drupal.org/node/2495001 lands.
$this->renderer->render($html);
});
// RendererInterface::render() always causes bubbleable metadata to be
// stored in the render context, no need to check it conditionally.
$bubbleable_metadata = $render_context->pop();
$bubbleable_metadata->applyTo($html);
$content = $this->renderCache->getCacheableRenderArray($html);
// Also associate the required cache contexts.
// (Because we use ::render() above and not ::renderRoot(), we manually must
// ensure the HTML response varies by the required cache contexts.)
$content['#cache']['contexts'] = Cache::mergeContexts($content['#cache']['contexts'], $this->rendererConfig['required_cache_contexts']);
// Also associate the "rendered" cache tag. This allows us to invalidate the
// entire render cache, regardless of the cache bin.
$content['#cache']['tags'][] = 'rendered';

View file

@ -7,6 +7,7 @@
namespace Drupal\Core\Render;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
@ -170,12 +171,15 @@ class Renderer implements RendererInterface {
// Get the render array for the given placeholder
$placeholder_elements = $elements['#attached']['placeholders'][$placeholder];
// Prevent the render array from being auto-placeholdered again.
$placeholder_elements['#create_placeholder'] = FALSE;
// Render the placeholder into markup.
$markup = $this->renderPlain($placeholder_elements);
// Replace the placeholder with its rendered markup, and merge its
// bubbleable metadata with the main elements'.
$elements['#markup'] = str_replace($placeholder, $markup, $elements['#markup']);
$elements['#markup'] = SafeString::create(str_replace($placeholder, $markup, $elements['#markup']));
$elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements);
// Remove the placeholder that we've just rendered.
@ -337,6 +341,10 @@ class Renderer implements RendererInterface {
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)));
}
}
// Determine whether to do auto-placeholdering.
if (isset($elements['#lazy_builder']) && (!isset($elements['#create_placeholder']) || $elements['#create_placeholder'] !== FALSE) && $this->shouldAutomaticallyPlaceholder($elements)) {
$elements['#create_placeholder'] = TRUE;
}
// 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.
@ -364,6 +372,12 @@ class Renderer implements RendererInterface {
$elements = $new_elements;
$elements['#lazy_builder_built'] = TRUE;
}
// All render elements support #markup and #plain_text.
if (!empty($elements['#markup']) || !empty($elements['#plain_text'])) {
$elements = $this->ensureMarkupIsSafe($elements);
}
// 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.
@ -405,12 +419,6 @@ class Renderer implements RendererInterface {
$elements['#children'] = '';
}
if (!empty($elements['#markup'])) {
// @todo Decide how to support non-HTML in the render API in
// https://www.drupal.org/node/2501313.
$elements['#markup'] = $this->xssFilterAdminIfUnsafe($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.
@ -511,7 +519,7 @@ class Renderer implements RendererInterface {
$prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
$suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
$elements['#markup'] = SafeString::create($prefix . $elements['#children'] . $suffix);
// We've rendered this element (and its subtree!), now update the context.
$context->update($elements);
@ -546,7 +554,7 @@ class Renderer implements RendererInterface {
$context->bubble();
$elements['#printed'] = TRUE;
return SafeString::create($elements['#markup']);
return $elements['#markup'];
}
/**
@ -635,6 +643,37 @@ class Renderer implements RendererInterface {
return TRUE;
}
/**
* Whether the given render array should be automatically placeholdered.
*
* @param array $element
* The render array whose cacheability to analyze.
*
* @return bool
* Whether the given render array's cacheability meets the placeholdering
* conditions.
*/
protected function shouldAutomaticallyPlaceholder(array $element) {
$conditions = $this->rendererConfig['auto_placeholder_conditions'];
// Auto-placeholder if max-age is at or below the configured threshold.
if (isset($element['#cache']['max-age']) && $element['#cache']['max-age'] !== Cache::PERMANENT && $element['#cache']['max-age'] <= $conditions['max-age']) {
return TRUE;
}
// Auto-placeholder if a high-cardinality cache context is set.
if (isset($element['#cache']['contexts']) && array_intersect($element['#cache']['contexts'], $conditions['contexts'])) {
return TRUE;
}
// Auto-placeholder if a high-invalidation frequency cache tag is set.
if (isset($element['#cache']['tags']) && array_intersect($element['#cache']['tags'], $conditions['tags'])) {
return TRUE;
}
return FALSE;
}
/**
* Turns this element into a placeholder.
*
@ -669,7 +708,7 @@ class Renderer implements RendererInterface {
$attributes = new Attribute();
$attributes['callback'] = $placeholder_render_array['#lazy_builder'][0];
$attributes['arguments'] = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
$attributes['token'] = hash('sha1', serialize($placeholder_render_array));
$attributes['token'] = hash('crc32b', serialize($placeholder_render_array));
$placeholder_markup = SafeMarkup::format('<drupal-render-placeholder@attributes></drupal-render-placeholder>', ['@attributes' => $attributes]);
// Build the placeholder element to return.
@ -718,4 +757,48 @@ class Renderer implements RendererInterface {
return SafeString::create($string);
}
/**
* Escapes #plain_text or filters #markup as required.
*
* Drupal uses Twig's auto-escape feature to improve security. This feature
* automatically escapes any HTML that is not known to be safe. Due to this
* the render system needs to ensure that all markup it generates is marked
* safe so that Twig does not do any additional escaping.
*
* By default all #markup is filtered to protect against XSS using the admin
* tag list. Render arrays can alter the list of tags allowed by the filter
* using the #allowed_tags property. This value should be an array of tags
* that Xss::filter() would accept. Render arrays can escape text instead
* of XSS filtering by setting the #plain_text property instead of #markup. If
* #plain_text is used #allowed_tags is ignored.
*
* @param array $elements
* A render array with #markup set.
*
* @return \Drupal\Component\Utility\SafeStringInterface|string
* The escaped markup wrapped in a SafeString object. If
* SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be
* escaped or filtered again.
*
* @see \Drupal\Component\Utility\Html::escape()
* @see \Drupal\Component\Utility\Xss::filter()
* @see \Drupal\Component\Utility\Xss::adminFilter()
*/
protected function ensureMarkupIsSafe(array $elements) {
if (empty($elements['#markup']) && empty($elements['#plain_text'])) {
return $elements;
}
if (!empty($elements['#plain_text'])) {
$elements['#markup'] = SafeString::create(Html::escape($elements['#plain_text']));
}
elseif (!SafeMarkup::isSafe($elements['#markup'])) {
// The default behaviour is to XSS filter using the admin tag list.
$tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList();
$elements['#markup'] = SafeString::create(Xss::filter($elements['#markup'], $tags));
}
return $elements;
}
}

View file

@ -148,7 +148,7 @@ interface RendererInterface {
* $pre_bubbling_cid.
* - If this element has #type defined and the default attributes for this
* element have not already been merged in (#defaults_loaded = TRUE) then
* the defaults for this type of element, defined in hook_element_info(),
* the defaults for this type of element, defined by an element plugin,
* are merged into the array. #defaults_loaded is set by functions that
* process render arrays and call the element info service before passing
* the array to Renderer::render(), such as form_builder() in the Form

View file

@ -8,7 +8,7 @@
namespace Drupal\Core\Render;
use Drupal\Component\Utility\SafeStringInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\SafeStringTrait;
/**
* Defines an object that passes safe strings through the render system.
@ -18,67 +18,13 @@ use Drupal\Component\Utility\Unicode;
* filtered first, it must not be used.
*
* @internal
* This object is marked as internal because it should only be used during
* rendering. Currently, there is no use case for this object by contrib or
* custom code.
* This object is marked as internal because it should only be used whilst
* rendering.
*
* @see \Drupal\Core\Template\TwigExtension::escapeFilter
* @see \Twig_Markup
* @see \Drupal\Component\Utility\SafeMarkup
*/
class SafeString implements SafeStringInterface, \Countable {
/**
* The safe string.
*
* @var string
*/
protected $string;
/**
* Creates a SafeString object if necessary.
*
* If $string is equal to a blank string then it is not necessary to create a
* SafeString object. If $string is an object that implements
* SafeStringInterface it is returned unchanged.
*
* @param mixed $string
* The string to mark as safe. This value will be cast to a string.
*
* @return string|\Drupal\Component\Utility\SafeStringInterface
* A safe string.
*/
public static function create($string) {
if ($string instanceof SafeStringInterface) {
return $string;
}
$string = (string) $string;
if ($string === '') {
return '';
}
$safe_string = new static();
$safe_string->string = $string;
return $safe_string;
}
/**
* Returns the string version of the SafeString object.
*
* @return string
* The safe string content.
*/
public function __toString() {
return $this->string;
}
/**
* Returns the string length.
*
* @return int
* The length of the string.
*/
public function count() {
return Unicode::strlen($this->string);
}
final class SafeString implements SafeStringInterface, \Countable {
use SafeStringTrait;
}

View file

@ -99,10 +99,12 @@
* before the template file is invoked to modify the variables that are passed
* to the template. These make up the "preprocessing" phase, and are executed
* (if they exist), in the following order (note that in the following list,
* HOOK indicates the theme hook name, MODULE indicates a module name, THEME
* indicates a theme name, and ENGINE indicates a theme engine name). Modules,
* themes, and theme engines can provide these functions to modify how the
* data is preprocessed, before it is passed to the theme template:
* HOOK indicates the hook being called or a less specific hook. For example, if
* '#theme' => 'node__article' is called, hook is node__article and node. MODULE
* indicates a module name, THEME indicates a theme name, and ENGINE indicates a
* theme engine name). Modules, themes, and theme engines can provide these
* functions to modify how the data is preprocessed, before it is passed to the
* theme template:
* - template_preprocess(&$variables, $hook): Creates a default set of variables
* for all theme hooks with template implementations. Provided by Drupal Core.
* - template_preprocess_HOOK(&$variables): Should be implemented by the module
@ -271,8 +273,29 @@
* vectors. (I.e, <script> and <style> are not allowed.) See
* \Drupal\Component\Utility\Xss::$adminTags for the list of tags that will
* be allowed. If your markup needs any of the tags that are not in this
* whitelist, then you should implement a theme hook and template file and/or
* an asset library.
* whitelist, then you can implement a theme hook and template file and/or
* an asset library. Aternatively, you can use the render array key
* #allowed_tags to alter which tags are filtered.
* - #plain_text: Specifies that the array provides text that needs to be
* escaped. This value takes precedence over #markup if present.
* - #allowed_tags: If #markup is supplied this can be used to change which tags
* are using to filter the markup. The value should be an array of tags that
* Xss::filter() would accept. If #plain_text is set this value is ignored.
*
* Usage example:
* @code
* $output['admin_filtered_string'] = array(
* '#markup' => '<em>This is filtered using the admin tag list</em>',
* );
* $output['filtered_string'] = array(
* '#markup' => '<em>This is filtered</em>',
* '#allowed_tags' => ['strong'],
* );
* $output['escaped_string'] = array(
* '#plain_text' => '<em>This is escaped</em>',
* );
* @endcode
*
* @see core.libraries.yml
* @see hook_theme()
*
@ -297,10 +320,7 @@
* on plugins, and look for classes with the RenderElement or FormElement
* annotation to discover what render elements are available.
*
* Modules can also currently define render elements by implementing
* hook_element_info(), although defining a plugin is preferred.
* properties. Look through implementations of hook_element_info() to discover
* elements defined this way.
* Modules can define render elements by defining an element plugin.
*
* @section sec_caching Caching
* The Drupal rendering process has the ability to cache rendered output at any
@ -693,32 +713,6 @@ function hook_render_template($template_file, $variables) {
return $twig_service->loadTemplate($template_file)->render($variables);
}
/**
* Allows modules to declare their own Form API element types and specify their
* default values.
*
* This hook allows modules to declare their own form element types and to
* specify their default values. The values returned by this hook will be
* merged with the elements returned by form constructor implementations and so
* can return defaults for any Form APIs keys in addition to those explicitly
* documented by \Drupal\Core\Render\ElementInfoManagerInterface::getInfo().
*
* @return array
* An associative array with structure identical to that of the return value
* of \Drupal\Core\Render\ElementInfoManagerInterface::getInfo().
*
* @deprecated Use an annotated class instead, see
* \Drupal\Core\Render\Element\ElementInterface.
*
* @see hook_element_info_alter()
*/
function hook_element_info() {
$types['filter_format'] = array(
'#input' => TRUE,
);
return $types;
}
/**
* Alter the element type information returned from modules.
*
@ -729,7 +723,8 @@ function hook_element_info() {
* An associative array with structure identical to that of the return value
* of \Drupal\Core\Render\ElementInfoManagerInterface::getInfo().
*
* @see hook_element_info()
* @see \Drupal\Core\Render\ElementInfoManager
* @see \Drupal\Core\Render\Element\ElementInterface
*/
function hook_element_info_alter(array &$types) {
// Decrease the default size of textfields.