Update to Drupal 8.0.0 beta 14. For more information, see https://drupal.org/node/2544542
This commit is contained in:
parent
3b2511d96d
commit
81ccda77eb
2155 changed files with 54307 additions and 46870 deletions
|
@ -73,6 +73,40 @@ class BubbleableMetadata extends CacheableMetadata implements AttachmentsInterfa
|
|||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bubbleable metadata object from a depended object.
|
||||
*
|
||||
* @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $object
|
||||
* The object whose cacheability metadata to retrieve. If it implements
|
||||
* CacheableDependencyInterface, its cacheability metadata will be used,
|
||||
* otherwise, the passed in object must be assumed to be uncacheable, so
|
||||
* max-age 0 is set.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function createFromObject($object) {
|
||||
$meta = parent::createFromObject($object);
|
||||
|
||||
if ($object instanceof AttachmentsInterface) {
|
||||
$meta->attachments = $object->getAttachments();
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addCacheableDependency($other_object) {
|
||||
parent::addCacheableDependency($other_object);
|
||||
|
||||
if ($other_object instanceof AttachmentsInterface) {
|
||||
$this->addAttachments($other_object->getAttachments());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two attachments arrays (which live under the '#attached' key).
|
||||
*
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
|
||||
/**
|
||||
* Provides helper methods for Drupal render elements.
|
||||
|
@ -136,11 +137,6 @@ class Element {
|
|||
foreach (static::children($elements) as $key) {
|
||||
$child = $elements[$key];
|
||||
|
||||
// Skip un-accessible children.
|
||||
if (isset($child['#access']) && !$child['#access']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip value and hidden elements, since they are not rendered.
|
||||
if (!static::isVisibleElement($child)) {
|
||||
continue;
|
||||
|
@ -162,7 +158,9 @@ class Element {
|
|||
* TRUE if the element is visible, otherwise FALSE.
|
||||
*/
|
||||
public static function isVisibleElement($element) {
|
||||
return (!isset($element['#type']) || !in_array($element['#type'], ['value', 'hidden', 'token'])) && (!isset($element['#access']) || $element['#access']);
|
||||
return (!isset($element['#type']) || !in_array($element['#type'], ['value', 'hidden', 'token']))
|
||||
&& (!isset($element['#access'])
|
||||
|| (($element['#access'] instanceof AccessResultInterface && $element['#access']->isAllowed()) || ($element['#access'] === TRUE)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,6 +14,18 @@ use Drupal\Component\Utility\Color as ColorUtility;
|
|||
/**
|
||||
* Provides a form element for choosing a color.
|
||||
*
|
||||
* Properties:
|
||||
* - #default_value: Default value, in a format like #ffffff.
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* $form['color'] = array(
|
||||
* '#type' => 'color',
|
||||
* '#title' => 'Color',
|
||||
* '#default_value' => '#ffffff',
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @FormElement("color")
|
||||
*/
|
||||
class Color extends FormElement {
|
||||
|
|
|
@ -7,15 +7,23 @@
|
|||
|
||||
namespace Drupal\Core\Render\Element;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
|
||||
/**
|
||||
* Provides a form element for date selection.
|
||||
*
|
||||
* The #default_value will be today's date if no value is supplied. The format
|
||||
* for the #default_value and the #return_value is an array with three elements
|
||||
* with the keys: 'year', month', and 'day'. For example,
|
||||
* array('year' => 2007, 'month' => 2, 'day' => 15)
|
||||
* Properties:
|
||||
* - #default_value: An array with the keys: 'year', 'month', and 'day'.
|
||||
* Defaults to the current date if no value is supplied.
|
||||
*
|
||||
* @code
|
||||
* $form['expiration'] = array(
|
||||
* '#type' => 'date',
|
||||
* '#title' => t('Content expiration'),
|
||||
* '#default_value' => array('year' => 2020, 'month' => 2, 'day' => 15,)
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @FormElement("date")
|
||||
*/
|
||||
|
@ -26,22 +34,47 @@ class Date extends FormElement {
|
|||
*/
|
||||
public function getInfo() {
|
||||
$class = get_class($this);
|
||||
return array(
|
||||
return [
|
||||
'#input' => TRUE,
|
||||
'#theme' => 'input__date',
|
||||
'#pre_render' => array(
|
||||
array($class, 'preRenderDate'),
|
||||
),
|
||||
'#theme_wrappers' => array('form_element'),
|
||||
);
|
||||
'#process' => [[$class, 'processDate']],
|
||||
'#pre_render' => [[$class, 'preRenderDate']],
|
||||
'#theme_wrappers' => ['form_element'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a date form element.
|
||||
*
|
||||
* @param array $element
|
||||
* The form element to process. Properties used:
|
||||
* - #attributes: An associative array containing:
|
||||
* - type: The type of date field rendered.
|
||||
* - #date_date_format: The date format used in PHP formats.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param array $complete_form
|
||||
* The complete form structure.
|
||||
*
|
||||
* @return array
|
||||
* The processed element.
|
||||
*/
|
||||
public static function processDate(&$element, FormStateInterface $form_state, &$complete_form) {
|
||||
// Attach JS support for the date field, if we can determine which date
|
||||
// format should be used.
|
||||
if ($element['#attributes']['type'] == 'date' && !empty($element['#date_date_format'])) {
|
||||
$element['#attached']['library'][] = 'core/drupal.date';
|
||||
$element['#attributes']['data-drupal-date-format'] = [$element['#date_date_format']];
|
||||
}
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds form-specific attributes to a 'date' #type element.
|
||||
*
|
||||
* Supports HTML5 types of 'date', 'datetime', 'datetime-local', and 'time'.
|
||||
* Falls back to a plain textfield. Used as a sub-element by the datetime
|
||||
* element type.
|
||||
* Falls back to a plain textfield with JS datepicker support. Used as a
|
||||
* sub-element by the datetime element type.
|
||||
*
|
||||
* @param array $element
|
||||
* An associative array containing the properties of the element.
|
||||
|
|
|
@ -13,6 +13,19 @@ use Drupal\Core\Render\Element;
|
|||
/**
|
||||
* Provides a form input element for entering an email address.
|
||||
*
|
||||
* Properties:
|
||||
* - #default_value: An RFC-compliant email address.
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* $form['email'] = array(
|
||||
* '#type' => 'email',
|
||||
* '#title' => t('Email'),
|
||||
* );
|
||||
* @end
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Render\Textfield
|
||||
*
|
||||
* @FormElement("email")
|
||||
*/
|
||||
class Email extends FormElement {
|
||||
|
|
|
@ -13,6 +13,13 @@ use Drupal\Core\Render\Element;
|
|||
/**
|
||||
* Provides a form element for uploading a file.
|
||||
*
|
||||
* If you add this element to a form the enctype="multipart/form-data" attribute
|
||||
* will automatically be added to the form element.
|
||||
*
|
||||
* Properties:
|
||||
* - #multiple: A Boolean indicating whether multiple files may be uploaded.
|
||||
* - #size: The size of the file input element in characters.
|
||||
*
|
||||
* @FormElement("file")
|
||||
*/
|
||||
class File extends FormElement {
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
namespace Drupal\Core\Render\Element;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Provides a base class for form element plugins.
|
||||
|
@ -111,18 +113,29 @@ abstract class FormElement extends RenderElement implements FormElementInterface
|
|||
* The form element.
|
||||
*/
|
||||
public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) {
|
||||
$url = NULL;
|
||||
$access = FALSE;
|
||||
|
||||
if (!empty($element['#autocomplete_route_name'])) {
|
||||
$parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array();
|
||||
|
||||
$path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters);
|
||||
$access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser());
|
||||
$url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)->toString(TRUE);
|
||||
/** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */
|
||||
$access_manager = \Drupal::service('access_manager');
|
||||
$access = $access_manager->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser(), TRUE);
|
||||
}
|
||||
|
||||
if ($access) {
|
||||
$element['#attributes']['class'][] = 'form-autocomplete';
|
||||
$element['#attached']['library'][] = 'core/drupal.autocomplete';
|
||||
// Provide a data attribute for the JavaScript behavior to bind to.
|
||||
$element['#attributes']['data-autocomplete-path'] = $path;
|
||||
$metadata = BubbleableMetadata::createFromRenderArray($element);
|
||||
if ($access->isAllowed()) {
|
||||
$element['#attributes']['class'][] = 'form-autocomplete';
|
||||
$metadata->addAttachments(['library' => ['core/drupal.autocomplete']]);
|
||||
// Provide a data attribute for the JavaScript behavior to bind to.
|
||||
$element['#attributes']['data-autocomplete-path'] = $url->getGeneratedUrl();
|
||||
$metadata = $metadata->merge($url);
|
||||
}
|
||||
$metadata
|
||||
->merge(BubbleableMetadata::createFromObject($access))
|
||||
->applyTo($element);
|
||||
}
|
||||
|
||||
return $element;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
namespace Drupal\Core\Render\Element;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\Xss;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
|
||||
/**
|
||||
|
@ -50,7 +51,7 @@ class HtmlTag extends RenderElement {
|
|||
* 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\Component\Utility\SafeMarkup::checkAdminXss() to regard it as safe
|
||||
* \Drupal\Core\Render\Renderer::xssFilterAdminIfUnsafe() to regard it as safe
|
||||
* and bypass the call to \Drupal\Component\Utility\Xss::filterAdmin().
|
||||
*
|
||||
* @param array $element
|
||||
|
@ -161,7 +162,7 @@ class HtmlTag extends RenderElement {
|
|||
}
|
||||
else {
|
||||
// The IE expression might contain some user input data.
|
||||
$expression = SafeMarkup::checkAdminXss($browsers['IE']);
|
||||
$expression = Xss::filterAdmin($browsers['IE']);
|
||||
}
|
||||
|
||||
// If the #prefix and #suffix properties are used, wrap them with
|
||||
|
@ -173,8 +174,8 @@ 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']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
|
||||
$suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';
|
||||
$prefix = isset($elements['#prefix']) ? Xss::filterAdmin($elements['#prefix']) : '';
|
||||
$suffix = isset($elements['#suffix']) ? Xss::filterAdmin($elements['#suffix']) : '';
|
||||
|
||||
// Now calling SafeMarkup::set is safe, because we ensured the
|
||||
// data coming in was at least admin escaped.
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace Drupal\Core\Render\Element;
|
|||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Component\Utility\Html as HtmlUtility;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Url as CoreUrl;
|
||||
|
||||
/**
|
||||
|
@ -83,7 +84,7 @@ class Link extends RenderElement {
|
|||
$link_generator = \Drupal::service('link_generator');
|
||||
$generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), TRUE);
|
||||
$element['#markup'] = $generated_link->getGeneratedLink();
|
||||
$generated_link->merge(CacheableMetadata::createFromRenderArray($element))
|
||||
$generated_link->merge(BubbleableMetadata::createFromRenderArray($element))
|
||||
->applyTo($element);
|
||||
}
|
||||
return $element;
|
||||
|
|
|
@ -15,9 +15,58 @@ use Drupal\Core\Language\LanguageInterface;
|
|||
* Provides a machine name render element.
|
||||
*
|
||||
* Provides a form element to enter a machine name, which is validated to ensure
|
||||
* that the name is unique and does not contain disallowed characters. All
|
||||
* disallowed characters are replaced with a replacement character via
|
||||
* JavaScript.
|
||||
* that the name is unique and does not contain disallowed characters.
|
||||
*
|
||||
* The element may be automatically populated via JavaScript when used in
|
||||
* conjunction with a separate "source" form element (typically specifying the
|
||||
* human-readable name). As the user types text into the source element, the
|
||||
* JavaScript converts all values to lower case, replaces any remaining
|
||||
* disallowed characters with a replacement, and populates the associated
|
||||
* machine name form element.
|
||||
*
|
||||
* Properties:
|
||||
* - #machine_name: An associative array containing:
|
||||
* - exists: A callable to invoke for checking whether a submitted machine
|
||||
* name value already exists. The submitted value is passed as an argument.
|
||||
* In most cases, an existing API or menu argument loader function can be
|
||||
* re-used. The callback is only invoked if the submitted value differs from
|
||||
* the element's #default_value.
|
||||
* - source: (optional) The #array_parents of the form element containing the
|
||||
* human-readable name (i.e., as contained in the $form structure) to use as
|
||||
* source for the machine name. Defaults to array('label').
|
||||
* - label: (optional) Text to display as label for the machine name value
|
||||
* after the human-readable name form element. Defaults to t('Machine name').
|
||||
* - replace_pattern: (optional) A regular expression (without delimiters)
|
||||
* matching disallowed characters in the machine name. Defaults to
|
||||
* '[^a-z0-9_]+'.
|
||||
* - replace: (optional) A character to replace disallowed characters in the
|
||||
* machine name via JavaScript. Defaults to '_' (underscore). When using a
|
||||
* different character, 'replace_pattern' needs to be set accordingly.
|
||||
* - error: (optional) A custom form error message string to show, if the
|
||||
* machine name contains disallowed characters.
|
||||
* - standalone: (optional) Whether the live preview should stay in its own
|
||||
* form element rather than in the suffix of the source element. Defaults
|
||||
* to FALSE.
|
||||
* - #maxlength: (optional) Maximum allowed length of the machine name. Defaults
|
||||
* to 64.
|
||||
* - #disabled: (optional) Should be set to TRUE if an existing machine name
|
||||
* must not be changed after initial creation.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['id'] = array(
|
||||
* '#type' => 'machine_name',
|
||||
* '#default_value' => $this->entity->id(),
|
||||
* '#disabled' => !$this->entity->isNew(),
|
||||
* '#maxlength' => 64,
|
||||
* '#description' => $this->t('A unique name for this item. It must only contain lowercase letters, numbers, and underscores.'),
|
||||
* '#machine_name' => array(
|
||||
* 'exists' => array($this, 'exists'),
|
||||
* ),
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Textfield
|
||||
*
|
||||
* @FormElement("machine_name")
|
||||
*/
|
||||
|
@ -62,35 +111,7 @@ class MachineName extends Textfield {
|
|||
* Processes a machine-readable name form element.
|
||||
*
|
||||
* @param array $element
|
||||
* The form element to process. Properties used:
|
||||
* - #machine_name: An associative array containing:
|
||||
* - exists: A callable to invoke for checking whether a submitted machine
|
||||
* name value already exists. The submitted value is passed as an
|
||||
* argument. In most cases, an existing API or menu argument loader
|
||||
* function can be re-used. The callback is only invoked if the
|
||||
* submitted value differs from the element's #default_value.
|
||||
* - source: (optional) The #array_parents of the form element containing
|
||||
* the human-readable name (i.e., as contained in the $form structure)
|
||||
* to use as source for the machine name. Defaults to array('label').
|
||||
* - label: (optional) Text to display as label for the machine name value
|
||||
* after the human-readable name form element. Defaults to "Machine
|
||||
* name".
|
||||
* - replace_pattern: (optional) A regular expression (without delimiters)
|
||||
* matching disallowed characters in the machine name. Defaults to
|
||||
* '[^a-z0-9_]+'.
|
||||
* - replace: (optional) A character to replace disallowed characters in
|
||||
* the machine name via JavaScript. Defaults to '_' (underscore). When
|
||||
* using a different character, 'replace_pattern' needs to be set
|
||||
* accordingly.
|
||||
* - error: (optional) A custom form error message string to show, if the
|
||||
* machine name contains disallowed characters.
|
||||
* - standalone: (optional) Whether the live preview should stay in its
|
||||
* own form element rather than in the suffix of the source
|
||||
* element. Defaults to FALSE.
|
||||
* - #maxlength: (optional) Maximum allowed length of the machine name.
|
||||
* Defaults to 64.
|
||||
* - #disabled: (optional) Should be set to TRUE if an existing machine
|
||||
* name must not be changed after initial creation.
|
||||
* The form element to process. See main class documentation for properties.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param array $complete_form
|
||||
|
|
|
@ -14,6 +14,25 @@ use Drupal\Component\Utility\Number as NumberUtility;
|
|||
/**
|
||||
* Provides a form element for numeric input, with special numeric validation.
|
||||
*
|
||||
* Properties:
|
||||
* - #default_value: A valid floating point number.
|
||||
* - #min: Minimum value.
|
||||
* - #max: Maximum value.
|
||||
* - #step: Ensures that the number is an even multiple of step, offset by #min
|
||||
* if specified. A #min of 1 and a #step of 2 would allow values of 1, 3, 5,
|
||||
* etc.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['quantity'] = array(
|
||||
* '#type' => 'number',
|
||||
* '#title' => t('Quantity'),
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Range
|
||||
* @see \Drupal\Core\Render\Element\Textfield
|
||||
*
|
||||
* @FormElement("number")
|
||||
*/
|
||||
class Number extends FormElement {
|
||||
|
|
|
@ -34,18 +34,29 @@ class Pager extends RenderElement{
|
|||
'#quantity' => 9,
|
||||
// An array of labels for the controls in the pager.
|
||||
'#tags' => [],
|
||||
// The name of the route to be used to build pager links. By default no
|
||||
// path is provided, which will make links relative to the current URL.
|
||||
// This makes the page more effectively cacheable.
|
||||
'#route_name' => '<none>',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* #pre_render callback to associate the appropriate cache context.
|
||||
*
|
||||
*
|
||||
* @param array $pager
|
||||
* A renderable array of #type => pager.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function preRenderPager(array $pager) {
|
||||
// Note: the default pager theme process function
|
||||
// template_preprocess_pager() also calls pager_query_add_page(), which
|
||||
// maintains the existing query string. Therefore
|
||||
// template_preprocess_pager() adds the 'url.query_args' cache context,
|
||||
// which causes the more specific cache context below to be optimized away.
|
||||
// In other themes, however, that may not be the case.
|
||||
$pager['#cache']['contexts'][] = 'url.query_args.pagers:' . $pager['#element'];
|
||||
return $pager;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,18 @@ use Drupal\Core\Render\Element;
|
|||
/**
|
||||
* Provides a form element for entering a password, with hidden text.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['pass'] = array(
|
||||
* '#type' => 'password',
|
||||
* '#title => t('Password'),
|
||||
* '#size' => 25,
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\PasswordConfirm
|
||||
* @see \Drupal\Core\Render\Element\Textfield
|
||||
*
|
||||
* @FormElement("password")
|
||||
*/
|
||||
class Password extends FormElement {
|
||||
|
|
|
@ -15,6 +15,17 @@ use Drupal\Core\Form\FormStateInterface;
|
|||
* Formats as a pair of password fields, which do not validate unless the two
|
||||
* entered passwords match.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['pass'] = array(
|
||||
* '#type' => 'password_confirm',
|
||||
* '#title' => t('Password'),
|
||||
* '#size' => 25,
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Password
|
||||
*
|
||||
* @FormElement("password_confirm")
|
||||
*/
|
||||
class PasswordConfirm extends FormElement {
|
||||
|
|
|
@ -11,7 +11,24 @@ use Drupal\Core\Form\FormStateInterface;
|
|||
use Drupal\Core\Render\Element;
|
||||
|
||||
/**
|
||||
* Provides a form element for input of a number within a specific range.
|
||||
* Provides a slider for input of a number within a specific range.
|
||||
*
|
||||
* Provides an HTML5 input element with type of "range".
|
||||
*
|
||||
* Properties:
|
||||
* - #min: Minimum value (defaults to 0).
|
||||
* - #max: Maximum value (defaults to 100).
|
||||
* Refer to \Drupal\Core\Render\Element\Number for additional properties.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['quantity'] = array(
|
||||
* '#type' => 'number',
|
||||
* '#title' => t('Quantity'),
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Number
|
||||
*
|
||||
* @FormElement("range")
|
||||
*/
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace Drupal\Core\Render\Element;
|
|||
use Drupal\Core\Form\FormBuilderInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
|
@ -128,14 +129,7 @@ abstract class RenderElement extends PluginBase implements ElementInterface {
|
|||
* @see self::preRenderAjaxForm()
|
||||
*/
|
||||
public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
|
||||
$element = static::preRenderAjaxForm($element);
|
||||
|
||||
// If the element was processed as an #ajax element, and a custom URL was
|
||||
// provided, set the form to be cached.
|
||||
if (!empty($element['#ajax_processed']) && !empty($element['#ajax']['url'])) {
|
||||
$form_state->setCached();
|
||||
}
|
||||
return $element;
|
||||
return static::preRenderAjaxForm($element);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -238,10 +232,6 @@ abstract class RenderElement extends PluginBase implements ElementInterface {
|
|||
// content negotiation takes care of formatting the response appropriately.
|
||||
// However, 'url' and 'options' may be set when wanting server processing
|
||||
// to be substantially different for a JavaScript triggered submission.
|
||||
// One such substantial difference is form elements that use
|
||||
// #ajax['callback'] for determining which part of the form needs
|
||||
// re-rendering. For that, we have a special 'system.ajax' route which
|
||||
// must be manually set.
|
||||
$settings += [
|
||||
'url' => NULL,
|
||||
'options' => ['query' => []],
|
||||
|
@ -264,7 +254,11 @@ abstract class RenderElement extends PluginBase implements ElementInterface {
|
|||
|
||||
// Convert \Drupal\Core\Url object to string.
|
||||
if (isset($settings['url']) && $settings['url'] instanceof Url) {
|
||||
$settings['url'] = $settings['url']->setOptions($settings['options'])->toString();
|
||||
$url = $settings['url']->setOptions($settings['options'])->toString(TRUE);
|
||||
BubbleableMetadata::createFromRenderArray($element)
|
||||
->merge($url)
|
||||
->applyTo($element);
|
||||
$settings['url'] = $url->getGeneratedUrl();
|
||||
}
|
||||
else {
|
||||
$settings['url'] = NULL;
|
||||
|
|
|
@ -10,11 +10,17 @@ namespace Drupal\Core\Render\Element;
|
|||
use Drupal\Core\Render\Element;
|
||||
|
||||
/**
|
||||
* Provides a form input element for searching.
|
||||
* Provides an HTML5 input element with type of "search".
|
||||
*
|
||||
* This is commonly used to provide a filter or search box at the top of a
|
||||
* long listing page, to allow users to find specific items in the list for
|
||||
* faster input.
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['search'] = array(
|
||||
* '#type' => 'search',
|
||||
* '#title' => t('Search'),
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Textfield
|
||||
*
|
||||
* @FormElement("search")
|
||||
*/
|
||||
|
|
|
@ -12,6 +12,19 @@ use Drupal\Core\Render\Element;
|
|||
/**
|
||||
* Provides a form element for entering a telephone number.
|
||||
*
|
||||
* Provides an HTML5 input element with type of "tel". It provides no special
|
||||
* validation.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['phone'] = array(
|
||||
* '#type' => 'tel',
|
||||
* '#title' => t('Phone'),
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element
|
||||
*
|
||||
* @FormElement("tel")
|
||||
*/
|
||||
class Tel extends FormElement {
|
||||
|
|
|
@ -12,6 +12,23 @@ use Drupal\Core\Render\Element;
|
|||
/**
|
||||
* Provides a form element for input of multiple-line text.
|
||||
*
|
||||
* Properties:
|
||||
* - #rows: Number of rows in the text box.
|
||||
* - #cols: Number of columns in the text box.
|
||||
* - #resizable: Controls whether the text area is resizable. Allowed values
|
||||
* are "none", "vertical", "horizontal", or "both" (defaults to "vertical").
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['text'] = array(
|
||||
* '#type' => 'textarea',
|
||||
* '#title' => t('Text'),
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Textfield
|
||||
* @see \Drupal\filter\Element\TextFormat
|
||||
*
|
||||
* @FormElement("textarea")
|
||||
*/
|
||||
class Textarea extends FormElement {
|
||||
|
|
|
@ -13,6 +13,36 @@ use Drupal\Core\Render\Element;
|
|||
/**
|
||||
* Provides a one-line text field form element.
|
||||
*
|
||||
* Properties:
|
||||
* - #maxlength: Maximum number of characters of input allowed.
|
||||
* - #size: The size of the input element in characters.
|
||||
* - #autocomplete_route_name: A route to be used as callback URL by the
|
||||
* autocomplete JavaScript library.
|
||||
* - #autocomplete_route_parameters: An array of parameters to be used in
|
||||
* conjunction with the route name.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['title'] = array(
|
||||
* '#type' => 'textfield',
|
||||
* '#title' => t('Subject'),
|
||||
* '#default_value' => $node->title,
|
||||
* '#size' => 60,
|
||||
* '#maxlength' => 128,
|
||||
* '#required' => TRUE,
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Color
|
||||
* @see \Drupal\Core\Render\Element\Email
|
||||
* @see \Drupal\Core\Render\Element\MachineName
|
||||
* @see \Drupal\Core\Render\Element\Number
|
||||
* @see \Drupal\Core\Render\Element\Password
|
||||
* @see \Drupal\Core\Render\Element\PasswordConfirm
|
||||
* @see \Drupal\Core\Render\Element\Range
|
||||
* @see \Drupal\Core\Render\Element\Tel
|
||||
* @see \Drupal\Core\Render\Element\Url
|
||||
*
|
||||
* @FormElement("textfield")
|
||||
*/
|
||||
class Textfield extends FormElement {
|
||||
|
|
|
@ -14,6 +14,21 @@ use Drupal\Core\Render\Element;
|
|||
/**
|
||||
* Provides a form element for input of a URL.
|
||||
*
|
||||
* Properties:
|
||||
* - #default_value: A valid URL string.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* $form['homepage'] = array(
|
||||
* '#type' => 'url',
|
||||
* '#title' => t('Home Page'),
|
||||
* '#size' => 30,
|
||||
* ...
|
||||
* );
|
||||
* @end_code
|
||||
*
|
||||
* @see \Drupal\Core\Render\Element\Textfield
|
||||
*
|
||||
* @FormElement("url")
|
||||
*/
|
||||
class Url extends FormElement {
|
||||
|
|
|
@ -36,6 +36,7 @@ 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' => []]];
|
||||
$this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
|
||||
$this->setAttachments($content['#attached']);
|
||||
$content = $content['#markup'];
|
||||
|
|
|
@ -64,7 +64,7 @@ class AjaxRenderer implements MainContentRendererInterface {
|
|||
}
|
||||
}
|
||||
|
||||
$html = $this->drupalRenderRoot($main_content);
|
||||
$html = (string) $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 = $this->drupalRenderRoot($status_messages);
|
||||
$output = (string) $this->drupalRenderRoot($status_messages);
|
||||
if (!empty($output)) {
|
||||
$response->addCommand(new PrependCommand(NULL, $output));
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface;
|
|||
use Drupal\Core\Render\HtmlResponse;
|
||||
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
|
||||
use Drupal\Core\Render\RenderCacheInterface;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Render\RenderEvents;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
|
@ -181,7 +182,15 @@ class HtmlRenderer implements MainContentRendererInterface {
|
|||
// ::renderResponse().
|
||||
// @todo Remove this once https://www.drupal.org/node/2359901 lands.
|
||||
if (!empty($main_content)) {
|
||||
$this->renderer->render($main_content, FALSE);
|
||||
$this->renderer->executeInRenderContext(new RenderContext(), function() use (&$main_content) {
|
||||
if (isset($main_content['#cache']['keys'])) {
|
||||
// Retain #title, otherwise, dynamically generated titles would be
|
||||
// missing for controllers whose entire returned render array is
|
||||
// render cached.
|
||||
$main_content['#cache_properties'][] = '#title';
|
||||
}
|
||||
return $this->renderer->render($main_content, FALSE);
|
||||
});
|
||||
$main_content = $this->renderCache->getCacheableRenderArray($main_content) + [
|
||||
'#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL
|
||||
];
|
||||
|
@ -192,7 +201,9 @@ class HtmlRenderer implements MainContentRendererInterface {
|
|||
if (!$page_display instanceof PageVariantInterface) {
|
||||
throw new \LogicException('Cannot render the main content for this page because the provided display variant does not implement PageVariantInterface.');
|
||||
}
|
||||
$page_display->setMainContent($main_content);
|
||||
$page_display
|
||||
->setMainContent($main_content)
|
||||
->setConfiguration($event->getPluginConfiguration());
|
||||
|
||||
// Generate a #type => page render array using the page display variant,
|
||||
// the page display will build the content for the various page regions.
|
||||
|
|
142
core/lib/Drupal/Core/Render/MetadataBubblingUrlGenerator.php
Normal file
142
core/lib/Drupal/Core/Render/MetadataBubblingUrlGenerator.php
Normal file
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Render\MetadataBubblingUrlGenerator.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Core\GeneratedUrl;
|
||||
use Drupal\Core\Routing\UrlGeneratorInterface;
|
||||
use Symfony\Component\Routing\RequestContext as SymfonyRequestContext;
|
||||
|
||||
/**
|
||||
* Decorator for the URL generator, which bubbles bubbleable URL metadata.
|
||||
*
|
||||
* Implements a decorator for the URL generator that allows to automatically
|
||||
* collect and bubble up bubbleable metadata associated with URLs due to
|
||||
* outbound path and route processing. This approach helps keeping the render
|
||||
* and the routing subsystems decoupled.
|
||||
*
|
||||
* @see \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface
|
||||
* @see \Drupal\Core\PathProcessor\OutboundPathProcessorInterface
|
||||
* @see \Drupal\Core\Render\BubbleableMetadata
|
||||
*/
|
||||
class MetadataBubblingUrlGenerator implements UrlGeneratorInterface {
|
||||
|
||||
/**
|
||||
* The non-bubbling URL generator.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\UrlGeneratorInterface
|
||||
*/
|
||||
protected $urlGenerator;
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs a new bubbling URL generator service.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
|
||||
* The non-bubbling URL generator.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer.
|
||||
*/
|
||||
public function __construct(UrlGeneratorInterface $url_generator, RendererInterface $renderer) {
|
||||
$this->urlGenerator = $url_generator;
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setContext(SymfonyRequestContext $context) {
|
||||
$this->urlGenerator->setContext($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContext() {
|
||||
return $this->urlGenerator->getContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPathFromRoute($name, $parameters = array()) {
|
||||
return $this->urlGenerator->getPathFromRoute($name, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bubbles the bubbleable metadata to the current render context.
|
||||
*
|
||||
* @param \Drupal\Core\GeneratedUrl $generated_url
|
||||
* The generated URL whose bubbleable metadata to bubble.
|
||||
* @param array $options
|
||||
* (optional) The URL options. Defaults to none.
|
||||
*/
|
||||
protected function bubble(GeneratedUrl $generated_url, array $options = []) {
|
||||
// Bubbling metadata makes sense only if the code is executed inside a
|
||||
// render context. All code running outside controllers has no render
|
||||
// context by default, so URLs used there are not supposed to affect the
|
||||
// response cacheability.
|
||||
if ($this->renderer->hasRenderContext()) {
|
||||
$build = [];
|
||||
$generated_url->applyTo($build);
|
||||
$this->renderer->render($build);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generate($name, $parameters = array(), $absolute = FALSE) {
|
||||
$options['absolute'] = $absolute;
|
||||
$generated_url = $this->generateFromRoute($name, $parameters, $options, TRUE);
|
||||
$this->bubble($generated_url);
|
||||
return $generated_url->getGeneratedUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE) {
|
||||
$generated_url = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE);
|
||||
if (!$collect_bubbleable_metadata) {
|
||||
$this->bubble($generated_url, $options);
|
||||
}
|
||||
return $collect_bubbleable_metadata ? $generated_url : $generated_url->getGeneratedUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generateFromPath($path = NULL, $options = array(), $collect_bubbleable_metadata = FALSE) {
|
||||
$generated_url = $this->urlGenerator->generateFromPath($path, $options, TRUE);
|
||||
if (!$collect_bubbleable_metadata) {
|
||||
$this->bubble($generated_url, $options);
|
||||
}
|
||||
return $collect_bubbleable_metadata ? $generated_url : $generated_url->getGeneratedUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports($name) {
|
||||
return $this->urlGenerator->supports($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRouteDebugMessage($name, array $parameters = array()) {
|
||||
return $this->urlGenerator->getRouteDebugMessage($name, $parameters);
|
||||
}
|
||||
|
||||
}
|
|
@ -25,6 +25,13 @@ class PageDisplayVariantSelectionEvent extends Event {
|
|||
*/
|
||||
protected $pluginId;
|
||||
|
||||
/**
|
||||
* The configuration for the selected page display variant.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $pluginConfiguration = [];
|
||||
|
||||
/**
|
||||
* The current route match.
|
||||
*
|
||||
|
@ -50,9 +57,12 @@ class PageDisplayVariantSelectionEvent extends Event {
|
|||
*
|
||||
* @param string $plugin_id
|
||||
* The ID of the page display variant plugin to use.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPluginId($plugin_id) {
|
||||
$this->pluginId = $plugin_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,6 +74,28 @@ class PageDisplayVariantSelectionEvent extends Event {
|
|||
return $this->pluginId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the configuration for the selected page display variant.
|
||||
*
|
||||
* @param array $configuration
|
||||
* The configuration for the selected page display variant.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPluginConfiguration(array $configuration) {
|
||||
$this->pluginConfiguration = $configuration;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration for the selected page display variant.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPluginConfiguration() {
|
||||
return $this->pluginConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current route match.
|
||||
*
|
||||
|
|
|
@ -32,6 +32,7 @@ class SimplePageVariant extends VariantBase implements PageVariantInterface {
|
|||
*/
|
||||
public function setMainContent(array $main_content) {
|
||||
$this->mainContent = $main_content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Cache\Context\CacheContextsManager;
|
||||
use Drupal\Core\Cache\CacheFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
@ -96,7 +98,6 @@ class RenderCache implements RenderCacheInterface {
|
|||
$data = $this->getCacheableRenderArray($elements);
|
||||
|
||||
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
|
||||
$expire = ($elements['#cache']['max-age'] === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $elements['#cache']['max-age'];
|
||||
$cache = $this->cacheFactory->get($bin);
|
||||
|
||||
// Calculate the pre-bubbling CID.
|
||||
|
@ -207,26 +208,28 @@ class RenderCache implements RenderCacheInterface {
|
|||
// across requests. That's the strategy employed below and tested in
|
||||
// \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing().
|
||||
|
||||
// The set of cache contexts for this element, including the bubbled ones,
|
||||
// for which we are handling a cache miss.
|
||||
$cache_contexts = $data['#cache']['contexts'];
|
||||
|
||||
// Get the contexts by which this element should be varied according to
|
||||
// the current redirecting cache item, if any.
|
||||
$stored_cache_contexts = [];
|
||||
$stored_cache_tags = [];
|
||||
// Get the cacheability of this element according to the current (stored)
|
||||
// redirecting cache item, if any.
|
||||
$redirect_cacheability = new CacheableMetadata();
|
||||
if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) {
|
||||
$stored_cache_contexts = $stored_cache_redirect->data['#cache']['contexts'];
|
||||
$stored_cache_tags = $stored_cache_redirect->data['#cache']['tags'];
|
||||
$redirect_cacheability = CacheableMetadata::createFromRenderArray($stored_cache_redirect->data);
|
||||
}
|
||||
|
||||
// Calculate the union of the cache contexts for this request and the
|
||||
// stored cache contexts.
|
||||
$merged_cache_contexts = Cache::mergeContexts($stored_cache_contexts, $cache_contexts);
|
||||
// Calculate the union of the cacheability for this request and the
|
||||
// current (stored) redirecting cache item. We need:
|
||||
// - the union of cache contexts, because that is how we know which cache
|
||||
// item to redirect to;
|
||||
// - the union of cache tags, because that is how we know when the cache
|
||||
// redirect cache item itself is invalidated;
|
||||
// - the union of max ages, because that is how we know when the cache
|
||||
// redirect cache item itself becomes stale. (Without this, we might end
|
||||
// up toggling between a permanently and a briefly cacheable cache
|
||||
// redirect, because the last update's max-age would always "win".)
|
||||
$redirect_cacheability_updated = CacheableMetadata::createFromRenderArray($data)->merge($redirect_cacheability);
|
||||
|
||||
// Stored cache contexts incomplete: this request causes cache contexts to
|
||||
// be added to the redirecting cache item.
|
||||
if (array_diff($merged_cache_contexts, $stored_cache_contexts)) {
|
||||
if (array_diff($redirect_cacheability_updated->getCacheContexts(), $redirect_cacheability->getCacheContexts())) {
|
||||
$redirect_data = [
|
||||
'#cache_redirect' => TRUE,
|
||||
'#cache' => [
|
||||
|
@ -234,14 +237,16 @@ class RenderCache implements RenderCacheInterface {
|
|||
// across requests.
|
||||
'keys' => $elements['#cache']['keys'],
|
||||
// The union of the current element's and stored cache contexts.
|
||||
'contexts' => $merged_cache_contexts,
|
||||
'contexts' => $redirect_cacheability_updated->getCacheContexts(),
|
||||
// The union of the current element's and stored cache tags.
|
||||
'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']),
|
||||
'tags' => $redirect_cacheability_updated->getCacheTags(),
|
||||
// The union of the current element's and stored cache max-ages.
|
||||
'max-age' => $redirect_cacheability_updated->getCacheMaxAge(),
|
||||
// The same cache bin as the one for the actual render cache items.
|
||||
'bin' => $bin,
|
||||
],
|
||||
];
|
||||
$cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
|
||||
$cache->set($pre_bubbling_cid, $redirect_data, $this->maxAgeToExpire($redirect_cacheability_updated->getCacheMaxAge()), Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
|
||||
}
|
||||
|
||||
// Current cache contexts incomplete: this request only uses a subset of
|
||||
|
@ -249,20 +254,35 @@ class RenderCache implements RenderCacheInterface {
|
|||
// additional (conditional) cache contexts as well, otherwise the
|
||||
// redirecting cache item would be pointing to a cache item that can never
|
||||
// exist.
|
||||
if (array_diff($merged_cache_contexts, $cache_contexts)) {
|
||||
if (array_diff($redirect_cacheability_updated->getCacheContexts(), $data['#cache']['contexts'])) {
|
||||
// Recalculate the cache ID.
|
||||
$recalculated_cid_pseudo_element = [
|
||||
'#cache' => [
|
||||
'keys' => $elements['#cache']['keys'],
|
||||
'contexts' => $merged_cache_contexts,
|
||||
'contexts' => $redirect_cacheability_updated->getCacheContexts(),
|
||||
]
|
||||
];
|
||||
$cid = $this->createCacheID($recalculated_cid_pseudo_element);
|
||||
// Ensure the about-to-be-cached data uses the merged cache contexts.
|
||||
$data['#cache']['contexts'] = $merged_cache_contexts;
|
||||
$data['#cache']['contexts'] = $redirect_cacheability_updated->getCacheContexts();
|
||||
}
|
||||
}
|
||||
$cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered']));
|
||||
$cache->set($cid, $data, $this->maxAgeToExpire($elements['#cache']['max-age']), Cache::mergeTags($data['#cache']['tags'], ['rendered']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a #cache[max-age] value to an "expire" value for the Cache API.
|
||||
*
|
||||
* @param int $max_age
|
||||
* A #cache[max-age] value.
|
||||
*
|
||||
* @return int
|
||||
* A corresponding "expire" value.
|
||||
*
|
||||
* @see \Drupal\Core\Cache\CacheBackendInterface::set()
|
||||
*/
|
||||
protected function maxAgeToExpire($max_age) {
|
||||
return ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $max_age;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -270,13 +290,13 @@ class RenderCache implements RenderCacheInterface {
|
|||
*
|
||||
* Creates the cache ID string based on #cache['keys'] + #cache['contexts'].
|
||||
*
|
||||
* @param array $elements
|
||||
* @param array &$elements
|
||||
* A renderable array.
|
||||
*
|
||||
* @return string
|
||||
* The cache ID string, or FALSE if the element may not be cached.
|
||||
*/
|
||||
protected function createCacheID(array $elements) {
|
||||
protected function createCacheID(array &$elements) {
|
||||
// If the maximum age is zero, then caching is effectively prohibited.
|
||||
if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) {
|
||||
return FALSE;
|
||||
|
@ -285,8 +305,11 @@ class RenderCache implements RenderCacheInterface {
|
|||
if (isset($elements['#cache']['keys'])) {
|
||||
$cid_parts = $elements['#cache']['keys'];
|
||||
if (!empty($elements['#cache']['contexts'])) {
|
||||
$contexts = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']);
|
||||
$cid_parts = array_merge($cid_parts, $contexts);
|
||||
$context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']);
|
||||
$cid_parts = array_merge($cid_parts, $context_cache_keys->getKeys());
|
||||
CacheableMetadata::createFromRenderArray($elements)
|
||||
->merge($context_cache_keys)
|
||||
->applyTo($elements);
|
||||
}
|
||||
return implode(':', $cid_parts);
|
||||
}
|
||||
|
@ -313,6 +336,13 @@ class RenderCache implements RenderCacheInterface {
|
|||
// the cache entry size.
|
||||
if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) {
|
||||
$data['#cache_properties'] = $elements['#cache_properties'];
|
||||
// Ensure that any safe strings are a SafeString object.
|
||||
foreach (Element::properties(array_flip($elements['#cache_properties'])) as $cache_property) {
|
||||
if (isset($elements[$cache_property]) && is_scalar($elements[$cache_property]) && SafeMarkup::isSafe($elements[$cache_property])) {
|
||||
$elements[$cache_property] = SafeString::create($elements[$cache_property]);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract all the cacheable items from the element using cache
|
||||
// properties.
|
||||
$cacheable_items = array_intersect_key($elements, array_flip($elements['#cache_properties']));
|
||||
|
@ -321,12 +351,14 @@ class RenderCache implements RenderCacheInterface {
|
|||
$data['#markup'] = '';
|
||||
// Cache only cacheable children's markup.
|
||||
foreach ($cacheable_children as $key) {
|
||||
$cacheable_items[$key] = ['#markup' => $cacheable_items[$key]['#markup']];
|
||||
// We can assume that #markup is safe at this point.
|
||||
$cacheable_items[$key] = ['#markup' => SafeString::create($cacheable_items[$key]['#markup'])];
|
||||
}
|
||||
}
|
||||
$data += $cacheable_items;
|
||||
}
|
||||
|
||||
$data['#markup'] = SafeString::create($data['#markup']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
|
62
core/lib/Drupal/Core/Render/RenderContext.php
Normal file
62
core/lib/Drupal/Core/Render/RenderContext.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Render\RenderContext.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
/**
|
||||
* The render context: a stack containing bubbleable rendering metadata.
|
||||
*
|
||||
* A stack of \Drupal\Core\Render\BubbleableMetadata objects.
|
||||
*
|
||||
* @see \Drupal\Core\Render\RendererInterface
|
||||
* @see \Drupal\Core\Render\Renderer
|
||||
* @see \Drupal\Core\Render\BubbleableMetadata
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RenderContext extends \SplStack {
|
||||
|
||||
/**
|
||||
* Updates the current frame of the stack.
|
||||
*
|
||||
* @param array &$element
|
||||
* The element of the render array that has just been rendered. The stack
|
||||
* frame for this element will be updated with the bubbleable rendering
|
||||
* metadata of this element.
|
||||
*/
|
||||
public function update(&$element) {
|
||||
// The latest frame represents the bubbleable metadata for the subtree.
|
||||
$frame = $this->pop();
|
||||
// Update the frame, but also update the current element, to ensure it
|
||||
// contains up-to-date information in case it gets render cached.
|
||||
$updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame);
|
||||
$updated_frame->applyTo($element);
|
||||
$this->push($updated_frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bubbles the stack.
|
||||
*
|
||||
* Whenever another level in the render array has been rendered, the stack
|
||||
* must be bubbled, to merge its rendering metadata with that of the parent
|
||||
* element.
|
||||
*/
|
||||
public function bubble() {
|
||||
// If there's only one frame on the stack, then this is the root call, and
|
||||
// we can't bubble up further. ::renderRoot() will reset the stack, but we
|
||||
// must not reset it here to allow users of ::executeInRenderContext() to
|
||||
// access the stack directly.
|
||||
if ($this->count() === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge the current and the parent stack frame.
|
||||
$current = $this->pop();
|
||||
$parent = $this->pop();
|
||||
$this->push($current->merge($parent));
|
||||
}
|
||||
|
||||
}
|
|
@ -7,14 +7,16 @@
|
|||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Component\Utility\Xss;
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Controller\ControllerResolverInterface;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Core\Theme\ThemeManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Turns a render array into a HTML string.
|
||||
|
@ -57,11 +59,35 @@ class Renderer implements RendererInterface {
|
|||
protected $rendererConfig;
|
||||
|
||||
/**
|
||||
* The stack containing bubbleable rendering metadata.
|
||||
* Whether we're currently in a ::renderRoot() call.
|
||||
*
|
||||
* @var \SplStack|null
|
||||
* @var bool
|
||||
*/
|
||||
protected static $stack;
|
||||
protected $isRenderingRoot = FALSE;
|
||||
|
||||
/**
|
||||
* The request stack.
|
||||
*
|
||||
* @var \Symfony\Component\HttpFoundation\RequestStack
|
||||
*/
|
||||
protected $requestStack;
|
||||
|
||||
/**
|
||||
* The render context collection.
|
||||
*
|
||||
* An individual global render context is tied to the current request. We then
|
||||
* need to maintain a different context for each request to correctly handle
|
||||
* rendering in subrequests.
|
||||
*
|
||||
* This must be static as long as some controllers rebuild the container
|
||||
* during a request. This causes multiple renderer instances to co-exist
|
||||
* simultaneously, render state getting lost, and therefore causing pages to
|
||||
* fail to render correctly. As soon as it is guaranteed that during a request
|
||||
* the same container is used, it no longer needs to be static.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RenderContext[]
|
||||
*/
|
||||
protected static $contextCollection;
|
||||
|
||||
/**
|
||||
* Constructs a new Renderer.
|
||||
|
@ -74,33 +100,52 @@ class Renderer implements RendererInterface {
|
|||
* The element info.
|
||||
* @param \Drupal\Core\Render\RenderCacheInterface $render_cache
|
||||
* The render cache service.
|
||||
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
|
||||
* The request stack.
|
||||
* @param array $renderer_config
|
||||
* The renderer configuration array.
|
||||
*/
|
||||
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, array $renderer_config) {
|
||||
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, RequestStack $request_stack, array $renderer_config) {
|
||||
$this->controllerResolver = $controller_resolver;
|
||||
$this->theme = $theme;
|
||||
$this->elementInfo = $element_info;
|
||||
$this->renderCache = $render_cache;
|
||||
$this->rendererConfig = $renderer_config;
|
||||
$this->requestStack = $request_stack;
|
||||
|
||||
// Initialize the context collection if needed.
|
||||
if (!isset(static::$contextCollection)) {
|
||||
static::$contextCollection = new \SplObjectStorage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function renderRoot(&$elements) {
|
||||
return $this->render($elements, TRUE);
|
||||
// Disallow calling ::renderRoot() from within another ::renderRoot() call.
|
||||
if ($this->isRenderingRoot) {
|
||||
$this->isRenderingRoot = FALSE;
|
||||
throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.');
|
||||
}
|
||||
|
||||
// Render in its own render context.
|
||||
$this->isRenderingRoot = TRUE;
|
||||
$output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
|
||||
return $this->render($elements, TRUE);
|
||||
});
|
||||
$this->isRenderingRoot = FALSE;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function renderPlain(&$elements) {
|
||||
$current_stack = static::$stack;
|
||||
$this->resetStack();
|
||||
$output = $this->renderRoot($elements);
|
||||
static::$stack = $current_stack;
|
||||
return $output;
|
||||
return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
|
||||
return $this->render($elements, TRUE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -150,16 +195,17 @@ class Renderer implements RendererInterface {
|
|||
// possible that any of them throw an exception that will cause a different
|
||||
// page to be rendered (e.g. throwing
|
||||
// \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause
|
||||
// the 404 page to be rendered). That page might also use Renderer::render()
|
||||
// but if exceptions aren't caught here, the stack will be left in an
|
||||
// inconsistent state.
|
||||
// Hence, catch all exceptions and reset the stack and re-throw them.
|
||||
// the 404 page to be rendered). That page might also use
|
||||
// Renderer::renderRoot() but if exceptions aren't caught here, it will be
|
||||
// impossible to call Renderer::renderRoot() again.
|
||||
// Hence, catch all exceptions, reset the isRenderingRoot property and
|
||||
// re-throw exceptions.
|
||||
try {
|
||||
return $this->doRender($elements, $is_root_call);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Reset stack and re-throw exception.
|
||||
$this->resetStack();
|
||||
// Mark the ::rootRender() call finished due to this exception & re-throw.
|
||||
$this->isRenderingRoot = FALSE;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +214,10 @@ class Renderer implements RendererInterface {
|
|||
* 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']);
|
||||
|
@ -176,8 +226,18 @@ class Renderer implements RendererInterface {
|
|||
}
|
||||
|
||||
// Early-return nothing if user does not have access.
|
||||
if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
|
||||
return '';
|
||||
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.
|
||||
|
@ -185,10 +245,11 @@ class Renderer implements RendererInterface {
|
|||
return '';
|
||||
}
|
||||
|
||||
if (!isset(static::$stack)) {
|
||||
static::$stack = new \SplStack();
|
||||
$context = $this->getCurrentRenderContext();
|
||||
if (!isset($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::$stack->push(new BubbleableMetadata());
|
||||
$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
|
||||
|
@ -217,22 +278,16 @@ class Renderer implements RendererInterface {
|
|||
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']);
|
||||
}
|
||||
// Mark the element markup as safe if is it a string.
|
||||
if (is_string($elements['#markup'])) {
|
||||
$elements['#markup'] = SafeString::create($elements['#markup']);
|
||||
}
|
||||
// The render cache item contains all the bubbleable rendering metadata
|
||||
// for the subtree.
|
||||
$this->updateStack($elements);
|
||||
$context->update($elements);
|
||||
// Render cache hit, so rendering is finished, all necessary info
|
||||
// collected!
|
||||
$this->bubbleStack();
|
||||
$context->bubble();
|
||||
return $elements['#markup'];
|
||||
}
|
||||
}
|
||||
|
@ -330,9 +385,9 @@ class Renderer implements RendererInterface {
|
|||
if (!empty($elements['#printed'])) {
|
||||
// The #printed element contains all the bubbleable rendering metadata for
|
||||
// the subtree.
|
||||
$this->updateStack($elements);
|
||||
$context->update($elements);
|
||||
// #printed, so rendering is finished, all necessary info collected!
|
||||
$this->bubbleStack();
|
||||
$context->bubble();
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -350,10 +405,10 @@ class Renderer implements RendererInterface {
|
|||
$elements['#children'] = '';
|
||||
}
|
||||
|
||||
if (isset($elements['#markup'])) {
|
||||
if (!empty($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']);
|
||||
$elements['#markup'] = $this->xssFilterAdminIfUnsafe($elements['#markup']);
|
||||
}
|
||||
|
||||
// Assume that if #theme is set it represents an implemented hook.
|
||||
|
@ -367,7 +422,7 @@ class Renderer implements RendererInterface {
|
|||
);
|
||||
foreach ($markup_keys as $key) {
|
||||
if (!empty($elements[$key]) && is_scalar($elements[$key])) {
|
||||
$elements[$key] = SafeMarkup::checkAdminXss($elements[$key]);
|
||||
$elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -392,7 +447,7 @@ class Renderer implements RendererInterface {
|
|||
foreach ($children as $key) {
|
||||
$elements['#children'] .= $this->doRender($elements[$key]);
|
||||
}
|
||||
$elements['#children'] = SafeMarkup::set($elements['#children']);
|
||||
$elements['#children'] = SafeString::create($elements['#children']);
|
||||
}
|
||||
|
||||
// If #theme is not implemented and the element has raw #markup as a
|
||||
|
@ -403,7 +458,7 @@ class Renderer implements RendererInterface {
|
|||
// 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']);
|
||||
$elements['#children'] = SafeString::create($elements['#markup'] . $elements['#children']);
|
||||
}
|
||||
|
||||
// Let the theme functions in #theme_wrappers add markup around the rendered
|
||||
|
@ -453,13 +508,13 @@ class Renderer implements RendererInterface {
|
|||
// 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']) : '';
|
||||
$prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
|
||||
$suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
|
||||
|
||||
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
|
||||
|
||||
// We've rendered this element (and its subtree!), now update the stack.
|
||||
$this->updateStack($elements);
|
||||
// We've rendered this element (and its subtree!), now update the context.
|
||||
$context->update($elements);
|
||||
|
||||
// Cache the processed element if both $pre_bubbling_elements and $elements
|
||||
// have the metadata necessary to generate a cache ID.
|
||||
|
@ -481,66 +536,71 @@ class Renderer implements RendererInterface {
|
|||
// that is handled earlier in Renderer::render().
|
||||
if ($is_root_call) {
|
||||
$this->replacePlaceholders($elements);
|
||||
if (static::$stack->count() !== 1) {
|
||||
// @todo remove as part of https://www.drupal.org/node/2511330.
|
||||
if ($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!
|
||||
$this->bubbleStack();
|
||||
$context->bubble();
|
||||
|
||||
$elements['#printed'] = TRUE;
|
||||
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
|
||||
return $elements['#markup'];
|
||||
return SafeString::create($elements['#markup']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the renderer service's internal stack (used for bubbling metadata).
|
||||
*
|
||||
* Only necessary in very rare/advanced situations, such as when rendering an
|
||||
* error page if an exception occurred *during* rendering.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function resetStack() {
|
||||
static::$stack = NULL;
|
||||
public function hasRenderContext() {
|
||||
return (bool) $this->getCurrentRenderContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the stack.
|
||||
*
|
||||
* @param array &$element
|
||||
* The element of the render array that has just been rendered. The stack
|
||||
* frame for this element will be updated with the bubbleable rendering
|
||||
* metadata of this element.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function updateStack(&$element) {
|
||||
// The latest frame represents the bubbleable metadata for the subtree.
|
||||
$frame = static::$stack->pop();
|
||||
// Update the frame, but also update the current element, to ensure it
|
||||
// contains up-to-date information in case it gets render cached.
|
||||
$updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame);
|
||||
$updated_frame->applyTo($element);
|
||||
static::$stack->push($updated_frame);
|
||||
}
|
||||
public function executeInRenderContext(RenderContext $context, callable $callable) {
|
||||
// Store the current render context.
|
||||
$previous_context = $this->getCurrentRenderContext();
|
||||
|
||||
/**
|
||||
* Bubbles the stack.
|
||||
*
|
||||
* Whenever another level in the render array has been rendered, the stack
|
||||
* must be bubbled, to merge its rendering metadata with that of the parent
|
||||
* element.
|
||||
*/
|
||||
protected function bubbleStack() {
|
||||
// If there's only one frame on the stack, then this is the root call, and
|
||||
// we can't bubble up further. Reset the stack for the next root call.
|
||||
if (static::$stack->count() === 1) {
|
||||
$this->resetStack();
|
||||
return;
|
||||
// Set the provided context and call the callable, it will use that context.
|
||||
$this->setCurrentRenderContext($context);
|
||||
$result = $callable();
|
||||
// @todo Convert to an assertion in https://www.drupal.org/node/2408013
|
||||
if ($context->count() > 1) {
|
||||
throw new \LogicException('Bubbling failed.');
|
||||
}
|
||||
|
||||
// Merge the current and the parent stack frame.
|
||||
$current = static::$stack->pop();
|
||||
$parent = static::$stack->pop();
|
||||
static::$stack->push($current->merge($parent));
|
||||
// Restore the original render context.
|
||||
$this->setCurrentRenderContext($previous_context);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current render context.
|
||||
*
|
||||
* @return \Drupal\Core\Render\RenderContext
|
||||
* The current render context.
|
||||
*/
|
||||
protected function getCurrentRenderContext() {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current render context.
|
||||
*
|
||||
* @param \Drupal\Core\Render\RenderContext|null $context
|
||||
* The render context. This can be NULL for instance when restoring the
|
||||
* original render context, which is in fact NULL.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function setCurrentRenderContext(RenderContext $context = NULL) {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
static::$contextCollection[$request] = $context;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -638,4 +698,24 @@ class Renderer implements RendererInterface {
|
|||
$meta_a->merge($meta_b)->applyTo($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a very permissive XSS/HTML filter for admin-only use.
|
||||
*
|
||||
* Note: This method only filters if $string is not marked safe already. This
|
||||
* ensures that HTML intended for display is not filtered.
|
||||
*
|
||||
* @param string|\Drupal\Core\Render\SafeString $string
|
||||
* A string.
|
||||
*
|
||||
* @return \Drupal\Core\Render\SafeString
|
||||
* The escaped string wrapped in a SafeString object. If
|
||||
* SafeMarkup::isSafe($string) returns TRUE, it won't be escaped again.
|
||||
*/
|
||||
protected function xssFilterAdminIfUnsafe($string) {
|
||||
if (!SafeMarkup::isSafe($string)) {
|
||||
$string = Xss::filterAdmin($string);
|
||||
}
|
||||
return SafeString::create($string);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,13 +22,18 @@ interface RendererInterface {
|
|||
* - system internals that are responsible for rendering the final HTML
|
||||
* - render arrays for non-HTML responses, such as feeds
|
||||
*
|
||||
* (Cannot be executed within another render context.)
|
||||
*
|
||||
* @param array $elements
|
||||
* The structured array describing the data to be rendered.
|
||||
*
|
||||
* @return string
|
||||
* @return \Drupal\Component\Utility\SafeStringInterface
|
||||
* The rendered HTML.
|
||||
*
|
||||
* @see ::render()
|
||||
*
|
||||
* @throws \LogicException
|
||||
* When called from inside another renderRoot() call.
|
||||
*/
|
||||
public function renderRoot(&$elements);
|
||||
|
||||
|
@ -45,13 +50,15 @@ interface RendererInterface {
|
|||
* ::renderRoot() call, but that is generally highly problematic (and hence an
|
||||
* exception is thrown when a ::renderRoot() call happens within another
|
||||
* ::renderRoot() call). However, in this case, we only care about the output,
|
||||
* not about the bubbling. Hence this uses a separate render stack, to not
|
||||
* not about the bubbling. Hence this uses a separate render context, to not
|
||||
* affect the parent ::renderRoot() call.
|
||||
*
|
||||
* (Can be executed within another render context: it runs in isolation.)
|
||||
*
|
||||
* @param array $elements
|
||||
* The structured array describing the data to be rendered.
|
||||
*
|
||||
* @return string
|
||||
* @return \Drupal\Component\Utility\SafeStringInterface
|
||||
* The rendered HTML.
|
||||
*
|
||||
* @see ::renderRoot()
|
||||
|
@ -88,8 +95,8 @@ interface RendererInterface {
|
|||
* or configuration that can affect that rendering changes.
|
||||
* - Placeholders, with associated self-contained placeholder render arrays,
|
||||
* for executing code to handle dynamic requirements that cannot be cached.
|
||||
* A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to
|
||||
* perform this bubbling.
|
||||
* A render context (\Drupal\Core\Render\RenderContext) can be used to perform
|
||||
* bubbling; it is a stack of \Drupal\Core\Render\BubbleableMetadata objects.
|
||||
*
|
||||
* Additionally, whether retrieving from cache or not, it is important to
|
||||
* know all of the assets (CSS and JavaScript) required by the rendered HTML,
|
||||
|
@ -103,9 +110,9 @@ interface RendererInterface {
|
|||
* - If this element has already been printed (#printed = TRUE) or the user
|
||||
* does not have access to it (#access = FALSE), then an empty string is
|
||||
* returned.
|
||||
* - If no stack data structure has been created yet, it is done now. Next,
|
||||
* - If no render context is set yet, an exception is thrown. Otherwise,
|
||||
* an empty \Drupal\Core\Render\BubbleableMetadata is pushed onto the
|
||||
* stack.
|
||||
* render context.
|
||||
* - If this element has #cache defined then the cached markup for this
|
||||
* element will be returned if it exists in Renderer::render()'s cache. To
|
||||
* use Renderer::render() caching, set the element's #cache property to an
|
||||
|
@ -295,17 +302,16 @@ interface RendererInterface {
|
|||
* (Internal use only.) Whether this is a recursive call or not. See
|
||||
* ::renderRoot().
|
||||
*
|
||||
* @return string
|
||||
* @return \Drupal\Component\Utility\SafeStringInterface
|
||||
* The rendered HTML.
|
||||
*
|
||||
* @throws \LogicException
|
||||
* If a root call to ::render() does not result in an empty stack, this
|
||||
* indicates an erroneous ::render() root call (a root call within a
|
||||
* root call, which makes no sense). Therefore, a logic exception is thrown.
|
||||
* When called outside of a render context. (i.e. outside of a renderRoot(),
|
||||
* renderPlain() or executeInRenderContext() call.)
|
||||
* @throws \Exception
|
||||
* If a #pre_render callback throws an exception, it is caught to reset the
|
||||
* stack used for bubbling rendering metadata, and then the exception is re-
|
||||
* thrown.
|
||||
* If a #pre_render callback throws an exception, it is caught to mark the
|
||||
* renderer as no longer being in a root render call, if any. Then the
|
||||
* exception is rethrown.
|
||||
*
|
||||
* @see \Drupal\Core\Render\ElementInfoManagerInterface::getInfo()
|
||||
* @see \Drupal\Core\Theme\ThemeManagerInterface::render()
|
||||
|
@ -315,6 +321,49 @@ interface RendererInterface {
|
|||
*/
|
||||
public function render(&$elements, $is_root_call = FALSE);
|
||||
|
||||
/**
|
||||
* Checks whether a render context is active.
|
||||
*
|
||||
* This is useful only in very specific situations to determine whether the
|
||||
* system is already capable of collecting bubbleable metadata. Normally it
|
||||
* should not be necessary to be concerned about this.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the renderer has a render context active, FALSE otherwise.
|
||||
*/
|
||||
public function hasRenderContext();
|
||||
|
||||
/**
|
||||
* Executes a callable within a render context.
|
||||
*
|
||||
* Only for very advanced use cases. Prefer using ::renderRoot() and
|
||||
* ::renderPlain() instead.
|
||||
*
|
||||
* All rendering must happen within a render context. Within a render context,
|
||||
* all bubbleable metadata is bubbled and hence tracked. Outside of a render
|
||||
* context, it would be lost. This could lead to missing assets, incorrect
|
||||
* cache variations (and thus security issues), insufficient cache
|
||||
* invalidations, and so on.
|
||||
*
|
||||
* Any and all rendering must therefore happen within a render context, and it
|
||||
* is this method that provides that.
|
||||
*
|
||||
* @see \Drupal\Core\Render\BubbleableMetadata
|
||||
*
|
||||
* @param \Drupal\Core\Render\RenderContext $context
|
||||
* The render context to execute the callable within.
|
||||
* @param callable $callable
|
||||
* The callable to execute.
|
||||
* @return mixed
|
||||
* The callable's return value.
|
||||
*
|
||||
* @see \Drupal\Core\Render\RenderContext
|
||||
*
|
||||
* @throws \LogicException
|
||||
* In case bubbling has failed, can only happen in case of broken code.
|
||||
*/
|
||||
public function executeInRenderContext(RenderContext $context, callable $callable);
|
||||
|
||||
/**
|
||||
* Merges the bubbleable rendering metadata o/t 2nd render array with the 1st.
|
||||
*
|
||||
|
|
84
core/lib/Drupal/Core/Render/SafeString.php
Normal file
84
core/lib/Drupal/Core/Render/SafeString.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Render\SafeString.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Component\Utility\SafeStringInterface;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
|
||||
/**
|
||||
* Defines an object that passes safe strings through the render system.
|
||||
*
|
||||
* This object should only be constructed with a known safe string. If there is
|
||||
* any risk that the string contains user-entered data that has not been
|
||||
* 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.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue