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

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

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains \Drupal\Component\Assertion\Handle.
*
* For PHP 5 this contains \AssertionError as well.
*/
namespace {
if (!class_exists('AssertionError', FALSE)) {
/**
* Emulates PHP 7 AssertionError as closely as possible.
*
* We force this class to exist at the root namespace for PHP 5.
* This class exists natively in PHP 7. Note that in PHP 7 it extends from
* Error, not Exception, but that isn't possible for PHP 5 - all exceptions
* must extend from exception.
*/
class AssertionError extends Exception {
/**
* {@inheritdoc}
*/
public function __construct($message = '', $code = 0, Exception $previous = NULL, $file = '', $line = 0) {
parent::__construct($message, $code, $previous);
// Preserve the filename and line number of the assertion failure.
$this->file = $file;
$this->line = $line;
}
}
}
}
namespace Drupal\Component\Assertion {
/**
* Handler for runtime assertion failures.
*
* This class allows PHP 5.x to throw exceptions on runtime assertion fails
* in the same manner as PHP 7, and sets the ASSERT_EXCEPTION flag to TRUE
* for the PHP 7 runtime.
*
* @ingroup php_assert
*/
class Handle {
/**
* Registers uniform assertion handling.
*/
public static function register() {
// Since we're using exceptions, turn error warnings off.
assert_options(ASSERT_WARNING, FALSE);
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) {
if (empty($message)) {
$message = $code;
}
throw new \AssertionError($message, 0, NULL, $file, $line);
});
}
else {
// PHP 7 - just turn exception throwing on.
assert_options(ASSERT_EXCEPTION, TRUE);
}
}
}
}

View file

@ -0,0 +1,629 @@
<?php
/**
* @file
* Contains \Drupal\Component\DependencyInjection\Container.
*/
namespace Drupal\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\IntrospectableContainerInterface;
use Symfony\Component\DependencyInjection\ScopeInterface;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
/**
* Provides a container optimized for Drupal's needs.
*
* This container implementation is compatible with the default Symfony
* dependency injection container and similar to the Symfony ContainerBuilder
* class, but optimized for speed.
*
* It is based on a PHP array container definition dumped as a
* performance-optimized machine-readable format.
*
* The best way to initialize this container is to use a Container Builder,
* compile it and then retrieve the definition via
* \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper::getArray().
*
* The retrieved array can be cached safely and then passed to this container
* via the constructor.
*
* As the container is unfrozen by default, a second parameter can be passed to
* the container to "freeze" the parameter bag.
*
* This container is different in behavior from the default Symfony container in
* the following ways:
*
* - It only allows lowercase service and parameter names, though it does only
* enforce it via assertions for performance reasons.
* - The following functions, that are not part of the interface, are explicitly
* not supported: getParameterBag(), isFrozen(), compile(),
* getAServiceWithAnIdByCamelCase().
* - The function getServiceIds() was added as it has a use-case in core and
* contrib.
* - Scopes are explicitly not allowed, because Symfony 2.8 has deprecated
* them and they will be removed in Symfony 3.0.
* - Synchronized services are explicitly not supported, because Symfony 2.8 has
* deprecated them and they will be removed in Symfony 3.0.
*
* @ingroup container
*/
class Container implements IntrospectableContainerInterface {
/**
* The parameters of the container.
*
* @var array
*/
protected $parameters = array();
/**
* The aliases of the container.
*
* @var array
*/
protected $aliases = array();
/**
* The service definitions of the container.
*
* @var array
*/
protected $serviceDefinitions = array();
/**
* The instantiated services.
*
* @var array
*/
protected $services = array();
/**
* The instantiated private services.
*
* @var array
*/
protected $privateServices = array();
/**
* The currently loading services.
*
* @var array
*/
protected $loading = array();
/**
* Whether the container parameters can still be changed.
*
* For testing purposes the container needs to be changed.
*
* @var bool
*/
protected $frozen = TRUE;
/**
* Constructs a new Container instance.
*
* @param array $container_definition
* An array containing the following keys:
* - aliases: The aliases of the container.
* - parameters: The parameters of the container.
* - services: The service definitions of the container.
* - frozen: Whether the container definition came from a frozen
* container builder or not.
* - machine_format: Whether this container definition uses the optimized
* machine-readable container format.
*/
public function __construct(array $container_definition = array()) {
if (!empty($container_definition) && (!isset($container_definition['machine_format']) || $container_definition['machine_format'] !== TRUE)) {
throw new InvalidArgumentException('The non-optimized format is not supported by this class. Use an optimized machine-readable format instead, e.g. as produced by \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper.');
}
$this->aliases = isset($container_definition['aliases']) ? $container_definition['aliases'] : array();
$this->parameters = isset($container_definition['parameters']) ? $container_definition['parameters'] : array();
$this->serviceDefinitions = isset($container_definition['services']) ? $container_definition['services'] : array();
$this->frozen = isset($container_definition['frozen']) ? $container_definition['frozen'] : FALSE;
// Register the service_container with itself.
$this->services['service_container'] = $this;
}
/**
* {@inheritdoc}
*/
public function get($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if (isset($this->aliases[$id])) {
$id = $this->aliases[$id];
}
// Re-use shared service instance if it exists.
if (isset($this->services[$id]) || ($invalid_behavior === ContainerInterface::NULL_ON_INVALID_REFERENCE && array_key_exists($id, $this->services))) {
return $this->services[$id];
}
if (isset($this->loading[$id])) {
throw new ServiceCircularReferenceException($id, array_keys($this->loading));
}
$definition = isset($this->serviceDefinitions[$id]) ? $this->serviceDefinitions[$id] : NULL;
if (!$definition && $invalid_behavior === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if (!$id) {
throw new ServiceNotFoundException($id);
}
throw new ServiceNotFoundException($id, NULL, NULL, $this->getServiceAlternatives($id));
}
// In case something else than ContainerInterface::NULL_ON_INVALID_REFERENCE
// is used, the actual wanted behavior is to re-try getting the service at a
// later point.
if (!$definition) {
return;
}
// Definition is a keyed array, so [0] is only defined when it is a
// serialized string.
if (isset($definition[0])) {
$definition = unserialize($definition);
}
// Now create the service.
$this->loading[$id] = TRUE;
try {
$service = $this->createService($definition, $id);
}
catch (\Exception $e) {
unset($this->loading[$id]);
// Remove a potentially shared service that was constructed incompletely.
if (array_key_exists($id, $this->services)) {
unset($this->services[$id]);
}
if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalid_behavior) {
return;
}
throw $e;
}
unset($this->loading[$id]);
return $service;
}
/**
* Creates a service from a service definition.
*
* @param array $definition
* The service definition to create a service from.
* @param string $id
* The service identifier, necessary so it can be shared if its public.
*
* @return object
* The service described by the service definition.
*
* @throws \Symfony\Component\DependencyInjection\Exception\RuntimeException
* Thrown when the service is a synthetic service.
* @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* Thrown when the configurator callable in $definition['configurator'] is
* not actually a callable.
* @throws \ReflectionException
* Thrown when the service class takes more than 10 parameters to construct,
* and cannot be instantiated.
*/
protected function createService(array $definition, $id) {
if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) {
throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The service container does not know how to construct this service. The service will need to be set before it is first used.', $id));
}
$arguments = array();
if (isset($definition['arguments'])) {
$arguments = $definition['arguments'];
if ($arguments instanceof \stdClass) {
$arguments = $this->resolveServicesAndParameters($arguments);
}
}
if (isset($definition['file'])) {
$file = $this->frozen ? $definition['file'] : current($this->resolveServicesAndParameters(array($definition['file'])));
require_once $file;
}
if (isset($definition['factory'])) {
$factory = $definition['factory'];
if (is_array($factory)) {
$factory = $this->resolveServicesAndParameters(array($factory[0], $factory[1]));
}
elseif (!is_string($factory)) {
throw new RuntimeException(sprintf('Cannot create service "%s" because of invalid factory', $id));
}
$service = call_user_func_array($factory, $arguments);
}
else {
$class = $this->frozen ? $definition['class'] : current($this->resolveServicesAndParameters(array($definition['class'])));
$length = isset($definition['arguments_count']) ? $definition['arguments_count'] : count($arguments);
// Optimize class instantiation for services with up to 10 parameters as
// ReflectionClass is noticeably slow.
switch ($length) {
case 0:
$service = new $class();
break;
case 1:
$service = new $class($arguments[0]);
break;
case 2:
$service = new $class($arguments[0], $arguments[1]);
break;
case 3:
$service = new $class($arguments[0], $arguments[1], $arguments[2]);
break;
case 4:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
break;
case 5:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
break;
case 6:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]);
break;
case 7:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]);
break;
case 8:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7]);
break;
case 9:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8]);
break;
case 10:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8], $arguments[9]);
break;
default:
$r = new \ReflectionClass($class);
$service = $r->newInstanceArgs($arguments);
break;
}
}
// Share the service if it is public.
if (!isset($definition['public']) || $definition['public'] !== FALSE) {
// Forward compatibility fix for Symfony 2.8 update.
if (!isset($definition['shared']) || $definition['shared'] !== FALSE) {
$this->services[$id] = $service;
}
}
if (isset($definition['calls'])) {
foreach ($definition['calls'] as $call) {
$method = $call[0];
$arguments = array();
if (!empty($call[1])) {
$arguments = $call[1];
if ($arguments instanceof \stdClass) {
$arguments = $this->resolveServicesAndParameters($arguments);
}
}
call_user_func_array(array($service, $method), $arguments);
}
}
if (isset($definition['properties'])) {
if ($definition['properties'] instanceof \stdClass) {
$definition['properties'] = $this->resolveServicesAndParameters($definition['properties']);
}
foreach ($definition['properties'] as $key => $value) {
$service->{$key} = $value;
}
}
if (isset($definition['configurator'])) {
$callable = $definition['configurator'];
if (is_array($callable)) {
$callable = $this->resolveServicesAndParameters($callable);
}
if (!is_callable($callable)) {
throw new InvalidArgumentException(sprintf('The configurator for class "%s" is not a callable.', get_class($service)));
}
call_user_func($callable, $service);
}
return $service;
}
/**
* {@inheritdoc}
*/
public function set($id, $service, $scope = ContainerInterface::SCOPE_CONTAINER) {
$this->services[$id] = $service;
}
/**
* {@inheritdoc}
*/
public function has($id) {
return isset($this->services[$id]) || isset($this->serviceDefinitions[$id]);
}
/**
* {@inheritdoc}
*/
public function getParameter($name) {
if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) {
if (!$name) {
throw new ParameterNotFoundException($name);
}
throw new ParameterNotFoundException($name, NULL, NULL, NULL, $this->getParameterAlternatives($name));
}
return $this->parameters[$name];
}
/**
* {@inheritdoc}
*/
public function hasParameter($name) {
return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters);
}
/**
* {@inheritdoc}
*/
public function setParameter($name, $value) {
if ($this->frozen) {
throw new LogicException('Impossible to call set() on a frozen ParameterBag.');
}
$this->parameters[$name] = $value;
}
/**
* {@inheritdoc}
*/
public function initialized($id) {
if (isset($this->aliases[$id])) {
$id = $this->aliases[$id];
}
return isset($this->services[$id]) || array_key_exists($id, $this->services);
}
/**
* Resolves arguments that represent services or variables to the real values.
*
* @param array|\stdClass $arguments
* The arguments to resolve.
*
* @return array
* The resolved arguments.
*
* @throws \Symfony\Component\DependencyInjection\Exception\RuntimeException
* If a parameter/service could not be resolved.
* @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* If an unknown type is met while resolving parameters and services.
*/
protected function resolveServicesAndParameters($arguments) {
// Check if this collection needs to be resolved.
if ($arguments instanceof \stdClass) {
if ($arguments->type !== 'collection') {
throw new InvalidArgumentException(sprintf('Undefined type "%s" while resolving parameters and services.', $arguments->type));
}
// In case there is nothing to resolve, we are done here.
if (!$arguments->resolve) {
return $arguments->value;
}
$arguments = $arguments->value;
}
// Process the arguments.
foreach ($arguments as $key => $argument) {
// For this machine-optimized format, only \stdClass arguments are
// processed and resolved. All other values are kept as is.
if ($argument instanceof \stdClass) {
$type = $argument->type;
// Check for parameter.
if ($type == 'parameter') {
$name = $argument->name;
if (!isset($this->parameters[$name])) {
$arguments[$key] = $this->getParameter($name);
// This can never be reached as getParameter() throws an Exception,
// because we already checked that the parameter is not set above.
}
// Update argument.
$argument = $arguments[$key] = $this->parameters[$name];
// In case there is not a machine readable value (e.g. a service)
// behind this resolved parameter, continue.
if (!($argument instanceof \stdClass)) {
continue;
}
// Fall through.
$type = $argument->type;
}
// Create a service.
if ($type == 'service') {
$id = $argument->id;
// Does the service already exist?
if (isset($this->aliases[$id])) {
$id = $this->aliases[$id];
}
if (isset($this->services[$id])) {
$arguments[$key] = $this->services[$id];
continue;
}
// Return the service.
$arguments[$key] = $this->get($id, $argument->invalidBehavior);
continue;
}
// Create private service.
elseif ($type == 'private_service') {
$id = $argument->id;
// Does the private service already exist.
if (isset($this->privateServices[$id])) {
$arguments[$key] = $this->privateServices[$id];
continue;
}
// Create the private service.
$arguments[$key] = $this->createService($argument->value, $id);
if ($argument->shared) {
$this->privateServices[$id] = $arguments[$key];
}
continue;
}
// Check for collection.
elseif ($type == 'collection') {
$value = $argument->value;
// Does this collection need resolving?
if ($argument->resolve) {
$arguments[$key] = $this->resolveServicesAndParameters($value);
}
else {
$arguments[$key] = $value;
}
continue;
}
if ($type !== NULL) {
throw new InvalidArgumentException(sprintf('Undefined type "%s" while resolving parameters and services.', $type));
}
}
}
return $arguments;
}
/**
* Provides alternatives for a given array and key.
*
* @param string $search_key
* The search key to get alternatives for.
* @param array $keys
* The search space to search for alternatives in.
*
* @return string[]
* An array of strings with suitable alternatives.
*/
protected function getAlternatives($search_key, array $keys) {
$alternatives = array();
foreach ($keys as $key) {
$lev = levenshtein($search_key, $key);
if ($lev <= strlen($search_key) / 3 || strpos($key, $search_key) !== FALSE) {
$alternatives[] = $key;
}
}
return $alternatives;
}
/**
* Provides alternatives in case a service was not found.
*
* @param string $id
* The service to get alternatives for.
*
* @return string[]
* An array of strings with suitable alternatives.
*/
protected function getServiceAlternatives($id) {
$all_service_keys = array_unique(array_merge(array_keys($this->services), array_keys($this->serviceDefinitions)));
return $this->getAlternatives($id, $all_service_keys);
}
/**
* Provides alternatives in case a parameter was not found.
*
* @param string $name
* The parameter to get alternatives for.
*
* @return string[]
* An array of strings with suitable alternatives.
*/
protected function getParameterAlternatives($name) {
return $this->getAlternatives($name, array_keys($this->parameters));
}
/**
* {@inheritdoc}
*/
public function enterScope($name) {
throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__));
}
/**
* {@inheritdoc}
*/
public function leaveScope($name) {
throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__));
}
/**
* {@inheritdoc}
*/
public function addScope(ScopeInterface $scope) {
throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__));
}
/**
* {@inheritdoc}
*/
public function hasScope($name) {
throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__));
}
/**
* {@inheritdoc}
*/
public function isScopeActive($name) {
throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__));
}
/**
* Gets all defined service IDs.
*
* @return array
* An array of all defined service IDs.
*/
public function getServiceIds() {
return array_keys($this->serviceDefinitions + $this->services);
}
}

View file

@ -0,0 +1,513 @@
<?php
/**
* @file
* Contains \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper.
*/
namespace Drupal\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Dumper\Dumper;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* OptimizedPhpArrayDumper dumps a service container as a serialized PHP array.
*
* The format of this dumper is very similar to the internal structure of the
* ContainerBuilder, but based on PHP arrays and \stdClass objects instead of
* rich value objects for performance reasons.
*
* By removing the abstraction and optimizing some cases like deep collections,
* fewer classes need to be loaded, fewer function calls need to be executed and
* fewer run time checks need to be made.
*
* In addition to that, this container dumper treats private services as
* strictly private with their own private services storage, whereas in the
* Symfony service container builder and PHP dumper, shared private services can
* still be retrieved via get() from the container.
*
* It is machine-optimized, for a human-readable version based on this one see
* \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper.
*
* @see \Drupal\Component\DependencyInjection\Container
*/
class OptimizedPhpArrayDumper extends Dumper {
/**
* Whether to serialize service definitions or not.
*
* Service definitions are serialized by default to avoid having to
* unserialize the whole container on loading time, which improves early
* bootstrap performance for e.g. the page cache.
*
* @var bool
*/
protected $serialize = TRUE;
/**
* {@inheritdoc}
*/
public function dump(array $options = array()) {
return serialize($this->getArray());
}
/**
* Gets the service container definition as a PHP array.
*
* @return array
* A PHP array representation of the service container.
*/
public function getArray() {
$definition = array();
$definition['aliases'] = $this->getAliases();
$definition['parameters'] = $this->getParameters();
$definition['services'] = $this->getServiceDefinitions();
$definition['frozen'] = $this->container->isFrozen();
$definition['machine_format'] = $this->supportsMachineFormat();
return $definition;
}
/**
* Gets the aliases as a PHP array.
*
* @return array
* The aliases.
*/
protected function getAliases() {
$alias_definitions = array();
$aliases = $this->container->getAliases();
foreach ($aliases as $alias => $id) {
$id = (string) $id;
while (isset($aliases[$id])) {
$id = (string) $aliases[$id];
}
$alias_definitions[$alias] = $id;
}
return $alias_definitions;
}
/**
* Gets parameters of the container as a PHP array.
*
* @return array
* The escaped and prepared parameters of the container.
*/
protected function getParameters() {
if (!$this->container->getParameterBag()->all()) {
return array();
}
$parameters = $this->container->getParameterBag()->all();
$is_frozen = $this->container->isFrozen();
return $this->prepareParameters($parameters, $is_frozen);
}
/**
* Gets services of the container as a PHP array.
*
* @return array
* The service definitions.
*/
protected function getServiceDefinitions() {
if (!$this->container->getDefinitions()) {
return array();
}
$services = array();
foreach ($this->container->getDefinitions() as $id => $definition) {
// Only store public service definitions, references to shared private
// services are handled in ::getReferenceCall().
if ($definition->isPublic()) {
$service_definition = $this->getServiceDefinition($definition);
$services[$id] = $this->serialize ? serialize($service_definition) : $service_definition;
}
}
return $services;
}
/**
* Prepares parameters for the PHP array dumping.
*
* @param array $parameters
* An array of parameters.
* @param bool $escape
* Whether keys with '%' should be escaped or not.
*
* @return array
* An array of prepared parameters.
*/
protected function prepareParameters(array $parameters, $escape = TRUE) {
$filtered = array();
foreach ($parameters as $key => $value) {
if (is_array($value)) {
$value = $this->prepareParameters($value, $escape);
}
elseif ($value instanceof Reference) {
$value = $this->dumpValue($value);
}
$filtered[$key] = $value;
}
return $escape ? $this->escape($filtered) : $filtered;
}
/**
* Escapes parameters.
*
* @param array $parameters
* The parameters to escape for '%' characters.
*
* @return array
* The escaped parameters.
*/
protected function escape(array $parameters) {
$args = array();
foreach ($parameters as $key => $value) {
if (is_array($value)) {
$args[$key] = $this->escape($value);
}
elseif (is_string($value)) {
$args[$key] = str_replace('%', '%%', $value);
}
else {
$args[$key] = $value;
}
}
return $args;
}
/**
* Gets a service definition as PHP array.
*
* @param \Symfony\Component\DependencyInjection\Definition $definition
* The definition to process.
*
* @return array
* The service definition as PHP array.
*
* @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* Thrown when the definition is marked as decorated, or with an explicit
* scope different from SCOPE_CONTAINER and SCOPE_PROTOTYPE.
*/
protected function getServiceDefinition(Definition $definition) {
$service = array();
if ($definition->getClass()) {
$service['class'] = $definition->getClass();
}
if (!$definition->isPublic()) {
$service['public'] = FALSE;
}
if ($definition->getFile()) {
$service['file'] = $definition->getFile();
}
if ($definition->isSynthetic()) {
$service['synthetic'] = TRUE;
}
if ($definition->isLazy()) {
$service['lazy'] = TRUE;
}
if ($definition->getArguments()) {
$arguments = $definition->getArguments();
$service['arguments'] = $this->dumpCollection($arguments);
$service['arguments_count'] = count($arguments);
}
else {
$service['arguments_count'] = 0;
}
if ($definition->getProperties()) {
$service['properties'] = $this->dumpCollection($definition->getProperties());
}
if ($definition->getMethodCalls()) {
$service['calls'] = $this->dumpMethodCalls($definition->getMethodCalls());
}
if (($scope = $definition->getScope()) !== ContainerInterface::SCOPE_CONTAINER) {
if ($scope === ContainerInterface::SCOPE_PROTOTYPE) {
// Scope prototype has been replaced with 'shared' => FALSE.
// This is a Symfony 2.8 forward compatibility fix.
// Reference: https://github.com/symfony/symfony/blob/2.8/UPGRADE-2.8.md#dependencyinjection
$service['shared'] = FALSE;
}
else {
throw new InvalidArgumentException("The 'scope' definition is deprecated in Symfony 3.0 and not supported by Drupal 8.");
}
}
if (($decorated = $definition->getDecoratedService()) !== NULL) {
throw new InvalidArgumentException("The 'decorated' definition is not supported by the Drupal 8 run-time container. The Container Builder should have resolved that during the DecoratorServicePass compiler pass.");
}
if ($callable = $definition->getFactory()) {
$service['factory'] = $this->dumpCallable($callable);
}
if ($callable = $definition->getConfigurator()) {
$service['configurator'] = $this->dumpCallable($callable);
}
return $service;
}
/**
* Dumps method calls to a PHP array.
*
* @param array $calls
* An array of method calls.
*
* @return array
* The PHP array representation of the method calls.
*/
protected function dumpMethodCalls(array $calls) {
$code = array();
foreach ($calls as $key => $call) {
$method = $call[0];
$arguments = array();
if (!empty($call[1])) {
$arguments = $this->dumpCollection($call[1]);
}
$code[$key] = [$method, $arguments];
}
return $code;
}
/**
* Dumps a collection to a PHP array.
*
* @param mixed $collection
* A collection to process.
* @param bool &$resolve
* Used for passing the information to the caller whether the given
* collection needed to be resolved or not. This is used for optimizing
* deep arrays that don't need to be traversed.
*
* @return \stdClass|array
* The collection in a suitable format.
*/
protected function dumpCollection($collection, &$resolve = FALSE) {
$code = array();
foreach ($collection as $key => $value) {
if (is_array($value)) {
$resolve_collection = FALSE;
$code[$key] = $this->dumpCollection($value, $resolve_collection);
if ($resolve_collection) {
$resolve = TRUE;
}
}
else {
if (is_object($value)) {
$resolve = TRUE;
}
$code[$key] = $this->dumpValue($value);
}
}
if (!$resolve) {
return $collection;
}
return (object) array(
'type' => 'collection',
'value' => $code,
'resolve' => $resolve,
);
}
/**
* Dumps callable to a PHP array.
*
* @param array|callable $callable
* The callable to process.
*
* @return callable
* The processed callable.
*/
protected function dumpCallable($callable) {
if (is_array($callable)) {
$callable[0] = $this->dumpValue($callable[0]);
$callable = array($callable[0], $callable[1]);
}
return $callable;
}
/**
* Gets a private service definition in a suitable format.
*
* @param string $id
* The ID of the service to get a private definition for.
* @param \Symfony\Component\DependencyInjection\Definition $definition
* The definition to process.
* @param bool $shared
* (optional) Whether the service will be shared with others.
* By default this parameter is FALSE.
*
* @return \stdClass
* A very lightweight private service value object.
*/
protected function getPrivateServiceCall($id, Definition $definition, $shared = FALSE) {
$service_definition = $this->getServiceDefinition($definition);
if (!$id) {
$hash = hash('sha1', serialize($service_definition));
$id = 'private__' . $hash;
}
return (object) array(
'type' => 'private_service',
'id' => $id,
'value' => $service_definition,
'shared' => $shared,
);
}
/**
* Dumps the value to PHP array format.
*
* @param mixed $value
* The value to dump.
*
* @return mixed
* The dumped value in a suitable format.
*
* @throws RuntimeException
* When trying to dump object or resource.
*/
protected function dumpValue($value) {
if (is_array($value)) {
$code = array();
foreach ($value as $k => $v) {
$code[$k] = $this->dumpValue($v);
}
return $code;
}
elseif ($value instanceof Reference) {
return $this->getReferenceCall((string) $value, $value);
}
elseif ($value instanceof Definition) {
return $this->getPrivateServiceCall(NULL, $value);
}
elseif ($value instanceof Parameter) {
return $this->getParameterCall((string) $value);
}
elseif ($value instanceof Expression) {
throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
}
elseif (is_object($value)) {
// Drupal specific: Instantiated objects have a _serviceId parameter.
if (isset($value->_serviceId)) {
return $this->getReferenceCall($value->_serviceId);
}
throw new RuntimeException('Unable to dump a service container if a parameter is an object without _serviceId.');
}
elseif (is_resource($value)) {
throw new RuntimeException('Unable to dump a service container if a parameter is a resource.');
}
return $value;
}
/**
* Gets a service reference for a reference in a suitable PHP array format.
*
* The main difference is that this function treats references to private
* services differently and returns a private service reference instead of
* a normal reference.
*
* @param string $id
* The ID of the service to get a reference for.
* @param \Symfony\Component\DependencyInjection\Reference|NULL $reference
* (optional) The reference object to process; needed to get the invalid
* behavior value.
*
* @return string|\stdClass
* A suitable representation of the service reference.
*/
protected function getReferenceCall($id, Reference $reference = NULL) {
$invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
if ($reference !== NULL) {
$invalid_behavior = $reference->getInvalidBehavior();
}
// Private shared service.
$definition = $this->container->getDefinition($id);
if (!$definition->isPublic()) {
// The ContainerBuilder does not share a private service, but this means a
// new service is instantiated every time. Use a private shared service to
// circumvent the problem.
return $this->getPrivateServiceCall($id, $definition, TRUE);
}
return $this->getServiceCall($id, $invalid_behavior);
}
/**
* Gets a service reference for an ID in a suitable PHP array format.
*
* @param string $id
* The ID of the service to get a reference for.
* @param int $invalid_behavior
* (optional) The invalid behavior of the service.
*
* @return string|\stdClass
* A suitable representation of the service reference.
*/
protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return (object) array(
'type' => 'service',
'id' => $id,
'invalidBehavior' => $invalid_behavior,
);
}
/**
* Gets a parameter reference in a suitable PHP array format.
*
* @param string $name
* The name of the parameter to get a reference for.
*
* @return string|\stdClass
* A suitable representation of the parameter reference.
*/
protected function getParameterCall($name) {
return (object) array(
'type' => 'parameter',
'name' => $name,
);
}
/**
* Whether this supports the machine-optimized format or not.
*
* @return bool
* TRUE if this supports machine-optimized format, FALSE otherwise.
*/
protected function supportsMachineFormat() {
return TRUE;
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* @file
* Contains \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper.
*/
namespace Drupal\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* PhpArrayDumper dumps a service container as a PHP array.
*
* The format of this dumper is a human-readable serialized PHP array, which is
* very similar to the YAML based format, but based on PHP arrays instead of
* YAML strings.
*
* It is human-readable, for a machine-optimized version based on this one see
* \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper.
*
* @see \Drupal\Component\DependencyInjection\PhpArrayContainer
*/
class PhpArrayDumper extends OptimizedPhpArrayDumper {
/**
* {@inheritdoc}
*/
public function getArray() {
$this->serialize = FALSE;
return parent::getArray();
}
/**
* {@inheritdoc}
*/
protected function dumpCollection($collection, &$resolve = FALSE) {
$code = array();
foreach ($collection as $key => $value) {
if (is_array($value)) {
$code[$key] = $this->dumpCollection($value);
}
else {
$code[$key] = $this->dumpValue($value);
}
}
return $code;
}
/**
* {@inheritdoc}
*/
protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return '@?' . $id;
}
return '@' . $id;
}
/**
* {@inheritdoc}
*/
protected function getParameterCall($name) {
return '%' . $name . '%';
}
/**
* {@inheritdoc}
*/
protected function supportsMachineFormat() {
return FALSE;
}
}

View file

@ -0,0 +1,276 @@
<?php
/**
* @file
* Contains \Drupal\Component\DependencyInjection\PhpArrayContainer.
*/
namespace Drupal\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
/**
* Provides a container optimized for Drupal's needs.
*
* This container implementation is compatible with the default Symfony
* dependency injection container and similar to the Symfony ContainerBuilder
* class, but optimized for speed.
*
* It is based on a human-readable PHP array container definition with a
* structure very similar to the YAML container definition.
*
* @see \Drupal\Component\DependencyInjection\Container
* @see \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper
* @see \Drupal\Component\DependencyInjection\DependencySerializationTrait
*
* @ingroup container
*/
class PhpArrayContainer extends Container {
/**
* {@inheritdoc}
*/
public function __construct(array $container_definition = array()) {
if (isset($container_definition['machine_format']) && $container_definition['machine_format'] === TRUE) {
throw new InvalidArgumentException('The machine-optimized format is not supported by this class. Use a human-readable format instead, e.g. as produced by \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper.');
}
// Do not call the parent's constructor as it would bail on the
// machine-optimized format.
$this->aliases = isset($container_definition['aliases']) ? $container_definition['aliases'] : array();
$this->parameters = isset($container_definition['parameters']) ? $container_definition['parameters'] : array();
$this->serviceDefinitions = isset($container_definition['services']) ? $container_definition['services'] : array();
$this->frozen = isset($container_definition['frozen']) ? $container_definition['frozen'] : FALSE;
// Register the service_container with itself.
$this->services['service_container'] = $this;
}
/**
* {@inheritdoc}
*/
protected function createService(array $definition, $id) {
// This method is a verbatim copy of
// \Drupal\Component\DependencyInjection\Container::createService
// except for the following difference:
// - There are no instanceof checks on \stdClass, which are used in the
// parent class to avoid resolving services and parameters when it is
// known from dumping that there is nothing to resolve.
if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) {
throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The service container does not know how to construct this service. The service will need to be set before it is first used.', $id));
}
$arguments = array();
if (isset($definition['arguments'])) {
$arguments = $this->resolveServicesAndParameters($definition['arguments']);
}
if (isset($definition['file'])) {
$file = $this->frozen ? $definition['file'] : current($this->resolveServicesAndParameters(array($definition['file'])));
require_once $file;
}
if (isset($definition['factory'])) {
$factory = $definition['factory'];
if (is_array($factory)) {
$factory = $this->resolveServicesAndParameters(array($factory[0], $factory[1]));
}
elseif (!is_string($factory)) {
throw new RuntimeException(sprintf('Cannot create service "%s" because of invalid factory', $id));
}
$service = call_user_func_array($factory, $arguments);
}
else {
$class = $this->frozen ? $definition['class'] : current($this->resolveServicesAndParameters(array($definition['class'])));
$length = isset($definition['arguments_count']) ? $definition['arguments_count'] : count($arguments);
// Optimize class instantiation for services with up to 10 parameters as
// reflection is noticeably slow.
switch ($length) {
case 0:
$service = new $class();
break;
case 1:
$service = new $class($arguments[0]);
break;
case 2:
$service = new $class($arguments[0], $arguments[1]);
break;
case 3:
$service = new $class($arguments[0], $arguments[1], $arguments[2]);
break;
case 4:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
break;
case 5:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
break;
case 6:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]);
break;
case 7:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]);
break;
case 8:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7]);
break;
case 9:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8]);
break;
case 10:
$service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8], $arguments[9]);
break;
default:
$r = new \ReflectionClass($class);
$service = $r->newInstanceArgs($arguments);
break;
}
}
// Share the service if it is public.
if (!isset($definition['public']) || $definition['public'] !== FALSE) {
// Forward compatibility fix for Symfony 2.8 update.
if (!isset($definition['shared']) || $definition['shared'] !== FALSE) {
$this->services[$id] = $service;
}
}
if (isset($definition['calls'])) {
foreach ($definition['calls'] as $call) {
$method = $call[0];
$arguments = array();
if (!empty($call[1])) {
$arguments = $call[1];
$arguments = $this->resolveServicesAndParameters($arguments);
}
call_user_func_array(array($service, $method), $arguments);
}
}
if (isset($definition['properties'])) {
$definition['properties'] = $this->resolveServicesAndParameters($definition['properties']);
foreach ($definition['properties'] as $key => $value) {
$service->{$key} = $value;
}
}
if (isset($definition['configurator'])) {
$callable = $definition['configurator'];
if (is_array($callable)) {
$callable = $this->resolveServicesAndParameters($callable);
}
if (!is_callable($callable)) {
throw new InvalidArgumentException(sprintf('The configurator for class "%s" is not a callable.', get_class($service)));
}
call_user_func($callable, $service);
}
return $service;
}
/**
* {@inheritdoc}
*/
protected function resolveServicesAndParameters($arguments) {
// This method is different from the parent method only for the following
// cases:
// - A service is denoted by '@service' and not by a \stdClass object.
// - A parameter is denoted by '%parameter%' and not by a \stdClass object.
// - The depth of the tree representing the arguments is not known in
// advance, so it needs to be fully traversed recursively.
foreach ($arguments as $key => $argument) {
if ($argument instanceof \stdClass) {
$type = $argument->type;
// Private services are a special flavor: In case a private service is
// only used by one other service, the ContainerBuilder uses a
// Definition object as an argument, which does not have an ID set.
// Therefore the format uses a \stdClass object to store the definition
// and to be able to create the service on the fly.
//
// Note: When constructing a private service by hand, 'id' must be set.
//
// The PhpArrayDumper just uses the hash of the private service
// definition to generate a unique ID.
//
// @see \Drupal\Component\DependecyInjection\Dumper\OptimizedPhpArrayDumper::getPrivateServiceCall
if ($type == 'private_service') {
$id = $argument->id;
// Check if the private service already exists - in case it is shared.
if (!empty($argument->shared) && isset($this->privateServices[$id])) {
$arguments[$key] = $this->privateServices[$id];
continue;
}
// Create a private service from a service definition.
$arguments[$key] = $this->createService($argument->value, $id);
if (!empty($argument->shared)) {
$this->privateServices[$id] = $arguments[$key];
}
continue;
}
if ($type !== NULL) {
throw new InvalidArgumentException("Undefined type '$type' while resolving parameters and services.");
}
}
if (is_array($argument)) {
$arguments[$key] = $this->resolveServicesAndParameters($argument);
continue;
}
if (!is_string($argument)) {
continue;
}
// Resolve parameters.
if ($argument[0] === '%') {
$name = substr($argument, 1, -1);
if (!isset($this->parameters[$name])) {
$arguments[$key] = $this->getParameter($name);
// This can never be reached as getParameter() throws an Exception,
// because we already checked that the parameter is not set above.
}
$argument = $this->parameters[$name];
$arguments[$key] = $argument;
}
// Resolve services.
if ($argument[0] === '@') {
$id = substr($argument, 1);
$invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
if ($id[0] === '?') {
$id = substr($id, 1);
$invalid_behavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
}
if (isset($this->services[$id])) {
$arguments[$key] = $this->services[$id];
}
else {
$arguments[$key] = $this->get($id, $invalid_behavior);
}
}
}
return $arguments;
}
}

View file

@ -0,0 +1,18 @@
{
"name": "drupal/core-dependency-injection",
"description": "Dependency Injection container optimized for Drupal's needs.",
"keywords": ["drupal", "dependency injection"],
"type": "library",
"homepage": "https://www.drupal.org/project/drupal",
"license": "GPL-2.0+",
"support": {
"issues": "https://www.drupal.org/project/issues/drupal",
"irc": "irc://irc.freenode.net/drupal-contribute",
"source": "https://www.drupal.org/project/drupal/git-instructions"
},
"autoload": {
"psr-4": {
"Drupal\\Component\\DependencyInjection\\": ""
}
}
}

View file

@ -8,7 +8,6 @@
namespace Drupal\Component\Diff\Engine;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\SafeMarkup;
/**
* Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
@ -38,10 +37,10 @@ class HWLDFWordAccumulator {
protected function _flushGroup($new_tag) {
if ($this->group !== '') {
if ($this->tag == 'mark') {
$this->line = SafeMarkup::format('@original_line<span class="diffchange">@group</span>', ['@original_line' => $this->line, '@group' => $this->group]);
$this->line = $this->line . '<span class="diffchange">' . $this->group . '</span>';
}
else {
$this->line = SafeMarkup::format('@original_line@group', ['@original_line' => $this->line, '@group' => $this->group]);
$this->line = $this->line . $this->group;
}
}
$this->group = '';

View file

@ -102,4 +102,10 @@ class FileReadOnlyStorage implements PhpStorageInterface {
return $names;
}
/**
* {@inheritdoc}
*/
public function garbageCollection() {
}
}

View file

@ -266,4 +266,10 @@ EOF;
return $names;
}
/**
* {@inheritdoc}
*/
public function garbageCollection() {
}
}

View file

@ -63,7 +63,7 @@ class MTimeProtectedFastFileStorage extends FileStorage {
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::save().
* {@inheritdoc}
*/
public function save($name, $data) {
$this->ensureDirectory($this->directory);
@ -78,44 +78,32 @@ class MTimeProtectedFastFileStorage extends FileStorage {
// permission.
chmod($temporary_path, 0444);
// Prepare a directory dedicated for just this file. Ensure it has a current
// mtime so that when the file (hashed on that mtime) is moved into it, the
// mtime remains the same (unless the clock ticks to the next second during
// the rename, in which case we'll try again).
$directory = $this->getContainingDirectoryFullPath($name);
if (file_exists($directory)) {
$this->unlink($directory);
}
$this->ensureDirectory($directory);
// Determine the exact modification time of the file.
$mtime = $this->getUncachedMTime($temporary_path);
// Move the file to its final place. The mtime of a directory is the time of
// the last file create or delete in the directory. So the moving will
// update the directory mtime. However, this update will very likely not
// show up, because it has a coarse, one second granularity and typical
// moves takes significantly less than that. In the unlucky case the clock
// ticks during the move, we need to keep trying until the mtime we hashed
// on and the updated mtime match.
$previous_mtime = 0;
$i = 0;
while (($mtime = $this->getUncachedMTime($directory)) && ($mtime != $previous_mtime)) {
$previous_mtime = $mtime;
// Reset the file back in the temporary location if this is not the first
// iteration.
if ($i > 0) {
$this->unlink($temporary_path);
$temporary_path = $this->tempnam($this->directory, '.');
rename($full_path, $temporary_path);
// Make sure to not loop infinitely on a hopelessly slow filesystem.
if ($i > 10) {
$this->unlink($temporary_path);
return FALSE;
}
}
$full_path = $this->getFullPath($name, $directory, $mtime);
rename($temporary_path, $full_path);
$i++;
// Move the temporary file into the proper directory. Note that POSIX
// compliant systems as well as modern Windows perform the rename operation
// atomically, i.e. there is no point at which another process attempting to
// access the new path will find it missing.
$directory = $this->getContainingDirectoryFullPath($name);
$this->ensureDirectory($directory);
$full_path = $this->getFullPath($name, $directory, $mtime);
$result = rename($temporary_path, $full_path);
// Finally reset the modification time of the directory to match the one of
// the newly created file. In order to prevent the creation of a file if the
// directory does not exist, ensure that the path terminates with a
// directory separator.
//
// Recall that when subsequently loading the file, the hash is calculated
// based on the file name, the containing mtime, and a the secret string.
// Hence updating the mtime here is comparable to pointing a symbolic link
// at a new target, i.e., the newly created file.
if ($result) {
$result &= touch($directory . '/', $mtime);
}
return TRUE;
return (bool) $result;
}
/**
@ -161,6 +149,44 @@ class MTimeProtectedFastFileStorage extends FileStorage {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function garbageCollection() {
$flags = \FilesystemIterator::CURRENT_AS_FILEINFO;
$flags += \FilesystemIterator::SKIP_DOTS;
foreach ($this->listAll() as $name) {
$directory = $this->getContainingDirectoryFullPath($name);
try {
$dir_iterator = new \FilesystemIterator($directory, $flags);
}
catch (\UnexpectedValueException $e) {
// FilesystemIterator throws an UnexpectedValueException if the
// specified path is not a directory, or if it is not accessible.
continue;
}
$directory_unlink = TRUE;
$directory_mtime = filemtime($directory);
foreach ($dir_iterator as $fileinfo) {
if ($directory_mtime > $fileinfo->getMTime()) {
// Ensure the folder is writable.
@chmod($directory, 0777);
@unlink($fileinfo->getPathName());
}
else {
// The directory still contains valid files.
$directory_unlink = FALSE;
}
}
if ($directory_unlink) {
$this->unlink($name);
}
}
}
/**
* Gets the full path of the containing directory where the file is or should
* be stored.
@ -208,4 +234,5 @@ class MTimeProtectedFastFileStorage extends FileStorage {
} while (file_exists($path));
return $path;
}
}

View file

@ -99,4 +99,11 @@ interface PhpStorageInterface {
*/
public function listAll();
/**
* Performs garbage collection on the storage.
*
* The storage may choose to delete expired or invalidated items.
*/
public function garbageCollection();
}

View file

@ -46,7 +46,7 @@ class ReflectionFactory extends DefaultFactory {
* @param string $plugin_id
* The identifier of the plugin implementation.
* @param mixed $plugin_definition
* The definition associated to the plugin_id.
* The definition associated with the plugin_id.
* @param array $configuration
* An array of configuration that may be passed to the instance.
*

View file

@ -338,14 +338,59 @@ EOD;
* "&lt;", not "<"). Be careful when using this function, as it will revert
* previous sanitization efforts (&lt;script&gt; will become <script>).
*
* This method is not the opposite of Html::escape(). For example, this method
* will convert "&eacute;" to "é", whereas Html::escape() will not convert "é"
* to "&eacute;".
*
* @param string $text
* The text to decode entities in.
*
* @return string
* The input $text, with all HTML entities decoded once.
*
* @see html_entity_decode()
* @see \Drupal\Component\Utility\Html::escape()
*/
public static function decodeEntities($text) {
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
/**
* Escapes text by converting special characters to HTML entities.
*
* This method escapes HTML for sanitization purposes by replacing the
* following special characters with their HTML entity equivalents:
* - & (ampersand) becomes &amp;
* - " (double quote) becomes &quot;
* - ' (single quote) becomes &#039;
* - < (less than) becomes &lt;
* - > (greater than) becomes &gt;
* Special characters that have already been escaped will be double-escaped
* (for example, "&lt;" becomes "&amp;lt;"), and invalid UTF-8 encoding
* will be converted to the Unicode replacement character ("<EFBFBD>").
*
* This method is not the opposite of Html::decodeEntities(). For example,
* this method will not encode "é" to "&eacute;", whereas
* Html::decodeEntities() will convert all HTML entities to UTF-8 bytes,
* including "&eacute;" and "&lt;" to "é" and "<".
*
* When constructing @link theme_render render arrays @endlink passing the output of Html::escape() to
* '#markup' is not recommended. Use the '#plain_text' key instead and the
* renderer will autoescape the text.
*
* @param string $text
* The input text.
*
* @return string
* The text with all HTML special characters converted.
*
* @see htmlspecialchars()
* @see \Drupal\Component\Utility\Html::decodeEntities()
*
* @ingroup sanitization
*/
public static function escape($text) {
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}

View file

@ -15,9 +15,9 @@ namespace Drupal\Component\Utility;
* provides a store for known safe strings and methods to manage them
* throughout the page request.
*
* Strings sanitized by self::checkPlain() and self::escape() or
* self::xssFilter() are automatically marked safe, as are markup strings
* created from @link theme_render render arrays @endlink via drupal_render().
* 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
@ -35,57 +35,28 @@ 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();
/**
* Adds a string to a list of strings marked as secure.
*
* 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::set('<');
* @endcode
* or:
* @code
* SafeMarkup::set('<script>');
* @endcode
*
* @param string $string
* The content to be marked as secure.
* @param string $strategy
* The escaping strategy used for this string. Two values 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.
*
* @return string
* The input string that was marked as safe.
*/
public static function set($string, $strategy = 'html') {
$string = (string) $string;
static::$safeStrings[$string][$strategy] = TRUE;
return $string;
}
/**
* Checks if a string is safe to output.
*
* @param string|\Drupal\Component\Utility\SafeStringInterface $string
* The content to be checked.
* @param string $strategy
* The escaping strategy. See self::set(). Defaults to 'html'.
* 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.
*
* @return bool
* TRUE if the string has been marked secure, FALSE otherwise.
@ -100,14 +71,34 @@ class SafeMarkup {
/**
* Adds previously retrieved known safe strings to the safe string list.
*
* This is useful for the batch and form APIs, where it is important to
* preserve the safe markup state across page requests. The strings will be
* added to any safe strings already marked for the current request.
* 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) {
@ -124,98 +115,6 @@ class SafeMarkup {
}
}
/**
* Encodes special characters in a plain-text string for display as HTML.
*
* @param string $string
* A string.
*
* @return string
* The escaped string. If $string was already set as safe with
* self::set(), it won't be escaped again.
*/
public static function escape($string) {
return static::isSafe($string) ? $string : static::checkPlain($string);
}
/**
* Applies a very permissive XSS/HTML filter for admin-only use.
*
* Note: This method only filters if $string is not marked safe already.
*
* @deprecated as of Drupal 8.0.x, will be removed before Drupal 8.0.0. If the
* string used as part of a @link theme_render render array @endlink use
* #markup to allow the render system to filter automatically. If the result
* is not being used directly in the rendering system (for example, when its
* result is being combined with other strings before rendering), use
* Xss::filterAdmin(). Otherwise, use SafeMarkup::xssFilter() and the tag
* list provided by Xss::getAdminTagList() instead. In the rare instance
* that the caller does not want to filter strings that are marked safe
* already, it needs to check SafeMarkup::isSafe() itself.
*
* @see \Drupal\Component\Utility\SafeMarkup::xssFilter()
* @see \Drupal\Component\Utility\SafeMarkup::isSafe()
* @see \Drupal\Component\Utility\Xss::filterAdmin()
* @see \Drupal\Component\Utility\Xss::getAdminTagList()
*/
public static function checkAdminXss($string) {
return static::isSafe($string) ? $string : static::xssFilter($string, Xss::getAdminTagList());
}
/**
* Filters HTML for XSS vulnerabilities and marks the result as safe.
*
* Calling this method unnecessarily will result in bloating the safe string
* list and increases the chance of unintended side effects.
*
* If Twig receives a value that is not marked as safe then it will
* automatically encode special characters in a plain-text string for display
* as HTML. Therefore, SafeMarkup::xssFilter() should only be used when the
* string might contain HTML that needs to be rendered properly by the
* browser.
*
* If you need to filter for admin use, like Xss::filterAdmin(), then:
* - If the string is used as part of a @link theme_render render array @endlink,
* use #markup to allow the render system to filter by the admin tag list
* automatically.
* - Otherwise, use the SafeMarkup::xssFilter() with tag list provided by
* Xss::getAdminTagList() instead.
*
* This method should only be used instead of Xss::filter() when the result is
* being added to a render array that is constructed before rendering begins.
*
* In the rare instance that the caller does not want to filter strings that
* are marked safe already, it needs to check SafeMarkup::isSafe() itself.
*
* @param $string
* The string with raw HTML in it. It will be stripped of everything that
* can cause an XSS attack. The string provided will always be escaped
* regardless of whether the string is already marked as safe.
* @param array $html_tags
* (optional) An array of HTML tags. If omitted, it uses the default tag
* list defined by \Drupal\Component\Utility\Xss::filter().
*
* @return string
* An XSS-safe version of $string, or an empty string if $string is not
* valid UTF-8. The string is marked as safe.
*
* @ingroup sanitization
*
* @see \Drupal\Component\Utility\Xss::filter()
* @see \Drupal\Component\Utility\Xss::filterAdmin()
* @see \Drupal\Component\Utility\Xss::getAdminTagList()
* @see \Drupal\Component\Utility\SafeMarkup::isSafe()
*/
public static function xssFilter($string, $html_tags = NULL) {
if (is_null($html_tags)) {
$string = Xss::filter($string);
}
else {
$string = Xss::filter($string, $html_tags);
}
return static::set($string);
}
/**
* Gets all strings currently marked as safe.
*
@ -244,10 +143,17 @@ class SafeMarkup {
*
* @ingroup sanitization
*
* @deprecated Will be removed before Drupal 8.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
* possible, \Drupal\Component\Utility\Html::escape() can be used in places
* where explicit escaping is needed.
*
* @see drupal_validate_utf8()
*/
public static function checkPlain($text) {
$string = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
$string = Html::escape($text);
static::$safeStrings[$string]['html'] = TRUE;
return $string;
}
@ -275,8 +181,8 @@ class SafeMarkup {
* 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 and formatted using self::placeholder(),
* which makes the following HTML code:
* - %variable: Escaped to HTML wrapped in <em> tags, which makes the
* following HTML code:
* @code
* <em class="placeholder">text output here.</em>
* @endcode
@ -296,7 +202,7 @@ class SafeMarkup {
*
* @see t()
*/
public static function format($string, array $args = array()) {
public static function format($string, array $args) {
$safe = TRUE;
// Transform arguments before inserting them.
@ -304,13 +210,18 @@ class SafeMarkup {
switch ($key[0]) {
case '@':
// Escaped only.
$args[$key] = static::escape($value);
if (!SafeMarkup::isSafe($value)) {
$args[$key] = Html::escape($value);
}
break;
case '%':
default:
// Escaped and placeholder.
$args[$key] = static::placeholder($value);
if (!SafeMarkup::isSafe($value)) {
$value = Html::escape($value);
}
$args[$key] = '<em class="placeholder">' . $value . '</em>';
break;
case '!':
@ -329,68 +240,4 @@ class SafeMarkup {
return $output;
}
/**
* Formats text for emphasized display in a placeholder inside a sentence.
*
* Used automatically by self::format().
*
* @param string $text
* The text to format (plain-text).
*
* @return string
* The formatted text (html).
*/
public static function placeholder($text) {
$string = '<em class="placeholder">' . static::escape($text) . '</em>';
static::$safeStrings[$string]['html'] = TRUE;
return $string;
}
/**
* Replaces all occurrences of the search string with the replacement string.
*
* Functions identically to str_replace(), but marks the returned output as
* safe if all the inputs and the subject have also been marked as safe.
*
* @param string|array $search
* The value being searched for. An array may be used to designate multiple
* values to search for.
* @param string|array $replace
* The replacement value that replaces found search values. An array may be
* used to designate multiple replacements.
* @param string $subject
* The string or array being searched and replaced on.
*
* @return string
* The passed subject with replaced values.
*/
public static function replace($search, $replace, $subject) {
$output = str_replace($search, $replace, $subject);
// If any replacement is unsafe, then the output is also unsafe, so just
// return the output.
if (!is_array($replace)) {
if (!SafeMarkup::isSafe($replace)) {
return $output;
}
}
else {
foreach ($replace as $replacement) {
if (!SafeMarkup::isSafe($replacement)) {
return $output;
}
}
}
// If the subject is unsafe, then the output is as well, so return it.
if (!SafeMarkup::isSafe($subject)) {
return $output;
}
else {
// If we have reached this point, then all replacements were safe. If the
// subject was also safe, then mark the entire output as safe.
return SafeMarkup::set($output);
}
}
}

View file

@ -10,20 +10,29 @@ 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. Currently, there is no use case for this
* interface in contrib or custom code.
* 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\SafeMarkup::set()
* @see \Drupal\Component\Utility\SafeStringTrait
* @see \Drupal\Component\Utility\SafeMarkup::isSafe()
* @see \Drupal\Core\Template\TwigExtension::escapeFilter()
*/
interface SafeStringInterface {
interface SafeStringInterface extends \JsonSerializable {
/**
* Returns a safe string.

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\Component\Utility\SafeStringTrait.
*/
namespace Drupal\Component\Utility;
/**
* Implements SafeStringInterface and Countable for rendered objects.
*
* @see \Drupal\Component\Utility\SafeStringInterface
*/
trait SafeStringTrait {
/**
* The safe string.
*
* @var string
*/
protected $string;
/**
* Creates a SafeString object if necessary.
*
* If $string is equal to a blank string then it is not necessary to create a
* SafeString object. If $string is an object that implements
* SafeStringInterface it is returned unchanged.
*
* @param mixed $string
* The string to mark as safe. This value will be cast to a string.
*
* @return string|\Drupal\Component\Utility\SafeStringInterface
* A safe string.
*/
public static function create($string) {
if ($string instanceof SafeStringInterface) {
return $string;
}
$string = (string) $string;
if ($string === '') {
return '';
}
$safe_string = new static();
$safe_string->string = $string;
return $safe_string;
}
/**
* Returns the string version of the SafeString object.
*
* @return string
* The safe string content.
*/
public function __toString() {
return $this->string;
}
/**
* Returns the string length.
*
* @return int
* The length of the string.
*/
public function count() {
return Unicode::strlen($this->string);
}
/**
* Returns a representation of the object for use in JSON serialization.
*
* @return string
* The safe string content.
*/
public function jsonSerialize() {
return $this->__toString();
}
}

View file

@ -508,7 +508,7 @@ EOD;
* @param bool $add_ellipsis
* If TRUE, add '...' to the end of the truncated string (defaults to
* FALSE). The string length will still fall within $max_length.
* @param bool $min_wordsafe_length
* @param int $min_wordsafe_length
* If $wordsafe is TRUE, the minimum acceptable length for truncation (before
* adding an ellipsis, if $add_ellipsis is TRUE). Has no effect if $wordsafe
* is FALSE. This can be used to prevent having a very short resulting string

View file

@ -272,7 +272,7 @@ class UrlHelper {
// Get the plain text representation of the attribute value (i.e. its
// meaning).
$string = Html::decodeEntities($string);
return SafeMarkup::checkPlain(static::stripDangerousProtocols($string));
return Html::escape(static::stripDangerousProtocols($string));
}
/**
@ -300,10 +300,11 @@ class UrlHelper {
*
* This function must be called for all URIs within user-entered input prior
* to being output to an HTML attribute value. It is often called as part of
* check_url() or 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.
* \Drupal\Component\Utility\UrlHelper::filterBadProtocol() or
* \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.
*
* @param string $uri
* A plain-text URI that might contain dangerous protocols.

View file

@ -46,9 +46,9 @@ class Variable {
elseif (is_string($var)) {
if (strpos($var, "\n") !== FALSE || strpos($var, "'") !== FALSE) {
// If the string contains a line break or a single quote, use the
// double quote export mode. Encode backslash and double quotes and
// transform some common control characters.
$var = str_replace(array('\\', '"', "\n", "\r", "\t"), array('\\\\', '\"', '\n', '\r', '\t'), $var);
// double quote export mode. Encode backslash, dollar symbols, and
// double quotes and transform some common control characters.
$var = str_replace(array('\\', '$', '"', "\n", "\r", "\t"), array('\\\\', '\$', '\"', '\n', '\r', '\t'), $var);
$output = '"' . $var . '"';
}
else {

View file

@ -15,7 +15,7 @@ namespace Drupal\Component\Utility;
class Xss {
/**
* The list of html tags allowed by filterAdmin().
* The list of HTML tags allowed by filterAdmin().
*
* @var array
*
@ -23,19 +23,21 @@ class Xss {
*/
protected static $adminTags = array('a', 'abbr', 'acronym', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'menu', 'meter', 'nav', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr');
/**
* The default list of HTML tags allowed by filter().
*
* @var array
*
* @see \Drupal\Component\Utility\Xss::filter()
*/
protected static $htmlTags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd');
/**
* Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
*
* Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses.
* For examples of various XSS attacks, see: http://ha.ckers.org/xss.html.
*
* This method is preferred to
* \Drupal\Component\Utility\SafeMarkup::xssFilter() when the result is not
* being used directly in the rendering system (for example, when its result
* is being combined with other strings before rendering). This avoids
* bloating the safe string list with partial strings if the whole result will
* be marked safe.
*
* This code does four things:
* - Removes characters and constructs that can trick browsers.
* - Makes sure all HTML entities are well-formed.
@ -54,11 +56,13 @@ class Xss {
* valid UTF-8.
*
* @see \Drupal\Component\Utility\Unicode::validateUtf8()
* @see \Drupal\Component\Utility\SafeMarkup::xssFilter()
*
* @ingroup sanitization
*/
public static function filter($string, $html_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) {
public static function filter($string, array $html_tags = NULL) {
if (is_null($html_tags)) {
$html_tags = static::$htmlTags;
}
// Only operate on valid UTF-8 strings. This is necessary to prevent cross
// site scripting issues on Internet Explorer 6.
if (!Unicode::validateUtf8($string)) {
@ -84,10 +88,7 @@ class Xss {
$splitter = function ($matches) use ($html_tags, $class) {
return $class::split($matches[1], $html_tags, $class);
};
// Strip any tags that are not in the whitelist, then mark the text as safe
// for output. All other known XSS vectors have been filtered out by this
// point and any HTML tags remaining will have been deliberately allowed, so
// it is acceptable to call SafeMarkup::set() on the resultant string.
// Strip any tags that are not in the whitelist.
return preg_replace_callback('%
(
<(?=[^a-zA-Z!/]) # a lone <
@ -108,13 +109,6 @@ class Xss {
* is desired (so \Drupal\Component\Utility\SafeMarkup::checkPlain() is
* not acceptable).
*
* This method is preferred to
* \Drupal\Component\Utility\SafeMarkup::xssFilter() when the result is
* not being used directly in the rendering system (for example, when its
* result is being combined with other strings before rendering). This avoids
* bloating the safe string list with partial strings if the whole result will
* be marked safe.
*
* Allows all tags that can be used inside an HTML body, save
* for scripts and styles.
*
@ -126,7 +120,6 @@ class Xss {
*
* @ingroup sanitization
*
* @see \Drupal\Component\Utility\SafeMarkup::xssFilter()
* @see \Drupal\Component\Utility\Xss::getAdminTagList()
*
*/
@ -338,13 +331,22 @@ class Xss {
}
/**
* Gets the list of html tags allowed by Xss::filterAdmin().
* Gets the list of HTML tags allowed by Xss::filterAdmin().
*
* @return array
* The list of html tags allowed by filterAdmin().
* The list of HTML tags allowed by filterAdmin().
*/
public static function getAdminTagList() {
return static::$adminTags;
}
/**
* Gets the standard list of HTML tags allowed by Xss::filter().
*
* @return array
* The list of HTML tags allowed by Xss::filter().
*/
public static function getHtmlTagList() {
return static::$htmlTags;
}
}

View file

@ -8,7 +8,7 @@
namespace Drupal\Component\Uuid;
/**
* Interface that defines a UUID backend.
* Interface for generating UUIDs.
*/
interface UuidInterface {

View file

@ -8,6 +8,8 @@ namespace Drupal\Core\Access;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Config\ConfigBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
@ -25,47 +27,10 @@ use Drupal\Core\Session\AccountInterface;
*
* When using ::orIf() and ::andIf(), cacheability metadata will be merged
* accordingly as well.
*
* @todo Use RefinableCacheableDependencyInterface and the corresponding trait in
* https://www.drupal.org/node/2526326.
*/
abstract class AccessResult implements AccessResultInterface, CacheableDependencyInterface {
abstract class AccessResult implements AccessResultInterface, RefinableCacheableDependencyInterface {
/**
* The cache context IDs (to vary a cache item ID based on active contexts).
*
* @see \Drupal\Core\Cache\Context\CacheContextInterface
* @see \Drupal\Core\Cache\Context\CacheContextsManager::convertTokensToKeys()
*
* @var string[]
*/
protected $contexts;
/**
* The cache tags.
*
* @var array
*/
protected $tags;
/**
* The maximum caching time in seconds.
*
* @var int
*/
protected $maxAge;
/**
* Constructs a new AccessResult object.
*/
public function __construct() {
$this->resetCacheContexts()
->resetCacheTags()
// Max-age must be non-zero for an access result to be cacheable.
// Typically, cache items are invalidated via associated cache tags, not
// via a maximum age.
->setCacheMaxAge(Cache::PERMANENT);
}
use RefinableCacheableDependencyTrait;
/**
* Creates an AccessResultInterface object with isNeutral() === TRUE.
@ -215,35 +180,21 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
* {@inheritdoc}
*/
public function getCacheContexts() {
sort($this->contexts);
return $this->contexts;
return $this->cacheContexts;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->tags;
return $this->cacheTags;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->maxAge;
}
/**
* Adds cache contexts associated with the access result.
*
* @param string[] $contexts
* An array of cache context IDs, used to generate a cache ID.
*
* @return $this
*/
public function addCacheContexts(array $contexts) {
$this->contexts = array_unique(array_merge($this->contexts, $contexts));
return $this;
return $this->cacheMaxAge;
}
/**
@ -252,20 +203,7 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
* @return $this
*/
public function resetCacheContexts() {
$this->contexts = array();
return $this;
}
/**
* Adds cache tags associated with the access result.
*
* @param array $tags
* An array of cache tags.
*
* @return $this
*/
public function addCacheTags(array $tags) {
$this->tags = Cache::mergeTags($this->tags, $tags);
$this->cacheContexts = [];
return $this;
}
@ -275,7 +213,7 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
* @return $this
*/
public function resetCacheTags() {
$this->tags = array();
$this->cacheTags = [];
return $this;
}
@ -288,7 +226,7 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
* @return $this
*/
public function setCacheMaxAge($max_age) {
$this->maxAge = $max_age;
$this->cacheMaxAge = $max_age;
return $this;
}
@ -342,28 +280,6 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
return $this->addCacheableDependency($configuration);
}
/**
* Adds a dependency on an object: merges its cacheability metadata.
*
* @param \Drupal\Core\Cache\CacheableDependencyInterface|object $other_object
* The dependency. If the object implements CacheableDependencyInterface,
* then its cacheability metadata will be used. Otherwise, the passed in
* object must be assumed to be uncacheable, so max-age 0 is set.
*
* @return $this
*/
public function addCacheableDependency($other_object) {
if ($other_object instanceof CacheableDependencyInterface) {
$this->contexts = Cache::mergeContexts($this->contexts, $other_object->getCacheContexts());
$this->tags = Cache::mergeTags($this->tags, $other_object->getCacheTags());
$this->maxAge = Cache::mergeMaxAges($this->maxAge, $other_object->getCacheMaxAge());
}
else {
$this->maxAge = 0;
}
return $this;
}
/**
* {@inheritdoc}
*/
@ -452,12 +368,19 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
/**
* Inherits the cacheability of the other access result, if any.
*
* inheritCacheability() differs from addCacheableDependency() in how it
* handles max-age, because it is designed to inherit the cacheability of the
* second operand in the andIf() and orIf() operations. There, the situation
* "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000
* as the end result.
*
* @param \Drupal\Core\Access\AccessResultInterface $other
* The other access result, whose cacheability (if any) to inherit.
*
* @return $this
*/
public function inheritCacheability(AccessResultInterface $other) {
$this->addCacheableDependency($other);
if ($other instanceof CacheableDependencyInterface) {
if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
$this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
@ -465,14 +388,6 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
else {
$this->setCacheMaxAge($other->getCacheMaxAge());
}
$this->addCacheContexts($other->getCacheContexts());
$this->addCacheTags($other->getCacheTags());
}
// If any of the access results don't provide cacheability metadata, then
// we cannot cache the combined access result, for we may not make
// assumptions.
else {
$this->setCacheMaxAge(0);
}
return $this;
}

View file

@ -164,15 +164,15 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
$resource_commands = array();
if ($css_assets) {
$css_render_array = $this->cssCollectionRenderer->render($css_assets);
$resource_commands[] = new AddCssCommand((string) $this->renderer->renderPlain($css_render_array));
$resource_commands[] = new AddCssCommand($this->renderer->renderPlain($css_render_array));
}
if ($js_assets_header) {
$js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header);
$resource_commands[] = new PrependCommand('head', (string) $this->renderer->renderPlain($js_header_render_array));
$resource_commands[] = new PrependCommand('head', $this->renderer->renderPlain($js_header_render_array));
}
if ($js_assets_footer) {
$js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer);
$resource_commands[] = new AppendCommand('body', (string) $this->renderer->renderPlain($js_footer_render_array));
$resource_commands[] = new AppendCommand('body', $this->renderer->renderPlain($js_footer_render_array));
}
foreach (array_reverse($resource_commands) as $resource_command) {
$response->addCommand($resource_command, TRUE);

View file

@ -29,7 +29,7 @@ trait CommandWithAttachedAssetsTrait {
* If content is a render array, it may contain attached assets to be
* processed.
*
* @return string
* @return string|\Drupal\Component\Utility\SafeStringInterface
* HTML rendered content.
*/
protected function getRenderedContent() {
@ -37,10 +37,10 @@ trait CommandWithAttachedAssetsTrait {
if (is_array($this->content)) {
$html = \Drupal::service('renderer')->renderRoot($this->content);
$this->attachedAssets = AttachedAssets::createFromRenderArray($this->content);
return (string) $html;
return $html;
}
else {
return (string) $this->content;
return $this->content;
}
}

View file

@ -127,7 +127,6 @@ class AssetResolver implements AssetResolverInterface {
'type' => 'file',
'group' => CSS_AGGREGATE_DEFAULT,
'weight' => 0,
'every_page' => FALSE,
'media' => 'all',
'preprocess' => TRUE,
'browsers' => [],
@ -221,7 +220,7 @@ class AssetResolver implements AssetResolverInterface {
// Add the theme name to the cache key since themes may implement
// hook_js_alter(). Additionally add the current language to support
// translation of JavaScript files.
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets));
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
@ -231,7 +230,6 @@ class AssetResolver implements AssetResolverInterface {
$default_options = [
'type' => 'file',
'group' => JS_DEFAULT,
'every_page' => FALSE,
'weight' => 0,
'cache' => TRUE,
'preprocess' => TRUE,
@ -338,7 +336,6 @@ class AssetResolver implements AssetResolverInterface {
$settings_as_inline_javascript = [
'type' => 'setting',
'group' => JS_SETTING,
'every_page' => TRUE,
'weight' => 0,
'browsers' => [],
'data' => $settings,
@ -384,16 +381,6 @@ class AssetResolver implements AssetResolverInterface {
elseif ($a['group'] > $b['group']) {
return 1;
}
// Within a group, order all infrequently needed, page-specific files after
// common files needed throughout the website. Separating this way allows
// for the aggregate file generated for all of the common files to be reused
// across a site visit without being cut by a page using a less common file.
elseif ($a['every_page'] && !$b['every_page']) {
return -1;
}
elseif (!$a['every_page'] && $b['every_page']) {
return 1;
}
// Finally, order by weight.
elseif ($a['weight'] < $b['weight']) {
return -1;

View file

@ -58,9 +58,8 @@ class CssCollectionGrouper implements AssetCollectionGrouperInterface {
case 'file':
// Group file items if their 'preprocess' flag is TRUE.
// Help ensure maximum reuse of aggregate files by only grouping
// together items that share the same 'group' value and 'every_page'
// flag.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['media'], $item['browsers']) : FALSE;
// together items that share the same 'group' value.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['media'], $item['browsers']) : FALSE;
break;
case 'inline':

View file

@ -7,7 +7,7 @@
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\State\StateInterface;
/**
@ -103,7 +103,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
// For filthy IE hack.
$current_ie_group_keys = NULL;
$get_ie_group_key = function ($css_asset) {
return array($css_asset['type'], $css_asset['preprocess'], $css_asset['group'], $css_asset['every_page'], $css_asset['media'], $css_asset['browsers']);
return array($css_asset['type'], $css_asset['preprocess'], $css_asset['group'], $css_asset['media'], $css_asset['browsers']);
};
// Loop through all CSS assets, by key, to allow for the special IE
@ -123,9 +123,9 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
// LINK tag.
// - file CSS assets that can be aggregated (and possibly have been):
// in this case, figure out which subsequent file CSS assets share
// the same key properties ('group', 'every_page', 'media' and
// 'browsers') and output this group into as few STYLE tags as
// possible (a STYLE tag may contain only 31 @import statements).
// the same key properties ('group', 'media' and 'browsers') and
// output this group into as few STYLE tags as possible (a STYLE
// tag may contain only 31 @import statements).
case 'file':
// The dummy query string needs to be added to the URL to control
// browser-caching.
@ -159,7 +159,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
$import = array();
// Start with the current CSS asset, iterate over subsequent CSS
// assets and find which ones have the same 'type', 'group',
// 'every_page', 'preprocess', 'media' and 'browsers' properties.
// 'preprocess', 'media' and 'browsers' properties.
$j = $i;
$next_css_asset = $css_asset;
$current_ie_group_key = $get_ie_group_key($css_asset);
@ -168,7 +168,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
// control browser-caching. IE7 does not support a media type on
// the @import statement, so we instead specify the media for
// the group on the STYLE tag.
$import[] = '@import url("' . SafeMarkup::checkPlain(file_create_url($next_css_asset['data']) . '?' . $query_string) . '");';
$import[] = '@import url("' . Html::escape(file_create_url($next_css_asset['data']) . '?' . $query_string) . '");';
// Move the outer for loop skip the next item, since we
// processed it here.
$i = $j;

View file

@ -45,9 +45,8 @@ class JsCollectionGrouper implements AssetCollectionGrouperInterface {
case 'file':
// Group file items if their 'preprocess' flag is TRUE.
// Help ensure maximum reuse of aggregate files by only grouping
// together items that share the same 'group' value and 'every_page'
// flag.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['browsers']) : FALSE;
// together items that share the same 'group' value.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['browsers']) : FALSE;
break;
case 'external':

View file

@ -51,12 +51,6 @@ class JsCollectionRenderer implements AssetCollectionRendererInterface {
// query-string instead, to enforce reload on every page request.
$default_query_string = $this->state->get('system.css_js_query_string') ?: '0';
// For inline JavaScript to validate as XHTML, all JavaScript containing
// XHTML needs to be wrapped in CDATA. To make that backwards compatible
// with HTML 4, we need to comment out the CDATA-tag.
$embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
$embed_suffix = "\n//--><!]]>\n";
// Defaults for each SCRIPT element.
$element_defaults = array(
'#type' => 'html_tag',
@ -73,9 +67,13 @@ class JsCollectionRenderer implements AssetCollectionRendererInterface {
// Element properties that depend on item type.
switch ($js_asset['type']) {
case 'setting':
$element['#value_prefix'] = $embed_prefix;
$element['#value'] = 'var drupalSettings = ' . Json::encode($js_asset['data']) . ";";
$element['#value_suffix'] = $embed_suffix;
$element['#attributes'] = array(
// This type attribute prevents this from being parsed as an
// inline script.
'type' => 'application/json',
'data-drupal-selector' => 'drupal-settings-json',
);
$element['#value'] = Json::encode($js_asset['data']);
break;
case 'file':

View file

@ -8,7 +8,6 @@
namespace Drupal\Core\Block;
use Drupal\block\BlockInterface;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
@ -166,7 +165,7 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
$form['admin_label'] = array(
'#type' => 'item',
'#title' => $this->t('Block description'),
'#markup' => SafeMarkup::checkPlain($definition['admin_label']),
'#plain_text' => $definition['admin_label'],
);
$form['label'] = array(
'#type' => 'textfield',

View file

@ -63,6 +63,11 @@ interface BlockPluginInterface extends ConfigurablePluginInterface, PluginFormIn
/**
* Builds and returns the renderable array for this block plugin.
*
* If a block should not be rendered because it has no content, then this
* method must also ensure to return no content: it must then only return an
* empty array, or an empty array with #cache set (with cacheability metadata
* indicating the circumstances for it being empty).
*
* @return array
* A renderable array representing the content of the block.
*

View file

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\Core\Breadcrumb\Breadcrumb.
*/
namespace Drupal\Core\Breadcrumb;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Link;
/**
* Used to return generated breadcrumbs with associated cacheability metadata.
*
* @todo implement RenderableInterface once https://www.drupal.org/node/2529560 lands.
*/
class Breadcrumb extends CacheableMetadata {
/**
* An ordered list of links for the breadcrumb.
*
* @var \Drupal\Core\Link[]
*/
protected $links = [];
/**
* Gets the breadcrumb links.
*
* @return \Drupal\Core\Link[]
*/
public function getLinks() {
return $this->links;
}
/**
* Sets the breadcrumb links.
*
* @param \Drupal\Core\Link[] $links
* The breadcrumb links.
*
* @return $this
*
* @throws \LogicException
* Thrown when setting breadcrumb links after they've already been set.
*/
public function setLinks(array $links) {
if (!empty($this->links)) {
throw new \LogicException('Once breadcrumb links are set, only additional breadcrumb links can be added.');
}
$this->links = $links;
return $this;
}
/**
* Appends a link to the end of the ordered list of breadcrumb links.
*
* @param \Drupal\Core\Link $link
* The link appended to the breadcrumb.
*
* @return $this
*/
public function addLink(Link $link) {
$this->links[] = $link;
return $this;
}
}

View file

@ -32,9 +32,8 @@ interface BreadcrumbBuilderInterface {
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return \Drupal\Core\Link[]
* An array of links for the breadcrumb. Returning an empty array will
* suppress all breadcrumbs.
* @return \Drupal\Core\Breadcrumb\Breadcrumb
* A breadcrumb.
*/
public function build(RouteMatchInterface $route_match);

View file

@ -75,7 +75,7 @@ class BreadcrumbManager implements ChainBreadcrumbBuilderInterface {
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$breadcrumb = array();
$breadcrumb = new Breadcrumb();
$context = array('builder' => NULL);
// Call the build method of registered breadcrumb builders,
// until one of them returns an array.
@ -85,11 +85,9 @@ class BreadcrumbManager implements ChainBreadcrumbBuilderInterface {
continue;
}
$build = $builder->build($route_match);
$breadcrumb = $builder->build($route_match);
if (is_array($build)) {
// The builder returned an array of breadcrumb links.
$breadcrumb = $build;
if ($breadcrumb instanceof Breadcrumb) {
$context['builder'] = $builder;
break;
}
@ -99,7 +97,7 @@ class BreadcrumbManager implements ChainBreadcrumbBuilderInterface {
}
// Allow modules to alter the breadcrumb.
$this->moduleHandler->alter('system_breadcrumb', $breadcrumb, $route_match, $context);
// Fall back to an empty breadcrumb.
return $breadcrumb;
}

View file

@ -20,6 +20,8 @@ namespace Drupal\Core\Cache;
* to ensure fast retrieval on the next request. On cache sets and deletes, both
* backends will be invoked to ensure consistency.
*
* @see \Drupal\Core\Cache\ChainedFastBackend
*
* @ingroup cache
*/

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheableResponse.
*/
namespace Drupal\Core\Cache;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* A JsonResponse that contains and can expose cacheability metadata.
*
* Supports Drupal's caching concepts: cache tags for invalidation and cache
* contexts for variations.
*
* @see \Drupal\Core\Cache\Cache
* @see \Drupal\Core\Cache\CacheableMetadata
* @see \Drupal\Core\Cache\CacheableResponseTrait
*/
class CacheableJsonResponse extends JsonResponse implements CacheableResponseInterface {
use CacheableResponseTrait;
}

View file

@ -11,50 +11,16 @@ namespace Drupal\Core\Cache;
*
* @ingroup cache
*
* @todo Use RefinableCacheableDependencyInterface and the corresponding trait in
* https://www.drupal.org/node/2526326.
*/
class CacheableMetadata implements CacheableDependencyInterface {
class CacheableMetadata implements RefinableCacheableDependencyInterface {
/**
* Cache contexts.
*
* @var string[]
*/
protected $contexts = [];
/**
* Cache tags.
*
* @var string[]
*/
protected $tags = [];
/**
* Cache max-age.
*
* @var int
*/
protected $maxAge = Cache::PERMANENT;
use RefinableCacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->tags;
}
/**
* Adds cache tags.
*
* @param string[] $cache_tags
* The cache tags to be added.
*
* @return $this
*/
public function addCacheTags(array $cache_tags) {
$this->tags = Cache::mergeTags($this->tags, $cache_tags);
return $this;
return $this->cacheTags;
}
/**
@ -66,7 +32,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
* @return $this
*/
public function setCacheTags(array $cache_tags) {
$this->tags = $cache_tags;
$this->cacheTags = $cache_tags;
return $this;
}
@ -74,20 +40,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->contexts;
}
/**
* Adds cache contexts.
*
* @param string[] $cache_contexts
* The cache contexts to be added.
*
* @return $this
*/
public function addCacheContexts(array $cache_contexts) {
$this->contexts = Cache::mergeContexts($this->contexts, $cache_contexts);
return $this;
return $this->cacheContexts;
}
/**
@ -99,7 +52,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
* @return $this
*/
public function setCacheContexts(array $cache_contexts) {
$this->contexts = $cache_contexts;
$this->cacheContexts = $cache_contexts;
return $this;
}
@ -107,7 +60,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->maxAge;
return $this->cacheMaxAge;
}
/**
@ -128,36 +81,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
throw new \InvalidArgumentException('$max_age must be an integer');
}
$this->maxAge = $max_age;
return $this;
}
/**
* Adds a dependency on an object: merges its cacheability metadata.
*
* @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $other_object
* The dependency. If the object implements CacheableDependencyInterface,
* then its cacheability metadata will be used. Otherwise, the passed in
* object must be assumed to be uncacheable, so max-age 0 is set.
*
* @return $this
*/
public function addCacheableDependency($other_object) {
if ($other_object instanceof CacheableDependencyInterface) {
$this->addCacheTags($other_object->getCacheTags());
$this->addCacheContexts($other_object->getCacheContexts());
if ($this->maxAge === Cache::PERMANENT) {
$this->maxAge = $other_object->getCacheMaxAge();
}
elseif (($max_age = $other_object->getCacheMaxAge()) && $max_age !== Cache::PERMANENT) {
$this->maxAge = Cache::mergeMaxAges($this->maxAge, $max_age);
}
}
else {
// Not a cacheable dependency, this can not be cached.
$this->maxAge = 0;
}
$this->cacheMaxAge = $max_age;
return $this;
}
@ -175,34 +99,34 @@ class CacheableMetadata implements CacheableDependencyInterface {
// This is called many times per request, so avoid merging unless absolutely
// necessary.
if (empty($this->contexts)) {
$result->contexts = $other->contexts;
if (empty($this->cacheContexts)) {
$result->cacheContexts = $other->cacheContexts;
}
elseif (empty($other->contexts)) {
$result->contexts = $this->contexts;
elseif (empty($other->cacheContexts)) {
$result->cacheContexts = $this->cacheContexts;
}
else {
$result->contexts = Cache::mergeContexts($this->contexts, $other->contexts);
$result->cacheContexts = Cache::mergeContexts($this->cacheContexts, $other->cacheContexts);
}
if (empty($this->tags)) {
$result->tags = $other->tags;
if (empty($this->cacheTags)) {
$result->cacheTags = $other->cacheTags;
}
elseif (empty($other->tags)) {
$result->tags = $this->tags;
elseif (empty($other->cacheTags)) {
$result->cacheTags = $this->cacheTags;
}
else {
$result->tags = Cache::mergeTags($this->tags, $other->tags);
$result->cacheTags = Cache::mergeTags($this->cacheTags, $other->cacheTags);
}
if ($this->maxAge === Cache::PERMANENT) {
$result->maxAge = $other->maxAge;
if ($this->cacheMaxAge === Cache::PERMANENT) {
$result->cacheMaxAge = $other->cacheMaxAge;
}
elseif ($other->maxAge === Cache::PERMANENT) {
$result->maxAge = $this->maxAge;
elseif ($other->cacheMaxAge === Cache::PERMANENT) {
$result->cacheMaxAge = $this->cacheMaxAge;
}
else {
$result->maxAge = Cache::mergeMaxAges($this->maxAge, $other->maxAge);
$result->cacheMaxAge = Cache::mergeMaxAges($this->cacheMaxAge, $other->cacheMaxAge);
}
return $result;
}
@ -214,9 +138,9 @@ class CacheableMetadata implements CacheableDependencyInterface {
* A render array.
*/
public function applyTo(array &$build) {
$build['#cache']['contexts'] = $this->contexts;
$build['#cache']['tags'] = $this->tags;
$build['#cache']['max-age'] = $this->maxAge;
$build['#cache']['contexts'] = $this->cacheContexts;
$build['#cache']['tags'] = $this->cacheTags;
$build['#cache']['max-age'] = $this->cacheMaxAge;
}
/**
@ -229,9 +153,9 @@ class CacheableMetadata implements CacheableDependencyInterface {
*/
public static function createFromRenderArray(array $build) {
$meta = new static();
$meta->contexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : [];
$meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : [];
$meta->maxAge = (isset($build['#cache']['max-age'])) ? $build['#cache']['max-age'] : Cache::PERMANENT;
$meta->cacheContexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : [];
$meta->cacheTags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : [];
$meta->cacheMaxAge = (isset($build['#cache']['max-age'])) ? $build['#cache']['max-age'] : Cache::PERMANENT;
return $meta;
}
@ -249,16 +173,16 @@ class CacheableMetadata implements CacheableDependencyInterface {
public static function createFromObject($object) {
if ($object instanceof CacheableDependencyInterface) {
$meta = new static();
$meta->contexts = $object->getCacheContexts();
$meta->tags = $object->getCacheTags();
$meta->maxAge = $object->getCacheMaxAge();
$meta->cacheContexts = $object->getCacheContexts();
$meta->cacheTags = $object->getCacheTags();
$meta->cacheMaxAge = $object->getCacheMaxAge();
return $meta;
}
// Objects that don't implement CacheableDependencyInterface must be assumed
// to be uncacheable, so set max-age 0.
$meta = new static();
$meta->maxAge = 0;
$meta->cacheMaxAge = 0;
return $meta;
}

View file

@ -39,6 +39,15 @@ namespace Drupal\Core\Cache;
* Because this backend will mark all the cache entries in a bin as out-dated
* for each write to a bin, it is best suited to bins with fewer changes.
*
* Note that this is designed specifically for combining a fast inconsistent
* cache backend with a slower consistent cache back-end. To still function
* correctly, it needs to do a consistency check (see the "last write timestamp"
* logic). This contrasts with \Drupal\Core\Cache\BackendChain, which assumes
* both chained cache backends are consistent, thus a consistency check being
* pointless.
*
* @see \Drupal\Core\Cache\BackendChain
*
* @ingroup cache
*/
class ChainedFastBackend implements CacheBackendInterface, CacheTagsInvalidatorInterface {

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\Core\Cache\Context\PathCacheContext.
*/
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the PathCacheContext service, for "per URL path" caching.
*
* Cache context ID: 'url.path'.
*
* (This allows for caching relative URLs.)
*
* @see \Symfony\Component\HttpFoundation\Request::getBasePath()
* @see \Symfony\Component\HttpFoundation\Request::getPathInfo()
*/
class PathCacheContext extends RequestStackCacheContextBase implements CacheContextInterface {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Path');
}
/**
* {@inheritdoc}
*/
public function getContext() {
$request = $this->requestStack->getCurrentRequest();
return $request->getBasePath() . $request->getPathInfo();
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}

View file

@ -147,17 +147,26 @@ class DatabaseBackend implements CacheBackendInterface {
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::set().
* {@inheritdoc}
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) {
Cache::validateTags($tags);
$tags = array_unique($tags);
// Sort the cache tags so that they are stored consistently in the database.
sort($tags);
$this->setMultiple([
$cid => [
'data' => $data,
'expire' => $expire,
'tags' => $tags,
],
]);
}
/**
* {@inheritdoc}
*/
public function setMultiple(array $items) {
$try_again = FALSE;
try {
// The bin might not yet exist.
$this->doSet($cid, $data, $expire, $tags);
$this->doSetMultiple($items);
}
catch (\Exception $e) {
// If there was an exception, try to create the bins.
@ -169,39 +178,19 @@ class DatabaseBackend implements CacheBackendInterface {
}
// Now that the bin has been created, try again if necessary.
if ($try_again) {
$this->doSet($cid, $data, $expire, $tags);
$this->doSetMultiple($items);
}
}
/**
* Actually set the cache.
* Stores multiple items in the persistent cache.
*
* @param array $items
* An array of cache items, keyed by cid.
*
* @see \Drupal\Core\Cache\CacheBackendInterface::setMultiple()
*/
protected function doSet($cid, $data, $expire, $tags) {
$fields = array(
'created' => round(microtime(TRUE), 3),
'expire' => $expire,
'tags' => implode(' ', $tags),
'checksum' => $this->checksumProvider->getCurrentChecksum($tags),
);
if (!is_string($data)) {
$fields['data'] = serialize($data);
$fields['serialized'] = 1;
}
else {
$fields['data'] = $data;
$fields['serialized'] = 0;
}
$this->connection->merge($this->bin)
->key('cid', $this->normalizeCid($cid))
->fields($fields)
->execute();
}
/**
* {@inheritdoc}
*/
public function setMultiple(array $items) {
protected function doSetMultiple(array $items) {
$values = array();
foreach ($items as $cid => $item) {
@ -216,7 +205,7 @@ class DatabaseBackend implements CacheBackendInterface {
sort($item['tags']);
$fields = array(
'cid' => $cid,
'cid' => $this->normalizeCid($cid),
'expire' => $item['expire'],
'created' => round(microtime(TRUE), 3),
'tags' => implode(' ', $item['tags']),
@ -234,34 +223,20 @@ class DatabaseBackend implements CacheBackendInterface {
$values[] = $fields;
}
// Use a transaction so that the database can write the changes in a single
// commit. The transaction is started after calculating the tag checksums
// since that can create a table and this causes an exception when using
// PostgreSQL.
$transaction = $this->connection->startTransaction();
try {
// Delete all items first so we can do one insert. Rather than multiple
// merge queries.
$this->deleteMultiple(array_keys($items));
$query = $this->connection
->insert($this->bin)
->fields(array('cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized'));
foreach ($values as $fields) {
// Only pass the values since the order of $fields matches the order of
// the insert fields. This is a performance optimization to avoid
// unnecessary loops within the method.
$query->values(array_values($fields));
}
$query->execute();
}
catch (\Exception $e) {
$transaction->rollback();
// @todo Log something here or just re throw?
throw $e;
// Use an upsert query which is atomic and optimized for multiple-row
// merges.
$query = $this->connection
->upsert($this->bin)
->key('cid')
->fields(array('cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized'));
foreach ($values as $fields) {
// Only pass the values since the order of $fields matches the order of
// the insert fields. This is a performance optimization to avoid
// unnecessary loops within the method.
$query->values(array_values($fields));
}
$query->execute();
}
/**

View file

@ -217,4 +217,13 @@ class MemoryBackend implements CacheBackendInterface, CacheTagsInvalidatorInterf
return [];
}
/**
* Reset statically cached variables.
*
* This is only used by tests.
*/
public function reset() {
$this->cache = [];
}
}

View file

@ -44,7 +44,7 @@ trait RefinableCacheableDependencyTrait {
}
else {
// Not a cacheable dependency, this can not be cached.
$this->maxAge = 0;
$this->cacheMaxAge = 0;
}
return $this;
}
@ -53,7 +53,9 @@ trait RefinableCacheableDependencyTrait {
* {@inheritdoc}
*/
public function addCacheContexts(array $cache_contexts) {
$this->cacheContexts = Cache::mergeContexts($this->cacheContexts, $cache_contexts);
if ($cache_contexts) {
$this->cacheContexts = Cache::mergeContexts($this->cacheContexts, $cache_contexts);
}
return $this;
}
@ -61,7 +63,9 @@ trait RefinableCacheableDependencyTrait {
* {@inheritdoc}
*/
public function addCacheTags(array $cache_tags) {
$this->cacheTags = Cache::mergeTags($this->cacheTags, $cache_tags);
if ($cache_tags) {
$this->cacheTags = Cache::mergeTags($this->cacheTags, $cache_tags);
}
return $this;
}

View file

@ -129,12 +129,9 @@ class DbDumpCommand extends Command {
* An array of table names.
*/
protected function getTables() {
$pattern = $this->connection->tablePrefix() . '%';
$tables = array_values($this->connection->schema()->findTables($pattern));
foreach ($tables as $key => $table) {
// The prefix is removed for the resultant script.
$table = $tables[$key] = str_replace($this->connection->tablePrefix(), '', $table);
$tables = array_values($this->connection->schema()->findTables('%'));
foreach ($tables as $key => $table) {
// Remove any explicitly excluded tables.
foreach ($this->excludeTables as $pattern) {
if (preg_match('/^' . $pattern . '$/', $table)) {
@ -142,6 +139,7 @@ class DbDumpCommand extends Command {
}
}
}
return $tables;
}

View file

@ -53,11 +53,13 @@ class Condition extends Plugin {
public $module;
/**
* An array of contextual data.
* An array of context definitions describing the context used by the plugin.
*
* @var array
* The array is keyed by context names.
*
* @var \Drupal\Core\Annotation\ContextDefinition[]
*/
public $condition = array();
public $context = array();
/**
* The category under which the condition should listed in the UI.

View file

@ -158,6 +158,7 @@ class ConfigInstaller implements ConfigInstallerInterface {
*/
public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
$profile = $this->drupalGetProfile();
$optional_profile_config = [];
if (!$storage) {
// Search the install profile's optional configuration too.
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE);
@ -168,6 +169,7 @@ class ConfigInstaller implements ConfigInstallerInterface {
// Creates a profile storage to search for overrides.
$profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
$optional_profile_config = $profile_storage->listAll();
}
else {
// Profile has not been set yet. For example during the first steps of the
@ -178,7 +180,8 @@ class ConfigInstaller implements ConfigInstallerInterface {
$enabled_extensions = $this->getEnabledExtensions();
$existing_config = $this->getActiveStorages()->listAll();
$list = array_filter($storage->listAll(), function($config_name) use ($existing_config) {
$list = array_unique(array_merge($storage->listAll(), $optional_profile_config));
$list = array_filter($list, function($config_name) use ($existing_config) {
// Only list configuration that:
// - does not already exist
// - is a configuration entity (this also excludes config that has an
@ -188,7 +191,8 @@ class ConfigInstaller implements ConfigInstallerInterface {
$all_config = array_merge($existing_config, $list);
$config_to_create = $storage->readMultiple($list);
// Check to see if the corresponding override storage has any overrides.
// Check to see if the corresponding override storage has any overrides or
// new configuration that can be installed.
if ($profile_storage) {
$config_to_create = $profile_storage->readMultiple($list) + $config_to_create;
}

View file

@ -193,19 +193,23 @@ class FileStorage implements StorageInterface {
* Implements Drupal\Core\Config\StorageInterface::listAll().
*/
public function listAll($prefix = '') {
// glob() silently ignores the error of a non-existing search directory,
// even with the GLOB_ERR flag.
$dir = $this->getCollectionDirectory();
if (!file_exists($dir)) {
if (!is_dir($dir)) {
return array();
}
$extension = '.' . static::getFileExtension();
// \GlobIterator on Windows requires an absolute path.
$files = new \GlobIterator(realpath($dir) . '/' . $prefix . '*' . $extension);
// glob() directly calls into libc glob(), which is not aware of PHP stream
// wrappers. Same for \GlobIterator (which additionally requires an absolute
// realpath() on Windows).
// @see https://github.com/mikey179/vfsStream/issues/2
$files = scandir($dir);
$names = array();
foreach ($files as $file) {
$names[] = $file->getBasename($extension);
if ($file[0] !== '.' && fnmatch($prefix . '*' . $extension, $file)) {
$names[] = basename($file, $extension);
}
}
return $names;
@ -299,13 +303,15 @@ class FileStorage implements StorageInterface {
$collections[] = $collection . '.' . $sub_collection;
}
}
// Check that the collection is valid by searching if for configuration
// Check that the collection is valid by searching it for configuration
// objects. A directory without any configuration objects is not a valid
// collection.
// \GlobIterator on Windows requires an absolute path.
$files = new \GlobIterator(realpath($directory . '/' . $collection) . '/*.' . $this->getFileExtension());
if (count($files)) {
$collections[] = $collection;
// @see \Drupal\Core\Config\FileStorage::listAll()
foreach (scandir($directory . '/' . $collection) as $file) {
if ($file[0] !== '.' && fnmatch('*.' . $this->getFileExtension(), $file)) {
$collections[] = $collection;
break;
}
}
}
}

View file

@ -195,10 +195,17 @@ class InstallStorage extends FileStorage {
// We don't have to use ExtensionDiscovery here because our list of
// extensions was already obtained through an ExtensionDiscovery scan.
$directory = $this->getComponentFolder($extension_object);
if (file_exists($directory)) {
$files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension);
if (is_dir($directory)) {
// glob() directly calls into libc glob(), which is not aware of PHP
// stream wrappers. Same for \GlobIterator (which additionally requires
// an absolute realpath() on Windows).
// @see https://github.com/mikey179/vfsStream/issues/2
$files = scandir($directory);
foreach ($files as $file) {
$folders[$file->getBasename($extension)] = $directory;
if ($file[0] !== '.' && fnmatch('*' . $extension, $file)) {
$folders[basename($file, $extension)] = $directory;
}
}
}
}
@ -215,10 +222,17 @@ class InstallStorage extends FileStorage {
$extension = '.' . $this->getFileExtension();
$folders = array();
$directory = $this->getCoreFolder();
if (file_exists($directory)) {
$files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension);
if (is_dir($directory)) {
// glob() directly calls into libc glob(), which is not aware of PHP
// stream wrappers. Same for \GlobIterator (which additionally requires an
// absolute realpath() on Windows).
// @see https://github.com/mikey179/vfsStream/issues/2
$files = scandir($directory);
foreach ($files as $file) {
$folders[$file->getBasename($extension)] = $directory;
if ($file[0] !== '.' && fnmatch('*' . $extension, $file)) {
$folders[basename($file, $extension)] = $directory;
}
}
}
return $folders;

View file

@ -42,13 +42,20 @@ trait SchemaCheckTrait {
* @param string $config_name
* The configuration name.
* @param array $config_data
* The configuration data.
* The configuration data, assumed to be data for a top-level config object.
*
* @return array|bool
* FALSE if no schema found. List of errors if any found. TRUE if fully
* valid.
*/
public function checkConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data) {
// We'd like to verify that the top-level type is either config_base,
// config_entity, or a derivative. The only thing we can really test though
// is that the schema supports having langcode in it. So add 'langcode' to
// the data if it doesn't already exist.
if (!isset($config_data['langcode'])) {
$config_data['langcode'] = 'en';
}
$this->configName = $config_name;
if (!$typed_config->hasConfigSchema($config_name)) {
return FALSE;

View file

@ -73,7 +73,7 @@ class ConfigSchemaChecker implements EventSubscriberInterface {
$name = $saved_config->getName();
$data = $saved_config->get();
$checksum = crc32(serialize($data));
$checksum = hash('crc32b', serialize($data));
$exceptions = array(
// Following are used to test lack of or partial schema. Where partial
// schema is provided, that is explicitly tested in specific tests.

View file

@ -7,11 +7,13 @@
namespace Drupal\Core\Controller;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Routing\RouteMatch;
use Psr\Log\LoggerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ControllerResolver as BaseControllerResolver;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
/**
* ControllerResolver to enhance controllers beyond Symfony's basic handling.
@ -37,13 +39,24 @@ class ControllerResolver extends BaseControllerResolver implements ControllerRes
*/
protected $classResolver;
/**
* The PSR-7 converter.
*
* @var \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface
*/
protected $httpMessageFactory;
/**
* Constructs a new ControllerResolver.
*
* @param \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface $http_message_factory
* The PSR-7 converter.
*
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
*/
public function __construct(ClassResolverInterface $class_resolver) {
public function __construct(HttpMessageFactoryInterface $http_message_factory, ClassResolverInterface $class_resolver) {
$this->httpMessageFactory = $http_message_factory;
$this->classResolver = $class_resolver;
}
@ -94,10 +107,10 @@ class ControllerResolver extends BaseControllerResolver implements ControllerRes
* A PHP callable.
*
* @throws \LogicException
* If the controller cannot be parsed
* If the controller cannot be parsed.
*
* @throws \InvalidArgumentException
* If the controller class does not exist
* If the controller class does not exist.
*/
protected function createController($controller) {
// Controller in the service:method notation.
@ -135,7 +148,10 @@ class ControllerResolver extends BaseControllerResolver implements ControllerRes
elseif ($param->getClass() && $param->getClass()->isInstance($request)) {
$arguments[] = $request;
}
elseif ($param->getClass() && ($param->getClass()->name == 'Drupal\Core\Routing\RouteMatchInterface' || is_subclass_of($param->getClass()->name, 'Drupal\Core\Routing\RouteMatchInterface'))) {
elseif ($param->getClass() && $param->getClass()->name === ServerRequestInterface::class) {
$arguments[] = $this->httpMessageFactory->createRequest($request);
}
elseif ($param->getClass() && ($param->getClass()->name == RouteMatchInterface::class || is_subclass_of($param->getClass()->name, RouteMatchInterface::class))) {
$arguments[] = RouteMatch::createFromRequest($request);
}
elseif ($param->isDefaultValueAvailable()) {

View file

@ -19,6 +19,7 @@ use Drupal\Core\DependencyInjection\Compiler\DependencySerializationTraitPass;
use Drupal\Core\DependencyInjection\Compiler\StackedKernelPass;
use Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterStreamWrappersPass;
use Drupal\Core\DependencyInjection\Compiler\TwigExtensionPass;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass;
@ -78,6 +79,8 @@ class CoreServiceProvider implements ServiceProviderInterface {
$container->addCompilerPass(new RegisterStreamWrappersPass());
$container->addCompilerPass(new GuzzleMiddlewarePass());
$container->addCompilerPass(new TwigExtensionPass());
// Add a compiler pass for registering event subscribers.
$container->addCompilerPass(new RegisterKernelListenersPass(), PassConfig::TYPE_AFTER_REMOVING);

View file

@ -138,6 +138,13 @@ abstract class Connection {
*/
protected $prefixReplace = array();
/**
* List of un-prefixed table names, keyed by prefixed table names.
*
* @var array
*/
protected $unprefixedTablesMap = [];
/**
* Constructs a Connection object.
*
@ -185,7 +192,9 @@ abstract class Connection {
// Destroy all references to this connection by setting them to NULL.
// The Statement class attribute only accepts a new value that presents a
// proper callable, so we reset it to PDOStatement.
$this->connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
if (!empty($this->statementClass)) {
$this->connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
}
$this->schema = NULL;
}
@ -289,6 +298,13 @@ abstract class Connection {
$this->prefixReplace[] = $this->prefixes['default'];
$this->prefixSearch[] = '}';
$this->prefixReplace[] = '';
// Set up a map of prefixed => un-prefixed tables.
foreach ($this->prefixes as $table_name => $prefix) {
if ($table_name !== 'default') {
$this->unprefixedTablesMap[$prefix . $table_name] = $table_name;
}
}
}
/**
@ -327,6 +343,17 @@ abstract class Connection {
}
}
/**
* Gets a list of individually prefixed table names.
*
* @return array
* An array of un-prefixed table names, keyed by their fully qualified table
* names (i.e. prefix + table_name).
*/
public function getUnprefixedTablesMap() {
return $this->unprefixedTablesMap;
}
/**
* Get a fully qualified table name.
*
@ -502,7 +529,7 @@ abstract class Connection {
* A sanitized version of the query comment string.
*/
protected function filterComment($comment = '') {
return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment);
return strtr($comment, ['*' => ' * ']);
}
/**
@ -786,6 +813,23 @@ abstract class Connection {
return new $class($this, $table, $options);
}
/**
* Prepares and returns an UPSERT query object.
*
* @param string $table
* The table to use for the upsert query.
* @param array $options
* (optional) An array of options on the query.
*
* @return \Drupal\Core\Database\Query\Upsert
* A new Upsert query object.
*
* @see \Drupal\Core\Database\Query\Upsert
*/
public function upsert($table, array $options = array()) {
$class = $this->getDriverClass('Upsert');
return new $class($this, $table, $options);
}
/**
* Prepares and returns an UPDATE query object.
@ -1216,6 +1260,13 @@ abstract class Connection {
return $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
/**
* Returns the version of the database client.
*/
public function clientVersion() {
return $this->connection->getAttribute(\PDO::ATTR_CLIENT_VERSION);
}
/**
* Determines if this driver supports transactions.
*

View file

@ -28,6 +28,11 @@ class Connection extends DatabaseConnection {
*/
const DATABASE_NOT_FOUND = 1049;
/**
* Error code for "Can't initialize character set" error.
*/
const UNSUPPORTED_CHARSET = 2019;
/**
* Flag to indicate if the cleanup function in __destruct() should run.
*
@ -82,6 +87,13 @@ class Connection extends DatabaseConnection {
* {@inheritdoc}
*/
public static function open(array &$connection_options = array()) {
if (isset($connection_options['_dsn_utf8_fallback']) && $connection_options['_dsn_utf8_fallback'] === TRUE) {
// Only used during the installer version check, as a fallback from utf8mb4.
$charset = 'utf8';
}
else {
$charset = 'utf8mb4';
}
// The DSN should use either a socket or a host/port.
if (isset($connection_options['unix_socket'])) {
$dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
@ -93,7 +105,7 @@ class Connection extends DatabaseConnection {
// Character set is added to dsn to ensure PDO uses the proper character
// set when escaping. This has security implications. See
// https://www.drupal.org/node/1201452 for further discussion.
$dsn .= ';charset=utf8mb4';
$dsn .= ';charset=' . $charset;
if (!empty($connection_options['database'])) {
$dsn .= ';dbname=' . $connection_options['database'];
}
@ -124,10 +136,10 @@ class Connection extends DatabaseConnection {
// certain one has been set; otherwise, MySQL defaults to
// 'utf8mb4_general_ci' for utf8mb4.
if (!empty($connection_options['collation'])) {
$pdo->exec('SET NAMES utf8mb4 COLLATE ' . $connection_options['collation']);
$pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
}
else {
$pdo->exec('SET NAMES utf8mb4');
$pdo->exec('SET NAMES ' . $charset);
}
// Set MySQL init_commands if not already defined. Default Drupal's MySQL

View file

@ -55,30 +55,7 @@ class Insert extends QueryInsert {
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
$max_placeholder = 0;
$values = array();
if (count($this->insertValues)) {
foreach ($this->insertValues as $insert_values) {
$placeholders = array();
// Default fields aren't really placeholders, but this is the most convenient
// way to handle them.
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
$new_placeholder = $max_placeholder + count($insert_values);
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
$placeholders[] = ':db_insert_placeholder_' . $i;
}
$max_placeholder = $new_placeholder;
$values[] = '(' . implode(', ', $placeholders) . ')';
}
}
else {
// If there are no values, then this is a default-only query. We still need to handle that.
$placeholders = array_fill(0, count($this->defaultFields), 'default');
$values[] = '(' . implode(', ', $placeholders) . ')';
}
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
$query .= implode(', ', $values);
return $query;

View file

@ -16,6 +16,17 @@ use Drupal\Core\Database\DatabaseNotFoundException;
* Specifies installation tasks for MySQL and equivalent databases.
*/
class Tasks extends InstallTasks {
/**
* Minimum required MySQLnd version.
*/
const MYSQLND_MINIMUM_VERSION = '5.0.9';
/**
* Minimum required libmysqlclient version.
*/
const LIBMYSQLCLIENT_MINIMUM_VERSION = '5.5.3';
/**
* The PDO driver name for MySQL and equivalent databases.
*
@ -27,13 +38,6 @@ class Tasks extends InstallTasks {
* Constructs a \Drupal\Core\Database\Driver\mysql\Install\Tasks object.
*/
public function __construct() {
$this->tasks[] = array(
'arguments' => array(
'SET NAMES utf8mb4',
'The %name database server supports utf8mb4 character encoding.',
'The %name database server must support utf8mb4 character encoding to work with Drupal. Make sure to use a database server that supports utf8mb4 character encoding, such as MySQL/MariaDB/Percona versions 5.5.3 and up.',
),
);
$this->tasks[] = array(
'arguments' => array(),
'function' => 'ensureInnoDbAvailable',
@ -62,7 +66,34 @@ class Tasks extends InstallTasks {
// This doesn't actually test the connection.
db_set_active();
// Now actually do a check.
Database::getConnection();
try {
Database::getConnection();
}
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')));
$info = Database::getConnectionInfo();
$info_copy = $info;
// Set a flag to fall back to utf8. Note: this flag should only be
// used here and is for internal use only.
$info_copy['default']['_dsn_utf8_fallback'] = TRUE;
// In order to change the Database::$databaseInfo array, we need to
// remove the active connection, then re-add it with the new info.
Database::removeConnection('default');
Database::addConnectionInfo('default', 'default', $info_copy['default']);
// Connect with the new database info, using the utf8 character set so
// that we can run the checkEngineVersion test.
Database::getConnection();
// Revert to the old settings.
Database::removeConnection('default');
Database::addConnectionInfo('default', 'default', $info['default']);
}
else {
// Rethrow the exception.
throw $e;
}
}
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (\Exception $e) {
@ -121,4 +152,27 @@ class Tasks extends InstallTasks {
}
}
/**
* {@inheritdoc}
*/
protected function checkEngineVersion() {
parent::checkEngineVersion();
// Ensure that the MySQL driver supports utf8mb4 encoding.
$version = Database::getConnection()->clientVersion();
if (FALSE !== strpos($version, 'mysqlnd')) {
// 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)));
}
}
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)));
}
}
}
}

View file

@ -9,6 +9,7 @@ namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
use Drupal\Core\Database\Schema as DatabaseSchema;
@ -60,8 +61,7 @@ class Schema extends DatabaseSchema {
$info['table'] = substr($table, ++$pos);
}
else {
$db_info = Database::getConnectionInfo();
$info['database'] = $db_info[$this->connection->getTarget()]['database'];
$info['database'] = $this->connection->getConnectionOptions()['database'];
$info['table'] = $table;
}
return $info;
@ -299,14 +299,17 @@ class Schema extends DatabaseSchema {
* Shortens indexes to 191 characters if they apply to utf8mb4-encoded
* fields, in order to comply with the InnoDB index limitation of 756 bytes.
*
* @param $spec
* @param array $spec
* The table specification.
*
* @return array
* List of shortened indexes.
*
* @throws \Drupal\Core\Database\SchemaException
* Thrown if field specification is missing.
*/
protected function getNormalizedIndexes($spec) {
$indexes = $spec['indexes'];
protected function getNormalizedIndexes(array $spec) {
$indexes = isset($spec['indexes']) ? $spec['indexes'] : [];
foreach ($indexes as $index_name => $index_fields) {
foreach ($index_fields as $index_key => $index_field) {
// Get the name of the field from the index specification.
@ -323,6 +326,9 @@ class Schema extends DatabaseSchema {
}
}
}
else {
throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index");
}
}
}
return $indexes;
@ -486,7 +492,10 @@ class Schema extends DatabaseSchema {
return TRUE;
}
public function addIndex($table, $name, $fields) {
/**
* {@inheritdoc}
*/
public function addIndex($table, $name, $fields, array $spec) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
@ -494,7 +503,10 @@ class Schema extends DatabaseSchema {
throw new SchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')');
$spec['indexes'][$name] = $fields;
$indexes = $this->getNormalizedIndexes($spec);
$this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')');
}
public function dropIndex($table, $name) {

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Upsert.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
/**
* Implements the Upsert query for the MySQL database driver.
*/
class Upsert extends QueryUpsert {
/**
* {@inheritdoc}
*/
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
$query .= implode(', ', $values);
// Updating the unique / primary key is not necessary.
unset($insert_fields[$this->key]);
$update = [];
foreach ($insert_fields as $field) {
$update[] = "$field = VALUES($field)";
}
$query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update);
return $query;
}
}

View file

@ -383,6 +383,22 @@ class Connection extends DatabaseConnection {
$this->rollback($savepoint_name);
}
}
/**
* {@inheritdoc}
*/
public function upsert($table, array $options = array()) {
// Use the (faster) native Upsert implementation for PostgreSQL >= 9.5.
if (version_compare($this->version(), '9.5', '>=')) {
$class = $this->getDriverClass('NativeUpsert');
}
else {
$class = $this->getDriverClass('Upsert');
}
return new $class($this, $table, $options);
}
}
/**

View file

@ -128,30 +128,7 @@ class Insert extends QueryInsert {
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
$max_placeholder = 0;
$values = array();
if (count($this->insertValues)) {
foreach ($this->insertValues as $insert_values) {
$placeholders = array();
// Default fields aren't really placeholders, but this is the most convenient
// way to handle them.
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
$new_placeholder = $max_placeholder + count($insert_values);
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
$placeholders[] = ':db_insert_placeholder_' . $i;
}
$max_placeholder = $new_placeholder;
$values[] = '(' . implode(', ', $placeholders) . ')';
}
}
else {
// If there are no values, then this is a default-only query. We still need to handle that.
$placeholders = array_fill(0, count($this->defaultFields), 'default');
$values[] = '(' . implode(', ', $placeholders) . ')';
}
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
$query .= implode(', ', $values);
return $query;

View file

@ -180,8 +180,8 @@ class Tasks extends InstallTasks {
* Verify that a binary data roundtrip returns the original string.
*/
protected function checkBinaryOutputSuccess() {
$bytea_output = db_query("SELECT 'encoding'::bytea AS output")->fetchField();
return ($bytea_output == 'encoding');
$bytea_output = db_query("SHOW bytea_output")->fetchField();
return ($bytea_output == 'escape');
}
/**
@ -192,17 +192,9 @@ class Tasks extends InstallTasks {
// like we do with table names. This is so that we don't double up if more
// than one instance of Drupal is running on a single database. We therefore
// avoid trying to create them again in that case.
// At the same time checking for the existence of the function fixes
// concurrency issues, when both try to update at the same time.
try {
// Create functions.
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric) RETURNS numeric AS
\'SELECT CASE WHEN (($1 > $2) OR ($2 IS NULL)) THEN $1 ELSE $2 END;\'
LANGUAGE \'sql\''
);
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric, numeric) RETURNS numeric AS
\'SELECT greatest($1, greatest($2, $3));\'
LANGUAGE \'sql\''
);
// 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
@ -211,37 +203,17 @@ class Tasks extends InstallTasks {
);
}
db_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\''
);
// Using || to concatenate in Drupal is not recommended because there are
// database drivers for Drupal that do not support the syntax, however
// they do support CONCAT(item1, item2) which we can replicate in
// PostgreSQL. PostgreSQL requires the function to be defined for each
// different argument variation the function can handle.
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, anynonarray) RETURNS text AS
\'SELECT CAST($1 AS text) || CAST($2 AS text);\'
LANGUAGE \'sql\'
');
db_query('CREATE OR REPLACE FUNCTION "concat"(text, anynonarray) RETURNS text AS
\'SELECT $1 || CAST($2 AS text);\'
LANGUAGE \'sql\'
');
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, text) RETURNS text AS
\'SELECT CAST($1 AS text) || $2;\'
LANGUAGE \'sql\'
');
db_query('CREATE OR REPLACE FUNCTION "concat"(text, text) RETURNS text AS
\'SELECT $1 || $2;\'
LANGUAGE \'sql\'
');
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
\'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
LANGUAGE \'sql\''
);
}
$this->pass(t('PostgreSQL has initialized itself.'));
}
catch (\Exception $e) {
$this->fail(t('Drupal could not be correctly setup with the existing database. Revise any errors.'));
$this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()]));
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\NativeUpsert.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
/**
* Implements the native Upsert query for the PostgreSQL database driver.
*
* @see http://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
*/
class NativeUpsert extends QueryUpsert {
/**
* {@inheritdoc}
*/
public function execute() {
if (!$this->preExecute()) {
return NULL;
}
$stmt = $this->connection->prepareQuery((string) $this);
// Fetch the list of blobs and sequences used on that table.
$table_information = $this->connection->schema()->queryTableInformation($this->table);
$max_placeholder = 0;
$blobs = [];
$blob_count = 0;
foreach ($this->insertValues as $insert_values) {
foreach ($this->insertFields as $idx => $field) {
if (isset($table_information->blob_fields[$field])) {
$blobs[$blob_count] = fopen('php://memory', 'a');
fwrite($blobs[$blob_count], $insert_values[$idx]);
rewind($blobs[$blob_count]);
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
// Pre-increment is faster in PHP than increment.
++$blob_count;
}
else {
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
}
}
// Check if values for a serial field has been passed.
if (!empty($table_information->serial_fields)) {
foreach ($table_information->serial_fields as $index => $serial_field) {
$serial_key = array_search($serial_field, $this->insertFields);
if ($serial_key !== FALSE) {
$serial_value = $insert_values[$serial_key];
// Sequences must be greater than or equal to 1.
if ($serial_value === NULL || !$serial_value) {
$serial_value = 1;
}
// Set the sequence to the bigger value of either the passed
// value or the max value of the column. It can happen that another
// thread calls nextval() which could lead to a serial number being
// used twice. However, trying to insert a value into a serial
// column should only be done in very rare cases and is not thread
// safe by definition.
$this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", array(':serial_value' => (int)$serial_value));
}
}
}
}
$options = $this->queryOptions;
if (!empty($table_information->sequences)) {
$options['sequence_name'] = $table_information->sequences[0];
}
$this->connection->query($stmt, [], $options);
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = [];
return TRUE;
}
/**
* {@inheritdoc}
*/
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
$insert_fields = array_map(function($f) { return $this->connection->escapeField($f); }, $insert_fields);
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
$query .= implode(', ', $values);
// Updating the unique / primary key is not necessary.
unset($insert_fields[$this->key]);
$update = [];
foreach ($insert_fields as $field) {
$update[] = "$field = EXCLUDED.$field";
}
$query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
return $query;
}
}

View file

@ -93,9 +93,17 @@ class Schema extends DatabaseSchema {
public function queryTableInformation($table) {
// Generate a key to reference this table's information on.
$key = $this->connection->prefixTables('{' . $table . '}');
if (strpos($key, '.') === FALSE) {
// Take into account that temporary tables are stored in a different schema.
// \Drupal\Core\Database\Connection::generateTemporaryTableName() sets the
// 'db_temporary_' prefix to all temporary tables.
if (strpos($key, '.') === FALSE && strpos($table, 'db_temporary_') === FALSE) {
$key = 'public.' . $key;
}
else {
$schema = $this->connection->query('SELECT nspname FROM pg_namespace WHERE oid = pg_my_temp_schema()')->fetchField();
$key = $schema . '.' . $key;
}
if (!isset($this->tableInformation[$key])) {
// Split the key into schema and table for querying.
@ -580,13 +588,28 @@ class Schema extends DatabaseSchema {
/**
* Helper function: check if a constraint (PK, FK, UK) exists.
*
* @param $table
* @param string $table
* The name of the table.
* @param $name
* The name of the constraint (typically 'pkey' or '[constraint]_key').
* @param string $name
* The name of the constraint (typically 'pkey' or '[constraint]__key').
*
* @return bool
* TRUE if the constraint exists, FALSE otherwise.
*/
public function constraintExists($table, $name) {
$constraint_name = $this->ensureIdentifiersLength($table, $name);
// ::ensureIdentifiersLength() expects three parameters, although not
// explicitly stated in its signature, thus we split our constraint name in
// a proper name and a suffix.
if ($name == 'pkey') {
$suffix = $name;
$name = '';
}
else {
$pos = strrpos($name, '__');
$suffix = substr($name, $pos + 2);
$name = substr($name, 0, $pos);
}
$constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix);
// Remove leading and trailing quotes because the index name is in a WHERE
// clause and not used as an identifier.
$constraint_name = str_replace('"', '', $constraint_name);
@ -637,7 +660,10 @@ class Schema extends DatabaseSchema {
return TRUE;
}
public function addIndex($table, $name, $fields) {
/**
* {@inheritdoc}
*/
public function addIndex($table, $name, $fields, array $spec) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
@ -779,7 +805,10 @@ class Schema extends DatabaseSchema {
}
if (isset($new_keys['indexes'])) {
foreach ($new_keys['indexes'] as $name => $fields) {
$this->addIndex($table, $name, $fields);
// Even though $new_keys is not a full schema it still has 'indexes' and
// so is a partial schema. Technically addIndex() doesn't do anything
// with it so passing an empty array would work as well.
$this->addIndex($table, $name, $fields, $new_keys);
}
}
}

View file

@ -0,0 +1,88 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Upsert.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
/**
* Implements the Upsert query for the PostgreSQL database driver.
*/
class Upsert extends QueryUpsert {
/**
* {@inheritdoc}
*/
public function execute() {
if (!$this->preExecute()) {
return NULL;
}
// Default options for upsert queries.
$this->queryOptions += array(
'throw_exception' => TRUE,
);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
$table = $this->connection->escapeTable($this->table);
// We have to execute multiple queries, therefore we wrap everything in a
// transaction so that it is atomic where possible.
$transaction = $this->connection->startTransaction();
try {
// First, lock the table we're upserting into.
$this->connection->query('LOCK TABLE {' . $table . '} IN SHARE ROW EXCLUSIVE MODE', [], $this->queryOptions);
// Second, delete all items first so we can do one insert.
$unique_key_position = array_search($this->key, $insert_fields);
$delete_ids = [];
foreach ($this->insertValues as $insert_values) {
$delete_ids[] = $insert_values[$unique_key_position];
}
// Delete in chunks when a large array is passed.
foreach (array_chunk($delete_ids, 1000) as $delete_ids_chunk) {
$this->connection->delete($this->table, $this->queryOptions)
->condition($this->key, $delete_ids_chunk, 'IN')
->execute();
}
// Third, insert all the values.
$insert = $this->connection->insert($this->table, $this->queryOptions)
->fields($insert_fields);
foreach ($this->insertValues as $insert_values) {
$insert->values($insert_values);
}
$insert->execute();
}
catch (\Exception $e) {
// One of the queries failed, rollback the whole batch.
$transaction->rollback();
// Rethrow the exception for the calling code.
throw $e;
}
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = array();
// Transaction commits here where $transaction looses scope.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function __toString() {
// Nothing to do.
}
}

View file

@ -154,9 +154,9 @@ class Connection extends DatabaseConnection {
// We can prune the database file if it doesn't have any tables.
if ($count == 0) {
// Detach the database.
$this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
// Destroy the database file.
// Detaching the database fails at this point, but no other queries
// are executed after the connection is destructed so we can simply
// remove the database file.
unlink($this->connectionOptions['database'] . '-' . $prefix);
}
}
@ -168,6 +168,18 @@ class Connection extends DatabaseConnection {
}
}
/**
* Gets all the attached databases.
*
* @return array
* An array of attached database names.
*
* @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct()
*/
public function getAttachedDatabases() {
return $this->attachedDatabases;
}
/**
* SQLite compatibility implementation for the IF() SQL function.
*/

View file

@ -582,7 +582,10 @@ class Schema extends DatabaseSchema {
return $key_definition;
}
public function addIndex($table, $name, $fields) {
/**
* {@inheritdoc}
*/
public function addIndex($table, $name, $fields, array $spec) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
@ -693,16 +696,31 @@ class Schema extends DatabaseSchema {
$this->alterTable($table, $old_schema, $new_schema);
}
/**
* {@inheritdoc}
*/
public function findTables($table_expression) {
// Don't add the prefix, $table_expression already includes the prefix.
$info = $this->getPrefixInfo($table_expression, FALSE);
$tables = [];
// Can't use query placeholders for the schema because the query would have
// to be :prefixsqlite_master, which does not work.
$result = db_query("SELECT name FROM " . $info['schema'] . ".sqlite_master WHERE type = :type AND name LIKE :table_name", array(
':type' => 'table',
':table_name' => $info['table'],
));
return $result->fetchAllKeyed(0, 0);
// The SQLite implementation doesn't need to use the same filtering strategy
// as the parent one because individually prefixed tables live in their own
// schema (database), which means that neither the main database nor any
// attached one will contain a prefixed table name, so we just need to loop
// over all known schemas and filter by the user-supplied table expression.
$attached_dbs = $this->connection->getAttachedDatabases();
foreach ($attached_dbs as $schema) {
// Can't use query placeholders for the schema because the query would
// have to be :prefixsqlite_master, which does not work. We also need to
// ignore the internal SQLite tables.
$result = db_query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", array(
':type' => 'table',
':table_name' => $table_expression,
':pattern' => 'sqlite_%',
));
$tables += $result->fetchAllKeyed(0, 0);
}
return $tables;
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Upsert.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
/**
* Implements the Upsert query for the SQLite database driver.
*/
class Upsert extends QueryUpsert {
/**
* {@inheritdoc}
*/
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
$query = $comments . 'INSERT OR REPLACE INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
$query .= implode(', ', $values);
return $query;
}
}

View file

@ -7,7 +7,6 @@
namespace Drupal\Core\Database\Install;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Database\Database;
/**
@ -80,7 +79,10 @@ abstract class Tasks {
*
* @var array
*/
protected $results = array();
protected $results = array(
'fail' => array(),
'pass' => array(),
);
/**
* Ensure the PDO driver is supported by the version of PHP in use.
@ -93,14 +95,14 @@ abstract class Tasks {
* Assert test as failed.
*/
protected function fail($message) {
$this->results[$message] = FALSE;
$this->results['fail'][] = $message;
}
/**
* Assert test as a pass.
*/
protected function pass($message) {
$this->results[$message] = TRUE;
$this->results['pass'][] = $message;
}
/**
@ -128,6 +130,9 @@ abstract class Tasks {
/**
* Run database tasks and tests to see if Drupal can run on the database.
*
* @return array
* A list of error messages.
*/
public function runTasks() {
// We need to establish a connection before we can run tests.
@ -143,21 +148,11 @@ abstract class Tasks {
}
}
else {
throw new TaskException(t("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
$this->fail(t("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
}
}
}
// Check for failed results and compile message
$message = '';
foreach ($this->results as $result => $success) {
if (!$success) {
$message = SafeMarkup::isSafe($result) ? $result : SafeMarkup::checkPlain($result);
}
}
if (!empty($message)) {
$message = SafeMarkup::set('Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/getting-started/install">installation handbook</a>, or contact your hosting provider.' . $message);
throw new TaskException($message);
}
return $this->results['fail'];
}
/**
@ -196,8 +191,9 @@ abstract class Tasks {
* Check the engine version.
*/
protected function checkEngineVersion() {
// Ensure that the database server has the right version.
if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) {
$this->fail(t("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
$this->fail(t("The database server version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
}
}

View file

@ -16,43 +16,7 @@ use Drupal\Core\Database\Database;
*/
class Insert extends Query {
/**
* The table on which to insert.
*
* @var string
*/
protected $table;
/**
* An array of fields on which to insert.
*
* @var array
*/
protected $insertFields = array();
/**
* An array of fields that should be set to their database-defined defaults.
*
* @var array
*/
protected $defaultFields = array();
/**
* A nested array of values to insert.
*
* $insertValues is an array of arrays. Each sub-array is either an
* associative array whose keys are field names and whose values are field
* values to insert, or a non-associative array of values in the same order
* as $insertFields.
*
* Whether multiple insert sets will be run in a single query or multiple
* queries is left to individual drivers to implement in whatever manner is
* most appropriate. The order of values in each sub-array must match the
* order of fields in $insertFields.
*
* @var array
*/
protected $insertValues = array();
use InsertTrait;
/**
* A SelectQuery object to fetch the rows that should be inserted.
@ -79,96 +43,6 @@ class Insert extends Query {
$this->table = $table;
}
/**
* Adds a set of field->value pairs to be inserted.
*
* This method may only be called once. Calling it a second time will be
* ignored. To queue up multiple sets of values to be inserted at once,
* use the values() method.
*
* @param $fields
* An array of fields on which to insert. This array may be indexed or
* associative. If indexed, the array is taken to be the list of fields.
* If associative, the keys of the array are taken to be the fields and
* the values are taken to be corresponding values to insert. If a
* $values argument is provided, $fields must be indexed.
* @param $values
* An array of fields to insert into the database. The values must be
* specified in the same order as the $fields array.
*
* @return \Drupal\Core\Database\Query\Insert
* The called object.
*/
public function fields(array $fields, array $values = array()) {
if (empty($this->insertFields)) {
if (empty($values)) {
if (!is_numeric(key($fields))) {
$values = array_values($fields);
$fields = array_keys($fields);
}
}
$this->insertFields = $fields;
if (!empty($values)) {
$this->insertValues[] = $values;
}
}
return $this;
}
/**
* Adds another set of values to the query to be inserted.
*
* If $values is a numeric-keyed array, it will be assumed to be in the same
* order as the original fields() call. If it is associative, it may be
* in any order as long as the keys of the array match the names of the
* fields.
*
* @param $values
* An array of values to add to the query.
*
* @return \Drupal\Core\Database\Query\Insert
* The called object.
*/
public function values(array $values) {
if (is_numeric(key($values))) {
$this->insertValues[] = $values;
}
else {
// Reorder the submitted values to match the fields array.
foreach ($this->insertFields as $key) {
$insert_values[$key] = $values[$key];
}
// For consistency, the values array is always numerically indexed.
$this->insertValues[] = array_values($insert_values);
}
return $this;
}
/**
* Specifies fields for which the database defaults should be used.
*
* If you want to force a given field to use the database-defined default,
* not NULL or undefined, use this method to instruct the database to use
* default values explicitly. In most cases this will not be necessary
* unless you are inserting a row that is all default values, as you cannot
* specify no values in an INSERT query.
*
* Specifying a field both in fields() and in useDefaults() is an error
* and will not execute.
*
* @param $fields
* An array of values for which to use the default values
* specified in the table definition.
*
* @return \Drupal\Core\Database\Query\Insert
* The called object.
*/
public function useDefaults(array $fields) {
$this->defaultFields = $fields;
return $this;
}
/**
* Sets the fromQuery on this InsertQuery object.
*
@ -265,13 +139,13 @@ class Insert extends Query {
/**
* Preprocesses and validates the query.
*
* @return
* @return bool
* TRUE if the validation was successful, FALSE if not.
*
* @throws \Drupal\Core\Database\Query\FieldsOverlapException
* @throws \Drupal\Core\Database\Query\NoFieldsException
*/
public function preExecute() {
protected function preExecute() {
// Confirm that the user did not try to specify an identical
// field and default field.
if (array_intersect($this->insertFields, $this->defaultFields)) {

View file

@ -0,0 +1,184 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\InsertTrait.
*/
namespace Drupal\Core\Database\Query;
/**
* Provides common functionality for INSERT and UPSERT queries.
*
* @ingroup database
*/
trait InsertTrait {
/**
* The table on which to insert.
*
* @var string
*/
protected $table;
/**
* An array of fields on which to insert.
*
* @var array
*/
protected $insertFields = array();
/**
* An array of fields that should be set to their database-defined defaults.
*
* @var array
*/
protected $defaultFields = array();
/**
* A nested array of values to insert.
*
* $insertValues is an array of arrays. Each sub-array is either an
* associative array whose keys are field names and whose values are field
* values to insert, or a non-associative array of values in the same order
* as $insertFields.
*
* Whether multiple insert sets will be run in a single query or multiple
* queries is left to individual drivers to implement in whatever manner is
* most appropriate. The order of values in each sub-array must match the
* order of fields in $insertFields.
*
* @var array
*/
protected $insertValues = array();
/**
* Adds a set of field->value pairs to be inserted.
*
* This method may only be called once. Calling it a second time will be
* ignored. To queue up multiple sets of values to be inserted at once,
* use the values() method.
*
* @param array $fields
* An array of fields on which to insert. This array may be indexed or
* associative. If indexed, the array is taken to be the list of fields.
* If associative, the keys of the array are taken to be the fields and
* the values are taken to be corresponding values to insert. If a
* $values argument is provided, $fields must be indexed.
* @param array $values
* (optional) An array of fields to insert into the database. The values
* must be specified in the same order as the $fields array.
*
* @return $this
* The called object.
*/
public function fields(array $fields, array $values = array()) {
if (empty($this->insertFields)) {
if (empty($values)) {
if (!is_numeric(key($fields))) {
$values = array_values($fields);
$fields = array_keys($fields);
}
}
$this->insertFields = $fields;
if (!empty($values)) {
$this->insertValues[] = $values;
}
}
return $this;
}
/**
* Adds another set of values to the query to be inserted.
*
* If $values is a numeric-keyed array, it will be assumed to be in the same
* order as the original fields() call. If it is associative, it may be
* in any order as long as the keys of the array match the names of the
* fields.
*
* @param array $values
* An array of values to add to the query.
*
* @return $this
* The called object.
*/
public function values(array $values) {
if (is_numeric(key($values))) {
$this->insertValues[] = $values;
}
elseif ($this->insertFields) {
// Reorder the submitted values to match the fields array.
foreach ($this->insertFields as $key) {
$insert_values[$key] = $values[$key];
}
// For consistency, the values array is always numerically indexed.
$this->insertValues[] = array_values($insert_values);
}
return $this;
}
/**
* Specifies fields for which the database defaults should be used.
*
* If you want to force a given field to use the database-defined default,
* not NULL or undefined, use this method to instruct the database to use
* default values explicitly. In most cases this will not be necessary
* unless you are inserting a row that is all default values, as you cannot
* specify no values in an INSERT query.
*
* Specifying a field both in fields() and in useDefaults() is an error
* and will not execute.
*
* @param array $fields
* An array of values for which to use the default values
* specified in the table definition.
*
* @return $this
* The called object.
*/
public function useDefaults(array $fields) {
$this->defaultFields = $fields;
return $this;
}
/**
* Returns the query placeholders for values that will be inserted.
*
* @param array $nested_insert_values
* A nested array of values to insert.
* @param array $default_fields
* An array of fields that should be set to their database-defined defaults.
*
* @return array
* An array of insert placeholders.
*/
protected function getInsertPlaceholderFragment(array $nested_insert_values, array $default_fields) {
$max_placeholder = 0;
$values = array();
if ($nested_insert_values) {
foreach ($nested_insert_values as $insert_values) {
$placeholders = array();
// Default fields aren't really placeholders, but this is the most convenient
// way to handle them.
$placeholders = array_pad($placeholders, count($default_fields), 'default');
$new_placeholder = $max_placeholder + count($insert_values);
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
$placeholders[] = ':db_insert_placeholder_' . $i;
}
$max_placeholder = $new_placeholder;
$values[] = '(' . implode(', ', $placeholders) . ')';
}
}
else {
// If there are no values, then this is a default-only query. We still need to handle that.
$placeholders = array_fill(0, count($default_fields), 'default');
$values[] = '(' . implode(', ', $placeholders) . ')';
}
return $values;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\NoUniqueFieldException.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\DatabaseException;
/**
* Exception thrown if an upsert query doesn't specify a unique field.
*/
class NoUniqueFieldException extends \InvalidArgumentException implements DatabaseException {}

View file

@ -147,6 +147,9 @@ interface SelectInterface extends ConditionInterface, AlterableInterface, Extend
* For some database drivers, it may also wrap the field name in
* database-specific escape characters.
*
* @param string $string
* An unsanitized field name.
*
* @return
* The sanitized field name string.
*/

View file

@ -0,0 +1,119 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Upsert.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
/**
* General class for an abstracted "Upsert" (UPDATE or INSERT) query operation.
*
* This class can only be used with a table with a single unique index.
* Often, this will be the primary key. On such a table this class works like
* Insert except the rows will be set to the desired values even if the key
* existed before.
*/
abstract class Upsert extends Query {
use InsertTrait;
/**
* The unique or primary key of the table.
*
* @var string
*/
protected $key;
/**
* Constructs an Upsert object.
*
* @param \Drupal\Core\Database\Connection $connection
* A Connection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* (optional) An array of database options.
*/
public function __construct(Connection $connection, $table, array $options = []) {
$options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options);
$this->table = $table;
}
/**
* Sets the unique / primary key field to be used as condition for this query.
*
* @param string $field
* The name of the field to set.
*
* @return $this
*/
public function key($field) {
$this->key = $field;
return $this;
}
/**
* Preprocesses and validates the query.
*
* @return bool
* TRUE if the validation was successful, FALSE otherwise.
*
* @throws \Drupal\Core\Database\Query\NoUniqueFieldException
* @throws \Drupal\Core\Database\Query\FieldsOverlapException
* @throws \Drupal\Core\Database\Query\NoFieldsException
*/
protected function preExecute() {
// Confirm that the user set the unique/primary key of the table.
if (!$this->key) {
throw new NoUniqueFieldException('There is no unique field specified.');
}
// Confirm that the user did not try to specify an identical
// field and default field.
if (array_intersect($this->insertFields, $this->defaultFields)) {
throw new FieldsOverlapException('You may not specify the same field to have a value and a schema-default value.');
}
// Don't execute query without fields.
if (count($this->insertFields) + count($this->defaultFields) == 0) {
throw new NoFieldsException('There are no fields available to insert with.');
}
// If no values have been added, silently ignore this query. This can happen
// if values are added conditionally, so we don't want to throw an
// exception.
return isset($this->insertValues[0]) || $this->insertFields;
}
/**
* {@inheritdoc}
*/
public function execute() {
if (!$this->preExecute()) {
return NULL;
}
$max_placeholder = 0;
$values = array();
foreach ($this->insertValues as $insert_values) {
foreach ($insert_values as $value) {
$values[':db_insert_placeholder_' . $max_placeholder++] = $value;
}
}
$last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = array();
return $last_insert_id;
}
}

View file

@ -16,6 +16,11 @@ use Drupal\Core\Database\Query\PlaceholderInterface;
*/
abstract class Schema implements PlaceholderInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
@ -173,25 +178,62 @@ abstract class Schema implements PlaceholderInterface {
}
/**
* Find all tables that are like the specified base table name.
* Finds all tables that are like the specified base table name.
*
* @param $table_expression
* An SQL expression, for example "simpletest%" (without the quotes).
* BEWARE: this is not prefixed, the caller should take care of that.
* @param string $table_expression
* An SQL expression, for example "cache_%" (without the quotes).
*
* @return
* Array, both the keys and the values are the matching tables.
* @return array
* Both the keys and the values are the matching tables.
*/
public function findTables($table_expression) {
$condition = $this->buildTableNameCondition($table_expression, 'LIKE', FALSE);
// Load all the tables up front in order to take into account per-table
// prefixes. The actual matching is done at the bottom of the method.
$condition = $this->buildTableNameCondition('%', 'LIKE');
$condition->compile($this->connection, $this);
$individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
$default_prefix = $this->connection->tablePrefix();
$default_prefix_length = strlen($default_prefix);
$tables = [];
// Normally, we would heartily discourage the use of string
// concatenation for conditionals like this however, we
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.tables table.
return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
$results = $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
foreach ($results as $table) {
// Take into account tables that have an individual prefix.
if (isset($individually_prefixed_tables[$table->table_name])) {
$prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->table_name]));
}
elseif ($default_prefix && substr($table->table_name, 0, $default_prefix_length) !== $default_prefix) {
// This table name does not start the default prefix, which means that
// it is not managed by Drupal so it should be excluded from the result.
continue;
}
else {
$prefix_length = $default_prefix_length;
}
// Remove the prefix from the returned tables.
$unprefixed_table_name = substr($table->table_name, $prefix_length);
// The pattern can match a table which is the same as the prefix. That
// will become an empty string when we remove the prefix, which will
// probably surprise the caller, besides not being a prefixed table. So
// remove it.
if (!empty($unprefixed_table_name)) {
$tables[$unprefixed_table_name] = $unprefixed_table_name;
}
}
// Convert the table expression from its SQL LIKE syntax to a regular
// expression and escape the delimiter that will be used for matching.
$table_expression = str_replace(array('%', '_'), array('.*?', '.'), preg_quote($table_expression, '/'));
$tables = preg_grep('/^' . $table_expression . '$/i', $tables);
return $tables;
}
/**
@ -413,13 +455,51 @@ abstract class Schema implements PlaceholderInterface {
* @code
* $fields = ['foo', ['bar', 4]];
* @endcode
* @param array $spec
* The table specification for the table to be altered. This is used in
* order to be able to ensure that the index length is not too long.
* This schema definition can usually be obtained through hook_schema(), or
* in case the table was created by the Entity API, through the schema
* handler listed in the entity class definition. For reference, see
* SqlContentEntityStorageSchema::getDedicatedTableSchema() and
* SqlContentEntityStorageSchema::getSharedTableFieldSchema().
*
* In order to prevent human error, it is recommended to pass in the
* complete table specification. However, in the edge case of the complete
* table specification not being available, we can pass in a partial table
* definition containing only the fields that apply to the index:
* @code
* $spec = [
* // Example partial specification for a table:
* 'fields' => [
* 'example_field' => [
* 'description' => 'An example field',
* 'type' => 'varchar',
* 'length' => 32,
* 'not null' => TRUE,
* 'default' => '',
* ],
* ],
* 'indexes' => [
* 'table_example_field' => ['example_field'],
* ],
* ];
* @endcode
* Note that the above is a partial table definition and that we would
* usually pass a complete table definition as obtained through
* hook_schema() instead.
*
* @see schemaapi
* @see hook_schema()
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table doesn't exist.
* @throws \Drupal\Core\Database\SchemaObjectExistsException
* If the specified table already has an index by that name.
*
* @todo remove the $spec argument whenever schema introspection is added.
*/
abstract public function addIndex($table, $name, $fields);
abstract public function addIndex($table, $name, $fields, array $spec);
/**
* Drop an index.

View file

@ -55,19 +55,21 @@ class Datelist extends DateElementBase {
$date = NULL;
if ($input !== FALSE) {
$return = $input;
if (isset($input['ampm'])) {
if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
$input['hour'] += 12;
if (empty(static::checkEmptyInputs($input, $parts))) {
if (isset($input['ampm'])) {
if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
$input['hour'] += 12;
}
elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
$input['hour'] -= 12;
}
unset($input['ampm']);
}
elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
$input['hour'] -= 12;
$timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
$date = DrupalDateTime::createFromArray($input, $timezone);
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
static::incrementRound($date, $increment);
}
unset($input['ampm']);
}
$timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
$date = DrupalDateTime::createFromArray($input, $timezone);
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
static::incrementRound($date, $increment);
}
}
else {
@ -250,7 +252,7 @@ class Datelist extends DateElementBase {
$title = '';
}
$default = !empty($element['#value'][$part]) ? $element['#value'][$part] : '';
$default = isset($element['#value'][$part]) && trim($element['#value'][$part]) != '' ? $element['#value'][$part] : '';
$value = $date instanceOf DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
if (!empty($value) && $part != 'ampm') {
$value = intval($value);
@ -265,7 +267,7 @@ class Datelist extends DateElementBase {
'#attributes' => $element['#attributes'],
'#options' => $options,
'#required' => $element['#required'],
'#error_no_message' => TRUE,
'#error_no_message' => FALSE,
);
}
@ -300,6 +302,7 @@ class Datelist extends DateElementBase {
$input_exists = FALSE;
$input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
if ($input_exists) {
$all_empty = static::checkEmptyInputs($input, $element['#date_part_order']);
// If there's empty input and the field is not required, set it to empty.
if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
@ -309,6 +312,11 @@ class Datelist extends DateElementBase {
elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
$form_state->setError($element, t('The %field date is required.'));
}
elseif (!empty($all_empty)) {
foreach ($all_empty as $value){
$form_state->setError($element[$value], t('A value must be selected for %part.', array('%part' => $value)));
}
}
else {
// If the input is valid, set it.
$date = $input['object'];
@ -317,12 +325,34 @@ class Datelist extends DateElementBase {
}
// If the input is invalid, set an error.
else {
$form_state->setError($element, t('The %field date is invalid.'));
$form_state->setError($element, t('The %field date is invalid.', array('%field' => !empty($element['#title']) ? $element['#title'] : '')));
}
}
}
}
/**
* Checks the input array for empty values.
*
* Input array keys are checked against values in the parts array. Elements
* not in the parts array are ignored. Returns an array representing elements
* from the input array that have no value. If no empty values are found,
* returned array is empty.
*
* @param array $input
* Array of individual inputs to check for value.
* @param array $parts
* Array to check input against, ignoring elements not in this array.
*
* @return array
* Array of keys from the input array that have no value, may be empty.
*/
protected static function checkEmptyInputs($input, $parts) {
// Filters out empty array values, any valid value would have a string length.
$filtered_input = array_filter($input, 'strlen');
return array_diff($parts, array_keys($filtered_input));
}
/**
* Rounds minutes and seconds to nearest requested value.
*

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\Core\DependencyInjection\Compiler\TwigExtensionPass.
*/
namespace Drupal\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Adds the twig_extension_hash parameter to the container.
*
* twig_extension_hash is a hash of all extension mtimes for Twig template
* invalidation.
*/
class TwigExtensionPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container) {
$twig_extension_hash = '';
foreach (array_keys($container->findTaggedServiceIds('twig.extension')) as $service_id) {
$class_name = $container->getDefinition($service_id)->getClass();
$reflection = new \ReflectionClass($class_name);
// We use the class names as hash in order to invalidate on new extensions
// and mtime for every time we change an existing file.
$twig_extension_hash .= $class_name . filemtime($reflection->getFileName());
}
$container->setParameter('twig_extension_hash', hash('crc32b', $twig_extension_hash));
}
}

View file

@ -7,17 +7,18 @@
namespace Drupal\Core\DependencyInjection;
use Symfony\Component\DependencyInjection\Container as SymfonyContainer;
use Drupal\Component\DependencyInjection\Container as DrupalContainer;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Extends the symfony container to set the service ID on the created object.
* Extends the Drupal container to set the service ID on the created object.
*/
class Container extends SymfonyContainer {
class Container extends DrupalContainer {
/**
* {@inheritdoc}
*/
public function set($id, $service, $scope = SymfonyContainer::SCOPE_CONTAINER) {
public function set($id, $service, $scope = ContainerInterface::SCOPE_CONTAINER) {
parent::set($id, $service, $scope);
// Ensure that the _serviceId property is set on synthetic services as well.
@ -30,7 +31,7 @@ class Container extends SymfonyContainer {
* {@inheritdoc}
*/
public function __sleep() {
trigger_error('The container was serialized.', E_USER_ERROR);
assert(FALSE, 'The container was serialized.');
return array_keys(get_object_vars($this));
}

View file

@ -118,7 +118,7 @@ class ContainerBuilder extends SymfonyContainerBuilder {
* {@inheritdoc}
*/
public function __sleep() {
trigger_error('The container was serialized.', E_USER_ERROR);
assert(FALSE, 'The container was serialized.');
return array_keys(get_object_vars($this));
}

View file

@ -9,7 +9,7 @@ namespace Drupal\Core\Diff;
use Drupal\Component\Diff\DiffFormatter as DiffFormatterBase;
use Drupal\Component\Diff\WordLevelDiff;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\ConfigFactoryInterface;
/**
@ -107,7 +107,7 @@ class DiffFormatter extends DiffFormatterBase {
'class' => 'diff-marker',
),
array(
'data' => $line,
'data' => ['#markup' => $line],
'class' => 'diff-context diff-addedline',
)
);
@ -129,7 +129,7 @@ class DiffFormatter extends DiffFormatterBase {
'class' => 'diff-marker',
),
array(
'data' => $line,
'data' => ['#markup' => $line],
'class' => 'diff-context diff-deletedline',
)
);
@ -148,7 +148,7 @@ class DiffFormatter extends DiffFormatterBase {
return array(
' ',
array(
'data' => $line,
'data' => ['#markup' => $line],
'class' => 'diff-context',
)
);
@ -172,7 +172,7 @@ class DiffFormatter extends DiffFormatterBase {
*/
protected function _added($lines) {
foreach ($lines as $line) {
$this->rows[] = array_merge($this->emptyLine(), $this->addedLine(SafeMarkup::checkPlain($line)));
$this->rows[] = array_merge($this->emptyLine(), $this->addedLine(Html::escape($line)));
}
}
@ -181,7 +181,7 @@ class DiffFormatter extends DiffFormatterBase {
*/
protected function _deleted($lines) {
foreach ($lines as $line) {
$this->rows[] = array_merge($this->deletedLine(SafeMarkup::checkPlain($line)), $this->emptyLine());
$this->rows[] = array_merge($this->deletedLine(Html::escape($line)), $this->emptyLine());
}
}
@ -190,7 +190,7 @@ class DiffFormatter extends DiffFormatterBase {
*/
protected function _context($lines) {
foreach ($lines as $line) {
$this->rows[] = array_merge($this->contextLine(SafeMarkup::checkPlain($line)), $this->contextLine(SafeMarkup::checkPlain($line)));
$this->rows[] = array_merge($this->contextLine(Html::escape($line)), $this->contextLine(Html::escape($line)));
}
}
@ -198,6 +198,8 @@ class DiffFormatter extends DiffFormatterBase {
* {@inheritdoc}
*/
protected function _changed($orig, $closing) {
$orig = array_map('\Drupal\Component\Utility\Html::escape', $orig);
$closing = array_map('\Drupal\Component\Utility\Html::escape', $closing);
$diff = new WordLevelDiff($orig, $closing);
$del = $diff->orig();
$add = $diff->closing();

View file

@ -15,6 +15,7 @@ use Drupal\Core\Config\BootstrapConfigStorageFactory;
use Drupal\Core\Config\NullStorage;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\Core\DependencyInjection\YamlFileLoader;
use Drupal\Core\Extension\ExtensionDiscovery;
@ -22,12 +23,10 @@ use Drupal\Core\File\MimeType\MimeTypeGuesser;
use Drupal\Core\Http\TrustedHostsRequestFactory;
use Drupal\Core\Language\Language;
use Drupal\Core\PageCache\RequestPolicyInterface;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\Core\Site\Settings;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
@ -52,6 +51,55 @@ use Symfony\Component\Routing\Route;
*/
class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
/**
* Holds the class used for dumping the container to a PHP array.
*
* In combination with swapping the container class this is useful to e.g.
* dump to the human-readable PHP array format to debug the container
* definition in an easier way.
*
* @var string
*/
protected $phpArrayDumperClass = '\Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper';
/**
* Holds the default bootstrap container definition.
*
* @var array
*/
protected $defaultBootstrapContainerDefinition = [
'parameters' => [],
'services' => [
'database' => [
'class' => 'Drupal\Core\Database\Connection',
'factory' => 'Drupal\Core\Database\Database::getConnection',
'arguments' => ['default'],
],
'cache.container' => [
'class' => 'Drupal\Core\Cache\DatabaseBackend',
'arguments' => ['@database', '@cache_tags_provider.container', 'container'],
],
'cache_tags_provider.container' => [
'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum',
'arguments' => ['@database'],
],
],
];
/**
* Holds the class used for instantiating the bootstrap container.
*
* @var string
*/
protected $bootstrapContainerClass = '\Drupal\Component\DependencyInjection\PhpArrayContainer';
/**
* Holds the bootstrap container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $bootstrapContainer;
/**
* Holds the container instance.
*
@ -96,13 +144,6 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
*/
protected $moduleData = array();
/**
* PHP code storage object to use for the compiled container.
*
* @var \Drupal\Component\PhpStorage\PhpStorageInterface
*/
protected $storage;
/**
* The class loader object.
*
@ -151,13 +192,16 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
protected $serviceYamls;
/**
* List of discovered service provider class names.
* List of discovered service provider class names or objects.
*
* This is a nested array whose top-level keys are 'app' and 'site', denoting
* the origin of a service provider. Site-specific providers have to be
* collected separately, because they need to be processed last, so as to be
* able to override services from application service providers.
*
* Allowing objects is for example used to allow
* \Drupal\KernelTests\KernelTestBase to register itself as service provider.
*
* @var array
*/
protected $serviceProviderClasses;
@ -393,6 +437,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
FileCacheFactory::setConfiguration($configuration);
FileCacheFactory::setPrefix(Settings::getApcuPrefix('file_cache', $this->root));
$this->bootstrapContainer = new $this->bootstrapContainerClass(Settings::get('bootstrap_container_definition', $this->defaultBootstrapContainerDefinition));
// Initialize the container.
$this->initializeContainer();
@ -427,6 +473,34 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
return $this->container;
}
/**
* {@inheritdoc}
*/
public function setContainer(ContainerInterface $container = NULL) {
if (isset($this->container)) {
throw new \Exception('The container should not override an existing container.');
}
if ($this->booted) {
throw new \Exception('The container cannot be set after a booted kernel.');
}
$this->container = $container;
return $this;
}
/**
* {@inheritdoc}
*/
public function getCachedContainerDefinition() {
$cache = $this->bootstrapContainer->get('cache.container')->get($this->getContainerCacheKey());
if ($cache) {
return $cache->data;
}
return NULL;
}
/**
* {@inheritdoc}
*/
@ -465,16 +539,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
// Put the request on the stack.
$this->container->get('request_stack')->push($request);
// Set the allowed protocols once we have the config available.
$allowed_protocols = $this->container->getParameter('filter_protocols');
if (!$allowed_protocols) {
// \Drupal\Component\Utility\UrlHelper::filterBadProtocol() is called by
// the installer and update.php, in which case the configuration may not
// exist (yet). Provide a minimal default set of allowed protocols for
// these cases.
$allowed_protocols = array('http', 'https');
}
UrlHelper::setAllowedProtocols($allowed_protocols);
// Set the allowed protocols.
UrlHelper::setAllowedProtocols($this->container->getParameter('filter_protocols'));
// Override of Symfony's mime type guesser singleton.
MimeTypeGuesser::registerWithSymfonyGuesser($this->container);
@ -522,14 +588,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
// Add site-specific service providers.
if (!empty($GLOBALS['conf']['container_service_providers'])) {
foreach ($GLOBALS['conf']['container_service_providers'] as $class) {
if (class_exists($class)) {
if ((is_string($class) && class_exists($class)) || (is_object($class) && ($class instanceof ServiceProviderInterface || $class instanceof ServiceModifierInterface))) {
$this->serviceProviderClasses['site'][] = $class;
}
}
}
if (!$this->addServiceFiles(Settings::get('container_yamls'))) {
throw new \Exception('The container_yamls setting is missing from settings.php');
}
$this->addServiceFiles(Settings::get('container_yamls', []));
}
/**
@ -602,6 +666,9 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
*
* @return Response
* A Response instance
*
* @throws \Exception
* If the passed in exception cannot be turned into a response.
*/
protected function handleException(\Exception $e, $request, $type) {
if ($e instanceof HttpExceptionInterface) {
@ -610,24 +677,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
return $response;
}
else {
// @todo: _drupal_log_error() and thus _drupal_exception_handler() prints
// the message directly. Extract a function which generates and returns it
// instead, then remove the output buffer hack here.
ob_start();
try {
// @todo: The exception handler prints the message directly. Extract a
// function which returns the message instead.
_drupal_exception_handler($e);
}
catch (\Exception $e) {
$message = Settings::get('rebuild_message', 'If you have just changed code (for example deployed a new module or moved an existing one) read <a href="https://www.drupal.org/documentation/rebuild">https://www.drupal.org/documentation/rebuild</a>');
if ($message && Settings::get('rebuild_access', FALSE)) {
$rebuild_path = $GLOBALS['base_url'] . '/rebuild.php';
$message .= " or run the <a href=\"$rebuild_path\">rebuild script</a>";
}
print $message;
}
return new Response(ob_get_clean(), 500);
throw $e;
}
}
@ -711,24 +761,14 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
}
/**
* Returns the classname based on environment.
* Returns the container cache key based on the environment.
*
* @return string
* The class name.
* The cache key used for the service container.
*/
protected function getClassName() {
$parts = array('service_container', $this->environment, hash('crc32b', \Drupal::VERSION . Settings::get('deployment_identifier')));
return implode('_', $parts);
}
/**
* Returns the container class namespace based on the environment.
*
* @return string
* The class name.
*/
protected function getClassNamespace() {
return 'Drupal\\Core\\DependencyInjection\\Container\\' . $this->environment;
protected function getContainerCacheKey() {
$parts = array('service_container', $this->environment, \Drupal::VERSION, Settings::get('deployment_identifier'));
return implode(':', $parts);
}
/**
@ -767,28 +807,42 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
}
}
// If the module list hasn't already been set in updateModules and we are
// not forcing a rebuild, then try and load the container from the disk.
if (empty($this->moduleList) && !$this->containerNeedsRebuild) {
$fully_qualified_class_name = '\\' . $this->getClassNamespace() . '\\' . $this->getClassName();
// First, try to load from storage.
if (!class_exists($fully_qualified_class_name, FALSE)) {
$this->storage()->load($this->getClassName() . '.php');
}
// If the load succeeded or the class already existed, use it.
if (class_exists($fully_qualified_class_name, FALSE)) {
$container = new $fully_qualified_class_name;
}
// If we haven't booted yet but there is a container, then we're asked to
// boot the container injected via setContainer().
// @see \Drupal\KernelTests\KernelTestBase::setUp()
if (isset($this->container) && !$this->booted) {
$container = $this->container;
}
if (!isset($container)) {
// If the module list hasn't already been set in updateModules and we are
// not forcing a rebuild, then try and load the container from the cache.
if (empty($this->moduleList) && !$this->containerNeedsRebuild) {
$container_definition = $this->getCachedContainerDefinition();
}
// If there is no container and no cached container definition, build a new
// one from scratch.
if (!isset($container) && !isset($container_definition)) {
$container = $this->compileContainer();
// Only dump the container if dumping is allowed. This is useful for
// KernelTestBase, which never wants to use the real container, but always
// the container builder.
if ($this->allowDumping) {
$dumper = new $this->phpArrayDumperClass($container);
$container_definition = $dumper->getArray();
}
}
// The container was rebuilt successfully.
$this->containerNeedsRebuild = FALSE;
// Only create a new class if we have a container definition.
if (isset($container_definition)) {
$class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container');
$container = new $class($container_definition);
}
$this->attachSynthetic($container);
$this->container = $container;
@ -813,9 +867,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
\Drupal::setContainer($this->container);
// If needs dumping flag was set, dump the container.
$base_class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container');
if ($this->containerNeedsDumping && !$this->dumpDrupalContainer($this->container, $base_class)) {
$this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be written to disk');
if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) {
$this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be saved to cache.');
}
return $this->container;
@ -870,6 +923,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
// Simpletest's internal browser.
define('DRUPAL_TEST_IN_CHILD_SITE', TRUE);
// Web tests are to be conducted with runtime assertions active.
assert_options(ASSERT_ACTIVE, TRUE);
// Now synchronize PHP 5 and 7's handling of assertions as much as
// possible.
\Drupal\Component\Assertion\Handle::register();
// Log fatal errors to the test site directory.
ini_set('log_errors', 1);
ini_set('error_log', DRUPAL_ROOT . '/sites/simpletest/' . substr($test_prefix, 10) . '/error.log');
@ -1031,9 +1090,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
return;
}
// Also wipe the PHP Storage caches, so that the container is rebuilt
// for the next request.
$this->storage()->deleteAll();
// Also remove the container definition from the cache backend.
$this->bootstrapContainer->get('cache.container')->deleteAll();
}
/**
@ -1171,7 +1229,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
);
foreach ($this->serviceProviderClasses as $origin => $classes) {
foreach ($classes as $name => $class) {
$this->serviceProviders[$origin][$name] = new $class;
if (!is_object($class)) {
$this->serviceProviders[$origin][$name] = new $class;
}
else {
$this->serviceProviders[$origin][$name] = $class;
}
}
}
}
@ -1186,35 +1249,28 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
}
/**
* Dumps the service container to PHP code in the config directory.
* Stores the container definition in a cache.
*
* This method is based on the dumpContainer method in the parent class, but
* that method is reliant on the Config component which we do not use here.
*
* @param ContainerBuilder $container
* The service container.
* @param string $baseClass
* The name of the container's base class
* @param array $container_definition
* The container definition to cache.
*
* @return bool
* TRUE if the container was successfully dumped to disk.
* TRUE if the container was successfully cached.
*/
protected function dumpDrupalContainer(ContainerBuilder $container, $baseClass) {
if (!$this->storage()->writeable()) {
return FALSE;
protected function cacheDrupalContainer(array $container_definition) {
$saved = TRUE;
try {
$this->bootstrapContainer->get('cache.container')->set($this->getContainerCacheKey(), $container_definition);
}
catch (\Exception $e) {
// There is no way to get from the Cache API if the cache set was
// successful or not, hence an Exception is caught and the caller informed
// about the error condition.
$saved = FALSE;
}
// Cache the container.
$dumper = new PhpDumper($container);
$class = $this->getClassName();
$namespace = $this->getClassNamespace();
$content = $dumper->dump([
'class' => $class,
'base_class' => $baseClass,
'namespace' => $namespace,
]);
return $this->storage()->save($class . '.php', $content);
}
return $saved;
}
/**
* Gets a http kernel from the container
@ -1225,18 +1281,6 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
return $this->container->get('http_kernel');
}
/**
* Gets the PHP code storage object to use for the compiled container.
*
* @return \Drupal\Component\PhpStorage\PhpStorageInterface
*/
protected function storage() {
if (!isset($this->storage)) {
$this->storage = PhpStorageFactory::get('service_container');
}
return $this->storage;
}
/**
* Returns the active configuration storage to use during building the container.
*
@ -1435,17 +1479,10 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
/**
* Add service files.
*
* @param $service_yamls
* @param string[] $service_yamls
* A list of service files.
*
* @return bool
* TRUE if the list was an array, FALSE otherwise.
*/
protected function addServiceFiles($service_yamls) {
if (is_array($service_yamls)) {
$this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists');
return TRUE;
}
return FALSE;
protected function addServiceFiles(array $service_yamls) {
$this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists');
}
}

View file

@ -7,6 +7,7 @@
namespace Drupal\Core;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
@ -16,7 +17,7 @@ use Symfony\Component\HttpFoundation\Request;
* This interface extends Symfony's KernelInterface and adds methods for
* responding to modules being enabled or disabled during its lifetime.
*/
interface DrupalKernelInterface extends HttpKernelInterface {
interface DrupalKernelInterface extends HttpKernelInterface, ContainerAwareInterface {
/**
* Boots the current kernel.
@ -57,6 +58,16 @@ interface DrupalKernelInterface extends HttpKernelInterface {
*/
public function getContainer();
/**
* Returns the cached container definition - if any.
*
* This also allows inspecting a built container for debugging purposes.
*
* @return array|NULL
* The cached container definition or NULL if not found in cache.
*/
public function getCachedContainerDefinition();
/**
* Set the current site path.
*

View file

@ -801,6 +801,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$translation->translations = &$this->translations;
$translation->enforceIsNew = &$this->enforceIsNew;
$translation->newRevision = &$this->newRevision;
$translation->entityKeys = &$this->entityKeys;
$translation->translatableEntityKeys = &$this->translatableEntityKeys;
$translation->translationInitialize = FALSE;
$translation->typedData = NULL;

View file

@ -49,7 +49,7 @@ class EntityFormDisplay extends EntityDisplayBase implements EntityFormDisplayIn
* Returns the entity_form_display object used to build an entity form.
*
* Depending on the configuration of the form mode for the entity bundle, this
* can be either the display object associated to the form mode, or the
* can be either the display object associated with the form mode, or the
* 'default' display.
*
* This method should only be used internally when rendering an entity form.

View file

@ -46,8 +46,8 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
* Returns the display objects used to render a set of entities.
*
* Depending on the configuration of the view mode for each bundle, this can
* be either the display object associated to the view mode, or the 'default'
* display.
* be either the display object associated with the view mode, or the
* 'default' display.
*
* This method should only be used internally when rendering an entity. When
* assigning suggested display options for a component in a given view mode,

View file

@ -9,6 +9,7 @@ namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
@ -95,13 +96,13 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
* {@inheritdoc}
*/
public function applyUpdates() {
$change_list = $this->getChangeList();
if ($change_list) {
$complete_change_list = $this->getChangeList();
if ($complete_change_list) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
foreach ($change_list as $entity_type_id => $change_list) {
foreach ($complete_change_list as $entity_type_id => $change_list) {
// Process entity type definition changes before storage definitions ones
// this is necessary when you change an entity type from non-revisionable
// to revisionable and at the same time add revisionable fields to the
@ -127,42 +128,76 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
/**
* {@inheritdoc}
*/
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE) {
$change_list = $this->getChangeList();
if (!isset($change_list[$entity_type_id]) || $change_list[$entity_type_id]['entity_type'] !== $op) {
return FALSE;
}
if ($reset_cached_definitions) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
$this->doEntityUpdate($op, $entity_type_id);
return TRUE;
public function getEntityType($entity_type_id) {
$entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id);
return $entity_type ? clone $entity_type : NULL;
}
/**
* {@inheritdoc}
*/
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE) {
$change_list = $this->getChangeList();
if (!isset($change_list[$entity_type_id]['field_storage_definitions']) || $change_list[$entity_type_id]['field_storage_definitions'][$field_name] !== $op) {
return FALSE;
public function installEntityType(EntityTypeInterface $entity_type) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeCreate($entity_type);
}
/**
* {@inheritdoc}
*/
public function updateEntityType(EntityTypeInterface $entity_type) {
$original = $this->getEntityType($entity_type->id());
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
}
/**
* {@inheritdoc}
*/
public function uninstallEntityType(EntityTypeInterface $entity_type) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeDelete($entity_type);
}
/**
* {@inheritdoc}
*/
public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition) {
// @todo Pass a mutable field definition interface when we have one. See
// https://www.drupal.org/node/2346329.
if ($storage_definition instanceof BaseFieldDefinition) {
$storage_definition
->setName($name)
->setTargetEntityTypeId($entity_type_id)
->setProvider($provider)
->setTargetBundle(NULL);
}
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
}
if ($reset_cached_definitions) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
/**
* {@inheritdoc}
*/
public function getFieldStorageDefinition($name, $entity_type_id) {
$storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
return isset($storage_definitions[$name]) ? clone $storage_definitions[$name] : NULL;
}
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
$original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
$storage_definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : NULL;
$original_storage_definition = isset($original_storage_definitions[$field_name]) ? $original_storage_definitions[$field_name] : NULL;
/**
* {@inheritdoc}
*/
public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
$original = $this->getFieldStorageDefinition($storage_definition->getName(), $storage_definition->getTargetEntityTypeId());
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $original);
}
$this->doFieldUpdate($op, $storage_definition, $original_storage_definition);
return TRUE;
/**
* {@inheritdoc}
*/
public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionDelete($storage_definition);
}
/**

View file

@ -7,6 +7,8 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines an interface for managing entity definition updates.
*
@ -25,12 +27,22 @@ namespace Drupal\Core\Entity;
* report the differences or when to apply each update. This interface is for
* managing that.
*
* This interface also provides methods to retrieve instances of the definitions
* to be updated ready to be manipulated. In fact when definitions change in
* code the system needs to be notified about that and the definitions stored in
* state need to be reconciled with the ones living in code. This typically
* happens in Update API functions, which need to take the system from a known
* state to another known state. Relying on the definitions living in code might
* prevent this, as the system might transition directly to the last available
* state, and thus skipping the intermediate steps. Manipulating the definitions
* in state allows to avoid this and ensures that the various steps of the
* update process are predictable and repeatable.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityTypeListenerInterface
* @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
* @see hook_update_N()
*/
interface EntityDefinitionUpdateManagerInterface {
@ -75,6 +87,9 @@ interface EntityDefinitionUpdateManagerInterface {
/**
* Applies all the detected valid changes.
*
* Use this with care, as it will apply updates for any module, which will
* lead to unpredictable results.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
@ -84,67 +99,92 @@ interface EntityDefinitionUpdateManagerInterface {
public function applyUpdates();
/**
* Performs a single entity definition update.
* Returns an entity type definition ready to be manipulated.
*
* This method should be used from hook_update_N() functions to process
* entity definition updates as part of the update function. This is only
* necessary if the hook_update_N() implementation relies on the entity
* definition update. All remaining entity definition updates will be run
* automatically after the hook_update_N() implementations.
* When needing to apply updates to existing entity type definitions, this
* method should always be used to retrieve a definition ready to be
* manipulated.
*
* @param string $op
* The operation to perform, either static::DEFINITION_CREATED or
* static::DEFINITION_UPDATED.
* @param string $entity_type_id
* The entity type to update.
* @param bool $reset_cached_definitions
* (optional). Determines whether to clear the Entity Manager's cached
* definitions before applying the update. Defaults to TRUE. Can be used
* to prevent unnecessary cache invalidation when a hook_update_N() makes
* multiple calls to this method.
* The entity type identifier.
*
* @return bool
* TRUE if the entity update is processed, FALSE if not.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
* apply some other process, such as a custom update function or a
* migration via the Migrate module.
* @return \Drupal\Core\Entity\EntityTypeInterface
* The entity type definition.
*/
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE);
public function getEntityType($entity_type_id);
/**
* Performs a single field storage definition update.
* Installs a new entity type definition.
*
* This method should be used from hook_update_N() functions to process field
* storage definition updates as part of the update function. This is only
* necessary if the hook_update_N() implementation relies on the field storage
* definition update. All remaining field storage definition updates will be
* run automatically after the hook_update_N() implementations.
*
* @param string $op
* The operation to perform, possible values are static::DEFINITION_CREATED,
* static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
* @param string $entity_type_id
* The entity type to update.
* @param string $field_name
* The field name to update.
* @param bool $reset_cached_definitions
* (optional). Determines whether to clear the Entity Manager's cached
* definitions before applying the update. Defaults to TRUE. Can be used
* to prevent unnecessary cache invalidation when a hook_update_N() makes
* multiple calls to this method.
* @return bool
* TRUE if the entity update is processed, FALSE if not.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
* apply some other process, such as a custom update function or a
* migration via the Migrate module.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE);
public function installEntityType(EntityTypeInterface $entity_type);
/**
* Applies any change performed to the passed entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function updateEntityType(EntityTypeInterface $entity_type);
/**
* Uninstalls an entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function uninstallEntityType(EntityTypeInterface $entity_type);
/**
* Returns a field storage definition ready to be manipulated.
*
* When needing to apply updates to existing field storage definitions, this
* method should always be used to retrieve a storage definition ready to be
* manipulated.
*
* @param string $name
* The field name.
* @param string $entity_type_id
* The entity type identifier.
*
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface
* The field storage definition.
*
* @todo Make this return a mutable storage definition interface when we have
* one. See https://www.drupal.org/node/2346329.
*/
public function getFieldStorageDefinition($name, $entity_type_id);
/**
* Installs a new field storage definition.
*
* @param string $name
* The field storage definition name.
* @param string $entity_type_id
* The target entity type identifier.
* @param string $provider
* The name of the definition provider.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition);
/**
* Applies any change performed to the passed field storage definition.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
/**
* Uninstalls a field storage definition.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
}

View file

@ -256,19 +256,9 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
parent::calculateDependencies();
$target_entity_type = $this->entityManager()->getDefinition($this->targetEntityType);
$bundle_entity_type_id = $target_entity_type->getBundleEntityType();
if ($bundle_entity_type_id != 'bundle') {
// If the target entity type uses entities to manage its bundles then
// depend on the bundle entity.
if (!$bundle_entity = $this->entityManager()->getStorage($bundle_entity_type_id)->load($this->bundle)) {
throw new \LogicException("Missing bundle entity, entity type $bundle_entity_type_id, entity id {$this->bundle}.");
}
$this->addDependency('config', $bundle_entity->getConfigDependencyName());
}
else {
// Depend on the provider of the entity type.
$this->addDependency('module', $target_entity_type->getProvider());
}
// Create dependency on the bundle.
$bundle_config_dependency = $target_entity_type->getBundleConfigDependency($this->bundle);
$this->addDependency($bundle_config_dependency['type'], $bundle_config_dependency['name']);
// If field.module is enabled, add dependencies on 'field_config' entities
// for both displayed and hidden fields. We intentionally leave out base

View file

@ -14,7 +14,10 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a base class for entity handlers.
*
* @todo Deprecate this in https://www.drupal.org/node/2471663.
* @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0.
* Implement the container injection pattern of
* \Drupal\Core\Entity\EntityHandlerInterface::createInstance() to obtain the
* module handler service for your class.
*/
abstract class EntityHandlerBase {
use StringTranslationTrait;

View file

@ -9,7 +9,6 @@ namespace Drupal\Core\Entity;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines a generic implementation to build a listing of entities.
@ -40,9 +39,12 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
protected $entityType;
/**
* The number of entities to list per page.
* The number of entities to list per page, or FALSE to list all entities.
*
* @var int
* For example, set this to FALSE if the list uses client-side filters that
* require all entities to be listed (like the views overview).
*
* @var int|false
*/
protected $limit = 50;
@ -92,25 +94,31 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
* An array of entity IDs.
*/
protected function getEntityIds() {
$query = $this->getStorage()->getQuery();
$keys = $this->entityType->getKeys();
return $query
->sort($keys['id'])
->pager($this->limit)
->execute();
$query = $this->getStorage()->getQuery()
->sort($this->entityType->getKey('id'));
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
return $query->execute();
}
/**
* Gets the escaped label of an entity.
* Gets the label of an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being listed.
*
* @return string
* The escaped entity label.
* The entity label.
*
* @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0
* Use $entity->label() instead. This method used to escape the entity
* label. The render system's autoescape is now relied upon.
*/
protected function getLabel(EntityInterface $entity) {
return SafeMarkup::checkPlain($entity->label());
return $entity->label();
}
/**
@ -227,9 +235,13 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
$build['table']['#rows'][$entity->id()] = $row;
}
}
$build['pager'] = array(
'#type' => 'pager',
);
// Only add the pager if a limit is specified.
if ($this->limit) {
$build['pager'] = array(
'#type' => 'pager',
);
}
return $build;
}

View file

@ -423,7 +423,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
$keys = array_filter($entity_type->getKeys());
// Fail with an exception for non-fieldable entity types.
if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
throw new \LogicException("Getting the base fields is not supported for entity type {$entity_type->getLabel()}.");
}
@ -655,7 +655,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
// bundles, and we do not expect to have so many different entity
// types for this to become a bottleneck.
foreach ($this->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) {
$bundles = array_keys($this->getBundleInfo($entity_type_id));
foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $base_field_definition) {
$this->fieldMap[$entity_type_id][$field_name] = [
@ -1205,7 +1205,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
$this->eventDispatcher->dispatch(EntityTypeEvents::CREATE, new EntityTypeEvent($entity_type));
$this->setLastInstalledDefinition($entity_type);
if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) {
$this->setLastInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id));
}
}

View file

@ -119,7 +119,7 @@ class EntityType implements EntityTypeInterface {
*
* @var string
*/
protected $bundle_entity_type = 'bundle';
protected $bundle_entity_type = NULL;
/**
* The name of the entity type for which bundles are provided.
@ -232,6 +232,13 @@ class EntityType implements EntityTypeInterface {
*/
protected $constraints = array();
/**
* Any additional properties and values.
*
* @var array
*/
protected $additional = [];
/**
* Constructs a new EntityType.
*
@ -248,7 +255,7 @@ class EntityType implements EntityTypeInterface {
}
foreach ($definition as $property => $value) {
$this->{$property} = $value;
$this->set($property, $value);
}
// Ensure defaults.
@ -279,14 +286,25 @@ class EntityType implements EntityTypeInterface {
* {@inheritdoc}
*/
public function get($property) {
return isset($this->{$property}) ? $this->{$property} : NULL;
if (property_exists($this, $property)) {
$value = isset($this->{$property}) ? $this->{$property} : NULL;
}
else {
$value = isset($this->additional[$property]) ? $this->additional[$property] : NULL;
}
return $value;
}
/**
* {@inheritdoc}
*/
public function set($property, $value) {
$this->{$property} = $value;
if (property_exists($this, $property)) {
$this->{$property} = $value;
}
else {
$this->additional[$property] = $value;
}
return $this;
}
@ -764,4 +782,30 @@ class EntityType implements EntityTypeInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function getBundleConfigDependency($bundle) {
// If this entity type uses entities to manage its bundles then depend on
// the bundle entity.
if ($bundle_entity_type_id = $this->getBundleEntityType()) {
if (!$bundle_entity = \Drupal::entityManager()->getStorage($bundle_entity_type_id)->load($bundle)) {
throw new \LogicException(sprintf('Missing bundle entity, entity type %s, entity id %s.', $bundle_entity_type_id, $bundle));
}
$config_dependency = [
'type' => 'config',
'name' => $bundle_entity->getConfigDependencyName(),
];
}
else {
// Depend on the provider of the entity type.
$config_dependency = [
'type' => 'module',
'name' => $this->getProvider(),
];
}
return $config_dependency;
}
}

View file

@ -732,4 +732,17 @@ interface EntityTypeInterface {
*/
public function addConstraint($constraint_name, $options = NULL);
/**
* Gets the config dependency info for this entity, if any exists.
*
* @param string $bundle
* The bundle name.
*
* @return array
* An associative array containing the following keys:
* - 'type': The config dependency type (e.g. 'module', 'config').
* - 'name': The name of the config dependency.
*/
public function getBundleConfigDependency($bundle);
}

View file

@ -465,7 +465,7 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
// series of fields individually for cases such as views tables.
$entity_type_id = $entity->getEntityTypeId();
$bundle = $entity->bundle();
$key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . crc32(serialize($display_options));
$key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . hash('crc32b', serialize($display_options));
if (!isset($this->singleFieldDisplays[$key])) {
$this->singleFieldDisplays[$key] = EntityViewDisplay::create(array(
'targetEntityType' => $entity_type_id,

View file

@ -7,7 +7,6 @@
namespace Drupal\Core\Entity\Plugin\DataType;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
@ -114,7 +113,7 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
*/
public function getProperties($include_computed = FALSE) {
if (!isset($this->entity)) {
throw new MissingDataException(SafeMarkup::format('Unable to get properties as no entity has been provided.'));
throw new MissingDataException('Unable to get properties as no entity has been provided.');
}
if (!$this->entity instanceof FieldableEntityInterface) {
// @todo: Add support for config entities in

View file

@ -189,9 +189,10 @@ class DefaultTableMapping implements TableMappingInterface {
public function getColumnNames($field_name) {
if (!isset($this->columnMapping[$field_name])) {
$this->columnMapping[$field_name] = array();
$storage_definition = $this->fieldStorageDefinitions[$field_name];
foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($storage_definition, $property_name);
if (isset($this->fieldStorageDefinitions[$field_name])) {
foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
}
}
}
return $this->columnMapping[$field_name];

View file

@ -10,6 +10,8 @@ namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
use Drupal\Core\Entity\EntityBundleListenerInterface;
@ -1351,7 +1353,9 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* {@inheritdoc}
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
$this->getStorageSchema()->onEntityTypeCreate($entity_type);
$this->wrapSchemaException(function () use ($entity_type) {
$this->getStorageSchema()->onEntityTypeCreate($entity_type);
});
}
/**
@ -1364,14 +1368,18 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// definition.
$this->initTableLayout();
// Let the schema handler adapt to possible table layout changes.
$this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
$this->wrapSchemaException(function () use ($entity_type, $original) {
$this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
});
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this->getStorageSchema()->onEntityTypeDelete($entity_type);
$this->wrapSchemaException(function () use ($entity_type) {
$this->getStorageSchema()->onEntityTypeDelete($entity_type);
});
}
/**
@ -1386,14 +1394,18 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
$this->tableMapping = NULL;
}
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
$this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
});
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
$this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
$this->wrapSchemaException(function () use ($storage_definition, $original) {
$this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
});
}
/**
@ -1421,7 +1433,31 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
}
// Update the field schema.
$this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
$this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
});
}
/**
* Wraps a database schema exception into an entity storage exception.
*
* @param callable $callback
* The callback to be executed.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* When a database schema exception is thrown.
*/
protected function wrapSchemaException(callable $callback) {
$message = 'Exception thrown while performing a schema update.';
try {
$callback();
}
catch (SchemaException $e) {
throw new EntityStorageException($message, 0, $e);
}
catch (DatabaseExceptionWrapper $e) {
throw new EntityStorageException($message, 0, $e);
}
}
/**
@ -1598,10 +1634,12 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
foreach ($storage_definition->getColumns() as $column_name => $data) {
$or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
}
$query
->condition($or)
->fields('t', array('entity_id'))
->distinct(TRUE);
$query->condition($or);
if (!$as_bool) {
$query
->fields('t', array('entity_id'))
->distinct(TRUE);
}
}
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
// Ascertain the table this field is mapped too.

View file

@ -186,27 +186,44 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
return TRUE;
}
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
return $this->getDedicatedTableSchema($storage_definition) != $this->loadFieldSchemaData($original);
}
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
$field_name = $storage_definition->getName();
$schema = array();
foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
}
}
return $schema != $this->loadFieldSchemaData($original);
}
else {
if ($storage_definition->hasCustomStorage()) {
// The field has custom storage, so we don't know if a schema change is
// needed or not, but since per the initial checks earlier in this
// function, nothing about the definition changed that we manage, we
// return FALSE.
return FALSE;
}
return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original);
}
/**
* Gets the schema data for the given field storage definition.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition. The field that must not have custom
* storage, i.e. the storage must take care of storing the field.
*
* @return array
* The schema data.
*/
protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
assert('!$storage_definition->hasCustomStorage();');
$table_mapping = $this->storage->getTableMapping();
$schema = [];
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
$schema = $this->getDedicatedTableSchema($storage_definition);
}
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
$field_name = $storage_definition->getName();
foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
}
}
}
return $schema;
}
/**
@ -285,7 +302,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// If a migration is required, we can't proceed.
if ($this->requiresEntityDataMigration($entity_type, $original)) {
throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type with data.');
throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type->id() . ') with data.');
}
// If we have no data just recreate the entity schema from scratch.
@ -311,36 +328,12 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
else {
$schema_handler = $this->database->schema();
// Drop original indexes and unique keys.
foreach ($this->loadEntitySchemaData($entity_type) as $table_name => $schema) {
if (!empty($schema['indexes'])) {
foreach ($schema['indexes'] as $name => $specifier) {
$schema_handler->dropIndex($table_name, $name);
}
}
if (!empty($schema['unique keys'])) {
foreach ($schema['unique keys'] as $name => $specifier) {
$schema_handler->dropUniqueKey($table_name, $name);
}
}
}
$this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
// Create new indexes and unique keys.
$entity_schema = $this->getEntitySchema($entity_type, TRUE);
foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) {
if (!empty($schema['indexes'])) {
foreach ($schema['indexes'] as $name => $specifier) {
$schema_handler->addIndex($table_name, $name, $specifier);
}
}
if (!empty($schema['unique keys'])) {
foreach ($schema['unique keys'] as $name => $specifier) {
$schema_handler->addUniqueKey($table_name, $name, $specifier);
}
}
}
$this->createEntitySchemaIndexes($entity_schema);
// Store the updated entity schema.
$this->saveEntitySchemaData($entity_type, $entity_schema);
@ -411,7 +404,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// @todo Add purging to all fields: https://www.drupal.org/node/2282119.
try {
if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field with data that cannot be purged.');
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
}
}
catch (DatabaseException $e) {
@ -1138,9 +1131,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
// Check if the index exists because it might already have been
// created as part of the earlier entity type update event.
if (!$schema_handler->indexExists($table_name, $name)) {
$schema_handler->addIndex($table_name, $name, $specifier);
}
$this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
}
}
if (!empty($schema[$table_name]['unique keys'])) {
@ -1154,7 +1145,14 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
}
$this->saveFieldSchemaData($storage_definition, $schema);
if (!$only_save) {
// Make sure any entity index involving this field is re-created if
// needed.
$this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
}
}
/**
@ -1185,6 +1183,9 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
* The storage definition of the field being deleted.
*/
protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
// Make sure any entity index involving this field is dropped.
$this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
$deleted_field_name = $storage_definition->getName();
$table_mapping = $this->storage->getTableMapping(
$this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
@ -1263,8 +1264,8 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
else {
if ($storage_definition->getColumns() != $original->getColumns()) {
throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
if ($this->hasColumnChanges($storage_definition, $original)) {
throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
}
// There is data, so there are no column changes. Drop all the prior
// indexes and create all the new ones, except for all the priors that
@ -1273,9 +1274,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$table = $table_mapping->getDedicatedDataTableName($original);
$revision_table = $table_mapping->getDedicatedRevisionTableName($original);
// Get the field schemas.
$schema = $storage_definition->getSchema();
$original_schema = $original->getSchema();
// Gets the SQL schema for a dedicated tables.
$actual_schema = $this->getDedicatedTableSchema($storage_definition);
foreach ($original_schema['indexes'] as $name => $columns) {
if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
$real_name = $this->getFieldIndexName($storage_definition, $name);
@ -1302,8 +1307,10 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
}
}
$this->database->schema()->addIndex($table, $real_name, $real_columns);
$this->database->schema()->addIndex($revision_table, $real_name, $real_columns);
// Check if the index exists because it might already have been
// created as part of the earlier entity type update event.
$this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
$this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
}
}
$this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
@ -1348,8 +1355,8 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
else {
if ($storage_definition->getColumns() != $original->getColumns()) {
throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
if ($this->hasColumnChanges($storage_definition, $original)) {
throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
}
$updated_field_name = $storage_definition->getName();
@ -1366,6 +1373,20 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
if ($field_name == $updated_field_name) {
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
// Handle NOT NULL constraints.
foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
$not_null = !empty($specifier['not null']);
$original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
if ($not_null !== $original_not_null) {
if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
}
$column_schema = $original_schema[$table_name]['fields'][$column_name];
$column_schema['not null'] = $not_null;
$schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
}
}
// Drop original indexes and unique keys.
if (!empty($original_schema[$table_name]['indexes'])) {
foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
@ -1380,7 +1401,10 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// Create new indexes and unique keys.
if (!empty($schema[$table_name]['indexes'])) {
foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
$schema_handler->addIndex($table_name, $name, $specifier);
// Check if the index exists because it might already have been
// created as part of the earlier entity type update event.
$this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
}
}
if (!empty($schema[$table_name]['unique keys'])) {
@ -1397,6 +1421,120 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
/**
* Creates the specified entity schema indexes and keys.
*
* @param array $entity_schema
* The entity schema definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface|NULL $storage_definition
* (optional) If a field storage definition is specified, only indexes and
* keys involving its columns will be processed. Otherwise all defined
* entity indexes and keys will be processed.
*/
protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
$schema_handler = $this->database->schema();
if ($storage_definition) {
$table_mapping = $this->storage->getTableMapping();
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
}
$index_keys = [
'indexes' => 'addIndex',
'unique keys' => 'addUniqueKey',
];
foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
// Add fields schema because database driver may depend on this data to
// perform index normalization.
$schema['fields'] = $entity_schema[$table_name]['fields'];
foreach ($index_keys as $key => $add_method) {
if (!empty($schema[$key])) {
foreach ($schema[$key] as $name => $specifier) {
// If a set of field columns were specified we process only indexes
// involving them. Only indexes for which all columns exist are
// actually created.
$create = FALSE;
$specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
$create = TRUE;
foreach ($specifier_columns as $specifier_column_name) {
// This may happen when adding more than one field in the same
// update run. Eventually when all field columns have been
// created the index will be created too.
if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
$create = FALSE;
break;
}
}
}
if ($create) {
$this->{$add_method}($table_name, $name, $specifier, $schema);
}
}
}
}
}
}
/**
* Deletes the specified entity schema indexes and keys.
*
* @param array $entity_schema_data
* The entity schema data definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface|NULL $storage_definition
* (optional) If a field storage definition is specified, only indexes and
* keys involving its columns will be processed. Otherwise all defined
* entity indexes and keys will be processed.
*/
protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
$schema_handler = $this->database->schema();
if ($storage_definition) {
$table_mapping = $this->storage->getTableMapping();
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
}
$index_keys = [
'indexes' => 'dropIndex',
'unique keys' => 'dropUniqueKey',
];
foreach ($entity_schema_data as $table_name => $schema) {
foreach ($index_keys as $key => $drop_method) {
if (!empty($schema[$key])) {
foreach ($schema[$key] as $name => $specifier) {
$specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
$schema_handler->{$drop_method}($table_name, $name);
}
}
}
}
}
}
/**
* Checks whether a field property has NULL values.
*
* @param string $table_name
* The name of the table to inspect.
* @param string $column_name
* The name of the column holding the field property data.
*
* @return bool
* TRUE if NULL data is found, FALSE otherwise.
*/
protected function hasNullFieldPropertyData($table_name, $column_name) {
$query = $this->database->select($table_name, 't')
->fields('t', [$column_name])
->range(0, 1);
$query->isNull('t.' . $column_name);
$result = $query->execute()->fetchAssoc();
return (bool) $result;
}
/**
* Gets the schema for a single field definition.
*
@ -1738,6 +1876,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
return $storage_definition->getName() . '_' . $index;
}
/**
* Checks whether a database table is non-existent or empty.
*
@ -1758,4 +1897,91 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
->fetchField();
}
/**
* Compares schemas to check for changes in the column definitions.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* Current field storage definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
* The original field storage definition.
*
* @return bool
* Returns TRUE if there are schema changes in the column definitions.
*/
protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
if ($storage_definition->getColumns() != $original->getColumns()) {
// Base field definitions have schema data stored in the original
// definition.
return TRUE;
}
if (!$storage_definition->hasCustomStorage()) {
$keys = array_flip($this->getColumnSchemaRelevantKeys());
$definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
foreach ($table_schema['fields'] as $name => $spec) {
$definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
$stored_spec = array_intersect_key($spec, $keys);
if ($definition_spec != $stored_spec) {
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Returns a list of column schema keys affecting data storage.
*
* When comparing schema definitions, only changes in certain properties
* actually affect how data is stored and thus, if applied, may imply data
* manipulation.
*
* @return string[]
* An array of key names.
*/
protected function getColumnSchemaRelevantKeys() {
return ['type', 'size', 'length', 'unsigned'];
}
/**
* Creates an index, dropping it if already existing.
*
* @param string $table
* The table name.
* @param string $name
* The index name.
* @param array $specifier
* The fields to index.
* @param array $schema
* The table specification.
*
* @see \Drupal\Core\Database\Schema::addIndex()
*/
protected function addIndex($table, $name, array $specifier, array $schema) {
$schema_handler = $this->database->schema();
$schema_handler->dropIndex($table, $name);
$schema_handler->addIndex($table, $name, $specifier, $schema);
}
/**
* Creates a unique key, dropping it if already existing.
*
* @param string $table
* The table name.
* @param string $name
* The index name.
* @param array $specifier
* The unique fields.
*
* @see \Drupal\Core\Database\Schema::addUniqueKey()
*/
protected function addUniqueKey($table, $name, array $specifier) {
$schema_handler = $this->database->schema();
$schema_handler->dropUniqueKey($table, $name);
$schema_handler->addUniqueKey($table, $name, $specifier);
}
}

View file

@ -101,6 +101,13 @@ class AjaxResponseSubscriber implements EventSubscriberInterface {
// @see https://www.drupal.org/node/1009382
$response->setContent('<textarea>' . $response->getContent() . '</textarea>');
}
// User-uploaded files cannot set any response headers, so a custom header
// is used to indicate to ajax.js that this response is safe. Note that
// most Ajax requests bound using the Form API will be protected by having
// the URL flagged as trusted in Drupal.settings, so this header is used
// only for things like custom markup that gets Ajax behaviors attached.
$response->headers->set('X-Drupal-Ajax-Token', 1);
}
}

View file

@ -161,7 +161,7 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
// just log it. The DefaultExceptionSubscriber will catch the original
// exception and handle it normally.
$error = Error::decodeException($e);
$this->logger->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error);
$this->logger->log($error['severity_level'], '%type: @message in %function (line %line of %file).', $error);
}
}
}

View file

@ -91,12 +91,20 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface {
if (substr($error['%file'], 0, $root_length) == DRUPAL_ROOT) {
$error['%file'] = substr($error['%file'], $root_length + 1);
}
// Do not translate the string to avoid errors producing more errors.
unset($error['backtrace']);
$message = SafeMarkup::format('%type: !message in %function (line %line of %file).', $error);
// Check if verbose error reporting is on.
if ($this->getErrorLevel() == ERROR_REPORTING_DISPLAY_VERBOSE) {
unset($error['backtrace']);
if ($this->getErrorLevel() != ERROR_REPORTING_DISPLAY_VERBOSE) {
// Without verbose logging, use a simple message.
// We call SafeMarkup::format directly here, rather than use t() since
// we are in the middle of error handling, and we don't want t() to
// cause further errors.
$message = SafeMarkup::format('%type: @message in %function (line %line of %file).', $error);
}
else {
// With verbose logging, we will also include a backtrace.
$backtrace_exception = $exception;
while ($backtrace_exception->getPrevious()) {
$backtrace_exception = $backtrace_exception->getPrevious();
@ -108,9 +116,9 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface {
// once more in the backtrace.
array_shift($backtrace);
// Generate a backtrace containing only scalar argument values. Make
// sure the backtrace is escaped as it can contain user submitted data.
$message .= '<pre class="backtrace">' . SafeMarkup::escape(Error::formatBacktrace($backtrace)) . '</pre>';
// Generate a backtrace containing only scalar argument values.
$error['@backtrace'] = Error::formatBacktrace($backtrace);
$message = SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $error);
}
}

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