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

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -62,6 +62,15 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
// Update the changed timestamp of the entity.
$this->updateChangedTime($this->entity);
}
/**
* {@inheritdoc}
*/
@ -164,7 +173,7 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
// language.
$this->initFormLangcodes($form_state);
$langcode = $this->getFormLangcode($form_state);
$this->entity = $this->entity->getTranslation($langcode);
$this->entity = $this->entity->hasTranslation($langcode) ? $this->entity->getTranslation($langcode) : $this->entity->addTranslation($langcode);
$form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation());
$this->setFormDisplay($form_display, $form_state);
@ -269,4 +278,18 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
}
}
/**
* Updates the changed time of the entity.
*
* Applies only if the entity implements the EntityChangedInterface.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity updated with the submitted values.
*/
public function updateChangedTime(EntityInterface $entity) {
if ($entity->getEntityType()->isSubclassOf(EntityChangedInterface::class)) {
$entity->setChangedTime(REQUEST_TIME);
}
}
}

View file

@ -336,7 +336,7 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Dyn
$this->invokeHook('translation_insert', $entity->getTranslation($langcode));
}
elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) {
$this->invokeHook('translation_delete', $entity->getTranslation($langcode));
$this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
}
}
}

View file

@ -91,17 +91,14 @@ class EntityViewController implements ContainerInjectionInterface {
* @param string $view_mode
* (optional) The view mode that should be used to display the entity.
* Defaults to 'full'.
* @param string $langcode
* (optional) For which language the entity should be rendered, defaults to
* the current content language.
*
* @return array
* A render array as expected by drupal_render().
*/
public function view(EntityInterface $_entity, $view_mode = 'full', $langcode = NULL) {
public function view(EntityInterface $_entity, $view_mode = 'full') {
$page = $this->entityManager
->getViewBuilder($_entity->getEntityTypeId())
->view($_entity, $view_mode, $langcode);
->view($_entity, $view_mode);
$page['#pre_render'][] = [$this, 'buildTitle'];
$page['#entity_type'] = $_entity->getEntityTypeId();

View file

@ -155,7 +155,7 @@ class EntityAutocomplete extends Textfield {
if ($match === NULL) {
// Try to get a match from the input string when the user didn't use
// the autocomplete but filled in a value manually.
$match = $handler->validateAutocompleteInput($input, $element, $form_state, $complete_form, !$autocreate);
$match = static::matchEntityByTitle($handler, $input, $element, $form_state, !$autocreate);
}
if ($match !== NULL) {
@ -202,6 +202,60 @@ class EntityAutocomplete extends Textfield {
$form_state->setValueForElement($element, $value);
}
/**
* Finds an entity from an autocomplete input without an explicit ID.
*
* The method will return an entity ID if one single entity unambuguously
* matches the incoming input, and sill assign form errors otherwise.
*
* @param string $input
* Single string from autocomplete element.
* @param array $element
* The form element to set a form error.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param bool $strict
* Whether to trigger a form error if an element from $input (eg. an entity)
* is not found.
*
* @return integer|null
* Value of a matching entity ID, or NULL if none.
*/
protected static function matchEntityByTitle($handler, $input, &$element, FormStateInterface $form_state, $strict) {
$entities_by_bundle = $handler->getReferenceableEntities($input, '=', 6);
$entities = array_reduce($entities_by_bundle, function ($flattened, $bundle_entities) {
return $flattened + $bundle_entities;
}, []);
$params = array(
'%value' => $input,
'@value' => $input,
);
if (empty($entities)) {
if ($strict) {
// Error if there are no entities available for a required field.
$form_state->setError($element, t('There are no entities matching "%value".', $params));
}
}
elseif (count($entities) > 5) {
$params['@id'] = key($entities);
// Error if there are more than 5 matching entities.
$form_state->setError($element, t('Many entities are called %value. Specify the one you want by appending the id in parentheses, like "@value (@id)".', $params));
}
elseif (count($entities) > 1) {
// More helpful error if there are only a few matching entities.
$multiples = array();
foreach ($entities as $id => $name) {
$multiples[] = $name . ' (' . $id . ')';
}
$params['@id'] = $id;
$form_state->setError($element, t('Multiple entities match this reference; "%multiple". Specify the one you want by appending the id in parentheses, like "@value (@id)".', array('%multiple' => implode('", "', $multiples))));
}
else {
// Take the one and only matching entity.
return key($entities);
}
}
/**
* Converts an array of entity objects into a string of entity labels.
*

View file

@ -311,9 +311,9 @@ abstract class Entity implements EntityInterface {
->getAccessControlHandler($this->entityTypeId)
->createAccess($this->bundle(), $account, [], $return_as_object);
}
return $this->entityManager()
return $this->entityManager()
->getAccessControlHandler($this->entityTypeId)
->access($this, $operation, LanguageInterface::LANGCODE_DEFAULT, $account, $return_as_object);
->access($this, $operation, $account, $return_as_object);
}
/**

View file

@ -181,6 +181,7 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
// Reset the render cache for the target entity type.
parent::postSave($storage, $update);
if (\Drupal::entityManager()->hasHandler($this->targetEntityType, 'view_builder')) {
\Drupal::entityManager()->getViewBuilder($this->targetEntityType)->resetCache();
}
@ -248,7 +249,7 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
$items = $grouped_items[$id];
/** @var \Drupal\Core\Access\AccessResultInterface $field_access */
$field_access = $items->access('view', NULL, TRUE);
$build_list[$id][$name] = $field_access->isAllowed() ? $formatter->view($items) : [];
$build_list[$id][$name] = $field_access->isAllowed() ? $formatter->view($items, $entity->language()->getId()) : [];
// Apply the field access cacheability metadata to the render array.
$this->renderer->addCacheableDependency($build_list[$id][$name], $field_access);
}

View file

@ -53,8 +53,9 @@ class EntityAccessControlHandler extends EntityHandlerBase implements EntityAcce
/**
* {@inheritdoc}
*/
public function access(EntityInterface $entity, $operation, $langcode = LanguageInterface::LANGCODE_DEFAULT, AccountInterface $account = NULL, $return_as_object = FALSE) {
public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$account = $this->prepareUser($account);
$langcode = $entity->language()->getId();
if (($return = $this->getCache($entity->uuid(), $operation, $langcode, $account)) !== NULL) {
// Cache hit, no work necessary.
@ -71,8 +72,8 @@ class EntityAccessControlHandler extends EntityHandlerBase implements EntityAcce
// - No modules say to deny access.
// - At least one module says to grant access.
$access = array_merge(
$this->moduleHandler()->invokeAll('entity_access', array($entity, $operation, $account, $langcode)),
$this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', array($entity, $operation, $account, $langcode))
$this->moduleHandler()->invokeAll('entity_access', [$entity, $operation, $account]),
$this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', [$entity, $operation, $account])
);
$return = $this->processAccessHookResults($access);
@ -80,7 +81,7 @@ class EntityAccessControlHandler extends EntityHandlerBase implements EntityAcce
// Also execute the default access check except when the access result is
// already forbidden, as in that case, it can not be anything else.
if (!$return->isForbidden()) {
$return = $return->orIf($this->checkAccess($entity, $operation, $langcode, $account));
$return = $return->orIf($this->checkAccess($entity, $operation, $account));
}
$result = $this->setCache($return, $entity->uuid(), $operation, $langcode, $account);
return $return_as_object ? $result : $result->isAllowed();
@ -124,15 +125,13 @@ class EntityAccessControlHandler extends EntityHandlerBase implements EntityAcce
* The entity for which to check access.
* @param string $operation
* The entity operation. Usually one of 'view', 'update' or 'delete'.
* @param string $langcode
* The language code for which to check access.
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to check access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($operation == 'delete' && $entity->isNew()) {
return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
}

View file

@ -29,9 +29,6 @@ interface EntityAccessControlHandlerInterface {
* @param string $operation
* The operation access should be checked for.
* Usually one of "view", "update" or "delete".
* @param string $langcode
* (optional) The language code for which to check access. Defaults to
* LanguageInterface::LANGCODE_DEFAULT.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) The user session for which to check access, or NULL to check
* access for the current user. Defaults to NULL.
@ -45,7 +42,7 @@ interface EntityAccessControlHandlerInterface {
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function access(EntityInterface $entity, $operation, $langcode = LanguageInterface::LANGCODE_DEFAULT, AccountInterface $account = NULL, $return_as_object = FALSE);
public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE);
/**
* Checks access to create an entity.

View file

@ -8,7 +8,7 @@
namespace Drupal\Core\Entity;
/**
* An interface for reacting to entity bundle creation, deletion, and renames.
* An interface for reacting to entity bundle creation and deletion.
*
* @todo Convert to Symfony events: https://www.drupal.org/node/2332935
*/
@ -24,20 +24,6 @@ interface EntityBundleListenerInterface {
*/
public function onBundleCreate($bundle, $entity_type_id);
/**
* Reacts to a bundle being renamed.
*
* This method runs before fields are updated with the new bundle name.
*
* @param string $bundle
* The name of the bundle being renamed.
* @param string $bundle_new
* The new name of the bundle.
* @param string $entity_type_id
* The entity type to which the bundle is bound; e.g. 'node' or 'user'.
*/
public function onBundleRename($bundle, $bundle_new, $entity_type_id);
/**
* Reacts to a bundle being deleted.
*

View file

@ -29,6 +29,16 @@ interface EntityChangedInterface {
*/
public function getChangedTime();
/**
* Sets the timestamp of the last entity change for the current translation.
*
* @param int $timestamp
* The timestamp of the last entity save operation.
*
* @return $this
*/
public function setChangedTime($timestamp);
/**
* Gets the timestamp of the last entity change across all translations.
*

View file

@ -28,4 +28,26 @@ trait EntityChangedTrait {
return $changed;
}
/**
* Gets the timestamp of the last entity change for the current translation.
*
* @return int
* The timestamp of the last entity save operation.
*/
public function getChangedTime() {
return $this->get('changed')->value;
}
/**
* Sets the timestamp of the last entity change for the current translation.
*
* @param int $timestamp
* The timestamp of the last entity save operation.
*
* @return $this
*/
public function setChangedTime($timestamp) {
$this->set('changed', $timestamp);
return $this;
}
}

View file

@ -34,7 +34,7 @@ class EntityDeleteForm extends EntityConfirmFormBase {
if (!($entity instanceof ConfigEntityInterface)) {
return $form;
}
$this->addDependencyListsToForm($form, $entity->getConfigDependencyKey(), [$entity->getConfigDependencyName()], $this->getConfigManager(), $this->entityManager);
$this->addDependencyListsToForm($form, $entity->getConfigDependencyKey(), $this->getConfigNamesToDelete($entity), $this->getConfigManager(), $this->entityManager);
return $form;
}
@ -49,4 +49,17 @@ class EntityDeleteForm extends EntityConfirmFormBase {
return \Drupal::service('config.manager');
}
/**
* Returns config names to delete for the deletion confirmation form.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
* The entity being deleted.
*
* @return string[]
* A list of configuration names that will be deleted by this form.
*/
protected function getConfigNamesToDelete(ConfigEntityInterface $entity) {
return [$entity->getConfigDependencyName()];
}
}

View file

@ -278,7 +278,7 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
$mode_entity = $this->entityManager()->getStorage('entity_' . $this->displayContext . '_mode')->load($target_entity_type->id() . '.' . $this->mode);
$this->addDependency('config', $mode_entity->getConfigDependencyName());
}
return $this->dependencies;
return $this;
}
/**
@ -428,18 +428,87 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
}
}
foreach ($this->getComponents() as $name => $component) {
if (isset($component['type']) && $definition = $this->pluginManager->getDefinition($component['type'], FALSE)) {
if (in_array($definition['provider'], $dependencies['module'])) {
if ($renderer = $this->getRenderer($name)) {
if (in_array($renderer->getPluginDefinition()['provider'], $dependencies['module'])) {
// Revert to the defaults if the plugin that supplies the widget or
// formatter depends on a module that is being uninstalled.
$this->setComponent($name);
$changed = TRUE;
}
// Give this component the opportunity to react on dependency removal.
$component_removed_dependencies = $this->getPluginRemovedDependencies($renderer->calculateDependencies(), $dependencies);
if ($component_removed_dependencies) {
if ($renderer->onDependencyRemoval($component_removed_dependencies)) {
// Update component settings to reflect changes.
$component['settings'] = $renderer->getSettings();
$component['third_party_settings'] = [];
foreach ($renderer->getThirdPartyProviders() as $module) {
$component['third_party_settings'][$module] = $renderer->getThirdPartySettings($module);
}
$this->setComponent($name, $component);
$changed = TRUE;
}
// If there are unresolved deleted dependencies left, disable this
// component to avoid the removal of the entire display entity.
if ($this->getPluginRemovedDependencies($renderer->calculateDependencies(), $dependencies)) {
$this->removeComponent($name);
$arguments = [
'@display' => (string) $this->getEntityType()->getLabel(),
'@id' => $this->id(),
'@name' => $name,
];
$this->getLogger()->warning("@display '@id': Component '@name' was disabled because its settings depend on removed dependencies.", $arguments);
$changed = TRUE;
}
}
}
}
return $changed;
}
/**
* Returns the plugin dependencies being removed.
*
* The function recursively computes the intersection between all plugin
* dependencies and all removed dependencies.
*
* Note: The two arguments do not have the same structure.
*
* @param array[] $plugin_dependencies
* A list of dependencies having the same structure as the return value of
* ConfigEntityInterface::calculateDependencies().
* @param array[] $removed_dependencies
* A list of dependencies having the same structure as the input argument of
* ConfigEntityInterface::onDependencyRemoval().
*
* @return array
* A recursively computed intersection.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval()
*/
protected function getPluginRemovedDependencies(array $plugin_dependencies, array $removed_dependencies) {
$intersect = [];
foreach ($plugin_dependencies as $type => $dependencies) {
if ($removed_dependencies[$type]) {
// Config and content entities have the dependency names as keys while
// module and theme dependencies are indexed arrays of dependency names.
// @see \Drupal\Core\Config\ConfigManager::callOnDependencyRemoval()
if (in_array($type, ['config', 'content'])) {
$removed = array_intersect_key($removed_dependencies[$type], array_flip($dependencies));
}
else {
$removed = array_values(array_intersect($removed_dependencies[$type], $dependencies));
}
if ($removed) {
$intersect[$type] = $removed;
}
}
}
return $intersect;
}
/**
* {@inheritdoc}
*/
@ -471,4 +540,14 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
$this->__construct($values, $this->entityTypeId);
}
/**
* Provides the 'system' channel logger service.
*
* @return \Psr\Log\LoggerInterface
* The 'system' channel logger.
*/
protected function getLogger() {
return \Drupal::logger('system');
}
}

View file

@ -93,7 +93,7 @@ abstract class EntityDisplayModeBase extends ConfigEntityBase implements EntityD
parent::calculateDependencies();
$target_entity_type = \Drupal::entityManager()->getDefinition($this->targetEntityType);
$this->addDependency('module', $target_entity_type->getProvider());
return $this->dependencies;
return $this;
}
/**

View file

@ -240,10 +240,19 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
/**
* Acts on an entity before the presave hook is invoked.
*
* Used before the entity is saved and before invoking the presave hook.
* Used before the entity is saved and before invoking the presave hook. Note
* that in case of translatable content entities this callback is only fired
* on their current translation. It is up to the developer to iterate
* over all translations if needed. This is different from its counterpart in
* the Field API, FieldItemListInterface::preSave(), which is fired on all
* field translations automatically.
* @todo Adjust existing implementations and the documentation according to
* https://www.drupal.org/node/2577609 to have a consistent API.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage object.
*
* @see \Drupal\Core\Field\FieldItemListInterface::preSave()
*/
public function preSave(EntityStorageInterface $storage);
@ -251,7 +260,9 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
* Acts on a saved entity before the insert or update hook is invoked.
*
* Used after the entity is saved, but before invoking the insert or update
* hook.
* hook. Note that in case of translatable content entities this callback is
* only fired on their current translation. It is up to the developer to
* iterate over all translations if needed.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage object.

View file

@ -228,6 +228,7 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
'#empty' => $this->t('There is no @label yet.', array('@label' => $this->entityType->getLabel())),
'#cache' => [
'contexts' => $this->entityType->getListCacheContexts(),
'tags' => $this->entityType->getListCacheTags(),
],
);
foreach ($this->load() as $entity) {

View file

@ -32,7 +32,7 @@ use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\Core\TypedData\TypedDataManager;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -111,7 +111,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
/**
* The typed data manager.
*
* @var \Drupal\Core\TypedData\TypedDataManager
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $typedDataManager;
@ -193,14 +193,14 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
* The string translationManager.
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
* @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
* The typed data manager.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The keyvalue factory.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManager $typed_data_manager, KeyValueFactoryInterface $key_value_factory, EventDispatcherInterface $event_dispatcher) {
public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManagerInterface $typed_data_manager, KeyValueFactoryInterface $key_value_factory, EventDispatcherInterface $event_dispatcher) {
parent::__construct('Entity', $namespaces, $module_handler, 'Drupal\Core\Entity\EntityInterface');
$this->setCacheBackend($cache, 'entity_type', array('entity_types'));
@ -945,7 +945,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
foreach ($definitions as $entity_type_id => $definition) {
if ($group) {
$options[$definition->getGroupLabel()][$entity_type_id] = $definition->getLabel();
$options[(string) $definition->getGroupLabel()][$entity_type_id] = $definition->getLabel();
}
else {
$options[$entity_type_id] = $definition->getLabel();
@ -960,7 +960,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
// Make sure that the 'Content' group is situated at the top.
$content = $this->t('Content', array(), array('context' => 'Entity type group'));
$options = array($content => $options[$content]) + $options;
$options = array((string) $content => $options[(string) $content]) + $options;
}
return $options;
@ -1088,15 +1088,29 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
/**
* {@inheritdoc}
*/
public function getViewModeOptions($entity_type, $include_disabled = FALSE) {
return $this->getDisplayModeOptions('view_mode', $entity_type, $include_disabled);
public function getViewModeOptions($entity_type_id) {
return $this->getDisplayModeOptions('view_mode', $entity_type_id);
}
/**
* {@inheritdoc}
*/
public function getFormModeOptions($entity_type, $include_disabled = FALSE) {
return $this->getDisplayModeOptions('form_mode', $entity_type, $include_disabled);
public function getFormModeOptions($entity_type_id) {
return $this->getDisplayModeOptions('form_mode', $entity_type_id);
}
/**
* {@inheritdoc}
*/
public function getViewModeOptionsByBundle($entity_type_id, $bundle) {
return $this->getDisplayModeOptionsByBundle('view_mode', $entity_type_id, $bundle);
}
/**
* {@inheritdoc}
*/
public function getFormModeOptionsByBundle($entity_type_id, $bundle) {
return $this->getDisplayModeOptionsByBundle('form_mode', $entity_type_id, $bundle);
}
/**
@ -1106,19 +1120,55 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
* The display type to be retrieved. It can be "view_mode" or "form_mode".
* @param string $entity_type_id
* The entity type whose display mode options should be returned.
* @param bool $include_disabled
* Force to include disabled display modes. Defaults to FALSE.
*
* @return array
* An array of display mode labels, keyed by the display mode ID.
*/
protected function getDisplayModeOptions($display_type, $entity_type_id, $include_disabled = FALSE) {
protected function getDisplayModeOptions($display_type, $entity_type_id) {
$options = array('default' => t('Default'));
foreach ($this->getDisplayModesByEntityType($display_type, $entity_type_id) as $mode => $settings) {
if (!empty($settings['status']) || $include_disabled) {
$options[$mode] = $settings['label'];
$options[$mode] = $settings['label'];
}
return $options;
}
/**
* Returns an array of display mode options by bundle.
*
* @param $display_type
* The display type to be retrieved. It can be "view_mode" or "form_mode".
* @param string $entity_type_id
* The entity type whose display mode options should be returned.
* @param string $bundle
* The name of the bundle.
*
* @return array
* An array of display mode labels, keyed by the display mode ID.
*/
protected function getDisplayModeOptionsByBundle($display_type, $entity_type_id, $bundle) {
// Collect all the entity's display modes.
$options = $this->getDisplayModeOptions($display_type, $entity_type_id);
// Filter out modes for which the entity display is disabled
// (or non-existent).
$load_ids = array();
// Get the list of available entity displays for the current bundle.
foreach (array_keys($options) as $mode) {
$load_ids[] = $entity_type_id . '.' . $bundle . '.' . $mode;
}
// Load the corresponding displays.
$displays = $this->getStorage($display_type == 'form_mode' ? 'entity_form_display' : 'entity_view_display')
->loadMultiple($load_ids);
// Unset the display modes that are not active or do not exist.
foreach (array_keys($options) as $mode) {
$display_id = $entity_type_id . '.' . $bundle . '.' . $mode;
if (!isset($displays[$display_id]) || !$displays[$display_id]->status()) {
unset($options[$mode]);
}
}
return $options;
}
@ -1317,31 +1367,6 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
$this->moduleHandler->invokeAll('entity_bundle_create', array($entity_type_id, $bundle));
}
/**
* {@inheritdoc}
*/
public function onBundleRename($bundle_old, $bundle_new, $entity_type_id) {
$this->clearCachedBundles();
// Notify the entity storage.
$storage = $this->getStorage($entity_type_id);
if ($storage instanceof EntityBundleListenerInterface) {
$storage->onBundleRename($bundle_old, $bundle_new, $entity_type_id);
}
// Rename existing base field bundle overrides.
$overrides = $this->getStorage('base_field_override')->loadByProperties(array('entity_type' => $entity_type_id, 'bundle' => $bundle_old));
foreach ($overrides as $override) {
$override->set('id', $entity_type_id . '.' . $bundle_new . '.' . $override->getName());
$override->set('bundle', $bundle_new);
$override->allowBundleRename();
$override->save();
}
// Invoke hook_entity_bundle_rename() hook.
$this->moduleHandler->invokeAll('entity_bundle_rename', array($entity_type_id, $bundle_old, $bundle_new));
$this->clearCachedFieldDefinitions();
}
/**
* {@inheritdoc}
*/

View file

@ -252,9 +252,9 @@ interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListe
* Creates a new handler instance for a entity type and handler type.
*
* @param string $entity_type
* The entity type for this controller.
* The entity type for this handler.
* @param string $handler_type
* The controller type to create an instance for.
* The handler type to create an instance for.
*
* @return object
* A handler instance.
@ -432,26 +432,48 @@ interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListe
*
* @param string $entity_type_id
* The entity type whose view mode options should be returned.
* @param bool $include_disabled
* Force to include disabled view modes. Defaults to FALSE.
*
* @return array
* An array of view mode labels, keyed by the display mode ID.
*/
public function getViewModeOptions($entity_type_id, $include_disabled = FALSE);
public function getViewModeOptions($entity_type_id);
/**
* Gets an array of form mode options.
*
* @param string $entity_type_id
* The entity type whose form mode options should be returned.
* @param bool $include_disabled
* Force to include disabled form modes. Defaults to FALSE.
*
* @return array
* An array of form mode labels, keyed by the display mode ID.
*/
public function getFormModeOptions($entity_type_id, $include_disabled = FALSE);
public function getFormModeOptions($entity_type_id);
/**
* Returns an array of view mode options by bundle.
*
* @param string $entity_type_id
* The entity type whose view mode options should be returned.
* @param string $bundle
* The name of the bundle.
*
* @return array
* An array of view mode labels, keyed by the display mode ID.
*/
public function getViewModeOptionsByBundle($entity_type_id, $bundle);
/**
* Returns an array of form mode options by bundle.
*
* @param string $entity_type_id
* The entity type whose form mode options should be returned.
* @param string $bundle
* The name of the bundle.
*
* @return array
* An array of form mode labels, keyed by the display mode ID.
*/
public function getFormModeOptionsByBundle($entity_type_id, $bundle);
/**
* Loads an entity by UUID.

View file

@ -8,16 +8,13 @@
namespace Drupal\Core\Entity\EntityReferenceSelection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Interface definition for Entity Reference Selection plugins.
*
* @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\SelectionBase
* @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager
* @see \Drupal\Core\Entity\Annotation\EntityReferenceSelection
* @see \Drupal\Core\Entity\Plugin\Derivative\SelectionBase
* @see plugin_api
*/
interface SelectionInterface extends PluginFormInterface {
@ -27,7 +24,7 @@ interface SelectionInterface extends PluginFormInterface {
*
* @return array
* A nested array of entities, the first level is keyed by the
* entity bundle, which contains an array of entity labels (safe HTML),
* entity bundle, which contains an array of entity labels (escaped),
* keyed by the entity ID.
*/
public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0);
@ -48,28 +45,6 @@ interface SelectionInterface extends PluginFormInterface {
*/
public function validateReferenceableEntities(array $ids);
/**
* Validates input from an autocomplete widget that has no ID.
*
* @param string $input
* Single string from autocomplete widget.
* @param array $element
* The form element to set a form error.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param array $form
* The form.
* @param bool $strict
* Whether to trigger a form error if an element from $input (eg. an entity)
* is not found. Defaults to TRUE.
*
* @return integer|null
* Value of a matching entity ID, or NULL if none.
*
* @see \Drupal\entity_reference\Plugin\Field\FieldWidget::elementValidate()
*/
public function validateAutocompleteInput($input, &$element, FormStateInterface $form_state, $form, $strict = TRUE);
/**
* Allows the selection to alter the SelectQuery generated by EntityFieldQuery.
*

View file

@ -19,8 +19,6 @@ use Drupal\Core\Plugin\DefaultPluginManager;
*
* @see \Drupal\Core\Entity\Annotation\EntityReferenceSelection
* @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
* @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\SelectionBase
* @see \Drupal\Core\Entity\Plugin\Derivative\SelectionBase
* @see plugin_api
*/
class SelectionPluginManager extends DefaultPluginManager implements SelectionPluginManagerInterface, FallbackPluginManagerInterface {

View file

@ -351,20 +351,26 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
return;
}
// Ensure that the entities are keyed by ID.
$keyed_entities = [];
foreach ($entities as $entity) {
$keyed_entities[$entity->id()] = $entity;
}
// Allow code to run before deleting.
$entity_class = $this->entityClass;
$entity_class::preDelete($this, $entities);
foreach ($entities as $entity) {
$entity_class::preDelete($this, $keyed_entities);
foreach ($keyed_entities as $entity) {
$this->invokeHook('predelete', $entity);
}
// Perform the delete and reset the static cache for the deleted entities.
$this->doDelete($entities);
$this->resetCache(array_keys($entities));
$this->doDelete($keyed_entities);
$this->resetCache(array_keys($keyed_entities));
// Allow code to run after deleting.
$entity_class::postDelete($this, $entities);
foreach ($entities as $entity) {
$entity_class::postDelete($this, $keyed_entities);
foreach ($keyed_entities as $entity) {
$this->invokeHook('delete', $entity);
}
}

View file

@ -110,7 +110,15 @@ class EntityType implements EntityTypeInterface {
/**
* The name of a callback that returns the label of the entity.
*
* @var string|null
* @var callable|null
*
* @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
* Use Drupal\Core\Entity\EntityInterface::label() for complex label
* generation as needed.
*
* @see \Drupal\Core\Entity\EntityInterface::label()
*
* @todo Remove usages of label_callback https://www.drupal.org/node/2450793.
*/
protected $label_callback = NULL;
@ -688,7 +696,7 @@ class EntityType implements EntityTypeInterface {
* {@inheritdoc}
*/
public function getLabel() {
return (string) $this->label;
return $this->label;
}
/**
@ -725,7 +733,7 @@ class EntityType implements EntityTypeInterface {
* {@inheritdoc}
*/
public function getGroupLabel() {
return !empty($this->group_label) ? (string) $this->group_label : $this->t('Other', array(), array('context' => 'Entity type group'));
return !empty($this->group_label) ? $this->group_label : $this->t('Other', array(), array('context' => 'Entity type group'));
}
/**

View file

@ -7,6 +7,8 @@
namespace Drupal\Core\Entity;
use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
/**
* Provides an interface for an entity type and its metadata.
*
@ -15,7 +17,7 @@ namespace Drupal\Core\Entity;
* implemented to alter existing data and fill-in defaults. Module-specific
* properties should be documented in the hook implementations defining them.
*/
interface EntityTypeInterface {
interface EntityTypeInterface extends PluginDefinitionInterface {
/**
* The maximum length of ID, in characters.
@ -66,14 +68,6 @@ interface EntityTypeInterface {
*/
public function getProvider();
/**
* Gets the name of the entity type class.
*
* @return string
* The name of the entity type class.
*/
public function getClass();
/**
* Gets the name of the original entity type class.
*
@ -108,8 +102,8 @@ interface EntityTypeInterface {
* - label: (optional) The name of the property that contains the entity
* label. For example, if the entity's label is located in
* $entity->subject, then 'subject' should be specified here. If complex
* logic is required to build the label, a 'label_callback' should be
* defined instead (see the $label_callback block above for details).
* logic is required to build the label,
* \Drupal\Core\Entity\EntityInterface::label() should be used.
* - langcode: (optional) The name of the property that contains the
* language code. For instance, if the entity's language is located in
* $entity->langcode, then 'langcode' should be specified here.
@ -170,16 +164,6 @@ interface EntityTypeInterface {
*/
public function isPersistentlyCacheable();
/**
* Sets the name of the entity type class.
*
* @param string $class
* The name of the entity type class.
*
* @return $this
*/
public function setClass($class);
/**
* Determines if there is a handler for a given type.
*
@ -493,15 +477,23 @@ interface EntityTypeInterface {
* entity label is the main string associated with an entity; for example, the
* title of a node or the subject of a comment. If there is an entity object
* property that defines the label, use the 'label' element of the
* 'entity_keys' return value component to provide this information (see
* below). If more complex logic is needed to determine the label of an
* entity, you can instead specify a callback function here, which will be
* called to determine the entity label. See also the
* \Drupal\Core\Entity\EntityInterface::label() method, which implements this
* logic.
* 'entity_keys' return value component to provide this information. If more
* complex logic is needed to determine the label of an entity, you can
* instead specify a callback function here, which will be called to determine
* the entity label.
*
* @return callable|null
* The callback, or NULL if none exists.
*
* @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
* Use Drupal\Core\Entity\EntityInterface::label() for complex label
* generation as needed.
*
* @see \Drupal\Core\Entity\EntityInterface::label()
* @see \Drupal\Core\Entity\EntityTypeInterface::setLabelCallback()
* @see \Drupal\Core\Entity\EntityTypeInterface::hasLabelCallback()
*
* @todo Remove usages of label_callback https://www.drupal.org/node/2450793.
*/
public function getLabelCallback();
@ -512,6 +504,13 @@ interface EntityTypeInterface {
* A callable that returns the label of the entity.
*
* @return $this
*
* @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
* Use EntityInterface::label() for complex label generation as needed.
*
* @see \Drupal\Core\Entity\EntityInterface::label()
* @see \Drupal\Core\Entity\EntityTypeInterface::getLabelCallback()
* @see \Drupal\Core\Entity\EntityTypeInterface::hasLabelCallback()
*/
public function setLabelCallback($callback);
@ -519,6 +518,13 @@ interface EntityTypeInterface {
* Indicates if a label callback exists.
*
* @return bool
*
* @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
* Use EntityInterface::label() for complex label generation as needed.
*
* @see \Drupal\Core\Entity\EntityInterface::label()
* @see \Drupal\Core\Entity\EntityTypeInterface::getLabelCallback()
* @see \Drupal\Core\Entity\EntityTypeInterface::setLabelCallback()
*/
public function hasLabelCallback();
@ -645,6 +651,20 @@ interface EntityTypeInterface {
*/
public function setUriCallback($callback);
/**
* Gets the machine name of the entity type group.
*
* @return string
*/
public function getGroup();
/**
* Gets the human-readable name of the entity type group.
*
* @return string
*/
public function getGroupLabel();
/**
* The list cache contexts associated with this entity type.
*

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