composer update
This commit is contained in:
parent
f6abc3dce2
commit
71dfaca858
1753 changed files with 45274 additions and 14619 deletions
|
@ -284,9 +284,9 @@ Menu UI
|
|||
Migrate
|
||||
- Adam Globus-Hoenich 'phenaproxima' https://www.drupal.org/u/phenaproxima
|
||||
- Lucas Hedding 'heddn' https://www.drupal.org/u/heddn
|
||||
- Michael Lutz 'mikelutz' https://www.drupal.org/u/mikelutz
|
||||
- Markus Sipilä 'masipila' https://www.drupal.org/u/masipila
|
||||
- Vicki Spagnolo 'quietone' https://www.drupal.org/u/quietone
|
||||
- Maxime Turcotte 'maxocub' https://www.drupal.org/u/maxocub
|
||||
|
||||
Node
|
||||
- Tim Millwood 'timmillwood' https://www.drupal.org/u/timmillwood
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"symfony/process": "~3.4.0",
|
||||
"symfony/polyfill-iconv": "^1.0",
|
||||
"symfony/yaml": "~3.4.5",
|
||||
"typo3/phar-stream-wrapper": "^2.0.1",
|
||||
"twig/twig": "^1.35.0",
|
||||
"doctrine/common": "^2.5",
|
||||
"doctrine/annotations": "^1.2",
|
||||
|
|
|
@ -251,42 +251,46 @@ function check_url($uri) {
|
|||
* A translated string representation of the size.
|
||||
*/
|
||||
function format_size($size, $langcode = NULL) {
|
||||
if ($size < Bytes::KILOBYTE) {
|
||||
$absolute_size = abs($size);
|
||||
if ($absolute_size < Bytes::KILOBYTE) {
|
||||
return \Drupal::translation()->formatPlural($size, '1 byte', '@count bytes', [], ['langcode' => $langcode]);
|
||||
}
|
||||
else {
|
||||
// Convert bytes to kilobytes.
|
||||
$size = $size / Bytes::KILOBYTE;
|
||||
$units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
foreach ($units as $unit) {
|
||||
if (round($size, 2) >= Bytes::KILOBYTE) {
|
||||
$size = $size / Bytes::KILOBYTE;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$args = ['@size' => round($size, 2)];
|
||||
$options = ['langcode' => $langcode];
|
||||
switch ($unit) {
|
||||
case 'KB':
|
||||
return new TranslatableMarkup('@size KB', $args, $options);
|
||||
case 'MB':
|
||||
return new TranslatableMarkup('@size MB', $args, $options);
|
||||
case 'GB':
|
||||
return new TranslatableMarkup('@size GB', $args, $options);
|
||||
case 'TB':
|
||||
return new TranslatableMarkup('@size TB', $args, $options);
|
||||
case 'PB':
|
||||
return new TranslatableMarkup('@size PB', $args, $options);
|
||||
case 'EB':
|
||||
return new TranslatableMarkup('@size EB', $args, $options);
|
||||
case 'ZB':
|
||||
return new TranslatableMarkup('@size ZB', $args, $options);
|
||||
case 'YB':
|
||||
return new TranslatableMarkup('@size YB', $args, $options);
|
||||
// Create a multiplier to preserve the sign of $size.
|
||||
$sign = $absolute_size / $size;
|
||||
foreach (['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] as $unit) {
|
||||
$absolute_size /= Bytes::KILOBYTE;
|
||||
$rounded_size = round($absolute_size, 2);
|
||||
if ($rounded_size < Bytes::KILOBYTE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$args = ['@size' => $rounded_size * $sign];
|
||||
$options = ['langcode' => $langcode];
|
||||
switch ($unit) {
|
||||
case 'KB':
|
||||
return new TranslatableMarkup('@size KB', $args, $options);
|
||||
|
||||
case 'MB':
|
||||
return new TranslatableMarkup('@size MB', $args, $options);
|
||||
|
||||
case 'GB':
|
||||
return new TranslatableMarkup('@size GB', $args, $options);
|
||||
|
||||
case 'TB':
|
||||
return new TranslatableMarkup('@size TB', $args, $options);
|
||||
|
||||
case 'PB':
|
||||
return new TranslatableMarkup('@size PB', $args, $options);
|
||||
|
||||
case 'EB':
|
||||
return new TranslatableMarkup('@size EB', $args, $options);
|
||||
|
||||
case 'ZB':
|
||||
return new TranslatableMarkup('@size ZB', $args, $options);
|
||||
|
||||
case 'YB':
|
||||
return new TranslatableMarkup('@size YB', $args, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -490,7 +494,7 @@ function drupal_js_defaults($data = NULL) {
|
|||
*
|
||||
* Every condition is a key/value pair, whose key is a jQuery selector that
|
||||
* denotes another element on the page, and whose value is an array of
|
||||
* conditions, which must bet met on that element:
|
||||
* conditions, which must be met on that element:
|
||||
* @code
|
||||
* array(
|
||||
* 'visible' => array(
|
||||
|
|
|
@ -82,7 +82,7 @@ class Drupal {
|
|||
/**
|
||||
* The current system version.
|
||||
*/
|
||||
const VERSION = '8.6.3';
|
||||
const VERSION = '8.6.7';
|
||||
|
||||
/**
|
||||
* Core API compatibility.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Drupal\Component\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\Context\ContextInterface;
|
||||
use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionInterface;
|
||||
use Drupal\Component\Plugin\Exception\ContextException;
|
||||
use Drupal\Component\Plugin\Context\Context;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
|
@ -67,7 +68,12 @@ abstract class ContextAwarePluginBase extends PluginBase implements ContextAware
|
|||
*/
|
||||
public function getContextDefinitions() {
|
||||
$definition = $this->getPluginDefinition();
|
||||
return !empty($definition['context']) ? $definition['context'] : [];
|
||||
if ($definition instanceof ContextAwarePluginDefinitionInterface) {
|
||||
return $definition->getContextDefinitions();
|
||||
}
|
||||
else {
|
||||
return !empty($definition['context']) ? $definition['context'] : [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,10 +81,15 @@ abstract class ContextAwarePluginBase extends PluginBase implements ContextAware
|
|||
*/
|
||||
public function getContextDefinition($name) {
|
||||
$definition = $this->getPluginDefinition();
|
||||
if (empty($definition['context'][$name])) {
|
||||
throw new ContextException(sprintf("The %s context is not a valid context.", $name));
|
||||
if ($definition instanceof ContextAwarePluginDefinitionInterface) {
|
||||
if ($definition->hasContextDefinition($name)) {
|
||||
return $definition->getContextDefinition($name);
|
||||
}
|
||||
}
|
||||
return $definition['context'][$name];
|
||||
elseif (!empty($definition['context'][$name])) {
|
||||
return $definition['context'][$name];
|
||||
}
|
||||
throw new ContextException(sprintf("The %s context is not a valid context.", $name));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -107,6 +107,29 @@ class PhpTransliteration implements TransliterationInterface {
|
|||
public function transliterate($string, $langcode = 'en', $unknown_character = '?', $max_length = NULL) {
|
||||
$result = '';
|
||||
$length = 0;
|
||||
$hash = FALSE;
|
||||
|
||||
// Replace question marks with a unique hash if necessary. This because
|
||||
// mb_convert_encoding() replaces all invalid characters with a question
|
||||
// mark.
|
||||
if ($unknown_character != '?' && strpos($string, '?') !== FALSE) {
|
||||
$hash = hash('sha256', $string);
|
||||
$string = str_replace('?', $hash, $string);
|
||||
}
|
||||
|
||||
// Ensure the string is valid UTF8 for preg_split(). Unknown characters will
|
||||
// be replaced by a question mark.
|
||||
$string = mb_convert_encoding($string, 'UTF-8', 'UTF-8');
|
||||
|
||||
// Use the provided unknown character instead of a question mark.
|
||||
if ($unknown_character != '?') {
|
||||
$string = str_replace('?', $unknown_character, $string);
|
||||
// Restore original question marks if necessary.
|
||||
if ($hash !== FALSE) {
|
||||
$string = str_replace($hash, '?', $string);
|
||||
}
|
||||
}
|
||||
|
||||
// Split into Unicode characters and transliterate each one.
|
||||
foreach (preg_split('//u', $string, 0, PREG_SPLIT_NO_EMPTY) as $character) {
|
||||
$code = self::ordUTF8($character);
|
||||
|
|
67
web/core/lib/Drupal/Component/Utility/Mail.php
Normal file
67
web/core/lib/Drupal/Component/Utility/Mail.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Component\Utility;
|
||||
|
||||
/**
|
||||
* Provides helpers to ensure emails are compliant with RFCs.
|
||||
*
|
||||
* @ingroup utility
|
||||
*/
|
||||
class Mail {
|
||||
|
||||
/**
|
||||
* RFC-2822 "specials" characters.
|
||||
*/
|
||||
const RFC_2822_SPECIALS = '()<>[]:;@\,."';
|
||||
|
||||
/**
|
||||
* Return a RFC-2822 compliant "display-name" component.
|
||||
*
|
||||
* The "display-name" component is used in mail header "Originator" fields
|
||||
* (From, Sender, Reply-to) to give a human-friendly description of the
|
||||
* address, i.e. From: My Display Name <xyz@example.org>. RFC-822 and
|
||||
* RFC-2822 define its syntax and rules. This method gets as input a string
|
||||
* to be used as "display-name" and formats it to be RFC compliant.
|
||||
*
|
||||
* @param string $string
|
||||
* A string to be used as "display-name".
|
||||
*
|
||||
* @return string
|
||||
* A RFC compliant version of the string, ready to be used as
|
||||
* "display-name" in mail originator header fields.
|
||||
*/
|
||||
public static function formatDisplayName($string) {
|
||||
// Make sure we don't process html-encoded characters. They may create
|
||||
// unneeded trouble if left encoded, besides they will be correctly
|
||||
// processed if decoded.
|
||||
$string = Html::decodeEntities($string);
|
||||
|
||||
// If string contains non-ASCII characters it must be (short) encoded
|
||||
// according to RFC-2047. The output of a "B" (Base64) encoded-word is
|
||||
// always safe to be used as display-name.
|
||||
$safe_display_name = Unicode::mimeHeaderEncode($string, TRUE);
|
||||
|
||||
// Encoded-words are always safe to be used as display-name because don't
|
||||
// contain any RFC 2822 "specials" characters. However
|
||||
// Unicode::mimeHeaderEncode() encodes a string only if it contains any
|
||||
// non-ASCII characters, and leaves its value untouched (un-encoded) if
|
||||
// ASCII only. For this reason in order to produce a valid display-name we
|
||||
// still need to make sure there are no "specials" characters left.
|
||||
if (preg_match('/[' . preg_quote(Mail::RFC_2822_SPECIALS) . ']/', $safe_display_name)) {
|
||||
|
||||
// If string is already quoted, it may or may not be escaped properly, so
|
||||
// don't trust it and reset.
|
||||
if (preg_match('/^"(.+)"$/', $safe_display_name, $matches)) {
|
||||
$safe_display_name = str_replace(['\\\\', '\\"'], ['\\', '"'], $matches[1]);
|
||||
}
|
||||
|
||||
// Transform the string in a RFC-2822 "quoted-string" by wrapping it in
|
||||
// double-quotes. Also make sure '"' and '\' occurrences are escaped.
|
||||
$safe_display_name = '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $safe_display_name) . '"';
|
||||
|
||||
}
|
||||
|
||||
return $safe_display_name;
|
||||
}
|
||||
|
||||
}
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
/**
|
||||
* Note on Drupal 8 porting.
|
||||
* This file origin is Tar.php, release 1.4.0 (stable) with some code
|
||||
* This file origin is Tar.php, release 1.4.5 (stable) with some code
|
||||
* from PEAR.php, release 1.9.5 (stable) both at http://pear.php.net.
|
||||
* To simplify future porting from pear of this file, you should not
|
||||
* do cosmetic or other non significant changes to this file.
|
||||
|
@ -151,6 +151,13 @@ class ArchiveTar
|
|||
*/
|
||||
public $error_object = null;
|
||||
|
||||
/**
|
||||
* Format for data extraction
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $_fmt ='';
|
||||
|
||||
/**
|
||||
* Archive_Tar Class constructor. This flavour of the constructor only
|
||||
* declare a new Archive_Tar object, identifying it by the name of the
|
||||
|
@ -257,6 +264,18 @@ class ArchiveTar
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (version_compare(PHP_VERSION, "5.5.0-dev") < 0) {
|
||||
$this->_fmt = "a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" .
|
||||
"a8checksum/a1typeflag/a100link/a6magic/a2version/" .
|
||||
"a32uname/a32gname/a8devmajor/a8devminor/a131prefix";
|
||||
} else {
|
||||
$this->_fmt = "Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/" .
|
||||
"Z8checksum/Z1typeflag/Z100link/Z6magic/Z2version/" .
|
||||
"Z32uname/Z32gname/Z8devmajor/Z8devminor/Z131prefix";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
|
@ -712,7 +731,7 @@ class ArchiveTar
|
|||
}
|
||||
|
||||
// ----- Get the arguments
|
||||
$v_att_list = & func_get_args();
|
||||
$v_att_list = func_get_args();
|
||||
|
||||
// ----- Read the attributes
|
||||
$i = 0;
|
||||
|
@ -1392,10 +1411,20 @@ class ArchiveTar
|
|||
if ($p_stored_filename == '') {
|
||||
$p_stored_filename = $p_filename;
|
||||
}
|
||||
$v_reduce_filename = $this->_pathReduction($p_stored_filename);
|
||||
$v_reduced_filename = $this->_pathReduction($p_stored_filename);
|
||||
|
||||
if (strlen($v_reduce_filename) > 99) {
|
||||
if (!$this->_writeLongHeader($v_reduce_filename)) {
|
||||
if (strlen($v_reduced_filename) > 99) {
|
||||
if (!$this->_writeLongHeader($v_reduced_filename, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$v_linkname = '';
|
||||
if (@is_link($p_filename)) {
|
||||
$v_linkname = readlink($p_filename);
|
||||
}
|
||||
if (strlen($v_linkname) > 99) {
|
||||
if (!$this->_writeLongHeader($v_linkname, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1404,14 +1433,10 @@ class ArchiveTar
|
|||
$v_uid = sprintf("%07s", DecOct($v_info[4]));
|
||||
$v_gid = sprintf("%07s", DecOct($v_info[5]));
|
||||
$v_perms = sprintf("%07s", DecOct($v_info['mode'] & 000777));
|
||||
|
||||
$v_mtime = sprintf("%011s", DecOct($v_info['mtime']));
|
||||
|
||||
$v_linkname = '';
|
||||
|
||||
if (@is_link($p_filename)) {
|
||||
$v_typeflag = '2';
|
||||
$v_linkname = readlink($p_filename);
|
||||
$v_size = sprintf("%011s", DecOct(0));
|
||||
} elseif (@is_dir($p_filename)) {
|
||||
$v_typeflag = "5";
|
||||
|
@ -1423,7 +1448,6 @@ class ArchiveTar
|
|||
}
|
||||
|
||||
$v_magic = 'ustar ';
|
||||
|
||||
$v_version = ' ';
|
||||
|
||||
if (function_exists('posix_getpwuid')) {
|
||||
|
@ -1438,14 +1462,12 @@ class ArchiveTar
|
|||
}
|
||||
|
||||
$v_devmajor = '';
|
||||
|
||||
$v_devminor = '';
|
||||
|
||||
$v_prefix = '';
|
||||
|
||||
$v_binary_data_first = pack(
|
||||
"a100a8a8a8a12a12",
|
||||
$v_reduce_filename,
|
||||
$v_perms,
|
||||
$v_uid,
|
||||
$v_gid,
|
||||
|
@ -1485,7 +1507,7 @@ class ArchiveTar
|
|||
$this->_writeBlock($v_binary_data_first, 148);
|
||||
|
||||
// ----- Write the calculated checksum
|
||||
$v_checksum = sprintf("%06s ", DecOct($v_checksum));
|
||||
$v_checksum = sprintf("%06s\0 ", DecOct($v_checksum));
|
||||
$v_binary_data = pack("a8", $v_checksum);
|
||||
$this->_writeBlock($v_binary_data, 8);
|
||||
|
||||
|
@ -1517,7 +1539,7 @@ class ArchiveTar
|
|||
$p_filename = $this->_pathReduction($p_filename);
|
||||
|
||||
if (strlen($p_filename) > 99) {
|
||||
if (!$this->_writeLongHeader($p_filename)) {
|
||||
if (!$this->_writeLongHeader($p_filename, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1613,36 +1635,31 @@ class ArchiveTar
|
|||
* @param string $p_filename
|
||||
* @return bool
|
||||
*/
|
||||
public function _writeLongHeader($p_filename)
|
||||
public function _writeLongHeader($p_filename, $is_link = false)
|
||||
{
|
||||
$v_size = sprintf("%11s ", DecOct(strlen($p_filename)));
|
||||
|
||||
$v_typeflag = 'L';
|
||||
|
||||
$v_uid = sprintf("%07s", 0);
|
||||
$v_gid = sprintf("%07s", 0);
|
||||
$v_perms = sprintf("%07s", 0);
|
||||
$v_size = sprintf("%'011s", DecOct(strlen($p_filename)));
|
||||
$v_mtime = sprintf("%011s", 0);
|
||||
$v_typeflag = ($is_link ? 'K' : 'L');
|
||||
$v_linkname = '';
|
||||
|
||||
$v_magic = '';
|
||||
|
||||
$v_version = '';
|
||||
|
||||
$v_magic = 'ustar ';
|
||||
$v_version = ' ';
|
||||
$v_uname = '';
|
||||
|
||||
$v_gname = '';
|
||||
|
||||
$v_devmajor = '';
|
||||
|
||||
$v_devminor = '';
|
||||
|
||||
$v_prefix = '';
|
||||
|
||||
$v_binary_data_first = pack(
|
||||
"a100a8a8a8a12a12",
|
||||
'././@LongLink',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$v_perms,
|
||||
$v_uid,
|
||||
$v_gid,
|
||||
$v_size,
|
||||
0
|
||||
$v_mtime
|
||||
);
|
||||
$v_binary_data_last = pack(
|
||||
"a1a100a6a2a32a32a8a8a155a12",
|
||||
|
@ -1677,7 +1694,7 @@ class ArchiveTar
|
|||
$this->_writeBlock($v_binary_data_first, 148);
|
||||
|
||||
// ----- Write the calculated checksum
|
||||
$v_checksum = sprintf("%06s ", DecOct($v_checksum));
|
||||
$v_checksum = sprintf("%06s\0 ", DecOct($v_checksum));
|
||||
$v_binary_data = pack("a8", $v_checksum);
|
||||
$this->_writeBlock($v_binary_data, 8);
|
||||
|
||||
|
@ -1718,28 +1735,12 @@ class ArchiveTar
|
|||
// ----- Calculate the checksum
|
||||
$v_checksum = 0;
|
||||
// ..... First part of the header
|
||||
for ($i = 0; $i < 148; $i++) {
|
||||
$v_checksum += ord(substr($v_binary_data, $i, 1));
|
||||
}
|
||||
// ..... Ignore the checksum value and replace it by ' ' (space)
|
||||
for ($i = 148; $i < 156; $i++) {
|
||||
$v_checksum += ord(' ');
|
||||
}
|
||||
// ..... Last part of the header
|
||||
for ($i = 156; $i < 512; $i++) {
|
||||
$v_checksum += ord(substr($v_binary_data, $i, 1));
|
||||
}
|
||||
$v_binary_split = str_split($v_binary_data);
|
||||
$v_checksum += array_sum(array_map('ord', array_slice($v_binary_split, 0, 148)));
|
||||
$v_checksum += array_sum(array_map('ord', array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',)));
|
||||
$v_checksum += array_sum(array_map('ord', array_slice($v_binary_split, 156, 512)));
|
||||
|
||||
if (version_compare(PHP_VERSION, "5.5.0-dev") < 0) {
|
||||
$fmt = "a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" .
|
||||
"a8checksum/a1typeflag/a100link/a6magic/a2version/" .
|
||||
"a32uname/a32gname/a8devmajor/a8devminor/a131prefix";
|
||||
} else {
|
||||
$fmt = "Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/" .
|
||||
"Z8checksum/Z1typeflag/Z100link/Z6magic/Z2version/" .
|
||||
"Z32uname/Z32gname/Z8devmajor/Z8devminor/Z131prefix";
|
||||
}
|
||||
$v_data = unpack($fmt, $v_binary_data);
|
||||
$v_data = unpack($this->_fmt, $v_binary_data);
|
||||
|
||||
if (strlen($v_data["prefix"]) > 0) {
|
||||
$v_data["filename"] = "$v_data[prefix]/$v_data[filename]";
|
||||
|
@ -1775,7 +1776,7 @@ class ArchiveTar
|
|||
$v_header['mode'] = OctDec(trim($v_data['mode']));
|
||||
$v_header['uid'] = OctDec(trim($v_data['uid']));
|
||||
$v_header['gid'] = OctDec(trim($v_data['gid']));
|
||||
$v_header['size'] = OctDec(trim($v_data['size']));
|
||||
$v_header['size'] = $this->_tarRecToSize($v_data['size']);
|
||||
$v_header['mtime'] = OctDec(trim($v_data['mtime']));
|
||||
if (($v_header['typeflag'] = $v_data['typeflag']) == "5") {
|
||||
$v_header['size'] = 0;
|
||||
|
@ -1794,6 +1795,41 @@ class ArchiveTar
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Tar record size to actual size
|
||||
*
|
||||
* @param string $tar_size
|
||||
* @return size of tar record in bytes
|
||||
*/
|
||||
private function _tarRecToSize($tar_size)
|
||||
{
|
||||
/*
|
||||
* First byte of size has a special meaning if bit 7 is set.
|
||||
*
|
||||
* Bit 7 indicates base-256 encoding if set.
|
||||
* Bit 6 is the sign bit.
|
||||
* Bits 5:0 are most significant value bits.
|
||||
*/
|
||||
$ch = ord($tar_size[0]);
|
||||
if ($ch & 0x80) {
|
||||
// Full 12-bytes record is required.
|
||||
$rec_str = $tar_size . "\x00";
|
||||
|
||||
$size = ($ch & 0x40) ? -1 : 0;
|
||||
$size = ($size << 6) | ($ch & 0x3f);
|
||||
|
||||
for ($num_ch = 1; $num_ch < 12; ++$num_ch) {
|
||||
$size = ($size * 256) + ord($rec_str[$num_ch]);
|
||||
}
|
||||
|
||||
return $size;
|
||||
|
||||
} else {
|
||||
return OctDec(trim($tar_size));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detect and report a malicious file name
|
||||
*
|
||||
|
@ -1803,10 +1839,13 @@ class ArchiveTar
|
|||
*/
|
||||
private function _maliciousFilename($file)
|
||||
{
|
||||
if (strpos($file, '/../') !== false) {
|
||||
if (strpos($file, 'phar://') === 0) {
|
||||
return true;
|
||||
}
|
||||
if (strpos($file, '../') === 0) {
|
||||
if (strpos($file, DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR) !== false) {
|
||||
return true;
|
||||
}
|
||||
if (strpos($file, '..' . DIRECTORY_SEPARATOR) === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -1871,11 +1910,20 @@ class ArchiveTar
|
|||
continue;
|
||||
}
|
||||
|
||||
// ----- Look for long filename
|
||||
if ($v_header['typeflag'] == 'L') {
|
||||
if (!$this->_readLongHeader($v_header)) {
|
||||
return null;
|
||||
}
|
||||
switch ($v_header['typeflag']) {
|
||||
case 'L': {
|
||||
if (!$this->_readLongHeader($v_header)) {
|
||||
return null;
|
||||
}
|
||||
} break;
|
||||
|
||||
case 'K': {
|
||||
$v_link_header = $v_header;
|
||||
if (!$this->_readLongHeader($v_link_header)) {
|
||||
return null;
|
||||
}
|
||||
$v_header['link'] = $v_link_header['filename'];
|
||||
} break;
|
||||
}
|
||||
|
||||
if ($v_header['filename'] == $p_filename) {
|
||||
|
@ -1976,11 +2024,20 @@ class ArchiveTar
|
|||
continue;
|
||||
}
|
||||
|
||||
// ----- Look for long filename
|
||||
if ($v_header['typeflag'] == 'L') {
|
||||
if (!$this->_readLongHeader($v_header)) {
|
||||
return false;
|
||||
}
|
||||
switch ($v_header['typeflag']) {
|
||||
case 'L': {
|
||||
if (!$this->_readLongHeader($v_header)) {
|
||||
return null;
|
||||
}
|
||||
} break;
|
||||
|
||||
case 'K': {
|
||||
$v_link_header = $v_header;
|
||||
if (!$this->_readLongHeader($v_link_header)) {
|
||||
return null;
|
||||
}
|
||||
$v_header['link'] = $v_link_header['filename'];
|
||||
} break;
|
||||
}
|
||||
|
||||
// ignore extended / pax headers
|
||||
|
|
|
@ -11,6 +11,7 @@ use Drupal\Component\Utility\NestedArray;
|
|||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Plugin\PluginWithFormsInterface;
|
||||
use Drupal\Core\Plugin\PluginWithFormsTrait;
|
||||
use Drupal\Core\Render\PreviewFallbackInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Component\Transliteration\TransliterationInterface;
|
||||
|
||||
|
@ -23,7 +24,7 @@ use Drupal\Component\Transliteration\TransliterationInterface;
|
|||
*
|
||||
* @ingroup block_api
|
||||
*/
|
||||
abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface {
|
||||
abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface, PreviewFallbackInterface {
|
||||
|
||||
use ContextAwarePluginAssignmentTrait;
|
||||
use MessengerTrait;
|
||||
|
@ -252,6 +253,13 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
|
|||
return $transliterated;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPreviewFallbackString() {
|
||||
return $this->t('Placeholder for the "@block" block', ['@block' => $this->label()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the transliteration service.
|
||||
*
|
||||
|
|
|
@ -156,7 +156,8 @@ class MemoryBackend implements CacheBackendInterface, CacheTagsInvalidatorInterf
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function invalidateMultiple(array $cids) {
|
||||
foreach ($cids as $cid) {
|
||||
$items = array_intersect_key($this->cache, array_flip($cids));
|
||||
foreach ($items as $cid => $item) {
|
||||
$this->cache[$cid]->expire = $this->getRequestTime() - 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,12 +162,19 @@ class DbDumpCommand extends DbCommandBase {
|
|||
$definition['fields'][$name]['precision'] = $matches[2];
|
||||
$definition['fields'][$name]['scale'] = $matches[3];
|
||||
}
|
||||
elseif ($type === 'time' || $type === 'datetime') {
|
||||
elseif ($type === 'time') {
|
||||
// @todo Core doesn't support these, but copied from `migrate-db.sh` for now.
|
||||
// Convert to varchar.
|
||||
$definition['fields'][$name]['type'] = 'varchar';
|
||||
$definition['fields'][$name]['length'] = '100';
|
||||
}
|
||||
elseif ($type === 'datetime') {
|
||||
// Adjust for other database types.
|
||||
$definition['fields'][$name]['mysql_type'] = 'datetime';
|
||||
$definition['fields'][$name]['pgsql_type'] = 'timestamp without time zone';
|
||||
$definition['fields'][$name]['sqlite_type'] = 'varchar';
|
||||
$definition['fields'][$name]['sqlsrv_type'] = 'smalldatetime';
|
||||
}
|
||||
elseif (!isset($definition['fields'][$name]['size'])) {
|
||||
// Try use the provided length, if it doesn't exist default to 100. It's
|
||||
// not great but good enough for our dumps at this point.
|
||||
|
|
|
@ -33,7 +33,7 @@ abstract class Tasks {
|
|||
],
|
||||
[
|
||||
'arguments' => [
|
||||
'CREATE TABLE {drupal_install_test} (id int NULL)',
|
||||
'CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)',
|
||||
'Drupal can use CREATE TABLE database commands.',
|
||||
'Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>',
|
||||
TRUE,
|
||||
|
|
|
@ -248,18 +248,15 @@ use Drupal\Core\Database\Query\Condition;
|
|||
*
|
||||
* The following keys are defined:
|
||||
* - 'description': A string in non-markup plain text describing this table
|
||||
* and its purpose. References to other tables should be enclosed in
|
||||
* curly-brackets. For example, the node_field_revision table
|
||||
* description field might contain "Stores per-revision title and
|
||||
* body data for each {node}."
|
||||
* and its purpose. References to other tables should be enclosed in curly
|
||||
* brackets.
|
||||
* - 'fields': An associative array ('fieldname' => specification)
|
||||
* that describes the table's database columns. The specification
|
||||
* is also an array. The following specification parameters are defined:
|
||||
* - 'description': A string in non-markup plain text describing this field
|
||||
* and its purpose. References to other tables should be enclosed in
|
||||
* curly-brackets. For example, the node table vid field
|
||||
* description might contain "Always holds the largest (most
|
||||
* recent) {node_field_revision}.vid value for this nid."
|
||||
* and its purpose. References to other tables should be enclosed in curly
|
||||
* brackets. For example, the users_data table 'uid' field description
|
||||
* might contain "The {users}.uid this record affects."
|
||||
* - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int',
|
||||
* 'float', 'numeric', or 'serial'. Most types just map to the according
|
||||
* database engine specific data types. Use 'serial' for auto incrementing
|
||||
|
@ -322,64 +319,70 @@ use Drupal\Core\Database\Query\Condition;
|
|||
* key column specifiers (see below) that form an index on the
|
||||
* table.
|
||||
*
|
||||
* A key column specifier is either a string naming a column or an
|
||||
* array of two elements, column name and length, specifying a prefix
|
||||
* of the named column.
|
||||
* A key column specifier is either a string naming a column or an array of two
|
||||
* elements, column name and length, specifying a prefix of the named column.
|
||||
*
|
||||
* As an example, here is a SUBSET of the schema definition for
|
||||
* Drupal's 'node' table. It show four fields (nid, vid, type, and
|
||||
* title), the primary key on field 'nid', a unique key named 'vid' on
|
||||
* field 'vid', and two indexes, one named 'nid' on field 'nid' and
|
||||
* one named 'node_title_type' on the field 'title' and the first four
|
||||
* bytes of the field 'type':
|
||||
* As an example, this is the schema definition for the 'users_data' table. It
|
||||
* shows five fields ('uid', 'module', 'name', 'value', and 'serialized'), the
|
||||
* primary key (on the 'uid', 'module', and 'name' fields), and two indexes (the
|
||||
* 'module' index on the 'module' field and the 'name' index on the 'name'
|
||||
* field).
|
||||
*
|
||||
* @code
|
||||
* $schema['node'] = array(
|
||||
* 'description' => 'The base table for nodes.',
|
||||
* 'fields' => array(
|
||||
* 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
|
||||
* 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE,'default' => 0),
|
||||
* 'type' => array('type' => 'varchar','length' => 32,'not null' => TRUE, 'default' => ''),
|
||||
* 'language' => array('type' => 'varchar','length' => 12,'not null' => TRUE,'default' => ''),
|
||||
* 'title' => array('type' => 'varchar','length' => 255,'not null' => TRUE, 'default' => ''),
|
||||
* 'uid' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||
* 'status' => array('type' => 'int', 'not null' => TRUE, 'default' => 1),
|
||||
* 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||
* 'changed' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||
* 'comment' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||
* 'promote' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||
* 'moderate' => array('type' => 'int', 'not null' => TRUE,'default' => 0),
|
||||
* 'sticky' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||
* 'translate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||
* ),
|
||||
* 'indexes' => array(
|
||||
* 'node_changed' => array('changed'),
|
||||
* 'node_created' => array('created'),
|
||||
* 'node_moderate' => array('moderate'),
|
||||
* 'node_frontpage' => array('promote', 'status', 'sticky', 'created'),
|
||||
* 'node_status_type' => array('status', 'type', 'nid'),
|
||||
* 'node_title_type' => array('title', array('type', 4)),
|
||||
* 'node_type' => array(array('type', 4)),
|
||||
* 'uid' => array('uid'),
|
||||
* 'translate' => array('translate'),
|
||||
* ),
|
||||
* 'unique keys' => array(
|
||||
* 'vid' => array('vid'),
|
||||
* ),
|
||||
* $schema['users_data'] = [
|
||||
* 'description' => 'Stores module data as key/value pairs per user.',
|
||||
* 'fields' => [
|
||||
* 'uid' => [
|
||||
* 'description' => 'The {users}.uid this record affects.',
|
||||
* 'type' => 'int',
|
||||
* 'unsigned' => TRUE,
|
||||
* 'not null' => TRUE,
|
||||
* 'default' => 0,
|
||||
* ],
|
||||
* 'module' => [
|
||||
* 'description' => 'The name of the module declaring the variable.',
|
||||
* 'type' => 'varchar_ascii',
|
||||
* 'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
|
||||
* 'not null' => TRUE,
|
||||
* 'default' => '',
|
||||
* ],
|
||||
* 'name' => [
|
||||
* 'description' => 'The identifier of the data.',
|
||||
* 'type' => 'varchar_ascii',
|
||||
* 'length' => 128,
|
||||
* 'not null' => TRUE,
|
||||
* 'default' => '',
|
||||
* ],
|
||||
* 'value' => [
|
||||
* 'description' => 'The value.',
|
||||
* 'type' => 'blob',
|
||||
* 'not null' => FALSE,
|
||||
* 'size' => 'big',
|
||||
* ],
|
||||
* 'serialized' => [
|
||||
* 'description' => 'Whether value is serialized.',
|
||||
* 'type' => 'int',
|
||||
* 'size' => 'tiny',
|
||||
* 'unsigned' => TRUE,
|
||||
* 'default' => 0,
|
||||
* ],
|
||||
* ],
|
||||
* 'primary key' => ['uid', 'module', 'name'],
|
||||
* 'indexes' => [
|
||||
* 'module' => ['module'],
|
||||
* 'name' => ['name'],
|
||||
* ],
|
||||
* // For documentation purposes only; foreign keys are not created in the
|
||||
* // database.
|
||||
* 'foreign keys' => array(
|
||||
* 'node_revision' => array(
|
||||
* 'table' => 'node_field_revision',
|
||||
* 'columns' => array('vid' => 'vid'),
|
||||
* ),
|
||||
* 'node_author' => array(
|
||||
* 'foreign keys' => [
|
||||
* 'data_user' => [
|
||||
* 'table' => 'users',
|
||||
* 'columns' => array('uid' => 'uid'),
|
||||
* ),
|
||||
* ),
|
||||
* 'primary key' => array('nid'),
|
||||
* );
|
||||
* 'columns' => [
|
||||
* 'uid' => 'uid',
|
||||
* ],
|
||||
* ],
|
||||
* ],
|
||||
* ];
|
||||
* @endcode
|
||||
*
|
||||
* @see drupal_install_schema()
|
||||
|
@ -490,60 +493,61 @@ function hook_query_TAG_alter(Drupal\Core\Database\Query\AlterableInterface $que
|
|||
* @ingroup schemaapi
|
||||
*/
|
||||
function hook_schema() {
|
||||
$schema['node'] = [
|
||||
// Example (partial) specification for table "node".
|
||||
'description' => 'The base table for nodes.',
|
||||
$schema['users_data'] = [
|
||||
'description' => 'Stores module data as key/value pairs per user.',
|
||||
'fields' => [
|
||||
'nid' => [
|
||||
'description' => 'The primary identifier for a node.',
|
||||
'type' => 'serial',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
],
|
||||
'vid' => [
|
||||
'description' => 'The current {node_field_revision}.vid version identifier.',
|
||||
'uid' => [
|
||||
'description' => 'The {users}.uid this record affects.',
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
],
|
||||
'type' => [
|
||||
'description' => 'The type of this node.',
|
||||
'type' => 'varchar',
|
||||
'length' => 32,
|
||||
'module' => [
|
||||
'description' => 'The name of the module declaring the variable.',
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
],
|
||||
'title' => [
|
||||
'description' => 'The node title.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'name' => [
|
||||
'description' => 'The identifier of the data.',
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
],
|
||||
'value' => [
|
||||
'description' => 'The value.',
|
||||
'type' => 'blob',
|
||||
'not null' => FALSE,
|
||||
'size' => 'big',
|
||||
],
|
||||
'serialized' => [
|
||||
'description' => 'Whether value is serialized.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'unsigned' => TRUE,
|
||||
'default' => 0,
|
||||
],
|
||||
],
|
||||
'primary key' => ['uid', 'module', 'name'],
|
||||
'indexes' => [
|
||||
'node_changed' => ['changed'],
|
||||
'node_created' => ['created'],
|
||||
],
|
||||
'unique keys' => [
|
||||
'nid_vid' => ['nid', 'vid'],
|
||||
'vid' => ['vid'],
|
||||
'module' => ['module'],
|
||||
'name' => ['name'],
|
||||
],
|
||||
// For documentation purposes only; foreign keys are not created in the
|
||||
// database.
|
||||
'foreign keys' => [
|
||||
'node_revision' => [
|
||||
'table' => 'node_field_revision',
|
||||
'columns' => ['vid' => 'vid'],
|
||||
],
|
||||
'node_author' => [
|
||||
'data_user' => [
|
||||
'table' => 'users',
|
||||
'columns' => ['uid' => 'uid'],
|
||||
'columns' => [
|
||||
'uid' => 'uid',
|
||||
],
|
||||
],
|
||||
],
|
||||
'primary key' => ['nid'],
|
||||
];
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ use Drupal\Core\File\MimeType\MimeTypeGuesser;
|
|||
use Drupal\Core\Http\TrustedHostsRequestFactory;
|
||||
use Drupal\Core\Installer\InstallerRedirectTrait;
|
||||
use Drupal\Core\Language\Language;
|
||||
use Drupal\Core\Security\PharExtensionInterceptor;
|
||||
use Drupal\Core\Security\RequestSanitizer;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\Core\Test\TestDatabase;
|
||||
|
@ -35,6 +36,9 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Symfony\Component\HttpKernel\TerminableInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use TYPO3\PharStreamWrapper\Manager as PharStreamWrapperManager;
|
||||
use TYPO3\PharStreamWrapper\Behavior as PharStreamWrapperBehavior;
|
||||
use TYPO3\PharStreamWrapper\PharStreamWrapper;
|
||||
|
||||
/**
|
||||
* The DrupalKernel class is the core of Drupal itself.
|
||||
|
@ -471,6 +475,26 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
|
|||
// Initialize the container.
|
||||
$this->initializeContainer();
|
||||
|
||||
if (in_array('phar', stream_get_wrappers(), TRUE)) {
|
||||
// Set up a stream wrapper to handle insecurities due to PHP's builtin
|
||||
// phar stream wrapper. This is not registered as a regular stream wrapper
|
||||
// to prevent \Drupal\Core\File\FileSystem::validScheme() treating "phar"
|
||||
// as a valid scheme.
|
||||
try {
|
||||
$behavior = new PharStreamWrapperBehavior();
|
||||
PharStreamWrapperManager::initialize(
|
||||
$behavior->withAssertion(new PharExtensionInterceptor())
|
||||
);
|
||||
}
|
||||
catch (\LogicException $e) {
|
||||
// Continue if the PharStreamWrapperManager is already initialized. For
|
||||
// example, this occurs during a module install.
|
||||
// @see \Drupal\Core\Extension\ModuleInstaller::install()
|
||||
};
|
||||
stream_wrapper_unregister('phar');
|
||||
stream_wrapper_register('phar', PharStreamWrapper::class);
|
||||
}
|
||||
|
||||
$this->booted = TRUE;
|
||||
|
||||
return $this;
|
||||
|
|
|
@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityDisplayPluginCollection;
|
|||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Entity\EntityDisplayBase;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
|
||||
|
||||
/**
|
||||
|
@ -269,7 +270,7 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
|
|||
foreach ($entities as $id => $entity) {
|
||||
// Assign the configured weights.
|
||||
foreach ($this->getComponents() as $name => $options) {
|
||||
if (isset($build_list[$id][$name])) {
|
||||
if (isset($build_list[$id][$name]) && !Element::isEmpty($build_list[$id][$name])) {
|
||||
$build_list[$id][$name]['#weight'] = $options['weight'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace Drupal\Core\Entity\Plugin\DataType;
|
||||
|
||||
use Drupal\Core\Config\TypedConfigManagerInterface;
|
||||
use Drupal\Core\TypedData\Exception\MissingDataException;
|
||||
use Drupal\Core\TypedData\TypedDataManagerInterface;
|
||||
|
||||
/**
|
||||
* Enhances EntityAdapter for config entities.
|
||||
|
@ -16,6 +18,13 @@ class ConfigEntityAdapter extends EntityAdapter {
|
|||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The typed config manager.
|
||||
*
|
||||
* @var \Drupal\Core\Config\TypedConfigManagerInterface
|
||||
*/
|
||||
protected $typedConfigManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -68,10 +77,31 @@ class ConfigEntityAdapter extends EntityAdapter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the typed data manager.
|
||||
* Gets the typed config manager.
|
||||
*
|
||||
* @return \Drupal\Core\Config\TypedConfigManagerInterface
|
||||
* The typed data manager.
|
||||
* The typed config manager.
|
||||
*/
|
||||
protected function getTypedConfigManager() {
|
||||
if (empty($this->typedConfigManager)) {
|
||||
// Use the typed data manager if it is also the typed config manager.
|
||||
// @todo Remove this in https://www.drupal.org/node/3011137.
|
||||
$typed_data_manager = $this->getTypedDataManager();
|
||||
if ($typed_data_manager instanceof TypedConfigManagerInterface) {
|
||||
$this->typedConfigManager = $typed_data_manager;
|
||||
}
|
||||
else {
|
||||
$this->typedConfigManager = \Drupal::service('config.typed');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->typedConfigManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @todo Remove this in https://www.drupal.org/node/3011137.
|
||||
*/
|
||||
public function getTypedDataManager() {
|
||||
if (empty($this->typedDataManager)) {
|
||||
|
@ -81,6 +111,19 @@ class ConfigEntityAdapter extends EntityAdapter {
|
|||
return $this->typedDataManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @todo Remove this in https://www.drupal.org/node/3011137.
|
||||
*/
|
||||
public function setTypedDataManager(TypedDataManagerInterface $typed_data_manager) {
|
||||
$this->typedDataManager = $typed_data_manager;
|
||||
if ($typed_data_manager instanceof TypedConfigManagerInterface) {
|
||||
$this->typedConfigManager = $typed_data_manager;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -97,7 +140,7 @@ class ConfigEntityAdapter extends EntityAdapter {
|
|||
* The typed data.
|
||||
*/
|
||||
protected function getConfigTypedData() {
|
||||
return $this->getTypedDataManager()->createFromNameAndData($this->entity->getConfigDependencyName(), $this->entity->toArray());
|
||||
return $this->getTypedConfigManager()->createFromNameAndData($this->entity->getConfigDependencyName(), $this->entity->toArray());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -184,6 +184,7 @@ class Tables implements TablesInterface {
|
|||
// finds the property first. The data table is preferred, which is why
|
||||
// it gets added before the base table.
|
||||
$entity_tables = [];
|
||||
$revision_table = NULL;
|
||||
if ($all_revisions && $field_storage && $field_storage->isRevisionable()) {
|
||||
$data_table = $entity_type->getRevisionDataTable();
|
||||
$entity_base_table = $entity_type->getRevisionTable();
|
||||
|
@ -191,11 +192,18 @@ class Tables implements TablesInterface {
|
|||
else {
|
||||
$data_table = $entity_type->getDataTable();
|
||||
$entity_base_table = $entity_type->getBaseTable();
|
||||
|
||||
if ($field_storage && $field_storage->isRevisionable() && in_array($field_storage->getName(), $entity_type->getRevisionMetadataKeys())) {
|
||||
$revision_table = $entity_type->getRevisionTable();
|
||||
}
|
||||
}
|
||||
if ($data_table) {
|
||||
$this->sqlQuery->addMetaData('simple_query', FALSE);
|
||||
$entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
|
||||
}
|
||||
if ($revision_table) {
|
||||
$entity_tables[$revision_table] = $this->getTableMapping($revision_table, $entity_type_id);
|
||||
}
|
||||
$entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
|
||||
$sql_column = $specifier;
|
||||
|
||||
|
|
|
@ -821,10 +821,13 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
if ($update) {
|
||||
$default_revision = $entity->isDefaultRevision();
|
||||
if ($default_revision) {
|
||||
// Remove the ID from the record to enable updates on SQL variants
|
||||
// that prevent updating serial columns, for example, mssql.
|
||||
unset($record->{$this->idKey});
|
||||
$this->database
|
||||
->update($this->baseTable)
|
||||
->fields((array) $record)
|
||||
->condition($this->idKey, $record->{$this->idKey})
|
||||
->condition($this->idKey, $entity->get($this->idKey)->value)
|
||||
->execute();
|
||||
}
|
||||
if ($this->revisionTable) {
|
||||
|
@ -833,11 +836,15 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
}
|
||||
else {
|
||||
$record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
|
||||
// Remove the revision ID from the record to enable updates on SQL
|
||||
// variants that prevent updating serial columns, for example,
|
||||
// mssql.
|
||||
unset($record->{$this->revisionKey});
|
||||
$entity->preSaveRevision($this, $record);
|
||||
$this->database
|
||||
->update($this->revisionTable)
|
||||
->fields((array) $record)
|
||||
->condition($this->revisionKey, $record->{$this->revisionKey})
|
||||
->condition($this->revisionKey, $entity->getRevisionId())
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
@ -1064,19 +1071,21 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
->condition($this->idKey, $record->{$this->idKey})
|
||||
->execute();
|
||||
}
|
||||
// Make sure to update the new revision key for the entity.
|
||||
$entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
|
||||
}
|
||||
else {
|
||||
// Remove the revision ID from the record to enable updates on SQL
|
||||
// variants that prevent updating serial columns, for example,
|
||||
// mssql.
|
||||
unset($record->{$this->revisionKey});
|
||||
$this->database
|
||||
->update($this->revisionTable)
|
||||
->fields((array) $record)
|
||||
->condition($this->revisionKey, $record->{$this->revisionKey})
|
||||
->condition($this->revisionKey, $entity->getRevisionId())
|
||||
->execute();
|
||||
}
|
||||
|
||||
// Make sure to update the new revision key for the entity.
|
||||
$entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
|
||||
|
||||
return $record->{$this->revisionKey};
|
||||
return $entity->getRevisionId();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -811,10 +811,10 @@ function hook_entity_view_mode_info_alter(&$view_modes) {
|
|||
* An associative array of all entity bundles, keyed by the entity
|
||||
* type name, and then the bundle name, with the following keys:
|
||||
* - label: The human-readable name of the bundle.
|
||||
* - uri_callback: The same as the 'uri_callback' key defined for the entity
|
||||
* type in the EntityManager, but for the bundle only. When determining
|
||||
* the URI of an entity, if a 'uri_callback' is defined for both the
|
||||
* entity type and the bundle, the one for the bundle is used.
|
||||
* - uri_callback: (optional) The same as the 'uri_callback' key defined for
|
||||
* the entity type in the EntityManager, but for the bundle only. When
|
||||
* determining the URI of an entity, if a 'uri_callback' is defined for both
|
||||
* the entity type and the bundle, the one for the bundle is used.
|
||||
* - translatable: (optional) A boolean value specifying whether this bundle
|
||||
* has translation support enabled. Defaults to FALSE.
|
||||
*
|
||||
|
|
|
@ -684,6 +684,20 @@ function hook_update_N(&$sandbox) {
|
|||
* Drupal also ensures to not execute the same hook_post_update_NAME() function
|
||||
* twice.
|
||||
*
|
||||
* @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 batch updates. See above for more information.
|
||||
*
|
||||
|
|
|
@ -124,7 +124,6 @@ class StringFormatter extends FormatterBase implements ContainerFactoryPluginInt
|
|||
$elements = [];
|
||||
$url = NULL;
|
||||
if ($this->getSetting('link_to_entity')) {
|
||||
// For the default revision this falls back to 'canonical'.
|
||||
$url = $this->getEntityUrl($items->getEntity());
|
||||
}
|
||||
|
||||
|
@ -173,8 +172,11 @@ class StringFormatter extends FormatterBase implements ContainerFactoryPluginInt
|
|||
* The URI elements of the entity.
|
||||
*/
|
||||
protected function getEntityUrl(EntityInterface $entity) {
|
||||
// For the default revision this falls back to 'canonical'.
|
||||
return $entity->toUrl('revision');
|
||||
// For the default revision, the 'revision' link template falls back to
|
||||
// 'canonical'.
|
||||
// @see \Drupal\Core\Entity\Entity::toUrl()
|
||||
$rel = $entity->getEntityType()->hasLinkTemplate('revision') ? 'revision' : 'canonical';
|
||||
return $entity->toUrl($rel);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
|
|||
use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
|
||||
use Drupal\Core\Layout\Annotation\Layout;
|
||||
use Drupal\Core\Plugin\FilteredPluginManagerTrait;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Provides a plugin manager for layouts.
|
||||
|
@ -71,6 +72,10 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa
|
|||
if (!$this->discovery) {
|
||||
$discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
|
||||
$discovery = new YamlDiscoveryDecorator($discovery, 'layouts', $this->moduleHandler->getModuleDirectories() + $this->themeHandler->getThemeDirectories());
|
||||
$discovery
|
||||
->addTranslatableProperty('label')
|
||||
->addTranslatableProperty('description')
|
||||
->addTranslatableProperty('category');
|
||||
$discovery = new AnnotationBridgeDecorator($discovery, $this->pluginDefinitionAnnotationName);
|
||||
$discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
|
||||
$this->discovery = $discovery;
|
||||
|
@ -140,6 +145,15 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa
|
|||
if (!$definition->getDefaultRegion()) {
|
||||
$definition->setDefaultRegion(key($definition->getRegions()));
|
||||
}
|
||||
// Makes sure region names are translatable.
|
||||
$regions = array_map(function ($region) {
|
||||
if (!$region['label'] instanceof TranslatableMarkup) {
|
||||
// Region labels from YAML discovery needs translation.
|
||||
$region['label'] = new TranslatableMarkup($region['label'], [], ['context' => 'layout_region']);
|
||||
}
|
||||
return $region;
|
||||
}, $definition->getRegions());
|
||||
$definition->setRegions($regions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,7 +5,7 @@ namespace Drupal\Core\Mail;
|
|||
use Drupal\Component\Render\MarkupInterface;
|
||||
use Drupal\Component\Render\PlainTextOutput;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\Component\Utility\Mail as MailHelper;
|
||||
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
|
||||
use Drupal\Core\Messenger\MessengerTrait;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
|
@ -254,12 +254,8 @@ class MailManager extends DefaultPluginManager implements MailManagerInterface {
|
|||
// Return-Path headers should have a domain authorized to use the
|
||||
// originating SMTP server.
|
||||
$headers['Sender'] = $headers['Return-Path'] = $site_mail;
|
||||
// Headers are usually encoded in the mail plugin that implements
|
||||
// \Drupal\Core\Mail\MailInterface::mail(), for example,
|
||||
// \Drupal\Core\Mail\Plugin\Mail\PhpMail::mail(). The site name must be
|
||||
// encoded here to prevent mail plugins from encoding the email address,
|
||||
// which would break the header.
|
||||
$headers['From'] = Unicode::mimeHeaderEncode($site_config->get('name'), TRUE) . ' <' . $site_mail . '>';
|
||||
// Make sure the site-name is a RFC-2822 compliant 'display-name'.
|
||||
$headers['From'] = MailHelper::formatDisplayName($site_config->get('name')) . ' <' . $site_mail . '>';
|
||||
if ($reply) {
|
||||
$headers['Reply-to'] = $reply;
|
||||
}
|
||||
|
|
|
@ -106,11 +106,11 @@ class AliasStorage implements AliasStorageInterface {
|
|||
$this->catchException($e);
|
||||
$original = FALSE;
|
||||
}
|
||||
$fields['pid'] = $pid;
|
||||
$query = $this->connection->update(static::TABLE)
|
||||
->fields($fields)
|
||||
->condition('pid', $pid);
|
||||
$pid = $query->execute();
|
||||
$fields['pid'] = $pid;
|
||||
$fields['original'] = $original;
|
||||
$operation = 'update';
|
||||
}
|
||||
|
|
|
@ -270,7 +270,7 @@ class ContextDefinition implements ContextDefinitionInterface {
|
|||
public function setConstraints(array $constraints) {
|
||||
// If the backwards compatibility layer is present, delegate to that.
|
||||
if ($this->entityContextDefinition) {
|
||||
$this->entityContextDefinition->setConstraint();
|
||||
$this->entityContextDefinition->setConstraints($constraints);
|
||||
}
|
||||
|
||||
$this->constraints = $constraints;
|
||||
|
|
|
@ -48,7 +48,10 @@ trait ContextAwarePluginAssignmentTrait {
|
|||
];
|
||||
}
|
||||
|
||||
if (count($options) > 1 || !$definition->isRequired()) {
|
||||
// Show the context selector only if there is more than 1 option to choose
|
||||
// from. Also, show if there is a single option but the plugin does not
|
||||
// require a context.
|
||||
if (count($options) > 1 || (count($options) == 1 && !$definition->isRequired())) {
|
||||
$assignments = $plugin->getContextMapping();
|
||||
$element[$context_slot] = [
|
||||
'#title' => $definition->getLabel() ?: $this->t('Select a @context value:', ['@context' => $context_slot]),
|
||||
|
|
|
@ -70,7 +70,7 @@ abstract class ContextAwarePluginBase extends ComponentContextAwarePluginBase im
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function setContextValue($name, $value) {
|
||||
$this->context[$name] = Context::createFromContext($this->getContext($name), $value);
|
||||
$this->setContext($name, Context::createFromContext($this->getContext($name), $value));
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class File extends FormElement {
|
|||
*/
|
||||
public static function processFile(&$element, FormStateInterface $form_state, &$complete_form) {
|
||||
if ($element['#multiple']) {
|
||||
$element['#attributes'] = ['multiple' => 'multiple'];
|
||||
$element['#attributes']['multiple'] = 'multiple';
|
||||
$element['#name'] .= '[]';
|
||||
}
|
||||
return $element;
|
||||
|
|
|
@ -76,7 +76,9 @@ class StatusMessages extends RenderElement {
|
|||
public static function renderMessages($type = NULL) {
|
||||
$render = [];
|
||||
if (isset($type)) {
|
||||
$messages = \Drupal::messenger()->deleteByType($type);
|
||||
$messages = [
|
||||
$type => \Drupal::messenger()->deleteByType($type),
|
||||
];
|
||||
}
|
||||
else {
|
||||
$messages = \Drupal::messenger()->deleteAll();
|
||||
|
|
21
web/core/lib/Drupal/Core/Render/PreviewFallbackInterface.php
Normal file
21
web/core/lib/Drupal/Core/Render/PreviewFallbackInterface.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
/**
|
||||
* Allows an element to provide a fallback representation of itself for preview.
|
||||
*/
|
||||
interface PreviewFallbackInterface {
|
||||
|
||||
/**
|
||||
* Returns a string to be used as a fallback during preview.
|
||||
*
|
||||
* This is typically used when an element has no output and must be displayed,
|
||||
* for example during configuration.
|
||||
*
|
||||
* @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
|
||||
* A string representing for this.
|
||||
*/
|
||||
public function getPreviewFallbackString();
|
||||
|
||||
}
|
|
@ -125,6 +125,7 @@ class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterf
|
|||
throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
|
||||
}
|
||||
$collection = $this->applyRouteFilters($collection, $request);
|
||||
$collection = $this->applyFitOrder($collection);
|
||||
|
||||
if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
|
||||
return $this->applyRouteEnhancers($ret, $request);
|
||||
|
@ -286,6 +287,44 @@ class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterf
|
|||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reapplies the fit order to a RouteCollection object.
|
||||
*
|
||||
* Route filters can reorder route collections. For example, routes with an
|
||||
* explicit _format requirement will be preferred. This can result in a less
|
||||
* fit route being used. For example, as a result of filtering /user/% comes
|
||||
* before /user/login. In order to not break this fundamental property of
|
||||
* routes, we need to reapply the fit order. We also need to ensure that order
|
||||
* within each group of the same fit is preserved.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\RouteCollection $collection
|
||||
* The route collection.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\RouteCollection
|
||||
* The reordered route collection.
|
||||
*/
|
||||
protected function applyFitOrder(RouteCollection $collection) {
|
||||
$buckets = [];
|
||||
// Sort all the routes by fit descending.
|
||||
foreach ($collection->all() as $name => $route) {
|
||||
$fit = $route->compile()->getFit();
|
||||
$buckets += [$fit => []];
|
||||
$buckets[$fit][] = [$name, $route];
|
||||
}
|
||||
krsort($buckets);
|
||||
|
||||
$flattened = array_reduce($buckets, 'array_merge', []);
|
||||
|
||||
// Add them back onto a new route collection.
|
||||
$collection = new RouteCollection();
|
||||
foreach ($flattened as $pair) {
|
||||
$name = $pair[0];
|
||||
$route = $pair[1];
|
||||
$collection->add($name, $route);
|
||||
}
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Security;
|
||||
|
||||
use TYPO3\PharStreamWrapper\Assertable;
|
||||
use TYPO3\PharStreamWrapper\Helper;
|
||||
use TYPO3\PharStreamWrapper\Exception;
|
||||
|
||||
/**
|
||||
* An alternate PharExtensionInterceptor to support phar-based CLI tools.
|
||||
*
|
||||
* @see \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor
|
||||
*/
|
||||
class PharExtensionInterceptor implements Assertable {
|
||||
|
||||
/**
|
||||
* Determines whether phar file is allowed to execute.
|
||||
*
|
||||
* The phar file is allowed to execute if:
|
||||
* - the base file name has a ".phar" suffix.
|
||||
* - it is the CLI tool that has invoked the interceptor.
|
||||
*
|
||||
* @param string $path
|
||||
* The path of the phar file to check.
|
||||
* @param string $command
|
||||
* The command being carried out.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the phar file is allowed to execute.
|
||||
*
|
||||
* @throws Exception
|
||||
* Thrown when the file is not allowed to execute.
|
||||
*/
|
||||
public function assert($path, $command) {
|
||||
if ($this->baseFileContainsPharExtension($path)) {
|
||||
return TRUE;
|
||||
}
|
||||
throw new Exception(
|
||||
sprintf(
|
||||
'Unexpected file extension in "%s"',
|
||||
$path
|
||||
),
|
||||
1535198703
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a path has a .phar extension or invoked execution.
|
||||
*
|
||||
* @param string $path
|
||||
* The path of the phar file to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the file has a .phar extension or if the execution has been
|
||||
* invoked by the phar file.
|
||||
*/
|
||||
private function baseFileContainsPharExtension($path) {
|
||||
$baseFile = Helper::determineBaseFile($path);
|
||||
if ($baseFile === NULL) {
|
||||
return FALSE;
|
||||
}
|
||||
// If the stream wrapper is registered by invoking a phar file that does
|
||||
// not not have .phar extension then this should be allowed. For
|
||||
// example, some CLI tools recommend removing the extension.
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
// Find the last entry in the backtrace containing a 'file' key as
|
||||
// sometimes the last caller is executed outside the scope of a file. For
|
||||
// example, this occurs with shutdown functions.
|
||||
do {
|
||||
$caller = array_pop($backtrace);
|
||||
} while (empty($caller['file']) && !empty($backtrace));
|
||||
if (isset($caller['file']) && $baseFile === Helper::determineBaseFile($caller['file'])) {
|
||||
return TRUE;
|
||||
}
|
||||
$fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
|
||||
return strtolower($fileExtension) === 'phar';
|
||||
}
|
||||
|
||||
}
|
|
@ -218,6 +218,11 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
|
|||
|
||||
if ($this->isStarted()) {
|
||||
$old_session_id = $this->getId();
|
||||
// Save and close the old session. Call the parent method to avoid issue
|
||||
// with session destruction due to the session being considered obsolete.
|
||||
parent::save();
|
||||
// Ensure the session is reloaded correctly.
|
||||
$this->startedLazy = TRUE;
|
||||
}
|
||||
session_id(Crypt::randomBytesBase64());
|
||||
|
||||
|
@ -230,10 +235,7 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
|
|||
$this->migrateStoredSession($old_session_id);
|
||||
}
|
||||
|
||||
if (!$this->isStarted()) {
|
||||
// Start the session when it doesn't exist yet.
|
||||
$this->startNow();
|
||||
}
|
||||
$this->startNow();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Drupal\Core\TempStore;
|
||||
|
||||
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
||||
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
|
||||
use Drupal\Core\Lock\LockBackendInterface;
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
|
@ -27,6 +28,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
|
|||
* \Drupal\Core\TempStore\SharedTempStore.
|
||||
*/
|
||||
class PrivateTempStore {
|
||||
use DependencySerializationTrait;
|
||||
|
||||
/**
|
||||
* The key/value storage object used for this data.
|
||||
|
|
|
@ -126,10 +126,16 @@ class RecursiveContextualValidator implements ContextualValidatorInterface {
|
|||
$metadata = $this->metadataFactory->getMetadataFor($data);
|
||||
$cache_key = spl_object_hash($data);
|
||||
$property_path = $is_root_call ? '' : PropertyPath::append($previous_path, $data->getName());
|
||||
|
||||
// Prefer a specific instance of the typed data manager stored by the data
|
||||
// if it is available. This is necessary for specialized typed data objects,
|
||||
// for example those using the typed config subclass of the manager.
|
||||
$typed_data_manager = method_exists($data, 'getTypedDataManager') ? $data->getTypedDataManager() : $this->typedDataManager;
|
||||
|
||||
// Pass the canonical representation of the data as validated value to
|
||||
// constraint validators, such that they do not have to care about Typed
|
||||
// Data.
|
||||
$value = $this->typedDataManager->getCanonicalRepresentation($data);
|
||||
$value = $typed_data_manager->getCanonicalRepresentation($data);
|
||||
$this->context->setNode($value, $data, $metadata, $property_path);
|
||||
|
||||
if (isset($constraints) || !$this->context->isGroupValidated($cache_key, Constraint::DEFAULT_GROUP)) {
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Drupal\Core\Update;
|
|||
use Drupal\Core\DrupalKernel;
|
||||
use Drupal\Core\Session\AnonymousUserSession;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\Core\StackMiddleware\ReverseProxyMiddleware;
|
||||
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
@ -59,6 +60,7 @@ class UpdateKernel extends DrupalKernel {
|
|||
|
||||
// First boot up basic things, like loading the include files.
|
||||
$this->initializeSettings($request);
|
||||
ReverseProxyMiddleware::setSettingsOnRequest($request, Settings::getInstance());
|
||||
$this->boot();
|
||||
$container = $this->getContainer();
|
||||
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
|
||||
|
|
|
@ -326,4 +326,4 @@
|
|||
|
||||
// Expose constructor in the public space.
|
||||
Drupal.TableHeader = TableHeader;
|
||||
})(jQuery, Drupal, window.parent.Drupal.displace);
|
||||
})(jQuery, Drupal, window.Drupal.displace);
|
||||
|
|
|
@ -164,4 +164,4 @@
|
|||
});
|
||||
|
||||
Drupal.TableHeader = TableHeader;
|
||||
})(jQuery, Drupal, window.parent.Drupal.displace);
|
||||
})(jQuery, Drupal, window.Drupal.displace);
|
|
@ -70,9 +70,9 @@ abstract class AggregatorTestBase extends WebTestBase {
|
|||
$view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
|
||||
$this->assert(isset($view_link), 'The message area contains a link to a feed');
|
||||
|
||||
$fid = db_query("SELECT fid FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $edit['title[0][value]'], ':url' => $edit['url[0][value]']])->fetchField();
|
||||
$this->assertTrue(!empty($fid), 'The feed found in database.');
|
||||
return Feed::load($fid);
|
||||
$fids = \Drupal::entityQuery('aggregator_feed')->condition('title', $edit['title[0][value]'])->condition('url', $edit['url[0][value]'])->execute();
|
||||
$this->assertNotEmpty($fids, 'The feed found in database.');
|
||||
return Feed::load(array_values($fids)[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -179,10 +179,10 @@ abstract class AggregatorTestBase extends WebTestBase {
|
|||
$this->clickLink('Update items');
|
||||
|
||||
// Ensure we have the right number of items.
|
||||
$result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()]);
|
||||
$iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
|
||||
$feed->items = [];
|
||||
foreach ($result as $item) {
|
||||
$feed->items[] = $item->iid;
|
||||
foreach ($iids as $iid) {
|
||||
$feed->items[] = $iid;
|
||||
}
|
||||
|
||||
if ($expected_count !== NULL) {
|
||||
|
@ -211,11 +211,12 @@ abstract class AggregatorTestBase extends WebTestBase {
|
|||
* Expected number of feed items.
|
||||
*/
|
||||
public function updateAndDelete(FeedInterface $feed, $expected_count) {
|
||||
$count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
|
||||
$this->updateFeedItems($feed, $expected_count);
|
||||
$count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
|
||||
$count = $count_query->execute();
|
||||
$this->assertTrue($count);
|
||||
$this->deleteFeedItems($feed);
|
||||
$count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
|
||||
$count = $count_query->execute();
|
||||
$this->assertTrue($count == 0);
|
||||
}
|
||||
|
||||
|
@ -231,7 +232,7 @@ abstract class AggregatorTestBase extends WebTestBase {
|
|||
* TRUE if feed is unique.
|
||||
*/
|
||||
public function uniqueFeed($feed_name, $feed_url) {
|
||||
$result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed_name, ':url' => $feed_url])->fetchField();
|
||||
$result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed_name)->condition('url', $feed_url)->count()->execute();
|
||||
return (1 == $result);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,31 +20,23 @@ class AggregatorCronTest extends AggregatorTestBase {
|
|||
// Create feed and test basic updating on cron.
|
||||
$this->createSampleNodes();
|
||||
$feed = $this->createFeed();
|
||||
$count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
|
||||
|
||||
$this->cronRun();
|
||||
$this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
|
||||
$this->assertEqual(5, $count_query->execute());
|
||||
$this->deleteFeedItems($feed);
|
||||
$this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
|
||||
$this->assertEqual(0, $count_query->execute());
|
||||
$this->cronRun();
|
||||
$this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
|
||||
$this->assertEqual(5, $count_query->execute());
|
||||
|
||||
// Test feed locking when queued for update.
|
||||
$this->deleteFeedItems($feed);
|
||||
db_update('aggregator_feed')
|
||||
->condition('fid', $feed->id())
|
||||
->fields([
|
||||
'queued' => REQUEST_TIME,
|
||||
])
|
||||
->execute();
|
||||
$feed->setQueuedTime(REQUEST_TIME)->save();
|
||||
$this->cronRun();
|
||||
$this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
|
||||
db_update('aggregator_feed')
|
||||
->condition('fid', $feed->id())
|
||||
->fields([
|
||||
'queued' => 0,
|
||||
])
|
||||
->execute();
|
||||
$this->assertEqual(0, $count_query->execute());
|
||||
$feed->setQueuedTime(0)->save();
|
||||
$this->cronRun();
|
||||
$this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
|
||||
$this->assertEqual(5, $count_query->execute());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -67,9 +67,9 @@ abstract class AggregatorTestBase extends BrowserTestBase {
|
|||
$view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
|
||||
$this->assert(isset($view_link), 'The message area contains a link to a feed');
|
||||
|
||||
$fid = db_query("SELECT fid FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $edit['title[0][value]'], ':url' => $edit['url[0][value]']])->fetchField();
|
||||
$this->assertTrue(!empty($fid), 'The feed found in database.');
|
||||
return Feed::load($fid);
|
||||
$fids = \Drupal::entityQuery('aggregator_feed')->condition('title', $edit['title[0][value]'])->condition('url', $edit['url[0][value]'])->execute();
|
||||
$this->assertNotEmpty($fids, 'The feed found in database.');
|
||||
return Feed::load(array_values($fids)[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,10 +176,10 @@ abstract class AggregatorTestBase extends BrowserTestBase {
|
|||
$this->clickLink('Update items');
|
||||
|
||||
// Ensure we have the right number of items.
|
||||
$result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()]);
|
||||
$iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
|
||||
$feed->items = [];
|
||||
foreach ($result as $item) {
|
||||
$feed->items[] = $item->iid;
|
||||
foreach ($iids as $iid) {
|
||||
$feed->items[] = $iid;
|
||||
}
|
||||
|
||||
if ($expected_count !== NULL) {
|
||||
|
@ -208,11 +208,12 @@ abstract class AggregatorTestBase extends BrowserTestBase {
|
|||
* Expected number of feed items.
|
||||
*/
|
||||
public function updateAndDelete(FeedInterface $feed, $expected_count) {
|
||||
$count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
|
||||
$this->updateFeedItems($feed, $expected_count);
|
||||
$count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
|
||||
$count = $count_query->execute();
|
||||
$this->assertTrue($count);
|
||||
$this->deleteFeedItems($feed);
|
||||
$count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
|
||||
$count = $count_query->execute();
|
||||
$this->assertTrue($count == 0);
|
||||
}
|
||||
|
||||
|
@ -228,7 +229,7 @@ abstract class AggregatorTestBase extends BrowserTestBase {
|
|||
* TRUE if feed is unique.
|
||||
*/
|
||||
public function uniqueFeed($feed_name, $feed_url) {
|
||||
$result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed_name, ':url' => $feed_url])->fetchField();
|
||||
$result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed_name)->condition('url', $feed_url)->count()->execute();
|
||||
return (1 == $result);
|
||||
}
|
||||
|
||||
|
|
|
@ -43,8 +43,8 @@ class DeleteFeedTest extends AggregatorTestBase {
|
|||
$this->assertResponse(404, 'Deleted feed source does not exist.');
|
||||
|
||||
// Check database for feed.
|
||||
$result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed1->label(), ':url' => $feed1->getUrl()])->fetchField();
|
||||
$this->assertFalse($result, 'Feed not found in database');
|
||||
$result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed1->label())->condition('url', $feed1->getUrl())->count()->execute();
|
||||
$this->assertEquals(0, $result, 'Feed not found in database');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Drupal\Tests\aggregator\Functional;
|
|||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\aggregator\Entity\Feed;
|
||||
use Drupal\aggregator\Entity\Item;
|
||||
|
||||
/**
|
||||
* Tests the built-in feed parser with valid feed samples.
|
||||
|
@ -57,16 +58,17 @@ class FeedParserTest extends AggregatorTestBase {
|
|||
$this->assertText('Atom-Powered Robots Run Amok');
|
||||
$this->assertLinkByHref('http://example.org/2003/12/13/atom03');
|
||||
$this->assertText('Some text.');
|
||||
$this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', [':link' => 'http://example.org/2003/12/13/atom03'])->fetchField(), 'Atom entry id element is parsed correctly.');
|
||||
$iids = \Drupal::entityQuery('aggregator_item')->condition('link', 'http://example.org/2003/12/13/atom03')->execute();
|
||||
$item = Item::load(array_values($iids)[0]);
|
||||
$this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', $item->getGuid(), 'Atom entry id element is parsed correctly.');
|
||||
|
||||
// Check for second feed entry.
|
||||
$this->assertText('We tried to stop them, but we failed.');
|
||||
$this->assertLinkByHref('http://example.org/2003/12/14/atom03');
|
||||
$this->assertText('Some other text.');
|
||||
$db_guid = db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', [
|
||||
':link' => 'http://example.org/2003/12/14/atom03',
|
||||
])->fetchField();
|
||||
$this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a', $db_guid, 'Atom entry id element is parsed correctly.');
|
||||
$iids = \Drupal::entityQuery('aggregator_item')->condition('link', 'http://example.org/2003/12/14/atom03')->execute();
|
||||
$item = Item::load(array_values($iids)[0]);
|
||||
$this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a', $item->getGuid(), 'Atom entry id element is parsed correctly.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Drupal\Tests\aggregator\Functional;
|
||||
|
||||
use Drupal\aggregator\Entity\Feed;
|
||||
|
||||
/**
|
||||
* Tests OPML import.
|
||||
*
|
||||
|
@ -44,7 +46,8 @@ class ImportOpmlTest extends AggregatorTestBase {
|
|||
* Submits form filled with invalid fields.
|
||||
*/
|
||||
public function validateImportFormFields() {
|
||||
$before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
|
||||
$count_query = \Drupal::entityQuery('aggregator_feed')->count();
|
||||
$before = $count_query->execute();
|
||||
|
||||
$edit = [];
|
||||
$this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
|
||||
|
@ -62,7 +65,7 @@ class ImportOpmlTest extends AggregatorTestBase {
|
|||
$this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
|
||||
$this->assertText(t('The URL invalidUrl://empty is not valid.'), 'Error if the URL is invalid.');
|
||||
|
||||
$after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
|
||||
$after = $count_query->execute();
|
||||
$this->assertEqual($before, $after, 'No feeds were added during the three last form submissions.');
|
||||
}
|
||||
|
||||
|
@ -70,7 +73,8 @@ class ImportOpmlTest extends AggregatorTestBase {
|
|||
* Submits form with invalid, empty, and valid OPML files.
|
||||
*/
|
||||
protected function submitImportForm() {
|
||||
$before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
|
||||
$count_query = \Drupal::entityQuery('aggregator_feed')->count();
|
||||
$before = $count_query->execute();
|
||||
|
||||
$form['files[upload]'] = $this->getInvalidOpml();
|
||||
$this->drupalPostForm('admin/config/services/aggregator/add/opml', $form, t('Import'));
|
||||
|
@ -80,10 +84,12 @@ class ImportOpmlTest extends AggregatorTestBase {
|
|||
$this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
|
||||
$this->assertText(t('No new feed has been added.'), 'Attempting to load empty OPML from remote URL.');
|
||||
|
||||
$after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
|
||||
$after = $count_query->execute();
|
||||
$this->assertEqual($before, $after, 'No feeds were added during the two last form submissions.');
|
||||
|
||||
db_delete('aggregator_feed')->execute();
|
||||
foreach (Feed::loadMultiple() as $feed) {
|
||||
$feed->delete();
|
||||
}
|
||||
|
||||
$feeds[0] = $this->getFeedEditArray();
|
||||
$feeds[1] = $this->getFeedEditArray();
|
||||
|
@ -96,15 +102,15 @@ class ImportOpmlTest extends AggregatorTestBase {
|
|||
$this->assertRaw(t('A feed with the URL %url already exists.', ['%url' => $feeds[0]['url[0][value]']]), 'Verifying that a duplicate URL was identified');
|
||||
$this->assertRaw(t('A feed named %title already exists.', ['%title' => $feeds[1]['title[0][value]']]), 'Verifying that a duplicate title was identified');
|
||||
|
||||
$after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
|
||||
$after = $count_query->execute();
|
||||
$this->assertEqual($after, 2, 'Verifying that two distinct feeds were added.');
|
||||
|
||||
$feeds_from_db = db_query("SELECT title, url, refresh FROM {aggregator_feed}");
|
||||
$feed_entities = Feed::loadMultiple();
|
||||
$refresh = TRUE;
|
||||
foreach ($feeds_from_db as $feed) {
|
||||
$title[$feed->url] = $feed->title;
|
||||
$url[$feed->title] = $feed->url;
|
||||
$refresh = $refresh && $feed->refresh == 900;
|
||||
foreach ($feed_entities as $feed_entity) {
|
||||
$title[$feed_entity->getUrl()] = $feed_entity->label();
|
||||
$url[$feed_entity->label()] = $feed_entity->getUrl();
|
||||
$refresh = $refresh && $feed_entity->getRefreshRate() == 900;
|
||||
}
|
||||
|
||||
$this->assertEqual($title[$feeds[0]['url[0][value]']], $feeds[0]['title[0][value]'], 'First feed was added correctly.');
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Drupal\Tests\aggregator\Functional;
|
||||
|
||||
use Drupal\aggregator\Entity\Feed;
|
||||
use Drupal\aggregator\Entity\Item;
|
||||
|
||||
/**
|
||||
* Update feed items from a feed.
|
||||
|
@ -43,26 +44,24 @@ class UpdateFeedItemTest extends AggregatorTestBase {
|
|||
$view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
|
||||
$this->assert(isset($view_link), 'The message area contains a link to a feed');
|
||||
|
||||
$fid = db_query("SELECT fid FROM {aggregator_feed} WHERE url = :url", [':url' => $edit['url[0][value]']])->fetchField();
|
||||
$feed = Feed::load($fid);
|
||||
$fids = \Drupal::entityQuery('aggregator_feed')->condition('url', $edit['url[0][value]'])->execute();
|
||||
$feed = Feed::load(array_values($fids)[0]);
|
||||
|
||||
$feed->refreshItems();
|
||||
$before = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
|
||||
$iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
|
||||
$before = Item::load(array_values($iids)[0])->getPostedTime();
|
||||
|
||||
// Sleep for 3 second.
|
||||
sleep(3);
|
||||
db_update('aggregator_feed')
|
||||
->condition('fid', $feed->id())
|
||||
->fields([
|
||||
'checked' => 0,
|
||||
'hash' => '',
|
||||
'etag' => '',
|
||||
'modified' => 0,
|
||||
])
|
||||
->execute();
|
||||
$feed
|
||||
->setLastCheckedTime(0)
|
||||
->setHash('')
|
||||
->setEtag('')
|
||||
->setLastModified(0)
|
||||
->save();
|
||||
$feed->refreshItems();
|
||||
|
||||
$after = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
|
||||
$after = Item::load(array_values($iids)[0])->getPostedTime();
|
||||
$this->assertTrue($before === $after, format_string('Publish timestamp of feed item was not updated (@before === @after)', ['@before' => $before, '@after' => $after]));
|
||||
|
||||
// Make sure updating items works even after uninstalling a module
|
||||
|
|
|
@ -93,7 +93,8 @@
|
|||
*/
|
||||
Drupal.behaviors.blockHighlightPlacement = {
|
||||
attach(context, settings) {
|
||||
if (settings.blockPlacement) {
|
||||
// Ensure that the block we are attempting to scroll to actually exists.
|
||||
if (settings.blockPlacement && $('.js-block-placed').length) {
|
||||
$(context)
|
||||
.find('[data-drupal-selector="edit-blocks"]')
|
||||
.once('block-highlight')
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
Drupal.behaviors.blockHighlightPlacement = {
|
||||
attach: function attach(context, settings) {
|
||||
if (settings.blockPlacement) {
|
||||
if (settings.blockPlacement && $('.js-block-placed').length) {
|
||||
$(context).find('[data-drupal-selector="edit-blocks"]').once('block-highlight').each(function () {
|
||||
var $container = $(this);
|
||||
|
||||
|
|
|
@ -193,6 +193,9 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
if ($this->request->query->has('block-placement')) {
|
||||
$placement = $this->request->query->get('block-placement');
|
||||
$form['#attached']['drupalSettings']['blockPlacement'] = $placement;
|
||||
// Remove the block placement from the current request so that it is not
|
||||
// passed on to any redirect destinations.
|
||||
$this->request->query->remove('block-placement');
|
||||
}
|
||||
|
||||
// Loop over each region and build blocks.
|
||||
|
@ -378,9 +381,6 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
$entity->save();
|
||||
}
|
||||
$this->messenger->addStatus($this->t('The block settings have been updated.'));
|
||||
|
||||
// Remove any previously set block placement.
|
||||
$this->request->query->remove('block-placement');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\block\Plugin\migrate\source\d7;
|
||||
|
||||
use Drupal\block\Plugin\migrate\source\Block;
|
||||
|
||||
/**
|
||||
* Gets i18n block data from source database.
|
||||
*
|
||||
* @MigrateSource(
|
||||
* id = "d7_block_translation",
|
||||
* source_module = "i18n_block"
|
||||
* )
|
||||
*/
|
||||
class BlockTranslation extends Block {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
// Let the parent set the block table to use, but do not use the parent
|
||||
// query. Instead build a query so can use an inner join to the selected
|
||||
// block table.
|
||||
parent::query();
|
||||
$query = $this->select('i18n_string', 'i18n')
|
||||
->fields('i18n')
|
||||
->fields('b', [
|
||||
'bid',
|
||||
'module',
|
||||
'delta',
|
||||
'theme',
|
||||
'status',
|
||||
'weight',
|
||||
'region',
|
||||
'custom',
|
||||
'visibility',
|
||||
'pages',
|
||||
'title',
|
||||
'cache',
|
||||
'i18n_mode',
|
||||
])
|
||||
->fields('lt', [
|
||||
'lid',
|
||||
'translation',
|
||||
'language',
|
||||
'plid',
|
||||
'plural',
|
||||
'i18n_status',
|
||||
])
|
||||
->condition('i18n_mode', 1);
|
||||
$query->leftjoin($this->blockTable, 'b', ('b.delta = i18n.objectid'));
|
||||
$query->leftjoin('locales_target', 'lt', 'lt.lid = i18n.lid');
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fields() {
|
||||
return [
|
||||
'bid' => $this->t('The block numeric identifier.'),
|
||||
'module' => $this->t('The module providing the block.'),
|
||||
'delta' => $this->t("The block's delta."),
|
||||
'theme' => $this->t('Which theme the block is placed in.'),
|
||||
'status' => $this->t('Block enabled status'),
|
||||
'weight' => $this->t('Block weight within region'),
|
||||
'region' => $this->t('Theme region within which the block is set'),
|
||||
'visibility' => $this->t('Visibility'),
|
||||
'pages' => $this->t('Pages list.'),
|
||||
'title' => $this->t('Block title.'),
|
||||
'cache' => $this->t('Cache rule.'),
|
||||
'i18n_mode' => $this->t('Multilingual mode'),
|
||||
'lid' => $this->t('Language string ID'),
|
||||
'textgroup' => $this->t('A module defined group of translations'),
|
||||
'context' => $this->t('Full string ID for quick search: type:objectid:property.'),
|
||||
'objectid' => $this->t('Object ID'),
|
||||
'type' => $this->t('Object type for this string'),
|
||||
'property' => $this->t('Object property for this string'),
|
||||
'objectindex' => $this->t('Integer value of Object ID'),
|
||||
'format' => $this->t('The {filter_format}.format of the string'),
|
||||
'translation' => $this->t('Translation'),
|
||||
'language' => $this->t('Language code'),
|
||||
'plid' => $this->t('Parent lid'),
|
||||
'plural' => $this->t('Plural index number'),
|
||||
'i18n_status' => $this->t('Translation needs update'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIds() {
|
||||
$ids['delta']['type'] = 'string';
|
||||
$ids['delta']['alias'] = 'b';
|
||||
$ids['language']['type'] = 'string';
|
||||
return $ids;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\block_test\Plugin\Block;
|
||||
|
||||
use Drupal\Core\Block\BlockBase;
|
||||
|
||||
/**
|
||||
* Provides a context-aware block that uses a not-passed, non-required context.
|
||||
*
|
||||
* @Block(
|
||||
* id = "test_context_aware_no_valid_context_options",
|
||||
* admin_label = @Translation("Test context-aware block - no valid context options"),
|
||||
* context_definitions = {
|
||||
* "email" = @ContextDefinition("email", required = FALSE)
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class TestContextAwareNoValidContextOptionsBlock extends BlockBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build() {
|
||||
return [
|
||||
'#markup' => 'Rendered block with no valid context options',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
namespace Drupal\Tests\block\Functional;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
|
@ -239,6 +241,13 @@ class BlockUiTest extends BrowserTestBase {
|
|||
$this->assertText('User context found.');
|
||||
$this->assertRaw($expected_text);
|
||||
|
||||
// Test context mapping form element is not visible if there are no valid
|
||||
// context options for the block (the test_context_aware_no_valid_context_options
|
||||
// block has one context defined which is not available for it on the
|
||||
// Block Layout interface).
|
||||
$this->drupalGet('admin/structure/block/add/test_context_aware_no_valid_context_options/classy');
|
||||
$this->assertSession()->fieldNotExists('edit-settings-context-mapping-email');
|
||||
|
||||
// Test context mapping allows empty selection for optional contexts.
|
||||
$this->drupalGet('admin/structure/block/manage/testcontextawareblock');
|
||||
$edit = [
|
||||
|
@ -281,6 +290,24 @@ class BlockUiTest extends BrowserTestBase {
|
|||
* Tests the block placement indicator.
|
||||
*/
|
||||
public function testBlockPlacementIndicator() {
|
||||
// Test the block placement indicator with using the domain as URL language
|
||||
// indicator. This causes destination query parameters to be absolute URLs.
|
||||
\Drupal::service('module_installer')->install(['language', 'locale']);
|
||||
$this->container = \Drupal::getContainer();
|
||||
ConfigurableLanguage::createFromLangcode('it')->save();
|
||||
$config = $this->config('language.types');
|
||||
$config->set('negotiation.language_interface.enabled', [
|
||||
LanguageNegotiationUrl::METHOD_ID => -10,
|
||||
]);
|
||||
$config->save();
|
||||
$config = $this->config('language.negotiation');
|
||||
$config->set('url.source', LanguageNegotiationUrl::CONFIG_DOMAIN);
|
||||
$config->set('url.domains', [
|
||||
'en' => \Drupal::request()->getHost(),
|
||||
'it' => 'it.example.com',
|
||||
]);
|
||||
$config->save();
|
||||
|
||||
// Select the 'Powered by Drupal' block to be placed.
|
||||
$block = [];
|
||||
$block['id'] = strtolower($this->randomMachineName());
|
||||
|
@ -289,11 +316,30 @@ class BlockUiTest extends BrowserTestBase {
|
|||
|
||||
// After adding a block, it will indicate which block was just added.
|
||||
$this->drupalPostForm('admin/structure/block/add/system_powered_by_block', $block, t('Save block'));
|
||||
$this->assertUrl('admin/structure/block/list/classy?block-placement=' . Html::getClass($block['id']));
|
||||
$this->assertSession()->addressEquals('admin/structure/block/list/classy?block-placement=' . Html::getClass($block['id']));
|
||||
|
||||
// Resaving the block page will remove the block indicator.
|
||||
// Resaving the block page will remove the block placement indicator.
|
||||
$this->drupalPostForm(NULL, [], t('Save blocks'));
|
||||
$this->assertUrl('admin/structure/block/list/classy');
|
||||
$this->assertSession()->addressEquals('admin/structure/block/list/classy');
|
||||
|
||||
// Place another block and test the remove functionality works with the
|
||||
// block placement indicator. Click the first 'Place block' link to bring up
|
||||
// the list of blocks to place in the first available region.
|
||||
$this->clickLink('Place block');
|
||||
// Select the first available block.
|
||||
$this->clickLink('Place block');
|
||||
$block = [];
|
||||
$block['id'] = strtolower($this->randomMachineName());
|
||||
$block['theme'] = 'classy';
|
||||
$this->submitForm([], 'Save block');
|
||||
$this->assertSession()->addressEquals('admin/structure/block/list/classy?block-placement=' . Html::getClass($block['id']));
|
||||
|
||||
// Removing a block will remove the block placement indicator.
|
||||
$this->clickLink('Remove');
|
||||
$this->submitForm([], 'Remove');
|
||||
// @todo https://www.drupal.org/project/drupal/issues/2980527 this should be
|
||||
// 'admin/structure/block/list/classy' but there is a bug.
|
||||
$this->assertSession()->addressEquals('admin/structure/block');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\block\Kernel\Migrate\d7;
|
||||
|
||||
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
|
||||
|
||||
/**
|
||||
* Tests migration of i18n block translations.
|
||||
*
|
||||
* @group migrate_drupal_7
|
||||
*/
|
||||
class MigrateBlockContentTranslationTest extends MigrateDrupal7TestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'text',
|
||||
'aggregator',
|
||||
'book',
|
||||
'block',
|
||||
'comment',
|
||||
'forum',
|
||||
'views',
|
||||
'block_content',
|
||||
'config_translation',
|
||||
'content_translation',
|
||||
'language',
|
||||
'statistics',
|
||||
'taxonomy',
|
||||
// Required for translation migrations.
|
||||
'migrate_drupal_multilingual',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installConfig(['block']);
|
||||
$this->installConfig(['block_content']);
|
||||
$this->installEntitySchema('block_content');
|
||||
|
||||
$this->executeMigrations([
|
||||
'language',
|
||||
'd7_filter_format',
|
||||
'block_content_type',
|
||||
'block_content_body_field',
|
||||
'd7_custom_block',
|
||||
'd7_user_role',
|
||||
'd7_block',
|
||||
'd7_block_translation',
|
||||
]);
|
||||
block_rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the migration of block title translation.
|
||||
*/
|
||||
public function testBlockContentTranslation() {
|
||||
/** @var \Drupal\language\ConfigurableLanguageManagerInterface $language_manager */
|
||||
$language_manager = $this->container->get('language_manager');
|
||||
|
||||
$config = $language_manager->getLanguageConfigOverride('fr', 'block.block.bartik_user_login');
|
||||
$this->assertSame('fr - User login title', $config->get('settings.label'));
|
||||
}
|
||||
|
||||
}
|
|
@ -112,10 +112,10 @@ class MigrateBlockTest extends MigrateDrupal7TestBase {
|
|||
public function testBlockMigration() {
|
||||
$this->assertEntity('bartik_system_main', 'system_main_block', [], '', 'content', 'bartik', 0, '', '0');
|
||||
$this->assertEntity('bartik_search_form', 'search_form_block', [], '', 'sidebar_first', 'bartik', -1, '', '0');
|
||||
$this->assertEntity('bartik_user_login', 'user_login_block', [], '', 'sidebar_first', 'bartik', 0, '', '0');
|
||||
$this->assertEntity('bartik_user_login', 'user_login_block', [], '', 'sidebar_first', 'bartik', 0, 'User login title', 'visible');
|
||||
$this->assertEntity('bartik_system_powered_by', 'system_powered_by_block', [], '', 'footer_fifth', 'bartik', 10, '', '0');
|
||||
$this->assertEntity('seven_system_main', 'system_main_block', [], '', 'content', 'seven', 0, '', '0');
|
||||
$this->assertEntity('seven_user_login', 'user_login_block', [], '', 'content', 'seven', 10, '', '0');
|
||||
$this->assertEntity('seven_user_login', 'user_login_block', [], '', 'content', 'seven', 10, 'User login title', 'visible');
|
||||
|
||||
// The d7_custom_block migration should have migrated a block containing a
|
||||
// mildly amusing limerick. We'll need its UUID to determine
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\block\Kernel\Plugin\migrate\source\d7;
|
||||
|
||||
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
|
||||
|
||||
/**
|
||||
* Tests i18n block source plugin.
|
||||
*
|
||||
* @covers \Drupal\block\Plugin\migrate\source\d7\BlockTranslation
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class BlockTranslationTest extends MigrateSqlSourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['block', 'migrate_drupal'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function providerSource() {
|
||||
|
||||
// The source data.
|
||||
$tests[0]['source_data']['block'] = [
|
||||
[
|
||||
'bid' => 1,
|
||||
'module' => 'system',
|
||||
'delta' => 'main',
|
||||
'theme' => 'bartik',
|
||||
'status' => 1,
|
||||
'weight' => 0,
|
||||
'region' => 'content',
|
||||
'custom' => '0',
|
||||
'visibility' => 0,
|
||||
'pages' => '',
|
||||
'title' => '',
|
||||
'cache' => -1,
|
||||
'i18n_mode' => 0,
|
||||
],
|
||||
[
|
||||
'bid' => 2,
|
||||
'module' => 'system',
|
||||
'delta' => 'navigation',
|
||||
'theme' => 'bartik',
|
||||
'status' => 1,
|
||||
'weight' => 0,
|
||||
'region' => 'sidebar_first',
|
||||
'custom' => '0',
|
||||
'visibility' => 0,
|
||||
'pages' => '',
|
||||
'title' => 'Navigation',
|
||||
'cache' => -1,
|
||||
'i18n_mode' => 1,
|
||||
],
|
||||
];
|
||||
$tests[0]['source_data']['block_role'] = [
|
||||
[
|
||||
'module' => 'block',
|
||||
'delta' => 1,
|
||||
'rid' => 2,
|
||||
],
|
||||
[
|
||||
'module' => 'block',
|
||||
'delta' => 2,
|
||||
'rid' => 2,
|
||||
],
|
||||
[
|
||||
'module' => 'block',
|
||||
'delta' => 2,
|
||||
'rid' => 100,
|
||||
],
|
||||
];
|
||||
$tests[0]['source_data']['i18n_string'] = [
|
||||
[
|
||||
'lid' => 1,
|
||||
'textgroup' => 'block',
|
||||
'context' => '1',
|
||||
'objectid' => 'navigation',
|
||||
'type' => 'system',
|
||||
'property' => 'title',
|
||||
'objectindex' => 0,
|
||||
'format' => '',
|
||||
],
|
||||
];
|
||||
|
||||
$tests[0]['source_data']['locales_target'] = [
|
||||
[
|
||||
'lid' => 1,
|
||||
'translation' => 'fr - Navigation',
|
||||
'language' => 'fr',
|
||||
'plid' => 0,
|
||||
'plural' => 0,
|
||||
'i18n_status' => 0,
|
||||
],
|
||||
];
|
||||
$tests[0]['source_data']['role'] = [
|
||||
[
|
||||
'rid' => 2,
|
||||
'name' => 'authenticated user',
|
||||
],
|
||||
];
|
||||
$tests[0]['source_data']['system'] = [
|
||||
[
|
||||
'filename' => 'modules/system/system.module',
|
||||
'name' => 'system',
|
||||
'type' => 'module',
|
||||
'owner' => '',
|
||||
'status' => '1',
|
||||
'throttle' => '0',
|
||||
'bootstrap' => '0',
|
||||
'schema_version' => '7055',
|
||||
'weight' => '0',
|
||||
'info' => 'a:0:{}',
|
||||
],
|
||||
];
|
||||
// The expected results.
|
||||
$tests[0]['expected_data'] = [
|
||||
[
|
||||
'bid' => 2,
|
||||
'module' => 'system',
|
||||
'delta' => 'navigation',
|
||||
'theme' => 'bartik',
|
||||
'status' => 1,
|
||||
'weight' => 0,
|
||||
'region' => 'sidebar_first',
|
||||
'custom' => '0',
|
||||
'visibility' => 0,
|
||||
'pages' => '',
|
||||
'title' => 'Navigation',
|
||||
'cache' => -1,
|
||||
'i18n_mode' => 1,
|
||||
'lid' => 1,
|
||||
'translation' => 'fr - Navigation',
|
||||
'language' => 'fr',
|
||||
'plid' => 0,
|
||||
'plural' => 0,
|
||||
'i18n_status' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
}
|
|
@ -293,8 +293,8 @@ class BlockContentAccessHandlerTest extends KernelTestBase {
|
|||
'forbidden',
|
||||
],
|
||||
];
|
||||
return $cases;
|
||||
}
|
||||
return $cases;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -46,6 +46,17 @@ class MigrateCommentTypeTest extends MigrateDrupal7TestBase {
|
|||
* Tests the migrated comment types.
|
||||
*/
|
||||
public function testMigration() {
|
||||
$comment_fields = [
|
||||
'comment' => 'Default comment setting',
|
||||
'comment_default_mode' => 'Default display mode',
|
||||
'comment_default_per_page' => 'Default comments per page',
|
||||
'comment_anonymous' => 'Anonymous commenting',
|
||||
'comment_subject_field' => 'Comment subject field',
|
||||
'comment_preview' => 'Preview comment',
|
||||
'comment_form_location' => 'Location of comment submission form',
|
||||
];
|
||||
$this->assertArraySubset($comment_fields, $this->migration->getSourcePlugin()->fields());
|
||||
|
||||
$this->assertEntity('comment_node_article', 'Article comment');
|
||||
$this->assertEntity('comment_node_blog', 'Blog entry comment');
|
||||
$this->assertEntity('comment_node_book', 'Book page comment');
|
||||
|
|
|
@ -184,11 +184,16 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
// Sync translations.
|
||||
if ($entity->getEntityType()->hasKey('langcode')) {
|
||||
$entity_langcode = $entity->language()->getId();
|
||||
if (!$content_moderation_state->hasTranslation($entity_langcode)) {
|
||||
$content_moderation_state->addTranslation($entity_langcode);
|
||||
if ($entity->isDefaultTranslation()) {
|
||||
$content_moderation_state->langcode = $entity_langcode;
|
||||
}
|
||||
if ($content_moderation_state->language()->getId() !== $entity_langcode) {
|
||||
$content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
|
||||
else {
|
||||
if (!$content_moderation_state->hasTranslation($entity_langcode)) {
|
||||
$content_moderation_state->addTranslation($entity_langcode);
|
||||
}
|
||||
if ($content_moderation_state->language()->getId() !== $entity_langcode) {
|
||||
$content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ class ModerationStateFieldItemList extends FieldItemList {
|
|||
if ($entity->getEntityType()->hasKey('langcode')) {
|
||||
$langcode = $entity->language()->getId();
|
||||
if (!$content_moderation_state->hasTranslation($langcode)) {
|
||||
$content_moderation_state->addTranslation($langcode);
|
||||
$content_moderation_state->addTranslation($langcode, $content_moderation_state->toArray());
|
||||
}
|
||||
if ($content_moderation_state->language()->getId() !== $langcode) {
|
||||
$content_moderation_state = $content_moderation_state->getTranslation($langcode);
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
|
||||
/**
|
||||
* Test content_moderation functionality with content_translation.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationContentTranslationTest extends BrowserTestBase {
|
||||
|
||||
use ContentModerationTestTrait;
|
||||
|
||||
/**
|
||||
* A user with permission to bypass access content.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'locale',
|
||||
'content_translation',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->rootUser);
|
||||
// Create an Article content type.
|
||||
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article'])->save();
|
||||
$edit = [
|
||||
'predefined_langcode' => 'fr',
|
||||
];
|
||||
$this->drupalPostForm('admin/config/regional/language/add', $edit, 'Add language');
|
||||
// Enable content translation on articles.
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
$edit = [
|
||||
'entity_types[node]' => TRUE,
|
||||
'settings[node][article][translatable]' => TRUE,
|
||||
'settings[node][article][settings][language][language_alterable]' => TRUE,
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, 'Save configuration');
|
||||
// Adding languages requires a container rebuild in the test running
|
||||
// environment so that multilingual services are used.
|
||||
$this->rebuildContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests existing translations being edited after enabling content moderation.
|
||||
*/
|
||||
public function testModerationWithExistingContent() {
|
||||
// Create a published article in English.
|
||||
$edit = [
|
||||
'title[0][value]' => 'Published English node',
|
||||
'langcode[0][value]' => 'en',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, 'Save');
|
||||
$this->assertSession()->pageTextContains('Article Published English node has been created.');
|
||||
$english_node = $this->drupalGetNodeByTitle('Published English node');
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink('Add');
|
||||
$edit = [
|
||||
'title[0][value]' => 'Published French node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, 'Save (this translation)');
|
||||
$this->assertSession()->pageTextContains('Article Published French node has been updated.');
|
||||
|
||||
// Install content moderation and enable moderation on Article node type.
|
||||
\Drupal::service('module_installer')->install(['content_moderation']);
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
|
||||
$workflow->save();
|
||||
$this->drupalLogin($this->rootUser);
|
||||
|
||||
// Edit the English node.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/edit');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$edit = [
|
||||
'title[0][value]' => 'Published English new node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, 'Save');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains('Article Published English new node has been updated.');
|
||||
// Edit the French translation.
|
||||
$this->drupalGet('fr/node/' . $english_node->id() . '/edit');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$edit = [
|
||||
'title[0][value]' => 'Published French new node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, 'Save (this translation)');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains('Article Published French new node has been updated.');
|
||||
}
|
||||
|
||||
}
|
|
@ -296,7 +296,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
// Create a French translation.
|
||||
$french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
|
||||
$french_node->setUnpublished();
|
||||
// Revision 1 (fr).
|
||||
// Revision 2 (fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
|
||||
$this->assertEquals('draft', $french_node->moderation_state->value);
|
||||
|
@ -305,7 +305,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
// Move English node to create another draft.
|
||||
$english_node = $this->reloadEntity($english_node);
|
||||
$english_node->moderation_state->value = 'draft';
|
||||
// Revision 2 (en, fr).
|
||||
// Revision 3 (en, fr).
|
||||
$english_node->save();
|
||||
$english_node = $this->reloadEntity($english_node);
|
||||
$this->assertEquals('draft', $english_node->moderation_state->value);
|
||||
|
@ -316,7 +316,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
|
||||
// Publish the French node.
|
||||
$french_node->moderation_state->value = 'published';
|
||||
// Revision 3 (en, fr).
|
||||
// Revision 4 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadEntity($french_node)->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
|
@ -327,7 +327,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
|
||||
// Publish the English node.
|
||||
$english_node->moderation_state->value = 'published';
|
||||
// Revision 4 (en, fr).
|
||||
// Revision 5 (en, fr).
|
||||
$english_node->save();
|
||||
$english_node = $this->reloadEntity($english_node);
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
|
@ -336,15 +336,15 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$french_node->moderation_state->value = 'draft';
|
||||
// Revision 5 (en, fr).
|
||||
// Revision 6 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadEntity($english_node, 5)->getTranslation('fr');
|
||||
$french_node = $this->reloadEntity($english_node, 6)->getTranslation('fr');
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
$this->assertTrue($french_node->getTranslation('en')->isPublished());
|
||||
|
||||
// Republish the French node.
|
||||
$french_node->moderation_state->value = 'published';
|
||||
// Revision 6 (en, fr).
|
||||
// Revision 7 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
|
@ -353,7 +353,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$content_moderation_state = ContentModerationState::load(1);
|
||||
$content_moderation_state->set('moderation_state', 'draft');
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
// Revision 7 (en, fr).
|
||||
// Revision 8 (en, fr).
|
||||
$content_moderation_state->save();
|
||||
$english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
|
||||
|
||||
|
@ -366,12 +366,12 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$content_moderation_state = $content_moderation_state->getTranslation('fr');
|
||||
$content_moderation_state->set('moderation_state', 'draft');
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
// Revision 8 (en, fr).
|
||||
// Revision 9 (en, fr).
|
||||
$content_moderation_state->save();
|
||||
|
||||
$english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
|
||||
$this->assertEquals('draft', $english_node->moderation_state->value);
|
||||
$french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
|
||||
$french_node = $this->reloadEntity($english_node, '9')->getTranslation('fr');
|
||||
$this->assertEquals('draft', $french_node->moderation_state->value);
|
||||
// Switching the moderation state to an unpublished state should update the
|
||||
// entity.
|
||||
|
@ -380,7 +380,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
// Get the default english node.
|
||||
$english_node = $this->reloadEntity($english_node);
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
$this->assertEquals(6, $english_node->getRevisionId());
|
||||
$this->assertEquals(7, $english_node->getRevisionId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -416,25 +416,83 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
|
||||
/**
|
||||
* Tests that entities with special languages can be moderated.
|
||||
*
|
||||
* @dataProvider moderationWithSpecialLanguagesTestCases
|
||||
*/
|
||||
public function testModerationWithSpecialLanguages() {
|
||||
public function testModerationWithSpecialLanguages($original_language, $updated_language) {
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
|
||||
$workflow->save();
|
||||
|
||||
// Create a test entity.
|
||||
$entity = EntityTestRev::create([
|
||||
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
'langcode' => $original_language,
|
||||
]);
|
||||
$entity->save();
|
||||
$this->assertEquals('draft', $entity->moderation_state->value);
|
||||
|
||||
$entity->moderation_state->value = 'published';
|
||||
$entity->langcode = $updated_language;
|
||||
$entity->save();
|
||||
|
||||
$this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for ::testModerationWithSpecialLanguages().
|
||||
*/
|
||||
public function moderationWithSpecialLanguagesTestCases() {
|
||||
return [
|
||||
'Not specified to not specified' => [
|
||||
LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
],
|
||||
'English to not specified' => [
|
||||
'en',
|
||||
LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
],
|
||||
'Not specified to english' => [
|
||||
LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
'en',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test changing the language of content without adding a translation.
|
||||
*/
|
||||
public function testChangingContentLangcode() {
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
NodeType::create([
|
||||
'type' => 'test_type',
|
||||
])->save();
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
|
||||
$workflow->save();
|
||||
|
||||
$entity = Node::create([
|
||||
'title' => 'Test node',
|
||||
'langcode' => 'en',
|
||||
'type' => 'test_type',
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
|
||||
$this->assertCount(1, $entity->getTranslationLanguages());
|
||||
$this->assertCount(1, $content_moderation_state->getTranslationLanguages());
|
||||
$this->assertEquals('en', $entity->langcode->value);
|
||||
$this->assertEquals('en', $content_moderation_state->langcode->value);
|
||||
|
||||
$entity->langcode = 'fr';
|
||||
$entity->save();
|
||||
|
||||
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
|
||||
$this->assertCount(1, $entity->getTranslationLanguages());
|
||||
$this->assertCount(1, $content_moderation_state->getTranslationLanguages());
|
||||
$this->assertEquals('fr', $entity->langcode->value);
|
||||
$this->assertEquals('fr', $content_moderation_state->langcode->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a non-translatable entity type with a langcode can be moderated.
|
||||
*/
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList
|
||||
|
@ -64,6 +66,8 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
|
|||
$this->testNode->save();
|
||||
\Drupal::entityTypeManager()->getStorage('node')->resetCache();
|
||||
$this->testNode = Node::load($this->testNode->id());
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('de')->save();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -332,4 +336,37 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the field item list when used with existing unmoderated content.
|
||||
*/
|
||||
public function testWithExistingUnmoderatedContent() {
|
||||
$node = Node::create([
|
||||
'title' => 'Test title',
|
||||
'type' => 'unmoderated',
|
||||
]);
|
||||
$node->save();
|
||||
$translation = $node->addTranslation('de', $node->toArray());
|
||||
$translation->title = 'Translated';
|
||||
$translation->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'unmoderated');
|
||||
$workflow->save();
|
||||
|
||||
// After enabling moderation, both the original node and translation should
|
||||
// have a published moderation state.
|
||||
$node = Node::load($node->id());
|
||||
$translation = $node->getTranslation('de');
|
||||
$this->assertEquals('published', $node->moderation_state->value);
|
||||
$this->assertEquals('published', $translation->moderation_state->value);
|
||||
|
||||
// After the node has been updated, both the original node and translation
|
||||
// should still have a value.
|
||||
$node->title = 'Updated title';
|
||||
$node->save();
|
||||
$translation = $node->getTranslation('de');
|
||||
$this->assertEquals('published', $node->moderation_state->value);
|
||||
$this->assertEquals('published', $translation->moderation_state->value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
id: d6_taxonomy_term_localized_translation
|
||||
label: Taxonomy localized term translations
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d6_term_localized_translation
|
||||
translations: true
|
||||
process:
|
||||
# If you are using this file to build a custom migration consider removing
|
||||
# the tid field to allow incremental migrations.
|
||||
tid: tid
|
||||
langcode: language
|
||||
vid:
|
||||
plugin: migration
|
||||
migration: d6_taxonomy_vocabulary
|
||||
source: vid
|
||||
name:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- name_translated
|
||||
- name
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
description:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- description_translated
|
||||
- description
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
destination:
|
||||
plugin: entity:taxonomy_term
|
||||
translations: true
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d6_taxonomy_term
|
|
@ -0,0 +1,77 @@
|
|||
id: d7_block_translation
|
||||
label: Block translation
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Configuration
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d7_block_translation
|
||||
constants:
|
||||
dest_label: 'settings/label'
|
||||
process:
|
||||
multilingual:
|
||||
plugin: skip_on_empty
|
||||
source: i18n_mode
|
||||
method: row
|
||||
langcode: language
|
||||
property: constants/dest_label
|
||||
translation: translation
|
||||
id:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
migration: d7_block
|
||||
source:
|
||||
- module
|
||||
- delta
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
# The plugin process is copied from d7_block.yml
|
||||
plugin:
|
||||
-
|
||||
plugin: static_map
|
||||
bypass: true
|
||||
source:
|
||||
- module
|
||||
- delta
|
||||
map:
|
||||
book:
|
||||
navigation: book_navigation
|
||||
comment:
|
||||
recent: views_block:comments_recent-block_1
|
||||
forum:
|
||||
active: forum_active_block
|
||||
new: forum_new_block
|
||||
# locale:
|
||||
# 0: language_block
|
||||
node:
|
||||
syndicate: node_syndicate_block
|
||||
search:
|
||||
form: search_form_block
|
||||
statistics:
|
||||
popular: statistics_popular_block
|
||||
system:
|
||||
main: system_main_block
|
||||
'powered-by': system_powered_by_block
|
||||
user:
|
||||
login: user_login_block
|
||||
# 1: system_menu_block:tools
|
||||
new: views_block:who_s_new-block_1
|
||||
online: views_block:who_s_online-who_s_online_block
|
||||
-
|
||||
plugin: block_plugin_id
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
# The theme process is copied from d7_block.yml
|
||||
theme:
|
||||
plugin: block_theme
|
||||
source:
|
||||
- theme
|
||||
- default_theme
|
||||
- admin_theme
|
||||
destination:
|
||||
plugin: entity:block
|
||||
migration_dependencies:
|
||||
optional:
|
||||
- d7_block
|
|
@ -108,7 +108,12 @@ class MigrateTaxonomyTermTranslationTest extends MigrateDrupal6TestBase {
|
|||
|
||||
$this->assertArrayHasKey($tid, $this->treeData[$vid], "Term $tid exists in taxonomy tree");
|
||||
$term = $this->treeData[$vid][$tid];
|
||||
$this->assertEquals($parent_ids, array_filter($term->parents), "Term $tid has correct parents in taxonomy tree");
|
||||
// PostgreSQL, MySQL and SQLite may not return the parent terms in the same
|
||||
// order so sort before testing.
|
||||
sort($parent_ids);
|
||||
$actual_terms = array_filter($term->parents);
|
||||
sort($actual_terms);
|
||||
$this->assertEquals($parent_ids, $actual_terms, "Term $tid has correct parents in taxonomy tree");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\datetime\Functional\Views;
|
||||
|
||||
use Drupal\Core\Datetime\DrupalDateTime;
|
||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
|
||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\views\Tests\ViewTestData;
|
||||
|
||||
/**
|
||||
* Tests Views filters for datetime fields.
|
||||
*
|
||||
* @group datetime
|
||||
*/
|
||||
class FilterDateTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Name of the field.
|
||||
*
|
||||
* Note, this is used in the default test view.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldName = 'field_date';
|
||||
|
||||
/**
|
||||
* Nodes to test.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $nodes = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'datetime',
|
||||
'datetime_test',
|
||||
'node',
|
||||
'views',
|
||||
'views_ui',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $testViews = ['test_filter_datetime'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Create nodes with relative dates of yesterday, today, and tomorrow.
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$now = \Drupal::time()->getRequestTime();
|
||||
|
||||
$admin_user = $this->drupalCreateUser(['administer views']);
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
|
||||
|
||||
// Add a date field to page nodes.
|
||||
$fieldStorage = FieldStorageConfig::create([
|
||||
'field_name' => $this->fieldName,
|
||||
'entity_type' => 'node',
|
||||
'type' => 'datetime',
|
||||
'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
|
||||
]);
|
||||
$fieldStorage->save();
|
||||
$field = FieldConfig::create([
|
||||
'field_storage' => $fieldStorage,
|
||||
'bundle' => 'page',
|
||||
'required' => TRUE,
|
||||
]);
|
||||
$field->save();
|
||||
|
||||
// Create some nodes.
|
||||
$dates = [
|
||||
// Tomorrow.
|
||||
DrupalDateTime::createFromTimestamp($now + 86400, DateTimeItemInterface::STORAGE_TIMEZONE)->format(DateTimeItemInterface::DATE_STORAGE_FORMAT),
|
||||
// Today.
|
||||
DrupalDateTime::createFromTimestamp($now, DateTimeItemInterface::STORAGE_TIMEZONE)->format(DateTimeItemInterface::DATE_STORAGE_FORMAT),
|
||||
// Yesterday.
|
||||
DrupalDateTime::createFromTimestamp($now - 86400, DateTimeItemInterface::STORAGE_TIMEZONE)->format(DateTimeItemInterface::DATE_STORAGE_FORMAT),
|
||||
];
|
||||
|
||||
$this->nodes = [];
|
||||
foreach ($dates as $date) {
|
||||
$this->nodes[] = $this->drupalCreateNode([
|
||||
$this->fieldName => [
|
||||
'value' => $date,
|
||||
],
|
||||
]);
|
||||
}
|
||||
// Add a node where the date field is empty.
|
||||
$this->nodes[] = $this->drupalCreateNode();
|
||||
|
||||
// Views needs to be aware of the new field.
|
||||
$this->container->get('views.views_data')->clear();
|
||||
|
||||
// Load test views.
|
||||
ViewTestData::createTestViews(get_class($this), ['datetime_test']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests exposed grouped filters.
|
||||
*/
|
||||
public function testExposedGroupedFilters() {
|
||||
// Expose the empty and not empty operators in a grouped filter.
|
||||
$this->drupalPostForm('admin/structure/views/nojs/handler/test_filter_datetime/default/filter/' . $this->fieldName . '_value', [], t('Expose filter'));
|
||||
$this->drupalPostForm(NULL, [], 'Grouped filters');
|
||||
|
||||
$edit = [];
|
||||
$edit['options[group_info][group_items][1][title]'] = 'empty';
|
||||
$edit['options[group_info][group_items][1][operator]'] = 'empty';
|
||||
$edit['options[group_info][group_items][2][title]'] = 'not empty';
|
||||
$edit['options[group_info][group_items][2][operator]'] = 'not empty';
|
||||
|
||||
$this->drupalPostForm(NULL, $edit, 'Apply');
|
||||
|
||||
// Test that the exposed filter works as expected.
|
||||
$path = 'test_filter_datetime-path';
|
||||
$this->drupalPostForm('admin/structure/views/view/test_filter_datetime/edit', [], 'Add Page');
|
||||
$this->drupalPostForm('admin/structure/views/nojs/display/test_filter_datetime/page_1/path', ['path' => $path], 'Apply');
|
||||
$this->drupalPostForm(NULL, [], t('Save'));
|
||||
|
||||
$this->drupalGet($path);
|
||||
|
||||
// Filter the Preview by 'empty'.
|
||||
$this->getSession()->getPage()->findField($this->fieldName . '_value')->selectOption(1);
|
||||
$this->getSession()->getPage()->pressButton('Apply');
|
||||
$results = $this->cssSelect('.view-content .field-content');
|
||||
$this->assertEquals(1, count($results));
|
||||
|
||||
// Filter the Preview by 'not empty'.
|
||||
$this->getSession()->getPage()->findField($this->fieldName . '_value')->selectOption(2);
|
||||
$this->getSession()->getPage()->pressButton('Apply');
|
||||
$results = $this->cssSelect('.view-content .field-content');
|
||||
$this->assertEquals(3, count($results));
|
||||
}
|
||||
|
||||
}
|
|
@ -71,6 +71,15 @@ class FilterDateTest extends DateTimeHandlerTestBase {
|
|||
$node->save();
|
||||
$this->nodes[] = $node;
|
||||
}
|
||||
|
||||
// Add a node where the date field is empty.
|
||||
$node = Node::create([
|
||||
'title' => $this->randomMachineName(8),
|
||||
'type' => 'page',
|
||||
'field_date' => [],
|
||||
]);
|
||||
$node->save();
|
||||
$this->nodes[] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -130,6 +139,30 @@ class FilterDateTest extends DateTimeHandlerTestBase {
|
|||
];
|
||||
$this->assertIdenticalResultset($view, $expected_result, $this->map);
|
||||
$view->destroy();
|
||||
|
||||
// Test the empty operator.
|
||||
$view->initHandlers();
|
||||
$view->filter[$field]->operator = 'empty';
|
||||
$view->setDisplay('default');
|
||||
$this->executeView($view);
|
||||
$expected_result = [
|
||||
['nid' => $this->nodes[3]->id()],
|
||||
];
|
||||
$this->assertIdenticalResultset($view, $expected_result, $this->map);
|
||||
$view->destroy();
|
||||
|
||||
// Test the not empty operator.
|
||||
$view->initHandlers();
|
||||
$view->filter[$field]->operator = 'not empty';
|
||||
$view->setDisplay('default');
|
||||
$this->executeView($view);
|
||||
$expected_result = [
|
||||
['nid' => $this->nodes[0]->id()],
|
||||
['nid' => $this->nodes[1]->id()],
|
||||
['nid' => $this->nodes[2]->id()],
|
||||
];
|
||||
$this->assertIdenticalResultset($view, $expected_result, $this->map);
|
||||
$view->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\field\Tests\EntityReference;
|
||||
namespace Drupal\Tests\field\Functional\EntityReference;
|
||||
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\field_ui\Tests\FieldUiTestTrait;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
|
||||
|
||||
/**
|
||||
* Tests for the administrative UI.
|
||||
*
|
||||
* @group entity_reference
|
||||
*/
|
||||
class EntityReferenceAdminTest extends WebTestBase {
|
||||
class EntityReferenceAdminTest extends BrowserTestBase {
|
||||
|
||||
use FieldUiTestTrait;
|
||||
|
||||
|
@ -65,145 +66,6 @@ class EntityReferenceAdminTest extends WebTestBase {
|
|||
*/
|
||||
public function testFieldAdminHandler() {
|
||||
$bundle_path = 'admin/structure/types/manage/' . $this->type;
|
||||
|
||||
// First step: 'Add new field' on the 'Manage fields' page.
|
||||
$this->drupalGet($bundle_path . '/fields/add-field');
|
||||
|
||||
// Check if the commonly referenced entity types appear in the list.
|
||||
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:node');
|
||||
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:user');
|
||||
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Test label',
|
||||
'field_name' => 'test',
|
||||
'new_storage_type' => 'entity_reference',
|
||||
], t('Save and continue'));
|
||||
|
||||
// Node should be selected by default.
|
||||
$this->assertFieldByName('settings[target_type]', 'node');
|
||||
|
||||
// Check that all entity types can be referenced.
|
||||
$this->assertFieldSelectOptions('settings[target_type]', array_keys(\Drupal::entityManager()->getDefinitions()));
|
||||
|
||||
// Second step: 'Field settings' form.
|
||||
$this->drupalPostForm(NULL, [], t('Save field settings'));
|
||||
|
||||
// The base handler should be selected by default.
|
||||
$this->assertFieldByName('settings[handler]', 'default:node');
|
||||
|
||||
// The base handler settings should be displayed.
|
||||
$entity_type_id = 'node';
|
||||
// Check that the type label is correctly displayed.
|
||||
$this->assertText('Content type');
|
||||
$bundles = $this->container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id);
|
||||
foreach ($bundles as $bundle_name => $bundle_info) {
|
||||
$this->assertFieldByName('settings[handler_settings][target_bundles][' . $bundle_name . ']');
|
||||
}
|
||||
|
||||
reset($bundles);
|
||||
|
||||
// Test the sort settings.
|
||||
// Option 0: no sort.
|
||||
$this->assertFieldByName('settings[handler_settings][sort][field]', '_none');
|
||||
$this->assertNoFieldByName('settings[handler_settings][sort][direction]');
|
||||
// Option 1: sort by field.
|
||||
$this->drupalPostAjaxForm(NULL, ['settings[handler_settings][sort][field]' => 'nid'], 'settings[handler_settings][sort][field]');
|
||||
$this->assertFieldByName('settings[handler_settings][sort][direction]', 'ASC');
|
||||
|
||||
// Test that a non-translatable base field is a sort option.
|
||||
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='nid']");
|
||||
// Test that a translatable base field is a sort option.
|
||||
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='title']");
|
||||
// Test that a configurable field is a sort option.
|
||||
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='body.value']");
|
||||
|
||||
// Set back to no sort.
|
||||
$this->drupalPostAjaxForm(NULL, ['settings[handler_settings][sort][field]' => '_none'], 'settings[handler_settings][sort][field]');
|
||||
$this->assertNoFieldByName('settings[handler_settings][sort][direction]');
|
||||
|
||||
// Third step: confirm.
|
||||
$this->drupalPostForm(NULL, [
|
||||
'required' => '1',
|
||||
'settings[handler_settings][target_bundles][' . key($bundles) . ']' => key($bundles),
|
||||
], t('Save settings'));
|
||||
|
||||
// Check that the field appears in the overview form.
|
||||
$this->assertFieldByXPath('//table[@id="field-overview"]//tr[@id="field-test"]/td[1]', 'Test label', 'Field was created and appears in the overview page.');
|
||||
|
||||
// Check that the field settings form can be submitted again, even when the
|
||||
// field is required.
|
||||
// The first 'Edit' link is for the Body field.
|
||||
$this->clickLink(t('Edit'), 1);
|
||||
$this->drupalPostForm(NULL, [], t('Save settings'));
|
||||
|
||||
// Switch the target type to 'taxonomy_term' and check that the settings
|
||||
// specific to its selection handler are displayed.
|
||||
$field_name = 'node.' . $this->type . '.field_test';
|
||||
$edit = [
|
||||
'settings[target_type]' => 'taxonomy_term',
|
||||
];
|
||||
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$this->assertFieldByName('settings[handler_settings][auto_create]');
|
||||
|
||||
// Switch the target type to 'user' and check that the settings specific to
|
||||
// its selection handler are displayed.
|
||||
$field_name = 'node.' . $this->type . '.field_test';
|
||||
$edit = [
|
||||
'settings[target_type]' => 'user',
|
||||
];
|
||||
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$this->assertFieldByName('settings[handler_settings][filter][type]', '_none');
|
||||
|
||||
// Switch the target type to 'node'.
|
||||
$field_name = 'node.' . $this->type . '.field_test';
|
||||
$edit = [
|
||||
'settings[target_type]' => 'node',
|
||||
];
|
||||
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
|
||||
|
||||
// Try to select the views handler.
|
||||
$edit = [
|
||||
'settings[handler]' => 'views',
|
||||
];
|
||||
$this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
|
||||
$this->assertRaw(t('No eligible views were found. <a href=":create">Create a view</a> with an <em>Entity Reference</em> display, or add such a display to an <a href=":existing">existing view</a>.', [
|
||||
':create' => \Drupal::url('views_ui.add'),
|
||||
':existing' => \Drupal::url('entity.view.collection'),
|
||||
]));
|
||||
$this->drupalPostForm(NULL, $edit, t('Save settings'));
|
||||
// If no eligible view is available we should see a message.
|
||||
$this->assertText('The views entity selection mode requires a view.');
|
||||
|
||||
// Enable the entity_reference_test module which creates an eligible view.
|
||||
$this->container->get('module_installer')->install(['entity_reference_test']);
|
||||
$this->resetAll();
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
|
||||
$edit = [
|
||||
'settings[handler_settings][view][view_and_display]' => 'test_entity_reference:entity_reference_1',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save settings'));
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Switch the target type to 'entity_test'.
|
||||
$edit = [
|
||||
'settings[target_type]' => 'entity_test',
|
||||
];
|
||||
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$edit = [
|
||||
'settings[handler]' => 'views',
|
||||
];
|
||||
$this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
|
||||
$edit = [
|
||||
'required' => FALSE,
|
||||
'settings[handler_settings][view][view_and_display]' => 'test_entity_reference_entity_test:entity_reference_1',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save settings'));
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Create a new view and display it as a entity reference.
|
||||
$edit = [
|
||||
'id' => 'node_test_view',
|
||||
|
@ -253,7 +115,7 @@ class EntityReferenceAdminTest extends WebTestBase {
|
|||
$edit = [
|
||||
'settings[handler]' => 'views',
|
||||
];
|
||||
$this->drupalPostAjaxForm(NULL, $edit, 'settings[handler]');
|
||||
$this->drupalPostForm(NULL, $edit, t('Change handler'));
|
||||
$edit = [
|
||||
'required' => FALSE,
|
||||
'settings[handler_settings][view][view_and_display]' => 'node_test_view:entity_reference_1',
|
||||
|
@ -275,7 +137,7 @@ class EntityReferenceAdminTest extends WebTestBase {
|
|||
// Try to add a new node and fill the entity reference field.
|
||||
$this->drupalGet('node/add/' . $this->type);
|
||||
$result = $this->xpath('//input[@name="field_test_entity_ref_field[0][target_id]" and contains(@data-autocomplete-path, "/entity_reference_autocomplete/node/views/")]');
|
||||
$target_url = $this->getAbsoluteUrl($result[0]['data-autocomplete-path']);
|
||||
$target_url = $this->getAbsoluteUrl($result[0]->getAttribute('data-autocomplete-path'));
|
||||
$this->drupalGet($target_url, ['query' => ['q' => 'Foo']]);
|
||||
$this->assertRaw($node1->getTitle() . ' (' . $node1->id() . ')');
|
||||
$this->assertRaw($node2->getTitle() . ' (' . $node2->id() . ')');
|
||||
|
@ -446,7 +308,8 @@ class EntityReferenceAdminTest extends WebTestBase {
|
|||
'settings[handler_settings][target_bundles][' . $vocabularies[1]->id() . ']' => TRUE,
|
||||
];
|
||||
// Enable the second vocabulary as a target bundle.
|
||||
$this->drupalPostAjaxForm($path, $edit, key($edit));
|
||||
$this->drupalPostForm($path, $edit, 'Save settings');
|
||||
$this->drupalGet($path);
|
||||
// Expect a select element with the two vocabularies as options.
|
||||
$this->assertFieldByXPath("//select[@name='settings[handler_settings][auto_create_bundle]']/option[@value='" . $vocabularies[0]->id() . "']");
|
||||
$this->assertFieldByXPath("//select[@name='settings[handler_settings][auto_create_bundle]']/option[@value='" . $vocabularies[1]->id() . "']");
|
||||
|
@ -513,49 +376,23 @@ class EntityReferenceAdminTest extends WebTestBase {
|
|||
* The field name.
|
||||
* @param array $expected_options
|
||||
* An array of expected options.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the assertion succeeded, FALSE otherwise.
|
||||
*/
|
||||
protected function assertFieldSelectOptions($name, array $expected_options) {
|
||||
$xpath = $this->buildXPathQuery('//select[@name=:name]', [':name' => $name]);
|
||||
$fields = $this->xpath($xpath);
|
||||
if ($fields) {
|
||||
$field = $fields[0];
|
||||
$options = $this->getAllOptionsList($field);
|
||||
|
||||
$options = $field->findAll('xpath', 'option');
|
||||
array_walk($options, function (NodeElement &$option) {
|
||||
$option = $option->getValue();
|
||||
});
|
||||
sort($options);
|
||||
sort($expected_options);
|
||||
|
||||
return $this->assertIdentical($options, $expected_options);
|
||||
$this->assertIdentical($options, $expected_options);
|
||||
}
|
||||
else {
|
||||
return $this->fail('Unable to find field ' . $name);
|
||||
$this->fail('Unable to find field ' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all options from a select element.
|
||||
*
|
||||
* @param \SimpleXMLElement $element
|
||||
* The select element field information.
|
||||
*
|
||||
* @return array
|
||||
* An array of option values as strings.
|
||||
*/
|
||||
protected function getAllOptionsList(\SimpleXMLElement $element) {
|
||||
$options = [];
|
||||
// Add all options items.
|
||||
foreach ($element->option as $option) {
|
||||
$options[] = (string) $option['value'];
|
||||
}
|
||||
|
||||
// Loops trough all the option groups
|
||||
foreach ($element->optgroup as $optgroup) {
|
||||
$options = array_merge($this->getAllOptionsList($optgroup), $options);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\field\FunctionalJavascript\EntityReference;
|
||||
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Drupal\Component\Render\FormattableMarkup;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
|
||||
|
||||
/**
|
||||
* Tests for the administrative UI.
|
||||
*
|
||||
* @group entity_reference
|
||||
*/
|
||||
class EntityReferenceAdminTest extends WebDriverTestBase {
|
||||
|
||||
use FieldUiTestTrait;
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* Enable path module to ensure that the selection handler does not fail for
|
||||
* entities with a path field.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['node', 'field_ui', 'path', 'taxonomy', 'block', 'views_ui'];
|
||||
|
||||
/**
|
||||
* The name of the content type created for testing purposes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalPlaceBlock('system_breadcrumb_block');
|
||||
|
||||
// Create a content type, with underscores.
|
||||
$type_name = strtolower($this->randomMachineName(8)) . '_test';
|
||||
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
|
||||
$this->type = $type->id();
|
||||
|
||||
// Create test user.
|
||||
$admin_user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
'administer node fields',
|
||||
'administer node display',
|
||||
'administer views',
|
||||
'create ' . $type_name . ' content',
|
||||
'edit own ' . $type_name . ' content',
|
||||
]);
|
||||
$this->drupalLogin($admin_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the Entity Reference Admin UI.
|
||||
*/
|
||||
public function testFieldAdminHandler() {
|
||||
$bundle_path = 'admin/structure/types/manage/' . $this->type;
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// First step: 'Add new field' on the 'Manage fields' page.
|
||||
$this->drupalGet($bundle_path . '/fields/add-field');
|
||||
|
||||
// Check if the commonly referenced entity types appear in the list.
|
||||
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:node');
|
||||
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:user');
|
||||
|
||||
$page->findField('new_storage_type')->setValue('entity_reference');
|
||||
$assert_session->waitForField('label')->setValue('Test');
|
||||
$machine_name = $assert_session->waitForElement('xpath', '//*[@id="edit-label-machine-name-suffix"]/span[2]/span[contains(text(), "field_test")]');
|
||||
$this->assertNotEmpty($machine_name);
|
||||
$page->pressButton('Save and continue');
|
||||
|
||||
// Node should be selected by default.
|
||||
$this->assertFieldByName('settings[target_type]', 'node');
|
||||
|
||||
// Check that all entity types can be referenced.
|
||||
$this->assertFieldSelectOptions('settings[target_type]', array_keys(\Drupal::entityManager()->getDefinitions()));
|
||||
|
||||
// Second step: 'Field settings' form.
|
||||
$this->drupalPostForm(NULL, [], t('Save field settings'));
|
||||
|
||||
// The base handler should be selected by default.
|
||||
$this->assertFieldByName('settings[handler]', 'default:node');
|
||||
|
||||
// The base handler settings should be displayed.
|
||||
$entity_type_id = 'node';
|
||||
// Check that the type label is correctly displayed.
|
||||
$assert_session->pageTextContains('Content type');
|
||||
$bundles = $this->container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id);
|
||||
foreach ($bundles as $bundle_name => $bundle_info) {
|
||||
$this->assertFieldByName('settings[handler_settings][target_bundles][' . $bundle_name . ']');
|
||||
}
|
||||
|
||||
reset($bundles);
|
||||
|
||||
// Test the sort settings.
|
||||
// Option 0: no sort.
|
||||
$this->assertFieldByName('settings[handler_settings][sort][field]', '_none');
|
||||
$this->assertNoFieldByName('settings[handler_settings][sort][direction]');
|
||||
// Option 1: sort by field.
|
||||
$page->findField('settings[handler_settings][sort][field]')->setValue('nid');
|
||||
$assert_session->waitForField('settings[handler_settings][sort][direction]');
|
||||
$this->assertFieldByName('settings[handler_settings][sort][direction]', 'ASC');
|
||||
|
||||
// Test that a non-translatable base field is a sort option.
|
||||
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='nid']");
|
||||
// Test that a translatable base field is a sort option.
|
||||
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='title']");
|
||||
// Test that a configurable field is a sort option.
|
||||
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='body.value']");
|
||||
|
||||
// Set back to no sort.
|
||||
$page->findField('settings[handler_settings][sort][field]')->setValue('_none');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertNoFieldByName('settings[handler_settings][sort][direction]');
|
||||
|
||||
// Third step: confirm.
|
||||
$page->findField('settings[handler_settings][target_bundles][' . key($bundles) . ']')->setValue(key($bundles));
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->drupalPostForm(NULL, [
|
||||
'required' => '1',
|
||||
], t('Save settings'));
|
||||
|
||||
// Check that the field appears in the overview form.
|
||||
$this->assertFieldByXPath('//table[@id="field-overview"]//tr[@id="field-test"]/td[1]', 'Test', 'Field was created and appears in the overview page.');
|
||||
|
||||
// Check that the field settings form can be submitted again, even when the
|
||||
// field is required.
|
||||
// The first 'Edit' link is for the Body field.
|
||||
$this->clickLink(t('Edit'), 1);
|
||||
$this->drupalPostForm(NULL, [], t('Save settings'));
|
||||
|
||||
// Switch the target type to 'taxonomy_term' and check that the settings
|
||||
// specific to its selection handler are displayed.
|
||||
$field_name = 'node.' . $this->type . '.field_test';
|
||||
$edit = [
|
||||
'settings[target_type]' => 'taxonomy_term',
|
||||
];
|
||||
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$this->assertFieldByName('settings[handler_settings][auto_create]');
|
||||
|
||||
// Switch the target type to 'user' and check that the settings specific to
|
||||
// its selection handler are displayed.
|
||||
$field_name = 'node.' . $this->type . '.field_test';
|
||||
$edit = [
|
||||
'settings[target_type]' => 'user',
|
||||
];
|
||||
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$this->assertFieldByName('settings[handler_settings][filter][type]', '_none');
|
||||
|
||||
// Switch the target type to 'node'.
|
||||
$field_name = 'node.' . $this->type . '.field_test';
|
||||
$edit = [
|
||||
'settings[target_type]' => 'node',
|
||||
];
|
||||
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
|
||||
|
||||
// Try to select the views handler.
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$page->findField('settings[handler]')->setValue('views');
|
||||
$views_text = (string) new FormattableMarkup('No eligible views were found. <a href=":create">Create a view</a> with an <em>Entity Reference</em> display, or add such a display to an <a href=":existing">existing view</a>.', [
|
||||
':create' => \Drupal::url('views_ui.add'),
|
||||
':existing' => \Drupal::url('entity.view.collection'),
|
||||
]);
|
||||
$assert_session->waitForElement('xpath', '//a[contains(text(), "Create a view")]');
|
||||
$assert_session->responseContains($views_text);
|
||||
|
||||
$this->drupalPostForm(NULL, [], t('Save settings'));
|
||||
// If no eligible view is available we should see a message.
|
||||
$assert_session->pageTextContains('The views entity selection mode requires a view.');
|
||||
|
||||
// Enable the entity_reference_test module which creates an eligible view.
|
||||
$this->container->get('module_installer')
|
||||
->install(['entity_reference_test']);
|
||||
$this->resetAll();
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$page->findField('settings[handler]')->setValue('views');
|
||||
$assert_session
|
||||
->waitForField('settings[handler_settings][view][view_and_display]')
|
||||
->setValue('test_entity_reference:entity_reference_1');
|
||||
$this->drupalPostForm(NULL, [], t('Save settings'));
|
||||
$assert_session->pageTextContains('Saved Test configuration.');
|
||||
|
||||
// Switch the target type to 'entity_test'.
|
||||
$edit = [
|
||||
'settings[target_type]' => 'entity_test',
|
||||
];
|
||||
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
|
||||
$this->drupalGet($bundle_path . '/fields/' . $field_name);
|
||||
$page->findField('settings[handler]')->setValue('views');
|
||||
$assert_session
|
||||
->waitForField('settings[handler_settings][view][view_and_display]')
|
||||
->setValue('test_entity_reference_entity_test:entity_reference_1');
|
||||
$edit = [
|
||||
'required' => FALSE,
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save settings'));
|
||||
$assert_session->pageTextContains('Saved Test configuration.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a select element contains the specified options.
|
||||
*
|
||||
* @param string $name
|
||||
* The field name.
|
||||
* @param array $expected_options
|
||||
* An array of expected options.
|
||||
*/
|
||||
protected function assertFieldSelectOptions($name, array $expected_options) {
|
||||
$xpath = $this->buildXPathQuery('//select[@name=:name]', [':name' => $name]);
|
||||
$fields = $this->xpath($xpath);
|
||||
if ($fields) {
|
||||
$field = $fields[0];
|
||||
$options = $field->findAll('xpath', 'option');
|
||||
$optgroups = $field->findAll('xpath', 'optgroup');
|
||||
foreach ($optgroups as $optgroup) {
|
||||
$options = array_merge($options, $optgroup->findAll('xpath', 'option'));
|
||||
}
|
||||
array_walk($options, function (NodeElement &$option) {
|
||||
$option = $option->getAttribute('value');
|
||||
});
|
||||
|
||||
sort($options);
|
||||
sort($expected_options);
|
||||
|
||||
$this->assertIdentical($options, $expected_options);
|
||||
}
|
||||
else {
|
||||
$this->fail('Unable to find field ' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -24,6 +24,13 @@ class StringFormatterTest extends KernelTestBase {
|
|||
*/
|
||||
public static $modules = ['field', 'text', 'entity_test', 'system', 'filter', 'user'];
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
@ -79,6 +86,8 @@ class StringFormatterTest extends KernelTestBase {
|
|||
'settings' => [],
|
||||
]);
|
||||
$this->display->save();
|
||||
|
||||
$this->entityTypeManager = \Drupal::entityTypeManager();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,7 +154,7 @@ class StringFormatterTest extends KernelTestBase {
|
|||
$value2 = $this->randomMachineName();
|
||||
$entity->{$this->fieldName}->value = $value2;
|
||||
$entity->save();
|
||||
$entity_new_revision = \Drupal::entityManager()->getStorage('entity_test_rev')->loadRevision($old_revision_id);
|
||||
$entity_new_revision = $this->entityTypeManager->getStorage('entity_test_rev')->loadRevision($old_revision_id);
|
||||
|
||||
$this->renderEntityFields($entity, $this->display);
|
||||
$this->assertLink($value2, 0);
|
||||
|
@ -154,6 +163,19 @@ class StringFormatterTest extends KernelTestBase {
|
|||
$this->renderEntityFields($entity_new_revision, $this->display);
|
||||
$this->assertLink($value, 0);
|
||||
$this->assertLinkByHref('/entity_test_rev/' . $entity_new_revision->id() . '/revision/' . $entity_new_revision->getRevisionId() . '/view');
|
||||
|
||||
// Check that linking to a revisionable entity works if the entity type does
|
||||
// not specify a 'revision' link template.
|
||||
$entity_type = clone $this->entityTypeManager->getDefinition('entity_test_rev');
|
||||
$link_templates = $entity_type->getLinkTemplates();
|
||||
unset($link_templates['revision']);
|
||||
$entity_type->set('links', $link_templates);
|
||||
\Drupal::state()->set('entity_test_rev.entity_type', $entity_type);
|
||||
$this->entityTypeManager->clearCachedDefinitions();
|
||||
|
||||
$this->renderEntityFields($entity_new_revision, $this->display);
|
||||
$this->assertLink($value, 0);
|
||||
$this->assertLinkByHref($entity->url('canonical'));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ class ManageFieldsFunctionalTest extends BrowserTestBase {
|
|||
/**
|
||||
* Tests adding a new field.
|
||||
*
|
||||
* @todo Assert properties can bet set in the form and read back in
|
||||
* @todo Assert properties can be set in the form and read back in
|
||||
* $field_storage and $fields.
|
||||
*/
|
||||
public function createField() {
|
||||
|
|
|
@ -23,7 +23,7 @@ use Drupal\Core\Template\Attribute;
|
|||
/**
|
||||
* The regex pattern used when checking for insecure file types.
|
||||
*/
|
||||
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(php|pl|py|cgi|asp|js)(\.|$)/i');
|
||||
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');
|
||||
|
||||
// Load all Field module hooks for File.
|
||||
require_once __DIR__ . '/file.field.inc';
|
||||
|
@ -859,7 +859,6 @@ function _file_save_upload_from_form(array $element, FormStateInterface $form_st
|
|||
* @todo: move this logic to a service in https://www.drupal.org/node/2244513.
|
||||
*/
|
||||
function file_save_upload($form_field_name, $validators = [], $destination = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME) {
|
||||
$user = \Drupal::currentUser();
|
||||
static $upload_cache;
|
||||
|
||||
$all_files = \Drupal::request()->files->get('files', []);
|
||||
|
@ -887,176 +886,7 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
|
|||
|
||||
$files = [];
|
||||
foreach ($uploaded_files as $i => $file_info) {
|
||||
// Check for file upload errors and return FALSE for this file if a lower
|
||||
// level system error occurred. For a complete list of errors:
|
||||
// See http://php.net/manual/features.file-upload.errors.php.
|
||||
switch ($file_info->getError()) {
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
\Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size())]));
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
|
||||
case UPLOAD_ERR_PARTIAL:
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
\Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
|
||||
case UPLOAD_ERR_OK:
|
||||
// Final check that this is a valid upload, if it isn't, use the
|
||||
// default error handler.
|
||||
if (is_uploaded_file($file_info->getRealPath())) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Unknown error
|
||||
default:
|
||||
\Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
|
||||
}
|
||||
// Begin building file entity.
|
||||
$values = [
|
||||
'uid' => $user->id(),
|
||||
'status' => 0,
|
||||
'filename' => $file_info->getClientOriginalName(),
|
||||
'uri' => $file_info->getRealPath(),
|
||||
'filesize' => $file_info->getSize(),
|
||||
];
|
||||
$values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
|
||||
$file = File::create($values);
|
||||
|
||||
$extensions = '';
|
||||
if (isset($validators['file_validate_extensions'])) {
|
||||
if (isset($validators['file_validate_extensions'][0])) {
|
||||
// Build the list of non-munged extensions if the caller provided them.
|
||||
$extensions = $validators['file_validate_extensions'][0];
|
||||
}
|
||||
else {
|
||||
// If 'file_validate_extensions' is set and the list is empty then the
|
||||
// caller wants to allow any extension. In this case we have to remove the
|
||||
// validator or else it will reject all extensions.
|
||||
unset($validators['file_validate_extensions']);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No validator was provided, so add one using the default list.
|
||||
// Build a default non-munged safe list for file_munge_filename().
|
||||
$extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
|
||||
$validators['file_validate_extensions'] = [];
|
||||
$validators['file_validate_extensions'][0] = $extensions;
|
||||
}
|
||||
|
||||
if (!empty($extensions)) {
|
||||
// Munge the filename to protect against possible malicious extension
|
||||
// hiding within an unknown file type (ie: filename.html.foo).
|
||||
$file->setFilename(file_munge_filename($file->getFilename(), $extensions));
|
||||
}
|
||||
|
||||
// Rename potentially executable files, to help prevent exploits (i.e. will
|
||||
// rename filename.php.foo and filename.php to filename.php.foo.txt and
|
||||
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
|
||||
// evaluates to TRUE.
|
||||
if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
|
||||
$file->setMimeType('text/plain');
|
||||
// The destination filename will also later be used to create the URI.
|
||||
$file->setFilename($file->getFilename() . '.txt');
|
||||
// The .txt extension may not be in the allowed list of extensions. We have
|
||||
// to add it here or else the file upload will fail.
|
||||
if (!empty($extensions)) {
|
||||
$validators['file_validate_extensions'][0] .= ' txt';
|
||||
\Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
|
||||
}
|
||||
}
|
||||
|
||||
// If the destination is not provided, use the temporary directory.
|
||||
if (empty($destination)) {
|
||||
$destination = 'temporary://';
|
||||
}
|
||||
|
||||
// Assert that the destination contains a valid stream.
|
||||
$destination_scheme = file_uri_scheme($destination);
|
||||
if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
|
||||
\Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
}
|
||||
|
||||
$file->source = $form_field_name;
|
||||
// A file URI may already have a trailing slash or look like "public://".
|
||||
if (substr($destination, -1) != '/') {
|
||||
$destination .= '/';
|
||||
}
|
||||
$file->destination = file_destination($destination . $file->getFilename(), $replace);
|
||||
// If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
|
||||
// there's an existing file so we need to bail.
|
||||
if ($file->destination === FALSE) {
|
||||
\Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add in our check of the file name length.
|
||||
$validators['file_validate_name_length'] = [];
|
||||
|
||||
// Call the validation functions specified by this function's caller.
|
||||
$errors = file_validate($file, $validators);
|
||||
|
||||
// Check for errors.
|
||||
if (!empty($errors)) {
|
||||
$message = [
|
||||
'error' => [
|
||||
'#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
|
||||
],
|
||||
'item_list' => [
|
||||
'#theme' => 'item_list',
|
||||
'#items' => $errors,
|
||||
],
|
||||
];
|
||||
// @todo Add support for render arrays in
|
||||
// \Drupal\Core\Messenger\MessengerInterface::addMessage()?
|
||||
// @see https://www.drupal.org/node/2505497.
|
||||
\Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
}
|
||||
|
||||
$file->setFileUri($file->destination);
|
||||
if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
|
||||
\Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
|
||||
\Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the permissions on the new file.
|
||||
drupal_chmod($file->getFileUri());
|
||||
|
||||
// If we are replacing an existing file re-use its database record.
|
||||
// @todo Do not create a new entity in order to update it. See
|
||||
// https://www.drupal.org/node/2241865.
|
||||
if ($replace == FILE_EXISTS_REPLACE) {
|
||||
$existing_files = entity_load_multiple_by_properties('file', ['uri' => $file->getFileUri()]);
|
||||
if (count($existing_files)) {
|
||||
$existing = reset($existing_files);
|
||||
$file->fid = $existing->id();
|
||||
$file->setOriginalId($existing->id());
|
||||
}
|
||||
}
|
||||
|
||||
// If we made it this far it's safe to record this file in the database.
|
||||
$file->save();
|
||||
$files[$i] = $file;
|
||||
// Allow an anonymous user who creates a non-public file to see it. See
|
||||
// \Drupal\file\FileAccessControlHandler::checkAccess().
|
||||
if ($user->isAnonymous() && $destination_scheme !== 'public') {
|
||||
$session = \Drupal::request()->getSession();
|
||||
$allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
|
||||
$allowed_temp_files[$file->id()] = $file->id();
|
||||
$session->set('anonymous_allowed_file_ids', $allowed_temp_files);
|
||||
}
|
||||
$files[$i] = _file_save_upload_single($file_info, $form_field_name, $validators, $destination, $replace);
|
||||
}
|
||||
|
||||
// Add files to the cache.
|
||||
|
@ -1065,6 +895,201 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
|
|||
return isset($delta) ? $files[$delta] : $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a file upload to a new location.
|
||||
*
|
||||
* @param \SplFileInfo $file_info
|
||||
* The file upload to save.
|
||||
* @param string $form_field_name
|
||||
* A string that is the associative array key of the upload form element in
|
||||
* the form array.
|
||||
* @param array $validators
|
||||
* (optional) An associative array of callback functions used to validate the
|
||||
* file.
|
||||
* @param bool $destination
|
||||
* (optional) A string containing the URI that the file should be copied to.
|
||||
* @param int $replace
|
||||
* (optional) The replace behavior when the destination file already exists.
|
||||
*
|
||||
* @return \Drupal\file\FileInterface|false
|
||||
* The created file entity or FALSE if the uploaded file not saved.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
*
|
||||
* @internal
|
||||
* This method should only be called from file_save_upload(). Use that method
|
||||
* instead.
|
||||
*
|
||||
* @see file_save_upload()
|
||||
*/
|
||||
function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $validators = [], $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
|
||||
$user = \Drupal::currentUser();
|
||||
// Check for file upload errors and return FALSE for this file if a lower
|
||||
// level system error occurred. For a complete list of errors:
|
||||
// See http://php.net/manual/features.file-upload.errors.php.
|
||||
switch ($file_info->getError()) {
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
\Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size())]));
|
||||
return FALSE;
|
||||
|
||||
case UPLOAD_ERR_PARTIAL:
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
\Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
|
||||
return FALSE;
|
||||
|
||||
case UPLOAD_ERR_OK:
|
||||
// Final check that this is a valid upload, if it isn't, use the
|
||||
// default error handler.
|
||||
if (is_uploaded_file($file_info->getRealPath())) {
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown error
|
||||
\Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
|
||||
return FALSE;
|
||||
|
||||
}
|
||||
// Begin building file entity.
|
||||
$values = [
|
||||
'uid' => $user->id(),
|
||||
'status' => 0,
|
||||
'filename' => $file_info->getClientOriginalName(),
|
||||
'uri' => $file_info->getRealPath(),
|
||||
'filesize' => $file_info->getSize(),
|
||||
];
|
||||
$values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
|
||||
$file = File::create($values);
|
||||
|
||||
$extensions = '';
|
||||
if (isset($validators['file_validate_extensions'])) {
|
||||
if (isset($validators['file_validate_extensions'][0])) {
|
||||
// Build the list of non-munged extensions if the caller provided them.
|
||||
$extensions = $validators['file_validate_extensions'][0];
|
||||
}
|
||||
else {
|
||||
// If 'file_validate_extensions' is set and the list is empty then the
|
||||
// caller wants to allow any extension. In this case we have to remove the
|
||||
// validator or else it will reject all extensions.
|
||||
unset($validators['file_validate_extensions']);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No validator was provided, so add one using the default list.
|
||||
// Build a default non-munged safe list for file_munge_filename().
|
||||
$extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
|
||||
$validators['file_validate_extensions'] = [];
|
||||
$validators['file_validate_extensions'][0] = $extensions;
|
||||
}
|
||||
|
||||
if (!empty($extensions)) {
|
||||
// Munge the filename to protect against possible malicious extension
|
||||
// hiding within an unknown file type (ie: filename.html.foo).
|
||||
$file->setFilename(file_munge_filename($file->getFilename(), $extensions));
|
||||
}
|
||||
|
||||
// Rename potentially executable files, to help prevent exploits (i.e. will
|
||||
// rename filename.php.foo and filename.php to filename.php.foo.txt and
|
||||
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
|
||||
// evaluates to TRUE.
|
||||
if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
|
||||
$file->setMimeType('text/plain');
|
||||
// The destination filename will also later be used to create the URI.
|
||||
$file->setFilename($file->getFilename() . '.txt');
|
||||
// The .txt extension may not be in the allowed list of extensions. We have
|
||||
// to add it here or else the file upload will fail.
|
||||
if (!empty($extensions)) {
|
||||
$validators['file_validate_extensions'][0] .= ' txt';
|
||||
\Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
|
||||
}
|
||||
}
|
||||
|
||||
// If the destination is not provided, use the temporary directory.
|
||||
if (empty($destination)) {
|
||||
$destination = 'temporary://';
|
||||
}
|
||||
|
||||
// Assert that the destination contains a valid stream.
|
||||
$destination_scheme = file_uri_scheme($destination);
|
||||
if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
|
||||
\Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$file->source = $form_field_name;
|
||||
// A file URI may already have a trailing slash or look like "public://".
|
||||
if (substr($destination, -1) != '/') {
|
||||
$destination .= '/';
|
||||
}
|
||||
$file->destination = file_destination($destination . $file->getFilename(), $replace);
|
||||
// If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
|
||||
// there's an existing file so we need to bail.
|
||||
if ($file->destination === FALSE) {
|
||||
\Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Add in our check of the file name length.
|
||||
$validators['file_validate_name_length'] = [];
|
||||
|
||||
// Call the validation functions specified by this function's caller.
|
||||
$errors = file_validate($file, $validators);
|
||||
|
||||
// Check for errors.
|
||||
if (!empty($errors)) {
|
||||
$message = [
|
||||
'error' => [
|
||||
'#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
|
||||
],
|
||||
'item_list' => [
|
||||
'#theme' => 'item_list',
|
||||
'#items' => $errors,
|
||||
],
|
||||
];
|
||||
// @todo Add support for render arrays in
|
||||
// \Drupal\Core\Messenger\MessengerInterface::addMessage()?
|
||||
// @see https://www.drupal.org/node/2505497.
|
||||
\Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$file->setFileUri($file->destination);
|
||||
if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
|
||||
\Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
|
||||
\Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Set the permissions on the new file.
|
||||
drupal_chmod($file->getFileUri());
|
||||
|
||||
// If we are replacing an existing file re-use its database record.
|
||||
// @todo Do not create a new entity in order to update it. See
|
||||
// https://www.drupal.org/node/2241865.
|
||||
if ($replace == FILE_EXISTS_REPLACE) {
|
||||
$existing_files = entity_load_multiple_by_properties('file', ['uri' => $file->getFileUri()]);
|
||||
if (count($existing_files)) {
|
||||
$existing = reset($existing_files);
|
||||
$file->fid = $existing->id();
|
||||
$file->setOriginalId($existing->id());
|
||||
}
|
||||
}
|
||||
|
||||
// If we made it this far it's safe to record this file in the database.
|
||||
$file->save();
|
||||
|
||||
// Allow an anonymous user who creates a non-public file to see it. See
|
||||
// \Drupal\file\FileAccessControlHandler::checkAccess().
|
||||
if ($user->isAnonymous() && $destination_scheme !== 'public') {
|
||||
$session = \Drupal::request()->getSession();
|
||||
$allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
|
||||
$allowed_temp_files[$file->id()] = $file->id();
|
||||
$session->set('anonymous_allowed_file_ids', $allowed_temp_files);
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the preferred upload progress implementation.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\file\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests multiple file upload.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class MultipleFileUploadTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['file'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$admin = $this->drupalCreateUser(['administer themes']);
|
||||
$this->drupalLogin($admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests multiple file field with all file extensions.
|
||||
*/
|
||||
public function testMultipleFileFieldWithAllFileExtensions() {
|
||||
$theme = 'test_theme_settings';
|
||||
\Drupal::service('theme_handler')->install([$theme]);
|
||||
$this->drupalGet("admin/appearance/settings/$theme");
|
||||
|
||||
$edit = [];
|
||||
// Create few files with non-typical extensions.
|
||||
foreach (['file1.wtf', 'file2.wtf'] as $i => $file) {
|
||||
$file_path = $this->root . "/sites/default/files/simpletest/$file";
|
||||
file_put_contents($file_path, 'File with non-default extension.', FILE_APPEND | LOCK_EX);
|
||||
$edit["files[multi_file][$i]"] = $file_path;
|
||||
}
|
||||
|
||||
// @todo: Replace after https://www.drupal.org/project/drupal/issues/2917885
|
||||
$this->drupalGet("admin/appearance/settings/$theme");
|
||||
$submit_xpath = $this->assertSession()->buttonExists('Save configuration')->getXpath();
|
||||
$client = $this->getSession()->getDriver()->getClient();
|
||||
$form = $client->getCrawler()->filterXPath($submit_xpath)->form();
|
||||
$client->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $edit);
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertNotContains('Only files with the following extensions are allowed', $page->getContent());
|
||||
$this->assertContains('The configuration options have been saved.', $page->getContent());
|
||||
$this->assertContains('file1.wtf', $page->getContent());
|
||||
$this->assertContains('file2.wtf', $page->getContent());
|
||||
}
|
||||
|
||||
}
|
|
@ -90,11 +90,11 @@ class ConfigurableLanguageManager extends LanguageManager implements Configurabl
|
|||
protected $initialized = FALSE;
|
||||
|
||||
/**
|
||||
* Whether already in the process of language initialization.
|
||||
* Whether language types are in the process of language initialization.
|
||||
*
|
||||
* @var bool
|
||||
* @var bool[]
|
||||
*/
|
||||
protected $initializing = FALSE;
|
||||
protected $initializing = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
@ -213,12 +213,12 @@ class ConfigurableLanguageManager extends LanguageManager implements Configurabl
|
|||
$this->negotiatedLanguages[$type] = $this->getDefaultLanguage();
|
||||
|
||||
if ($this->negotiator && $this->isMultilingual()) {
|
||||
if (!$this->initializing) {
|
||||
$this->initializing = TRUE;
|
||||
if (!isset($this->initializing[$type])) {
|
||||
$this->initializing[$type] = TRUE;
|
||||
$negotiation = $this->negotiator->initializeType($type);
|
||||
$this->negotiatedLanguages[$type] = reset($negotiation);
|
||||
$this->negotiatedMethods[$type] = key($negotiation);
|
||||
$this->initializing = FALSE;
|
||||
unset($this->initializing[$type]);
|
||||
}
|
||||
// If the current interface language needs to be retrieved during
|
||||
// initialization we return the system language. This way string
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\language\Functional;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\language\Entity\ContentLanguageSettings;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests Language Negotiation.
|
||||
*
|
||||
* Uses different negotiators for content and interface.
|
||||
*
|
||||
* @group language
|
||||
*/
|
||||
class ConfigurableLanguageManagerTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'node',
|
||||
'locale',
|
||||
'block',
|
||||
'system',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
/** @var \Drupal\user\UserInterface $user */
|
||||
$user = $this->createUser([], '', TRUE);
|
||||
$this->drupalLogin($user);
|
||||
ConfigurableLanguage::createFromLangcode('es')->save();
|
||||
|
||||
// Create a page node type and make it translatable.
|
||||
NodeType::create([
|
||||
'type' => 'page',
|
||||
'name' => t('Page'),
|
||||
])->save();
|
||||
|
||||
$config = ContentLanguageSettings::loadByEntityTypeBundle('node', 'page');
|
||||
$config->setDefaultLangcode('en')
|
||||
->setLanguageAlterable(TRUE)
|
||||
->save();
|
||||
|
||||
// Create a Node with title 'English' and translate it to Spanish.
|
||||
$node = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => 'English',
|
||||
]);
|
||||
$node->save();
|
||||
$node->addTranslation('es', ['title' => 'Español']);
|
||||
$node->save();
|
||||
|
||||
// Enable both language_interface and language_content language negotiation.
|
||||
\Drupal::getContainer()->get('language_negotiator')->updateConfiguration([
|
||||
'language_interface',
|
||||
'language_content',
|
||||
]);
|
||||
|
||||
// Set the preferred language of the user for admin pages to English.
|
||||
$user->set('preferred_admin_langcode', 'en')->save();
|
||||
|
||||
// Make sure node edit pages are administration pages.
|
||||
$this->config('node.settings')->set('use_admin_theme', '1')->save();
|
||||
$this->container->get('router.builder')->rebuild();
|
||||
|
||||
// Place a Block with a translatable string on the page.
|
||||
$this->placeBlock('system_powered_by_block', ['region' => 'content']);
|
||||
|
||||
// Load the Spanish Node page once, to register the translatable string.
|
||||
$this->drupalGet('/es/node/1');
|
||||
|
||||
// Translate the Powered by string.
|
||||
/** @var \Drupal\locale\StringStorageInterface $string_storage */
|
||||
$string_storage = \Drupal::getContainer()->get('locale.storage');
|
||||
$source = $string_storage->findString(['source' => 'Powered by <a href=":poweredby">Drupal</a>']);
|
||||
$string_storage->createTranslation([
|
||||
'lid' => $source->lid,
|
||||
'language' => 'es',
|
||||
'translation' => 'Funciona con ...',
|
||||
])->save();
|
||||
// Invalidate caches so that the new translation will be used.
|
||||
Cache::invalidateTags(['rendered', 'locale']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test translation with URL and Preferred Admin Language negotiators.
|
||||
*
|
||||
* The interface language uses the preferred language for admin pages of the
|
||||
* user and after that the URL. The Content uses just the URL.
|
||||
*/
|
||||
public function testUrlContentTranslationWithPreferredAdminLanguage() {
|
||||
$assert_session = $this->assertSession();
|
||||
// Set the interface language to use the preferred administration language
|
||||
// and then the URL.
|
||||
/** @var \Drupal\language\LanguageNegotiatorInterface $language_negotiator */
|
||||
$language_negotiator = \Drupal::getContainer()->get('language_negotiator');
|
||||
$language_negotiator->saveConfiguration('language_interface', [
|
||||
'language-user-admin' => 1,
|
||||
'language-url' => 2,
|
||||
'language-selected' => 3,
|
||||
]);
|
||||
// Set Content Language Negotiator to use just the URL.
|
||||
$language_negotiator->saveConfiguration('language_content', [
|
||||
'language-url' => 4,
|
||||
'language-selected' => 5,
|
||||
]);
|
||||
|
||||
// See if the full view of the node in english is present and the
|
||||
// string in the Powered By Block is in English.
|
||||
$this->drupalGet('/node/1');
|
||||
$assert_session->pageTextContains('English');
|
||||
$assert_session->pageTextContains('Powered by');
|
||||
|
||||
// Load the spanish node page again and see if both the node and the string
|
||||
// are translated.
|
||||
$this->drupalGet('/es/node/1');
|
||||
$assert_session->pageTextContains('Español');
|
||||
$assert_session->pageTextContains('Funciona con');
|
||||
$assert_session->pageTextNotContains('Powered by');
|
||||
|
||||
// Check if the Powered by string is shown in English on an
|
||||
// administration page, and the node content is shown in Spanish.
|
||||
$this->drupalGet('/es/node/1/edit');
|
||||
$assert_session->pageTextContains('Español');
|
||||
$assert_session->pageTextContains('Powered by');
|
||||
$assert_session->pageTextNotContains('Funciona con');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test translation with URL and Session Language Negotiators.
|
||||
*/
|
||||
public function testUrlContentTranslationWithSessionLanguage() {
|
||||
$assert_session = $this->assertSession();
|
||||
/** @var \Drupal\language\LanguageNegotiatorInterface $language_negotiator */
|
||||
$language_negotiator = \Drupal::getContainer()->get('language_negotiator');
|
||||
// Set Interface Language Negotiator to Session.
|
||||
$language_negotiator->saveConfiguration('language_interface', [
|
||||
'language-session' => 1,
|
||||
'language-url' => 2,
|
||||
'language-selected' => 3,
|
||||
]);
|
||||
|
||||
// Set Content Language Negotiator to URL.
|
||||
$language_negotiator->saveConfiguration('language_content', [
|
||||
'language-url' => 4,
|
||||
'language-selected' => 5,
|
||||
]);
|
||||
|
||||
// See if the full view of the node in english is present and the
|
||||
// string in the Powered By Block is in English.
|
||||
$this->drupalGet('/node/1');
|
||||
$assert_session->pageTextContains('English');
|
||||
$assert_session->pageTextContains('Powered by');
|
||||
|
||||
// The language session variable has not been set yet, so
|
||||
// The string should be in Spanish.
|
||||
$this->drupalGet('/es/node/1');
|
||||
$assert_session->pageTextContains('Español');
|
||||
$assert_session->pageTextNotContains('Powered by');
|
||||
$assert_session->pageTextContains('Funciona con');
|
||||
|
||||
// Set the session language to Spanish but load the English node page.
|
||||
$this->drupalGet('/node/1', ['query' => ['language' => 'es']]);
|
||||
$assert_session->pageTextContains('English');
|
||||
$assert_session->pageTextNotContains('Español');
|
||||
$assert_session->pageTextContains('Funciona con');
|
||||
$assert_session->pageTextNotContains('Powered by');
|
||||
|
||||
// Set the session language to English but load the node page in Spanish.
|
||||
$this->drupalGet('/es/node/1', ['query' => ['language' => 'en']]);
|
||||
$assert_session->pageTextNotContains('English');
|
||||
$assert_session->pageTextContains('Español');
|
||||
$assert_session->pageTextNotContains('Funciona con');
|
||||
$assert_session->pageTextContains('Powered by');
|
||||
}
|
||||
|
||||
}
|
|
@ -45,9 +45,9 @@ class MigrateLanguageContentTaxonomyVocabularySettingsTest extends MigrateDrupal
|
|||
// Set language to vocabulary.
|
||||
$this->assertLanguageContentSettings($target_entity, 'vocabulary_2_i_1_', 'fr', FALSE, ['enabled' => FALSE]);
|
||||
// Localize terms.
|
||||
$this->assertLanguageContentSettings($target_entity, 'vocabulary_3_i_2_', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => TRUE]);
|
||||
$this->assertLanguageContentSettings($target_entity, 'vocabulary_3_i_2_', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => FALSE]);
|
||||
// None translation enabled.
|
||||
$this->assertLanguageContentSettings($target_entity, 'vocabulary_name_much_longer_than', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
|
||||
$this->assertLanguageContentSettings($target_entity, 'vocabulary_name_much_longer_than', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => TRUE]);
|
||||
$this->assertLanguageContentSettings($target_entity, 'tags', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
|
||||
$this->assertLanguageContentSettings($target_entity, 'forums', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
|
||||
$this->assertLanguageContentSettings($target_entity, 'type', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
|
||||
|
|
|
@ -85,3 +85,32 @@
|
|||
display: block;
|
||||
padding-top: 0.55em;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .inline-block-create-button {
|
||||
display: block;
|
||||
padding: 24px;
|
||||
padding-left: 44px;
|
||||
font-size: 16px;
|
||||
color: #eee;
|
||||
background: url(../../../misc/icons/bebebe/plus.svg) transparent 16px no-repeat;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .inline-block-create-button,
|
||||
#drupal-off-canvas .inline-block-list__item {
|
||||
margin: 0 -20px;
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .inline-block-create-button:hover,
|
||||
#drupal-off-canvas .inline-block-list__item:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .inline-block-list {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .inline-block-list__item {
|
||||
display: block;
|
||||
padding: 15px 0 15px 25px;
|
||||
}
|
||||
|
|
|
@ -9,3 +9,5 @@ dependencies:
|
|||
- drupal:contextual
|
||||
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/2935999.
|
||||
- drupal:field_ui
|
||||
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/3003610.
|
||||
- drupal:block
|
||||
|
|
|
@ -19,6 +19,7 @@ use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
|
|||
use Drupal\layout_builder\InlineBlockEntityOperations;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
|
@ -62,8 +63,8 @@ function layout_builder_entity_type_alter(array &$entity_types) {
|
|||
function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
|
||||
// Hides the Layout Builder field. It is rendered directly in
|
||||
// \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
|
||||
unset($form['fields']['layout_builder__layout']);
|
||||
$key = array_search('layout_builder__layout', $form['#fields']);
|
||||
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
|
||||
$key = array_search(OverridesSectionStorage::FIELD_NAME, $form['#fields']);
|
||||
if ($key !== FALSE) {
|
||||
unset($form['#fields'][$key]);
|
||||
}
|
||||
|
@ -177,7 +178,7 @@ function layout_builder_cron() {
|
|||
function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
|
||||
// @todo Determine the 'inline_block' blocks should be allowed outside
|
||||
// of layout_builder https://www.drupal.org/node/2979142.
|
||||
if ($consumer !== 'layout_builder') {
|
||||
if ($consumer !== 'layout_builder' || !isset($extra['list']) || $extra['list'] !== 'inline_blocks') {
|
||||
foreach ($definitions as $id => $definition) {
|
||||
if ($definition['id'] === 'inline_block') {
|
||||
unset($definitions[$id]);
|
||||
|
@ -202,3 +203,21 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation
|
|||
}
|
||||
return AccessResult::forbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
|
||||
*/
|
||||
function layout_builder_plugin_filter_block__block_ui_alter(array &$definitions, array $extra) {
|
||||
foreach ($definitions as $id => $definition) {
|
||||
// Filter out any layout_builder definition with required contexts.
|
||||
if ($definition['provider'] === 'layout_builder' && !empty($definition['context'])) {
|
||||
/** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context */
|
||||
foreach ($definition['context'] as $context) {
|
||||
if ($context->isRequired()) {
|
||||
unset($definitions[$id]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,19 @@ layout_builder.add_block:
|
|||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.choose_inline_block:
|
||||
path: '/layout_builder/choose/inline-block/{section_storage_type}/{section_storage}/{delta}/{region}'
|
||||
defaults:
|
||||
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList'
|
||||
_title: 'Add a new Inline Block'
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.update_block:
|
||||
path: '/layout_builder/update/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
|
||||
defaults:
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Drupal\layout_builder\Controller;
|
|||
use Drupal\Core\Ajax\AjaxHelperTrait;
|
||||
use Drupal\Core\Block\BlockManagerInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
|
||||
|
@ -29,14 +30,24 @@ class ChooseBlockController implements ContainerInjectionInterface {
|
|||
*/
|
||||
protected $blockManager;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* ChooseBlockController constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
|
||||
* The block manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(BlockManagerInterface $block_manager) {
|
||||
public function __construct(BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->blockManager = $block_manager;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,7 +55,8 @@ class ChooseBlockController implements ContainerInjectionInterface {
|
|||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('plugin.manager.block')
|
||||
$container->get('plugin.manager.block'),
|
||||
$container->get('entity_type.manager')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -63,8 +75,43 @@ class ChooseBlockController implements ContainerInjectionInterface {
|
|||
*/
|
||||
public function build(SectionStorageInterface $section_storage, $delta, $region) {
|
||||
$build['#title'] = $this->t('Choose a block');
|
||||
$build['#type'] = 'container';
|
||||
$build['#attributes']['class'][] = 'block-categories';
|
||||
if ($this->entityTypeManager->hasDefinition('block_content_type') && $types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple()) {
|
||||
if (count($types) === 1) {
|
||||
$type = reset($types);
|
||||
$plugin_id = 'inline_block:' . $type->id();
|
||||
if ($this->blockManager->hasDefinition($plugin_id)) {
|
||||
$url = Url::fromRoute('layout_builder.add_block', [
|
||||
'section_storage_type' => $section_storage->getStorageType(),
|
||||
'section_storage' => $section_storage->getStorageId(),
|
||||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
'plugin_id' => $plugin_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$url = Url::fromRoute('layout_builder.choose_inline_block', [
|
||||
'section_storage_type' => $section_storage->getStorageType(),
|
||||
'section_storage' => $section_storage->getStorageId(),
|
||||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
]);
|
||||
}
|
||||
if (isset($url)) {
|
||||
$build['add_block'] = [
|
||||
'#type' => 'link',
|
||||
'#url' => $url,
|
||||
'#title' => $this->t('Create @entity_type', [
|
||||
'@entity_type' => $this->entityTypeManager->getDefinition('block_content')->getSingularLabel(),
|
||||
]),
|
||||
'#attributes' => $this->getAjaxAttributes(),
|
||||
];
|
||||
$build['add_block']['#attributes']['class'][] = 'inline-block-create-button';
|
||||
}
|
||||
}
|
||||
|
||||
$block_categories['#type'] = 'container';
|
||||
$block_categories['#attributes']['class'][] = 'block-categories';
|
||||
|
||||
// @todo Explicitly cast delta to an integer, remove this in
|
||||
// https://www.drupal.org/project/drupal/issues/2984509.
|
||||
|
@ -75,35 +122,116 @@ class ChooseBlockController implements ContainerInjectionInterface {
|
|||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
]);
|
||||
foreach ($this->blockManager->getGroupedDefinitions($definitions) as $category => $blocks) {
|
||||
$build[$category]['#type'] = 'details';
|
||||
$build[$category]['#open'] = TRUE;
|
||||
$build[$category]['#title'] = $category;
|
||||
$build[$category]['links'] = [
|
||||
'#theme' => 'links',
|
||||
];
|
||||
foreach ($blocks as $block_id => $block) {
|
||||
$link = [
|
||||
'title' => $block['admin_label'],
|
||||
'url' => Url::fromRoute('layout_builder.add_block',
|
||||
[
|
||||
'section_storage_type' => $section_storage->getStorageType(),
|
||||
'section_storage' => $section_storage->getStorageId(),
|
||||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
'plugin_id' => $block_id,
|
||||
]
|
||||
),
|
||||
];
|
||||
if ($this->isAjax()) {
|
||||
$link['attributes']['class'][] = 'use-ajax';
|
||||
$link['attributes']['data-dialog-type'][] = 'dialog';
|
||||
$link['attributes']['data-dialog-renderer'][] = 'off_canvas';
|
||||
}
|
||||
$build[$category]['links']['#links'][] = $link;
|
||||
$grouped_definitions = $this->blockManager->getGroupedDefinitions($definitions);
|
||||
foreach ($grouped_definitions as $category => $blocks) {
|
||||
$block_categories[$category]['#type'] = 'details';
|
||||
$block_categories[$category]['#open'] = TRUE;
|
||||
$block_categories[$category]['#title'] = $category;
|
||||
$block_categories[$category]['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks);
|
||||
}
|
||||
$build['block_categories'] = $block_categories;
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the UI for choosing a new inline block.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param int $delta
|
||||
* The delta of the section to splice.
|
||||
* @param string $region
|
||||
* The region the block is going in.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*/
|
||||
public function inlineBlockList(SectionStorageInterface $section_storage, $delta, $region) {
|
||||
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getAvailableContexts($section_storage), [
|
||||
'section_storage' => $section_storage,
|
||||
'region' => $region,
|
||||
'list' => 'inline_blocks',
|
||||
]);
|
||||
$blocks = $this->blockManager->getGroupedDefinitions($definitions);
|
||||
$build = [];
|
||||
if (isset($blocks['Inline blocks'])) {
|
||||
$build['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks['Inline blocks']);
|
||||
$build['links']['#attributes']['class'][] = 'inline-block-list';
|
||||
foreach ($build['links']['#links'] as &$link) {
|
||||
$link['attributes']['class'][] = 'inline-block-list__item';
|
||||
}
|
||||
$build['back_button'] = [
|
||||
'#type' => 'link',
|
||||
'#url' => Url::fromRoute('layout_builder.choose_block',
|
||||
[
|
||||
'section_storage_type' => $section_storage->getStorageType(),
|
||||
'section_storage' => $section_storage->getStorageId(),
|
||||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
]
|
||||
),
|
||||
'#title' => $this->t('Back'),
|
||||
'#attributes' => $this->getAjaxAttributes(),
|
||||
];
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a render array of block links.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param int $delta
|
||||
* The delta of the section to splice.
|
||||
* @param string $region
|
||||
* The region the block is going in.
|
||||
* @param array $blocks
|
||||
* The information for each block.
|
||||
*
|
||||
* @return array
|
||||
* The block links render array.
|
||||
*/
|
||||
protected function getBlockLinks(SectionStorageInterface $section_storage, $delta, $region, array $blocks) {
|
||||
$links = [];
|
||||
foreach ($blocks as $block_id => $block) {
|
||||
$link = [
|
||||
'title' => $block['admin_label'],
|
||||
'url' => Url::fromRoute('layout_builder.add_block',
|
||||
[
|
||||
'section_storage_type' => $section_storage->getStorageType(),
|
||||
'section_storage' => $section_storage->getStorageId(),
|
||||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
'plugin_id' => $block_id,
|
||||
]
|
||||
),
|
||||
'attributes' => $this->getAjaxAttributes(),
|
||||
];
|
||||
|
||||
$links[] = $link;
|
||||
}
|
||||
return [
|
||||
'#theme' => 'links',
|
||||
'#links' => $links,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dialog attributes if an ajax request.
|
||||
*
|
||||
* @return array
|
||||
* The attributes array.
|
||||
*/
|
||||
protected function getAjaxAttributes() {
|
||||
if ($this->isAjax()) {
|
||||
return [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'dialog',
|
||||
'data-dialog-renderer' => 'off_canvas',
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Drupal\layout_builder\Controller;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxHelperTrait;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Plugin\PluginFormInterface;
|
||||
|
@ -24,6 +25,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
|
|||
|
||||
use LayoutBuilderContextTrait;
|
||||
use StringTranslationTrait;
|
||||
use AjaxHelperTrait;
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
|
@ -90,6 +92,11 @@ class LayoutBuilderController implements ContainerInjectionInterface {
|
|||
$this->prepareLayout($section_storage, $is_rebuilding);
|
||||
|
||||
$output = [];
|
||||
if ($this->isAjax()) {
|
||||
$output['status_messages'] = [
|
||||
'#type' => 'status_messages',
|
||||
];
|
||||
}
|
||||
$count = 0;
|
||||
for ($i = 0; $i < $section_storage->count(); $i++) {
|
||||
$output[] = $this->buildAddSectionLink($section_storage, $count);
|
||||
|
@ -114,6 +121,11 @@ class LayoutBuilderController implements ContainerInjectionInterface {
|
|||
* Indicates if the layout is rebuilding.
|
||||
*/
|
||||
protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) {
|
||||
// If the layout has pending changes, add a warning.
|
||||
if ($this->layoutTempstoreRepository->has($section_storage)) {
|
||||
$this->messenger->addWarning($this->t('You have unsaved changes.'));
|
||||
}
|
||||
|
||||
// Only add sections if the layout is new and empty.
|
||||
if (!$is_rebuilding && $section_storage->count() === 0) {
|
||||
$sections = [];
|
||||
|
@ -269,7 +281,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
|
|||
],
|
||||
'remove' => [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Remove section'),
|
||||
'#title' => $this->t('Remove section <span class="visually-hidden">@section</span>', ['@section' => $delta + 1]),
|
||||
'#url' => Url::fromRoute('layout_builder.remove_section', [
|
||||
'section_storage_type' => $storage_type,
|
||||
'section_storage' => $storage_id,
|
||||
|
|
|
@ -9,6 +9,7 @@ use Drupal\Core\Plugin\Context\EntityContext;
|
|||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
|
||||
|
@ -110,10 +111,10 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
|
|||
$bundle = $this->getTargetBundle();
|
||||
|
||||
if ($new_value) {
|
||||
$this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout');
|
||||
$this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
|
||||
}
|
||||
else {
|
||||
$this->removeSectionField($entity_type_id, $bundle, 'layout_builder__layout');
|
||||
$this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,8 +275,8 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
|
|||
* The sections.
|
||||
*/
|
||||
protected function getRuntimeSections(FieldableEntityInterface $entity) {
|
||||
if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
|
||||
return $entity->get('layout_builder__layout')->getSections();
|
||||
if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
|
||||
return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
|
||||
}
|
||||
|
||||
return $this->getSections();
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace Drupal\layout_builder\EventSubscriber;
|
|||
use Drupal\block_content\Access\RefinableDependentAccessInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Block\BlockPluginInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Render\PreviewFallbackInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
|
||||
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
|
||||
|
@ -88,6 +90,14 @@ class BlockComponentRenderArray implements EventSubscriberInterface {
|
|||
if ($access->isAllowed()) {
|
||||
$event->addCacheableDependency($block);
|
||||
|
||||
$content = $block->build();
|
||||
$is_content_empty = Element::isEmpty($content);
|
||||
$is_placeholder_ready = $event->inPreview() && $block instanceof PreviewFallbackInterface;
|
||||
// If the content is empty and no placeholder is available, return.
|
||||
if ($is_content_empty && !$is_placeholder_ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
$build = [
|
||||
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
|
||||
'#theme' => 'block',
|
||||
|
@ -96,8 +106,11 @@ class BlockComponentRenderArray implements EventSubscriberInterface {
|
|||
'#base_plugin_id' => $block->getBaseId(),
|
||||
'#derivative_plugin_id' => $block->getDerivativeId(),
|
||||
'#weight' => $event->getComponent()->getWeight(),
|
||||
'content' => $block->build(),
|
||||
'content' => $content,
|
||||
];
|
||||
if ($is_content_empty && $is_placeholder_ready) {
|
||||
$build['content']['#markup'] = $block->getPreviewFallbackString();
|
||||
}
|
||||
$event->setBuild($build);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use Drupal\Core\Field\FieldDefinitionInterface;
|
|||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\field_ui\Form\EntityViewDisplayEditForm;
|
||||
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
|
||||
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
|
@ -48,8 +49,8 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
|
|||
$form = parent::form($form, $form_state);
|
||||
|
||||
// Remove the Layout Builder field from the list.
|
||||
$form['#fields'] = array_diff($form['#fields'], ['layout_builder__layout']);
|
||||
unset($form['fields']['layout_builder__layout']);
|
||||
$form['#fields'] = array_diff($form['#fields'], [OverridesSectionStorage::FIELD_NAME]);
|
||||
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
|
||||
|
||||
$is_enabled = $this->entity->isLayoutBuilderEnabled();
|
||||
if ($is_enabled) {
|
||||
|
@ -133,7 +134,7 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
|
|||
|
||||
$entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
|
||||
$query = $this->entityTypeManager->getStorage($display->getTargetEntityTypeId())->getQuery()
|
||||
->exists('layout_builder__layout');
|
||||
->exists(OverridesSectionStorage::FIELD_NAME);
|
||||
if ($bundle_key = $entity_type->getKey('bundle')) {
|
||||
$query->condition($bundle_key, $display->getTargetBundle());
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use Drupal\Component\Plugin\DerivativeInspectionInterface;
|
|||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
|
||||
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
|
||||
|
||||
/**
|
||||
* Methods to help with entities using the layout builder.
|
||||
|
@ -65,7 +66,7 @@ trait LayoutEntityHelperTrait {
|
|||
return $entity->getSections();
|
||||
}
|
||||
elseif ($this->isEntityUsingFieldOverride($entity)) {
|
||||
return $entity->get('layout_builder__layout')->getSections();
|
||||
return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
@ -102,7 +103,7 @@ trait LayoutEntityHelperTrait {
|
|||
* TRUE if the entity is using a field for a layout override.
|
||||
*/
|
||||
protected function isEntityUsingFieldOverride(EntityInterface $entity) {
|
||||
return $entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout');
|
||||
return $entity instanceof FieldableEntityInterface && $entity->hasField(OverridesSectionStorage::FIELD_NAME);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -45,6 +45,15 @@ class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
|
|||
return $section_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function has(SectionStorageInterface $section_storage) {
|
||||
$id = $section_storage->getStorageId();
|
||||
$tempstore = $this->getTempstore($section_storage)->get($id);
|
||||
return !empty($tempstore['section_storage']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
@ -35,6 +35,17 @@ interface LayoutTempstoreRepositoryInterface {
|
|||
*/
|
||||
public function set(SectionStorageInterface $section_storage);
|
||||
|
||||
/**
|
||||
* Checks for the existence of a tempstore version of a section storage.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage to check for in tempstore.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if there is a tempstore version of this section storage.
|
||||
*/
|
||||
public function has(SectionStorageInterface $section_storage);
|
||||
|
||||
/**
|
||||
* Removes the tempstore version of a section storage.
|
||||
*
|
||||
|
|
|
@ -130,13 +130,22 @@ class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface,
|
|||
// render array. If the hook is invoked the placeholder will be
|
||||
// replaced.
|
||||
// @see ::replaceFieldPlaceholder()
|
||||
'#markup' => new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]),
|
||||
'#markup' => $this->getPreviewFallbackString(),
|
||||
];
|
||||
}
|
||||
CacheableMetadata::createFromObject($this)->applyTo($build);
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPreviewFallbackString() {
|
||||
$entity = $this->getEntity();
|
||||
$extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
|
||||
return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all placeholders for a given field.
|
||||
*
|
||||
|
|
|
@ -17,7 +17,6 @@ use Drupal\Core\Field\FormatterPluginManager;
|
|||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Plugin\ContextAwarePluginInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
@ -160,13 +159,17 @@ class FieldBlock extends BlockBase implements ContextAwarePluginInterface, Conta
|
|||
$build = [];
|
||||
$this->logger->warning('The field "%field" failed to render with the error of "%error".', ['%field' => $this->fieldName, '%error' => $e->getMessage()]);
|
||||
}
|
||||
if (!empty($entity->in_preview) && !Element::getVisibleChildren($build)) {
|
||||
$build['content']['#markup'] = new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
|
||||
}
|
||||
CacheableMetadata::createFromObject($this)->applyTo($build);
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPreviewFallbackString() {
|
||||
return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
@ -106,7 +106,7 @@ class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface
|
|||
$derivative['default_formatter'] = $field_type_definition['default_formatter'];
|
||||
}
|
||||
|
||||
$derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]);
|
||||
$derivative['category'] = $this->t('@entity fields', ['@entity' => $entity_type_labels[$entity_type_id]]);
|
||||
|
||||
$derivative['admin_label'] = $field_definition->getLabel();
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
namespace Drupal\layout_builder\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageLocalTaskProviderInterface;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
|
@ -28,14 +28,24 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
|
|||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The section storage manager.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
|
||||
*/
|
||||
protected $sectionStorageManager;
|
||||
|
||||
/**
|
||||
* Constructs a new LayoutBuilderLocalTaskDeriver.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
|
||||
* The section storage manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, SectionStorageManagerInterface $section_storage_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->sectionStorageManager = $section_storage_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,7 +53,8 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
|
|||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager')
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('plugin.manager.layout_builder.section_storage')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -51,84 +62,13 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
foreach ($this->getEntityTypesForOverrides() as $entity_type_id => $entity_type) {
|
||||
// Overrides.
|
||||
$this->derivatives["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 15,
|
||||
'title' => $this->t('Layout'),
|
||||
'base_route' => "entity.$entity_type_id.canonical",
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
$this->derivatives["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.save",
|
||||
'title' => $this->t('Save Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
$this->derivatives["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
|
||||
'title' => $this->t('Cancel Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 5,
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
// @todo This link should be conditionally displayed, see
|
||||
// https://www.drupal.org/node/2917777.
|
||||
$this->derivatives["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.revert",
|
||||
'title' => $this->t('Revert to defaults'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 10,
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) {
|
||||
$section_storage = $this->sectionStorageManager->loadEmpty($plugin_id);
|
||||
if ($section_storage instanceof SectionStorageLocalTaskProviderInterface) {
|
||||
$this->derivatives += $section_storage->buildLocalTasks($base_plugin_definition);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->getEntityTypesForDefaults() as $entity_type_id => $entity_type) {
|
||||
// Defaults.
|
||||
$this->derivatives["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.view",
|
||||
'title' => $this->t('Manage layout'),
|
||||
'base_route' => "layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
$this->derivatives["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.save",
|
||||
'title' => $this->t('Save Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
$this->derivatives["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
|
||||
'title' => $this->t('Cancel Layout'),
|
||||
'weight' => 5,
|
||||
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
}
|
||||
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of entity types relevant for defaults.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface[]
|
||||
* An array of entity types.
|
||||
*/
|
||||
protected function getEntityTypesForDefaults() {
|
||||
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
|
||||
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of entity types relevant for overrides.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface[]
|
||||
* An array of entity types.
|
||||
*/
|
||||
protected function getEntityTypesForOverrides() {
|
||||
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
|
||||
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ use Symfony\Component\Routing\RouteCollection;
|
|||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface {
|
||||
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
|
@ -196,6 +196,32 @@ class DefaultsSectionStorage extends SectionStorageBase implements ContainerFact
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildLocalTasks($base_plugin_definition) {
|
||||
$local_tasks = [];
|
||||
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
|
||||
$local_tasks["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.view",
|
||||
'title' => $this->t('Manage layout'),
|
||||
'base_route' => "layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
$local_tasks["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.save",
|
||||
'title' => $this->t('Save Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
$local_tasks["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
|
||||
'title' => $this->t('Cancel Layout'),
|
||||
'weight' => 5,
|
||||
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
}
|
||||
return $local_tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of relevant entity types.
|
||||
*
|
||||
|
|
|
@ -30,7 +30,14 @@ use Symfony\Component\Routing\RouteCollection;
|
|||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface {
|
||||
class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
|
||||
|
||||
/**
|
||||
* The field name used by this storage.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const FIELD_NAME = 'layout_builder__layout';
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
|
@ -127,8 +134,8 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
|
|||
if (strpos($id, '.') !== FALSE) {
|
||||
list($entity_type_id, $entity_id) = explode('.', $id, 2);
|
||||
$entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
|
||||
if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) {
|
||||
return $entity->get('layout_builder__layout');
|
||||
if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) {
|
||||
return $entity->get(static::FIELD_NAME);
|
||||
}
|
||||
}
|
||||
throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
|
||||
|
@ -157,6 +164,45 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildLocalTasks($base_plugin_definition) {
|
||||
$local_tasks = [];
|
||||
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
|
||||
$local_tasks["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 15,
|
||||
'title' => $this->t('Layout'),
|
||||
'base_route' => "entity.$entity_type_id.canonical",
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
$local_tasks["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.save",
|
||||
'title' => $this->t('Save Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
$local_tasks["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
|
||||
'title' => $this->t('Cancel Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 5,
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
// @todo This link should be conditionally displayed, see
|
||||
// https://www.drupal.org/node/2917777.
|
||||
$local_tasks["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.revert",
|
||||
'title' => $this->t('Revert to defaults'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 10,
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
}
|
||||
return $local_tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this entity type's ID is stored as an integer.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\SectionStorage;
|
||||
|
||||
/**
|
||||
* Allows section storage plugins to provide local tasks.
|
||||
*
|
||||
* @see \Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver
|
||||
* @see \Drupal\layout_builder\SectionStorageInterface
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
interface SectionStorageLocalTaskProviderInterface {
|
||||
|
||||
/**
|
||||
* Provides the local tasks dynamically for Layout Builder plugins.
|
||||
*
|
||||
* @param mixed $base_plugin_definition
|
||||
* The definition of the base plugin.
|
||||
*
|
||||
* @return array
|
||||
* An array of full derivative definitions keyed on derivative ID.
|
||||
*/
|
||||
public function buildLocalTasks($base_plugin_definition);
|
||||
|
||||
}
|
|
@ -356,4 +356,13 @@ class Section {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method: Implements a deep clone.
|
||||
*/
|
||||
public function __clone() {
|
||||
foreach ($this->components as $uuid => $component) {
|
||||
$this->components[$uuid] = clone $component;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -111,4 +111,17 @@ trait SectionStorageTrait {
|
|||
return isset($this->getSections()[$delta]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method: Implements a deep clone.
|
||||
*/
|
||||
public function __clone() {
|
||||
$sections = $this->getSections();
|
||||
|
||||
foreach ($sections as $delta => $item) {
|
||||
$sections[$delta] = clone $item;
|
||||
}
|
||||
|
||||
$this->setSections($sections);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
|
||||
block.settings.field_block_test:*:*:*:
|
||||
type: block.settings.field_block:*:*:*
|
|
@ -0,0 +1,6 @@
|
|||
name: 'Layout Builder test'
|
||||
type: module
|
||||
description: 'Support module for testing layout building.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\layout_builder_fieldblock_test\Plugin\Block;
|
||||
|
||||
use Drupal\layout_builder\Plugin\Block\FieldBlock as LayoutBuilderFieldBlock;
|
||||
|
||||
/**
|
||||
* Provides test field block to test with Block UI.
|
||||
*
|
||||
* \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest provides
|
||||
* test coverage of complex AJAX interactions within certain field blocks.
|
||||
* layout_builder_plugin_filter_block__block_ui_alter() removes certain blocks
|
||||
* with 'layout_builder' as the provider. To make these blocks available during
|
||||
* testing, this plugin uses the same deriver but each derivative will have a
|
||||
* different provider.
|
||||
*
|
||||
* @Block(
|
||||
* id = "field_block_test",
|
||||
* deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver",
|
||||
* )
|
||||
*
|
||||
* @see \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest
|
||||
* @see layout_builder_plugin_filter_block__block_ui_alter()
|
||||
*/
|
||||
class FieldBlock extends LayoutBuilderFieldBlock {
|
||||
|
||||
}
|
|
@ -22,6 +22,7 @@ class LayoutBuilderTest extends BrowserTestBase {
|
|||
'layout_builder_views_test',
|
||||
'layout_test',
|
||||
'block',
|
||||
'block_test',
|
||||
'node',
|
||||
'layout_builder_test',
|
||||
];
|
||||
|
@ -90,7 +91,7 @@ class LayoutBuilderTest extends BrowserTestBase {
|
|||
// The body field is only present once.
|
||||
$assert_session->elementsCount('css', '.field--name-body', 1);
|
||||
// The extra field is only present once.
|
||||
$this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
|
||||
$assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
|
||||
// Save the defaults.
|
||||
$assert_session->linkExists('Save Layout');
|
||||
$this->clickLink('Save Layout');
|
||||
|
@ -105,7 +106,7 @@ class LayoutBuilderTest extends BrowserTestBase {
|
|||
// The body field is only present once.
|
||||
$assert_session->elementsCount('css', '.field--name-body', 1);
|
||||
// The extra field is only present once.
|
||||
$this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
|
||||
$assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
|
||||
|
||||
// Add a new block.
|
||||
$assert_session->linkExists('Add Block');
|
||||
|
@ -316,6 +317,11 @@ class LayoutBuilderTest extends BrowserTestBase {
|
|||
$page->fillField('id', 'myothermenu');
|
||||
$page->pressButton('Save');
|
||||
|
||||
$page->clickLink('Add link');
|
||||
$page->fillField('title[0][value]', 'My link');
|
||||
$page->fillField('link[0][uri]', '/');
|
||||
$page->pressButton('Save');
|
||||
|
||||
$this->drupalPostForm('admin/structure/types/manage/bundle_with_section_field/display', ['layout[enabled]' => TRUE], 'Save');
|
||||
$assert_session->linkExists('Manage layout');
|
||||
$this->clickLink('Manage layout');
|
||||
|
@ -514,13 +520,68 @@ class LayoutBuilderTest extends BrowserTestBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Asserts that a text string only appears once on the page.
|
||||
* Tests the usage of placeholders for empty blocks.
|
||||
*
|
||||
* @param string $needle
|
||||
* The string to look for.
|
||||
* @see \Drupal\Core\Block\BlockPluginInterface::getPlaceholderString()
|
||||
* @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
|
||||
*/
|
||||
protected function assertTextAppearsOnce($needle) {
|
||||
$this->assertEquals(1, substr_count($this->getSession()->getPage()->getContent(), $needle), "'$needle' only appears once on the page.");
|
||||
public function testBlockPlaceholder() {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'configure any layout',
|
||||
'administer node display',
|
||||
]));
|
||||
|
||||
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
|
||||
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save');
|
||||
|
||||
// Customize the default view mode.
|
||||
$this->drupalGet("$field_ui_prefix/display-layout/default");
|
||||
|
||||
// Add a block whose content is controlled by state and is empty by default.
|
||||
$this->clickLink('Add Block');
|
||||
$this->clickLink('Test block caching');
|
||||
$page->fillField('settings[label]', 'The block label');
|
||||
$page->pressButton('Add Block');
|
||||
|
||||
$block_content = 'I am content';
|
||||
$placeholder_content = 'Placeholder for the "The block label" block';
|
||||
|
||||
// The block placeholder is displayed and there is no content.
|
||||
$assert_session->pageTextContains($placeholder_content);
|
||||
$assert_session->pageTextNotContains($block_content);
|
||||
|
||||
// Set block content and reload the page.
|
||||
\Drupal::state()->set('block_test.content', $block_content);
|
||||
$this->getSession()->reload();
|
||||
|
||||
// The block placeholder is no longer displayed and the content is visible.
|
||||
$assert_session->pageTextNotContains($placeholder_content);
|
||||
$assert_session->pageTextContains($block_content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the Block UI when Layout Builder is installed.
|
||||
*/
|
||||
public function testBlockUiListing() {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'administer blocks',
|
||||
]));
|
||||
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$page->clickLink('Place block');
|
||||
|
||||
// Ensure that blocks expected to appear are available.
|
||||
$assert_session->pageTextContains('Test HTML block');
|
||||
$assert_session->pageTextContains('Block test');
|
||||
// Ensure that blocks not expected to appear are not available.
|
||||
$assert_session->pageTextNotContains('Body');
|
||||
$assert_session->pageTextNotContains('Content fields');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue