Update to drupal 8.0.0-rc1. For more information, see https://www.drupal.org/node/2582663

This commit is contained in:
Greg Anderson 2015-10-08 11:40:12 -07:00
parent eb34d130a8
commit f32e58e4b1
8476 changed files with 211648 additions and 170042 deletions

View file

@ -57,7 +57,7 @@ class Handle {
if (version_compare(PHP_VERSION, '7.0.0-dev') < 0) {
// PHP 5 - create a handler to throw the exception directly.
assert_options(ASSERT_CALLBACK, function($file, $line, $code, $message) {
assert_options(ASSERT_CALLBACK, function($file = '', $line = 0, $code = '', $message = '') {
if (empty($message)) {
$message = $code;
}

View file

@ -71,6 +71,9 @@ class Inspector {
/**
* Asserts that all members are strings.
*
* Use this only if it is vital that the members not be objects, otherwise
* test with ::assertAllStringable().
*
* @param mixed $traversable
* Variable to be examined.
*
@ -94,7 +97,7 @@ class Inspector {
public static function assertAllStringable($traversable) {
if (static::assertTraversable($traversable)) {
foreach ($traversable as $member) {
if (!(is_string($member) || (is_object($member) && method_exists($member, '__toString')))) {
if (!static::assertStringable($member)) {
return FALSE;
}
}
@ -103,6 +106,22 @@ class Inspector {
return FALSE;
}
/**
* Asserts argument is a string or an object castable to a string.
*
* Use this instead of is_string() alone unless the argument being an object
* in any way will cause a problem.
*
* @param mixed string
* Variable to be examined
*
* @return bool
* TRUE if $string is a string or an object castable to a string.
*/
public static function assertStringable($string) {
return is_string($string) || (is_object($string) && method_exists($string, '__toString'));
}
/**
* Asserts that all members are arrays.
*

View file

@ -5,6 +5,7 @@
* Contains \Drupal\Component\Datetime\DateTimePlus.
*/
namespace Drupal\Component\Datetime;
use Drupal\Component\Utility\ToStringTrait;
/**
* Wraps DateTime().
@ -28,6 +29,8 @@ namespace Drupal\Component\Datetime;
*/
class DateTimePlus {
use ToStringTrait;
const FORMAT = 'Y-m-d H:i:s';
/**
@ -271,16 +274,12 @@ class DateTimePlus {
}
/**
* Implements __toString() for dates.
* Renders the timezone name.
*
* The DateTime class does not implement this.
*
* @see https://bugs.php.net/bug.php?id=62911
* @see http://www.serverphorums.com/read.php?7,555645
* @return string
*/
public function __toString() {
$format = static::FORMAT;
return $this->format($format) . ' ' . $this->getTimeZone()->getName();
public function render() {
return $this->format(static::FORMAT) . ' ' . $this->getTimeZone()->getName();
}
/**

View file

@ -153,4 +153,13 @@ class FileCache implements FileCacheInterface {
}
}
/**
* Resets the static cache.
*
* @todo Replace this once https://www.drupal.org/node/2260187 is in.
*/
public static function reset() {
static::$cached = [];
}
}

View file

@ -193,7 +193,7 @@ class PoItem {
strpos($this->_source, LOCALE_PLURAL_DELIMITER) !== FALSE) {
$this->setSource(explode(LOCALE_PLURAL_DELIMITER, $this->_source));
$this->setTranslation(explode(LOCALE_PLURAL_DELIMITER, $this->_translation));
$this->setPlural(count($this->_translation) > 1);
$this->setPlural(count($this->_source) > 1);
}
}

View file

@ -31,20 +31,16 @@ class Context implements ContextInterface {
protected $contextDefinition;
/**
* Sets the contextDefinition for us without needing to call the setter.
* Create a context object.
*
* @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface $context_definition
* The context definition.
* @param mixed|null $context_value
* The value of the context.
*/
public function __construct(ContextDefinitionInterface $context_definition) {
public function __construct(ContextDefinitionInterface $context_definition, $context_value = NULL) {
$this->contextDefinition = $context_definition;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::setContextValue().
*/
public function setContextValue($value) {
$this->contextValue = $value;
$this->contextValue = $context_value;
}
/**
@ -74,13 +70,6 @@ class Context implements ContextInterface {
return (bool) $this->contextValue || (bool) $this->getContextDefinition()->getDefaultValue();
}
/**
* {@inheritdoc}
*/
public function setContextDefinition(ContextDefinitionInterface $context_definition) {
$this->contextDefinition = $context_definition;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::getContextDefinition().
*/

View file

@ -12,16 +12,6 @@ namespace Drupal\Component\Plugin\Context;
*/
interface ContextInterface {
/**
* Sets the context value.
*
* @param mixed $value
* The value of this context, matching the context definition.
*
* @see \Drupal\Component\Plugin\Context\ContextInterface::setContextDefinition().
*/
public function setContextValue($value);
/**
* Gets the context value.
*
@ -38,15 +28,6 @@ interface ContextInterface {
*/
public function hasContextValue();
/**
* Sets the definition that the context must conform to.
*
* @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface $context_definition
* A defining characteristic representation of the context against which
* that context can be validated.
*/
public function setContextDefinition(ContextDefinitionInterface $context_definition);
/**
* Gets the provided definition that the context must conform to.
*

View file

@ -41,17 +41,30 @@ abstract class ContextAwarePluginBase extends PluginBase implements ContextAware
* The plugin implementation definition.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
$context = array();
if (isset($configuration['context'])) {
$context = $configuration['context'];
unset($configuration['context']);
}
$context_configuration = isset($configuration['context']) ? $configuration['context'] : [];
unset($configuration['context']);
parent::__construct($configuration, $plugin_id, $plugin_definition);
foreach ($context as $key => $value) {
$this->contexts = $this->createContextFromConfiguration($context_configuration);
}
/**
* Creates context objects from any context mappings in configuration.
*
* @param array $context_configuration
* An associative array of context names and values.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of context objects.
*/
protected function createContextFromConfiguration(array $context_configuration) {
$contexts = [];
foreach ($context_configuration as $key => $value) {
$context_definition = $this->getContextDefinition($key);
$this->context[$key] = new Context($context_definition);
$this->context[$key]->setContextValue($value);
$contexts[$key] = new Context($context_definition, $value);
}
return $contexts;
}
/**
@ -124,7 +137,7 @@ abstract class ContextAwarePluginBase extends PluginBase implements ContextAware
* {@inheritdoc}
*/
public function setContextValue($name, $value) {
$this->getContext($name)->setContextValue($value);
$this->context[$name] = new Context($this->getContextDefinition($name), $value);
return $this;
}

View file

@ -0,0 +1,40 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\PluginDefinitionInterface.
*/
namespace Drupal\Component\Plugin\Definition;
/**
* Defines a plugin definition.
*
* Object-based plugin definitions MUST implement this interface.
*
* @ingroup Plugin
*/
interface PluginDefinitionInterface {
/**
* Sets the class.
*
* @param string $class
* A fully qualified class name.
*
* @return static
*
* @throws \InvalidArgumentException
* If the class is invalid.
*/
public function setClass($class);
/**
* Gets the class.
*
* @return string
* A fully qualified class name.
*/
public function getClass();
}

View file

@ -6,6 +6,7 @@
namespace Drupal\Component\Plugin\Factory;
use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Exception\PluginException;
@ -63,7 +64,7 @@ class DefaultFactory implements FactoryInterface {
*
* @param string $plugin_id
* The id of a plugin.
* @param mixed $plugin_definition
* @param \Drupal\Component\Plugin\Definition\PluginDefinitionInterface|mixed[] $plugin_definition
* The plugin definition associated with the plugin ID.
* @param string $required_interface
* (optional) THe required plugin interface.
@ -77,18 +78,32 @@ class DefaultFactory implements FactoryInterface {
*
*/
public static function getPluginClass($plugin_id, $plugin_definition = NULL, $required_interface = NULL) {
if (empty($plugin_definition['class'])) {
throw new PluginException(sprintf('The plugin (%s) did not specify an instance class.', $plugin_id));
}
$missing_class_message = sprintf('The plugin (%s) did not specify an instance class.', $plugin_id);
if (is_array($plugin_definition)) {
if (empty($plugin_definition['class'])) {
throw new PluginException($missing_class_message);
}
$class = $plugin_definition['class'];
$class = $plugin_definition['class'];
}
elseif ($plugin_definition instanceof PluginDefinitionInterface) {
if (!$plugin_definition->getClass()) {
throw new PluginException($missing_class_message);
}
$class = $plugin_definition->getClass();
}
else {
$plugin_definition_type = is_object($plugin_definition) ? get_class($plugin_definition) : gettype($plugin_definition);
throw new PluginException(sprintf('%s can only handle plugin definitions that are arrays or that implement %s, but %s given.', __CLASS__, PluginDefinitionInterface::class, $plugin_definition_type));
}
if (!class_exists($class)) {
throw new PluginException(sprintf('Plugin (%s) instance class "%s" does not exist.', $plugin_id, $class));
}
if ($required_interface && !is_subclass_of($plugin_definition['class'], $required_interface)) {
throw new PluginException(sprintf('Plugin "%s" (%s) must implement interface %s.', $plugin_id, $plugin_definition['class'], $required_interface));
if ($required_interface && !is_subclass_of($class, $required_interface)) {
throw new PluginException(sprintf('Plugin "%s" (%s) must implement interface %s.', $plugin_id, $class, $required_interface));
}
return $class;

View file

@ -0,0 +1,253 @@
<?php
/**
* @file
* Contains Drupal\Component\Render\FormattableMarkup.
*/
namespace Drupal\Component\Render;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
/**
* Formats a string for HTML display by replacing variable placeholders.
*
* When cast to a string, this object replaces variable placeholders in the
* string with the arguments passed in during construction and escapes the
* values so they can be safely displayed as HTML. See the documentation of
* \Drupal\Component\Render\FormattableMarkup::placeholderFormat() for details
* on the supported placeholders and how to use them securely. Incorrect use of
* this class can result in security vulnerabilities.
*
* In most cases, you should use TranslatableMarkup or PluralTranslatableMarkup
* rather than this object, since they will translate the text (on
* non-English-only sites) in addition to formatting it. Variables concatenated
* without the insertion of language-specific words or punctuation are some
* examples where translation is not applicable and using this class directly
* directly is appropriate.
*
* This class is designed for formatting messages that are mostly text, not as
* an HTML template language. As such:
* - The passed in string should contain no (or minimal) HTML.
* - Variable placeholders should not be used within the "<" and ">" of an
* HTML tag, such as in HTML attribute values. This would be a security
* risk. Examples:
* @code
* // Insecure (placeholder within "<" and ">"):
* $this->placeholderFormat('<@variable>text</@variable>', ['@variable' => $variable]);
* // Insecure (placeholder within "<" and ">"):
* $this->placeholderFormat('<a @variable>link text</a>', ['@variable' => $variable]);
* // Insecure (placeholder within "<" and ">"):
* $this->placeholderFormat('<a title="@variable">link text</a>', ['@variable' => $variable]);
* @endcode
* Only the "href" attribute is supported via the special ":variable"
* placeholder, to allow simple links to be inserted:
* @code
* // Secure (usage of ":variable" placeholder for href attribute):
* $this->placeholderFormat('<a href=":variable">link text</a>', [':variable' , $variable]);
* // Secure (usage of ":variable" placeholder for href attribute):
* $this->placeholderFormat('<a href=":variable" title="static text">link text</a>', [':variable' => $variable]);
* // Insecure (the "@variable" placeholder does not filter dangerous
* // protocols):
* $this->placeholderFormat('<a href="@variable">link text</a>', ['@variable' => $variable]);
* // Insecure ("@variable" placeholder within "<" and ">"):
* $this->placeholderFormat('<a href=":url" title="@variable">link text</a>', [':url' => $url, '@variable' => $variable]);
* @endcode
* To build non-minimal HTML, use an HTML template language such as Twig,
* rather than this class.
*
* @ingroup sanitization
*
* @see \Drupal\Core\StringTranslation\TranslatableMarkup
* @see \Drupal\Core\StringTranslation\PluralTranslatableMarkup
* @see \Drupal\Component\Render\FormattableMarkup::placeholderFormat()
*/
class FormattableMarkup implements MarkupInterface {
/**
* The arguments to replace placeholders with.
*
* @var array
*/
protected $arguments = [];
/**
* Constructs a new class instance.
*
* @param string $string
* A string containing placeholders. The string itself will not be escaped,
* any unsafe content must be in $args and inserted via placeholders.
* @param array $arguments
* An array with placeholder replacements, keyed by placeholder. See
* \Drupal\Component\Render\FormattableMarkup::placeholderFormat() for
* additional information about placeholders.
*
* @see \Drupal\Component\Render\FormattableMarkup::placeholderFormat()
*/
public function __construct($string, array $arguments) {
$this->string = (string) $string;
$this->arguments = $arguments;
}
/**
* {@inheritdoc}
*/
public function __toString() {
return static::placeholderFormat($this->string, $this->arguments);
}
/**
* Returns the string length.
*
* @return int
* The length of the string.
*/
public function count() {
return Unicode::strlen($this->string);
}
/**
* Returns a representation of the object for use in JSON serialization.
*
* @return string
* The safe string content.
*/
public function jsonSerialize() {
return $this->__toString();
}
/**
* Replaces placeholders in a string with values.
*
* @param string $string
* A string containing placeholders. The string itself is expected to be
* safe and correct HTML. Any unsafe content must be in $args and
* inserted via placeholders.
* @param array $args
* An associative array of replacements. Each array key should be the same
* as a placeholder in $string. The corresponding value should be a string
* or an object that implements
* \Drupal\Component\Render\MarkupInterface. The value replaces the
* placeholder in $string. Sanitization and formatting will be done before
* replacement. The type of sanitization and formatting depends on the first
* character of the key:
* - @variable: When the placeholder replacement value is:
* - A string, the replaced value in the returned string will be sanitized
* using \Drupal\Component\Utility\Html::escape().
* - A MarkupInterface object, the replaced value in the returned string
* will not be sanitized.
* - A MarkupInterface object cast to a string, the replaced value in the
* returned string be forcibly sanitized using
* \Drupal\Component\Utility\Html::escape().
* @code
* $this->placeholderFormat('This will force HTML-escaping of the replacement value: @text', ['@text' => (string) $safe_string_interface_object));
* @endcode
* Use this placeholder as the default choice for anything displayed on
* the site, but not within HTML attributes, JavaScript, or CSS. Doing so
* is a security risk.
* - %variable: Use when the replacement value is to be wrapped in <em>
* tags.
* A call like:
* @code
* $string = "%output_text";
* $arguments = ['output_text' => 'text output here.'];
* $this->placeholderFormat($string, $arguments);
* @endcode
* makes the following HTML code:
* @code
* <em class="placeholder">text output here.</em>
* @endcode
* As with @variable, do not use this within HTML attributes, JavaScript,
* or CSS. Doing so is a security risk.
* - :variable: Return value is escaped with
* \Drupal\Component\Utility\Html::escape() and filtered for dangerous
* protocols using UrlHelper::stripDangerousProtocols(). Use this when
* using the "href" attribute, ensuring the attribute value is always
* wrapped in quotes:
* @code
* // Secure (with quotes):
* $this->placeholderFormat('<a href=":url">@variable</a>', [':url' => $url, @variable => $variable]);
* // Insecure (without quotes):
* $this->placeholderFormat('<a href=:url>@variable</a>', [':url' => $url, @variable => $variable]);
* @endcode
* When ":variable" comes from arbitrary user input, the result is secure,
* but not guaranteed to be a valid URL (which means the resulting output
* could fail HTML validation). To guarantee a valid URL, use
* Url::fromUri($user_input)->toString() (which either throws an exception
* or returns a well-formed URL) before passing the result into a
* ":variable" placeholder.
*
* @return string
* A formatted HTML string with the placeholders replaced.
*
* @ingroup sanitization
*
* @see \Drupal\Core\StringTranslation\TranslatableMarkup
* @see \Drupal\Core\StringTranslation\PluralTranslatableMarkup
* @see \Drupal\Component\Utility\Html::escape()
* @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
* @see \Drupal\Core\Url::fromUri()
*/
protected static function placeholderFormat($string, array $args) {
// Transform arguments before inserting them.
foreach ($args as $key => $value) {
switch ($key[0]) {
case '@':
// Escape if the value is not an object from a class that implements
// \Drupal\Component\Render\MarkupInterface, for example strings will
// be escaped.
// \Drupal\Component\Utility\SafeMarkup\SafeMarkup::isSafe() may
// return TRUE for content that is safe within HTML fragments, but not
// within other contexts, so this placeholder type must not be used
// within HTML attributes, JavaScript, or CSS.
$args[$key] = static::placeholderEscape($value);
break;
case ':':
// Strip URL protocols that can be XSS vectors.
$value = UrlHelper::stripDangerousProtocols($value);
// Escape unconditionally, without checking
// \Drupal\Component\Utility\SafeMarkup\SafeMarkup::isSafe(). This
// forces characters that are unsafe for use in an "href" HTML
// attribute to be encoded. If a caller wants to pass a value that is
// extracted from HTML and therefore is already HTML encoded, it must
// invoke
// \Drupal\Component\Render\OutputStrategyInterface::renderFromHtml()
// on it prior to passing it in as a placeholder value of this type.
// @todo Add some advice and stronger warnings.
// https://www.drupal.org/node/2569041.
$args[$key] = Html::escape($value);
break;
case '%':
default:
// Similarly to @, escape non-safe values. Also, add wrapping markup
// in order to render as a placeholder. Not for use within attributes,
// per the warning above about
// \Drupal\Component\Utility\SafeMarkup\SafeMarkup::isSafe() and also
// due to the wrapping markup.
$args[$key] = '<em class="placeholder">' . static::placeholderEscape($value) . '</em>';
break;
}
}
return strtr($string, $args);
}
/**
* Escapes a placeholder replacement value if needed.
*
* @param string|\Drupal\Component\Render\MarkupInterface $value
* A placeholder replacement value.
*
* @return string
* The properly escaped replacement value.
*/
protected static function placeholderEscape($value) {
return SafeMarkup::isSafe($value) ? (string) $value : Html::escape($value);
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains \Drupal\Component\Render\HtmlEscapedText.
*/
namespace Drupal\Component\Render;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
/**
* Escapes HTML syntax characters to HTML entities for display in markup.
*
* This class can be used to provide theme engine-like late escaping
* functionality.
*
* @ingroup sanitization
*/
class HtmlEscapedText implements MarkupInterface {
/**
* The string to escape.
*
* @var string
*/
protected $string;
/**
* Constructs an HtmlEscapedText object.
*
* @param $string
* The string to escape. This value will be cast to a string.
*/
public function __construct($string) {
$this->string = (string) $string;
}
/**
* {@inheritdoc}
*/
public function __toString() {
return Html::escape($this->string);
}
/**
* {@inheritdoc}
*/
public function count() {
return Unicode::strlen($this->string);
}
/**
* {@inheritdoc}
*/
public function jsonSerialize() {
return $this->__toString();
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* @file
* Contains \Drupal\Component\Render\MarkupInterface.
*/
namespace Drupal\Component\Render;
/**
* Marks an object's __toString() method as returning markup.
*
* Objects that implement this interface will not be automatically XSS filtered
* by the render system or automatically escaped by the theme engine.
*
* If there is any risk of the object's __toString() method returning
* user-entered data that has not been filtered first, it must not be used. If
* the object that implements this does not perform automatic escaping or
* filtering itself, then it must be marked as "@internal". For example, Views
* has the internal ViewsRenderPipelineMarkup object to provide a custom render
* pipeline in order to render JSON and to fast render fields. By contrast,
* FormattableMarkup and TranslatableMarkup always sanitize their output when
* used correctly.
*
* If the object is going to be used directly in Twig templates it should
* implement \Countable so it can be used in if statements.
*
* @see \Drupal\Component\Render\MarkupTrait
* @see \Drupal\Component\Utility\SafeMarkup::isSafe()
* @see \Drupal\Core\Template\TwigExtension::escapeFilter()
* @see \Drupal\Component\Render\FormattableMarkup
* @see \Drupal\Core\StringTranslation\TranslatableMarkup
* @see \Drupal\views\Render\ViewsRenderPipelineMarkup
* @see twig_render_template()
* @see sanitization
* @see theme_render
*/
interface MarkupInterface extends \JsonSerializable {
/**
* Returns markup.
*
* @return string
* The markup.
*/
public function __toString();
}

View file

@ -2,17 +2,19 @@
/**
* @file
* Contains \Drupal\Component\Utility\SafeStringTrait.
* Contains \Drupal\Component\Render\MarkupTrait.
*/
namespace Drupal\Component\Utility;
namespace Drupal\Component\Render;
use Drupal\Component\Utility\Unicode;
/**
* Implements SafeStringInterface and Countable for rendered objects.
* Implements MarkupInterface and Countable for rendered objects.
*
* @see \Drupal\Component\Utility\SafeStringInterface
* @see \Drupal\Component\Render\MarkupInterface
*/
trait SafeStringTrait {
trait MarkupTrait {
/**
* The safe string.
@ -22,20 +24,20 @@ trait SafeStringTrait {
protected $string;
/**
* Creates a SafeString object if necessary.
* Creates a Markup 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.
* Markup object. If $string is an object that implements MarkupInterface 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
* @return string|\Drupal\Component\Render\MarkupInterface
* A safe string.
*/
public static function create($string) {
if ($string instanceof SafeStringInterface) {
if ($string instanceof MarkupInterface) {
return $string;
}
$string = (string) $string;
@ -48,7 +50,7 @@ trait SafeStringTrait {
}
/**
* Returns the string version of the SafeString object.
* Returns the string version of the Markup object.
*
* @return string
* The safe string content.

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\Component\Render\OutputStrategyInterface.
*/
namespace Drupal\Component\Render;
/**
* Provides an output strategy that formats HTML strings for a given context.
*
* Output strategies assist in transforming HTML strings into strings that are
* appropriate for a given context (e.g. plain-text), through performing the
* relevant formatting. No santization is applied.
*/
interface OutputStrategyInterface {
/**
* Transforms a given HTML string into to a context-appropriate output string.
*
* This transformation consists of performing the formatting appropriate to
* a given output context (e.g., plain-text email subjects, HTML attribute
* values).
*
* @param string|object $string
* An HTML string or an object with a ::__toString() magic method returning
* HTML markup. The source HTML markup is considered ready for output into
* HTML fragments and thus already properly escaped and sanitized.
*
* @return string
* A new string that is formatted according to the output strategy.
*/
public static function renderFromHtml($string);
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Component\Render\PlainTextOutput.
*/
namespace Drupal\Component\Render;
use Drupal\Component\Utility\Html;
/**
* Provides an output strategy for transforming HTML into simple plain text.
*
* Use this when rendering a given HTML string into a plain text string that
* does not need special formatting, such as a label or an email subject.
*
* Returns a string with HTML tags stripped and HTML entities decoded suitable
* for email or other non-HTML contexts.
*/
class PlainTextOutput implements OutputStrategyInterface {
/**
* {@inheritdoc}
*/
public static function renderFromHtml($string) {
return Html::decodeEntities(strip_tags((string) $string));
}
}

View file

@ -78,12 +78,24 @@ class Html {
public static function cleanCssIdentifier($identifier, array $filter = array(
' ' => '-',
'_' => '-',
'__' => '__',
'/' => '-',
'[' => '-',
']' => ''
']' => '',
)) {
$identifier = strtr($identifier, $filter);
// We could also use strtr() here but its much slower than str_replace(). In
// order to keep '__' to stay '__' we first replace it with a different
// placeholder after checking that it is not defined as a filter.
$double_underscore_replacements = 0;
if (!isset($filter['__'])) {
$identifier = str_replace('__', '##', $identifier, $double_underscore_replacements);
}
$identifier = str_replace(array_keys($filter), array_values($filter), $identifier);
// Replace temporary placeholder '##' with '__' only if the original
// $identifier contained '__'.
if ($double_underscore_replacements > 0) {
$identifier = str_replace('##', '__', $identifier);
}
// Valid characters in a CSS identifier are:
// - the hyphen (U+002D)
// - a-z (U+0030 - U+0039)
@ -250,9 +262,9 @@ class Html {
<body>!html</body>
</html>
EOD;
// PHP's \DOMDocument serialization adds straw whitespace in case the markup
// of the wrapping document contains newlines, so ensure to remove all
// newlines before injecting the actual HTML body to process.
// PHP's \DOMDocument serialization adds extra whitespace when the markup
// of the wrapping document contains newlines, so ensure we remove all
// newlines before injecting the actual HTML body to be processed.
$document = strtr($document, array("\n" => '', '!html' => $html));
$dom = new \DOMDocument();
@ -280,14 +292,16 @@ EOD;
$body_node = $document->getElementsByTagName('body')->item(0);
$html = '';
foreach ($body_node->getElementsByTagName('script') as $node) {
static::escapeCdataElement($node);
}
foreach ($body_node->getElementsByTagName('style') as $node) {
static::escapeCdataElement($node, '/*', '*/');
}
foreach ($body_node->childNodes as $node) {
$html .= $document->saveXML($node);
if ($body_node !== NULL) {
foreach ($body_node->getElementsByTagName('script') as $node) {
static::escapeCdataElement($node);
}
foreach ($body_node->getElementsByTagName('style') as $node) {
static::escapeCdataElement($node, '/*', '*/');
}
foreach ($body_node->childNodes as $node) {
$html .= $document->saveXML($node);
}
}
return $html;
}

View file

@ -7,23 +7,16 @@
namespace Drupal\Component\Utility;
use Drupal\Component\Render\HtmlEscapedText;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Render\MarkupInterface;
/**
* Manages known safe strings for rendering at the theme layer.
* Contains deprecated functionality related to sanitization of markup.
*
* The Twig theme engine autoescapes string variables in the template, so it
* is possible for a string of markup to become double-escaped. SafeMarkup
* provides a store for known safe strings and methods to manage them
* throughout the page request.
*
* Strings sanitized by self::checkPlain() and self::escape() are automatically
* marked safe, as are markup strings created from @link theme_render render
* arrays @endlink via drupal_render().
*
* This class should be limited to internal use only. Module developers should
* instead use the appropriate
* @link sanitization sanitization functions @endlink or the
* @link theme_render theme and render systems @endlink so that the output can
* can be themed, escaped, and altered properly.
* @deprecated Will be removed before Drupal 9.0.0. Use the appropriate
* @link sanitization sanitization functions @endlink or the @link theme_render theme and render systems @endlink
* so that the output can can be themed, escaped, and altered properly.
*
* @see TwigExtension::escapeFilter()
* @see twig_render_template()
@ -32,100 +25,23 @@ namespace Drupal\Component\Utility;
*/
class SafeMarkup {
/**
* The list of safe strings.
*
* Strings in this list are marked as secure for the entire page render, not
* just the code or element that set it. Therefore, only valid HTML should be
* marked as safe (never partial markup). For example, you should never mark
* string such as '<' or '<script>' safe.
*
* @var array
*/
protected static $safeStrings = array();
/**
* Checks if a string is safe to output.
*
* @param string|\Drupal\Component\Utility\SafeStringInterface $string
* @param string|\Drupal\Component\Render\MarkupInterface $string
* The content to be checked.
* @param string $strategy
* The escaping strategy. Defaults to 'html'. Two escaping strategies are
* supported by default:
* - 'html': (default) The string is safe for use in HTML code.
* - 'all': The string is safe for all use cases.
* See the
* @link http://twig.sensiolabs.org/doc/filters/escape.html Twig escape documentation @endlink
* for more information on escaping strategies in Twig.
* (optional) This value is ignored.
*
* @return bool
* TRUE if the string has been marked secure, FALSE otherwise.
*
* @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
* Instead, you should just check if a variable is an instance of
* \Drupal\Component\Render\MarkupInterface.
*/
public static function isSafe($string, $strategy = 'html') {
// Do the instanceof checks first to save unnecessarily casting the object
// to a string.
return $string instanceOf SafeStringInterface || isset(static::$safeStrings[(string) $string][$strategy]) ||
isset(static::$safeStrings[(string) $string]['all']);
}
/**
* Adds previously retrieved known safe strings to the safe string list.
*
* This method is for internal use. Do not use it to prevent escaping of
* markup; instead, use the appropriate
* @link sanitization sanitization functions @endlink or the
* @link theme_render theme and render systems @endlink so that the output
* can be themed, escaped, and altered properly.
*
* This marks strings as secure for the entire page render, not just the code
* or element that set it. Therefore, only valid HTML should be
* marked as safe (never partial markup). For example, you should never do:
* @code
* SafeMarkup::setMultiple(['<' => ['html' => TRUE]]);
* @endcode
* or:
* @code
* SafeMarkup::setMultiple(['<script>' => ['all' => TRUE]]);
* @endcode
* @param array $safe_strings
* A list of safe strings as previously retrieved by self::getAll().
* Every string in this list will be represented by a multidimensional
* array in which the keys are the string and the escaping strategy used for
* this string, and in which the value is the boolean TRUE.
* See self::isSafe() for the list of supported escaping strategies.
*
* @throws \UnexpectedValueException
*
* @internal This is called by FormCache, StringTranslation and the Batch API.
* It should not be used anywhere else.
*/
public static function setMultiple(array $safe_strings) {
foreach ($safe_strings as $string => $strategies) {
foreach ($strategies as $strategy => $value) {
$string = (string) $string;
if ($value === TRUE) {
static::$safeStrings[$string][$strategy] = TRUE;
}
else {
// Danger - something is very wrong.
throw new \UnexpectedValueException('Only the value TRUE is accepted for safe strings');
}
}
}
}
/**
* Gets all strings currently marked as safe.
*
* This is useful for the batch and form APIs, where it is important to
* preserve the safe markup state across page requests.
*
* @return array
* An array of strings currently marked safe.
*/
public static function getAll() {
return static::$safeStrings;
return $string instanceOf MarkupInterface;
}
/**
@ -137,13 +53,10 @@ class SafeMarkup {
* @param string $text
* The text to be checked or processed.
*
* @return string
* An HTML safe version of $text, or an empty string if $text is not valid
* UTF-8.
* @return \Drupal\Component\Render\HtmlEscapedText
* An HtmlEscapedText object that escapes when rendered to string.
*
* @ingroup sanitization
*
* @deprecated Will be removed before Drupal 8.0.0. Rely on Twig's
* @deprecated Will be removed before Drupal 9.0.0. Rely on Twig's
* auto-escaping feature, or use the @link theme_render #plain_text @endlink
* key when constructing a render array that contains plain text in order to
* use the renderer's auto-escaping feature. If neither of these are
@ -153,91 +66,32 @@ class SafeMarkup {
* @see drupal_validate_utf8()
*/
public static function checkPlain($text) {
$string = Html::escape($text);
static::$safeStrings[$string]['html'] = TRUE;
return $string;
return new HtmlEscapedText($text);
}
/**
* Formats a string for HTML display by replacing variable placeholders.
*
* This function replaces variable placeholders in a string with the requested
* values and escapes the values so they can be safely displayed as HTML. It
* should be used on any unknown text that is intended to be printed to an
* HTML page (especially text that may have come from untrusted users, since
* in that case it prevents cross-site scripting and other security problems).
*
* In most cases, you should use t() rather than calling this function
* directly, since it will translate the text (on non-English-only sites) in
* addition to formatting it.
*
* @param string $string
* A string containing placeholders. The string itself is not escaped, any
* unsafe content must be in $args and inserted via placeholders.
* A string containing placeholders. The string itself will not be escaped,
* any unsafe content must be in $args and inserted via placeholders.
* @param array $args
* An associative array of replacements to make. Occurrences in $string of
* any key in $args are replaced with the corresponding value, after
* optional sanitization and formatting. The type of sanitization and
* formatting depends on the first character of the key:
* - @variable: Escaped to HTML using self::escape(). Use this as the
* default choice for anything displayed on a page on the site.
* - %variable: Escaped to HTML wrapped in <em> tags, which makes the
* following HTML code:
* @code
* <em class="placeholder">text output here.</em>
* @endcode
* - !variable: Inserted as is, with no sanitization or formatting. Only
* use this when the resulting string is being generated for one of:
* - Non-HTML usage, such as a plain-text email.
* - Non-direct HTML output, such as a plain-text variable that will be
* printed as an HTML attribute value and therefore formatted with
* self::checkPlain() as part of that.
* - Some other special reason for suppressing sanitization.
* An array with placeholder replacements, keyed by placeholder. See
* \Drupal\Component\Render\FormattableMarkup::placeholderFormat() for
* additional information about placeholders.
*
* @return string
* The formatted string, which is marked as safe unless sanitization of an
* unsafe argument was suppressed (see above).
* @return string|\Drupal\Component\Render\MarkupInterface
* The formatted string, which is an instance of MarkupInterface unless
* sanitization of an unsafe argument was suppressed (see above).
*
* @ingroup sanitization
* @see \Drupal\Component\Render\FormattableMarkup::placeholderFormat()
* @see \Drupal\Component\Render\FormattableMarkup
*
* @see t()
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
* Use \Drupal\Component\Render\FormattableMarkup.
*/
public static function format($string, array $args) {
$safe = TRUE;
// Transform arguments before inserting them.
foreach ($args as $key => $value) {
switch ($key[0]) {
case '@':
// Escaped only.
if (!SafeMarkup::isSafe($value)) {
$args[$key] = Html::escape($value);
}
break;
case '%':
default:
// Escaped and placeholder.
if (!SafeMarkup::isSafe($value)) {
$value = Html::escape($value);
}
$args[$key] = '<em class="placeholder">' . $value . '</em>';
break;
case '!':
// Pass-through.
if (!static::isSafe($value)) {
$safe = FALSE;
}
}
}
$output = strtr($string, $args);
if ($safe) {
static::$safeStrings[$output]['html'] = TRUE;
}
return $output;
return new FormattableMarkup($string, $args);
}
}

View file

@ -1,45 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\Component\Utility\SafeStringInterface.
*/
namespace Drupal\Component\Utility;
/**
* Marks an object's __toString() method as returning safe markup.
*
* All objects that implement this interface should be marked @internal.
*
* This interface should only be used on objects that emit known safe strings
* from their __toString() method. If there is any risk of the method returning
* user-entered data that has not been filtered first, it must not be used.
*
* If the object is going to be used directly in Twig templates it should
* implement \Countable so it can be used in if statements.
*
* @internal
* This interface is marked as internal because it should only be used by
* objects used during rendering. This interface should be used by modules if
* they interrupt the render pipeline and explicitly deal with SafeString
* objects created by the render system. Additionally, if a module reuses the
* regular render pipeline internally and passes processed data into it. For
* example, Views implements a custom render pipeline in order to render JSON
* and to fast render fields.
*
* @see \Drupal\Component\Utility\SafeStringTrait
* @see \Drupal\Component\Utility\SafeMarkup::isSafe()
* @see \Drupal\Core\Template\TwigExtension::escapeFilter()
*/
interface SafeStringInterface extends \JsonSerializable {
/**
* Returns a safe string.
*
* @return string
* The safe string.
*/
public function __toString();
}

View file

@ -0,0 +1,48 @@
<?php
/**
* @file
* Contains \Drupal\Component\Utility\ToStringTrait.
*/
namespace Drupal\Component\Utility;
/**
* Wraps __toString in a trait to avoid some fatals.
*/
trait ToStringTrait {
/**
* Implements the magic __toString() method.
*/
public function __toString() {
try {
return (string) $this->render();
}
catch (\Exception $e) {
// User errors in __toString() methods are considered fatal in the Drupal
// error handler.
trigger_error(get_class($e) . ' thrown while calling __toString on a ' . get_class($this) . ' object in ' . $e->getFile() . ' on line ' . $e->getLine() . ': ' . $e->getMessage(), E_USER_ERROR);
// In case we are using another error handler that did not fatal on the
// E_USER_ERROR, we terminate execution. However, for test purposes allow
// a return value.
return $this->_die();
}
}
/**
* For test purposes, wrap die() in an overridable method.
*/
protected function _die() {
die();
}
/**
* Renders the object as a string.
*
* @return string|object
* The rendered string or an object implementing __toString().
*/
abstract public function render();
}

View file

@ -412,8 +412,8 @@ EOD;
// Find the starting byte offset.
$bytes = 0;
if ($start > 0) {
// Count all the continuation bytes from the start until we have found
// $start characters or the end of the string.
// Count all the characters except continuation bytes from the start
// until we have found $start characters or the end of the string.
$bytes = -1; $chars = -1;
while ($bytes < $strlen - 1 && $chars < $start) {
$bytes++;
@ -424,8 +424,8 @@ EOD;
}
}
elseif ($start < 0) {
// Count all the continuation bytes from the end until we have found
// abs($start) characters.
// Count all the characters except continuation bytes from the end
// until we have found abs($start) characters.
$start = abs($start);
$bytes = $strlen; $chars = 0;
while ($bytes > 0 && $chars < $start) {
@ -443,9 +443,9 @@ EOD;
$iend = $strlen;
}
elseif ($length > 0) {
// Count all the continuation bytes from the starting index until we have
// found $length characters or reached the end of the string, then
// backtrace one byte.
// Count all the characters except continuation bytes from the starting
// index until we have found $length characters or reached the end of
// the string, then backtrace one byte.
$iend = $istart - 1;
$chars = -1;
$last_real = FALSE;
@ -458,15 +458,15 @@ EOD;
$last_real = TRUE;
}
}
// Backtrace one byte if the last character we found was a real character
// and we don't need it.
// Backtrace one byte if the last character we found was a real
// character and we don't need it.
if ($last_real && $chars >= $length) {
$iend--;
}
}
elseif ($length < 0) {
// Count all the continuation bytes from the end until we have found
// abs($start) characters, then backtrace one byte.
// Count all the characters except continuation bytes from the end
// until we have found abs($start) characters, then backtrace one byte.
$length = abs($length);
$iend = $strlen; $chars = 0;
while ($iend > 0 && $chars < $length) {
@ -697,4 +697,33 @@ EOD;
return (preg_match('/^./us', $text) == 1);
}
/**
* Finds the position of the first occurrence of a string in another string.
*
* @param string $haystack
* The string to search in.
* @param string $needle
* The string to find in $haystack.
* @param int $offset
* If specified, start the search at this number of characters from the
* beginning (default 0).
*
* @return int|false
* The position where $needle occurs in $haystack, always relative to the
* beginning (independent of $offset), or FALSE if not found. Note that
* a return value of 0 is not the same as FALSE.
*/
public static function strpos($haystack, $needle, $offset = 0) {
if (static::getStatus() == static::STATUS_MULTIBYTE) {
return mb_strpos($haystack, $needle, $offset);
}
else {
// Remove Unicode continuation characters, to be compatible with
// Unicode::strlen() and Unicode::substr().
$haystack = preg_replace("/[\x80-\xBF]/", '', $haystack);
$needle = preg_replace("/[\x80-\xBF]/", '', $needle);
return strpos($haystack, $needle, $offset);
}
}
}

View file

@ -304,7 +304,22 @@ class UrlHelper {
* \Drupal\Component\Utility\Xss::filter(), but those functions return an
* HTML-encoded string, so this function can be called independently when the
* output needs to be a plain-text string for passing to functions that will
* call \Drupal\Component\Utility\SafeMarkup::checkPlain() separately.
* call Html::escape() separately. The exact behavior depends on the value:
* - If the value is a well-formed (per RFC 3986) relative URL or
* absolute URL that does not use a dangerous protocol (like
* "javascript:"), then the URL remains unchanged. This includes all
* URLs generated via Url::toString() and UrlGeneratorTrait::url().
* - If the value is a well-formed absolute URL with a dangerous protocol,
* the protocol is stripped. This process is repeated on the remaining URL
* until it is stripped down to a safe protocol.
* - If the value is not a well-formed URL, the same sanitization behavior as
* for well-formed URLs will be invoked, which strips most substrings that
* precede a ":". The result can be used in URL attributes such as "href"
* or "src" (only after calling Html::escape() separately), but this may not
* produce valid HTML (e.g., malformed URLs within "href" attributes fail
* HTML validation). This can be avoided by using
* Url::fromUri($possibly_not_a_url)->toString(), which either throws an
* exception or returns a well-formed URL.
*
* @param string $uri
* A plain-text URI that might contain dangerous protocols.
@ -314,6 +329,11 @@ class UrlHelper {
* strings, this return value must not be output to an HTML page without
* being sanitized first. However, it can be passed to functions
* expecting plain-text strings.
*
* @see \Drupal\Component\Utility\Html::escape()
* @see \Drupal\Core\Url::toString()
* @see \Drupal\Core\Routing\UrlGeneratorTrait::url()
* @see \Drupal\Core\Url::fromUri()
*/
public static function stripDangerousProtocols($uri) {
$allowed_protocols = array_flip(static::$allowedProtocols);

View file

@ -106,7 +106,7 @@ class Xss {
*
* Use only for fields where it is impractical to use the
* whole filter system, but where some (mainly inline) mark-up
* is desired (so \Drupal\Component\Utility\SafeMarkup::checkPlain() is
* is desired (so \Drupal\Component\Utility\Html::escape() is
* not acceptable).
*
* Allows all tags that can be used inside an HTML body, save

View file

@ -0,0 +1,52 @@
<?php
/**
* @file
* Contains \Drupal\Core\Ajax\BaseCommand.
*/
namespace Drupal\Core\Ajax;
/**
* Base command that only exists to simplify AJAX commands.
*/
class BaseCommand implements CommandInterface {
/**
* The name of the command.
*
* @var string
*/
protected $command;
/**
* The data to pass on to the client side.
*
* @var string
*/
protected $data;
/**
* Constructs a BaseCommand object.
*
* @param string $command
* The name of the command.
* @param string $data
* The data to pass on to the client side.
*/
public function __construct($command, $data) {
$this->command = $command;
$this->data = $data;
}
/**
* {@inheritdoc}
*/
public function render() {
return array(
'command' => $this->command,
'data' => $this->data,
);
}
}

View file

@ -29,7 +29,7 @@ trait CommandWithAttachedAssetsTrait {
* If content is a render array, it may contain attached assets to be
* processed.
*
* @return string|\Drupal\Component\Utility\SafeStringInterface
* @return string|\Drupal\Component\Render\MarkupInterface
* HTML rendered content.
*/
protected function getRenderedContent() {

View file

@ -8,7 +8,7 @@
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* @defgroup plugin_context Annotation for context definition
@ -113,7 +113,7 @@ class ContextDefinition extends Plugin {
// used in the classes they pass to.
foreach (['label', 'description'] as $key) {
// @todo Remove this workaround in https://www.drupal.org/node/2362727.
if (isset($values[$key]) && $values[$key] instanceof TranslationWrapper) {
if (isset($values[$key]) && $values[$key] instanceof TranslatableMarkup) {
$values[$key] = (string) $values[$key]->get();
}
else {

View file

@ -8,7 +8,7 @@
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\AnnotationBase;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* @defgroup plugin_translatable Annotation for translatable text
@ -60,7 +60,7 @@ class Translation extends AnnotationBase {
/**
* The string translation object.
*
* @var \Drupal\Core\StringTranslation\TranslationWrapper
* @var \Drupal\Core\StringTranslation\TranslatableMarkup
*/
protected $translation;
@ -86,7 +86,7 @@ class Translation extends AnnotationBase {
'context' => $values['context'],
);
}
$this->translation = new TranslationWrapper($string, $arguments, $options);
$this->translation = new TranslatableMarkup($string, $arguments, $options);
}
/**

File diff suppressed because it is too large Load diff

View file

@ -166,6 +166,7 @@ class AssetResolver implements AssetResolverInterface {
uasort($css, 'static::sort');
// Allow themes to remove CSS files by CSS files full path and file name.
// @todo Remove in Drupal 9.0.x.
if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) {
foreach ($css as $key => $options) {
if (isset($stylesheet_remove[$key])) {

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException.
*/
namespace Drupal\Core\Asset\Exception;
/**
* Defines a custom exception for an invalid libraries-extend specification.
*/
class InvalidLibrariesExtendSpecificationException extends \RuntimeException {
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException.
*/
namespace Drupal\Core\Asset\Exception;
/**
* Defines a custom exception if a definition refers to a non-existent library.
*/
class InvalidLibrariesOverrideSpecificationException extends \RuntimeException {
}

View file

@ -9,8 +9,6 @@ namespace Drupal\Core\Asset;
use Drupal\Core\Cache\CacheCollectorInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* Discovers available asset libraries in Drupal.
@ -87,6 +85,8 @@ class LibraryDiscovery implements LibraryDiscoveryInterface {
*/
public function clearCachedDefinitions() {
$this->cacheTagInvalidator->invalidateTags(['library_info']);
$this->libraryDefinitions = [];
$this->collector->clear();
}
}

View file

@ -7,6 +7,9 @@
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException;
use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Lock\LockBackendInterface;
@ -79,9 +82,94 @@ class LibraryDiscoveryCollector extends CacheCollector {
* {@inheritdoc}
*/
protected function resolveCacheMiss($key) {
$this->storage[$key] = $this->discoveryParser->buildByExtension($key);
$this->storage[$key] = $this->getLibraryDefinitions($key);
$this->persist($key);
return $this->storage[$key];
}
/**
* Returns the library definitions for a given extension.
*
* This also implements libraries-overrides for entire libraries that have
* been specified by the LibraryDiscoveryParser.
*
* @param string $extension
* The name of the extension for which library definitions will be returned.
*
* @return array
* The library definitions for $extension with overrides applied.
*
* @throws \Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException
*/
protected function getLibraryDefinitions($extension) {
$libraries = $this->discoveryParser->buildByExtension($extension);
foreach ($libraries as $name => $definition) {
// Handle libraries that are marked for override or removal.
// @see \Drupal\Core\Asset\LibraryDiscoveryParser::applyLibrariesOverride()
if (isset($definition['override'])) {
if ($definition['override'] === FALSE) {
// Remove the library definition if FALSE is given.
unset($libraries[$name]);
}
else {
// Otherwise replace with existing library definition if it exists.
// Throw an exception if it doesn't.
list($replacement_extension, $replacement_name) = explode('/', $definition['override']);
$replacement_definition = $this->get($replacement_extension);
if (isset($replacement_definition[$replacement_name])) {
$libraries[$name] = $replacement_definition[$replacement_name];
}
else {
throw new InvalidLibrariesOverrideSpecificationException(sprintf('The specified library %s does not exist.', $definition['override']));
}
}
}
else {
// If libraries are not overridden, then apply libraries-extend.
$libraries[$name] = $this->applyLibrariesExtend($extension, $name, $definition);
}
}
return $libraries;
}
/**
* Applies the libraries-extend specified by the active theme.
*
* This extends the library definitions with the those specified by the
* libraries-extend specifications for the active theme.
*
* @param string $extension
* The name of the extension for which library definitions will be extended.
* @param string $library_name
* The name of the library whose definitions is to be extended.
* @param $library_definition
* The library definition to be extended.
*
* @return array
* The library definition extended as specified by libraries-extend.
*
* @throws \Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException
*/
protected function applyLibrariesExtend($extension, $library_name, $library_definition) {
$libraries_extend = $this->themeManager->getActiveTheme()->getLibrariesExtend();
if (!empty($libraries_extend["$extension/$library_name"])) {
foreach ($libraries_extend["$extension/$library_name"] as $library_extend_name) {
if (!is_string($library_extend_name)) {
// Only string library names are allowed.
throw new InvalidLibrariesExtendSpecificationException('The libraries-extend specification for each library must be a list of strings.');
}
list($new_extension, $new_library_name) = explode('/', $library_extend_name, 2);
$new_libraries = $this->get($new_extension);
if (isset($new_libraries[$new_library_name])) {
$library_definition = NestedArray::mergeDeep($library_definition, $new_libraries[$new_library_name]);
}
else {
throw new InvalidLibrariesExtendSpecificationException(sprintf('The specified library "%s" does not exist.', $library_extend_name));
}
}
}
return $library_definition;
}
}

View file

@ -8,8 +8,10 @@
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException;
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
@ -88,6 +90,7 @@ class LibraryDiscoveryParser {
}
$libraries = $this->parseLibraryInfo($extension, $path);
$libraries = $this->applyLibrariesOverride($libraries, $extension);
foreach ($libraries as $id => &$library) {
if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings'])) {
@ -185,6 +188,13 @@ class LibraryDiscoveryParser {
elseif ($this->fileValidUri($source)) {
$options['data'] = $source;
}
// A regular URI (e.g., http://example.com/example.js) without
// 'external' explicitly specified, which may happen if, e.g.
// libraries-override is used.
elseif ($this->isValidUri($source)) {
$options['type'] = 'external';
$options['data'] = $source;
}
// By default, file paths are relative to the registering extension.
else {
$options['data'] = $path . '/' . $source;
@ -313,6 +323,70 @@ class LibraryDiscoveryParser {
return $libraries;
}
/**
* Apply libraries overrides specified for the current active theme.
*
* @param array $libraries
* The libraries definitions.
* @param string $extension
* The extension in which these libraries are defined.
*
* @return array
* The modified libraries definitions.
*/
protected function applyLibrariesOverride($libraries, $extension) {
$active_theme = $this->themeManager->getActiveTheme();
// ActiveTheme::getLibrariesOverride() returns libraries-overrides for the
// current theme as well as all its base themes.
$all_libraries_overrides = $active_theme->getLibrariesOverride();
foreach ($all_libraries_overrides as $theme_path => $libraries_overrides) {
foreach ($libraries as $library_name => $library) {
// Process libraries overrides.
if (isset($libraries_overrides["$extension/$library_name"])) {
// Active theme defines an override for this library.
$override_definition = $libraries_overrides["$extension/$library_name"];
if (is_string($override_definition) || $override_definition === FALSE) {
// A string or boolean definition implies an override (or removal)
// for the whole library. Use the override key to specify that this
// library will be overridden when it is called.
// @see \Drupal\Core\Asset\LibraryDiscovery::getLibraryByName()
if ($override_definition) {
$libraries[$library_name]['override'] = $override_definition;
}
else {
$libraries[$library_name]['override'] = FALSE;
}
}
elseif (is_array($override_definition)) {
// An array definition implies an override for an asset within this
// library.
foreach ($override_definition as $sub_key => $value) {
// Throw an exception if the asset is not properly specified.
if (!is_array($value)) {
throw new InvalidLibrariesOverrideSpecificationException(sprintf('Library asset %s is not correctly specified. It should be in the form "extension/library_name/sub_key/path/to/asset.js".', "$extension/$library_name/$sub_key"));
}
if ($sub_key === 'drupalSettings') {
// drupalSettings may not be overridden.
throw new InvalidLibrariesOverrideSpecificationException(sprintf('drupalSettings may not be overridden in libraries-override. Trying to override %s. Use hook_library_info_alter() instead.', "$extension/$library_name/$sub_key"));
}
elseif ($sub_key === 'css') {
// SMACSS category should be incorporated into the asset name.
foreach ($value as $category => $overrides) {
$this->setOverrideValue($libraries[$library_name], [$sub_key, $category], $overrides, $theme_path);
}
}
else {
$this->setOverrideValue($libraries[$library_name], [$sub_key], $value, $theme_path);
}
}
}
}
}
}
return $libraries;
}
/**
* Wraps drupal_get_path().
*/
@ -327,4 +401,67 @@ class LibraryDiscoveryParser {
return file_valid_uri($source);
}
/**
* Determines if the supplied string is a valid URI.
*/
protected function isValidUri($string) {
return count(explode('://', $string)) === 2;
}
/**
* Overrides the specified library asset.
*
* @param array $library
* The containing library definition.
* @param array $sub_key
* An array containing the sub-keys specifying the library asset, e.g.
* @code['js']@endcode or @code['css', 'component']@endcode
* @param array $overrides
* Specifies the overrides, this is an array where the key is the asset to
* be overridden while the value is overriding asset.
*/
protected function setOverrideValue(array &$library, array $sub_key, array $overrides, $theme_path) {
foreach ($overrides as $original => $replacement) {
// Get the attributes of the asset to be overridden. If the key does
// not exist, then throw an exception.
$key_exists = NULL;
$parents = array_merge($sub_key, [$original]);
// Save the attributes of the library asset to be overridden.
$attributes = NestedArray::getValue($library, $parents, $key_exists);
if ($key_exists) {
// Remove asset to be overridden.
NestedArray::unsetValue($library, $parents);
// No need to replace if FALSE is specified, since that is a removal.
if ($replacement) {
// Ensure the replacement path is relative to drupal root.
$replacement = $this->resolveThemeAssetPath($theme_path, $replacement);
$new_parents = array_merge($sub_key, [$replacement]);
// Replace with an override if specified.
NestedArray::setValue($library, $new_parents, $attributes);
}
}
}
}
/**
* Ensures that a full path is returned for an overriding theme asset.
*
* @param string $theme_path
* The theme or base theme.
* @param string $overriding_asset
* The overriding library asset.
*
* @return string
* A fully resolved theme asset path relative to the Drupal directory.
*/
protected function resolveThemeAssetPath($theme_path, $overriding_asset) {
if ($overriding_asset[0] !== '/' && !$this->isValidUri($overriding_asset)) {
// The destination is not an absolute path and it's not a URI (e.g.
// public://generated_js/example.js or http://example.com/js/my_js.js), so
// it's relative to the theme.
return '/' . $theme_path . '/' . $overriding_asset;
}
return $overriding_asset;
}
}

View file

@ -9,13 +9,12 @@ namespace Drupal\Core\Block;
use Drupal\block\BlockInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Transliteration\TransliterationInterface;
@ -30,6 +29,8 @@ use Drupal\Component\Transliteration\TransliterationInterface;
*/
abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface {
use ContextAwarePluginAssignmentTrait;
/**
* The transliteration service.
*
@ -47,7 +48,7 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
$definition = $this->getPluginDefinition();
// Cast the admin label to a string since it is an object.
// @see \Drupal\Core\StringTranslation\TranslationWrapper
// @see \Drupal\Core\StringTranslation\TranslatableMarkup
return (string) $definition['admin_label'];
}
@ -89,10 +90,6 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
'label' => '',
'provider' => $this->pluginDefinition['provider'],
'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
'cache' => array(
// Blocks are cacheable by default.
'max_age' => Cache::PERMANENT,
),
);
}
@ -180,24 +177,10 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
'#default_value' => ($this->configuration['label_display'] === BlockInterface::BLOCK_LABEL_VISIBLE),
'#return_value' => BlockInterface::BLOCK_LABEL_VISIBLE,
);
// Identical options to the ones for page caching.
// @see \Drupal\system\Form\PerformanceForm::buildForm()
$period = array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400);
$period = array_map(array(\Drupal::service('date.formatter'), 'formatInterval'), array_combine($period, $period));
$period[0] = '<' . $this->t('no caching') . '>';
$period[\Drupal\Core\Cache\Cache::PERMANENT] = $this->t('Forever');
$form['cache'] = array(
'#type' => 'details',
'#title' => $this->t('Cache settings'),
);
$form['cache']['max_age'] = array(
'#type' => 'select',
'#title' => $this->t('Maximum age'),
'#description' => $this->t('The maximum time this block may be cached.'),
'#default_value' => $this->configuration['cache']['max_age'],
'#options' => $period,
);
// Add context mapping UI form elements.
$contexts = $form_state->getTemporaryValue('gathered_contexts') ?: [];
$form['context_mapping'] = $this->addContextAssignmentElement($this, $contexts);
// Add plugin-specific settings for this block type.
$form += $this->blockForm($form, $form_state);
return $form;
@ -244,7 +227,6 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
$this->configuration['label'] = $form_state->getValue('label');
$this->configuration['label_display'] = $form_state->getValue('label_display');
$this->configuration['provider'] = $form_state->getValue('provider');
$this->configuration['cache'] = $form_state->getValue('cache');
$this->blockSubmit($form, $form_state);
}
}
@ -272,16 +254,6 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
return $transliterated;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
$max_age = parent::getCacheMaxAge();
// @todo Configurability of this will be removed in
// https://www.drupal.org/node/2458763.
return Cache::mergeMaxAges($max_age, (int) $this->configuration['cache']['max_age']);
}
/**
* Wraps the transliteration service.
*

View file

@ -10,7 +10,7 @@ namespace Drupal\Core\Block;
/**
* The interface for "main page content" blocks.
*
* A main page content block represents the content returns by the controller.
* A main page content block represents the content returned by the controller.
*
* @ingroup block_api
*/

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Block\PageTitleBlock.
*/
namespace Drupal\Core\Block\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\TitleBlockPluginInterface;
/**
* Provides a block to display the page title.
*
* @Block(
* id = "page_title_block",
* admin_label = @Translation("Page title"),
* )
*/
class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface {
/**
* The page title: a string (plain title) or a render array (formatted title).
*
* @var string|array
*/
protected $title = '';
/**
* {@inheritdoc}
*/
public function setTitle($title) {
$this->title = $title;
return $this;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return ['label_display' => FALSE];
}
/**
* {@inheritdoc}
*/
public function build() {
return [
'#type' => 'page_title',
'#title' => $this->title,
];
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Block\TitleBlockPluginInterface.
*/
namespace Drupal\Core\Block;
/**
* The interface for "title" blocks.
*
* A title block shows the title returned by the controller.
*
* @ingroup block_api
*
* @see \Drupal\Core\Render\Element\PageTitle
*/
interface TitleBlockPluginInterface extends BlockPluginInterface {
/**
* Sets the title.
*
* @param string|array $title
* The page title: either a string for plain titles or a render array for
* formatted titles.
*/
public function setTitle($title);
}

View file

@ -7,15 +7,17 @@
namespace Drupal\Core\Breadcrumb;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Link;
use Drupal\Core\Render\RenderableInterface;
/**
* Used to return generated breadcrumbs with associated cacheability metadata.
*
* @todo implement RenderableInterface once https://www.drupal.org/node/2529560 lands.
*/
class Breadcrumb extends CacheableMetadata {
class Breadcrumb implements RenderableInterface, RefinableCacheableDependencyInterface {
use RefinableCacheableDependencyTrait;
/**
* An ordered list of links for the breadcrumb.
@ -68,4 +70,24 @@ class Breadcrumb extends CacheableMetadata {
return $this;
}
/**
* {@inheritdoc}
*/
public function toRenderable() {
$build = [
'#cache' => [
'contexts' => $this->cacheContexts,
'tags' => $this->cacheTags,
'max-age' => $this->cacheMaxAge,
],
];
if (!empty($this->links)) {
$build += [
'#theme' => 'breadcrumb',
'#links' => $this->links,
];
}
return $build;
}
}

View file

@ -166,7 +166,7 @@ class ApcuBackend implements CacheBackendInterface {
* {@inheritdoc}
*/
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array()) {
Cache::validateTags($tags);
assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache tags must be strings.');
$tags = array_unique($tags);
$cache = new \stdClass();
$cache->cid = $cid;

View file

@ -34,7 +34,7 @@ class Cache {
*/
public static function mergeContexts(array $a = [], array $b = []) {
$cache_contexts = array_unique(array_merge($a, $b));
\Drupal::service('cache_contexts_manager')->validateTokens($cache_contexts);
assert('\Drupal::service(\'cache_contexts_manager\')->assertValidTokens($cache_contexts)');
sort($cache_contexts);
return $cache_contexts;
}
@ -59,8 +59,9 @@ class Cache {
* The merged array of cache tags.
*/
public static function mergeTags(array $a = [], array $b = []) {
assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($a) && \Drupal\Component\Assertion\Inspector::assertAllStrings($b)', 'Cache tags must be valid strings');
$cache_tags = array_unique(array_merge($a, $b));
static::validateTags($cache_tags);
sort($cache_tags);
return $cache_tags;
}
@ -99,6 +100,9 @@ class Cache {
* @param string[] $tags
* An array of cache tags.
*
* @deprecated
* Use assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)');
*
* @throws \LogicException
*/
public static function validateTags(array $tags) {

View file

@ -115,7 +115,7 @@ abstract class CacheCollector implements CacheCollectorInterface, DestructableIn
* (optional) The tags to specify for the cache item.
*/
public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, array $tags = array()) {
Cache::validateTags($tags);
assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache tags must be strings.');
$this->cid = $cid;
$this->cache = $cache;
$this->tags = $tags;

View file

@ -27,8 +27,7 @@ class CacheTagsInvalidator implements CacheTagsInvalidatorInterface {
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
// Validate the tags.
Cache::validateTags($tags);
assert('Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache tags must be strings.');
// Notify all added cache tags invalidators.
foreach ($this->invalidators as $invalidator) {

View file

@ -103,11 +103,9 @@ class CacheContextsManager {
* The ContextCacheKeys object containing the converted cache keys and
* cacheability metadata.
*
* @throws \LogicException
* Thrown if any of the context tokens or parameters are not valid.
*/
public function convertTokensToKeys(array $context_tokens) {
$this->validateTokens($context_tokens);
assert('$this->assertValidTokens($context_tokens)');
$cacheable_metadata = new CacheableMetadata();
$optimized_tokens = $this->optimizeTokens($context_tokens);
// Iterate over cache contexts that have been optimized away and get their
@ -299,4 +297,33 @@ class CacheContextsManager {
}
}
/**
* Asserts the context tokens are valid
*
* Similar to ::validateTokens, this method returns boolean TRUE when the
* context tokens are valid, and FALSE when they are not instead of returning
* NULL when they are valid and throwing a \LogicException when they are not.
* This function should be used with the assert() statement.
*
* @param mixed $context_tokens
* Variable to be examined - should be array of context_tokens.
*
* @return bool
* TRUE if context_tokens is an array of valid tokens.
*/
public function assertValidTokens($context_tokens) {
if (!is_array($context_tokens)) {
return FALSE;
}
try {
$this->validateTokens($context_tokens);
}
catch (\LogicException $e) {
return FALSE;
}
return TRUE;
}
}

View file

@ -199,7 +199,7 @@ class DatabaseBackend implements CacheBackendInterface {
'tags' => array(),
);
Cache::validateTags($item['tags']);
assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($item[\'tags\'])', 'Cache Tags must be strings.');
$item['tags'] = array_unique($item['tags']);
// Sort the cache tags so that they are stored consistently in the DB.
sort($item['tags']);

View file

@ -107,7 +107,7 @@ class MemoryBackend implements CacheBackendInterface, CacheTagsInvalidatorInterf
* Implements Drupal\Core\Cache\CacheBackendInterface::set().
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) {
Cache::validateTags($tags);
assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache Tags must be strings.');
$tags = array_unique($tags);
// Sort the cache tags so that they are stored consistently in the database.
sort($tags);

View file

@ -148,7 +148,7 @@ class PhpBackend implements CacheBackendInterface {
* {@inheritdoc}
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) {
Cache::validateTags($tags);
assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache Tags must be strings.');
$item = (object) array(
'cid' => $cid,
'data' => $data,

View file

@ -33,6 +33,27 @@ trait RefinableCacheableDependencyTrait {
*/
protected $cacheMaxAge = Cache::PERMANENT;
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->cacheTags;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->cacheContexts;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->cacheMaxAge;
}
/**
* {@inheritdoc}
*/

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\Core\Command\DbCommandBase.
*/
namespace Drupal\Core\Command;
use Drupal\Core\Database\Database;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
/**
* Base command that abstracts handling of database connection arguments.
*/
class DbCommandBase extends Command {
/**
* {@inheritdoc}
*/
protected function configure() {
$this->addOption('database', NULL, InputOption::VALUE_OPTIONAL, 'The database connection name to use.', 'default')
->addOption('database-url', 'db-url', InputOption::VALUE_OPTIONAL, 'A database url to parse and use as the database connection.')
->addOption('prefix', NULL, InputOption::VALUE_OPTIONAL, 'Override or set the table prefix used in the database connection.');
}
/**
* Parse input options decide on a database.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* Input object.
* @return \Drupal\Core\Database\Connection
*/
protected function getDatabaseConnection(InputInterface $input) {
// Load connection from a url.
if ($input->getOption('database-url')) {
// @todo this could probably be refactored to not use a global connection.
// Ensure database connection isn't set.
if (Database::getConnectionInfo('db-tools')) {
throw new \RuntimeException('Database "db-tools" is already defined. Cannot define database provided.');
}
$info = Database::convertDbUrlToConnectionInfo($input->getOption('database-url'), \Drupal::root());
Database::addConnectionInfo('db-tools', 'default', $info);
$key = 'db-tools';
}
else {
$key = $input->getOption('database');
}
// If they supplied a prefix, replace it in the connection information.
$prefix = $input->getOption('prefix');
if ($prefix) {
$info = Database::getConnectionInfo($key)['default'];
$info['prefix']['default'] = $prefix;
Database::removeConnection($key);
Database::addConnectionInfo($key, 'default', $info);
}
return Database::getConnection('default', $key);
}
}

View file

@ -7,8 +7,6 @@
namespace Drupal\Core\Command;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
@ -17,34 +15,6 @@ use Symfony\Component\Console\Input\InputInterface;
*/
class DbDumpApplication extends Application {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Construct the application.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
function __construct(Connection $connection, ModuleHandlerInterface $module_handler) {
$this->connection = $connection;
$this->moduleHandler = $module_handler;
parent::__construct();
}
/**
* {@inheritdoc}
*/
@ -58,7 +28,7 @@ class DbDumpApplication extends Application {
protected function getDefaultCommands() {
// Even though this is a single command, keep the HelpCommand (--help).
$default_commands = parent::getDefaultCommands();
$default_commands[] = new DbDumpCommand($this->connection, $this->moduleHandler);
$default_commands[] = new DbDumpCommand();
return $default_commands;
}

View file

@ -9,9 +9,8 @@ namespace Drupal\Core\Command;
use Drupal\Component\Utility\Variable;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
@ -30,21 +29,7 @@ use Symfony\Component\Console\Output\OutputInterface;
*
* @see \Drupal\Core\Command\DbDumpApplication
*/
class DbDumpCommand extends Command {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection $connection
*/
protected $connection;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
class DbDumpCommand extends DbCommandBase {
/**
* An array of table patterns to exclude completely.
@ -55,81 +40,78 @@ class DbDumpCommand extends Command {
*/
protected $excludeTables = ['simpletest.+'];
/**
* Table patterns for which to only dump the schema, no data.
*
* @var array
*/
protected $schemaOnly = ['cache.*', 'sessions', 'watchdog'];
/**
* Construct the database dump command.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to use.
*/
function __construct(Connection $connection, ModuleHandlerInterface $module_handler) {
// Check this is MySQL.
if ($connection->databaseType() !== 'mysql') {
throw new \RuntimeException('This script can only be used with MySQL database backends.');
}
$this->connection = $connection;
$this->moduleHandler = $module_handler;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure() {
$this->setName('dump-database-d8-mysql')
->setDescription('Dump the current database to a generation script');
->setDescription('Dump the current database to a generation script')
->addOption('schema-only', NULL, InputOption::VALUE_OPTIONAL, 'A comma separated list of tables to only export the schema without data.', 'cache.*,sessions,watchdog');
parent::configure();
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$connection = $this->getDatabaseConnection($input);
// If not explicitly set, disable ANSI which will break generated php.
if ($input->hasParameterOption(['--ansi']) !== TRUE) {
$output->setDecorated(FALSE);
}
$output->writeln($this->generateScript());
$schema_tables = $input->getOption('schema-only');
$schema_tables = explode(',', $schema_tables);
$output->writeln($this->generateScript($connection, $schema_tables), OutputInterface::OUTPUT_RAW);
}
/**
* Generates the database script.
*
* @return string
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param array $schema_only
* Table patterns for which to only dump the schema, no data.
* @return string The PHP script.
* The PHP script.
*/
protected function generateScript() {
protected function generateScript(Connection $connection, array $schema_only = []) {
$tables = '';
foreach ($this->getTables() as $table) {
$schema = $this->getTableSchema($table);
$data = $this->getTableData($table);
$schema_only_patterns = [];
foreach ($schema_only as $match) {
$schema_only_patterns[] = '/^' . $match . '$/';
}
foreach ($this->getTables($connection) as $table) {
$schema = $this->getTableSchema($connection, $table);
// Check for schema only.
if (empty($schema_only_patterns) || preg_replace($schema_only_patterns, '', $table)) {
$data = $this->getTableData($connection, $table);
}
else {
$data = [];
}
$tables .= $this->getTableScript($table, $schema, $data);
}
$script = $this->getTemplate();
// Substitute in the tables.
$script = str_replace('{{TABLES}}', trim($tables), $script);
// Modules.
$script = str_replace('{{MODULES}}', $this->getModulesScript(), $script);
return trim($script);
}
/**
* Returns a list of tables, not including those set to be excluded.
*
* @return array
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @return array An array of table names.
* An array of table names.
*/
protected function getTables() {
$tables = array_values($this->connection->schema()->findTables('%'));
protected function getTables(Connection $connection) {
$tables = array_values($connection->schema()->findTables('%'));
foreach ($tables as $key => $table) {
// Remove any explicitly excluded tables.
@ -146,6 +128,8 @@ class DbDumpCommand extends Command {
/**
* Returns a schema array for a given table.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param string $table
* The table name.
*
@ -154,14 +138,19 @@ class DbDumpCommand extends Command {
*
* @todo This implementation is hard-coded for MySQL.
*/
protected function getTableSchema($table) {
$query = $this->connection->query("SHOW FULL COLUMNS FROM {" . $table . "}");
protected function getTableSchema(Connection $connection, $table) {
// Check this is MySQL.
if ($connection->databaseType() !== 'mysql') {
throw new \RuntimeException('This script can only be used with MySQL database backends.');
}
$query = $connection->query("SHOW FULL COLUMNS FROM {" . $table . "}");
$definition = [];
while (($row = $query->fetchAssoc()) !== FALSE) {
$name = $row['Field'];
// Parse out the field type and meta information.
preg_match('@([a-z]+)(?:\((\d+)(?:,(\d+))?\))?\s*(unsigned)?@', $row['Type'], $matches);
$type = $this->fieldTypeMap($matches[1]);
$type = $this->fieldTypeMap($connection, $matches[1]);
if ($row['Extra'] === 'auto_increment') {
// If this is an auto increment, then the type is 'serial'.
$type = 'serial';
@ -170,7 +159,7 @@ class DbDumpCommand extends Command {
'type' => $type,
'not null' => $row['Null'] === 'NO',
];
if ($size = $this->fieldSizeMap($matches[1])) {
if ($size = $this->fieldSizeMap($connection, $matches[1])) {
$definition['fields'][$name]['size'] = $size;
}
if (isset($matches[2]) && $type === 'numeric') {
@ -216,10 +205,10 @@ class DbDumpCommand extends Command {
}
// Set primary key, unique keys, and indexes.
$this->getTableIndexes($table, $definition);
$this->getTableIndexes($connection, $table, $definition);
// Set table collation.
$this->getTableCollation($table, $definition);
$this->getTableCollation($connection, $table, $definition);
return $definition;
}
@ -227,15 +216,17 @@ class DbDumpCommand extends Command {
/**
* Adds primary key, unique keys, and index information to the schema.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param string $table
* The table to find indexes for.
* @param array &$definition
* The schema definition to modify.
*/
protected function getTableIndexes($table, &$definition) {
protected function getTableIndexes(Connection $connection, $table, &$definition) {
// Note, this query doesn't support ordering, so that is worked around
// below by keying the array on Seq_in_index.
$query = $this->connection->query("SHOW INDEX FROM {" . $table . "}");
$query = $connection->query("SHOW INDEX FROM {" . $table . "}");
while (($row = $query->fetchAssoc()) !== FALSE) {
$index_name = $row['Key_name'];
$column = $row['Column_name'];
@ -262,13 +253,15 @@ class DbDumpCommand extends Command {
/**
* Set the table collation.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param string $table
* The table to find indexes for.
* @param array &$definition
* The schema definition to modify.
*/
protected function getTableCollation($table, &$definition) {
$query = $this->connection->query("SHOW TABLE STATUS LIKE '{" . $table . "}'");
protected function getTableCollation(Connection $connection, $table, &$definition) {
$query = $connection->query("SHOW TABLE STATUS LIKE '{" . $table . "}'");
$data = $query->fetchAssoc();
// Set `mysql_character_set`. This will be ignored by other backends.
@ -280,21 +273,17 @@ class DbDumpCommand extends Command {
*
* If a table is set to be schema only, and empty array is returned.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param string $table
* The table to query.
*
* @return array
* The data from the table as an array.
*/
protected function getTableData($table) {
// Check for schema only.
foreach ($this->schemaOnly as $schema_only) {
if (preg_match('/^' . $schema_only . '$/', $table)) {
return [];
}
}
$order = $this->getFieldOrder($table);
$query = $this->connection->query("SELECT * FROM {" . $table . "} " . $order );
protected function getTableData(Connection $connection, $table) {
$order = $this->getFieldOrder($connection, $table);
$query = $connection->query("SELECT * FROM {" . $table . "} " . $order);
$results = [];
while (($row = $query->fetchAssoc()) !== FALSE) {
$results[] = $row;
@ -305,6 +294,8 @@ class DbDumpCommand extends Command {
/**
* Given a database field type, return a Drupal type.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param string $type
* The MySQL field type.
*
@ -312,9 +303,9 @@ class DbDumpCommand extends Command {
* The Drupal schema field type. If there is no mapping, the original field
* type is returned.
*/
protected function fieldTypeMap($type) {
protected function fieldTypeMap(Connection $connection, $type) {
// Convert everything to lowercase.
$map = array_map('strtolower', $this->connection->schema()->getFieldTypeMap());
$map = array_map('strtolower', $connection->schema()->getFieldTypeMap());
$map = array_flip($map);
// The MySql map contains type:size. Remove the size part.
@ -324,15 +315,17 @@ class DbDumpCommand extends Command {
/**
* Given a database field type, return a Drupal size.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param string $type
* The MySQL field type.
*
* @return string
* The Drupal schema field size.
*/
protected function fieldSizeMap($type) {
protected function fieldSizeMap(Connection $connection, $type) {
// Convert everything to lowercase.
$map = array_map('strtolower', $this->connection->schema()->getFieldTypeMap());
$map = array_map('strtolower', $connection->schema()->getFieldTypeMap());
$map = array_flip($map);
$schema_type = explode(':', $map[$type])[0];
@ -346,24 +339,26 @@ class DbDumpCommand extends Command {
/**
* Gets field ordering for a given table.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection to use.
* @param string $table
* The table name.
*
* @return string
* The order string to append to the query.
*/
protected function getFieldOrder($table) {
protected function getFieldOrder(Connection $connection, $table) {
// @todo this is MySQL only since there are no Database API functions for
// table column data.
// @todo this code is duplicated in `core/scripts/migrate-db.sh`.
$connection_info = $this->connection->getConnectionOptions();
$connection_info = $connection->getConnectionOptions();
// Order by primary keys.
$order = '';
$query = "SELECT `COLUMN_NAME` FROM `information_schema`.`COLUMNS`
WHERE (`TABLE_SCHEMA` = '" . $connection_info['database'] . "')
AND (`TABLE_NAME` = '{" . $table . "}') AND (`COLUMN_KEY` = 'PRI')
ORDER BY COLUMN_NAME";
$results = $this->connection->query($query);
$results = $connection->query($query);
while (($row = $results->fetchAssoc()) !== FALSE) {
$order .= $row['COLUMN_NAME'] . ', ';
}
@ -384,12 +379,9 @@ class DbDumpCommand extends Command {
<?php
/**
* @file
* Filled installation of Drupal 8.0, for test purposes.
* A database agnostic dump for testing purposes.
*
* This file was generated by the dump-database-d8.php script, from an
* installation of Drupal 8. It has the following modules installed:
*
{{MODULES}}
* This file was generated by the Drupal 8.0 db-tools.php script.
*/
use Drupal\Core\Database\Database;
@ -431,20 +423,4 @@ ENDOFSCRIPT;
return $output;
}
/**
* List of modules enabled for insertion into the script docblock.
*
* @return string
* The formatted list of enabled modules.
*/
protected function getModulesScript() {
$output = '';
$modules = $this->moduleHandler->getModuleList();
ksort($modules);
foreach ($modules as $module => $filename) {
$output .= " * - $module\n";
}
return rtrim($output, "\n");
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains \Drupal\Core\Command\DbDumpCommand.
*/
namespace Drupal\Core\Command;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\SchemaObjectExistsException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Provides a command to import the current database from a script.
*
* This script runs on databases exported using using one of the database dump
* commands and imports it into the current database connection.
*
* @see \Drupal\Core\Command\DbImportApplication
*/
class DbImportCommand extends DbCommandBase {
/**
* {@inheritdoc}
*/
protected function configure() {
parent::configure();
$this->setName('import')
->setDescription('Import database from a generation script.')
->addArgument('script', InputOption::VALUE_REQUIRED, 'Import script');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$script = $input->getArgument('script');
if (!is_file($script)) {
$output->writeln('File must exist.');
return;
}
$connection = $this->getDatabaseConnection($input);
$this->runScript($connection, $script);
$output->writeln('Import completed successfully.');
}
/**
* Run the database script.
*
* @param \Drupal\Core\Database\Connection $connection
* Connection used by the script when included.
* @param string $script
* Path to dump script.
*/
protected function runScript(Connection $connection, $script) {
$old_key = Database::setActiveConnection($connection->getKey());
if (substr($script, -3) == '.gz') {
$script = "compress.zlib://$script";
}
try {
require $script;
}
catch (SchemaObjectExistsException $e) {
throw new \RuntimeException('An existing Drupal installation exists at this location. Try removing all tables or changing the database prefix in your settings.php file.');
}
Database::setActiveConnection($old_key);
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\Core\Command\DbToolsApplication.
*/
namespace Drupal\Core\Command;
use Symfony\Component\Console\Application;
/**
* Provides a command to import a database generation script.
*/
class DbToolsApplication extends Application {
/**
* {@inheritdoc}
*/
public function __construct() {
parent::__construct('Database Tools', '8.0.x');
}
/**
* {@inheritdoc}
*/
protected function getDefaultCommands() {
$default_commands = parent::getDefaultCommands();
$default_commands[] = new DbDumpCommand();
$default_commands[] = new DbImportCommand();
return $default_commands;
}
}

View file

@ -48,7 +48,15 @@ class BootstrapConfigStorageFactory {
/**
* Returns a File-based configuration storage implementation.
*
* If there is no active configuration directory calling this method will
* result in an error.
*
* @return \Drupal\Core\Config\FileStorage
*
* @deprecated in Drupal 8.0.x and will be removed before 9.0.0. Drupal core
* no longer creates an active directory.
*
* @throws \Exception
*/
public static function getFileStorage() {
return new FileStorage(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));

View file

@ -108,8 +108,8 @@ class Config extends StorableConfigBase {
/**
* {@inheritdoc}
*/
public function setData(array $data, $validate_keys = TRUE) {
parent::setData($data, $validate_keys);
public function setData(array $data) {
parent::setData($data);
$this->resetOverriddenData();
return $this;
}

View file

@ -8,6 +8,7 @@
namespace Drupal\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
@ -153,10 +154,6 @@ abstract class ConfigBase implements RefinableCacheableDependencyInterface {
*
* @param array $data
* The new configuration data.
* @param bool $validate_keys
* (optional) Whether the data should be verified for valid keys. Set to
* FALSE if the $data is known to be valid already (for example, being
* loaded from the config storage).
*
* @return $this
* The configuration object.
@ -164,10 +161,9 @@ abstract class ConfigBase implements RefinableCacheableDependencyInterface {
* @throws \Drupal\Core\Config\ConfigValueException
* If any key in $data in any depth contains a dot.
*/
public function setData(array $data, $validate_keys = TRUE) {
if ($validate_keys) {
$this->validateKeys($data);
}
public function setData(array $data) {
$data = $this->castSafeStrings($data);
$this->validateKeys($data);
$this->data = $data;
return $this;
}
@ -187,6 +183,7 @@ abstract class ConfigBase implements RefinableCacheableDependencyInterface {
* If $value is an array and any of its keys in any depth contains a dot.
*/
public function set($key, $value) {
$value = $this->castSafeStrings($value);
// The dot/period is a reserved character; it may appear between keys, but
// not within keys.
if (is_array($value)) {
@ -280,4 +277,27 @@ abstract class ConfigBase implements RefinableCacheableDependencyInterface {
return $this->cacheMaxAge;
}
/**
* Casts any objects that implement MarkupInterface to string.
*
* @param mixed $data
* The configuration data.
*
* @return mixed
* The data with any safe strings cast to string.
*/
protected function castSafeStrings($data) {
if ($data instanceof MarkupInterface) {
$data = (string) $data;
}
else if (is_array($data)) {
array_walk_recursive($data, function (&$value) {
if ($value instanceof MarkupInterface) {
$value = (string) $value;
}
});
}
return $data;
}
}

View file

@ -367,8 +367,8 @@ class ConfigImporter {
$current_extensions = $this->storageComparer->getTargetStorage()->read('core.extension');
$new_extensions = $this->storageComparer->getSourceStorage()->read('core.extension');
// If there is no extension information in staging then exit. This is
// probably due to an empty staging directory.
// If there is no extension information in sync then exit. This is probably
// due to an empty sync directory.
if (!$new_extensions) {
return;
}
@ -718,11 +718,11 @@ class ConfigImporter {
$old_entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
$new_entity_type_id = $this->configManager->getEntityTypeIdByName($names['new_name']);
if ($old_entity_type_id != $new_entity_type_id) {
$this->logError($this->t('Entity type mismatch on rename. !old_type not equal to !new_type for existing configuration !old_name and staged configuration !new_name.', array('old_type' => $old_entity_type_id, 'new_type' => $new_entity_type_id, 'old_name' => $names['old_name'], 'new_name' => $names['new_name'])));
$this->logError($this->t('Entity type mismatch on rename. @old_type not equal to @new_type for existing configuration @old_name and staged configuration @new_name.', array('@old_type' => $old_entity_type_id, '@new_type' => $new_entity_type_id, '@old_name' => $names['old_name'], '@new_name' => $names['new_name'])));
}
// Has to be a configuration entity.
if (!$old_entity_type_id) {
$this->logError($this->t('Rename operation for simple configuration. Existing configuration !old_name and staged configuration !new_name.', array('old_name' => $names['old_name'], 'new_name' => $names['new_name'])));
$this->logError($this->t('Rename operation for simple configuration. Existing configuration @old_name and staged configuration @new_name.', array('@old_name' => $names['old_name'], '@new_name' => $names['new_name'])));
}
}
$this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this));
@ -762,7 +762,7 @@ class ConfigImporter {
}
}
catch (\Exception $e) {
$this->logError($this->t('Unexpected error during import with operation @op for @name: !message', array('@op' => $op, '@name' => $name, '!message' => $e->getMessage())));
$this->logError($this->t('Unexpected error during import with operation @op for @name: @message', array('@op' => $op, '@name' => $name, '@message' => $e->getMessage())));
// Error for that operation was logged, mark it as processed so that
// the import can continue.
$this->setProcessedConfiguration($collection, $op, $name);
@ -780,7 +780,7 @@ class ConfigImporter {
* The name of the extension to process.
*/
protected function processExtension($type, $op, $name) {
// Set the config installer to use the staging directory instead of the
// Set the config installer to use the sync directory instead of the
// extensions own default config directories.
\Drupal::service('config.installer')
->setSyncing(TRUE)

View file

@ -155,11 +155,15 @@ class ConfigManager implements ConfigManagerInterface {
// Check for new or removed files.
if ($source_data === array('false')) {
// Added file.
$source_data = array($this->t('File added'));
// Cast the result of t() to a string, as the diff engine doesn't know
// about objects.
$source_data = array((string) $this->t('File added'));
}
if ($target_data === array('false')) {
// Deleted file.
$target_data = array($this->t('File removed'));
// Cast the result of t() to a string, as the diff engine doesn't know
// about objects.
$target_data = array((string) $this->t('File removed'));
}
return new Diff($source_data, $target_data);
@ -316,7 +320,8 @@ class ConfigManager implements ConfigManagerInterface {
}
if ($this->callOnDependencyRemoval($dependent, $original_dependencies, $type, $names)) {
// Recalculate dependencies and update the dependency graph data.
$dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->calculateDependencies());
$dependent->calculateDependencies();
$dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->getDependencies());
// Based on the updated data rebuild the list of dependents.
$dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
// Ensure that the dependency has actually been fixed. It is possible
@ -454,6 +459,9 @@ class ConfigManager implements ConfigManagerInterface {
if (isset($config_data['dependencies']['content'])) {
$content_dependencies = array_merge($content_dependencies, $config_data['dependencies']['content']);
}
if (isset($config_data['dependencies']['enforced']['content'])) {
$content_dependencies = array_merge($content_dependencies, $config_data['dependencies']['enforced']['content']);
}
}
foreach (array_unique($content_dependencies) as $content_dependency) {
// Format of the dependency is entity_type:bundle:uuid.

View file

@ -53,8 +53,7 @@ trait ConfigDependencyDeleteFormTrait {
'#type' => 'details',
'#title' => $this->t('Configuration updates'),
'#description' => $this->t('The listed configuration will be updated.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#open' => TRUE,
'#access' => FALSE,
);
@ -72,7 +71,7 @@ trait ConfigDependencyDeleteFormTrait {
'#items' => array(),
);
}
$form['entity_updates'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id();
$form['entity_updates'][$entity_type_id]['#items'][$entity->id()] = $entity->label() ?: $entity->id();
}
if (!empty($dependent_entities['update'])) {
$form['entity_updates']['#access'] = TRUE;
@ -83,7 +82,7 @@ trait ConfigDependencyDeleteFormTrait {
foreach ($entity_types as $entity_type_id => $label) {
$form['entity_updates'][$entity_type_id]['#weight'] = $weight;
// Sort the list of entity labels alphabetically.
sort($form['entity_updates'][$entity_type_id]['#items'], SORT_FLAG_CASE);
ksort($form['entity_updates'][$entity_type_id]['#items'], SORT_FLAG_CASE);
$weight++;
}
}
@ -92,8 +91,7 @@ trait ConfigDependencyDeleteFormTrait {
'#type' => 'details',
'#title' => $this->t('Configuration deletions'),
'#description' => $this->t('The listed configuration will be deleted.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#open' => TRUE,
'#access' => FALSE,
);
@ -110,7 +108,7 @@ trait ConfigDependencyDeleteFormTrait {
'#items' => array(),
);
}
$form['entity_deletes'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id();
$form['entity_deletes'][$entity_type_id]['#items'][$entity->id()] = $entity->label() ?: $entity->id();
}
if (!empty($dependent_entities['delete'])) {
$form['entity_deletes']['#access'] = TRUE;
@ -119,10 +117,12 @@ trait ConfigDependencyDeleteFormTrait {
asort($entity_types, SORT_FLAG_CASE);
$weight = 0;
foreach ($entity_types as $entity_type_id => $label) {
$form['entity_deletes'][$entity_type_id]['#weight'] = $weight;
// Sort the list of entity labels alphabetically.
sort($form['entity_deletes'][$entity_type_id]['#items'], SORT_FLAG_CASE);
$weight++;
if (isset($form['entity_deletes'][$entity_type_id])) {
$form['entity_deletes'][$entity_type_id]['#weight'] = $weight;
// Sort the list of entity labels alphabetically.
ksort($form['entity_deletes'][$entity_type_id]['#items'], SORT_FLAG_CASE);
$weight++;
}
}
}

View file

@ -58,11 +58,11 @@ use Drupal\Component\Utility\SortArray;
* Configuration entity classes usually extend
* \Drupal\Core\Config\Entity\ConfigEntityBase. The base class provides a
* generic implementation of the calculateDependencies() method that can
* discover dependencies due to enforced dependencies, plugins, and third party
* settings. If the configuration entity has dependencies that cannot be
* discovered by the base class's implementation, then it needs to implement
* discover dependencies due to plugins, and third party settings. If the
* configuration entity has dependencies that cannot be discovered by the base
* class's implementation, then it needs to implement
* \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies() to
* calculate (and return) the dependencies. In this method, use
* calculate the dependencies. In this method, use
* \Drupal\Core\Config\Entity\ConfigEntityBase::addDependency() to add
* dependencies. Implementations should call the base class implementation to
* inherit the generic functionality.
@ -85,9 +85,9 @@ use Drupal\Component\Utility\SortArray;
* configuration object so that they can be checked without the module that
* provides the configuration entity class being installed. This is important
* for configuration synchronization, which needs to be able to validate
* configuration in the staging directory before the synchronization has
* occurred. Also, if you have a configuration entity object and you want to
* get the current dependencies without recalculation, you can use
* configuration in the sync directory before the synchronization has occurred.
* Also, if you have a configuration entity object and you want to get the
* current dependencies (without recalculation), you can use
* \Drupal\Core\Config\Entity\ConfigEntityInterface::getDependencies().
*
* When uninstalling a module or a theme, configuration entities that are
@ -115,6 +115,7 @@ use Drupal\Component\Utility\SortArray;
* module dependency in the sub-module only.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::getDependencies()
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval()
* @see \Drupal\Core\Config\Entity\ConfigEntityBase::addDependency()
* @see \Drupal\Core\Config\ConfigInstallerInterface::installDefaultConfig()

View file

@ -7,6 +7,7 @@
namespace Drupal\Core\Config\Entity;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
@ -341,7 +342,7 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
throw new ConfigDuplicateUUIDException("Attempt to save a configuration entity '{$this->id()}' with UUID '{$this->uuid()}' when this entity already exists with UUID '{$original->uuid()}'");
}
}
if (!$this->isSyncing() && !$this->trustedData) {
if (!$this->isSyncing()) {
// Ensure the correct dependencies are present. If the configuration is
// being written during a configuration synchronization then there is no
// need to recalculate the dependencies.
@ -353,16 +354,9 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
* {@inheritdoc}
*/
public function calculateDependencies() {
// Dependencies should be recalculated on every save. This ensures stale
// dependencies are never saved.
if (isset($this->dependencies['enforced'])) {
$dependencies = $this->dependencies['enforced'];
$this->dependencies = $dependencies;
$this->dependencies['enforced'] = $dependencies;
}
else {
$this->dependencies = array();
}
// All dependencies should be recalculated on every save apart from enforced
// dependencies. This ensures stale dependencies are never saved.
$this->dependencies = array_intersect_key($this->dependencies, ['enforced' => '']);
if ($this instanceof EntityWithPluginCollectionInterface) {
// Configuration entities need to depend on the providers of any plugins
// that they store the configuration for.
@ -379,13 +373,15 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
$this->addDependency('module', $provider);
}
}
return $this->dependencies;
return $this;
}
/**
* {@inheritdoc}
*/
public function urlInfo($rel = 'edit-form', array $options = []) {
// Unless language was already provided, avoid setting an explicit language.
$options += ['language' => NULL];
return parent::urlInfo($rel, $options);
}
@ -436,7 +432,14 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
* {@inheritdoc}
*/
public function getDependencies() {
return $this->dependencies;
$dependencies = $this->dependencies;
if (isset($dependencies['enforced'])) {
// Merge the enforced dependencies into the list of dependencies.
$enforced_dependencies = $dependencies['enforced'];
unset($dependencies['enforced']);
$dependencies = NestedArray::mergeDeep($dependencies, $enforced_dependencies);
}
return $dependencies;
}
/**

View file

@ -7,8 +7,7 @@
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Config\ConfigNameException;
use Drupal\Core\Entity\EntityStorageInterface;
/**
@ -19,31 +18,6 @@ use Drupal\Core\Entity\EntityStorageInterface;
*/
abstract class ConfigEntityBundleBase extends ConfigEntityBase {
/**
* Renames displays when a bundle is renamed.
*/
protected function renameDisplays() {
// Rename entity displays.
if ($this->getOriginalId() !== $this->id()) {
foreach ($this->loadDisplays('entity_view_display') as $display) {
$new_id = $this->getEntityType()->getBundleOf() . '.' . $this->id() . '.' . $display->getMode();
$display->set('id', $new_id);
$display->setTargetBundle($this->id());
$display->save();
}
}
// Rename entity form displays.
if ($this->getOriginalId() !== $this->id()) {
foreach ($this->loadDisplays('entity_form_display') as $form_display) {
$new_id = $this->getEntityType()->getBundleOf() . '.' . $this->id() . '.' . $form_display->getMode();
$form_display->set('id', $new_id);
$form_display->setTargetBundle($this->id());
$form_display->save();
}
}
}
/**
* Deletes display if a bundle is deleted.
*/
@ -80,12 +54,6 @@ abstract class ConfigEntityBundleBase extends ConfigEntityBase {
}
// Entity bundle field definitions may depend on bundle settings.
$entity_manager->clearCachedFieldDefinitions();
if ($this->getOriginalId() != $this->id()) {
// If the entity was renamed, update the displays.
$this->renameDisplays();
$entity_manager->onBundleRename($this->getOriginalId(), $this->id(), $bundle_of);
}
}
}
@ -101,6 +69,33 @@ abstract class ConfigEntityBundleBase extends ConfigEntityBase {
}
}
/**
* Acts on an entity before the presave hook is invoked.
*
* Used before the entity is saved and before invoking the presave hook.
*
* Ensure that config entities which are bundles of other entities cannot have
* their ID changed.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage object.
*
* @throws \Drupal\Core\Config\ConfigNameException
* Thrown when attempting to rename a bundle entity.
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Only handle renames, not creations.
if (!$this->isNew() && $this->getOriginalId() !== $this->id()) {
$bundle_type = $this->getEntityType();
$bundle_of = $bundle_type->getBundleOf();
if (!empty($bundle_of)) {
throw new ConfigNameException("The machine name of the '{$bundle_type->getLabel()}' bundle cannot be changed.");
}
}
}
/**
* Returns view or form displays for this bundle.
*

View file

@ -7,6 +7,8 @@
namespace Drupal\Core\Config\Entity;
use Drupal\Component\Utility\NestedArray;
/**
* Provides a value object to discover configuration dependencies.
*
@ -26,7 +28,7 @@ class ConfigEntityDependency {
*
* @var array
*/
protected $dependencies;
protected $dependencies = [];
/**
* Constructs the configuration entity dependency from the entity values.
@ -36,13 +38,16 @@ class ConfigEntityDependency {
* @param array $values
* (optional) The configuration entity's values.
*/
public function __construct($name, $values = array()) {
public function __construct($name, $values = []) {
$this->name = $name;
if (isset($values['dependencies'])) {
$this->dependencies = $values['dependencies'];
if (isset($values['dependencies']) && isset($values['dependencies']['enforced'])) {
// Merge the enforced dependencies into the list of dependencies.
$enforced_dependencies = $values['dependencies']['enforced'];
unset($values['dependencies']['enforced']);
$this->dependencies = NestedArray::mergeDeep($values['dependencies'], $enforced_dependencies);
}
else {
$this->dependencies = array();
elseif (isset($values['dependencies'])) {
$this->dependencies = $values['dependencies'];
}
}

View file

@ -144,8 +144,7 @@ interface ConfigEntityInterface extends EntityInterface, ThirdPartySettingsInter
/**
* Calculates dependencies and stores them in the dependency property.
*
* @return array
* An array of dependencies grouped by type (module, theme, entity).
* @return $this
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
*/

View file

@ -16,18 +16,21 @@ class FileStorageFactory {
* Returns a FileStorage object working with the active config directory.
*
* @return \Drupal\Core\Config\FileStorage FileStorage
*
* @deprecated in Drupal 8.0.x and will be removed before 9.0.0. Drupal core
* no longer creates an active directory.
*/
static function getActive() {
return new FileStorage(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
}
/**
* Returns a FileStorage object working with the staging config directory.
* Returns a FileStorage object working with the sync config directory.
*
* @return \Drupal\Core\Config\FileStorage FileStorage
*/
static function getStaging() {
return new FileStorage(config_get_config_directory(CONFIG_STAGING_DIRECTORY));
static function getSync() {
return new FileStorage(config_get_config_directory(CONFIG_SYNC_DIRECTORY));
}
}

View file

@ -7,27 +7,10 @@
namespace Drupal\Core\Config\Schema;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\TypedData\TypedData;
/**
* Defines a generic configuration element that contains multiple properties.
*/
abstract class ArrayElement extends TypedData implements \IteratorAggregate, TypedConfigInterface {
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfig;
/**
* The configuration value.
*
* @var mixed
*/
protected $value;
abstract class ArrayElement extends Element implements \IteratorAggregate, TypedConfigInterface {
/**
* Parsed elements.
@ -152,7 +135,7 @@ abstract class ArrayElement extends TypedData implements \IteratorAggregate, Typ
* @return \Drupal\Core\TypedData\TypedDataInterface
*/
protected function createElement($definition, $value, $key) {
return $this->typedConfig->create($definition, $value, $key, $this);
return $this->getTypedDataManager()->create($definition, $value, $key, $this);
}
/**
@ -170,20 +153,17 @@ abstract class ArrayElement extends TypedData implements \IteratorAggregate, Typ
* @return \Drupal\Core\TypedData\DataDefinitionInterface
*/
protected function buildDataDefinition($definition, $value, $key) {
return $this->typedConfig->buildDataDefinition($definition, $value, $key, $this);
return $this->getTypedDataManager()->buildDataDefinition($definition, $value, $key, $this);
}
/**
* Sets the typed config manager on the instance.
* Determines if this element allows NULL as a value.
*
* This must be called immediately after construction to enable
* self::parseElement() and self::buildDataDefinition() to work.
*
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* @return bool
* TRUE if NULL is a valid value, FALSE otherwise.
*/
public function setTypedConfig(TypedConfigManagerInterface $typed_config) {
$this->typedConfig = $typed_config;
public function isNullable() {
return isset($this->definition['nullable']) && $this->definition['nullable'] == TRUE;
}
}

View file

@ -7,7 +7,9 @@
namespace Drupal\Core\Config\Schema;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\TypedData\TypedData;
use Drupal\Core\TypedData\TypedDataManagerInterface;
/**
* Defines a generic configuration element.
@ -21,4 +23,41 @@ abstract class Element extends TypedData {
*/
protected $value;
/**
* Gets the typed configuration manager.
*
* Overrides \Drupal\Core\TypedData\TypedDataTrait::getTypedDataManager() to
* ensure the typed configuration manager is returned.
*
* @return \Drupal\Core\Config\TypedConfigManagerInterface
* The typed configuration manager.
*/
public function getTypedDataManager() {
if (empty($this->typedDataManager)) {
$this->setTypedDataManager(\Drupal::service('config.typed'));
}
return $this->typedDataManager;
}
/**
* Sets the typed config manager.
*
* Overrides \Drupal\Core\TypedData\TypedDataTrait::setTypedDataManager() to
* ensure that only typed configuration manager can be used.
*
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
* The typed config manager. This must be an instance of
* \Drupal\Core\Config\TypedConfigManagerInterface. If it is not, then this
* method will error when assertions are enabled. We can not narrow the
* typehint as this will cause PHP errors.
*
* @return $this
*/
public function setTypedDataManager(TypedDataManagerInterface $typed_data_manager) {
assert($typed_data_manager instanceof TypedConfigManagerInterface, '$typed_data_manager should be an instance of \Drupal\Core\Config\TypedConfigManagerInterface.');
$this->typedDataManager = $typed_data_manager;
return $this;
}
}

View file

@ -106,9 +106,13 @@ trait SchemaCheckTrait {
(($type == 'double' || $type == 'integer') && $element instanceof FloatInterface) ||
($type == 'boolean' && $element instanceof BooleanInterface) ||
($type == 'string' && $element instanceof StringInterface) ||
// Null values are allowed for all types.
// Null values are allowed for all primitive types.
($value === NULL);
}
// Array elements can also opt-in for allowing a NULL value.
elseif ($element instanceof ArrayElement && $element->isNullable() && $value === NULL) {
$success = TRUE;
}
$class = get_class($element);
if (!$success) {
return array($error_key => "variable type is $type but applied schema class is $class");

View file

@ -98,7 +98,7 @@ abstract class StorableConfigBase extends ConfigBase {
*/
public function initWithData(array $data) {
$this->isNew = FALSE;
$this->setData($data, FALSE);
$this->data = $data;
$this->originalData = $this->data;
return $this;
}

View file

@ -71,13 +71,7 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
/**
* Gets typed configuration data.
*
* @param string $name
* Configuration object name.
*
* @return \Drupal\Core\Config\Schema\TypedConfigInterface
* Typed configuration data.
* {@inheritdoc}
*/
public function get($name) {
$data = $this->configStorage->read($name);
@ -282,8 +276,12 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
return $value;
}
elseif (!$parts) {
$value = $data[$name];
if (is_bool($value)) {
$value = (int) $value;
}
// If no more parts left, this is the final property.
return (string)$data[$name];
return (string) $value;
}
else {
// Get nested value and continue processing.
@ -338,17 +336,4 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
}
}
/**
* {@inheritdoc}
*/
public function createInstance($data_type, array $configuration = array()) {
$instance = parent::createInstance($data_type, $configuration);
// Enable elements to construct their own definitions using the typed config
// manager.
if ($instance instanceof ArrayElement) {
$instance->setTypedConfig($this);
}
return $instance;
}
}

View file

@ -7,9 +7,7 @@
namespace Drupal\Core\Config;
use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
/**
* Defines an interface for managing config schema type plugins.
@ -19,7 +17,7 @@ use Drupal\Core\TypedData\DataDefinitionInterface;
* @see hook_config_schema_info_alter()
* @see https://www.drupal.org/node/1905070
*/
Interface TypedConfigManagerInterface extends PluginManagerInterface, CachedDiscoveryInterface {
Interface TypedConfigManagerInterface extends TypedDataManagerInterface {
/**
* Gets typed configuration data.
@ -32,48 +30,6 @@ Interface TypedConfigManagerInterface extends PluginManagerInterface, CachedDisc
*/
public function get($name);
/**
* Instantiates a typed configuration object.
*
* @param string $data_type
* The data type, for which a typed configuration object should be
* instantiated.
* @param array $configuration
* The plugin configuration array, i.e. an array with the following keys:
* - data definition: The data definition object, i.e. an instance of
* \Drupal\Core\TypedData\DataDefinitionInterface.
* - name: (optional) If a property or list item is to be created, the name
* of the property or the delta of the list item.
* - parent: (optional) If a property or list item is to be created, the
* parent typed data object implementing either the ListInterface or the
* ComplexDataInterface.
*
* @return \Drupal\Core\Config\Schema\Element
* The instantiated typed configuration object.
*/
public function createInstance($data_type, array $configuration = array());
/**
* Creates a new typed configuration object instance.
*
* @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
* The data definition of the typed data object.
* @param mixed $value
* The data value. If set, it has to match one of the supported
* data type format as documented for the data type classes.
* @param string $name
* (optional) If a property or list item is to be created, the name of the
* property or the delta of the list item.
* @param mixed $parent
* (optional) If a property or list item is to be created, the parent typed
* data object implementing either the ListInterface or the
* ComplexDataInterface.
*
* @return \Drupal\Core\Config\Schema\Element
* The instantiated typed data object.
*/
public function create(DataDefinitionInterface $definition, $value, $name = NULL, $parent = NULL);
/**
* Creates a new data definition object from a type definition array and
* actual configuration data. Since type definitions may contain variables

View file

@ -33,7 +33,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* @see \Drupal\Core\DependencyInjection\ContainerInjectionInterface
*
* @ingroup menu
* @ingroup routing
*/
abstract class ControllerBase implements ContainerInjectionInterface {

View file

@ -17,18 +17,20 @@ interface TitleResolverInterface {
/**
* Returns a static or dynamic title for the route.
*
* The returned title string must be safe to output in HTML. For example, an
* implementation should call \Drupal\Component\Utility\SafeMarkup::checkPlain()
* or \Drupal\Component\Utility\Xss::filterAdmin() on the string, or use
* appropriate placeholders to sanitize dynamic content inside a localized
* string before returning it. The title may contain HTML such as EM tags.
* If the returned title can contain HTML that should not be escaped it should
* return a render array, for example:
* @code
* ['#markup' => 'title', '#allowed_tags' => ['em']]
* @endcode
* If the method returns a string and it is not marked safe then it will be
* auto-escaped.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object passed to the title callback.
* @param \Symfony\Component\Routing\Route $route
* The route information of the route to fetch the title.
*
* @return string|null
* @return array|string|null
* The title for the route.
*/
public function getTitle(Request $request, Route $route);

View file

@ -24,7 +24,7 @@ use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass;
use Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterEventSubscribersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass;
use Drupal\Core\Plugin\PluginManagerPass;
@ -82,7 +82,7 @@ class CoreServiceProvider implements ServiceProviderInterface {
$container->addCompilerPass(new TwigExtensionPass());
// Add a compiler pass for registering event subscribers.
$container->addCompilerPass(new RegisterKernelListenersPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new RegisterEventSubscribersPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new RegisterAccessChecksPass());
$container->addCompilerPass(new RegisterLazyRouteEnhancers());

View file

@ -239,6 +239,12 @@ abstract class Connection {
* further up the call chain can take an appropriate action. To suppress
* that behavior and simply return NULL on failure, set this option to
* FALSE.
* - allow_delimiter_in_query: By default, queries which have the ; delimiter
* any place in them will cause an exception. This reduces the chance of SQL
* injection attacks that terminate the original query and add one or more
* additional queries (such as inserting new user accounts). In rare cases,
* such as creating an SQL function, a ; is needed and can be allowed by
* changing this option to TRUE.
*
* @return array
* An array of default query options.
@ -249,6 +255,7 @@ abstract class Connection {
'fetch' => \PDO::FETCH_OBJ,
'return' => Database::RETURN_STATEMENT,
'throw_exception' => TRUE,
'allow_delimiter_in_query' => FALSE,
);
}
@ -491,7 +498,7 @@ abstract class Connection {
return '';
// Flatten the array of comments.
$comment = implode('; ', $comments);
$comment = implode('. ', $comments);
// Sanitize the comment string so as to avoid SQL injection attacks.
return '/* ' . $this->filterComment($comment) . ' */ ';
@ -516,7 +523,7 @@ abstract class Connection {
*
* Would result in the following SQL statement being generated:
* @code
* "/ * Exploit * / DROP TABLE node; -- * / UPDATE example SET field2=..."
* "/ * Exploit * / DROP TABLE node. -- * / UPDATE example SET field2=..."
* @endcode
*
* Unless the comment is sanitised first, the SQL server would drop the
@ -529,7 +536,8 @@ abstract class Connection {
* A sanitized version of the query comment string.
*/
protected function filterComment($comment = '') {
return strtr($comment, ['*' => ' * ']);
// Change semicolons to period to avoid triggering multi-statement check.
return strtr($comment, ['*' => ' * ', ';' => '.']);
}
/**
@ -593,6 +601,16 @@ abstract class Connection {
}
else {
$this->expandArguments($query, $args);
// To protect against SQL injection, Drupal only supports executing one
// statement at a time. Thus, the presence of a SQL delimiter (the
// semicolon) is not allowed unless the option is set. Allowing
// semicolons should only be needed for special cases like defining a
// function or stored procedure in SQL. Trim any trailing delimiter to
// minimize false positives.
$query = rtrim($query, "; \t\n\r\0\x0B");
if (strpos($query, ';') !== FALSE && empty($options['allow_delimiter_in_query'])) {
throw new \InvalidArgumentException('; is not supported in SQL strings. Use only one statement at a time.');
}
$stmt = $this->prepareQuery($query);
$stmt->execute($args, $options);
}

View file

@ -72,7 +72,7 @@ class Tasks extends InstallTasks {
catch (\Exception $e) {
// Detect utf8mb4 incompability.
if ($e->getCode() == Connection::UNSUPPORTED_CHARSET) {
$this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href="@documentation" target="_blank">MySQL documentation</a> for more information.', array('@documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html')));
$this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href=":documentation" target="_blank">MySQL documentation</a> for more information.', array(':documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html')));
$info = Database::getConnectionInfo();
$info_copy = $info;
// Set a flag to fall back to utf8. Note: this flag should only be
@ -164,13 +164,13 @@ class Tasks extends InstallTasks {
// The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
$version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
if (version_compare($version, self::MYSQLND_MINIMUM_VERSION, '<')) {
$this->fail(t("The MySQLnd driver version %version is less than the minimum required version. Upgrade to MySQLnd version %mysqlnd_minimum_version or up, or alternatively switch mysql drivers to libmysqlclient version %libmysqlclient_minimum_version or up.", array('%version' => Database::getConnection()->version(), '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION)));
$this->fail(t("The MySQLnd driver version %version is less than the minimum required version. Upgrade to MySQLnd version %mysqlnd_minimum_version or up, or alternatively switch mysql drivers to libmysqlclient version %libmysqlclient_minimum_version or up.", array('%version' => $version, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION)));
}
}
else {
// The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
if (version_compare($version, self::LIBMYSQLCLIENT_MINIMUM_VERSION, '<')) {
$this->fail(t("The libmysqlclient driver version %version is less than the minimum required version. Upgrade to libmysqlclient version %libmysqlclient_minimum_version or up, or alternatively switch mysql drivers to MySQLnd version %mysqlnd_minimum_version or up.", array('%version' => Database::getConnection()->version(), '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION)));
$this->fail(t("The libmysqlclient driver version %version is less than the minimum required version. Upgrade to libmysqlclient version %libmysqlclient_minimum_version or up, or alternatively switch mysql drivers to MySQLnd version %mysqlnd_minimum_version or up.", array('%version' => $version, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION)));
}
}
}

View file

@ -539,7 +539,8 @@ class Schema extends DatabaseSchema {
// Add table prefixes before truncating.
$comment = Unicode::truncate($this->connection->prefixTables($comment), $length, TRUE, TRUE);
}
// Remove semicolons to avoid triggering multi-statement check.
$comment = strtr($comment, array(';' => '.'));
return $this->connection->quote($comment);
}

View file

@ -34,6 +34,10 @@ class Tasks extends InstallTasks {
'function' => 'checkBinaryOutput',
'arguments' => array(),
);
$this->tasks[] = array(
'function' => 'checkStandardConformingStrings',
'arguments' => array(),
);
$this->tasks[] = array(
'function' => 'initializeDatabase',
'arguments' => array(),
@ -118,10 +122,9 @@ class Tasks extends InstallTasks {
$this->pass(t('Database is encoded in UTF-8'));
}
else {
$this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See !link for more details.', array(
$this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See <a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a> for more details.', array(
'%encoding' => 'UTF8',
'%driver' => $this->name(),
'!link' => '<a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a>'
)));
}
}
@ -168,9 +171,9 @@ class Tasks extends InstallTasks {
'%setting' => 'bytea_output',
'%current_value' => 'hex',
'%needed_value' => 'escape',
'!query' => "<code>" . $query . "</code>",
'@query' => $query,
);
$this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: !query", $replacements));
$this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
}
}
}
@ -184,6 +187,58 @@ class Tasks extends InstallTasks {
return ($bytea_output == 'escape');
}
/**
* Ensures standard_conforming_strings setting is 'on'.
*
* When standard_conforming_strings setting is 'on' string literals ('...')
* treat backslashes literally, as specified in the SQL standard. This allows
* Drupal to convert between bytea, text and varchar columns.
*/
public function checkStandardConformingStrings() {
$database_connection = Database::getConnection();
if (!$this->checkStandardConformingStringsSuccess()) {
// First try to alter the database. If it fails, raise an error telling
// the user to do it themselves.
$connection_options = $database_connection->getConnectionOptions();
// It is safe to include the database name directly here, because this
// code is only called when a connection to the database is already
// established, thus the database name is guaranteed to be a correct
// value.
$query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET standard_conforming_strings = 'on';";
try {
$database_connection->query($query);
}
catch (\Exception $e) {
// Ignore possible errors when the user doesn't have the necessary
// privileges to ALTER the database.
}
// Close the database connection so that the configuration parameter
// is applied to the current connection.
Database::closeConnection();
// Recheck, if it fails, finally just rely on the end user to do the
// right thing.
if (!$this->checkStandardConformingStringsSuccess()) {
$replacements = array(
'%setting' => 'standard_conforming_strings',
'%current_value' => 'off',
'%needed_value' => 'on',
'@query' => $query,
);
$this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
}
}
}
/**
* Verifies the standard_conforming_strings setting.
*/
protected function checkStandardConformingStringsSuccess() {
$standard_conforming_strings = Database::getConnection()->query("SHOW standard_conforming_strings")->fetchField();
return ($standard_conforming_strings == 'on');
}
/**
* Make PostgreSQL Drupal friendly.
*/
@ -195,18 +250,23 @@ class Tasks extends InstallTasks {
// At the same time checking for the existence of the function fixes
// concurrency issues, when both try to update at the same time.
try {
$connection = Database::getConnection();
// Don't use {} around pg_proc table.
if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
db_query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
$connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
\'SELECT random();\'
LANGUAGE \'sql\''
LANGUAGE \'sql\'',
[],
[ 'allow_delimiter_in_query' => TRUE ]
);
}
if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) {
db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) {
$connection->query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
\'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
LANGUAGE \'sql\''
LANGUAGE \'sql\'',
[],
[ 'allow_delimiter_in_query' => TRUE ]
);
}

View file

@ -726,17 +726,23 @@ class Schema extends DatabaseSchema {
// Usually, we do this via a simple typecast 'USING fieldname::type'. But
// the typecast does not work for conversions to bytea.
// @see http://www.postgresql.org/docs/current/static/datatype-binary.html
$table_information = $this->queryTableInformation($table);
$is_bytea = !empty($table_information->blob_fields[$field]);
if ($spec['pgsql_type'] != 'bytea') {
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def);
if ($is_bytea) {
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING convert_from("' . $field . '"' . ", 'UTF8')");
}
else {
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def);
}
}
else {
// Do not attempt to convert a field that is bytea already.
$table_information = $this->queryTableInformation($table);
if (!in_array($field, $table_information->blob_fields)) {
if (!$is_bytea) {
// Convert to a bytea type by using the SQL replace() function to
// convert any single backslashes in the field content to double
// backslashes ('\' to '\\').
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", '\\', '\\\\'), 'escape');");
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", E'\\\\', E'\\\\\\\\'), 'escape');");
}
}

View file

@ -48,7 +48,7 @@ class Schema extends DatabaseSchema {
*/
public function createTableSql($name, $table) {
$sql = array();
$sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n);\n";
$sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n)\n";
return array_merge($sql, $this->createIndexSql($name, $table));
}
@ -60,12 +60,12 @@ class Schema extends DatabaseSchema {
$info = $this->getPrefixInfo($tablename);
if (!empty($schema['unique keys'])) {
foreach ($schema['unique keys'] as $key => $fields) {
$sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
$sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n";
}
}
if (!empty($schema['indexes'])) {
foreach ($schema['indexes'] as $key => $fields) {
$sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
$sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n";
}
}
return $sql;

View file

@ -188,6 +188,25 @@ class Condition implements ConditionInterface, \Countable {
'operator' => $condition['operator'],
'use_value' => TRUE,
);
// Remove potentially dangerous characters.
// If something passed in an invalid character stop early, so we
// don't rely on a broken SQL statement when we would just replace
// those characters.
if (stripos($condition['operator'], 'UNION') !== FALSE || strpbrk($condition['operator'], '[-\'"();') !== FALSE) {
$this->changed = TRUE;
$this->arguments = [];
// Provide a string which will result into an empty query result.
$this->stringVersion = '( AND 1 = 0 )';
// Conceptually throwing an exception caused by user input is bad
// as you result into a WSOD, which depending on your webserver
// configuration can result into the assumption that your site is
// broken.
// On top of that the database API relies on __toString() which
// does not allow to throw exceptions.
trigger_error('Invalid characters in query operator: ' . $condition['operator'], E_USER_ERROR);
return;
}
$operator = $connection->mapConditionOperator($condition['operator']);
if (!isset($operator)) {
$operator = $this->mapConditionOperator($condition['operator']);

View file

@ -45,95 +45,140 @@ class SelectExtender implements SelectInterface {
}
/**
* Implements Drupal\Core\Database\Query\PlaceholderInterface::uniqueIdentifier().
* {@inheritdoc}
*/
public function uniqueIdentifier() {
return $this->uniqueIdentifier;
}
/**
* Implements Drupal\Core\Database\Query\PlaceholderInterface::nextPlaceholder().
* {@inheritdoc}
*/
public function nextPlaceholder() {
return $this->placeholder++;
}
/* Implementations of Drupal\Core\Database\Query\AlterableInterface. */
/**
* {@inheritdoc}
*/
public function addTag($tag) {
$this->query->addTag($tag);
return $this;
}
/**
* {@inheritdoc}
*/
public function hasTag($tag) {
return $this->query->hasTag($tag);
}
/**
* {@inheritdoc}
*/
public function hasAllTags() {
return call_user_func_array(array($this->query, 'hasAllTags'), func_get_args());
}
/**
* {@inheritdoc}
*/
public function hasAnyTag() {
return call_user_func_array(array($this->query, 'hasAnyTag'), func_get_args());
}
/**
* {@inheritdoc}
*/
public function addMetaData($key, $object) {
$this->query->addMetaData($key, $object);
return $this;
}
/**
* {@inheritdoc}
*/
public function getMetaData($key) {
return $this->query->getMetaData($key);
}
/* Implementations of Drupal\Core\Database\Query\ConditionInterface for the WHERE clause. */
/**
* {@inheritdoc}
*/
public function condition($field, $value = NULL, $operator = '=') {
$this->query->condition($field, $value, $operator);
return $this;
}
/**
* {@inheritdoc}
*/
public function &conditions() {
return $this->query->conditions();
}
/**
* {@inheritdoc}
*/
public function arguments() {
return $this->query->arguments();
}
/**
* {@inheritdoc}
*/
public function where($snippet, $args = array()) {
$this->query->where($snippet, $args);
return $this;
}
/**
* {@inheritdoc}
*/
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
return $this->query->compile($connection, $queryPlaceholder);
}
/**
* {@inheritdoc}
*/
public function compiled() {
return $this->query->compiled();
}
/* Implementations of Drupal\Core\Database\Query\ConditionInterface for the HAVING clause. */
/**
* {@inheritdoc}
*/
public function havingCondition($field, $value = NULL, $operator = '=') {
$this->query->havingCondition($field, $value, $operator);
return $this;
}
/**
* {@inheritdoc}
*/
public function &havingConditions() {
return $this->query->havingConditions();
}
/**
* {@inheritdoc}
*/
public function havingArguments() {
return $this->query->havingArguments();
}
/**
* {@inheritdoc}
*/
public function having($snippet, $args = array()) {
$this->query->having($snippet, $args);
return $this;
}
/**
* {@inheritdoc}
*/
public function havingCompile(Connection $connection) {
return $this->query->havingCompile($connection);
}
@ -170,8 +215,9 @@ class SelectExtender implements SelectInterface {
return $this;
}
/* Implementations of Drupal\Core\Database\Query\ExtendableInterface. */
/**
* {@inheritdoc}
*/
public function extend($extender_name) {
$class = $this->connection->getDriverClass($extender_name);
return new $class($this, $this->connection);
@ -179,26 +225,44 @@ class SelectExtender implements SelectInterface {
/* Alter accessors to expose the query data to alter hooks. */
/**
* {@inheritdoc}
*/
public function &getFields() {
return $this->query->getFields();
}
/**
* {@inheritdoc}
*/
public function &getExpressions() {
return $this->query->getExpressions();
}
/**
* {@inheritdoc}
*/
public function &getOrderBy() {
return $this->query->getOrderBy();
}
/**
* {@inheritdoc}
*/
public function &getGroupBy() {
return $this->query->getGroupBy();
}
/**
* {@inheritdoc}
*/
public function &getTables() {
return $this->query->getTables();
}
/**
* {@inheritdoc}
*/
public function &getUnion() {
return $this->query->getUnion();
}
@ -218,14 +282,23 @@ class SelectExtender implements SelectInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function getArguments(PlaceholderInterface $queryPlaceholder = NULL) {
return $this->query->getArguments($queryPlaceholder);
}
/**
* {@inheritdoc}
*/
public function isPrepared() {
return $this->query->isPrepared();
}
/**
* {@inheritdoc}
*/
public function preExecute(SelectInterface $query = NULL) {
// If no query object is passed in, use $this.
if (!isset($query)) {
@ -235,6 +308,9 @@ class SelectExtender implements SelectInterface {
return $this->query->preExecute($query);
}
/**
* {@inheritdoc}
*/
public function execute() {
// By calling preExecute() here, we force it to preprocess the extender
// object rather than just the base query object. That means
@ -246,102 +322,168 @@ class SelectExtender implements SelectInterface {
return $this->query->execute();
}
/**
* {@inheritdoc}
*/
public function distinct($distinct = TRUE) {
$this->query->distinct($distinct);
return $this;
}
/**
* {@inheritdoc}
*/
public function addField($table_alias, $field, $alias = NULL) {
return $this->query->addField($table_alias, $field, $alias);
}
/**
* {@inheritdoc}
*/
public function fields($table_alias, array $fields = array()) {
$this->query->fields($table_alias, $fields);
return $this;
}
/**
* {@inheritdoc}
*/
public function addExpression($expression, $alias = NULL, $arguments = array()) {
return $this->query->addExpression($expression, $alias, $arguments);
}
/**
* {@inheritdoc}
*/
public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->join($table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->innerJoin($table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->leftJoin($table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->rightJoin($table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->addJoin($type, $table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function orderBy($field, $direction = 'ASC') {
$this->query->orderBy($field, $direction);
return $this;
}
/**
* {@inheritdoc}
*/
public function orderRandom() {
$this->query->orderRandom();
return $this;
}
/**
* {@inheritdoc}
*/
public function range($start = NULL, $length = NULL) {
$this->query->range($start, $length);
return $this;
}
/**
* {@inheritdoc}
*/
public function union(SelectInterface $query, $type = '') {
$this->query->union($query, $type);
return $this;
}
/**
* {@inheritdoc}
*/
public function groupBy($field) {
$this->query->groupBy($field);
return $this;
}
/**
* {@inheritdoc}
*/
public function forUpdate($set = TRUE) {
$this->query->forUpdate($set);
return $this;
}
/**
* {@inheritdoc}
*/
public function countQuery() {
return $this->query->countQuery();
}
/**
* {@inheritdoc}
*/
function isNull($field) {
$this->query->isNull($field);
return $this;
}
/**
* {@inheritdoc}
*/
function isNotNull($field) {
$this->query->isNotNull($field);
return $this;
}
/**
* {@inheritdoc}
*/
public function exists(SelectInterface $select) {
$this->query->exists($select);
return $this;
}
/**
* {@inheritdoc}
*/
public function notExists(SelectInterface $select) {
$this->query->notExists($select);
return $this;
}
/**
* {@inheritdoc}
*/
public function __toString() {
return (string) $this->query;
}
/**
* {@inheritdoc}
*/
public function __clone() {
$this->uniqueIdentifier = uniqid('', TRUE);

View file

@ -648,4 +648,12 @@ interface SelectInterface extends ConditionInterface, AlterableInterface, Extend
*/
public function forUpdate($set = TRUE);
/**
* Returns a string representation of how the query will be executed in SQL.
*
* @return string
* The Select Query object expressed as a string.
*/
public function __toString();
}

View file

@ -640,6 +640,8 @@ abstract class Schema implements PlaceholderInterface {
* The prepared comment.
*/
public function prepareComment($comment, $length = NULL) {
// Remove semicolons to avoid triggering multi-statement check.
$comment = strtr($comment, [';' => '.']);
return $this->connection->quote($comment);
}

View file

@ -7,7 +7,6 @@
namespace Drupal\Core\Datetime;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityManagerInterface;
@ -22,7 +21,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
*
* @ingroup i18n
*/
class DateFormatter {
class DateFormatter implements DateFormatterInterface {
use StringTranslationTrait;
/**
@ -105,33 +104,7 @@ class DateFormatter {
}
/**
* Formats a date, using a date type or a custom date format string.
*
* @param int $timestamp
* A UNIX timestamp to format.
* @param string $type
* (optional) The format to use, one of:
* - One of the built-in formats: 'short', 'medium',
* 'long', 'html_datetime', 'html_date', 'html_time',
* 'html_yearless_date', 'html_week', 'html_month', 'html_year'.
* - The name of a date type defined by a date format config entity.
* - The machine name of an administrator-defined date format.
* - 'custom', to use $format.
* Defaults to 'medium'.
* @param string $format
* (optional) If $type is 'custom', a PHP date format string suitable for
* input to date(). Use a backslash to escape ordinary text, so it does not
* get interpreted as date format characters.
* @param string|null $timezone
* (optional) Time zone identifier, as described at
* http://php.net/manual/timezones.php Defaults to the time zone used to
* display the page.
* @param string|null $langcode
* (optional) Language code to translate to. NULL (default) means to use
* the user interface language for the page.
*
* @return string
* A translated date string in the requested format.
* {@inheritdoc}
*/
public function format($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) {
if (!isset($timezone)) {
@ -168,32 +141,11 @@ class DateFormatter {
$settings = array(
'langcode' => $langcode,
);
return Xss::filter($date->format($format, $settings));
return $date->format($format, $settings);
}
/**
* Formats a time interval with the requested granularity.
*
* Note that for intervals over 30 days, the output is approximate: a "month"
* is always exactly 30 days, and a "year" is always 365 days. It is not
* possible to make a more exact representation, given that there is only one
* input in seconds. If you are formatting an interval between two specific
* timestamps, use \Drupal\Core\Datetime\DateFormatter::formatDiff() instead.
*
* @param int $interval
* The length of the interval in seconds.
* @param int $granularity
* (optional) How many different units to display in the string (2 by
* default).
* @param string|null $langcode
* (optional) langcode: The language code for the language used to format
* the date. Defaults to NULL, which results in the user interface language
* for the page being used.
*
* @return string
* A translated string representation of the interval.
*
* @see \Drupal\Core\Datetime\DateFormatter::formatDiff()
* {@inheritdoc}
*/
public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
$output = '';
@ -218,21 +170,7 @@ class DateFormatter {
}
/**
* Provides values for all date formatting characters for a given timestamp.
*
* @param string|null $langcode
* (optional) Language code of the date format, if different from the site
* default language.
* @param int|null $timestamp
* (optional) The Unix timestamp to format, defaults to current time.
* @param string|null $timezone
* (optional) The timezone to use, if different from the site's default
* timezone.
*
* @return array
* An array of formatted date values, indexed by the date format character.
*
* @see date()
* {@inheritdoc}
*/
public function getSampleDateFormats($langcode = NULL, $timestamp = NULL, $timezone = NULL) {
$timestamp = $timestamp ?: time();
@ -245,30 +183,7 @@ class DateFormatter {
}
/**
* Formats the time difference from the current request time to a timestamp.
*
* @param $timestamp
* A UNIX timestamp to compare against the current request time.
* @param array $options
* (optional) An associative array with additional options. The following
* keys can be used:
* - granularity: An integer value that signals how many different units to
* display in the string. Defaults to 2.
* - langcode: The language code for the language used to format the date.
* Defaults to NULL, which results in the user interface language for the
* page being used.
* - strict: A Boolean value indicating whether or not the timestamp can be
* before the current request time. If TRUE (default) and $timestamp is
* before the current request time, the result string will be "0 seconds".
* If FALSE and $timestamp is before the current request time, the result
* string will be the formatted time difference.
*
* @return string
* A translated string representation of the difference between the given
* timestamp and the current request time. This interval is always positive.
*
* @see \Drupal\Core\Datetime\DateFormatter::formatDiff()
* @see \Drupal\Core\Datetime\DateFormatter::formatTimeDiffSince()
* {@inheritdoc}
*/
public function formatTimeDiffUntil($timestamp, $options = array()) {
$request_time = $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME');
@ -276,30 +191,7 @@ class DateFormatter {
}
/**
* Formats the time difference from a timestamp to the current request time.
*
* @param $timestamp
* A UNIX timestamp to compare against the current request time.
* @param array $options
* (optional) An associative array with additional options. The following
* keys can be used:
* - granularity: An integer value that signals how many different units to
* display in the string. Defaults to 2.
* - langcode: The language code for the language used to format the date.
* Defaults to NULL, which results in the user interface language for the
* page being used.
* - strict: A Boolean value indicating whether or not the timestamp can be
* after the current request time. If TRUE (default) and $timestamp is
* after the current request time, the result string will be "0 seconds".
* If FALSE and $timestamp is after the current request time, the result
* string will be the formatted time difference.
*
* @return string
* A translated string representation of the difference between the given
* timestamp and the current request time. This interval is always positive.
*
* @see \Drupal\Core\Datetime\DateFormatter::formatDiff()
* @see \Drupal\Core\Datetime\DateFormatter::formatTimeDiffUntil()
* {@inheritdoc}
*/
public function formatTimeDiffSince($timestamp, $options = array()) {
$request_time = $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME');
@ -307,32 +199,7 @@ class DateFormatter {
}
/**
* Formats a time interval between two timestamps.
*
* @param int $from
* A UNIX timestamp, defining the from date and time.
* @param int $to
* A UNIX timestamp, defining the to date and time.
* @param array $options
* (optional) An associative array with additional options. The following
* keys can be used:
* - granularity: An integer value that signals how many different units to
* display in the string. Defaults to 2.
* - langcode: The language code for the language used to format the date.
* Defaults to NULL, which results in the user interface language for the
* page being used.
* - strict: A Boolean value indicating whether or not the $from timestamp
* can be after the $to timestamp. If TRUE (default) and $from is after
* $to, the result string will be "0 seconds". If FALSE and $from is
* after $to, the result string will be the formatted time difference.
*
* @return string
* A translated string representation of the interval. This interval is
* always positive.
*
* @see \Drupal\Core\Datetime\DateFormatter::formatInterval()
* @see \Drupal\Core\Datetime\DateFormatter::formatTimeDiffSince()
* @see \Drupal\Core\Datetime\DateFormatter::formatTimeDiffUntil()
* {@inheritdoc}
*/
public function formatDiff($from, $to, $options = array()) {

View file

@ -0,0 +1,178 @@
<?php
/**
* @file
* Contains \Drupal\Core\Datetime\DateFormatterInterface.
*/
namespace Drupal\Core\Datetime;
/**
* Provides an interface defining a date formatter.
*/
interface DateFormatterInterface {
/**
* Formats a date, using a date type or a custom date format string.
*
* @param int $timestamp
* A UNIX timestamp to format.
* @param string $type
* (optional) The format to use, one of:
* - One of the built-in formats: 'short', 'medium',
* 'long', 'html_datetime', 'html_date', 'html_time',
* 'html_yearless_date', 'html_week', 'html_month', 'html_year'.
* - The name of a date type defined by a date format config entity.
* - The machine name of an administrator-defined date format.
* - 'custom', to use $format.
* Defaults to 'medium'.
* @param string $format
* (optional) If $type is 'custom', a PHP date format string suitable for
* input to date(). Use a backslash to escape ordinary text, so it does not
* get interpreted as date format characters.
* @param string|null $timezone
* (optional) Time zone identifier, as described at
* http://php.net/manual/timezones.php Defaults to the time zone used to
* display the page.
* @param string|null $langcode
* (optional) Language code to translate to. NULL (default) means to use
* the user interface language for the page.
*
* @return string
* A translated date string in the requested format. Since the format may
* contain user input, this value should be escaped when output.
*/
public function format($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL);
/**
* Formats a time interval with the requested granularity.
*
* Note that for intervals over 30 days, the output is approximate: a "month"
* is always exactly 30 days, and a "year" is always 365 days. It is not
* possible to make a more exact representation, given that there is only one
* input in seconds. If you are formatting an interval between two specific
* timestamps, use \Drupal\Core\Datetime\DateFormatter::formatDiff() instead.
*
* @param int $interval
* The length of the interval in seconds.
* @param int $granularity
* (optional) How many different units to display in the string (2 by
* default).
* @param string|null $langcode
* (optional) langcode: The language code for the language used to format
* the date. Defaults to NULL, which results in the user interface language
* for the page being used.
*
* @return string
* A translated string representation of the interval.
*
* @see \Drupal\Core\Datetime\DateFormatterInterface::formatDiff()
*/
public function formatInterval($interval, $granularity = 2, $langcode = NULL);
/**
* Provides values for all date formatting characters for a given timestamp.
*
* @param string|null $langcode
* (optional) Language code of the date format, if different from the site
* default language.
* @param int|null $timestamp
* (optional) The Unix timestamp to format, defaults to current time.
* @param string|null $timezone
* (optional) The timezone to use, if different from the site's default
* timezone.
*
* @return array
* An array of formatted date values, indexed by the date format character.
*
* @see date()
*/
public function getSampleDateFormats($langcode = NULL, $timestamp = NULL, $timezone = NULL);
/**
* Formats the time difference from the current request time to a timestamp.
*
* @param $timestamp
* A UNIX timestamp to compare against the current request time.
* @param array $options
* (optional) An associative array with additional options. The following
* keys can be used:
* - granularity: An integer value that signals how many different units to
* display in the string. Defaults to 2.
* - langcode: The language code for the language used to format the date.
* Defaults to NULL, which results in the user interface language for the
* page being used.
* - strict: A Boolean value indicating whether or not the timestamp can be
* before the current request time. If TRUE (default) and $timestamp is
* before the current request time, the result string will be "0 seconds".
* If FALSE and $timestamp is before the current request time, the result
* string will be the formatted time difference.
*
* @return string
* A translated string representation of the difference between the given
* timestamp and the current request time. This interval is always positive.
*
* @see \Drupal\Core\Datetime\DateFormatterInterface::formatDiff()
* @see \Drupal\Core\Datetime\DateFormatterInterface::formatTimeDiffSince()
*/
public function formatTimeDiffUntil($timestamp, $options = array());
/**
* Formats the time difference from a timestamp to the current request time.
*
* @param $timestamp
* A UNIX timestamp to compare against the current request time.
* @param array $options
* (optional) An associative array with additional options. The following
* keys can be used:
* - granularity: An integer value that signals how many different units to
* display in the string. Defaults to 2.
* - langcode: The language code for the language used to format the date.
* Defaults to NULL, which results in the user interface language for the
* page being used.
* - strict: A Boolean value indicating whether or not the timestamp can be
* after the current request time. If TRUE (default) and $timestamp is
* after the current request time, the result string will be "0 seconds".
* If FALSE and $timestamp is after the current request time, the result
* string will be the formatted time difference.
*
* @return string
* A translated string representation of the difference between the given
* timestamp and the current request time. This interval is always positive.
*
* @see \Drupal\Core\Datetime\DateFormatterInterface::formatDiff()
* @see \Drupal\Core\Datetime\DateFormatterInterface::formatTimeDiffUntil()
*/
public function formatTimeDiffSince($timestamp, $options = array());
/**
* Formats a time interval between two timestamps.
*
* @param int $from
* A UNIX timestamp, defining the from date and time.
* @param int $to
* A UNIX timestamp, defining the to date and time.
* @param array $options
* (optional) An associative array with additional options. The following
* keys can be used:
* - granularity: An integer value that signals how many different units to
* display in the string. Defaults to 2.
* - langcode: The language code for the language used to format the date.
* Defaults to NULL, which results in the user interface language for the
* page being used.
* - strict: A Boolean value indicating whether or not the $from timestamp
* can be after the $to timestamp. If TRUE (default) and $from is after
* $to, the result string will be "0 seconds". If FALSE and $from is
* after $to, the result string will be the formatted time difference.
*
* @return string
* A translated string representation of the interval. This interval is
* always positive.
*
* @see \Drupal\Core\Datetime\DateFormatterInterface::formatInterval()
* @see \Drupal\Core\Datetime\DateFormatterInterface::formatTimeDiffSince()
* @see \Drupal\Core\Datetime\DateFormatterInterface::formatTimeDiffUntil()
*/
public function formatDiff($from, $to, $options = array());
}

View file

@ -90,7 +90,8 @@ class DrupalDateTime extends DateTimePlus {
* the result of the format() method. Defaults to NULL.
*
* @return string
* The formatted value of the date.
* The formatted value of the date. Since the format may contain user input,
* this value should be escaped when output.
*/
public function format($format, $settings = array()) {
$langcode = !empty($settings['langcode']) ? $settings['langcode'] : $this->langcode;

View file

@ -241,7 +241,7 @@ class Datetime extends DateElementBase {
// placeholders are invalid for HTML5 date and datetime, so an example
// format is appended to the title to appear in tooltips.
$extra_attributes = array(
'title' => t('Date (e.g. !format)', array('!format' => static::formatExample($date_format))),
'title' => t('Date (e.g. @format)', array('@format' => static::formatExample($date_format))),
'type' => $element['#date_date_element'],
);
@ -288,7 +288,7 @@ class Datetime extends DateElementBase {
// Adds the HTML5 attributes.
$extra_attributes = array(
'title' => t('Time (e.g. !format)', array('!format' => static::formatExample($time_format))),
'title' => t('Time (e.g. @format)', array('@format' => static::formatExample($time_format))),
'type' => $element['#date_time_element'],
'step' => $element['#date_increment'],
);

View file

@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass.
* Contains \Drupal\Core\DependencyInjection\Compiler\RegisterEventSubscribersPass.
*/
namespace Drupal\Core\DependencyInjection\Compiler;
@ -10,7 +10,10 @@ namespace Drupal\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
class RegisterKernelListenersPass implements CompilerPassInterface {
/**
* Registers all event subscribers to the event dispatcher.
*/
class RegisterEventSubscribersPass implements CompilerPassInterface {
public function process(ContainerBuilder $container) {
if (!$container->hasDefinition('event_dispatcher')) {
return;

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\Core\Display\ContextAwareVariantInterface.
*/
namespace Drupal\Core\Display;
/**
* Provides an interface for variant plugins that are context-aware.
*/
interface ContextAwareVariantInterface extends VariantInterface {
/**
* Gets the values for all defined contexts.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of set contexts, keyed by context name.
*/
public function getContexts();
/**
* Sets the context values for this display variant.
*
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* An array of contexts, keyed by context name.
*
* @return $this
*/
public function setContexts(array $contexts);
}

View file

@ -36,4 +36,15 @@ interface PageVariantInterface extends VariantInterface {
*/
public function setMainContent(array $main_content);
/**
* Sets the title for the page being rendered.
*
* @param string|array $title
* The page title: either a string for plain titles or a render array for
* formatted titles.
*
* @return $this
*/
public function setTitle($title);
}

View file

@ -7,6 +7,7 @@
namespace Drupal\Core\Display;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Plugin\PluginDependencyTrait;
@ -23,6 +24,7 @@ use Drupal\Core\Session\AccountInterface;
abstract class VariantBase extends PluginBase implements VariantInterface {
use PluginDependencyTrait;
use RefinableCacheableDependencyTrait;
/**
* {@inheritdoc}

View file

@ -9,6 +9,7 @@ namespace Drupal\Core\Display;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Session\AccountInterface;
@ -20,7 +21,7 @@ use Drupal\Core\Session\AccountInterface;
* @see \Drupal\Core\Display\VariantManager
* @see plugin_api
*/
interface VariantInterface extends PluginInspectionInterface, ConfigurablePluginInterface, PluginFormInterface {
interface VariantInterface extends PluginInspectionInterface, ConfigurablePluginInterface, PluginFormInterface, RefinableCacheableDependencyInterface {
/**
* Returns the user-facing display variant label.
@ -79,6 +80,10 @@ interface VariantInterface extends PluginInspectionInterface, ConfigurablePlugin
/**
* Builds and returns the renderable array for the display variant.
*
* The variant can contain cacheability metadata for the configuration that
* was passed in setConfiguration(). In the build() method, this should be
* added to the render array that is returned.
*
* @return array
* A render array for the display variant.
*/

View file

@ -992,47 +992,32 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
* @todo D8: Eliminate this entirely in favor of Request object.
*/
protected function initializeRequestGlobals(Request $request) {
// Provided by settings.php.
global $base_url;
// Set and derived from $base_url by this function.
global $base_path, $base_root;
global $base_secure_url, $base_insecure_url;
// @todo Refactor with the Symfony Request object.
if (isset($base_url)) {
// Parse fixed base URL from settings.php.
$parts = parse_url($base_url);
if (!isset($parts['path'])) {
$parts['path'] = '';
}
$base_path = $parts['path'] . '/';
// Build $base_root (everything until first slash after "scheme://").
$base_root = substr($base_url, 0, strlen($base_url) - strlen($parts['path']));
}
else {
// Create base URL.
$base_root = $request->getSchemeAndHttpHost();
// Create base URL.
$base_root = $request->getSchemeAndHttpHost();
$base_url = $base_root;
$base_url = $base_root;
// For a request URI of '/index.php/foo', $_SERVER['SCRIPT_NAME'] is
// '/index.php', whereas $_SERVER['PHP_SELF'] is '/index.php/foo'.
if ($dir = rtrim(dirname($request->server->get('SCRIPT_NAME')), '\/')) {
// Remove "core" directory if present, allowing install.php,
// authorize.php, and others to auto-detect a base path.
$core_position = strrpos($dir, '/core');
if ($core_position !== FALSE && strlen($dir) - 5 == $core_position) {
$base_path = substr($dir, 0, $core_position);
}
else {
$base_path = $dir;
}
$base_url .= $base_path;
$base_path .= '/';
// For a request URI of '/index.php/foo', $_SERVER['SCRIPT_NAME'] is
// '/index.php', whereas $_SERVER['PHP_SELF'] is '/index.php/foo'.
if ($dir = rtrim(dirname($request->server->get('SCRIPT_NAME')), '\/')) {
// Remove "core" directory if present, allowing install.php,
// authorize.php, and others to auto-detect a base path.
$core_position = strrpos($dir, '/core');
if ($core_position !== FALSE && strlen($dir) - 5 == $core_position) {
$base_path = substr($dir, 0, $core_position);
}
else {
$base_path = '/';
$base_path = $dir;
}
$base_url .= $base_path;
$base_path .= '/';
}
else {
$base_path = '/';
}
$base_secure_url = str_replace('http://', 'https://', $base_url);
$base_insecure_url = str_replace('https://', 'http://', $base_url);

View file

@ -6,7 +6,7 @@
*/
namespace Drupal\Core\Entity\Annotation;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a config entity type annotation object.
@ -37,7 +37,7 @@ class ConfigEntityType extends EntityType {
* {@inheritdoc}
*/
public function get() {
$this->definition['group_label'] = new TranslationWrapper('Configuration', array(), array('context' => 'Entity type group'));
$this->definition['group_label'] = new TranslatableMarkup('Configuration', array(), array('context' => 'Entity type group'));
return parent::get();
}

View file

@ -6,7 +6,7 @@
*/
namespace Drupal\Core\Entity\Annotation;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a content entity type annotation object.
@ -37,7 +37,7 @@ class ContentEntityType extends EntityType {
* {@inheritdoc}
*/
public function get() {
$this->definition['group_label'] = new TranslationWrapper('Content', array(), array('context' => 'Entity type group'));
$this->definition['group_label'] = new TranslatableMarkup('Content', array(), array('context' => 'Entity type group'));
return parent::get();
}

View file

@ -19,8 +19,6 @@ use Drupal\Component\Annotation\Plugin;
*
* @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager
* @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
* @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\SelectionBase
* @see \Drupal\Core\Entity\Plugin\Derivative\SelectionBase
* @see plugin_api
*
* @Annotation

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\BundleEntityFormBase.
*/
namespace Drupal\Core\Entity;
/**
* Class BundleEntityFormBase is a base form for bundle config entities.
*/
class BundleEntityFormBase extends EntityForm {
/**
* Protects the bundle entity's ID property's form element against changes.
*
* This method is assumed to be called on a completely built entity form,
* including a form element for the bundle config entity's ID property.
*
* @param array $form
* The completely built entity bundle form array.
*
* @return array
* The updated entity bundle form array.
*/
protected function protectBundleIdElement(array $form) {
$entity = $this->getEntity();
$id_key = $entity->getEntityType()->getKey('id');
assert('isset($form[$id_key])');
$element = &$form[$id_key];
// Make sure the element is not accidentally re-enabled if it has already
// been disabled.
if (empty($element['#disabled'])) {
$element['#disabled'] = !$entity->isNew();
}
return $form;
}
}

View file

@ -70,7 +70,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
/**
* Local cache for the available language objects.
*
* @var array
* @var \Drupal\Core\Language\LanguageInterface[]
*/
protected $languages;
@ -138,7 +138,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
protected $isDefaultRevision = TRUE;
/**
* Holds translatable entity keys such as the ID, bundle and revision ID.
* Holds untranslatable entity keys such as the ID, bundle, and revision ID.
*
* @var array
*/
@ -593,7 +593,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
}
return $this->entityManager()
->getAccessControlHandler($this->entityTypeId)
->access($this, $operation, $this->activeLangcode, $account, $return_as_object);
->access($this, $operation, $account, $return_as_object);
}
/**
@ -737,22 +737,11 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
if (isset($this->translations[$langcode]['entity'])) {
$translation = $this->translations[$langcode]['entity'];
}
else {
if (isset($this->translations[$langcode])) {
$translation = $this->initializeTranslation($langcode);
$this->translations[$langcode]['entity'] = $translation;
}
else {
// If we were given a valid language and there is no translation for it,
// we return a new one.
$this->getLanguages();
if (isset($this->languages[$langcode])) {
// If the entity or the requested language is not a configured
// language, we fall back to the entity itself, since in this case it
// cannot have translations.
$translation = !$this->languages[$this->defaultLangcode]->isLocked() && !$this->languages[$langcode]->isLocked() ? $this->addTranslation($langcode) : $this;
}
}
// Otherwise if an existing translation language was specified we need to
// instantiate the related translation.
elseif (isset($this->translations[$langcode])) {
$translation = $this->initializeTranslation($langcode);
$this->translations[$langcode]['entity'] = $translation;
}
if (empty($translation)) {
@ -823,10 +812,15 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
* {@inheritdoc}
*/
public function addTranslation($langcode, array $values = array()) {
// Make sure we do not attempt to create a translation if an invalid
// language is specified or the entity cannot be translated.
$this->getLanguages();
if (!isset($this->languages[$langcode]) || $this->hasTranslation($langcode)) {
if (!isset($this->languages[$langcode]) || $this->hasTranslation($langcode) || $this->languages[$langcode]->isLocked()) {
throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
}
if ($this->languages[$this->defaultLangcode]->isLocked()) {
throw new \InvalidArgumentException("The entity cannot be translated since it is language neutral ({$this->defaultLangcode}).");
}
// Instantiate a new empty entity so default values will be populated in the
// specified language.

Some files were not shown because too many files have changed in this diff Show more