Update to Drupal 8.0.0-beta15. For more information, see: https://www.drupal.org/node/2563023
This commit is contained in:
parent
2720a9ec4b
commit
f3791f1da3
1898 changed files with 54300 additions and 11481 deletions
|
@ -8,6 +8,8 @@ namespace Drupal\Core\Access;
|
|||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
|
||||
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
|
||||
use Drupal\Core\Config\ConfigBase;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
@ -25,47 +27,10 @@ use Drupal\Core\Session\AccountInterface;
|
|||
*
|
||||
* When using ::orIf() and ::andIf(), cacheability metadata will be merged
|
||||
* accordingly as well.
|
||||
*
|
||||
* @todo Use RefinableCacheableDependencyInterface and the corresponding trait in
|
||||
* https://www.drupal.org/node/2526326.
|
||||
*/
|
||||
abstract class AccessResult implements AccessResultInterface, CacheableDependencyInterface {
|
||||
abstract class AccessResult implements AccessResultInterface, RefinableCacheableDependencyInterface {
|
||||
|
||||
/**
|
||||
* The cache context IDs (to vary a cache item ID based on active contexts).
|
||||
*
|
||||
* @see \Drupal\Core\Cache\Context\CacheContextInterface
|
||||
* @see \Drupal\Core\Cache\Context\CacheContextsManager::convertTokensToKeys()
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $contexts;
|
||||
|
||||
/**
|
||||
* The cache tags.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
/**
|
||||
* The maximum caching time in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $maxAge;
|
||||
|
||||
/**
|
||||
* Constructs a new AccessResult object.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->resetCacheContexts()
|
||||
->resetCacheTags()
|
||||
// Max-age must be non-zero for an access result to be cacheable.
|
||||
// Typically, cache items are invalidated via associated cache tags, not
|
||||
// via a maximum age.
|
||||
->setCacheMaxAge(Cache::PERMANENT);
|
||||
}
|
||||
use RefinableCacheableDependencyTrait;
|
||||
|
||||
/**
|
||||
* Creates an AccessResultInterface object with isNeutral() === TRUE.
|
||||
|
@ -215,35 +180,21 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
sort($this->contexts);
|
||||
return $this->contexts;
|
||||
return $this->cacheContexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheTags() {
|
||||
return $this->tags;
|
||||
return $this->cacheTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheMaxAge() {
|
||||
return $this->maxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cache contexts associated with the access result.
|
||||
*
|
||||
* @param string[] $contexts
|
||||
* An array of cache context IDs, used to generate a cache ID.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addCacheContexts(array $contexts) {
|
||||
$this->contexts = array_unique(array_merge($this->contexts, $contexts));
|
||||
return $this;
|
||||
return $this->cacheMaxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -252,20 +203,7 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
|
|||
* @return $this
|
||||
*/
|
||||
public function resetCacheContexts() {
|
||||
$this->contexts = array();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cache tags associated with the access result.
|
||||
*
|
||||
* @param array $tags
|
||||
* An array of cache tags.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addCacheTags(array $tags) {
|
||||
$this->tags = Cache::mergeTags($this->tags, $tags);
|
||||
$this->cacheContexts = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -275,7 +213,7 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
|
|||
* @return $this
|
||||
*/
|
||||
public function resetCacheTags() {
|
||||
$this->tags = array();
|
||||
$this->cacheTags = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -288,7 +226,7 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
|
|||
* @return $this
|
||||
*/
|
||||
public function setCacheMaxAge($max_age) {
|
||||
$this->maxAge = $max_age;
|
||||
$this->cacheMaxAge = $max_age;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -342,28 +280,6 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
|
|||
return $this->addCacheableDependency($configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a dependency on an object: merges its cacheability metadata.
|
||||
*
|
||||
* @param \Drupal\Core\Cache\CacheableDependencyInterface|object $other_object
|
||||
* The dependency. If the object implements CacheableDependencyInterface,
|
||||
* then its cacheability metadata will be used. Otherwise, the passed in
|
||||
* object must be assumed to be uncacheable, so max-age 0 is set.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addCacheableDependency($other_object) {
|
||||
if ($other_object instanceof CacheableDependencyInterface) {
|
||||
$this->contexts = Cache::mergeContexts($this->contexts, $other_object->getCacheContexts());
|
||||
$this->tags = Cache::mergeTags($this->tags, $other_object->getCacheTags());
|
||||
$this->maxAge = Cache::mergeMaxAges($this->maxAge, $other_object->getCacheMaxAge());
|
||||
}
|
||||
else {
|
||||
$this->maxAge = 0;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -452,12 +368,19 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
|
|||
/**
|
||||
* Inherits the cacheability of the other access result, if any.
|
||||
*
|
||||
* inheritCacheability() differs from addCacheableDependency() in how it
|
||||
* handles max-age, because it is designed to inherit the cacheability of the
|
||||
* second operand in the andIf() and orIf() operations. There, the situation
|
||||
* "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000
|
||||
* as the end result.
|
||||
*
|
||||
* @param \Drupal\Core\Access\AccessResultInterface $other
|
||||
* The other access result, whose cacheability (if any) to inherit.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function inheritCacheability(AccessResultInterface $other) {
|
||||
$this->addCacheableDependency($other);
|
||||
if ($other instanceof CacheableDependencyInterface) {
|
||||
if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
|
||||
$this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
|
||||
|
@ -465,14 +388,6 @@ abstract class AccessResult implements AccessResultInterface, CacheableDependenc
|
|||
else {
|
||||
$this->setCacheMaxAge($other->getCacheMaxAge());
|
||||
}
|
||||
$this->addCacheContexts($other->getCacheContexts());
|
||||
$this->addCacheTags($other->getCacheTags());
|
||||
}
|
||||
// If any of the access results don't provide cacheability metadata, then
|
||||
// we cannot cache the combined access result, for we may not make
|
||||
// assumptions.
|
||||
else {
|
||||
$this->setCacheMaxAge(0);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
|
|
@ -164,15 +164,15 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
|
|||
$resource_commands = array();
|
||||
if ($css_assets) {
|
||||
$css_render_array = $this->cssCollectionRenderer->render($css_assets);
|
||||
$resource_commands[] = new AddCssCommand((string) $this->renderer->renderPlain($css_render_array));
|
||||
$resource_commands[] = new AddCssCommand($this->renderer->renderPlain($css_render_array));
|
||||
}
|
||||
if ($js_assets_header) {
|
||||
$js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header);
|
||||
$resource_commands[] = new PrependCommand('head', (string) $this->renderer->renderPlain($js_header_render_array));
|
||||
$resource_commands[] = new PrependCommand('head', $this->renderer->renderPlain($js_header_render_array));
|
||||
}
|
||||
if ($js_assets_footer) {
|
||||
$js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer);
|
||||
$resource_commands[] = new AppendCommand('body', (string) $this->renderer->renderPlain($js_footer_render_array));
|
||||
$resource_commands[] = new AppendCommand('body', $this->renderer->renderPlain($js_footer_render_array));
|
||||
}
|
||||
foreach (array_reverse($resource_commands) as $resource_command) {
|
||||
$response->addCommand($resource_command, TRUE);
|
||||
|
|
|
@ -29,7 +29,7 @@ trait CommandWithAttachedAssetsTrait {
|
|||
* If content is a render array, it may contain attached assets to be
|
||||
* processed.
|
||||
*
|
||||
* @return string
|
||||
* @return string|\Drupal\Component\Utility\SafeStringInterface
|
||||
* HTML rendered content.
|
||||
*/
|
||||
protected function getRenderedContent() {
|
||||
|
@ -37,10 +37,10 @@ trait CommandWithAttachedAssetsTrait {
|
|||
if (is_array($this->content)) {
|
||||
$html = \Drupal::service('renderer')->renderRoot($this->content);
|
||||
$this->attachedAssets = AttachedAssets::createFromRenderArray($this->content);
|
||||
return (string) $html;
|
||||
return $html;
|
||||
}
|
||||
else {
|
||||
return (string) $this->content;
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,6 @@ class AssetResolver implements AssetResolverInterface {
|
|||
'type' => 'file',
|
||||
'group' => CSS_AGGREGATE_DEFAULT,
|
||||
'weight' => 0,
|
||||
'every_page' => FALSE,
|
||||
'media' => 'all',
|
||||
'preprocess' => TRUE,
|
||||
'browsers' => [],
|
||||
|
@ -221,7 +220,7 @@ class AssetResolver implements AssetResolverInterface {
|
|||
// Add the theme name to the cache key since themes may implement
|
||||
// hook_js_alter(). Additionally add the current language to support
|
||||
// translation of JavaScript files.
|
||||
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets));
|
||||
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize;
|
||||
|
||||
if ($cached = $this->cache->get($cid)) {
|
||||
list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
|
||||
|
@ -231,7 +230,6 @@ class AssetResolver implements AssetResolverInterface {
|
|||
$default_options = [
|
||||
'type' => 'file',
|
||||
'group' => JS_DEFAULT,
|
||||
'every_page' => FALSE,
|
||||
'weight' => 0,
|
||||
'cache' => TRUE,
|
||||
'preprocess' => TRUE,
|
||||
|
@ -338,7 +336,6 @@ class AssetResolver implements AssetResolverInterface {
|
|||
$settings_as_inline_javascript = [
|
||||
'type' => 'setting',
|
||||
'group' => JS_SETTING,
|
||||
'every_page' => TRUE,
|
||||
'weight' => 0,
|
||||
'browsers' => [],
|
||||
'data' => $settings,
|
||||
|
@ -384,16 +381,6 @@ class AssetResolver implements AssetResolverInterface {
|
|||
elseif ($a['group'] > $b['group']) {
|
||||
return 1;
|
||||
}
|
||||
// Within a group, order all infrequently needed, page-specific files after
|
||||
// common files needed throughout the website. Separating this way allows
|
||||
// for the aggregate file generated for all of the common files to be reused
|
||||
// across a site visit without being cut by a page using a less common file.
|
||||
elseif ($a['every_page'] && !$b['every_page']) {
|
||||
return -1;
|
||||
}
|
||||
elseif (!$a['every_page'] && $b['every_page']) {
|
||||
return 1;
|
||||
}
|
||||
// Finally, order by weight.
|
||||
elseif ($a['weight'] < $b['weight']) {
|
||||
return -1;
|
||||
|
|
|
@ -58,9 +58,8 @@ class CssCollectionGrouper implements AssetCollectionGrouperInterface {
|
|||
case 'file':
|
||||
// Group file items if their 'preprocess' flag is TRUE.
|
||||
// Help ensure maximum reuse of aggregate files by only grouping
|
||||
// together items that share the same 'group' value and 'every_page'
|
||||
// flag.
|
||||
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['media'], $item['browsers']) : FALSE;
|
||||
// together items that share the same 'group' value.
|
||||
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['media'], $item['browsers']) : FALSE;
|
||||
break;
|
||||
|
||||
case 'inline':
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
|
||||
/**
|
||||
|
@ -103,7 +103,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
|
|||
// For filthy IE hack.
|
||||
$current_ie_group_keys = NULL;
|
||||
$get_ie_group_key = function ($css_asset) {
|
||||
return array($css_asset['type'], $css_asset['preprocess'], $css_asset['group'], $css_asset['every_page'], $css_asset['media'], $css_asset['browsers']);
|
||||
return array($css_asset['type'], $css_asset['preprocess'], $css_asset['group'], $css_asset['media'], $css_asset['browsers']);
|
||||
};
|
||||
|
||||
// Loop through all CSS assets, by key, to allow for the special IE
|
||||
|
@ -123,9 +123,9 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
|
|||
// LINK tag.
|
||||
// - file CSS assets that can be aggregated (and possibly have been):
|
||||
// in this case, figure out which subsequent file CSS assets share
|
||||
// the same key properties ('group', 'every_page', 'media' and
|
||||
// 'browsers') and output this group into as few STYLE tags as
|
||||
// possible (a STYLE tag may contain only 31 @import statements).
|
||||
// the same key properties ('group', 'media' and 'browsers') and
|
||||
// output this group into as few STYLE tags as possible (a STYLE
|
||||
// tag may contain only 31 @import statements).
|
||||
case 'file':
|
||||
// The dummy query string needs to be added to the URL to control
|
||||
// browser-caching.
|
||||
|
@ -159,7 +159,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
|
|||
$import = array();
|
||||
// Start with the current CSS asset, iterate over subsequent CSS
|
||||
// assets and find which ones have the same 'type', 'group',
|
||||
// 'every_page', 'preprocess', 'media' and 'browsers' properties.
|
||||
// 'preprocess', 'media' and 'browsers' properties.
|
||||
$j = $i;
|
||||
$next_css_asset = $css_asset;
|
||||
$current_ie_group_key = $get_ie_group_key($css_asset);
|
||||
|
@ -168,7 +168,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
|
|||
// control browser-caching. IE7 does not support a media type on
|
||||
// the @import statement, so we instead specify the media for
|
||||
// the group on the STYLE tag.
|
||||
$import[] = '@import url("' . SafeMarkup::checkPlain(file_create_url($next_css_asset['data']) . '?' . $query_string) . '");';
|
||||
$import[] = '@import url("' . Html::escape(file_create_url($next_css_asset['data']) . '?' . $query_string) . '");';
|
||||
// Move the outer for loop skip the next item, since we
|
||||
// processed it here.
|
||||
$i = $j;
|
||||
|
|
|
@ -45,9 +45,8 @@ class JsCollectionGrouper implements AssetCollectionGrouperInterface {
|
|||
case 'file':
|
||||
// Group file items if their 'preprocess' flag is TRUE.
|
||||
// Help ensure maximum reuse of aggregate files by only grouping
|
||||
// together items that share the same 'group' value and 'every_page'
|
||||
// flag.
|
||||
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['browsers']) : FALSE;
|
||||
// together items that share the same 'group' value.
|
||||
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['browsers']) : FALSE;
|
||||
break;
|
||||
|
||||
case 'external':
|
||||
|
|
|
@ -51,12 +51,6 @@ class JsCollectionRenderer implements AssetCollectionRendererInterface {
|
|||
// query-string instead, to enforce reload on every page request.
|
||||
$default_query_string = $this->state->get('system.css_js_query_string') ?: '0';
|
||||
|
||||
// For inline JavaScript to validate as XHTML, all JavaScript containing
|
||||
// XHTML needs to be wrapped in CDATA. To make that backwards compatible
|
||||
// with HTML 4, we need to comment out the CDATA-tag.
|
||||
$embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
|
||||
$embed_suffix = "\n//--><!]]>\n";
|
||||
|
||||
// Defaults for each SCRIPT element.
|
||||
$element_defaults = array(
|
||||
'#type' => 'html_tag',
|
||||
|
@ -73,9 +67,13 @@ class JsCollectionRenderer implements AssetCollectionRendererInterface {
|
|||
// Element properties that depend on item type.
|
||||
switch ($js_asset['type']) {
|
||||
case 'setting':
|
||||
$element['#value_prefix'] = $embed_prefix;
|
||||
$element['#value'] = 'var drupalSettings = ' . Json::encode($js_asset['data']) . ";";
|
||||
$element['#value_suffix'] = $embed_suffix;
|
||||
$element['#attributes'] = array(
|
||||
// This type attribute prevents this from being parsed as an
|
||||
// inline script.
|
||||
'type' => 'application/json',
|
||||
'data-drupal-selector' => 'drupal-settings-json',
|
||||
);
|
||||
$element['#value'] = Json::encode($js_asset['data']);
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
namespace Drupal\Core\Block;
|
||||
|
||||
use Drupal\block\BlockInterface;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
@ -166,7 +165,7 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
|
|||
$form['admin_label'] = array(
|
||||
'#type' => 'item',
|
||||
'#title' => $this->t('Block description'),
|
||||
'#markup' => SafeMarkup::checkPlain($definition['admin_label']),
|
||||
'#plain_text' => $definition['admin_label'],
|
||||
);
|
||||
$form['label'] = array(
|
||||
'#type' => 'textfield',
|
||||
|
|
|
@ -63,6 +63,11 @@ interface BlockPluginInterface extends ConfigurablePluginInterface, PluginFormIn
|
|||
/**
|
||||
* Builds and returns the renderable array for this block plugin.
|
||||
*
|
||||
* If a block should not be rendered because it has no content, then this
|
||||
* method must also ensure to return no content: it must then only return an
|
||||
* empty array, or an empty array with #cache set (with cacheability metadata
|
||||
* indicating the circumstances for it being empty).
|
||||
*
|
||||
* @return array
|
||||
* A renderable array representing the content of the block.
|
||||
*
|
||||
|
|
71
core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php
Normal file
71
core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Breadcrumb\Breadcrumb.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Breadcrumb;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Link;
|
||||
|
||||
/**
|
||||
* Used to return generated breadcrumbs with associated cacheability metadata.
|
||||
*
|
||||
* @todo implement RenderableInterface once https://www.drupal.org/node/2529560 lands.
|
||||
*/
|
||||
class Breadcrumb extends CacheableMetadata {
|
||||
|
||||
/**
|
||||
* An ordered list of links for the breadcrumb.
|
||||
*
|
||||
* @var \Drupal\Core\Link[]
|
||||
*/
|
||||
protected $links = [];
|
||||
|
||||
/**
|
||||
* Gets the breadcrumb links.
|
||||
*
|
||||
* @return \Drupal\Core\Link[]
|
||||
*/
|
||||
public function getLinks() {
|
||||
return $this->links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the breadcrumb links.
|
||||
*
|
||||
* @param \Drupal\Core\Link[] $links
|
||||
* The breadcrumb links.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \LogicException
|
||||
* Thrown when setting breadcrumb links after they've already been set.
|
||||
*/
|
||||
public function setLinks(array $links) {
|
||||
if (!empty($this->links)) {
|
||||
throw new \LogicException('Once breadcrumb links are set, only additional breadcrumb links can be added.');
|
||||
}
|
||||
|
||||
$this->links = $links;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a link to the end of the ordered list of breadcrumb links.
|
||||
*
|
||||
* @param \Drupal\Core\Link $link
|
||||
* The link appended to the breadcrumb.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addLink(Link $link) {
|
||||
$this->links[] = $link;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
|
@ -32,9 +32,8 @@ interface BreadcrumbBuilderInterface {
|
|||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The current route match.
|
||||
*
|
||||
* @return \Drupal\Core\Link[]
|
||||
* An array of links for the breadcrumb. Returning an empty array will
|
||||
* suppress all breadcrumbs.
|
||||
* @return \Drupal\Core\Breadcrumb\Breadcrumb
|
||||
* A breadcrumb.
|
||||
*/
|
||||
public function build(RouteMatchInterface $route_match);
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ class BreadcrumbManager implements ChainBreadcrumbBuilderInterface {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function build(RouteMatchInterface $route_match) {
|
||||
$breadcrumb = array();
|
||||
$breadcrumb = new Breadcrumb();
|
||||
$context = array('builder' => NULL);
|
||||
// Call the build method of registered breadcrumb builders,
|
||||
// until one of them returns an array.
|
||||
|
@ -85,11 +85,9 @@ class BreadcrumbManager implements ChainBreadcrumbBuilderInterface {
|
|||
continue;
|
||||
}
|
||||
|
||||
$build = $builder->build($route_match);
|
||||
$breadcrumb = $builder->build($route_match);
|
||||
|
||||
if (is_array($build)) {
|
||||
// The builder returned an array of breadcrumb links.
|
||||
$breadcrumb = $build;
|
||||
if ($breadcrumb instanceof Breadcrumb) {
|
||||
$context['builder'] = $builder;
|
||||
break;
|
||||
}
|
||||
|
@ -99,7 +97,7 @@ class BreadcrumbManager implements ChainBreadcrumbBuilderInterface {
|
|||
}
|
||||
// Allow modules to alter the breadcrumb.
|
||||
$this->moduleHandler->alter('system_breadcrumb', $breadcrumb, $route_match, $context);
|
||||
// Fall back to an empty breadcrumb.
|
||||
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ namespace Drupal\Core\Cache;
|
|||
* to ensure fast retrieval on the next request. On cache sets and deletes, both
|
||||
* backends will be invoked to ensure consistency.
|
||||
*
|
||||
* @see \Drupal\Core\Cache\ChainedFastBackend
|
||||
*
|
||||
* @ingroup cache
|
||||
*/
|
||||
|
||||
|
|
26
core/lib/Drupal/Core/Cache/CacheableJsonResponse.php
Normal file
26
core/lib/Drupal/Core/Cache/CacheableJsonResponse.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Cache\CacheableResponse.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Cache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
|
||||
/**
|
||||
* A JsonResponse that contains and can expose cacheability metadata.
|
||||
*
|
||||
* Supports Drupal's caching concepts: cache tags for invalidation and cache
|
||||
* contexts for variations.
|
||||
*
|
||||
* @see \Drupal\Core\Cache\Cache
|
||||
* @see \Drupal\Core\Cache\CacheableMetadata
|
||||
* @see \Drupal\Core\Cache\CacheableResponseTrait
|
||||
*/
|
||||
class CacheableJsonResponse extends JsonResponse implements CacheableResponseInterface {
|
||||
|
||||
use CacheableResponseTrait;
|
||||
|
||||
}
|
|
@ -11,50 +11,16 @@ namespace Drupal\Core\Cache;
|
|||
*
|
||||
* @ingroup cache
|
||||
*
|
||||
* @todo Use RefinableCacheableDependencyInterface and the corresponding trait in
|
||||
* https://www.drupal.org/node/2526326.
|
||||
*/
|
||||
class CacheableMetadata implements CacheableDependencyInterface {
|
||||
class CacheableMetadata implements RefinableCacheableDependencyInterface {
|
||||
|
||||
/**
|
||||
* Cache contexts.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $contexts = [];
|
||||
|
||||
/**
|
||||
* Cache tags.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $tags = [];
|
||||
|
||||
/**
|
||||
* Cache max-age.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $maxAge = Cache::PERMANENT;
|
||||
use RefinableCacheableDependencyTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheTags() {
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cache tags.
|
||||
*
|
||||
* @param string[] $cache_tags
|
||||
* The cache tags to be added.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addCacheTags(array $cache_tags) {
|
||||
$this->tags = Cache::mergeTags($this->tags, $cache_tags);
|
||||
return $this;
|
||||
return $this->cacheTags;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,7 +32,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
* @return $this
|
||||
*/
|
||||
public function setCacheTags(array $cache_tags) {
|
||||
$this->tags = $cache_tags;
|
||||
$this->cacheTags = $cache_tags;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -74,20 +40,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
return $this->contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cache contexts.
|
||||
*
|
||||
* @param string[] $cache_contexts
|
||||
* The cache contexts to be added.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addCacheContexts(array $cache_contexts) {
|
||||
$this->contexts = Cache::mergeContexts($this->contexts, $cache_contexts);
|
||||
return $this;
|
||||
return $this->cacheContexts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,7 +52,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
* @return $this
|
||||
*/
|
||||
public function setCacheContexts(array $cache_contexts) {
|
||||
$this->contexts = $cache_contexts;
|
||||
$this->cacheContexts = $cache_contexts;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -107,7 +60,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheMaxAge() {
|
||||
return $this->maxAge;
|
||||
return $this->cacheMaxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,36 +81,7 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
throw new \InvalidArgumentException('$max_age must be an integer');
|
||||
}
|
||||
|
||||
$this->maxAge = $max_age;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a dependency on an object: merges its cacheability metadata.
|
||||
*
|
||||
* @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $other_object
|
||||
* The dependency. If the object implements CacheableDependencyInterface,
|
||||
* then its cacheability metadata will be used. Otherwise, the passed in
|
||||
* object must be assumed to be uncacheable, so max-age 0 is set.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addCacheableDependency($other_object) {
|
||||
if ($other_object instanceof CacheableDependencyInterface) {
|
||||
$this->addCacheTags($other_object->getCacheTags());
|
||||
$this->addCacheContexts($other_object->getCacheContexts());
|
||||
if ($this->maxAge === Cache::PERMANENT) {
|
||||
$this->maxAge = $other_object->getCacheMaxAge();
|
||||
}
|
||||
elseif (($max_age = $other_object->getCacheMaxAge()) && $max_age !== Cache::PERMANENT) {
|
||||
$this->maxAge = Cache::mergeMaxAges($this->maxAge, $max_age);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Not a cacheable dependency, this can not be cached.
|
||||
$this->maxAge = 0;
|
||||
}
|
||||
|
||||
$this->cacheMaxAge = $max_age;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -175,34 +99,34 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
|
||||
// This is called many times per request, so avoid merging unless absolutely
|
||||
// necessary.
|
||||
if (empty($this->contexts)) {
|
||||
$result->contexts = $other->contexts;
|
||||
if (empty($this->cacheContexts)) {
|
||||
$result->cacheContexts = $other->cacheContexts;
|
||||
}
|
||||
elseif (empty($other->contexts)) {
|
||||
$result->contexts = $this->contexts;
|
||||
elseif (empty($other->cacheContexts)) {
|
||||
$result->cacheContexts = $this->cacheContexts;
|
||||
}
|
||||
else {
|
||||
$result->contexts = Cache::mergeContexts($this->contexts, $other->contexts);
|
||||
$result->cacheContexts = Cache::mergeContexts($this->cacheContexts, $other->cacheContexts);
|
||||
}
|
||||
|
||||
if (empty($this->tags)) {
|
||||
$result->tags = $other->tags;
|
||||
if (empty($this->cacheTags)) {
|
||||
$result->cacheTags = $other->cacheTags;
|
||||
}
|
||||
elseif (empty($other->tags)) {
|
||||
$result->tags = $this->tags;
|
||||
elseif (empty($other->cacheTags)) {
|
||||
$result->cacheTags = $this->cacheTags;
|
||||
}
|
||||
else {
|
||||
$result->tags = Cache::mergeTags($this->tags, $other->tags);
|
||||
$result->cacheTags = Cache::mergeTags($this->cacheTags, $other->cacheTags);
|
||||
}
|
||||
|
||||
if ($this->maxAge === Cache::PERMANENT) {
|
||||
$result->maxAge = $other->maxAge;
|
||||
if ($this->cacheMaxAge === Cache::PERMANENT) {
|
||||
$result->cacheMaxAge = $other->cacheMaxAge;
|
||||
}
|
||||
elseif ($other->maxAge === Cache::PERMANENT) {
|
||||
$result->maxAge = $this->maxAge;
|
||||
elseif ($other->cacheMaxAge === Cache::PERMANENT) {
|
||||
$result->cacheMaxAge = $this->cacheMaxAge;
|
||||
}
|
||||
else {
|
||||
$result->maxAge = Cache::mergeMaxAges($this->maxAge, $other->maxAge);
|
||||
$result->cacheMaxAge = Cache::mergeMaxAges($this->cacheMaxAge, $other->cacheMaxAge);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
@ -214,9 +138,9 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
* A render array.
|
||||
*/
|
||||
public function applyTo(array &$build) {
|
||||
$build['#cache']['contexts'] = $this->contexts;
|
||||
$build['#cache']['tags'] = $this->tags;
|
||||
$build['#cache']['max-age'] = $this->maxAge;
|
||||
$build['#cache']['contexts'] = $this->cacheContexts;
|
||||
$build['#cache']['tags'] = $this->cacheTags;
|
||||
$build['#cache']['max-age'] = $this->cacheMaxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -229,9 +153,9 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
*/
|
||||
public static function createFromRenderArray(array $build) {
|
||||
$meta = new static();
|
||||
$meta->contexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : [];
|
||||
$meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : [];
|
||||
$meta->maxAge = (isset($build['#cache']['max-age'])) ? $build['#cache']['max-age'] : Cache::PERMANENT;
|
||||
$meta->cacheContexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : [];
|
||||
$meta->cacheTags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : [];
|
||||
$meta->cacheMaxAge = (isset($build['#cache']['max-age'])) ? $build['#cache']['max-age'] : Cache::PERMANENT;
|
||||
return $meta;
|
||||
}
|
||||
|
||||
|
@ -249,16 +173,16 @@ class CacheableMetadata implements CacheableDependencyInterface {
|
|||
public static function createFromObject($object) {
|
||||
if ($object instanceof CacheableDependencyInterface) {
|
||||
$meta = new static();
|
||||
$meta->contexts = $object->getCacheContexts();
|
||||
$meta->tags = $object->getCacheTags();
|
||||
$meta->maxAge = $object->getCacheMaxAge();
|
||||
$meta->cacheContexts = $object->getCacheContexts();
|
||||
$meta->cacheTags = $object->getCacheTags();
|
||||
$meta->cacheMaxAge = $object->getCacheMaxAge();
|
||||
return $meta;
|
||||
}
|
||||
|
||||
// Objects that don't implement CacheableDependencyInterface must be assumed
|
||||
// to be uncacheable, so set max-age 0.
|
||||
$meta = new static();
|
||||
$meta->maxAge = 0;
|
||||
$meta->cacheMaxAge = 0;
|
||||
return $meta;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,15 @@ namespace Drupal\Core\Cache;
|
|||
* Because this backend will mark all the cache entries in a bin as out-dated
|
||||
* for each write to a bin, it is best suited to bins with fewer changes.
|
||||
*
|
||||
* Note that this is designed specifically for combining a fast inconsistent
|
||||
* cache backend with a slower consistent cache back-end. To still function
|
||||
* correctly, it needs to do a consistency check (see the "last write timestamp"
|
||||
* logic). This contrasts with \Drupal\Core\Cache\BackendChain, which assumes
|
||||
* both chained cache backends are consistent, thus a consistency check being
|
||||
* pointless.
|
||||
*
|
||||
* @see \Drupal\Core\Cache\BackendChain
|
||||
*
|
||||
* @ingroup cache
|
||||
*/
|
||||
class ChainedFastBackend implements CacheBackendInterface, CacheTagsInvalidatorInterface {
|
||||
|
|
46
core/lib/Drupal/Core/Cache/Context/PathCacheContext.php
Normal file
46
core/lib/Drupal/Core/Cache/Context/PathCacheContext.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Cache\Context\PathCacheContext.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Cache\Context;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
|
||||
/**
|
||||
* Defines the PathCacheContext service, for "per URL path" caching.
|
||||
*
|
||||
* Cache context ID: 'url.path'.
|
||||
*
|
||||
* (This allows for caching relative URLs.)
|
||||
*
|
||||
* @see \Symfony\Component\HttpFoundation\Request::getBasePath()
|
||||
* @see \Symfony\Component\HttpFoundation\Request::getPathInfo()
|
||||
*/
|
||||
class PathCacheContext extends RequestStackCacheContextBase implements CacheContextInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getLabel() {
|
||||
return t('Path');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContext() {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
return $request->getBasePath() . $request->getPathInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheableMetadata() {
|
||||
return new CacheableMetadata();
|
||||
}
|
||||
|
||||
}
|
|
@ -147,17 +147,26 @@ class DatabaseBackend implements CacheBackendInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Implements Drupal\Core\Cache\CacheBackendInterface::set().
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) {
|
||||
Cache::validateTags($tags);
|
||||
$tags = array_unique($tags);
|
||||
// Sort the cache tags so that they are stored consistently in the database.
|
||||
sort($tags);
|
||||
$this->setMultiple([
|
||||
$cid => [
|
||||
'data' => $data,
|
||||
'expire' => $expire,
|
||||
'tags' => $tags,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setMultiple(array $items) {
|
||||
$try_again = FALSE;
|
||||
try {
|
||||
// The bin might not yet exist.
|
||||
$this->doSet($cid, $data, $expire, $tags);
|
||||
$this->doSetMultiple($items);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// If there was an exception, try to create the bins.
|
||||
|
@ -169,39 +178,19 @@ class DatabaseBackend implements CacheBackendInterface {
|
|||
}
|
||||
// Now that the bin has been created, try again if necessary.
|
||||
if ($try_again) {
|
||||
$this->doSet($cid, $data, $expire, $tags);
|
||||
$this->doSetMultiple($items);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually set the cache.
|
||||
* Stores multiple items in the persistent cache.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of cache items, keyed by cid.
|
||||
*
|
||||
* @see \Drupal\Core\Cache\CacheBackendInterface::setMultiple()
|
||||
*/
|
||||
protected function doSet($cid, $data, $expire, $tags) {
|
||||
$fields = array(
|
||||
'created' => round(microtime(TRUE), 3),
|
||||
'expire' => $expire,
|
||||
'tags' => implode(' ', $tags),
|
||||
'checksum' => $this->checksumProvider->getCurrentChecksum($tags),
|
||||
);
|
||||
if (!is_string($data)) {
|
||||
$fields['data'] = serialize($data);
|
||||
$fields['serialized'] = 1;
|
||||
}
|
||||
else {
|
||||
$fields['data'] = $data;
|
||||
$fields['serialized'] = 0;
|
||||
}
|
||||
|
||||
$this->connection->merge($this->bin)
|
||||
->key('cid', $this->normalizeCid($cid))
|
||||
->fields($fields)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setMultiple(array $items) {
|
||||
protected function doSetMultiple(array $items) {
|
||||
$values = array();
|
||||
|
||||
foreach ($items as $cid => $item) {
|
||||
|
@ -216,7 +205,7 @@ class DatabaseBackend implements CacheBackendInterface {
|
|||
sort($item['tags']);
|
||||
|
||||
$fields = array(
|
||||
'cid' => $cid,
|
||||
'cid' => $this->normalizeCid($cid),
|
||||
'expire' => $item['expire'],
|
||||
'created' => round(microtime(TRUE), 3),
|
||||
'tags' => implode(' ', $item['tags']),
|
||||
|
@ -234,34 +223,20 @@ class DatabaseBackend implements CacheBackendInterface {
|
|||
$values[] = $fields;
|
||||
}
|
||||
|
||||
// Use a transaction so that the database can write the changes in a single
|
||||
// commit. The transaction is started after calculating the tag checksums
|
||||
// since that can create a table and this causes an exception when using
|
||||
// PostgreSQL.
|
||||
$transaction = $this->connection->startTransaction();
|
||||
|
||||
try {
|
||||
// Delete all items first so we can do one insert. Rather than multiple
|
||||
// merge queries.
|
||||
$this->deleteMultiple(array_keys($items));
|
||||
|
||||
$query = $this->connection
|
||||
->insert($this->bin)
|
||||
->fields(array('cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized'));
|
||||
foreach ($values as $fields) {
|
||||
// Only pass the values since the order of $fields matches the order of
|
||||
// the insert fields. This is a performance optimization to avoid
|
||||
// unnecessary loops within the method.
|
||||
$query->values(array_values($fields));
|
||||
}
|
||||
|
||||
$query->execute();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$transaction->rollback();
|
||||
// @todo Log something here or just re throw?
|
||||
throw $e;
|
||||
// Use an upsert query which is atomic and optimized for multiple-row
|
||||
// merges.
|
||||
$query = $this->connection
|
||||
->upsert($this->bin)
|
||||
->key('cid')
|
||||
->fields(array('cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized'));
|
||||
foreach ($values as $fields) {
|
||||
// Only pass the values since the order of $fields matches the order of
|
||||
// the insert fields. This is a performance optimization to avoid
|
||||
// unnecessary loops within the method.
|
||||
$query->values(array_values($fields));
|
||||
}
|
||||
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -217,4 +217,13 @@ class MemoryBackend implements CacheBackendInterface, CacheTagsInvalidatorInterf
|
|||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset statically cached variables.
|
||||
*
|
||||
* This is only used by tests.
|
||||
*/
|
||||
public function reset() {
|
||||
$this->cache = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ trait RefinableCacheableDependencyTrait {
|
|||
}
|
||||
else {
|
||||
// Not a cacheable dependency, this can not be cached.
|
||||
$this->maxAge = 0;
|
||||
$this->cacheMaxAge = 0;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
@ -53,7 +53,9 @@ trait RefinableCacheableDependencyTrait {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function addCacheContexts(array $cache_contexts) {
|
||||
$this->cacheContexts = Cache::mergeContexts($this->cacheContexts, $cache_contexts);
|
||||
if ($cache_contexts) {
|
||||
$this->cacheContexts = Cache::mergeContexts($this->cacheContexts, $cache_contexts);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -61,7 +63,9 @@ trait RefinableCacheableDependencyTrait {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function addCacheTags(array $cache_tags) {
|
||||
$this->cacheTags = Cache::mergeTags($this->cacheTags, $cache_tags);
|
||||
if ($cache_tags) {
|
||||
$this->cacheTags = Cache::mergeTags($this->cacheTags, $cache_tags);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
@ -129,12 +129,9 @@ class DbDumpCommand extends Command {
|
|||
* An array of table names.
|
||||
*/
|
||||
protected function getTables() {
|
||||
$pattern = $this->connection->tablePrefix() . '%';
|
||||
$tables = array_values($this->connection->schema()->findTables($pattern));
|
||||
foreach ($tables as $key => $table) {
|
||||
// The prefix is removed for the resultant script.
|
||||
$table = $tables[$key] = str_replace($this->connection->tablePrefix(), '', $table);
|
||||
$tables = array_values($this->connection->schema()->findTables('%'));
|
||||
|
||||
foreach ($tables as $key => $table) {
|
||||
// Remove any explicitly excluded tables.
|
||||
foreach ($this->excludeTables as $pattern) {
|
||||
if (preg_match('/^' . $pattern . '$/', $table)) {
|
||||
|
@ -142,6 +139,7 @@ class DbDumpCommand extends Command {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,11 +53,13 @@ class Condition extends Plugin {
|
|||
public $module;
|
||||
|
||||
/**
|
||||
* An array of contextual data.
|
||||
* An array of context definitions describing the context used by the plugin.
|
||||
*
|
||||
* @var array
|
||||
* The array is keyed by context names.
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\ContextDefinition[]
|
||||
*/
|
||||
public $condition = array();
|
||||
public $context = array();
|
||||
|
||||
/**
|
||||
* The category under which the condition should listed in the UI.
|
||||
|
|
|
@ -158,6 +158,7 @@ class ConfigInstaller implements ConfigInstallerInterface {
|
|||
*/
|
||||
public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
|
||||
$profile = $this->drupalGetProfile();
|
||||
$optional_profile_config = [];
|
||||
if (!$storage) {
|
||||
// Search the install profile's optional configuration too.
|
||||
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE);
|
||||
|
@ -168,6 +169,7 @@ class ConfigInstaller implements ConfigInstallerInterface {
|
|||
// Creates a profile storage to search for overrides.
|
||||
$profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
|
||||
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
|
||||
$optional_profile_config = $profile_storage->listAll();
|
||||
}
|
||||
else {
|
||||
// Profile has not been set yet. For example during the first steps of the
|
||||
|
@ -178,7 +180,8 @@ class ConfigInstaller implements ConfigInstallerInterface {
|
|||
$enabled_extensions = $this->getEnabledExtensions();
|
||||
$existing_config = $this->getActiveStorages()->listAll();
|
||||
|
||||
$list = array_filter($storage->listAll(), function($config_name) use ($existing_config) {
|
||||
$list = array_unique(array_merge($storage->listAll(), $optional_profile_config));
|
||||
$list = array_filter($list, function($config_name) use ($existing_config) {
|
||||
// Only list configuration that:
|
||||
// - does not already exist
|
||||
// - is a configuration entity (this also excludes config that has an
|
||||
|
@ -188,7 +191,8 @@ class ConfigInstaller implements ConfigInstallerInterface {
|
|||
|
||||
$all_config = array_merge($existing_config, $list);
|
||||
$config_to_create = $storage->readMultiple($list);
|
||||
// Check to see if the corresponding override storage has any overrides.
|
||||
// Check to see if the corresponding override storage has any overrides or
|
||||
// new configuration that can be installed.
|
||||
if ($profile_storage) {
|
||||
$config_to_create = $profile_storage->readMultiple($list) + $config_to_create;
|
||||
}
|
||||
|
|
|
@ -193,19 +193,23 @@ class FileStorage implements StorageInterface {
|
|||
* Implements Drupal\Core\Config\StorageInterface::listAll().
|
||||
*/
|
||||
public function listAll($prefix = '') {
|
||||
// glob() silently ignores the error of a non-existing search directory,
|
||||
// even with the GLOB_ERR flag.
|
||||
$dir = $this->getCollectionDirectory();
|
||||
if (!file_exists($dir)) {
|
||||
if (!is_dir($dir)) {
|
||||
return array();
|
||||
}
|
||||
$extension = '.' . static::getFileExtension();
|
||||
// \GlobIterator on Windows requires an absolute path.
|
||||
$files = new \GlobIterator(realpath($dir) . '/' . $prefix . '*' . $extension);
|
||||
|
||||
// glob() directly calls into libc glob(), which is not aware of PHP stream
|
||||
// wrappers. Same for \GlobIterator (which additionally requires an absolute
|
||||
// realpath() on Windows).
|
||||
// @see https://github.com/mikey179/vfsStream/issues/2
|
||||
$files = scandir($dir);
|
||||
|
||||
$names = array();
|
||||
foreach ($files as $file) {
|
||||
$names[] = $file->getBasename($extension);
|
||||
if ($file[0] !== '.' && fnmatch($prefix . '*' . $extension, $file)) {
|
||||
$names[] = basename($file, $extension);
|
||||
}
|
||||
}
|
||||
|
||||
return $names;
|
||||
|
@ -299,13 +303,15 @@ class FileStorage implements StorageInterface {
|
|||
$collections[] = $collection . '.' . $sub_collection;
|
||||
}
|
||||
}
|
||||
// Check that the collection is valid by searching if for configuration
|
||||
// Check that the collection is valid by searching it for configuration
|
||||
// objects. A directory without any configuration objects is not a valid
|
||||
// collection.
|
||||
// \GlobIterator on Windows requires an absolute path.
|
||||
$files = new \GlobIterator(realpath($directory . '/' . $collection) . '/*.' . $this->getFileExtension());
|
||||
if (count($files)) {
|
||||
$collections[] = $collection;
|
||||
// @see \Drupal\Core\Config\FileStorage::listAll()
|
||||
foreach (scandir($directory . '/' . $collection) as $file) {
|
||||
if ($file[0] !== '.' && fnmatch('*.' . $this->getFileExtension(), $file)) {
|
||||
$collections[] = $collection;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -195,10 +195,17 @@ class InstallStorage extends FileStorage {
|
|||
// We don't have to use ExtensionDiscovery here because our list of
|
||||
// extensions was already obtained through an ExtensionDiscovery scan.
|
||||
$directory = $this->getComponentFolder($extension_object);
|
||||
if (file_exists($directory)) {
|
||||
$files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension);
|
||||
if (is_dir($directory)) {
|
||||
// glob() directly calls into libc glob(), which is not aware of PHP
|
||||
// stream wrappers. Same for \GlobIterator (which additionally requires
|
||||
// an absolute realpath() on Windows).
|
||||
// @see https://github.com/mikey179/vfsStream/issues/2
|
||||
$files = scandir($directory);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$folders[$file->getBasename($extension)] = $directory;
|
||||
if ($file[0] !== '.' && fnmatch('*' . $extension, $file)) {
|
||||
$folders[basename($file, $extension)] = $directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -215,10 +222,17 @@ class InstallStorage extends FileStorage {
|
|||
$extension = '.' . $this->getFileExtension();
|
||||
$folders = array();
|
||||
$directory = $this->getCoreFolder();
|
||||
if (file_exists($directory)) {
|
||||
$files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension);
|
||||
if (is_dir($directory)) {
|
||||
// glob() directly calls into libc glob(), which is not aware of PHP
|
||||
// stream wrappers. Same for \GlobIterator (which additionally requires an
|
||||
// absolute realpath() on Windows).
|
||||
// @see https://github.com/mikey179/vfsStream/issues/2
|
||||
$files = scandir($directory);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$folders[$file->getBasename($extension)] = $directory;
|
||||
if ($file[0] !== '.' && fnmatch('*' . $extension, $file)) {
|
||||
$folders[basename($file, $extension)] = $directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $folders;
|
||||
|
|
|
@ -42,13 +42,20 @@ trait SchemaCheckTrait {
|
|||
* @param string $config_name
|
||||
* The configuration name.
|
||||
* @param array $config_data
|
||||
* The configuration data.
|
||||
* The configuration data, assumed to be data for a top-level config object.
|
||||
*
|
||||
* @return array|bool
|
||||
* FALSE if no schema found. List of errors if any found. TRUE if fully
|
||||
* valid.
|
||||
*/
|
||||
public function checkConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data) {
|
||||
// We'd like to verify that the top-level type is either config_base,
|
||||
// config_entity, or a derivative. The only thing we can really test though
|
||||
// is that the schema supports having langcode in it. So add 'langcode' to
|
||||
// the data if it doesn't already exist.
|
||||
if (!isset($config_data['langcode'])) {
|
||||
$config_data['langcode'] = 'en';
|
||||
}
|
||||
$this->configName = $config_name;
|
||||
if (!$typed_config->hasConfigSchema($config_name)) {
|
||||
return FALSE;
|
||||
|
|
|
@ -73,7 +73,7 @@ class ConfigSchemaChecker implements EventSubscriberInterface {
|
|||
|
||||
$name = $saved_config->getName();
|
||||
$data = $saved_config->get();
|
||||
$checksum = crc32(serialize($data));
|
||||
$checksum = hash('crc32b', serialize($data));
|
||||
$exceptions = array(
|
||||
// Following are used to test lack of or partial schema. Where partial
|
||||
// schema is provided, that is explicitly tested in specific tests.
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
|
||||
namespace Drupal\Core\Controller;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\Routing\RouteMatch;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Controller\ControllerResolver as BaseControllerResolver;
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
|
||||
/**
|
||||
* ControllerResolver to enhance controllers beyond Symfony's basic handling.
|
||||
|
@ -37,13 +39,24 @@ class ControllerResolver extends BaseControllerResolver implements ControllerRes
|
|||
*/
|
||||
protected $classResolver;
|
||||
|
||||
/**
|
||||
* The PSR-7 converter.
|
||||
*
|
||||
* @var \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface
|
||||
*/
|
||||
protected $httpMessageFactory;
|
||||
|
||||
/**
|
||||
* Constructs a new ControllerResolver.
|
||||
*
|
||||
* @param \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface $http_message_factory
|
||||
* The PSR-7 converter.
|
||||
*
|
||||
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
|
||||
* The class resolver.
|
||||
*/
|
||||
public function __construct(ClassResolverInterface $class_resolver) {
|
||||
public function __construct(HttpMessageFactoryInterface $http_message_factory, ClassResolverInterface $class_resolver) {
|
||||
$this->httpMessageFactory = $http_message_factory;
|
||||
$this->classResolver = $class_resolver;
|
||||
}
|
||||
|
||||
|
@ -94,10 +107,10 @@ class ControllerResolver extends BaseControllerResolver implements ControllerRes
|
|||
* A PHP callable.
|
||||
*
|
||||
* @throws \LogicException
|
||||
* If the controller cannot be parsed
|
||||
* If the controller cannot be parsed.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* If the controller class does not exist
|
||||
* If the controller class does not exist.
|
||||
*/
|
||||
protected function createController($controller) {
|
||||
// Controller in the service:method notation.
|
||||
|
@ -135,7 +148,10 @@ class ControllerResolver extends BaseControllerResolver implements ControllerRes
|
|||
elseif ($param->getClass() && $param->getClass()->isInstance($request)) {
|
||||
$arguments[] = $request;
|
||||
}
|
||||
elseif ($param->getClass() && ($param->getClass()->name == 'Drupal\Core\Routing\RouteMatchInterface' || is_subclass_of($param->getClass()->name, 'Drupal\Core\Routing\RouteMatchInterface'))) {
|
||||
elseif ($param->getClass() && $param->getClass()->name === ServerRequestInterface::class) {
|
||||
$arguments[] = $this->httpMessageFactory->createRequest($request);
|
||||
}
|
||||
elseif ($param->getClass() && ($param->getClass()->name == RouteMatchInterface::class || is_subclass_of($param->getClass()->name, RouteMatchInterface::class))) {
|
||||
$arguments[] = RouteMatch::createFromRequest($request);
|
||||
}
|
||||
elseif ($param->isDefaultValueAvailable()) {
|
||||
|
|
|
@ -19,6 +19,7 @@ use Drupal\Core\DependencyInjection\Compiler\DependencySerializationTraitPass;
|
|||
use Drupal\Core\DependencyInjection\Compiler\StackedKernelPass;
|
||||
use Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass;
|
||||
use Drupal\Core\DependencyInjection\Compiler\RegisterStreamWrappersPass;
|
||||
use Drupal\Core\DependencyInjection\Compiler\TwigExtensionPass;
|
||||
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass;
|
||||
|
@ -78,6 +79,8 @@ class CoreServiceProvider implements ServiceProviderInterface {
|
|||
$container->addCompilerPass(new RegisterStreamWrappersPass());
|
||||
$container->addCompilerPass(new GuzzleMiddlewarePass());
|
||||
|
||||
$container->addCompilerPass(new TwigExtensionPass());
|
||||
|
||||
// Add a compiler pass for registering event subscribers.
|
||||
$container->addCompilerPass(new RegisterKernelListenersPass(), PassConfig::TYPE_AFTER_REMOVING);
|
||||
|
||||
|
|
|
@ -138,6 +138,13 @@ abstract class Connection {
|
|||
*/
|
||||
protected $prefixReplace = array();
|
||||
|
||||
/**
|
||||
* List of un-prefixed table names, keyed by prefixed table names.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $unprefixedTablesMap = [];
|
||||
|
||||
/**
|
||||
* Constructs a Connection object.
|
||||
*
|
||||
|
@ -185,7 +192,9 @@ abstract class Connection {
|
|||
// Destroy all references to this connection by setting them to NULL.
|
||||
// The Statement class attribute only accepts a new value that presents a
|
||||
// proper callable, so we reset it to PDOStatement.
|
||||
$this->connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
|
||||
if (!empty($this->statementClass)) {
|
||||
$this->connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
|
||||
}
|
||||
$this->schema = NULL;
|
||||
}
|
||||
|
||||
|
@ -289,6 +298,13 @@ abstract class Connection {
|
|||
$this->prefixReplace[] = $this->prefixes['default'];
|
||||
$this->prefixSearch[] = '}';
|
||||
$this->prefixReplace[] = '';
|
||||
|
||||
// Set up a map of prefixed => un-prefixed tables.
|
||||
foreach ($this->prefixes as $table_name => $prefix) {
|
||||
if ($table_name !== 'default') {
|
||||
$this->unprefixedTablesMap[$prefix . $table_name] = $table_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -327,6 +343,17 @@ abstract class Connection {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of individually prefixed table names.
|
||||
*
|
||||
* @return array
|
||||
* An array of un-prefixed table names, keyed by their fully qualified table
|
||||
* names (i.e. prefix + table_name).
|
||||
*/
|
||||
public function getUnprefixedTablesMap() {
|
||||
return $this->unprefixedTablesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fully qualified table name.
|
||||
*
|
||||
|
@ -502,7 +529,7 @@ abstract class Connection {
|
|||
* A sanitized version of the query comment string.
|
||||
*/
|
||||
protected function filterComment($comment = '') {
|
||||
return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment);
|
||||
return strtr($comment, ['*' => ' * ']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -786,6 +813,23 @@ abstract class Connection {
|
|||
return new $class($this, $table, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and returns an UPSERT query object.
|
||||
*
|
||||
* @param string $table
|
||||
* The table to use for the upsert query.
|
||||
* @param array $options
|
||||
* (optional) An array of options on the query.
|
||||
*
|
||||
* @return \Drupal\Core\Database\Query\Upsert
|
||||
* A new Upsert query object.
|
||||
*
|
||||
* @see \Drupal\Core\Database\Query\Upsert
|
||||
*/
|
||||
public function upsert($table, array $options = array()) {
|
||||
$class = $this->getDriverClass('Upsert');
|
||||
return new $class($this, $table, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and returns an UPDATE query object.
|
||||
|
@ -1216,6 +1260,13 @@ abstract class Connection {
|
|||
return $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version of the database client.
|
||||
*/
|
||||
public function clientVersion() {
|
||||
return $this->connection->getAttribute(\PDO::ATTR_CLIENT_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this driver supports transactions.
|
||||
*
|
||||
|
|
|
@ -28,6 +28,11 @@ class Connection extends DatabaseConnection {
|
|||
*/
|
||||
const DATABASE_NOT_FOUND = 1049;
|
||||
|
||||
/**
|
||||
* Error code for "Can't initialize character set" error.
|
||||
*/
|
||||
const UNSUPPORTED_CHARSET = 2019;
|
||||
|
||||
/**
|
||||
* Flag to indicate if the cleanup function in __destruct() should run.
|
||||
*
|
||||
|
@ -82,6 +87,13 @@ class Connection extends DatabaseConnection {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public static function open(array &$connection_options = array()) {
|
||||
if (isset($connection_options['_dsn_utf8_fallback']) && $connection_options['_dsn_utf8_fallback'] === TRUE) {
|
||||
// Only used during the installer version check, as a fallback from utf8mb4.
|
||||
$charset = 'utf8';
|
||||
}
|
||||
else {
|
||||
$charset = 'utf8mb4';
|
||||
}
|
||||
// The DSN should use either a socket or a host/port.
|
||||
if (isset($connection_options['unix_socket'])) {
|
||||
$dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
|
||||
|
@ -93,7 +105,7 @@ class Connection extends DatabaseConnection {
|
|||
// Character set is added to dsn to ensure PDO uses the proper character
|
||||
// set when escaping. This has security implications. See
|
||||
// https://www.drupal.org/node/1201452 for further discussion.
|
||||
$dsn .= ';charset=utf8mb4';
|
||||
$dsn .= ';charset=' . $charset;
|
||||
if (!empty($connection_options['database'])) {
|
||||
$dsn .= ';dbname=' . $connection_options['database'];
|
||||
}
|
||||
|
@ -124,10 +136,10 @@ class Connection extends DatabaseConnection {
|
|||
// certain one has been set; otherwise, MySQL defaults to
|
||||
// 'utf8mb4_general_ci' for utf8mb4.
|
||||
if (!empty($connection_options['collation'])) {
|
||||
$pdo->exec('SET NAMES utf8mb4 COLLATE ' . $connection_options['collation']);
|
||||
$pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
|
||||
}
|
||||
else {
|
||||
$pdo->exec('SET NAMES utf8mb4');
|
||||
$pdo->exec('SET NAMES ' . $charset);
|
||||
}
|
||||
|
||||
// Set MySQL init_commands if not already defined. Default Drupal's MySQL
|
||||
|
|
|
@ -55,30 +55,7 @@ class Insert extends QueryInsert {
|
|||
|
||||
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$max_placeholder = 0;
|
||||
$values = array();
|
||||
if (count($this->insertValues)) {
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
$placeholders = array();
|
||||
|
||||
// Default fields aren't really placeholders, but this is the most convenient
|
||||
// way to handle them.
|
||||
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
|
||||
|
||||
$new_placeholder = $max_placeholder + count($insert_values);
|
||||
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
|
||||
$placeholders[] = ':db_insert_placeholder_' . $i;
|
||||
}
|
||||
$max_placeholder = $new_placeholder;
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If there are no values, then this is a default-only query. We still need to handle that.
|
||||
$placeholders = array_fill(0, count($this->defaultFields), 'default');
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
return $query;
|
||||
|
|
|
@ -16,6 +16,17 @@ use Drupal\Core\Database\DatabaseNotFoundException;
|
|||
* Specifies installation tasks for MySQL and equivalent databases.
|
||||
*/
|
||||
class Tasks extends InstallTasks {
|
||||
|
||||
/**
|
||||
* Minimum required MySQLnd version.
|
||||
*/
|
||||
const MYSQLND_MINIMUM_VERSION = '5.0.9';
|
||||
|
||||
/**
|
||||
* Minimum required libmysqlclient version.
|
||||
*/
|
||||
const LIBMYSQLCLIENT_MINIMUM_VERSION = '5.5.3';
|
||||
|
||||
/**
|
||||
* The PDO driver name for MySQL and equivalent databases.
|
||||
*
|
||||
|
@ -27,13 +38,6 @@ class Tasks extends InstallTasks {
|
|||
* Constructs a \Drupal\Core\Database\Driver\mysql\Install\Tasks object.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->tasks[] = array(
|
||||
'arguments' => array(
|
||||
'SET NAMES utf8mb4',
|
||||
'The %name database server supports utf8mb4 character encoding.',
|
||||
'The %name database server must support utf8mb4 character encoding to work with Drupal. Make sure to use a database server that supports utf8mb4 character encoding, such as MySQL/MariaDB/Percona versions 5.5.3 and up.',
|
||||
),
|
||||
);
|
||||
$this->tasks[] = array(
|
||||
'arguments' => array(),
|
||||
'function' => 'ensureInnoDbAvailable',
|
||||
|
@ -62,7 +66,34 @@ class Tasks extends InstallTasks {
|
|||
// This doesn't actually test the connection.
|
||||
db_set_active();
|
||||
// Now actually do a check.
|
||||
Database::getConnection();
|
||||
try {
|
||||
Database::getConnection();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Detect utf8mb4 incompability.
|
||||
if ($e->getCode() == Connection::UNSUPPORTED_CHARSET) {
|
||||
$this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href="@documentation" target="_blank">MySQL documentation</a> for more information.', array('@documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html')));
|
||||
$info = Database::getConnectionInfo();
|
||||
$info_copy = $info;
|
||||
// Set a flag to fall back to utf8. Note: this flag should only be
|
||||
// used here and is for internal use only.
|
||||
$info_copy['default']['_dsn_utf8_fallback'] = TRUE;
|
||||
// In order to change the Database::$databaseInfo array, we need to
|
||||
// remove the active connection, then re-add it with the new info.
|
||||
Database::removeConnection('default');
|
||||
Database::addConnectionInfo('default', 'default', $info_copy['default']);
|
||||
// Connect with the new database info, using the utf8 character set so
|
||||
// that we can run the checkEngineVersion test.
|
||||
Database::getConnection();
|
||||
// Revert to the old settings.
|
||||
Database::removeConnection('default');
|
||||
Database::addConnectionInfo('default', 'default', $info['default']);
|
||||
}
|
||||
else {
|
||||
// Rethrow the exception.
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$this->pass('Drupal can CONNECT to the database ok.');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
|
@ -121,4 +152,27 @@ class Tasks extends InstallTasks {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkEngineVersion() {
|
||||
parent::checkEngineVersion();
|
||||
|
||||
// Ensure that the MySQL driver supports utf8mb4 encoding.
|
||||
$version = Database::getConnection()->clientVersion();
|
||||
if (FALSE !== strpos($version, 'mysqlnd')) {
|
||||
// The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
|
||||
$version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
|
||||
if (version_compare($version, self::MYSQLND_MINIMUM_VERSION, '<')) {
|
||||
$this->fail(t("The MySQLnd driver version %version is less than the minimum required version. Upgrade to MySQLnd version %mysqlnd_minimum_version or up, or alternatively switch mysql drivers to libmysqlclient version %libmysqlclient_minimum_version or up.", array('%version' => Database::getConnection()->version(), '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION)));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
|
||||
if (version_compare($version, self::LIBMYSQLCLIENT_MINIMUM_VERSION, '<')) {
|
||||
$this->fail(t("The libmysqlclient driver version %version is less than the minimum required version. Upgrade to libmysqlclient version %libmysqlclient_minimum_version or up, or alternatively switch mysql drivers to MySQLnd version %mysqlnd_minimum_version or up.", array('%version' => Database::getConnection()->version(), '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Drupal\Core\Database\Driver\mysql;
|
|||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Database\Query\Condition;
|
||||
use Drupal\Core\Database\SchemaException;
|
||||
use Drupal\Core\Database\SchemaObjectExistsException;
|
||||
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
|
||||
use Drupal\Core\Database\Schema as DatabaseSchema;
|
||||
|
@ -60,8 +61,7 @@ class Schema extends DatabaseSchema {
|
|||
$info['table'] = substr($table, ++$pos);
|
||||
}
|
||||
else {
|
||||
$db_info = Database::getConnectionInfo();
|
||||
$info['database'] = $db_info[$this->connection->getTarget()]['database'];
|
||||
$info['database'] = $this->connection->getConnectionOptions()['database'];
|
||||
$info['table'] = $table;
|
||||
}
|
||||
return $info;
|
||||
|
@ -299,14 +299,17 @@ class Schema extends DatabaseSchema {
|
|||
* Shortens indexes to 191 characters if they apply to utf8mb4-encoded
|
||||
* fields, in order to comply with the InnoDB index limitation of 756 bytes.
|
||||
*
|
||||
* @param $spec
|
||||
* @param array $spec
|
||||
* The table specification.
|
||||
*
|
||||
* @return array
|
||||
* List of shortened indexes.
|
||||
*
|
||||
* @throws \Drupal\Core\Database\SchemaException
|
||||
* Thrown if field specification is missing.
|
||||
*/
|
||||
protected function getNormalizedIndexes($spec) {
|
||||
$indexes = $spec['indexes'];
|
||||
protected function getNormalizedIndexes(array $spec) {
|
||||
$indexes = isset($spec['indexes']) ? $spec['indexes'] : [];
|
||||
foreach ($indexes as $index_name => $index_fields) {
|
||||
foreach ($index_fields as $index_key => $index_field) {
|
||||
// Get the name of the field from the index specification.
|
||||
|
@ -323,6 +326,9 @@ class Schema extends DatabaseSchema {
|
|||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index");
|
||||
}
|
||||
}
|
||||
}
|
||||
return $indexes;
|
||||
|
@ -486,7 +492,10 @@ class Schema extends DatabaseSchema {
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
public function addIndex($table, $name, $fields) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addIndex($table, $name, $fields, array $spec) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||
}
|
||||
|
@ -494,7 +503,10 @@ class Schema extends DatabaseSchema {
|
|||
throw new SchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
|
||||
}
|
||||
|
||||
$this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')');
|
||||
$spec['indexes'][$name] = $fields;
|
||||
$indexes = $this->getNormalizedIndexes($spec);
|
||||
|
||||
$this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')');
|
||||
}
|
||||
|
||||
public function dropIndex($table, $name) {
|
||||
|
|
45
core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
Normal file
45
core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Driver\mysql\Upsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Driver\mysql;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* Implements the Upsert query for the MySQL database driver.
|
||||
*/
|
||||
class Upsert extends QueryUpsert {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Create a sanitized comment string to prepend to the query.
|
||||
$comments = $this->connection->makeComment($this->comments);
|
||||
|
||||
// Default fields are always placed first for consistency.
|
||||
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||
|
||||
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
// Updating the unique / primary key is not necessary.
|
||||
unset($insert_fields[$this->key]);
|
||||
|
||||
$update = [];
|
||||
foreach ($insert_fields as $field) {
|
||||
$update[] = "$field = VALUES($field)";
|
||||
}
|
||||
|
||||
$query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
|
@ -383,6 +383,22 @@ class Connection extends DatabaseConnection {
|
|||
$this->rollback($savepoint_name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function upsert($table, array $options = array()) {
|
||||
// Use the (faster) native Upsert implementation for PostgreSQL >= 9.5.
|
||||
if (version_compare($this->version(), '9.5', '>=')) {
|
||||
$class = $this->getDriverClass('NativeUpsert');
|
||||
}
|
||||
else {
|
||||
$class = $this->getDriverClass('Upsert');
|
||||
}
|
||||
|
||||
return new $class($this, $table, $options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -128,30 +128,7 @@ class Insert extends QueryInsert {
|
|||
|
||||
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$max_placeholder = 0;
|
||||
$values = array();
|
||||
if (count($this->insertValues)) {
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
$placeholders = array();
|
||||
|
||||
// Default fields aren't really placeholders, but this is the most convenient
|
||||
// way to handle them.
|
||||
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
|
||||
|
||||
$new_placeholder = $max_placeholder + count($insert_values);
|
||||
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
|
||||
$placeholders[] = ':db_insert_placeholder_' . $i;
|
||||
}
|
||||
$max_placeholder = $new_placeholder;
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If there are no values, then this is a default-only query. We still need to handle that.
|
||||
$placeholders = array_fill(0, count($this->defaultFields), 'default');
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
return $query;
|
||||
|
|
|
@ -180,8 +180,8 @@ class Tasks extends InstallTasks {
|
|||
* Verify that a binary data roundtrip returns the original string.
|
||||
*/
|
||||
protected function checkBinaryOutputSuccess() {
|
||||
$bytea_output = db_query("SELECT 'encoding'::bytea AS output")->fetchField();
|
||||
return ($bytea_output == 'encoding');
|
||||
$bytea_output = db_query("SHOW bytea_output")->fetchField();
|
||||
return ($bytea_output == 'escape');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,17 +192,9 @@ class Tasks extends InstallTasks {
|
|||
// like we do with table names. This is so that we don't double up if more
|
||||
// than one instance of Drupal is running on a single database. We therefore
|
||||
// avoid trying to create them again in that case.
|
||||
|
||||
// At the same time checking for the existence of the function fixes
|
||||
// concurrency issues, when both try to update at the same time.
|
||||
try {
|
||||
// Create functions.
|
||||
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric) RETURNS numeric AS
|
||||
\'SELECT CASE WHEN (($1 > $2) OR ($2 IS NULL)) THEN $1 ELSE $2 END;\'
|
||||
LANGUAGE \'sql\''
|
||||
);
|
||||
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric, numeric) RETURNS numeric AS
|
||||
\'SELECT greatest($1, greatest($2, $3));\'
|
||||
LANGUAGE \'sql\''
|
||||
);
|
||||
// Don't use {} around pg_proc table.
|
||||
if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
|
||||
db_query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
|
||||
|
@ -211,37 +203,17 @@ class Tasks extends InstallTasks {
|
|||
);
|
||||
}
|
||||
|
||||
db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
|
||||
\'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
|
||||
LANGUAGE \'sql\''
|
||||
);
|
||||
|
||||
// Using || to concatenate in Drupal is not recommended because there are
|
||||
// database drivers for Drupal that do not support the syntax, however
|
||||
// they do support CONCAT(item1, item2) which we can replicate in
|
||||
// PostgreSQL. PostgreSQL requires the function to be defined for each
|
||||
// different argument variation the function can handle.
|
||||
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, anynonarray) RETURNS text AS
|
||||
\'SELECT CAST($1 AS text) || CAST($2 AS text);\'
|
||||
LANGUAGE \'sql\'
|
||||
');
|
||||
db_query('CREATE OR REPLACE FUNCTION "concat"(text, anynonarray) RETURNS text AS
|
||||
\'SELECT $1 || CAST($2 AS text);\'
|
||||
LANGUAGE \'sql\'
|
||||
');
|
||||
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, text) RETURNS text AS
|
||||
\'SELECT CAST($1 AS text) || $2;\'
|
||||
LANGUAGE \'sql\'
|
||||
');
|
||||
db_query('CREATE OR REPLACE FUNCTION "concat"(text, text) RETURNS text AS
|
||||
\'SELECT $1 || $2;\'
|
||||
LANGUAGE \'sql\'
|
||||
');
|
||||
if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) {
|
||||
db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
|
||||
\'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
|
||||
LANGUAGE \'sql\''
|
||||
);
|
||||
}
|
||||
|
||||
$this->pass(t('PostgreSQL has initialized itself.'));
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->fail(t('Drupal could not be correctly setup with the existing database. Revise any errors.'));
|
||||
$this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
116
core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php
Normal file
116
core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Driver\pgsql\NativeUpsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Driver\pgsql;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* Implements the native Upsert query for the PostgreSQL database driver.
|
||||
*
|
||||
* @see http://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
|
||||
*/
|
||||
class NativeUpsert extends QueryUpsert {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
if (!$this->preExecute()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$stmt = $this->connection->prepareQuery((string) $this);
|
||||
|
||||
// Fetch the list of blobs and sequences used on that table.
|
||||
$table_information = $this->connection->schema()->queryTableInformation($this->table);
|
||||
|
||||
$max_placeholder = 0;
|
||||
$blobs = [];
|
||||
$blob_count = 0;
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
foreach ($this->insertFields as $idx => $field) {
|
||||
if (isset($table_information->blob_fields[$field])) {
|
||||
$blobs[$blob_count] = fopen('php://memory', 'a');
|
||||
fwrite($blobs[$blob_count], $insert_values[$idx]);
|
||||
rewind($blobs[$blob_count]);
|
||||
|
||||
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
|
||||
|
||||
// Pre-increment is faster in PHP than increment.
|
||||
++$blob_count;
|
||||
}
|
||||
else {
|
||||
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
|
||||
}
|
||||
}
|
||||
// Check if values for a serial field has been passed.
|
||||
if (!empty($table_information->serial_fields)) {
|
||||
foreach ($table_information->serial_fields as $index => $serial_field) {
|
||||
$serial_key = array_search($serial_field, $this->insertFields);
|
||||
if ($serial_key !== FALSE) {
|
||||
$serial_value = $insert_values[$serial_key];
|
||||
|
||||
// Sequences must be greater than or equal to 1.
|
||||
if ($serial_value === NULL || !$serial_value) {
|
||||
$serial_value = 1;
|
||||
}
|
||||
// Set the sequence to the bigger value of either the passed
|
||||
// value or the max value of the column. It can happen that another
|
||||
// thread calls nextval() which could lead to a serial number being
|
||||
// used twice. However, trying to insert a value into a serial
|
||||
// column should only be done in very rare cases and is not thread
|
||||
// safe by definition.
|
||||
$this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", array(':serial_value' => (int)$serial_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$options = $this->queryOptions;
|
||||
if (!empty($table_information->sequences)) {
|
||||
$options['sequence_name'] = $table_information->sequences[0];
|
||||
}
|
||||
|
||||
$this->connection->query($stmt, [], $options);
|
||||
|
||||
// Re-initialize the values array so that we can re-use this query.
|
||||
$this->insertValues = [];
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Create a sanitized comment string to prepend to the query.
|
||||
$comments = $this->connection->makeComment($this->comments);
|
||||
|
||||
// Default fields are always placed first for consistency.
|
||||
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||
$insert_fields = array_map(function($f) { return $this->connection->escapeField($f); }, $insert_fields);
|
||||
|
||||
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
// Updating the unique / primary key is not necessary.
|
||||
unset($insert_fields[$this->key]);
|
||||
|
||||
$update = [];
|
||||
foreach ($insert_fields as $field) {
|
||||
$update[] = "$field = EXCLUDED.$field";
|
||||
}
|
||||
|
||||
$query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
|
@ -93,9 +93,17 @@ class Schema extends DatabaseSchema {
|
|||
public function queryTableInformation($table) {
|
||||
// Generate a key to reference this table's information on.
|
||||
$key = $this->connection->prefixTables('{' . $table . '}');
|
||||
if (strpos($key, '.') === FALSE) {
|
||||
|
||||
// Take into account that temporary tables are stored in a different schema.
|
||||
// \Drupal\Core\Database\Connection::generateTemporaryTableName() sets the
|
||||
// 'db_temporary_' prefix to all temporary tables.
|
||||
if (strpos($key, '.') === FALSE && strpos($table, 'db_temporary_') === FALSE) {
|
||||
$key = 'public.' . $key;
|
||||
}
|
||||
else {
|
||||
$schema = $this->connection->query('SELECT nspname FROM pg_namespace WHERE oid = pg_my_temp_schema()')->fetchField();
|
||||
$key = $schema . '.' . $key;
|
||||
}
|
||||
|
||||
if (!isset($this->tableInformation[$key])) {
|
||||
// Split the key into schema and table for querying.
|
||||
|
@ -580,13 +588,28 @@ class Schema extends DatabaseSchema {
|
|||
/**
|
||||
* Helper function: check if a constraint (PK, FK, UK) exists.
|
||||
*
|
||||
* @param $table
|
||||
* @param string $table
|
||||
* The name of the table.
|
||||
* @param $name
|
||||
* The name of the constraint (typically 'pkey' or '[constraint]_key').
|
||||
* @param string $name
|
||||
* The name of the constraint (typically 'pkey' or '[constraint]__key').
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the constraint exists, FALSE otherwise.
|
||||
*/
|
||||
public function constraintExists($table, $name) {
|
||||
$constraint_name = $this->ensureIdentifiersLength($table, $name);
|
||||
// ::ensureIdentifiersLength() expects three parameters, although not
|
||||
// explicitly stated in its signature, thus we split our constraint name in
|
||||
// a proper name and a suffix.
|
||||
if ($name == 'pkey') {
|
||||
$suffix = $name;
|
||||
$name = '';
|
||||
}
|
||||
else {
|
||||
$pos = strrpos($name, '__');
|
||||
$suffix = substr($name, $pos + 2);
|
||||
$name = substr($name, 0, $pos);
|
||||
}
|
||||
$constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix);
|
||||
// Remove leading and trailing quotes because the index name is in a WHERE
|
||||
// clause and not used as an identifier.
|
||||
$constraint_name = str_replace('"', '', $constraint_name);
|
||||
|
@ -637,7 +660,10 @@ class Schema extends DatabaseSchema {
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
public function addIndex($table, $name, $fields) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addIndex($table, $name, $fields, array $spec) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||
}
|
||||
|
@ -779,7 +805,10 @@ class Schema extends DatabaseSchema {
|
|||
}
|
||||
if (isset($new_keys['indexes'])) {
|
||||
foreach ($new_keys['indexes'] as $name => $fields) {
|
||||
$this->addIndex($table, $name, $fields);
|
||||
// Even though $new_keys is not a full schema it still has 'indexes' and
|
||||
// so is a partial schema. Technically addIndex() doesn't do anything
|
||||
// with it so passing an empty array would work as well.
|
||||
$this->addIndex($table, $name, $fields, $new_keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
88
core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
Normal file
88
core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Driver\pgsql\Upsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Driver\pgsql;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* Implements the Upsert query for the PostgreSQL database driver.
|
||||
*/
|
||||
class Upsert extends QueryUpsert {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
if (!$this->preExecute()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Default options for upsert queries.
|
||||
$this->queryOptions += array(
|
||||
'throw_exception' => TRUE,
|
||||
);
|
||||
|
||||
// Default fields are always placed first for consistency.
|
||||
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||
|
||||
$table = $this->connection->escapeTable($this->table);
|
||||
|
||||
// We have to execute multiple queries, therefore we wrap everything in a
|
||||
// transaction so that it is atomic where possible.
|
||||
$transaction = $this->connection->startTransaction();
|
||||
|
||||
try {
|
||||
// First, lock the table we're upserting into.
|
||||
$this->connection->query('LOCK TABLE {' . $table . '} IN SHARE ROW EXCLUSIVE MODE', [], $this->queryOptions);
|
||||
|
||||
// Second, delete all items first so we can do one insert.
|
||||
$unique_key_position = array_search($this->key, $insert_fields);
|
||||
$delete_ids = [];
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
$delete_ids[] = $insert_values[$unique_key_position];
|
||||
}
|
||||
|
||||
// Delete in chunks when a large array is passed.
|
||||
foreach (array_chunk($delete_ids, 1000) as $delete_ids_chunk) {
|
||||
$this->connection->delete($this->table, $this->queryOptions)
|
||||
->condition($this->key, $delete_ids_chunk, 'IN')
|
||||
->execute();
|
||||
}
|
||||
|
||||
// Third, insert all the values.
|
||||
$insert = $this->connection->insert($this->table, $this->queryOptions)
|
||||
->fields($insert_fields);
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
$insert->values($insert_values);
|
||||
}
|
||||
$insert->execute();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// One of the queries failed, rollback the whole batch.
|
||||
$transaction->rollback();
|
||||
|
||||
// Rethrow the exception for the calling code.
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Re-initialize the values array so that we can re-use this query.
|
||||
$this->insertValues = array();
|
||||
|
||||
// Transaction commits here where $transaction looses scope.
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
}
|
|
@ -154,9 +154,9 @@ class Connection extends DatabaseConnection {
|
|||
|
||||
// We can prune the database file if it doesn't have any tables.
|
||||
if ($count == 0) {
|
||||
// Detach the database.
|
||||
$this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
|
||||
// Destroy the database file.
|
||||
// Detaching the database fails at this point, but no other queries
|
||||
// are executed after the connection is destructed so we can simply
|
||||
// remove the database file.
|
||||
unlink($this->connectionOptions['database'] . '-' . $prefix);
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +168,18 @@ class Connection extends DatabaseConnection {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the attached databases.
|
||||
*
|
||||
* @return array
|
||||
* An array of attached database names.
|
||||
*
|
||||
* @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct()
|
||||
*/
|
||||
public function getAttachedDatabases() {
|
||||
return $this->attachedDatabases;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite compatibility implementation for the IF() SQL function.
|
||||
*/
|
||||
|
|
|
@ -582,7 +582,10 @@ class Schema extends DatabaseSchema {
|
|||
return $key_definition;
|
||||
}
|
||||
|
||||
public function addIndex($table, $name, $fields) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addIndex($table, $name, $fields, array $spec) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||
}
|
||||
|
@ -693,16 +696,31 @@ class Schema extends DatabaseSchema {
|
|||
$this->alterTable($table, $old_schema, $new_schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function findTables($table_expression) {
|
||||
// Don't add the prefix, $table_expression already includes the prefix.
|
||||
$info = $this->getPrefixInfo($table_expression, FALSE);
|
||||
$tables = [];
|
||||
|
||||
// Can't use query placeholders for the schema because the query would have
|
||||
// to be :prefixsqlite_master, which does not work.
|
||||
$result = db_query("SELECT name FROM " . $info['schema'] . ".sqlite_master WHERE type = :type AND name LIKE :table_name", array(
|
||||
':type' => 'table',
|
||||
':table_name' => $info['table'],
|
||||
));
|
||||
return $result->fetchAllKeyed(0, 0);
|
||||
// The SQLite implementation doesn't need to use the same filtering strategy
|
||||
// as the parent one because individually prefixed tables live in their own
|
||||
// schema (database), which means that neither the main database nor any
|
||||
// attached one will contain a prefixed table name, so we just need to loop
|
||||
// over all known schemas and filter by the user-supplied table expression.
|
||||
$attached_dbs = $this->connection->getAttachedDatabases();
|
||||
foreach ($attached_dbs as $schema) {
|
||||
// Can't use query placeholders for the schema because the query would
|
||||
// have to be :prefixsqlite_master, which does not work. We also need to
|
||||
// ignore the internal SQLite tables.
|
||||
$result = db_query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", array(
|
||||
':type' => 'table',
|
||||
':table_name' => $table_expression,
|
||||
':pattern' => 'sqlite_%',
|
||||
));
|
||||
$tables += $result->fetchAllKeyed(0, 0);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
35
core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
Normal file
35
core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Driver\sqlite\Upsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Driver\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* Implements the Upsert query for the SQLite database driver.
|
||||
*/
|
||||
class Upsert extends QueryUpsert {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Create a sanitized comment string to prepend to the query.
|
||||
$comments = $this->connection->makeComment($this->comments);
|
||||
|
||||
// Default fields are always placed first for consistency.
|
||||
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||
|
||||
$query = $comments . 'INSERT OR REPLACE INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
namespace Drupal\Core\Database\Install;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Database\Database;
|
||||
|
||||
/**
|
||||
|
@ -80,7 +79,10 @@ abstract class Tasks {
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $results = array();
|
||||
protected $results = array(
|
||||
'fail' => array(),
|
||||
'pass' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Ensure the PDO driver is supported by the version of PHP in use.
|
||||
|
@ -93,14 +95,14 @@ abstract class Tasks {
|
|||
* Assert test as failed.
|
||||
*/
|
||||
protected function fail($message) {
|
||||
$this->results[$message] = FALSE;
|
||||
$this->results['fail'][] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert test as a pass.
|
||||
*/
|
||||
protected function pass($message) {
|
||||
$this->results[$message] = TRUE;
|
||||
$this->results['pass'][] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,6 +130,9 @@ abstract class Tasks {
|
|||
|
||||
/**
|
||||
* Run database tasks and tests to see if Drupal can run on the database.
|
||||
*
|
||||
* @return array
|
||||
* A list of error messages.
|
||||
*/
|
||||
public function runTasks() {
|
||||
// We need to establish a connection before we can run tests.
|
||||
|
@ -143,21 +148,11 @@ abstract class Tasks {
|
|||
}
|
||||
}
|
||||
else {
|
||||
throw new TaskException(t("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
|
||||
$this->fail(t("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for failed results and compile message
|
||||
$message = '';
|
||||
foreach ($this->results as $result => $success) {
|
||||
if (!$success) {
|
||||
$message = SafeMarkup::isSafe($result) ? $result : SafeMarkup::checkPlain($result);
|
||||
}
|
||||
}
|
||||
if (!empty($message)) {
|
||||
$message = SafeMarkup::set('Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/getting-started/install">installation handbook</a>, or contact your hosting provider.' . $message);
|
||||
throw new TaskException($message);
|
||||
}
|
||||
return $this->results['fail'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,8 +191,9 @@ abstract class Tasks {
|
|||
* Check the engine version.
|
||||
*/
|
||||
protected function checkEngineVersion() {
|
||||
// Ensure that the database server has the right version.
|
||||
if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) {
|
||||
$this->fail(t("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
|
||||
$this->fail(t("The database server version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,43 +16,7 @@ use Drupal\Core\Database\Database;
|
|||
*/
|
||||
class Insert extends Query {
|
||||
|
||||
/**
|
||||
* The table on which to insert.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* An array of fields on which to insert.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertFields = array();
|
||||
|
||||
/**
|
||||
* An array of fields that should be set to their database-defined defaults.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFields = array();
|
||||
|
||||
/**
|
||||
* A nested array of values to insert.
|
||||
*
|
||||
* $insertValues is an array of arrays. Each sub-array is either an
|
||||
* associative array whose keys are field names and whose values are field
|
||||
* values to insert, or a non-associative array of values in the same order
|
||||
* as $insertFields.
|
||||
*
|
||||
* Whether multiple insert sets will be run in a single query or multiple
|
||||
* queries is left to individual drivers to implement in whatever manner is
|
||||
* most appropriate. The order of values in each sub-array must match the
|
||||
* order of fields in $insertFields.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertValues = array();
|
||||
use InsertTrait;
|
||||
|
||||
/**
|
||||
* A SelectQuery object to fetch the rows that should be inserted.
|
||||
|
@ -79,96 +43,6 @@ class Insert extends Query {
|
|||
$this->table = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of field->value pairs to be inserted.
|
||||
*
|
||||
* This method may only be called once. Calling it a second time will be
|
||||
* ignored. To queue up multiple sets of values to be inserted at once,
|
||||
* use the values() method.
|
||||
*
|
||||
* @param $fields
|
||||
* An array of fields on which to insert. This array may be indexed or
|
||||
* associative. If indexed, the array is taken to be the list of fields.
|
||||
* If associative, the keys of the array are taken to be the fields and
|
||||
* the values are taken to be corresponding values to insert. If a
|
||||
* $values argument is provided, $fields must be indexed.
|
||||
* @param $values
|
||||
* An array of fields to insert into the database. The values must be
|
||||
* specified in the same order as the $fields array.
|
||||
*
|
||||
* @return \Drupal\Core\Database\Query\Insert
|
||||
* The called object.
|
||||
*/
|
||||
public function fields(array $fields, array $values = array()) {
|
||||
if (empty($this->insertFields)) {
|
||||
if (empty($values)) {
|
||||
if (!is_numeric(key($fields))) {
|
||||
$values = array_values($fields);
|
||||
$fields = array_keys($fields);
|
||||
}
|
||||
}
|
||||
$this->insertFields = $fields;
|
||||
if (!empty($values)) {
|
||||
$this->insertValues[] = $values;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another set of values to the query to be inserted.
|
||||
*
|
||||
* If $values is a numeric-keyed array, it will be assumed to be in the same
|
||||
* order as the original fields() call. If it is associative, it may be
|
||||
* in any order as long as the keys of the array match the names of the
|
||||
* fields.
|
||||
*
|
||||
* @param $values
|
||||
* An array of values to add to the query.
|
||||
*
|
||||
* @return \Drupal\Core\Database\Query\Insert
|
||||
* The called object.
|
||||
*/
|
||||
public function values(array $values) {
|
||||
if (is_numeric(key($values))) {
|
||||
$this->insertValues[] = $values;
|
||||
}
|
||||
else {
|
||||
// Reorder the submitted values to match the fields array.
|
||||
foreach ($this->insertFields as $key) {
|
||||
$insert_values[$key] = $values[$key];
|
||||
}
|
||||
// For consistency, the values array is always numerically indexed.
|
||||
$this->insertValues[] = array_values($insert_values);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies fields for which the database defaults should be used.
|
||||
*
|
||||
* If you want to force a given field to use the database-defined default,
|
||||
* not NULL or undefined, use this method to instruct the database to use
|
||||
* default values explicitly. In most cases this will not be necessary
|
||||
* unless you are inserting a row that is all default values, as you cannot
|
||||
* specify no values in an INSERT query.
|
||||
*
|
||||
* Specifying a field both in fields() and in useDefaults() is an error
|
||||
* and will not execute.
|
||||
*
|
||||
* @param $fields
|
||||
* An array of values for which to use the default values
|
||||
* specified in the table definition.
|
||||
*
|
||||
* @return \Drupal\Core\Database\Query\Insert
|
||||
* The called object.
|
||||
*/
|
||||
public function useDefaults(array $fields) {
|
||||
$this->defaultFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fromQuery on this InsertQuery object.
|
||||
*
|
||||
|
@ -265,13 +139,13 @@ class Insert extends Query {
|
|||
/**
|
||||
* Preprocesses and validates the query.
|
||||
*
|
||||
* @return
|
||||
* @return bool
|
||||
* TRUE if the validation was successful, FALSE if not.
|
||||
*
|
||||
* @throws \Drupal\Core\Database\Query\FieldsOverlapException
|
||||
* @throws \Drupal\Core\Database\Query\NoFieldsException
|
||||
*/
|
||||
public function preExecute() {
|
||||
protected function preExecute() {
|
||||
// Confirm that the user did not try to specify an identical
|
||||
// field and default field.
|
||||
if (array_intersect($this->insertFields, $this->defaultFields)) {
|
||||
|
|
184
core/lib/Drupal/Core/Database/Query/InsertTrait.php
Normal file
184
core/lib/Drupal/Core/Database/Query/InsertTrait.php
Normal file
|
@ -0,0 +1,184 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Query\InsertTrait.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Query;
|
||||
|
||||
/**
|
||||
* Provides common functionality for INSERT and UPSERT queries.
|
||||
*
|
||||
* @ingroup database
|
||||
*/
|
||||
trait InsertTrait {
|
||||
|
||||
/**
|
||||
* The table on which to insert.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* An array of fields on which to insert.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertFields = array();
|
||||
|
||||
/**
|
||||
* An array of fields that should be set to their database-defined defaults.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFields = array();
|
||||
|
||||
/**
|
||||
* A nested array of values to insert.
|
||||
*
|
||||
* $insertValues is an array of arrays. Each sub-array is either an
|
||||
* associative array whose keys are field names and whose values are field
|
||||
* values to insert, or a non-associative array of values in the same order
|
||||
* as $insertFields.
|
||||
*
|
||||
* Whether multiple insert sets will be run in a single query or multiple
|
||||
* queries is left to individual drivers to implement in whatever manner is
|
||||
* most appropriate. The order of values in each sub-array must match the
|
||||
* order of fields in $insertFields.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertValues = array();
|
||||
|
||||
/**
|
||||
* Adds a set of field->value pairs to be inserted.
|
||||
*
|
||||
* This method may only be called once. Calling it a second time will be
|
||||
* ignored. To queue up multiple sets of values to be inserted at once,
|
||||
* use the values() method.
|
||||
*
|
||||
* @param array $fields
|
||||
* An array of fields on which to insert. This array may be indexed or
|
||||
* associative. If indexed, the array is taken to be the list of fields.
|
||||
* If associative, the keys of the array are taken to be the fields and
|
||||
* the values are taken to be corresponding values to insert. If a
|
||||
* $values argument is provided, $fields must be indexed.
|
||||
* @param array $values
|
||||
* (optional) An array of fields to insert into the database. The values
|
||||
* must be specified in the same order as the $fields array.
|
||||
*
|
||||
* @return $this
|
||||
* The called object.
|
||||
*/
|
||||
public function fields(array $fields, array $values = array()) {
|
||||
if (empty($this->insertFields)) {
|
||||
if (empty($values)) {
|
||||
if (!is_numeric(key($fields))) {
|
||||
$values = array_values($fields);
|
||||
$fields = array_keys($fields);
|
||||
}
|
||||
}
|
||||
$this->insertFields = $fields;
|
||||
if (!empty($values)) {
|
||||
$this->insertValues[] = $values;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another set of values to the query to be inserted.
|
||||
*
|
||||
* If $values is a numeric-keyed array, it will be assumed to be in the same
|
||||
* order as the original fields() call. If it is associative, it may be
|
||||
* in any order as long as the keys of the array match the names of the
|
||||
* fields.
|
||||
*
|
||||
* @param array $values
|
||||
* An array of values to add to the query.
|
||||
*
|
||||
* @return $this
|
||||
* The called object.
|
||||
*/
|
||||
public function values(array $values) {
|
||||
if (is_numeric(key($values))) {
|
||||
$this->insertValues[] = $values;
|
||||
}
|
||||
elseif ($this->insertFields) {
|
||||
// Reorder the submitted values to match the fields array.
|
||||
foreach ($this->insertFields as $key) {
|
||||
$insert_values[$key] = $values[$key];
|
||||
}
|
||||
// For consistency, the values array is always numerically indexed.
|
||||
$this->insertValues[] = array_values($insert_values);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies fields for which the database defaults should be used.
|
||||
*
|
||||
* If you want to force a given field to use the database-defined default,
|
||||
* not NULL or undefined, use this method to instruct the database to use
|
||||
* default values explicitly. In most cases this will not be necessary
|
||||
* unless you are inserting a row that is all default values, as you cannot
|
||||
* specify no values in an INSERT query.
|
||||
*
|
||||
* Specifying a field both in fields() and in useDefaults() is an error
|
||||
* and will not execute.
|
||||
*
|
||||
* @param array $fields
|
||||
* An array of values for which to use the default values
|
||||
* specified in the table definition.
|
||||
*
|
||||
* @return $this
|
||||
* The called object.
|
||||
*/
|
||||
public function useDefaults(array $fields) {
|
||||
$this->defaultFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query placeholders for values that will be inserted.
|
||||
*
|
||||
* @param array $nested_insert_values
|
||||
* A nested array of values to insert.
|
||||
* @param array $default_fields
|
||||
* An array of fields that should be set to their database-defined defaults.
|
||||
*
|
||||
* @return array
|
||||
* An array of insert placeholders.
|
||||
*/
|
||||
protected function getInsertPlaceholderFragment(array $nested_insert_values, array $default_fields) {
|
||||
$max_placeholder = 0;
|
||||
$values = array();
|
||||
if ($nested_insert_values) {
|
||||
foreach ($nested_insert_values as $insert_values) {
|
||||
$placeholders = array();
|
||||
|
||||
// Default fields aren't really placeholders, but this is the most convenient
|
||||
// way to handle them.
|
||||
$placeholders = array_pad($placeholders, count($default_fields), 'default');
|
||||
|
||||
$new_placeholder = $max_placeholder + count($insert_values);
|
||||
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
|
||||
$placeholders[] = ':db_insert_placeholder_' . $i;
|
||||
}
|
||||
$max_placeholder = $new_placeholder;
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If there are no values, then this is a default-only query. We still need to handle that.
|
||||
$placeholders = array_fill(0, count($default_fields), 'default');
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Query\NoUniqueFieldException.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Query;
|
||||
|
||||
use Drupal\Core\Database\DatabaseException;
|
||||
|
||||
/**
|
||||
* Exception thrown if an upsert query doesn't specify a unique field.
|
||||
*/
|
||||
class NoUniqueFieldException extends \InvalidArgumentException implements DatabaseException {}
|
|
@ -147,6 +147,9 @@ interface SelectInterface extends ConditionInterface, AlterableInterface, Extend
|
|||
* For some database drivers, it may also wrap the field name in
|
||||
* database-specific escape characters.
|
||||
*
|
||||
* @param string $string
|
||||
* An unsanitized field name.
|
||||
*
|
||||
* @return
|
||||
* The sanitized field name string.
|
||||
*/
|
||||
|
|
119
core/lib/Drupal/Core/Database/Query/Upsert.php
Normal file
119
core/lib/Drupal/Core/Database/Query/Upsert.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Query\Upsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Query;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Database\Database;
|
||||
|
||||
/**
|
||||
* General class for an abstracted "Upsert" (UPDATE or INSERT) query operation.
|
||||
*
|
||||
* This class can only be used with a table with a single unique index.
|
||||
* Often, this will be the primary key. On such a table this class works like
|
||||
* Insert except the rows will be set to the desired values even if the key
|
||||
* existed before.
|
||||
*/
|
||||
abstract class Upsert extends Query {
|
||||
|
||||
use InsertTrait;
|
||||
|
||||
/**
|
||||
* The unique or primary key of the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* Constructs an Upsert object.
|
||||
*
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* A Connection object.
|
||||
* @param string $table
|
||||
* Name of the table to associate with this query.
|
||||
* @param array $options
|
||||
* (optional) An array of database options.
|
||||
*/
|
||||
public function __construct(Connection $connection, $table, array $options = []) {
|
||||
$options['return'] = Database::RETURN_AFFECTED;
|
||||
parent::__construct($connection, $options);
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the unique / primary key field to be used as condition for this query.
|
||||
*
|
||||
* @param string $field
|
||||
* The name of the field to set.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function key($field) {
|
||||
$this->key = $field;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses and validates the query.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the validation was successful, FALSE otherwise.
|
||||
*
|
||||
* @throws \Drupal\Core\Database\Query\NoUniqueFieldException
|
||||
* @throws \Drupal\Core\Database\Query\FieldsOverlapException
|
||||
* @throws \Drupal\Core\Database\Query\NoFieldsException
|
||||
*/
|
||||
protected function preExecute() {
|
||||
// Confirm that the user set the unique/primary key of the table.
|
||||
if (!$this->key) {
|
||||
throw new NoUniqueFieldException('There is no unique field specified.');
|
||||
}
|
||||
|
||||
// Confirm that the user did not try to specify an identical
|
||||
// field and default field.
|
||||
if (array_intersect($this->insertFields, $this->defaultFields)) {
|
||||
throw new FieldsOverlapException('You may not specify the same field to have a value and a schema-default value.');
|
||||
}
|
||||
|
||||
// Don't execute query without fields.
|
||||
if (count($this->insertFields) + count($this->defaultFields) == 0) {
|
||||
throw new NoFieldsException('There are no fields available to insert with.');
|
||||
}
|
||||
|
||||
// If no values have been added, silently ignore this query. This can happen
|
||||
// if values are added conditionally, so we don't want to throw an
|
||||
// exception.
|
||||
return isset($this->insertValues[0]) || $this->insertFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
if (!$this->preExecute()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$max_placeholder = 0;
|
||||
$values = array();
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
foreach ($insert_values as $value) {
|
||||
$values[':db_insert_placeholder_' . $max_placeholder++] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
|
||||
|
||||
// Re-initialize the values array so that we can re-use this query.
|
||||
$this->insertValues = array();
|
||||
|
||||
return $last_insert_id;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,6 +16,11 @@ use Drupal\Core\Database\Query\PlaceholderInterface;
|
|||
*/
|
||||
abstract class Schema implements PlaceholderInterface {
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
|
@ -173,25 +178,62 @@ abstract class Schema implements PlaceholderInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find all tables that are like the specified base table name.
|
||||
* Finds all tables that are like the specified base table name.
|
||||
*
|
||||
* @param $table_expression
|
||||
* An SQL expression, for example "simpletest%" (without the quotes).
|
||||
* BEWARE: this is not prefixed, the caller should take care of that.
|
||||
* @param string $table_expression
|
||||
* An SQL expression, for example "cache_%" (without the quotes).
|
||||
*
|
||||
* @return
|
||||
* Array, both the keys and the values are the matching tables.
|
||||
* @return array
|
||||
* Both the keys and the values are the matching tables.
|
||||
*/
|
||||
public function findTables($table_expression) {
|
||||
$condition = $this->buildTableNameCondition($table_expression, 'LIKE', FALSE);
|
||||
|
||||
// Load all the tables up front in order to take into account per-table
|
||||
// prefixes. The actual matching is done at the bottom of the method.
|
||||
$condition = $this->buildTableNameCondition('%', 'LIKE');
|
||||
$condition->compile($this->connection, $this);
|
||||
|
||||
$individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
|
||||
$default_prefix = $this->connection->tablePrefix();
|
||||
$default_prefix_length = strlen($default_prefix);
|
||||
$tables = [];
|
||||
// Normally, we would heartily discourage the use of string
|
||||
// concatenation for conditionals like this however, we
|
||||
// couldn't use db_select() here because it would prefix
|
||||
// information_schema.tables and the query would fail.
|
||||
// Don't use {} around information_schema.tables table.
|
||||
return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
|
||||
$results = $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
|
||||
foreach ($results as $table) {
|
||||
// Take into account tables that have an individual prefix.
|
||||
if (isset($individually_prefixed_tables[$table->table_name])) {
|
||||
$prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->table_name]));
|
||||
}
|
||||
elseif ($default_prefix && substr($table->table_name, 0, $default_prefix_length) !== $default_prefix) {
|
||||
// This table name does not start the default prefix, which means that
|
||||
// it is not managed by Drupal so it should be excluded from the result.
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
$prefix_length = $default_prefix_length;
|
||||
}
|
||||
|
||||
// Remove the prefix from the returned tables.
|
||||
$unprefixed_table_name = substr($table->table_name, $prefix_length);
|
||||
|
||||
// The pattern can match a table which is the same as the prefix. That
|
||||
// will become an empty string when we remove the prefix, which will
|
||||
// probably surprise the caller, besides not being a prefixed table. So
|
||||
// remove it.
|
||||
if (!empty($unprefixed_table_name)) {
|
||||
$tables[$unprefixed_table_name] = $unprefixed_table_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the table expression from its SQL LIKE syntax to a regular
|
||||
// expression and escape the delimiter that will be used for matching.
|
||||
$table_expression = str_replace(array('%', '_'), array('.*?', '.'), preg_quote($table_expression, '/'));
|
||||
$tables = preg_grep('/^' . $table_expression . '$/i', $tables);
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -413,13 +455,51 @@ abstract class Schema implements PlaceholderInterface {
|
|||
* @code
|
||||
* $fields = ['foo', ['bar', 4]];
|
||||
* @endcode
|
||||
* @param array $spec
|
||||
* The table specification for the table to be altered. This is used in
|
||||
* order to be able to ensure that the index length is not too long.
|
||||
* This schema definition can usually be obtained through hook_schema(), or
|
||||
* in case the table was created by the Entity API, through the schema
|
||||
* handler listed in the entity class definition. For reference, see
|
||||
* SqlContentEntityStorageSchema::getDedicatedTableSchema() and
|
||||
* SqlContentEntityStorageSchema::getSharedTableFieldSchema().
|
||||
*
|
||||
* In order to prevent human error, it is recommended to pass in the
|
||||
* complete table specification. However, in the edge case of the complete
|
||||
* table specification not being available, we can pass in a partial table
|
||||
* definition containing only the fields that apply to the index:
|
||||
* @code
|
||||
* $spec = [
|
||||
* // Example partial specification for a table:
|
||||
* 'fields' => [
|
||||
* 'example_field' => [
|
||||
* 'description' => 'An example field',
|
||||
* 'type' => 'varchar',
|
||||
* 'length' => 32,
|
||||
* 'not null' => TRUE,
|
||||
* 'default' => '',
|
||||
* ],
|
||||
* ],
|
||||
* 'indexes' => [
|
||||
* 'table_example_field' => ['example_field'],
|
||||
* ],
|
||||
* ];
|
||||
* @endcode
|
||||
* Note that the above is a partial table definition and that we would
|
||||
* usually pass a complete table definition as obtained through
|
||||
* hook_schema() instead.
|
||||
*
|
||||
* @see schemaapi
|
||||
* @see hook_schema()
|
||||
*
|
||||
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
|
||||
* If the specified table doesn't exist.
|
||||
* @throws \Drupal\Core\Database\SchemaObjectExistsException
|
||||
* If the specified table already has an index by that name.
|
||||
*
|
||||
* @todo remove the $spec argument whenever schema introspection is added.
|
||||
*/
|
||||
abstract public function addIndex($table, $name, $fields);
|
||||
abstract public function addIndex($table, $name, $fields, array $spec);
|
||||
|
||||
/**
|
||||
* Drop an index.
|
||||
|
|
|
@ -55,19 +55,21 @@ class Datelist extends DateElementBase {
|
|||
$date = NULL;
|
||||
if ($input !== FALSE) {
|
||||
$return = $input;
|
||||
if (isset($input['ampm'])) {
|
||||
if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
|
||||
$input['hour'] += 12;
|
||||
if (empty(static::checkEmptyInputs($input, $parts))) {
|
||||
if (isset($input['ampm'])) {
|
||||
if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
|
||||
$input['hour'] += 12;
|
||||
}
|
||||
elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
|
||||
$input['hour'] -= 12;
|
||||
}
|
||||
unset($input['ampm']);
|
||||
}
|
||||
elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
|
||||
$input['hour'] -= 12;
|
||||
$timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
|
||||
$date = DrupalDateTime::createFromArray($input, $timezone);
|
||||
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
|
||||
static::incrementRound($date, $increment);
|
||||
}
|
||||
unset($input['ampm']);
|
||||
}
|
||||
$timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
|
||||
$date = DrupalDateTime::createFromArray($input, $timezone);
|
||||
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
|
||||
static::incrementRound($date, $increment);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -250,7 +252,7 @@ class Datelist extends DateElementBase {
|
|||
$title = '';
|
||||
}
|
||||
|
||||
$default = !empty($element['#value'][$part]) ? $element['#value'][$part] : '';
|
||||
$default = isset($element['#value'][$part]) && trim($element['#value'][$part]) != '' ? $element['#value'][$part] : '';
|
||||
$value = $date instanceOf DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
|
||||
if (!empty($value) && $part != 'ampm') {
|
||||
$value = intval($value);
|
||||
|
@ -265,7 +267,7 @@ class Datelist extends DateElementBase {
|
|||
'#attributes' => $element['#attributes'],
|
||||
'#options' => $options,
|
||||
'#required' => $element['#required'],
|
||||
'#error_no_message' => TRUE,
|
||||
'#error_no_message' => FALSE,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -300,6 +302,7 @@ class Datelist extends DateElementBase {
|
|||
$input_exists = FALSE;
|
||||
$input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
|
||||
if ($input_exists) {
|
||||
$all_empty = static::checkEmptyInputs($input, $element['#date_part_order']);
|
||||
|
||||
// If there's empty input and the field is not required, set it to empty.
|
||||
if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
|
||||
|
@ -309,6 +312,11 @@ class Datelist extends DateElementBase {
|
|||
elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
|
||||
$form_state->setError($element, t('The %field date is required.'));
|
||||
}
|
||||
elseif (!empty($all_empty)) {
|
||||
foreach ($all_empty as $value){
|
||||
$form_state->setError($element[$value], t('A value must be selected for %part.', array('%part' => $value)));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If the input is valid, set it.
|
||||
$date = $input['object'];
|
||||
|
@ -317,12 +325,34 @@ class Datelist extends DateElementBase {
|
|||
}
|
||||
// If the input is invalid, set an error.
|
||||
else {
|
||||
$form_state->setError($element, t('The %field date is invalid.'));
|
||||
$form_state->setError($element, t('The %field date is invalid.', array('%field' => !empty($element['#title']) ? $element['#title'] : '')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the input array for empty values.
|
||||
*
|
||||
* Input array keys are checked against values in the parts array. Elements
|
||||
* not in the parts array are ignored. Returns an array representing elements
|
||||
* from the input array that have no value. If no empty values are found,
|
||||
* returned array is empty.
|
||||
*
|
||||
* @param array $input
|
||||
* Array of individual inputs to check for value.
|
||||
* @param array $parts
|
||||
* Array to check input against, ignoring elements not in this array.
|
||||
*
|
||||
* @return array
|
||||
* Array of keys from the input array that have no value, may be empty.
|
||||
*/
|
||||
protected static function checkEmptyInputs($input, $parts) {
|
||||
// Filters out empty array values, any valid value would have a string length.
|
||||
$filtered_input = array_filter($input, 'strlen');
|
||||
return array_diff($parts, array_keys($filtered_input));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds minutes and seconds to nearest requested value.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\DependencyInjection\Compiler\TwigExtensionPass.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* Adds the twig_extension_hash parameter to the container.
|
||||
*
|
||||
* twig_extension_hash is a hash of all extension mtimes for Twig template
|
||||
* invalidation.
|
||||
*/
|
||||
class TwigExtensionPass implements CompilerPassInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process(ContainerBuilder $container) {
|
||||
$twig_extension_hash = '';
|
||||
foreach (array_keys($container->findTaggedServiceIds('twig.extension')) as $service_id) {
|
||||
$class_name = $container->getDefinition($service_id)->getClass();
|
||||
$reflection = new \ReflectionClass($class_name);
|
||||
// We use the class names as hash in order to invalidate on new extensions
|
||||
// and mtime for every time we change an existing file.
|
||||
$twig_extension_hash .= $class_name . filemtime($reflection->getFileName());
|
||||
}
|
||||
|
||||
$container->setParameter('twig_extension_hash', hash('crc32b', $twig_extension_hash));
|
||||
}
|
||||
|
||||
}
|
|
@ -7,17 +7,18 @@
|
|||
|
||||
namespace Drupal\Core\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Container as SymfonyContainer;
|
||||
use Drupal\Component\DependencyInjection\Container as DrupalContainer;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Extends the symfony container to set the service ID on the created object.
|
||||
* Extends the Drupal container to set the service ID on the created object.
|
||||
*/
|
||||
class Container extends SymfonyContainer {
|
||||
class Container extends DrupalContainer {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function set($id, $service, $scope = SymfonyContainer::SCOPE_CONTAINER) {
|
||||
public function set($id, $service, $scope = ContainerInterface::SCOPE_CONTAINER) {
|
||||
parent::set($id, $service, $scope);
|
||||
|
||||
// Ensure that the _serviceId property is set on synthetic services as well.
|
||||
|
@ -30,7 +31,7 @@ class Container extends SymfonyContainer {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep() {
|
||||
trigger_error('The container was serialized.', E_USER_ERROR);
|
||||
assert(FALSE, 'The container was serialized.');
|
||||
return array_keys(get_object_vars($this));
|
||||
}
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ class ContainerBuilder extends SymfonyContainerBuilder {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep() {
|
||||
trigger_error('The container was serialized.', E_USER_ERROR);
|
||||
assert(FALSE, 'The container was serialized.');
|
||||
return array_keys(get_object_vars($this));
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace Drupal\Core\Diff;
|
|||
|
||||
use Drupal\Component\Diff\DiffFormatter as DiffFormatterBase;
|
||||
use Drupal\Component\Diff\WordLevelDiff;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
|
||||
/**
|
||||
|
@ -107,7 +107,7 @@ class DiffFormatter extends DiffFormatterBase {
|
|||
'class' => 'diff-marker',
|
||||
),
|
||||
array(
|
||||
'data' => $line,
|
||||
'data' => ['#markup' => $line],
|
||||
'class' => 'diff-context diff-addedline',
|
||||
)
|
||||
);
|
||||
|
@ -129,7 +129,7 @@ class DiffFormatter extends DiffFormatterBase {
|
|||
'class' => 'diff-marker',
|
||||
),
|
||||
array(
|
||||
'data' => $line,
|
||||
'data' => ['#markup' => $line],
|
||||
'class' => 'diff-context diff-deletedline',
|
||||
)
|
||||
);
|
||||
|
@ -148,7 +148,7 @@ class DiffFormatter extends DiffFormatterBase {
|
|||
return array(
|
||||
' ',
|
||||
array(
|
||||
'data' => $line,
|
||||
'data' => ['#markup' => $line],
|
||||
'class' => 'diff-context',
|
||||
)
|
||||
);
|
||||
|
@ -172,7 +172,7 @@ class DiffFormatter extends DiffFormatterBase {
|
|||
*/
|
||||
protected function _added($lines) {
|
||||
foreach ($lines as $line) {
|
||||
$this->rows[] = array_merge($this->emptyLine(), $this->addedLine(SafeMarkup::checkPlain($line)));
|
||||
$this->rows[] = array_merge($this->emptyLine(), $this->addedLine(Html::escape($line)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ class DiffFormatter extends DiffFormatterBase {
|
|||
*/
|
||||
protected function _deleted($lines) {
|
||||
foreach ($lines as $line) {
|
||||
$this->rows[] = array_merge($this->deletedLine(SafeMarkup::checkPlain($line)), $this->emptyLine());
|
||||
$this->rows[] = array_merge($this->deletedLine(Html::escape($line)), $this->emptyLine());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,7 +190,7 @@ class DiffFormatter extends DiffFormatterBase {
|
|||
*/
|
||||
protected function _context($lines) {
|
||||
foreach ($lines as $line) {
|
||||
$this->rows[] = array_merge($this->contextLine(SafeMarkup::checkPlain($line)), $this->contextLine(SafeMarkup::checkPlain($line)));
|
||||
$this->rows[] = array_merge($this->contextLine(Html::escape($line)), $this->contextLine(Html::escape($line)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,6 +198,8 @@ class DiffFormatter extends DiffFormatterBase {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
protected function _changed($orig, $closing) {
|
||||
$orig = array_map('\Drupal\Component\Utility\Html::escape', $orig);
|
||||
$closing = array_map('\Drupal\Component\Utility\Html::escape', $closing);
|
||||
$diff = new WordLevelDiff($orig, $closing);
|
||||
$del = $diff->orig();
|
||||
$add = $diff->closing();
|
||||
|
|
|
@ -15,6 +15,7 @@ use Drupal\Core\Config\BootstrapConfigStorageFactory;
|
|||
use Drupal\Core\Config\NullStorage;
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
|
||||
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
|
||||
use Drupal\Core\DependencyInjection\YamlFileLoader;
|
||||
use Drupal\Core\Extension\ExtensionDiscovery;
|
||||
|
@ -22,12 +23,10 @@ use Drupal\Core\File\MimeType\MimeTypeGuesser;
|
|||
use Drupal\Core\Http\TrustedHostsRequestFactory;
|
||||
use Drupal\Core\Language\Language;
|
||||
use Drupal\Core\PageCache\RequestPolicyInterface;
|
||||
use Drupal\Core\PhpStorage\PhpStorageFactory;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
@ -52,6 +51,55 @@ use Symfony\Component\Routing\Route;
|
|||
*/
|
||||
class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
||||
|
||||
/**
|
||||
* Holds the class used for dumping the container to a PHP array.
|
||||
*
|
||||
* In combination with swapping the container class this is useful to e.g.
|
||||
* dump to the human-readable PHP array format to debug the container
|
||||
* definition in an easier way.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $phpArrayDumperClass = '\Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper';
|
||||
|
||||
/**
|
||||
* Holds the default bootstrap container definition.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultBootstrapContainerDefinition = [
|
||||
'parameters' => [],
|
||||
'services' => [
|
||||
'database' => [
|
||||
'class' => 'Drupal\Core\Database\Connection',
|
||||
'factory' => 'Drupal\Core\Database\Database::getConnection',
|
||||
'arguments' => ['default'],
|
||||
],
|
||||
'cache.container' => [
|
||||
'class' => 'Drupal\Core\Cache\DatabaseBackend',
|
||||
'arguments' => ['@database', '@cache_tags_provider.container', 'container'],
|
||||
],
|
||||
'cache_tags_provider.container' => [
|
||||
'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum',
|
||||
'arguments' => ['@database'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Holds the class used for instantiating the bootstrap container.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bootstrapContainerClass = '\Drupal\Component\DependencyInjection\PhpArrayContainer';
|
||||
|
||||
/**
|
||||
* Holds the bootstrap container.
|
||||
*
|
||||
* @var \Symfony\Component\DependencyInjection\ContainerInterface
|
||||
*/
|
||||
protected $bootstrapContainer;
|
||||
|
||||
/**
|
||||
* Holds the container instance.
|
||||
*
|
||||
|
@ -96,13 +144,6 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
*/
|
||||
protected $moduleData = array();
|
||||
|
||||
/**
|
||||
* PHP code storage object to use for the compiled container.
|
||||
*
|
||||
* @var \Drupal\Component\PhpStorage\PhpStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* The class loader object.
|
||||
*
|
||||
|
@ -151,13 +192,16 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
protected $serviceYamls;
|
||||
|
||||
/**
|
||||
* List of discovered service provider class names.
|
||||
* List of discovered service provider class names or objects.
|
||||
*
|
||||
* This is a nested array whose top-level keys are 'app' and 'site', denoting
|
||||
* the origin of a service provider. Site-specific providers have to be
|
||||
* collected separately, because they need to be processed last, so as to be
|
||||
* able to override services from application service providers.
|
||||
*
|
||||
* Allowing objects is for example used to allow
|
||||
* \Drupal\KernelTests\KernelTestBase to register itself as service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $serviceProviderClasses;
|
||||
|
@ -393,6 +437,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
FileCacheFactory::setConfiguration($configuration);
|
||||
FileCacheFactory::setPrefix(Settings::getApcuPrefix('file_cache', $this->root));
|
||||
|
||||
$this->bootstrapContainer = new $this->bootstrapContainerClass(Settings::get('bootstrap_container_definition', $this->defaultBootstrapContainerDefinition));
|
||||
|
||||
// Initialize the container.
|
||||
$this->initializeContainer();
|
||||
|
||||
|
@ -427,6 +473,34 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setContainer(ContainerInterface $container = NULL) {
|
||||
if (isset($this->container)) {
|
||||
throw new \Exception('The container should not override an existing container.');
|
||||
}
|
||||
if ($this->booted) {
|
||||
throw new \Exception('The container cannot be set after a booted kernel.');
|
||||
}
|
||||
|
||||
$this->container = $container;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCachedContainerDefinition() {
|
||||
$cache = $this->bootstrapContainer->get('cache.container')->get($this->getContainerCacheKey());
|
||||
|
||||
if ($cache) {
|
||||
return $cache->data;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -465,16 +539,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
// Put the request on the stack.
|
||||
$this->container->get('request_stack')->push($request);
|
||||
|
||||
// Set the allowed protocols once we have the config available.
|
||||
$allowed_protocols = $this->container->getParameter('filter_protocols');
|
||||
if (!$allowed_protocols) {
|
||||
// \Drupal\Component\Utility\UrlHelper::filterBadProtocol() is called by
|
||||
// the installer and update.php, in which case the configuration may not
|
||||
// exist (yet). Provide a minimal default set of allowed protocols for
|
||||
// these cases.
|
||||
$allowed_protocols = array('http', 'https');
|
||||
}
|
||||
UrlHelper::setAllowedProtocols($allowed_protocols);
|
||||
// Set the allowed protocols.
|
||||
UrlHelper::setAllowedProtocols($this->container->getParameter('filter_protocols'));
|
||||
|
||||
// Override of Symfony's mime type guesser singleton.
|
||||
MimeTypeGuesser::registerWithSymfonyGuesser($this->container);
|
||||
|
@ -522,14 +588,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
// Add site-specific service providers.
|
||||
if (!empty($GLOBALS['conf']['container_service_providers'])) {
|
||||
foreach ($GLOBALS['conf']['container_service_providers'] as $class) {
|
||||
if (class_exists($class)) {
|
||||
if ((is_string($class) && class_exists($class)) || (is_object($class) && ($class instanceof ServiceProviderInterface || $class instanceof ServiceModifierInterface))) {
|
||||
$this->serviceProviderClasses['site'][] = $class;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$this->addServiceFiles(Settings::get('container_yamls'))) {
|
||||
throw new \Exception('The container_yamls setting is missing from settings.php');
|
||||
}
|
||||
$this->addServiceFiles(Settings::get('container_yamls', []));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -602,6 +666,9 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
*
|
||||
* @return Response
|
||||
* A Response instance
|
||||
*
|
||||
* @throws \Exception
|
||||
* If the passed in exception cannot be turned into a response.
|
||||
*/
|
||||
protected function handleException(\Exception $e, $request, $type) {
|
||||
if ($e instanceof HttpExceptionInterface) {
|
||||
|
@ -610,24 +677,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
return $response;
|
||||
}
|
||||
else {
|
||||
// @todo: _drupal_log_error() and thus _drupal_exception_handler() prints
|
||||
// the message directly. Extract a function which generates and returns it
|
||||
// instead, then remove the output buffer hack here.
|
||||
ob_start();
|
||||
try {
|
||||
// @todo: The exception handler prints the message directly. Extract a
|
||||
// function which returns the message instead.
|
||||
_drupal_exception_handler($e);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$message = Settings::get('rebuild_message', 'If you have just changed code (for example deployed a new module or moved an existing one) read <a href="https://www.drupal.org/documentation/rebuild">https://www.drupal.org/documentation/rebuild</a>');
|
||||
if ($message && Settings::get('rebuild_access', FALSE)) {
|
||||
$rebuild_path = $GLOBALS['base_url'] . '/rebuild.php';
|
||||
$message .= " or run the <a href=\"$rebuild_path\">rebuild script</a>";
|
||||
}
|
||||
print $message;
|
||||
}
|
||||
return new Response(ob_get_clean(), 500);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -711,24 +761,14 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the classname based on environment.
|
||||
* Returns the container cache key based on the environment.
|
||||
*
|
||||
* @return string
|
||||
* The class name.
|
||||
* The cache key used for the service container.
|
||||
*/
|
||||
protected function getClassName() {
|
||||
$parts = array('service_container', $this->environment, hash('crc32b', \Drupal::VERSION . Settings::get('deployment_identifier')));
|
||||
return implode('_', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the container class namespace based on the environment.
|
||||
*
|
||||
* @return string
|
||||
* The class name.
|
||||
*/
|
||||
protected function getClassNamespace() {
|
||||
return 'Drupal\\Core\\DependencyInjection\\Container\\' . $this->environment;
|
||||
protected function getContainerCacheKey() {
|
||||
$parts = array('service_container', $this->environment, \Drupal::VERSION, Settings::get('deployment_identifier'));
|
||||
return implode(':', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -767,28 +807,42 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
}
|
||||
}
|
||||
|
||||
// If the module list hasn't already been set in updateModules and we are
|
||||
// not forcing a rebuild, then try and load the container from the disk.
|
||||
if (empty($this->moduleList) && !$this->containerNeedsRebuild) {
|
||||
$fully_qualified_class_name = '\\' . $this->getClassNamespace() . '\\' . $this->getClassName();
|
||||
|
||||
// First, try to load from storage.
|
||||
if (!class_exists($fully_qualified_class_name, FALSE)) {
|
||||
$this->storage()->load($this->getClassName() . '.php');
|
||||
}
|
||||
// If the load succeeded or the class already existed, use it.
|
||||
if (class_exists($fully_qualified_class_name, FALSE)) {
|
||||
$container = new $fully_qualified_class_name;
|
||||
}
|
||||
// If we haven't booted yet but there is a container, then we're asked to
|
||||
// boot the container injected via setContainer().
|
||||
// @see \Drupal\KernelTests\KernelTestBase::setUp()
|
||||
if (isset($this->container) && !$this->booted) {
|
||||
$container = $this->container;
|
||||
}
|
||||
|
||||
if (!isset($container)) {
|
||||
// If the module list hasn't already been set in updateModules and we are
|
||||
// not forcing a rebuild, then try and load the container from the cache.
|
||||
if (empty($this->moduleList) && !$this->containerNeedsRebuild) {
|
||||
$container_definition = $this->getCachedContainerDefinition();
|
||||
}
|
||||
|
||||
// If there is no container and no cached container definition, build a new
|
||||
// one from scratch.
|
||||
if (!isset($container) && !isset($container_definition)) {
|
||||
$container = $this->compileContainer();
|
||||
|
||||
// Only dump the container if dumping is allowed. This is useful for
|
||||
// KernelTestBase, which never wants to use the real container, but always
|
||||
// the container builder.
|
||||
if ($this->allowDumping) {
|
||||
$dumper = new $this->phpArrayDumperClass($container);
|
||||
$container_definition = $dumper->getArray();
|
||||
}
|
||||
}
|
||||
|
||||
// The container was rebuilt successfully.
|
||||
$this->containerNeedsRebuild = FALSE;
|
||||
|
||||
// Only create a new class if we have a container definition.
|
||||
if (isset($container_definition)) {
|
||||
$class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container');
|
||||
$container = new $class($container_definition);
|
||||
}
|
||||
|
||||
$this->attachSynthetic($container);
|
||||
|
||||
$this->container = $container;
|
||||
|
@ -813,9 +867,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
\Drupal::setContainer($this->container);
|
||||
|
||||
// If needs dumping flag was set, dump the container.
|
||||
$base_class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container');
|
||||
if ($this->containerNeedsDumping && !$this->dumpDrupalContainer($this->container, $base_class)) {
|
||||
$this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be written to disk');
|
||||
if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) {
|
||||
$this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be saved to cache.');
|
||||
}
|
||||
|
||||
return $this->container;
|
||||
|
@ -870,6 +923,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
// Simpletest's internal browser.
|
||||
define('DRUPAL_TEST_IN_CHILD_SITE', TRUE);
|
||||
|
||||
// Web tests are to be conducted with runtime assertions active.
|
||||
assert_options(ASSERT_ACTIVE, TRUE);
|
||||
// Now synchronize PHP 5 and 7's handling of assertions as much as
|
||||
// possible.
|
||||
\Drupal\Component\Assertion\Handle::register();
|
||||
|
||||
// Log fatal errors to the test site directory.
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('error_log', DRUPAL_ROOT . '/sites/simpletest/' . substr($test_prefix, 10) . '/error.log');
|
||||
|
@ -1031,9 +1090,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
return;
|
||||
}
|
||||
|
||||
// Also wipe the PHP Storage caches, so that the container is rebuilt
|
||||
// for the next request.
|
||||
$this->storage()->deleteAll();
|
||||
// Also remove the container definition from the cache backend.
|
||||
$this->bootstrapContainer->get('cache.container')->deleteAll();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1171,7 +1229,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
);
|
||||
foreach ($this->serviceProviderClasses as $origin => $classes) {
|
||||
foreach ($classes as $name => $class) {
|
||||
$this->serviceProviders[$origin][$name] = new $class;
|
||||
if (!is_object($class)) {
|
||||
$this->serviceProviders[$origin][$name] = new $class;
|
||||
}
|
||||
else {
|
||||
$this->serviceProviders[$origin][$name] = $class;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1186,35 +1249,28 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Dumps the service container to PHP code in the config directory.
|
||||
* Stores the container definition in a cache.
|
||||
*
|
||||
* This method is based on the dumpContainer method in the parent class, but
|
||||
* that method is reliant on the Config component which we do not use here.
|
||||
*
|
||||
* @param ContainerBuilder $container
|
||||
* The service container.
|
||||
* @param string $baseClass
|
||||
* The name of the container's base class
|
||||
* @param array $container_definition
|
||||
* The container definition to cache.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the container was successfully dumped to disk.
|
||||
* TRUE if the container was successfully cached.
|
||||
*/
|
||||
protected function dumpDrupalContainer(ContainerBuilder $container, $baseClass) {
|
||||
if (!$this->storage()->writeable()) {
|
||||
return FALSE;
|
||||
protected function cacheDrupalContainer(array $container_definition) {
|
||||
$saved = TRUE;
|
||||
try {
|
||||
$this->bootstrapContainer->get('cache.container')->set($this->getContainerCacheKey(), $container_definition);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// There is no way to get from the Cache API if the cache set was
|
||||
// successful or not, hence an Exception is caught and the caller informed
|
||||
// about the error condition.
|
||||
$saved = FALSE;
|
||||
}
|
||||
// Cache the container.
|
||||
$dumper = new PhpDumper($container);
|
||||
$class = $this->getClassName();
|
||||
$namespace = $this->getClassNamespace();
|
||||
$content = $dumper->dump([
|
||||
'class' => $class,
|
||||
'base_class' => $baseClass,
|
||||
'namespace' => $namespace,
|
||||
]);
|
||||
return $this->storage()->save($class . '.php', $content);
|
||||
}
|
||||
|
||||
return $saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a http kernel from the container
|
||||
|
@ -1225,18 +1281,6 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
return $this->container->get('http_kernel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the PHP code storage object to use for the compiled container.
|
||||
*
|
||||
* @return \Drupal\Component\PhpStorage\PhpStorageInterface
|
||||
*/
|
||||
protected function storage() {
|
||||
if (!isset($this->storage)) {
|
||||
$this->storage = PhpStorageFactory::get('service_container');
|
||||
}
|
||||
return $this->storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active configuration storage to use during building the container.
|
||||
*
|
||||
|
@ -1435,17 +1479,10 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
/**
|
||||
* Add service files.
|
||||
*
|
||||
* @param $service_yamls
|
||||
* @param string[] $service_yamls
|
||||
* A list of service files.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the list was an array, FALSE otherwise.
|
||||
*/
|
||||
protected function addServiceFiles($service_yamls) {
|
||||
if (is_array($service_yamls)) {
|
||||
$this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists');
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
protected function addServiceFiles(array $service_yamls) {
|
||||
$this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
namespace Drupal\Core;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
|
@ -16,7 +17,7 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
* This interface extends Symfony's KernelInterface and adds methods for
|
||||
* responding to modules being enabled or disabled during its lifetime.
|
||||
*/
|
||||
interface DrupalKernelInterface extends HttpKernelInterface {
|
||||
interface DrupalKernelInterface extends HttpKernelInterface, ContainerAwareInterface {
|
||||
|
||||
/**
|
||||
* Boots the current kernel.
|
||||
|
@ -57,6 +58,16 @@ interface DrupalKernelInterface extends HttpKernelInterface {
|
|||
*/
|
||||
public function getContainer();
|
||||
|
||||
/**
|
||||
* Returns the cached container definition - if any.
|
||||
*
|
||||
* This also allows inspecting a built container for debugging purposes.
|
||||
*
|
||||
* @return array|NULL
|
||||
* The cached container definition or NULL if not found in cache.
|
||||
*/
|
||||
public function getCachedContainerDefinition();
|
||||
|
||||
/**
|
||||
* Set the current site path.
|
||||
*
|
||||
|
|
|
@ -801,6 +801,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
|
|||
$translation->translations = &$this->translations;
|
||||
$translation->enforceIsNew = &$this->enforceIsNew;
|
||||
$translation->newRevision = &$this->newRevision;
|
||||
$translation->entityKeys = &$this->entityKeys;
|
||||
$translation->translatableEntityKeys = &$this->translatableEntityKeys;
|
||||
$translation->translationInitialize = FALSE;
|
||||
$translation->typedData = NULL;
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ class EntityFormDisplay extends EntityDisplayBase implements EntityFormDisplayIn
|
|||
* Returns the entity_form_display object used to build an entity form.
|
||||
*
|
||||
* Depending on the configuration of the form mode for the entity bundle, this
|
||||
* can be either the display object associated to the form mode, or the
|
||||
* can be either the display object associated with the form mode, or the
|
||||
* 'default' display.
|
||||
*
|
||||
* This method should only be used internally when rendering an entity form.
|
||||
|
|
|
@ -46,8 +46,8 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
|
|||
* Returns the display objects used to render a set of entities.
|
||||
*
|
||||
* Depending on the configuration of the view mode for each bundle, this can
|
||||
* be either the display object associated to the view mode, or the 'default'
|
||||
* display.
|
||||
* be either the display object associated with the view mode, or the
|
||||
* 'default' display.
|
||||
*
|
||||
* This method should only be used internally when rendering an entity. When
|
||||
* assigning suggested display options for a component in a given view mode,
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Drupal\Core\Entity;
|
|||
|
||||
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
|
||||
use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
||||
|
@ -95,13 +96,13 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyUpdates() {
|
||||
$change_list = $this->getChangeList();
|
||||
if ($change_list) {
|
||||
$complete_change_list = $this->getChangeList();
|
||||
if ($complete_change_list) {
|
||||
// self::getChangeList() only disables the cache and does not invalidate.
|
||||
// In case there are changes, explicitly invalidate caches.
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
}
|
||||
foreach ($change_list as $entity_type_id => $change_list) {
|
||||
foreach ($complete_change_list as $entity_type_id => $change_list) {
|
||||
// Process entity type definition changes before storage definitions ones
|
||||
// this is necessary when you change an entity type from non-revisionable
|
||||
// to revisionable and at the same time add revisionable fields to the
|
||||
|
@ -127,42 +128,76 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE) {
|
||||
$change_list = $this->getChangeList();
|
||||
if (!isset($change_list[$entity_type_id]) || $change_list[$entity_type_id]['entity_type'] !== $op) {
|
||||
return FALSE;
|
||||
}
|
||||
if ($reset_cached_definitions) {
|
||||
// self::getChangeList() only disables the cache and does not invalidate.
|
||||
// In case there are changes, explicitly invalidate caches.
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
}
|
||||
$this->doEntityUpdate($op, $entity_type_id);
|
||||
return TRUE;
|
||||
public function getEntityType($entity_type_id) {
|
||||
$entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id);
|
||||
return $entity_type ? clone $entity_type : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE) {
|
||||
$change_list = $this->getChangeList();
|
||||
if (!isset($change_list[$entity_type_id]['field_storage_definitions']) || $change_list[$entity_type_id]['field_storage_definitions'][$field_name] !== $op) {
|
||||
return FALSE;
|
||||
public function installEntityType(EntityTypeInterface $entity_type) {
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
$this->entityManager->onEntityTypeCreate($entity_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function updateEntityType(EntityTypeInterface $entity_type) {
|
||||
$original = $this->getEntityType($entity_type->id());
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function uninstallEntityType(EntityTypeInterface $entity_type) {
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
$this->entityManager->onEntityTypeDelete($entity_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition) {
|
||||
// @todo Pass a mutable field definition interface when we have one. See
|
||||
// https://www.drupal.org/node/2346329.
|
||||
if ($storage_definition instanceof BaseFieldDefinition) {
|
||||
$storage_definition
|
||||
->setName($name)
|
||||
->setTargetEntityTypeId($entity_type_id)
|
||||
->setProvider($provider)
|
||||
->setTargetBundle(NULL);
|
||||
}
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
$this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
|
||||
}
|
||||
|
||||
if ($reset_cached_definitions) {
|
||||
// self::getChangeList() only disables the cache and does not invalidate.
|
||||
// In case there are changes, explicitly invalidate caches.
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
}
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFieldStorageDefinition($name, $entity_type_id) {
|
||||
$storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
|
||||
return isset($storage_definitions[$name]) ? clone $storage_definitions[$name] : NULL;
|
||||
}
|
||||
|
||||
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
|
||||
$original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
|
||||
$storage_definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : NULL;
|
||||
$original_storage_definition = isset($original_storage_definitions[$field_name]) ? $original_storage_definitions[$field_name] : NULL;
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
|
||||
$original = $this->getFieldStorageDefinition($storage_definition->getName(), $storage_definition->getTargetEntityTypeId());
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
$this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $original);
|
||||
}
|
||||
|
||||
$this->doFieldUpdate($op, $storage_definition, $original_storage_definition);
|
||||
return TRUE;
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
|
||||
$this->entityManager->clearCachedDefinitions();
|
||||
$this->entityManager->onFieldStorageDefinitionDelete($storage_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
namespace Drupal\Core\Entity;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
|
||||
/**
|
||||
* Defines an interface for managing entity definition updates.
|
||||
*
|
||||
|
@ -25,12 +27,22 @@ namespace Drupal\Core\Entity;
|
|||
* report the differences or when to apply each update. This interface is for
|
||||
* managing that.
|
||||
*
|
||||
* This interface also provides methods to retrieve instances of the definitions
|
||||
* to be updated ready to be manipulated. In fact when definitions change in
|
||||
* code the system needs to be notified about that and the definitions stored in
|
||||
* state need to be reconciled with the ones living in code. This typically
|
||||
* happens in Update API functions, which need to take the system from a known
|
||||
* state to another known state. Relying on the definitions living in code might
|
||||
* prevent this, as the system might transition directly to the last available
|
||||
* state, and thus skipping the intermediate steps. Manipulating the definitions
|
||||
* in state allows to avoid this and ensures that the various steps of the
|
||||
* update process are predictable and repeatable.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\EntityManagerInterface::getDefinition()
|
||||
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition()
|
||||
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions()
|
||||
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions()
|
||||
* @see \Drupal\Core\Entity\EntityTypeListenerInterface
|
||||
* @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
|
||||
* @see hook_update_N()
|
||||
*/
|
||||
interface EntityDefinitionUpdateManagerInterface {
|
||||
|
||||
|
@ -75,6 +87,9 @@ interface EntityDefinitionUpdateManagerInterface {
|
|||
/**
|
||||
* Applies all the detected valid changes.
|
||||
*
|
||||
* Use this with care, as it will apply updates for any module, which will
|
||||
* lead to unpredictable results.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* This exception is thrown if a change cannot be applied without
|
||||
* unacceptable data loss. In such a case, the site administrator needs to
|
||||
|
@ -84,67 +99,92 @@ interface EntityDefinitionUpdateManagerInterface {
|
|||
public function applyUpdates();
|
||||
|
||||
/**
|
||||
* Performs a single entity definition update.
|
||||
* Returns an entity type definition ready to be manipulated.
|
||||
*
|
||||
* This method should be used from hook_update_N() functions to process
|
||||
* entity definition updates as part of the update function. This is only
|
||||
* necessary if the hook_update_N() implementation relies on the entity
|
||||
* definition update. All remaining entity definition updates will be run
|
||||
* automatically after the hook_update_N() implementations.
|
||||
* When needing to apply updates to existing entity type definitions, this
|
||||
* method should always be used to retrieve a definition ready to be
|
||||
* manipulated.
|
||||
*
|
||||
* @param string $op
|
||||
* The operation to perform, either static::DEFINITION_CREATED or
|
||||
* static::DEFINITION_UPDATED.
|
||||
* @param string $entity_type_id
|
||||
* The entity type to update.
|
||||
* @param bool $reset_cached_definitions
|
||||
* (optional). Determines whether to clear the Entity Manager's cached
|
||||
* definitions before applying the update. Defaults to TRUE. Can be used
|
||||
* to prevent unnecessary cache invalidation when a hook_update_N() makes
|
||||
* multiple calls to this method.
|
||||
* The entity type identifier.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the entity update is processed, FALSE if not.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* This exception is thrown if a change cannot be applied without
|
||||
* unacceptable data loss. In such a case, the site administrator needs to
|
||||
* apply some other process, such as a custom update function or a
|
||||
* migration via the Migrate module.
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface
|
||||
* The entity type definition.
|
||||
*/
|
||||
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE);
|
||||
public function getEntityType($entity_type_id);
|
||||
|
||||
/**
|
||||
* Performs a single field storage definition update.
|
||||
* Installs a new entity type definition.
|
||||
*
|
||||
* This method should be used from hook_update_N() functions to process field
|
||||
* storage definition updates as part of the update function. This is only
|
||||
* necessary if the hook_update_N() implementation relies on the field storage
|
||||
* definition update. All remaining field storage definition updates will be
|
||||
* run automatically after the hook_update_N() implementations.
|
||||
*
|
||||
* @param string $op
|
||||
* The operation to perform, possible values are static::DEFINITION_CREATED,
|
||||
* static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
|
||||
* @param string $entity_type_id
|
||||
* The entity type to update.
|
||||
* @param string $field_name
|
||||
* The field name to update.
|
||||
* @param bool $reset_cached_definitions
|
||||
* (optional). Determines whether to clear the Entity Manager's cached
|
||||
* definitions before applying the update. Defaults to TRUE. Can be used
|
||||
* to prevent unnecessary cache invalidation when a hook_update_N() makes
|
||||
* multiple calls to this method.
|
||||
|
||||
* @return bool
|
||||
* TRUE if the entity update is processed, FALSE if not.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* This exception is thrown if a change cannot be applied without
|
||||
* unacceptable data loss. In such a case, the site administrator needs to
|
||||
* apply some other process, such as a custom update function or a
|
||||
* migration via the Migrate module.
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
*/
|
||||
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE);
|
||||
public function installEntityType(EntityTypeInterface $entity_type);
|
||||
|
||||
/**
|
||||
* Applies any change performed to the passed entity type definition.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
*/
|
||||
public function updateEntityType(EntityTypeInterface $entity_type);
|
||||
|
||||
/**
|
||||
* Uninstalls an entity type definition.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
*/
|
||||
public function uninstallEntityType(EntityTypeInterface $entity_type);
|
||||
|
||||
/**
|
||||
* Returns a field storage definition ready to be manipulated.
|
||||
*
|
||||
* When needing to apply updates to existing field storage definitions, this
|
||||
* method should always be used to retrieve a storage definition ready to be
|
||||
* manipulated.
|
||||
*
|
||||
* @param string $name
|
||||
* The field name.
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
*
|
||||
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface
|
||||
* The field storage definition.
|
||||
*
|
||||
* @todo Make this return a mutable storage definition interface when we have
|
||||
* one. See https://www.drupal.org/node/2346329.
|
||||
*/
|
||||
public function getFieldStorageDefinition($name, $entity_type_id);
|
||||
|
||||
/**
|
||||
* Installs a new field storage definition.
|
||||
*
|
||||
* @param string $name
|
||||
* The field storage definition name.
|
||||
* @param string $entity_type_id
|
||||
* The target entity type identifier.
|
||||
* @param string $provider
|
||||
* The name of the definition provider.
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
|
||||
* The field storage definition.
|
||||
*/
|
||||
public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition);
|
||||
|
||||
/**
|
||||
* Applies any change performed to the passed field storage definition.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
|
||||
* The field storage definition.
|
||||
*/
|
||||
public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
|
||||
|
||||
/**
|
||||
* Uninstalls a field storage definition.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
|
||||
* The field storage definition.
|
||||
*/
|
||||
public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
|
||||
|
||||
}
|
||||
|
|
|
@ -256,19 +256,9 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
|
|||
parent::calculateDependencies();
|
||||
$target_entity_type = $this->entityManager()->getDefinition($this->targetEntityType);
|
||||
|
||||
$bundle_entity_type_id = $target_entity_type->getBundleEntityType();
|
||||
if ($bundle_entity_type_id != 'bundle') {
|
||||
// If the target entity type uses entities to manage its bundles then
|
||||
// depend on the bundle entity.
|
||||
if (!$bundle_entity = $this->entityManager()->getStorage($bundle_entity_type_id)->load($this->bundle)) {
|
||||
throw new \LogicException("Missing bundle entity, entity type $bundle_entity_type_id, entity id {$this->bundle}.");
|
||||
}
|
||||
$this->addDependency('config', $bundle_entity->getConfigDependencyName());
|
||||
}
|
||||
else {
|
||||
// Depend on the provider of the entity type.
|
||||
$this->addDependency('module', $target_entity_type->getProvider());
|
||||
}
|
||||
// Create dependency on the bundle.
|
||||
$bundle_config_dependency = $target_entity_type->getBundleConfigDependency($this->bundle);
|
||||
$this->addDependency($bundle_config_dependency['type'], $bundle_config_dependency['name']);
|
||||
|
||||
// If field.module is enabled, add dependencies on 'field_config' entities
|
||||
// for both displayed and hidden fields. We intentionally leave out base
|
||||
|
|
|
@ -14,7 +14,10 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
|
|||
/**
|
||||
* Provides a base class for entity handlers.
|
||||
*
|
||||
* @todo Deprecate this in https://www.drupal.org/node/2471663.
|
||||
* @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0.
|
||||
* Implement the container injection pattern of
|
||||
* \Drupal\Core\Entity\EntityHandlerInterface::createInstance() to obtain the
|
||||
* module handler service for your class.
|
||||
*/
|
||||
abstract class EntityHandlerBase {
|
||||
use StringTranslationTrait;
|
||||
|
|
|
@ -9,7 +9,6 @@ namespace Drupal\Core\Entity;
|
|||
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Defines a generic implementation to build a listing of entities.
|
||||
|
@ -40,9 +39,12 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
|
|||
protected $entityType;
|
||||
|
||||
/**
|
||||
* The number of entities to list per page.
|
||||
* The number of entities to list per page, or FALSE to list all entities.
|
||||
*
|
||||
* @var int
|
||||
* For example, set this to FALSE if the list uses client-side filters that
|
||||
* require all entities to be listed (like the views overview).
|
||||
*
|
||||
* @var int|false
|
||||
*/
|
||||
protected $limit = 50;
|
||||
|
||||
|
@ -92,25 +94,31 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
|
|||
* An array of entity IDs.
|
||||
*/
|
||||
protected function getEntityIds() {
|
||||
$query = $this->getStorage()->getQuery();
|
||||
$keys = $this->entityType->getKeys();
|
||||
return $query
|
||||
->sort($keys['id'])
|
||||
->pager($this->limit)
|
||||
->execute();
|
||||
$query = $this->getStorage()->getQuery()
|
||||
->sort($this->entityType->getKey('id'));
|
||||
|
||||
// Only add the pager if a limit is specified.
|
||||
if ($this->limit) {
|
||||
$query->pager($this->limit);
|
||||
}
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the escaped label of an entity.
|
||||
* Gets the label of an entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being listed.
|
||||
*
|
||||
* @return string
|
||||
* The escaped entity label.
|
||||
* The entity label.
|
||||
*
|
||||
* @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0
|
||||
* Use $entity->label() instead. This method used to escape the entity
|
||||
* label. The render system's autoescape is now relied upon.
|
||||
*/
|
||||
protected function getLabel(EntityInterface $entity) {
|
||||
return SafeMarkup::checkPlain($entity->label());
|
||||
return $entity->label();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -227,9 +235,13 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
|
|||
$build['table']['#rows'][$entity->id()] = $row;
|
||||
}
|
||||
}
|
||||
$build['pager'] = array(
|
||||
'#type' => 'pager',
|
||||
);
|
||||
|
||||
// Only add the pager if a limit is specified.
|
||||
if ($this->limit) {
|
||||
$build['pager'] = array(
|
||||
'#type' => 'pager',
|
||||
);
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
|
||||
|
|
|
@ -423,7 +423,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
|
|||
$keys = array_filter($entity_type->getKeys());
|
||||
|
||||
// Fail with an exception for non-fieldable entity types.
|
||||
if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
|
||||
if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
|
||||
throw new \LogicException("Getting the base fields is not supported for entity type {$entity_type->getLabel()}.");
|
||||
}
|
||||
|
||||
|
@ -655,7 +655,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
|
|||
// bundles, and we do not expect to have so many different entity
|
||||
// types for this to become a bottleneck.
|
||||
foreach ($this->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
|
||||
if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) {
|
||||
$bundles = array_keys($this->getBundleInfo($entity_type_id));
|
||||
foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $base_field_definition) {
|
||||
$this->fieldMap[$entity_type_id][$field_name] = [
|
||||
|
@ -1205,7 +1205,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
|
|||
$this->eventDispatcher->dispatch(EntityTypeEvents::CREATE, new EntityTypeEvent($entity_type));
|
||||
|
||||
$this->setLastInstalledDefinition($entity_type);
|
||||
if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
|
||||
if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) {
|
||||
$this->setLastInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ class EntityType implements EntityTypeInterface {
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle_entity_type = 'bundle';
|
||||
protected $bundle_entity_type = NULL;
|
||||
|
||||
/**
|
||||
* The name of the entity type for which bundles are provided.
|
||||
|
@ -232,6 +232,13 @@ class EntityType implements EntityTypeInterface {
|
|||
*/
|
||||
protected $constraints = array();
|
||||
|
||||
/**
|
||||
* Any additional properties and values.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $additional = [];
|
||||
|
||||
/**
|
||||
* Constructs a new EntityType.
|
||||
*
|
||||
|
@ -248,7 +255,7 @@ class EntityType implements EntityTypeInterface {
|
|||
}
|
||||
|
||||
foreach ($definition as $property => $value) {
|
||||
$this->{$property} = $value;
|
||||
$this->set($property, $value);
|
||||
}
|
||||
|
||||
// Ensure defaults.
|
||||
|
@ -279,14 +286,25 @@ class EntityType implements EntityTypeInterface {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function get($property) {
|
||||
return isset($this->{$property}) ? $this->{$property} : NULL;
|
||||
if (property_exists($this, $property)) {
|
||||
$value = isset($this->{$property}) ? $this->{$property} : NULL;
|
||||
}
|
||||
else {
|
||||
$value = isset($this->additional[$property]) ? $this->additional[$property] : NULL;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function set($property, $value) {
|
||||
$this->{$property} = $value;
|
||||
if (property_exists($this, $property)) {
|
||||
$this->{$property} = $value;
|
||||
}
|
||||
else {
|
||||
$this->additional[$property] = $value;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -764,4 +782,30 @@ class EntityType implements EntityTypeInterface {
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBundleConfigDependency($bundle) {
|
||||
// If this entity type uses entities to manage its bundles then depend on
|
||||
// the bundle entity.
|
||||
if ($bundle_entity_type_id = $this->getBundleEntityType()) {
|
||||
if (!$bundle_entity = \Drupal::entityManager()->getStorage($bundle_entity_type_id)->load($bundle)) {
|
||||
throw new \LogicException(sprintf('Missing bundle entity, entity type %s, entity id %s.', $bundle_entity_type_id, $bundle));
|
||||
}
|
||||
$config_dependency = [
|
||||
'type' => 'config',
|
||||
'name' => $bundle_entity->getConfigDependencyName(),
|
||||
];
|
||||
}
|
||||
else {
|
||||
// Depend on the provider of the entity type.
|
||||
$config_dependency = [
|
||||
'type' => 'module',
|
||||
'name' => $this->getProvider(),
|
||||
];
|
||||
}
|
||||
|
||||
return $config_dependency;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -732,4 +732,17 @@ interface EntityTypeInterface {
|
|||
*/
|
||||
public function addConstraint($constraint_name, $options = NULL);
|
||||
|
||||
/**
|
||||
* Gets the config dependency info for this entity, if any exists.
|
||||
*
|
||||
* @param string $bundle
|
||||
* The bundle name.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the following keys:
|
||||
* - 'type': The config dependency type (e.g. 'module', 'config').
|
||||
* - 'name': The name of the config dependency.
|
||||
*/
|
||||
public function getBundleConfigDependency($bundle);
|
||||
|
||||
}
|
||||
|
|
|
@ -465,7 +465,7 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
|
|||
// series of fields individually for cases such as views tables.
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$bundle = $entity->bundle();
|
||||
$key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . crc32(serialize($display_options));
|
||||
$key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . hash('crc32b', serialize($display_options));
|
||||
if (!isset($this->singleFieldDisplays[$key])) {
|
||||
$this->singleFieldDisplays[$key] = EntityViewDisplay::create(array(
|
||||
'targetEntityType' => $entity_type_id,
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
namespace Drupal\Core\Entity\Plugin\DataType;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
|
||||
|
@ -114,7 +113,7 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
|
|||
*/
|
||||
public function getProperties($include_computed = FALSE) {
|
||||
if (!isset($this->entity)) {
|
||||
throw new MissingDataException(SafeMarkup::format('Unable to get properties as no entity has been provided.'));
|
||||
throw new MissingDataException('Unable to get properties as no entity has been provided.');
|
||||
}
|
||||
if (!$this->entity instanceof FieldableEntityInterface) {
|
||||
// @todo: Add support for config entities in
|
||||
|
|
|
@ -189,9 +189,10 @@ class DefaultTableMapping implements TableMappingInterface {
|
|||
public function getColumnNames($field_name) {
|
||||
if (!isset($this->columnMapping[$field_name])) {
|
||||
$this->columnMapping[$field_name] = array();
|
||||
$storage_definition = $this->fieldStorageDefinitions[$field_name];
|
||||
foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
|
||||
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($storage_definition, $property_name);
|
||||
if (isset($this->fieldStorageDefinitions[$field_name])) {
|
||||
foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
|
||||
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->columnMapping[$field_name];
|
||||
|
|
|
@ -10,6 +10,8 @@ namespace Drupal\Core\Entity\Sql;
|
|||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Database\DatabaseExceptionWrapper;
|
||||
use Drupal\Core\Database\SchemaException;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityStorageBase;
|
||||
use Drupal\Core\Entity\EntityBundleListenerInterface;
|
||||
|
@ -1351,7 +1353,9 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
|
||||
$this->getStorageSchema()->onEntityTypeCreate($entity_type);
|
||||
$this->wrapSchemaException(function () use ($entity_type) {
|
||||
$this->getStorageSchema()->onEntityTypeCreate($entity_type);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1364,14 +1368,18 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
// definition.
|
||||
$this->initTableLayout();
|
||||
// Let the schema handler adapt to possible table layout changes.
|
||||
$this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
|
||||
$this->wrapSchemaException(function () use ($entity_type, $original) {
|
||||
$this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
|
||||
$this->getStorageSchema()->onEntityTypeDelete($entity_type);
|
||||
$this->wrapSchemaException(function () use ($entity_type) {
|
||||
$this->getStorageSchema()->onEntityTypeDelete($entity_type);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1386,14 +1394,18 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
|
||||
$this->tableMapping = NULL;
|
||||
}
|
||||
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
|
||||
$this->wrapSchemaException(function () use ($storage_definition) {
|
||||
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
|
||||
$this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
|
||||
$this->wrapSchemaException(function () use ($storage_definition, $original) {
|
||||
$this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1421,7 +1433,31 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
}
|
||||
|
||||
// Update the field schema.
|
||||
$this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
|
||||
$this->wrapSchemaException(function () use ($storage_definition) {
|
||||
$this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a database schema exception into an entity storage exception.
|
||||
*
|
||||
* @param callable $callback
|
||||
* The callback to be executed.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* When a database schema exception is thrown.
|
||||
*/
|
||||
protected function wrapSchemaException(callable $callback) {
|
||||
$message = 'Exception thrown while performing a schema update.';
|
||||
try {
|
||||
$callback();
|
||||
}
|
||||
catch (SchemaException $e) {
|
||||
throw new EntityStorageException($message, 0, $e);
|
||||
}
|
||||
catch (DatabaseExceptionWrapper $e) {
|
||||
throw new EntityStorageException($message, 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1598,10 +1634,12 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
foreach ($storage_definition->getColumns() as $column_name => $data) {
|
||||
$or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
|
||||
}
|
||||
$query
|
||||
->condition($or)
|
||||
->fields('t', array('entity_id'))
|
||||
->distinct(TRUE);
|
||||
$query->condition($or);
|
||||
if (!$as_bool) {
|
||||
$query
|
||||
->fields('t', array('entity_id'))
|
||||
->distinct(TRUE);
|
||||
}
|
||||
}
|
||||
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
|
||||
// Ascertain the table this field is mapped too.
|
||||
|
|
|
@ -186,27 +186,44 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
|
||||
return $this->getDedicatedTableSchema($storage_definition) != $this->loadFieldSchemaData($original);
|
||||
}
|
||||
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
|
||||
$field_name = $storage_definition->getName();
|
||||
$schema = array();
|
||||
foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
|
||||
if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
|
||||
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
|
||||
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
|
||||
}
|
||||
}
|
||||
return $schema != $this->loadFieldSchemaData($original);
|
||||
}
|
||||
else {
|
||||
if ($storage_definition->hasCustomStorage()) {
|
||||
// The field has custom storage, so we don't know if a schema change is
|
||||
// needed or not, but since per the initial checks earlier in this
|
||||
// function, nothing about the definition changed that we manage, we
|
||||
// return FALSE.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the schema data for the given field storage definition.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
|
||||
* The field storage definition. The field that must not have custom
|
||||
* storage, i.e. the storage must take care of storing the field.
|
||||
*
|
||||
* @return array
|
||||
* The schema data.
|
||||
*/
|
||||
protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
|
||||
assert('!$storage_definition->hasCustomStorage();');
|
||||
$table_mapping = $this->storage->getTableMapping();
|
||||
$schema = [];
|
||||
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
|
||||
$schema = $this->getDedicatedTableSchema($storage_definition);
|
||||
}
|
||||
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
|
||||
$field_name = $storage_definition->getName();
|
||||
foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
|
||||
if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
|
||||
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
|
||||
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -285,7 +302,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
|
||||
// If a migration is required, we can't proceed.
|
||||
if ($this->requiresEntityDataMigration($entity_type, $original)) {
|
||||
throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type with data.');
|
||||
throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type->id() . ') with data.');
|
||||
}
|
||||
|
||||
// If we have no data just recreate the entity schema from scratch.
|
||||
|
@ -311,36 +328,12 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
}
|
||||
}
|
||||
else {
|
||||
$schema_handler = $this->database->schema();
|
||||
|
||||
// Drop original indexes and unique keys.
|
||||
foreach ($this->loadEntitySchemaData($entity_type) as $table_name => $schema) {
|
||||
if (!empty($schema['indexes'])) {
|
||||
foreach ($schema['indexes'] as $name => $specifier) {
|
||||
$schema_handler->dropIndex($table_name, $name);
|
||||
}
|
||||
}
|
||||
if (!empty($schema['unique keys'])) {
|
||||
foreach ($schema['unique keys'] as $name => $specifier) {
|
||||
$schema_handler->dropUniqueKey($table_name, $name);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
|
||||
|
||||
// Create new indexes and unique keys.
|
||||
$entity_schema = $this->getEntitySchema($entity_type, TRUE);
|
||||
foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) {
|
||||
if (!empty($schema['indexes'])) {
|
||||
foreach ($schema['indexes'] as $name => $specifier) {
|
||||
$schema_handler->addIndex($table_name, $name, $specifier);
|
||||
}
|
||||
}
|
||||
if (!empty($schema['unique keys'])) {
|
||||
foreach ($schema['unique keys'] as $name => $specifier) {
|
||||
$schema_handler->addUniqueKey($table_name, $name, $specifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->createEntitySchemaIndexes($entity_schema);
|
||||
|
||||
// Store the updated entity schema.
|
||||
$this->saveEntitySchemaData($entity_type, $entity_schema);
|
||||
|
@ -411,7 +404,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
// @todo Add purging to all fields: https://www.drupal.org/node/2282119.
|
||||
try {
|
||||
if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
|
||||
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field with data that cannot be purged.');
|
||||
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
|
||||
}
|
||||
}
|
||||
catch (DatabaseException $e) {
|
||||
|
@ -1138,9 +1131,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
|
||||
// Check if the index exists because it might already have been
|
||||
// created as part of the earlier entity type update event.
|
||||
if (!$schema_handler->indexExists($table_name, $name)) {
|
||||
$schema_handler->addIndex($table_name, $name, $specifier);
|
||||
}
|
||||
$this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
|
||||
}
|
||||
}
|
||||
if (!empty($schema[$table_name]['unique keys'])) {
|
||||
|
@ -1154,7 +1145,14 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->saveFieldSchemaData($storage_definition, $schema);
|
||||
|
||||
if (!$only_save) {
|
||||
// Make sure any entity index involving this field is re-created if
|
||||
// needed.
|
||||
$this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1185,6 +1183,9 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
* The storage definition of the field being deleted.
|
||||
*/
|
||||
protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
|
||||
// Make sure any entity index involving this field is dropped.
|
||||
$this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
|
||||
|
||||
$deleted_field_name = $storage_definition->getName();
|
||||
$table_mapping = $this->storage->getTableMapping(
|
||||
$this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
|
||||
|
@ -1263,8 +1264,8 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
}
|
||||
}
|
||||
else {
|
||||
if ($storage_definition->getColumns() != $original->getColumns()) {
|
||||
throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
|
||||
if ($this->hasColumnChanges($storage_definition, $original)) {
|
||||
throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
|
||||
}
|
||||
// There is data, so there are no column changes. Drop all the prior
|
||||
// indexes and create all the new ones, except for all the priors that
|
||||
|
@ -1273,9 +1274,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
$table = $table_mapping->getDedicatedDataTableName($original);
|
||||
$revision_table = $table_mapping->getDedicatedRevisionTableName($original);
|
||||
|
||||
// Get the field schemas.
|
||||
$schema = $storage_definition->getSchema();
|
||||
$original_schema = $original->getSchema();
|
||||
|
||||
// Gets the SQL schema for a dedicated tables.
|
||||
$actual_schema = $this->getDedicatedTableSchema($storage_definition);
|
||||
|
||||
foreach ($original_schema['indexes'] as $name => $columns) {
|
||||
if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
|
||||
$real_name = $this->getFieldIndexName($storage_definition, $name);
|
||||
|
@ -1302,8 +1307,10 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
$real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
|
||||
}
|
||||
}
|
||||
$this->database->schema()->addIndex($table, $real_name, $real_columns);
|
||||
$this->database->schema()->addIndex($revision_table, $real_name, $real_columns);
|
||||
// Check if the index exists because it might already have been
|
||||
// created as part of the earlier entity type update event.
|
||||
$this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
|
||||
$this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
|
||||
}
|
||||
}
|
||||
$this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
|
||||
|
@ -1348,8 +1355,8 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
}
|
||||
}
|
||||
else {
|
||||
if ($storage_definition->getColumns() != $original->getColumns()) {
|
||||
throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
|
||||
if ($this->hasColumnChanges($storage_definition, $original)) {
|
||||
throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
|
||||
}
|
||||
|
||||
$updated_field_name = $storage_definition->getName();
|
||||
|
@ -1366,6 +1373,20 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
if ($field_name == $updated_field_name) {
|
||||
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
|
||||
|
||||
// Handle NOT NULL constraints.
|
||||
foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
|
||||
$not_null = !empty($specifier['not null']);
|
||||
$original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
|
||||
if ($not_null !== $original_not_null) {
|
||||
if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
|
||||
throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
|
||||
}
|
||||
$column_schema = $original_schema[$table_name]['fields'][$column_name];
|
||||
$column_schema['not null'] = $not_null;
|
||||
$schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop original indexes and unique keys.
|
||||
if (!empty($original_schema[$table_name]['indexes'])) {
|
||||
foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
|
||||
|
@ -1380,7 +1401,10 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
// Create new indexes and unique keys.
|
||||
if (!empty($schema[$table_name]['indexes'])) {
|
||||
foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
|
||||
$schema_handler->addIndex($table_name, $name, $specifier);
|
||||
// Check if the index exists because it might already have been
|
||||
// created as part of the earlier entity type update event.
|
||||
$this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
|
||||
|
||||
}
|
||||
}
|
||||
if (!empty($schema[$table_name]['unique keys'])) {
|
||||
|
@ -1397,6 +1421,120 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the specified entity schema indexes and keys.
|
||||
*
|
||||
* @param array $entity_schema
|
||||
* The entity schema definition.
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface|NULL $storage_definition
|
||||
* (optional) If a field storage definition is specified, only indexes and
|
||||
* keys involving its columns will be processed. Otherwise all defined
|
||||
* entity indexes and keys will be processed.
|
||||
*/
|
||||
protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
|
||||
$schema_handler = $this->database->schema();
|
||||
|
||||
if ($storage_definition) {
|
||||
$table_mapping = $this->storage->getTableMapping();
|
||||
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
|
||||
}
|
||||
|
||||
$index_keys = [
|
||||
'indexes' => 'addIndex',
|
||||
'unique keys' => 'addUniqueKey',
|
||||
];
|
||||
|
||||
foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
|
||||
// Add fields schema because database driver may depend on this data to
|
||||
// perform index normalization.
|
||||
$schema['fields'] = $entity_schema[$table_name]['fields'];
|
||||
|
||||
foreach ($index_keys as $key => $add_method) {
|
||||
if (!empty($schema[$key])) {
|
||||
foreach ($schema[$key] as $name => $specifier) {
|
||||
// If a set of field columns were specified we process only indexes
|
||||
// involving them. Only indexes for which all columns exist are
|
||||
// actually created.
|
||||
$create = FALSE;
|
||||
$specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
|
||||
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
|
||||
$create = TRUE;
|
||||
foreach ($specifier_columns as $specifier_column_name) {
|
||||
// This may happen when adding more than one field in the same
|
||||
// update run. Eventually when all field columns have been
|
||||
// created the index will be created too.
|
||||
if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
|
||||
$create = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($create) {
|
||||
$this->{$add_method}($table_name, $name, $specifier, $schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified entity schema indexes and keys.
|
||||
*
|
||||
* @param array $entity_schema_data
|
||||
* The entity schema data definition.
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface|NULL $storage_definition
|
||||
* (optional) If a field storage definition is specified, only indexes and
|
||||
* keys involving its columns will be processed. Otherwise all defined
|
||||
* entity indexes and keys will be processed.
|
||||
*/
|
||||
protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
|
||||
$schema_handler = $this->database->schema();
|
||||
|
||||
if ($storage_definition) {
|
||||
$table_mapping = $this->storage->getTableMapping();
|
||||
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
|
||||
}
|
||||
|
||||
$index_keys = [
|
||||
'indexes' => 'dropIndex',
|
||||
'unique keys' => 'dropUniqueKey',
|
||||
];
|
||||
|
||||
foreach ($entity_schema_data as $table_name => $schema) {
|
||||
foreach ($index_keys as $key => $drop_method) {
|
||||
if (!empty($schema[$key])) {
|
||||
foreach ($schema[$key] as $name => $specifier) {
|
||||
$specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
|
||||
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
|
||||
$schema_handler->{$drop_method}($table_name, $name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a field property has NULL values.
|
||||
*
|
||||
* @param string $table_name
|
||||
* The name of the table to inspect.
|
||||
* @param string $column_name
|
||||
* The name of the column holding the field property data.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if NULL data is found, FALSE otherwise.
|
||||
*/
|
||||
protected function hasNullFieldPropertyData($table_name, $column_name) {
|
||||
$query = $this->database->select($table_name, 't')
|
||||
->fields('t', [$column_name])
|
||||
->range(0, 1);
|
||||
$query->isNull('t.' . $column_name);
|
||||
$result = $query->execute()->fetchAssoc();
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the schema for a single field definition.
|
||||
*
|
||||
|
@ -1738,6 +1876,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
|
||||
return $storage_definition->getName() . '_' . $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a database table is non-existent or empty.
|
||||
*
|
||||
|
@ -1758,4 +1897,91 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
->fetchField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares schemas to check for changes in the column definitions.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
|
||||
* Current field storage definition.
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
|
||||
* The original field storage definition.
|
||||
*
|
||||
* @return bool
|
||||
* Returns TRUE if there are schema changes in the column definitions.
|
||||
*/
|
||||
protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
|
||||
if ($storage_definition->getColumns() != $original->getColumns()) {
|
||||
// Base field definitions have schema data stored in the original
|
||||
// definition.
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (!$storage_definition->hasCustomStorage()) {
|
||||
$keys = array_flip($this->getColumnSchemaRelevantKeys());
|
||||
$definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
|
||||
foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
|
||||
foreach ($table_schema['fields'] as $name => $spec) {
|
||||
$definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
|
||||
$stored_spec = array_intersect_key($spec, $keys);
|
||||
if ($definition_spec != $stored_spec) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of column schema keys affecting data storage.
|
||||
*
|
||||
* When comparing schema definitions, only changes in certain properties
|
||||
* actually affect how data is stored and thus, if applied, may imply data
|
||||
* manipulation.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of key names.
|
||||
*/
|
||||
protected function getColumnSchemaRelevantKeys() {
|
||||
return ['type', 'size', 'length', 'unsigned'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an index, dropping it if already existing.
|
||||
*
|
||||
* @param string $table
|
||||
* The table name.
|
||||
* @param string $name
|
||||
* The index name.
|
||||
* @param array $specifier
|
||||
* The fields to index.
|
||||
* @param array $schema
|
||||
* The table specification.
|
||||
*
|
||||
* @see \Drupal\Core\Database\Schema::addIndex()
|
||||
*/
|
||||
protected function addIndex($table, $name, array $specifier, array $schema) {
|
||||
$schema_handler = $this->database->schema();
|
||||
$schema_handler->dropIndex($table, $name);
|
||||
$schema_handler->addIndex($table, $name, $specifier, $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique key, dropping it if already existing.
|
||||
*
|
||||
* @param string $table
|
||||
* The table name.
|
||||
* @param string $name
|
||||
* The index name.
|
||||
* @param array $specifier
|
||||
* The unique fields.
|
||||
*
|
||||
* @see \Drupal\Core\Database\Schema::addUniqueKey()
|
||||
*/
|
||||
protected function addUniqueKey($table, $name, array $specifier) {
|
||||
$schema_handler = $this->database->schema();
|
||||
$schema_handler->dropUniqueKey($table, $name);
|
||||
$schema_handler->addUniqueKey($table, $name, $specifier);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -101,6 +101,13 @@ class AjaxResponseSubscriber implements EventSubscriberInterface {
|
|||
// @see https://www.drupal.org/node/1009382
|
||||
$response->setContent('<textarea>' . $response->getContent() . '</textarea>');
|
||||
}
|
||||
|
||||
// User-uploaded files cannot set any response headers, so a custom header
|
||||
// is used to indicate to ajax.js that this response is safe. Note that
|
||||
// most Ajax requests bound using the Form API will be protected by having
|
||||
// the URL flagged as trusted in Drupal.settings, so this header is used
|
||||
// only for things like custom markup that gets Ajax behaviors attached.
|
||||
$response->headers->set('X-Drupal-Ajax-Token', 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -161,7 +161,7 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
|
|||
// just log it. The DefaultExceptionSubscriber will catch the original
|
||||
// exception and handle it normally.
|
||||
$error = Error::decodeException($e);
|
||||
$this->logger->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error);
|
||||
$this->logger->log($error['severity_level'], '%type: @message in %function (line %line of %file).', $error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,12 +91,20 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface {
|
|||
if (substr($error['%file'], 0, $root_length) == DRUPAL_ROOT) {
|
||||
$error['%file'] = substr($error['%file'], $root_length + 1);
|
||||
}
|
||||
// Do not translate the string to avoid errors producing more errors.
|
||||
unset($error['backtrace']);
|
||||
$message = SafeMarkup::format('%type: !message in %function (line %line of %file).', $error);
|
||||
|
||||
// Check if verbose error reporting is on.
|
||||
if ($this->getErrorLevel() == ERROR_REPORTING_DISPLAY_VERBOSE) {
|
||||
unset($error['backtrace']);
|
||||
|
||||
if ($this->getErrorLevel() != ERROR_REPORTING_DISPLAY_VERBOSE) {
|
||||
// Without verbose logging, use a simple message.
|
||||
|
||||
// We call SafeMarkup::format directly here, rather than use t() since
|
||||
// we are in the middle of error handling, and we don't want t() to
|
||||
// cause further errors.
|
||||
$message = SafeMarkup::format('%type: @message in %function (line %line of %file).', $error);
|
||||
}
|
||||
else {
|
||||
// With verbose logging, we will also include a backtrace.
|
||||
|
||||
$backtrace_exception = $exception;
|
||||
while ($backtrace_exception->getPrevious()) {
|
||||
$backtrace_exception = $backtrace_exception->getPrevious();
|
||||
|
@ -108,9 +116,9 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface {
|
|||
// once more in the backtrace.
|
||||
array_shift($backtrace);
|
||||
|
||||
// Generate a backtrace containing only scalar argument values. Make
|
||||
// sure the backtrace is escaped as it can contain user submitted data.
|
||||
$message .= '<pre class="backtrace">' . SafeMarkup::escape(Error::formatBacktrace($backtrace)) . '</pre>';
|
||||
// Generate a backtrace containing only scalar argument values.
|
||||
$error['@backtrace'] = Error::formatBacktrace($backtrace);
|
||||
$message = SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ class ExceptionLoggingSubscriber implements EventSubscriberInterface {
|
|||
public function onError(GetResponseForExceptionEvent $event) {
|
||||
$exception = $event->getException();
|
||||
$error = Error::decodeException($exception);
|
||||
$this->logger->get('php')->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error);
|
||||
$this->logger->get('php')->log($error['severity_level'], '%type: @message in %function (line %line of %file).', $error);
|
||||
|
||||
$is_critical = !$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500;
|
||||
if ($is_critical) {
|
||||
|
|
|
@ -51,7 +51,7 @@ class ExceptionTestSiteSubscriber extends HttpExceptionSubscriberBase {
|
|||
// as it uniquely identifies each PHP error.
|
||||
static $number = 0;
|
||||
$assertion = array(
|
||||
$error['!message'],
|
||||
$error['@message'],
|
||||
$error['%type'],
|
||||
array(
|
||||
'function' => $error['%function'],
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
namespace Drupal\Core\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
@ -79,7 +79,7 @@ class Fast404ExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
|
|||
if ($config->get('fast_404.enabled') && $exclude_paths && !preg_match($exclude_paths, $request->getPathInfo())) {
|
||||
$fast_paths = $config->get('fast_404.paths');
|
||||
if ($fast_paths && preg_match($fast_paths, $request->getPathInfo())) {
|
||||
$fast_404_html = strtr($config->get('fast_404.html'), ['@path' => SafeMarkup::checkPlain($request->getUri())]);
|
||||
$fast_404_html = strtr($config->get('fast_404.html'), ['@path' => Html::escape($request->getUri())]);
|
||||
$response = new Response($fast_404_html, Response::HTTP_NOT_FOUND);
|
||||
$event->setResponse($response);
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
|
|||
/**
|
||||
* Sets extra headers on successful responses.
|
||||
*
|
||||
* @param Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
|
||||
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
|
||||
* The event to process.
|
||||
*/
|
||||
public function onRespond(FilterResponseEvent $event) {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
namespace Drupal\Core\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Cache\CacheableResponseInterface;
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
@ -90,7 +92,14 @@ class MainContentViewSubscriber implements EventSubscriberInterface {
|
|||
$wrapper = isset($this->mainContentRenderers[$wrapper]) ? $wrapper : 'html';
|
||||
|
||||
$renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$wrapper]);
|
||||
$event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch));
|
||||
$response = $renderer->renderResponse($result, $request, $this->routeMatch);
|
||||
// The main content render array is rendered into a different Response
|
||||
// object, depending on the specified wrapper format.
|
||||
if ($response instanceof CacheableResponseInterface) {
|
||||
$main_content_view_subscriber_cacheability = (new CacheableMetadata())->setCacheContexts(['url.query_args:' . static::WRAPPER_FORMAT]);
|
||||
$response->addCacheableDependency($main_content_view_subscriber_cacheability);
|
||||
}
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,14 +44,10 @@ class SpecialAttributesRouteSubscriber extends RouteSubscriberBase {
|
|||
*
|
||||
* @param \Drupal\Core\Routing\RouteBuildEvent $event
|
||||
* The route build event.
|
||||
*
|
||||
* @return bool
|
||||
* Returns TRUE if the variables were successfully replaced, otherwise
|
||||
* FALSE.
|
||||
*/
|
||||
public function onAlterRoutes(RouteBuildEvent $event) {
|
||||
$collection = $event->getRouteCollection();
|
||||
return $this->alterRoutes($collection);
|
||||
$this->alterRoutes($collection);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -166,8 +166,9 @@ class Extension implements \Serializable {
|
|||
* Serializes the Extension object in the most optimized way.
|
||||
*/
|
||||
public function serialize() {
|
||||
// Don't serialize the app root, since this could change if the install is
|
||||
// moved.
|
||||
$data = array(
|
||||
'root' => $this->root,
|
||||
'type' => $this->type,
|
||||
'pathname' => $this->pathname,
|
||||
'filename' => $this->filename,
|
||||
|
@ -188,7 +189,8 @@ class Extension implements \Serializable {
|
|||
*/
|
||||
public function unserialize($data) {
|
||||
$data = unserialize($data);
|
||||
$this->root = $data['root'];
|
||||
// Get the app root from the container.
|
||||
$this->root = DRUPAL_ROOT;
|
||||
$this->type = $data['type'];
|
||||
$this->pathname = $data['pathname'];
|
||||
$this->filename = $data['filename'];
|
||||
|
|
|
@ -7,14 +7,10 @@
|
|||
|
||||
namespace Drupal\Core\Extension;
|
||||
|
||||
use Drupal\Component\Serialization\Yaml;
|
||||
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Parses extension .info.yml files.
|
||||
*/
|
||||
class InfoParser implements InfoParserInterface {
|
||||
class InfoParser extends InfoParserDynamic {
|
||||
|
||||
/**
|
||||
* Array of all info keyed by filename.
|
||||
|
@ -28,38 +24,9 @@ class InfoParser implements InfoParserInterface {
|
|||
*/
|
||||
public function parse($filename) {
|
||||
if (!isset(static::$parsedInfos[$filename])) {
|
||||
if (!file_exists($filename)) {
|
||||
static::$parsedInfos[$filename] = array();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
static::$parsedInfos[$filename] = Yaml::decode(file_get_contents($filename));
|
||||
}
|
||||
catch (InvalidDataTypeException $e) {
|
||||
$message = SafeMarkup::format("Unable to parse !file: !error", array('!file' => $filename, '!error' => $e->getMessage()));
|
||||
throw new InfoParserException($message);
|
||||
}
|
||||
$missing_keys = array_diff($this->getRequiredKeys(), array_keys(static::$parsedInfos[$filename]));
|
||||
if (!empty($missing_keys)) {
|
||||
$message = SafeMarkup::format('Missing required keys (!missing_keys) in !file.', array('!missing_keys' => implode(', ', $missing_keys), '!file' => $filename));
|
||||
throw new InfoParserException($message);
|
||||
}
|
||||
if (isset(static::$parsedInfos[$filename]['version']) && static::$parsedInfos[$filename]['version'] === 'VERSION') {
|
||||
static::$parsedInfos[$filename]['version'] = \Drupal::VERSION;
|
||||
}
|
||||
}
|
||||
static::$parsedInfos[$filename] = parent::parse($filename);
|
||||
}
|
||||
return static::$parsedInfos[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of keys required to exist in .info.yml file.
|
||||
*
|
||||
* @return array
|
||||
* An array of required keys.
|
||||
*/
|
||||
protected function getRequiredKeys() {
|
||||
return array('type', 'core', 'name');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
56
core/lib/Drupal/Core/Extension/InfoParserDynamic.php
Normal file
56
core/lib/Drupal/Core/Extension/InfoParserDynamic.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Extension\InfoParserDynamic.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Extension;
|
||||
|
||||
use Drupal\Component\Serialization\Yaml;
|
||||
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Parses dynamic .info.yml files that might change during the page request.
|
||||
*/
|
||||
class InfoParserDynamic implements InfoParserInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parse($filename) {
|
||||
if (!file_exists($filename)) {
|
||||
$parsed_info = array();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
$parsed_info = Yaml::decode(file_get_contents($filename));
|
||||
}
|
||||
catch (InvalidDataTypeException $e) {
|
||||
$message = SafeMarkup::format("Unable to parse !file: !error", array('!file' => $filename, '!error' => $e->getMessage()));
|
||||
throw new InfoParserException($message);
|
||||
}
|
||||
$missing_keys = array_diff($this->getRequiredKeys(), array_keys($parsed_info));
|
||||
if (!empty($missing_keys)) {
|
||||
$message = SafeMarkup::format('Missing required keys (!missing_keys) in !file.', array('!missing_keys' => implode(', ', $missing_keys), '!file' => $filename));
|
||||
throw new InfoParserException($message);
|
||||
}
|
||||
if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
|
||||
$parsed_info['version'] = \Drupal::VERSION;
|
||||
}
|
||||
}
|
||||
return $parsed_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of keys required to exist in .info.yml file.
|
||||
*
|
||||
* @return array
|
||||
* An array of required keys.
|
||||
*/
|
||||
protected function getRequiredKeys() {
|
||||
return array('type', 'core', 'name');
|
||||
}
|
||||
|
||||
}
|
|
@ -10,9 +10,9 @@ namespace Drupal\Core\Extension;
|
|||
use Drupal\Component\Serialization\Yaml;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Config\PreExistingConfigException;
|
||||
use Drupal\Core\Config\StorageInterface;
|
||||
use Drupal\Core\DrupalKernelInterface;
|
||||
use Drupal\Core\Entity\EntityStorageException;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
|
||||
/**
|
||||
* Default implementation of the module installer.
|
||||
|
@ -212,14 +212,34 @@ class ModuleInstaller implements ModuleInstallerInterface {
|
|||
$version = max(max($versions), $version);
|
||||
}
|
||||
|
||||
// Notify interested components that this module's entity types are new.
|
||||
// For example, a SQL-based storage handler can use this as an
|
||||
// opportunity to create the necessary database tables.
|
||||
// Notify interested components that this module's entity types and
|
||||
// field storage definitions are new. For example, a SQL-based storage
|
||||
// handler can use this as an opportunity to create the necessary
|
||||
// database tables.
|
||||
// @todo Clean this up in https://www.drupal.org/node/2350111.
|
||||
$entity_manager = \Drupal::entityManager();
|
||||
$update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
foreach ($entity_manager->getDefinitions() as $entity_type) {
|
||||
if ($entity_type->getProvider() == $module) {
|
||||
$entity_manager->onEntityTypeCreate($entity_type);
|
||||
$update_manager->installEntityType($entity_type);
|
||||
}
|
||||
elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) {
|
||||
// The module being installed may be adding new fields to existing
|
||||
// entity types. Field definitions for any entity type defined by
|
||||
// the module are handled in the if branch.
|
||||
foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
|
||||
if ($storage_definition->getProvider() == $module) {
|
||||
// If the module being installed is also defining a storage key
|
||||
// for the entity type, the entity schema may not exist yet. It
|
||||
// will be created later in that case.
|
||||
try {
|
||||
$update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,9 +382,25 @@ class ModuleInstaller implements ModuleInstallerInterface {
|
|||
// deleted. For example, a SQL-based storage handler can use this as an
|
||||
// opportunity to drop the corresponding database tables.
|
||||
// @todo Clean this up in https://www.drupal.org/node/2350111.
|
||||
$update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
foreach ($entity_manager->getDefinitions() as $entity_type) {
|
||||
if ($entity_type->getProvider() == $module) {
|
||||
$entity_manager->onEntityTypeDelete($entity_type);
|
||||
$update_manager->uninstallEntityType($entity_type);
|
||||
}
|
||||
elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) {
|
||||
// The module being installed may be adding new fields to existing
|
||||
// entity types. Field definitions for any entity type defined by
|
||||
// the module are handled in the if branch.
|
||||
$entity_type_id = $entity_type->id();
|
||||
/** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
|
||||
$storage = $entity_manager->getStorage($entity_type_id);
|
||||
foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
|
||||
// @todo We need to trigger field purging here.
|
||||
// See https://www.drupal.org/node/2282119.
|
||||
if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
|
||||
$update_manager->uninstallFieldStorageDefinition($storage_definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,55 @@
|
|||
* Hooks related to module and update systems.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Utility\UpdateException;
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\Utility\UpdateException;
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup update_api Update API
|
||||
* @{
|
||||
* Updating minor versions of modules
|
||||
*
|
||||
* When you update code in a module, you may need to update stored data so that
|
||||
* the stored data is compatible with the new code. If this update is between
|
||||
* two minor versions of your module within the same major version of Drupal,
|
||||
* you can use the Update API to update the data. This API is described in brief
|
||||
* here; for more details, see https://www.drupal.org/node/2535316. If you are
|
||||
* updating your module for a major version of Drupal (for instance, Drupal 7 to
|
||||
* Drupal 8), updates will not run and you will need to use the
|
||||
* @link migrate Migrate API @endlink instead.
|
||||
*
|
||||
* @section sec_when When to write update code
|
||||
* You need to provide code that performs an update to stored data whenever your
|
||||
* module makes a change to its data model. A data model change is any change
|
||||
* that makes stored data on an existing site incompatible with that site's
|
||||
* updated codebase. Examples:
|
||||
* - Configuration changes: adding/removing/renaming a config key, changing the
|
||||
* expected data type or value structure, changing dependencies, schema
|
||||
* changes, etc.
|
||||
* - Database schema changes: adding, changing, or removing a database table or
|
||||
* field; moving stored data to different fields or tables; changing the
|
||||
* format of stored data.
|
||||
* - Content entity or field changes: adding, changing, or removing a field
|
||||
* definition, entity definition, or any of their properties.
|
||||
*
|
||||
* @section sec_how How to write update code
|
||||
* Update code for a module is put into an implementation of hook_update_N(),
|
||||
* which goes into file mymodule.install (if your module's machine name is
|
||||
* mymodule). See the documentation of hook_update_N() and
|
||||
* https://www.drupal.org/node/2535316 for details and examples.
|
||||
*
|
||||
* @section sec_test Testing update code
|
||||
* Update code should be tested both manually and by writing an automated test.
|
||||
* Automated tests for update code extend
|
||||
* \Drupal\system\Tests\Update\UpdatePathTestBase -- see that class for details,
|
||||
* and find classes that extend it for examples.
|
||||
*
|
||||
* @see migration
|
||||
* @}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
|
@ -421,94 +468,119 @@ function hook_install_tasks_alter(&$tasks, $install_state) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Perform a single update.
|
||||
* Perform a single update between minor versions.
|
||||
*
|
||||
* For each change that requires one or more actions to be performed when
|
||||
* updating a site, add a new hook_update_N(), which will be called by
|
||||
* update.php. The documentation block preceding this function is stripped of
|
||||
* newlines and used as the description for the update on the pending updates
|
||||
* task list. Schema updates should adhere to the
|
||||
* @link https://www.drupal.org/node/150215 Schema API. @endlink
|
||||
* hook_update_N() can only be used to update between minor versions of a
|
||||
* module. To upgrade between major versions of Drupal (for example, between
|
||||
* Drupal 7 and 8), use the @link migrate Migrate API @endlink instead.
|
||||
*
|
||||
* @section sec_naming Naming and documenting your function
|
||||
* For each change in a module that requires one or more actions to be performed
|
||||
* when updating a site, add a new implementation of hook_update_N() to your
|
||||
* mymodule.install file (assuming mymodule is the machine name of your module).
|
||||
* Implementations of hook_update_N() are named (module name)_update_(number).
|
||||
* The numbers are composed of three parts:
|
||||
* - 1 digit for Drupal core compatibility.
|
||||
* - 1 digit for your module's major release version (e.g., is this the 8.x-1.*
|
||||
* (1) or 8.x-2.* (2) series of your module).
|
||||
* - 2 digits for sequential counting, starting with 01.
|
||||
*
|
||||
* The numbers are normally composed of three parts:
|
||||
* - 1 or 2 digits for Drupal core compatibility (Drupal 8, 9, 10, etc.). This
|
||||
* convention must be followed.
|
||||
* - 1 digit for your module's major release version; for example, for 8.x-1.*
|
||||
* use 1, for 8.x-2.* use 2, for Core 8.0.x use 0, and for Core 8.1.x use 1.
|
||||
* This convention is optional but suggested for clarity.
|
||||
* - 2 digits for sequential counting, starting with 01. Note that the x000
|
||||
* number can never be used: the lowest update number that will be recognized
|
||||
* and run for major version x is x001.
|
||||
* Examples:
|
||||
* - mymodule_update_8100(): This is the first update to get the database ready
|
||||
* to run mymodule 8.x-1.*.
|
||||
* - mymodule_update_8200(): This is the first update to get the database ready
|
||||
* to run mymodule 8.x-2.*.
|
||||
* - node_update_8001(): The first update for the Drupal 8.0.x version of the
|
||||
* Drupal Core node module.
|
||||
* - mymodule_update_8101(): The first update for your custom or contributed
|
||||
* module's 8.x-1.x versions.
|
||||
* - mymodule_update_8201(): The first update for the 8.x-2.x versions.
|
||||
*
|
||||
* As of Drupal 8.0, the database upgrade system no longer supports updating a
|
||||
* database from an earlier major version of Drupal: update.php can be used to
|
||||
* upgrade from 7.x-1.x to 7.x-2.x, or 8.x-1.x to 8.x-2.x, but not from 7.x to
|
||||
* 8.x. Therefore, only update hooks numbered 8001 or later will run for
|
||||
* Drupal 8. 8000 is reserved for the minimum core schema version and defining
|
||||
* mymodule_update_8000() will result in an exception. Use the
|
||||
* @link https://www.drupal.org/node/2127611 Migration API @endlink instead to
|
||||
* migrate data from an earlier major version of Drupal.
|
||||
* Never renumber update functions. The numeric part of the hook implementation
|
||||
* function is stored in the database to keep track of which updates have run,
|
||||
* so it is important to maintain this information consistently.
|
||||
*
|
||||
* For further information about releases and release numbers see:
|
||||
* @link https://www.drupal.org/node/711070 Maintaining a drupal.org project
|
||||
* with Git @endlink
|
||||
* The documentation block preceding this function is stripped of newlines and
|
||||
* used as the description for the update on the pending updates task list,
|
||||
* which users will see when they run the update.php script.
|
||||
*
|
||||
* Never renumber update functions.
|
||||
* @section sec_notes Notes about the function body
|
||||
* Writing hook_update_N() functions is tricky. There are several reasons why
|
||||
* this is the case:
|
||||
* - You do not know when updates will be run: someone could be keeping up with
|
||||
* every update and run them when the database and code are in the same state
|
||||
* as when you wrote your update function, or they could have waited until a
|
||||
* few more updates have come out, and run several at the same time.
|
||||
* - You do not know the state of other modules' updates either.
|
||||
* - Other modules can use hook_update_dependencies() to run updates between
|
||||
* your module's updates, so you also cannot count on your functions running
|
||||
* right after one another.
|
||||
* - You do not know what environment your update will run in (which modules
|
||||
* are installed, whether certain hooks are implemented or not, whether
|
||||
* services are overridden, etc.).
|
||||
*
|
||||
* Implementations of this hook should be placed in a mymodule.install file in
|
||||
* the same directory as mymodule.module. Drupal core's updates are implemented
|
||||
* using the system module as a name and stored in database/updates.inc.
|
||||
* Because of these reasons, you'll need to use care in writing your update
|
||||
* function. Some things to think about:
|
||||
* - Never assume that the database schema is the same when the update will run
|
||||
* as it is when you wrote the update function. So, when updating a database
|
||||
* table or field, put the schema information you want to update to directly
|
||||
* into your function instead of calling your hook_schema() function to
|
||||
* retrieve it (this is one case where the right thing to do is copy and paste
|
||||
* the code).
|
||||
* - Never assume that the configuration schema is the same when the update will
|
||||
* run as it is when you wrote the update function. So, when saving
|
||||
* configuration, use the $has_trusted_data = TRUE parameter so that schema is
|
||||
* ignored, and make sure that the configuration data you are saving matches
|
||||
* the configuration schema at the time when you write the update function
|
||||
* (later updates may change it again to match new schema changes).
|
||||
* - Never assume your field or entity type definitions are the same when the
|
||||
* update will run as they are when you wrote the update function. Always
|
||||
* retrieve the correct version via
|
||||
* \Drupal::entityDefinitionUpdateManager()::getEntityType() or
|
||||
* \Drupal::entityDefinitionUpdateManager()::getFieldStorageDefinition(). When
|
||||
* adding a new definition always replicate it in the update function body as
|
||||
* you would do with a schema definition.
|
||||
* - Never call \Drupal::entityDefinitionUpdateManager()::applyUpdates() in an
|
||||
* update function, as it will apply updates for any module not only yours,
|
||||
* which will lead to unpredictable results.
|
||||
* - Be careful about API functions and especially CRUD operations that you use
|
||||
* in your update function. If they invoke hooks or use services, they may
|
||||
* not behave as expected, and it may actually not be appropriate to use the
|
||||
* normal API functions that invoke all the hooks, use the database schema,
|
||||
* and/or use services in an update function -- you may need to switch to
|
||||
* using a more direct method (database query, etc.).
|
||||
* - In particular, loading, saving, or performing any other CRUD operation on
|
||||
* an entity is never safe to do (they always involve hooks and services).
|
||||
* - Never rebuild the router during an update function.
|
||||
*
|
||||
* Not all module functions are available from within a hook_update_N() function.
|
||||
* In order to call a function from your mymodule.module or an include file,
|
||||
* you need to explicitly load that file first.
|
||||
*
|
||||
* Implementations must ensure that APIs used are safe during updates. During
|
||||
* database updates the schema of any module could be out of date. For this
|
||||
* reason, caution is needed when using any API function within an update
|
||||
* function - particularly CRUD functions, functions that depend on the schema
|
||||
* (for example by using \Drupal\Core\Entity\Entity::save()), and any functions
|
||||
* that invoke hooks.
|
||||
*
|
||||
* The following actions are examples that are safe:
|
||||
* The following actions are examples of things that are safe to do during
|
||||
* updates:
|
||||
* - Cache invalidation.
|
||||
* - Using \Drupal::configFactory()->getEditable() and \Drupal::config().
|
||||
* Implementations must:
|
||||
* - Not make any assumption that the config data is valid.
|
||||
* - Use the correct data type when changing configuration values as specified
|
||||
* by its configuration schema at the time the update hook is written. If
|
||||
* the data type changes in a subsequent code change, a subsequent update
|
||||
* hook is responsible for ensuring the final data type aligns with the
|
||||
* configuration schema.
|
||||
* - Use the $has_trusted_data argument for \Drupal\Core\Config\Config::save()
|
||||
* so that configuration schemas are not used whilst saving configuration.
|
||||
* - Using \Drupal::configFactory()->getEditable() and \Drupal::config(), as
|
||||
* long as you make sure that your update data matches the schema, and you
|
||||
* use the $has_trusted_data argument in the save operation.
|
||||
* - Marking a container for rebuild.
|
||||
* - Using the API provided by \Drupal::entityDefinitionUpdateManager() to
|
||||
* update the entity schema based on changes in entity type or field
|
||||
* definitions provided by your module.
|
||||
*
|
||||
* The following actions are examples that are unsafe:
|
||||
* - Loading, saving, or performing any other operation on an entity.
|
||||
* - Rebuilding the router using \Drupal::service('router.builder')->rebuild().
|
||||
* See https://www.drupal.org/node/2535316 for more on writing update functions.
|
||||
*
|
||||
* The $sandbox parameter should be used when a multipass update is needed, in
|
||||
* circumstances where running the whole update at once could cause PHP to
|
||||
* timeout. Each pass is run in a way that avoids PHP timeouts, provided each
|
||||
* pass remains under the timeout limit. To signify that an update requires
|
||||
* at least one more pass, set $sandbox['#finished'] to a number less than 1
|
||||
* (you need to do this each pass). The value of $sandbox['#finished'] will be
|
||||
* unset between passes but all other data in $sandbox will be preserved. The
|
||||
* system will stop iterating this update when $sandbox['#finished'] is left
|
||||
* unset or set to a number higher than 1. It is recommended that
|
||||
* $sandbox['#finished'] is initially set to 0, and then updated each pass to a
|
||||
* number between 0 and 1 that represents the overall % completed for this
|
||||
* update, finishing with 1.
|
||||
* @section sec_bulk Batch updates
|
||||
* If running your update all at once could possibly cause PHP to time out, use
|
||||
* the $sandbox parameter to indicate that the Batch API should be used for your
|
||||
* update. In this case, your update function acts as an implementation of
|
||||
* callback_batch_operation(), and $sandbox acts as the batch context
|
||||
* parameter. In your function, read the state information from the previous
|
||||
* run from $sandbox (or initialize), run a chunk of updates, save the state in
|
||||
* $sandbox, and set $sandbox['#finished'] to a value between 0 and 1 to
|
||||
* indicate the percent completed, or 1 if it is finished (you need to do this
|
||||
* explicitly in each pass).
|
||||
*
|
||||
* See the @link batch Batch operations topic @endlink for more information on
|
||||
* how to use the Batch API.
|
||||
*
|
||||
* @param array $sandbox
|
||||
* Stores information for multipass updates. See above for more information.
|
||||
* Stores information for batch updates. See above for more information.
|
||||
*
|
||||
* @throws \Drupal\Core\Utility\UpdateException|PDOException
|
||||
* In case of error, update hooks should throw an instance of
|
||||
|
@ -521,58 +593,70 @@ function hook_install_tasks_alter(&$tasks, $install_state) {
|
|||
* displayed to the user after the update has completed. If no message is
|
||||
* returned, no message will be presented to the user.
|
||||
*
|
||||
* @ingroup update_api
|
||||
*
|
||||
* @see batch
|
||||
* @see schemaapi
|
||||
* @see hook_update_last_removed()
|
||||
* @see update_get_update_list()
|
||||
* @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
|
||||
* @see node_update_8001
|
||||
* @see system_update_8004
|
||||
* @see https://www.drupal.org/node/2535316
|
||||
*/
|
||||
function hook_update_N(&$sandbox) {
|
||||
// For non-multipass updates, the signature can simply be;
|
||||
// For non-batch updates, the signature can simply be:
|
||||
// function hook_update_N() {
|
||||
|
||||
// For most updates, the following is sufficient.
|
||||
db_add_field('mytable1', 'newcol', array('type' => 'int', 'not null' => TRUE, 'description' => 'My new integer column.'));
|
||||
// Example function body for adding a field to a database table, which does
|
||||
// not require a batch operation:
|
||||
$spec = array(
|
||||
'type' => 'varchar',
|
||||
'description' => "New Col",
|
||||
'length' => 20,
|
||||
'not null' => FALSE,
|
||||
);
|
||||
$schema = Database::getConnection()->schema();
|
||||
$schema->addField('mytable1', 'newcol', $spec);
|
||||
|
||||
// However, for more complex operations that may take a long time,
|
||||
// you may hook into Batch API as in the following example.
|
||||
|
||||
// Update 3 users at a time to have an exclamation point after their names.
|
||||
// (They're really happy that we can do batch API in this hook!)
|
||||
if (!isset($sandbox['progress'])) {
|
||||
$sandbox['progress'] = 0;
|
||||
$sandbox['current_uid'] = 0;
|
||||
// We'll -1 to disregard the uid 0...
|
||||
$sandbox['max'] = db_query('SELECT COUNT(DISTINCT uid) FROM {users}')->fetchField() - 1;
|
||||
// Example of what to do if there is an error during your update.
|
||||
if ($some_error_condition_met) {
|
||||
throw new UpdateException('Something went wrong; here is what you should do.');
|
||||
}
|
||||
|
||||
$users = db_select('users', 'u')
|
||||
->fields('u', array('uid', 'name'))
|
||||
->condition('uid', $sandbox['current_uid'], '>')
|
||||
->range(0, 3)
|
||||
->orderBy('uid', 'ASC')
|
||||
->execute();
|
||||
// Example function body for a batch update. In this example, the values in
|
||||
// a database field are updated.
|
||||
if (!isset($sandbox['progress'])) {
|
||||
// This must be the first run. Initialize the sandbox.
|
||||
$sandbox['progress'] = 0;
|
||||
$sandbox['current_pk'] = 0;
|
||||
$sandbox['max'] = Database::getConnection()->query('SELECT COUNT(myprimarykey) FROM {mytable1}')->fetchField() - 1;
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->setUsername($user->getUsername() . '!');
|
||||
db_update('users')
|
||||
->fields(array('name' => $user->getUsername()))
|
||||
->condition('uid', $user->id())
|
||||
// Update in chunks of 20.
|
||||
$records = Database::getConnection()->select('mytable1', 'm')
|
||||
->fields('m', array('myprimarykey', 'otherfield'))
|
||||
->condition('myprimarykey', $sandbox['current_pk'], '>')
|
||||
->range(0, 20)
|
||||
->orderBy('myprimarykey', 'ASC')
|
||||
->execute();
|
||||
foreach ($records as $record) {
|
||||
// Here, you would make an update something related to this record. In this
|
||||
// example, some text is added to the other field.
|
||||
Database::getConnection()->update('mytable1')
|
||||
->fields(array('otherfield' => $record->otherfield . '-suffix'))
|
||||
->condition('myprimarykey', $record->myprimarykey)
|
||||
->execute();
|
||||
|
||||
$sandbox['progress']++;
|
||||
$sandbox['current_uid'] = $user->id();
|
||||
$sandbox['current_pk'] = $record->myprimarykey;
|
||||
}
|
||||
|
||||
$sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
|
||||
|
||||
if ($some_error_condition_met) {
|
||||
// In case of an error, simply throw an exception with an error message.
|
||||
throw new UpdateException('Something went wrong; here is what you should do.');
|
||||
}
|
||||
|
||||
// To display a message to the user when the update is completed, return it.
|
||||
// If you do not want to display a completion message, simply return nothing.
|
||||
return t('The update did what it was supposed to do.');
|
||||
// If you do not want to display a completion message, return nothing.
|
||||
return t('All foo bars were updated with the new suffix');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -597,6 +681,8 @@ function hook_update_N(&$sandbox) {
|
|||
* you should always list the highest numbered one here (since updates within
|
||||
* a given module always run in numerical order).
|
||||
*
|
||||
* @ingroup update_api
|
||||
*
|
||||
* @see update_resolve_dependencies()
|
||||
* @see hook_update_N()
|
||||
*/
|
||||
|
@ -634,6 +720,8 @@ function hook_update_dependencies() {
|
|||
* An integer, corresponding to hook_update_N() which has been removed from
|
||||
* mymodule.install.
|
||||
*
|
||||
* @ingroup update_api
|
||||
*
|
||||
* @see hook_update_N()
|
||||
*/
|
||||
function hook_update_last_removed() {
|
||||
|
@ -663,6 +751,8 @@ function hook_update_last_removed() {
|
|||
* doesn't matter, but if you need to override an existing Updater, make
|
||||
* sure your Updater has a lighter weight so that it comes first.
|
||||
*
|
||||
* @ingroup update_api
|
||||
*
|
||||
* @see drupal_get_updaters()
|
||||
* @see hook_updater_info_alter()
|
||||
*/
|
||||
|
@ -692,6 +782,8 @@ function hook_updater_info() {
|
|||
* Associative array of updaters as defined through hook_updater_info().
|
||||
* Alter this array directly.
|
||||
*
|
||||
* @ingroup update_api
|
||||
*
|
||||
* @see drupal_get_updaters()
|
||||
* @see hook_updater_info()
|
||||
*/
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
|
||||
namespace Drupal\Core\Field;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\Xss;
|
||||
|
||||
/**
|
||||
* Useful methods when dealing with displaying allowed tags.
|
||||
*
|
||||
* @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
|
||||
* \Drupal\Core\Field\FieldFilteredString instead.
|
||||
*
|
||||
* @see \Drupal\Core\Field\FieldFilteredString
|
||||
*/
|
||||
trait AllowedTagsXssTrait {
|
||||
|
||||
|
@ -33,30 +34,21 @@ trait AllowedTagsXssTrait {
|
|||
* valid UTF-8.
|
||||
*/
|
||||
public function fieldFilterXss($string) {
|
||||
// All known XSS vectors are filtered out by
|
||||
// \Drupal\Component\Utility\Xss::filter(), all tags in the markup are
|
||||
// allowed intentionally by the trait, and no danger is added in by
|
||||
// \Drupal\Component\Utility\HTML::normalize(). Since the normalized value
|
||||
// is essentially the same markup, designate this string as safe as well.
|
||||
// This method is an internal part of field sanitization, so the resultant,
|
||||
// sanitized string should be printable as is.
|
||||
//
|
||||
// @todo Free this memory in https://www.drupal.org/node/2505963.
|
||||
return SafeMarkup::set(Html::normalize(Xss::filter($string, $this->allowedTags())));
|
||||
return FieldFilteredString::create($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of tags allowed by AllowedTagsXssTrait::fieldFilterXss().
|
||||
*/
|
||||
public function allowedTags() {
|
||||
return array('a', 'b', 'big', 'code', 'del', 'em', 'i', 'ins', 'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img');
|
||||
return FieldFilteredString::allowedTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable list of allowed tags for display in help texts.
|
||||
*/
|
||||
public function displayAllowedTags() {
|
||||
return '<' . implode('> <', $this->allowedTags()) . '>';
|
||||
return FieldFilteredString::displayAllowedTags();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use Drupal\Core\Field\FieldException;
|
|||
* @ConfigEntityType(
|
||||
* id = "base_field_override",
|
||||
* label = @Translation("Base field override"),
|
||||
* controllers = {
|
||||
* handlers = {
|
||||
* "storage" = "Drupal\Core\Field\BaseFieldOverrideStorage"
|
||||
* },
|
||||
* config_prefix = "base_field_override",
|
||||
|
|
|
@ -248,15 +248,10 @@ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigIn
|
|||
// @see \Drupal\Core\Field\FieldItemInterface::calculateDependencies()
|
||||
$this->addDependencies($definition['class']::calculateDependencies($this));
|
||||
|
||||
// If the target entity type uses entities to manage its bundles then
|
||||
// depend on the bundle entity.
|
||||
$bundle_entity_type_id = $this->entityManager()->getDefinition($this->entity_type)->getBundleEntityType();
|
||||
if ($bundle_entity_type_id != 'bundle') {
|
||||
if (!$bundle_entity = $this->entityManager()->getStorage($bundle_entity_type_id)->load($this->bundle)) {
|
||||
throw new \LogicException("Missing bundle entity, entity type {$bundle_entity_type_id}, entity id {$this->bundle}.");
|
||||
}
|
||||
$this->addDependency('config', $bundle_entity->getConfigDependencyName());
|
||||
}
|
||||
// Create dependency on the bundle.
|
||||
$bundle_config_dependency = $this->entityManager()->getDefinition($this->entity_type)->getBundleConfigDependency($this->bundle);
|
||||
$this->addDependency($bundle_config_dependency['type'], $bundle_config_dependency['name']);
|
||||
|
||||
return $this->dependencies;
|
||||
}
|
||||
|
||||
|
|
78
core/lib/Drupal/Core/Field/FieldFilteredString.php
Normal file
78
core/lib/Drupal/Core/Field/FieldFilteredString.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Field\FieldFilteredString.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Field;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\SafeStringInterface;
|
||||
use Drupal\Component\Utility\SafeStringTrait;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\Component\Utility\Xss;
|
||||
|
||||
/**
|
||||
* Defines an object that passes safe strings through the Field system.
|
||||
*
|
||||
* This object filters the string using a very restrictive tag list when it is
|
||||
* created.
|
||||
*
|
||||
* @internal
|
||||
* This object is marked as internal because it should only be used by the
|
||||
* Field module and field-related plugins.
|
||||
*
|
||||
* @see \Drupal\Core\Render\SafeString
|
||||
*/
|
||||
final class FieldFilteredString implements SafeStringInterface, \Countable {
|
||||
use SafeStringTrait;
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\Component\Utility\SafeStringTrait::create().
|
||||
*
|
||||
* @return string|\Drupal\Component\Utility\SafeStringInterface
|
||||
* A safe string filtered with the allowed tag list and normalized.
|
||||
*
|
||||
* @see \Drupal\Core\Field\FieldFilteredString::allowedTags()
|
||||
* @see \Drupal\Component\Utility\Xss::filter()
|
||||
* @see \Drupal\Component\Utility\Html::normalize()
|
||||
*/
|
||||
public static function create($string) {
|
||||
$string = (string) $string;
|
||||
if ($string === '') {
|
||||
return '';
|
||||
}
|
||||
$safe_string = new static();
|
||||
// All known XSS vectors are filtered out by
|
||||
// \Drupal\Component\Utility\Xss::filter(), all tags in the markup are
|
||||
// allowed intentionally by the trait, and no danger is added in by
|
||||
// \Drupal\Component\Utility\HTML::normalize(). Since the normalized value
|
||||
// is essentially the same markup, designate this string as safe as well.
|
||||
// This method is an internal part of field sanitization, so the resultant,
|
||||
// sanitized string should be printable as is.
|
||||
$safe_string->string = Html::normalize(Xss::filter($string, static::allowedTags()));
|
||||
return $safe_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allowed tag list.
|
||||
*
|
||||
* @return string[]
|
||||
* A list of allowed tags.
|
||||
*/
|
||||
public static function allowedTags() {
|
||||
return ['a', 'b', 'big', 'code', 'del', 'em', 'i', 'ins', 'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable list of allowed tags for display in help texts.
|
||||
*
|
||||
* @return string
|
||||
* A human-readable list of allowed tags for display in help texts.
|
||||
*/
|
||||
public static function displayAllowedTags() {
|
||||
return '<' . implode('> <', static::allowedTags()) . '>';
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Field\FormatterBase;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
|
||||
|
@ -37,7 +37,7 @@ class BasicStringFormatter extends FormatterBase {
|
|||
foreach ($items as $delta => $item) {
|
||||
// The text value has no text format assigned to it, so the user input
|
||||
// should equal the output, including newlines.
|
||||
$elements[$delta] = array('#markup' => nl2br(SafeMarkup::checkPlain($item->value)));
|
||||
$elements[$delta] = array('#markup' => nl2br(Html::escape($item->value)));
|
||||
}
|
||||
|
||||
return $elements;
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'entity reference ID' formatter.
|
||||
|
@ -33,7 +32,7 @@ class EntityReferenceIdFormatter extends EntityReferenceFormatterBase {
|
|||
foreach ($this->getEntitiesToView($items) as $delta => $entity) {
|
||||
if ($entity->id()) {
|
||||
$elements[$delta] = array(
|
||||
'#markup' => SafeMarkup::checkPlain($entity->id()),
|
||||
'#plain_text' => $entity->id(),
|
||||
// Create a cache tag entry for the referenced entity. In the case
|
||||
// that the referenced entity is deleted, the cache for referring
|
||||
// entities must be cleared.
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
@ -98,7 +97,7 @@ class EntityReferenceLabelFormatter extends EntityReferenceFormatterBase {
|
|||
}
|
||||
}
|
||||
else {
|
||||
$elements[$delta] = array('#markup' => SafeMarkup::checkPlain($label));
|
||||
$elements[$delta] = array('#plain_text' => $label);
|
||||
}
|
||||
$elements[$delta]['#cache']['tags'] = $entity->getCacheTags();
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
|
@ -121,8 +121,10 @@ class LanguageFormatter extends StringFormatter {
|
|||
// either displayed in its configured form (loaded directly from config
|
||||
// storage by LanguageManager::getLanguages()) or in its native language
|
||||
// name. That only depends on formatter settings and no language condition.
|
||||
$languages = $this->getSetting('native_language') ? $this->languageManager->getNativeLanguages() : $this->languageManager->getLanguages();
|
||||
return $item->language ? SafeMarkup::checkPlain($languages[$item->language->getId()]->getName()) : '';
|
||||
$languages = $this->getSetting('native_language') ? $this->languageManager->getNativeLanguages(LanguageInterface::STATE_ALL) : $this->languageManager->getLanguages(LanguageInterface::STATE_ALL);
|
||||
return [
|
||||
'#plain_text' => $item->language && isset($languages[$item->language->getId()]) ? $languages[$item->language->getId()]->getName() : ''
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -75,8 +75,8 @@ abstract class NumericFormatterBase extends FormatterBase {
|
|||
|
||||
// Account for prefix and suffix.
|
||||
if ($this->getSetting('prefix_suffix')) {
|
||||
$prefixes = isset($settings['prefix']) ? array_map(array($this, 'fieldFilterXss'), explode('|', $settings['prefix'])) : array('');
|
||||
$suffixes = isset($settings['suffix']) ? array_map(array($this, 'fieldFilterXss'), explode('|', $settings['suffix'])) : array('');
|
||||
$prefixes = isset($settings['prefix']) ? array_map(array('Drupal\Core\Field\FieldFilteredString', 'create'), explode('|', $settings['prefix'])) : array('');
|
||||
$suffixes = isset($settings['suffix']) ? array_map(array('Drupal\Core\Field\FieldFilteredString', 'create'), explode('|', $settings['suffix'])) : array('');
|
||||
$prefix = (count($prefixes) > 1) ? $this->formatPlural($item->value, $prefixes[0], $prefixes[1]) : $prefixes[0];
|
||||
$suffix = (count($suffixes) > 1) ? $this->formatPlural($item->value, $suffixes[0], $suffixes[1]) : $suffixes[0];
|
||||
$output = $prefix . $output . $suffix;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Entity\RevisionableInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
|
@ -128,16 +128,16 @@ class StringFormatter extends FormatterBase implements ContainerFactoryPluginInt
|
|||
}
|
||||
|
||||
foreach ($items as $delta => $item) {
|
||||
$string = $this->viewValue($item);
|
||||
$view_value = $this->viewValue($item);
|
||||
if ($url) {
|
||||
$elements[$delta] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $string,
|
||||
'#title' => $view_value,
|
||||
'#url' => $url,
|
||||
];
|
||||
}
|
||||
else {
|
||||
$elements[$delta] = ['#markup' => $string];
|
||||
$elements[$delta] = is_array($view_value) ? $view_value : ['#markup' => $view_value];
|
||||
}
|
||||
}
|
||||
return $elements;
|
||||
|
@ -149,13 +149,15 @@ class StringFormatter extends FormatterBase implements ContainerFactoryPluginInt
|
|||
* @param \Drupal\Core\Field\FieldItemInterface $item
|
||||
* One field item.
|
||||
*
|
||||
* @return string
|
||||
* The textual output generated.
|
||||
* @return array
|
||||
* The textual output generated as a render array.
|
||||
*/
|
||||
protected function viewValue(FieldItemInterface $item) {
|
||||
// The text value has no text format assigned to it, so the user input
|
||||
// should equal the output, including newlines.
|
||||
return nl2br(SafeMarkup::checkPlain($item->value));
|
||||
return [
|
||||
'#markup' => nl2br(Html::escape($item->value))
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue