Update to Drupal 8.0.0-beta15. For more information, see: https://www.drupal.org/node/2563023
This commit is contained in:
parent
2720a9ec4b
commit
f3791f1da3
1898 changed files with 54300 additions and 11481 deletions
75
core/lib/Drupal/Component/Assertion/Handle.php
Normal file
75
core/lib/Drupal/Component/Assertion/Handle.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
629
core/lib/Drupal/Component/DependencyInjection/Container.php
Normal file
629
core/lib/Drupal/Component/DependencyInjection/Container.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
18
core/lib/Drupal/Component/DependencyInjection/composer.json
Normal file
18
core/lib/Drupal/Component/DependencyInjection/composer.json
Normal 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\\": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = '';
|
||||
|
|
|
@ -102,4 +102,10 @@ class FileReadOnlyStorage implements PhpStorageInterface {
|
|||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function garbageCollection() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -266,4 +266,10 @@ EOF;
|
|||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function garbageCollection() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -338,14 +338,59 @@ EOD;
|
|||
* "<", not "<"). Be careful when using this function, as it will revert
|
||||
* previous sanitization efforts (<script> will become <script>).
|
||||
*
|
||||
* This method is not the opposite of Html::escape(). For example, this method
|
||||
* will convert "é" to "é", whereas Html::escape() will not convert "é"
|
||||
* to "é".
|
||||
*
|
||||
* @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 &
|
||||
* - " (double quote) becomes "
|
||||
* - ' (single quote) becomes '
|
||||
* - < (less than) becomes <
|
||||
* - > (greater than) becomes >
|
||||
* Special characters that have already been escaped will be double-escaped
|
||||
* (for example, "<" becomes "&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 "é", whereas
|
||||
* Html::decodeEntities() will convert all HTML entities to UTF-8 bytes,
|
||||
* including "é" and "<" 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');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
80
core/lib/Drupal/Component/Utility/SafeStringTrait.php
Normal file
80
core/lib/Drupal/Component/Utility/SafeStringTrait.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
namespace Drupal\Component\Uuid;
|
||||
|
||||
/**
|
||||
* Interface that defines a UUID backend.
|
||||
* Interface for generating UUIDs.
|
||||
*/
|
||||
interface UuidInterface {
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
71
core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php
Normal file
71
core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
||||
|
|
26
core/lib/Drupal/Core/Cache/CacheableJsonResponse.php
Normal file
26
core/lib/Drupal/Core/Cache/CacheableJsonResponse.php
Normal 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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
46
core/lib/Drupal/Core/Cache/Context/PathCacheContext.php
Normal file
46
core/lib/Drupal/Core/Cache/Context/PathCacheContext.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
45
core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
Normal file
45
core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
116
core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php
Normal file
116
core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
88
core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
Normal file
88
core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
Normal 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.
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
35
core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
Normal file
35
core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
184
core/lib/Drupal/Core/Database/Query/InsertTrait.php
Normal file
184
core/lib/Drupal/Core/Database/Query/InsertTrait.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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.
|
||||
*/
|
||||
|
|
119
core/lib/Drupal/Core/Database/Query/Upsert.php
Normal file
119
core/lib/Drupal/Core/Database/Query/Upsert.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Reference in a new issue