"composer/installers": "^1.2",
"drupal-composer/drupal-scaffold": "^2.2",
"cweagans/composer-patches": "~1.0",
- "drupal/core": "^8.5.0",
+ "drupal/core": "8.6.4",
"drupal/console": "^1.0",
"drupal/token": "^1.0",
"drupal/ctools": "^3.0",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "8cfc3879a25f15d6aba3eaccfc3cb818",
+ "content-hash": "38bc6fe27fbd03929a4718f3c529c7d0",
"packages": [
{
"name": "alchemy/zippy",
},
{
"name": "drupal/core",
- "version": "8.6.3",
+ "version": "8.6.4",
"source": {
"type": "git",
"url": "https://github.com/drupal/core.git",
- "reference": "9e9a1dd9e280ebaf10622217e54448b529167965"
+ "reference": "652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/drupal/core/zipball/9e9a1dd9e280ebaf10622217e54448b529167965",
- "reference": "9e9a1dd9e280ebaf10622217e54448b529167965",
+ "url": "https://api.github.com/repos/drupal/core/zipball/652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4",
+ "reference": "652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4",
"shasum": ""
},
"require": {
"GPL-2.0-or-later"
],
"description": "Drupal is an open source content management platform powering millions of websites and applications.",
- "time": "2018-11-07T14:45:40+00:00"
+ "time": "2018-12-05T11:58:02+00:00"
},
{
"name": "drupal/crop",
},
{
"name": "drupal/core",
- "version": "8.6.3",
- "version_normalized": "8.6.3.0",
+ "version": "8.6.4",
+ "version_normalized": "8.6.4.0",
"source": {
"type": "git",
"url": "https://github.com/drupal/core.git",
- "reference": "9e9a1dd9e280ebaf10622217e54448b529167965"
+ "reference": "652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/drupal/core/zipball/9e9a1dd9e280ebaf10622217e54448b529167965",
- "reference": "9e9a1dd9e280ebaf10622217e54448b529167965",
+ "url": "https://api.github.com/repos/drupal/core/zipball/652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4",
+ "reference": "652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4",
"shasum": ""
},
"require": {
"symfony/debug": "^3.4.0",
"symfony/phpunit-bridge": "^3.4.3"
},
- "time": "2018-11-07T14:45:40+00:00",
+ "time": "2018-12-05T11:58:02+00:00",
"type": "drupal-core",
"extra": {
"merge-plugin": {
*/
$url = parse_url($_SERVER['REQUEST_URI']);
-if (file_exists('.' . $url['path'])) {
+if (file_exists(__DIR__ . $url['path'])) {
// Serve the requested resource as-is.
return FALSE;
}
// fallback to index.php.
do {
$path = dirname($path);
- if (preg_match('/\.php$/', $path) && is_file('.' . $path)) {
+ if (preg_match('/\.php$/', $path) && is_file(__DIR__ . $path)) {
// Discovered that the path contains an existing PHP file. Use that as the
// script to include.
$script = ltrim($path, '/');
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
* 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);
+ }
}
/**
*
* 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(
/**
* The current system version.
*/
- const VERSION = '8.6.3';
+ const VERSION = '8.6.4';
/**
* Core API compatibility.
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;
*/
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'] : [];
+ }
}
/**
*/
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);
+ }
+ }
+ elseif (!empty($definition['context'][$name])) {
+ return $definition['context'][$name];
}
- return $definition['context'][$name];
+ throw new ContextException(sprintf("The %s context is not a valid context.", $name));
}
/**
--- /dev/null
+<?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;
+ }
+
+}
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;
*
* @ingroup block_api
*/
-abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface {
+abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface, PreviewFallbackInterface {
use ContextAwarePluginAssignmentTrait;
use MessengerTrait;
return $transliterated;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getPreviewFallbackString() {
+ return $this->t('Placeholder for the "@block" block', ['@block' => $this->label()]);
+ }
+
/**
* Wraps the transliteration service.
*
],
[
'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,
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;
/**
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'];
}
}
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.
*/
protected $entity;
+ /**
+ * The typed config manager.
+ *
+ * @var \Drupal\Core\Config\TypedConfigManagerInterface
+ */
+ protected $typedConfigManager;
+
/**
* {@inheritdoc}
*/
}
/**
- * 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)) {
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}
*/
* 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());
}
}
// 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();
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;
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) {
}
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();
}
}
->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();
}
/**
* 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.
*
$elements = [];
$url = NULL;
if ($this->getSetting('link_to_entity')) {
- // For the default revision this falls back to 'canonical'.
$url = $this->getEntityUrl($items->getEntity());
}
* 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);
}
}
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.
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;
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);
}
/**
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;
// 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;
}
$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';
}
* {@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;
}
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();
--- /dev/null
+<?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();
+
+}
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);
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}
*/
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());
$this->migrateStoredSession($old_session_id);
}
- if (!$this->isStarted()) {
- // Start the session when it doesn't exist yet.
- $this->startNow();
- }
+ $this->startNow();
}
/**
namespace Drupal\Core\TempStore;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Session\AccountProxyInterface;
* \Drupal\Core\TempStore\SharedTempStore.
*/
class PrivateTempStore {
+ use DependencySerializationTrait;
/**
* The key/value storage object used for this data.
$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)) {
// Expose constructor in the public space.
Drupal.TableHeader = TableHeader;
-})(jQuery, Drupal, window.parent.Drupal.displace);
+})(jQuery, Drupal, window.Drupal.displace);
});
Drupal.TableHeader = TableHeader;
-})(jQuery, Drupal, window.parent.Drupal.displace);
\ No newline at end of file
+})(jQuery, Drupal, window.Drupal.displace);
\ No newline at end of file
$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]);
}
/**
$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) {
* 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);
}
* 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);
}
// 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());
}
}
$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]);
}
/**
$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) {
* 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);
}
* 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);
}
$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');
}
}
use Drupal\Core\Url;
use Drupal\aggregator\Entity\Feed;
+use Drupal\aggregator\Entity\Item;
/**
* Tests the built-in feed parser with valid feed samples.
$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.');
}
/**
namespace Drupal\Tests\aggregator\Functional;
+use Drupal\aggregator\Entity\Feed;
+
/**
* Tests OPML import.
*
* 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'));
$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.');
}
* 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'));
$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();
$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.');
namespace Drupal\Tests\aggregator\Functional;
use Drupal\aggregator\Entity\Feed;
+use Drupal\aggregator\Entity\Item;
/**
* Update feed items from a feed.
$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
*/
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')
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);
if (isset($operations['delete'])) {
$operations['delete']['title'] = $this->t('Remove');
+ // Block operation links should have the `block-placement` query string
+ // parameter removed to ensure that JavaScript does not receive a block
+ // name that has been recently removed.
+ foreach ($operations as $operation) {
+ /** @var \Drupal\Core\Url $url */
+ $url = $operation['url'];
+ $query = $url->getOption('query');
+ $destination = $query['destination'];
+
+ $destinationUrl = Url::fromUserInput($destination);
+ $destinationQuery = $destinationUrl->getOption('query');
+ unset($destinationQuery['block-placement']);
+
+ $destinationUrl->setOption('query', $destinationQuery);
+ $query['destination'] = $destinationUrl->toString();
+ $url->setOption('query', $query);
+ }
}
return $operations;
}
--- /dev/null
+<?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;
+ }
+
+}
$this->assertNoRaw($block->id());
}
+ /**
+ * Tests the block operation links.
+ */
+ public function testBlockOperationLinks() {
+ $this->drupalGet('admin/structure/block');
+ // Go to the select block form.
+ $this->clickLink('Place block');
+ // Select the first available block.
+ $this->clickLink('Place block');
+ // Finally place the block
+ $this->submitForm([], 'Save block');
+
+ $url = $this->getUrl();
+ $parsed = parse_url($url);
+ $this->assertContains('block-placement', $parsed['query']);
+
+ $this->clickLink('Remove');
+ $this->submitForm([], 'Remove');
+
+ $url = $this->getUrl();
+ $parsed = parse_url($url);
+ $this->assertTrue(empty($parsed['query']));
+ }
+
/**
* Tests that the block form has a theme selector when not passed via the URL.
*/
--- /dev/null
+<?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'));
+ }
+
+}
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
--- /dev/null
+<?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;
+ }
+
+}
* 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');
// 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);
+ }
}
}
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);
--- /dev/null
+<?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.');
+ }
+
+}
// 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);
// 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);
// 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());
// 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());
$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());
$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);
$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.
// 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());
}
/**
/**
* 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.
*/
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
$this->testNode->save();
\Drupal::entityTypeManager()->getStorage('node')->resetCache();
$this->testNode = Node::load($this->testNode->id());
+
+ ConfigurableLanguage::createFromLangcode('de')->save();
}
/**
];
}
+ /**
+ * 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);
+ }
+
}
--- /dev/null
+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
--- /dev/null
+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
$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");
}
/**
<?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;
*/
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',
$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',
// 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() . ')');
'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() . "']");
* 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;
- }
-
}
--- /dev/null
+<?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);
+ }
+ }
+
+}
*/
public static $modules = ['field', 'text', 'entity_test', 'system', 'filter', 'user'];
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
/**
* @var string
*/
'settings' => [],
]);
$this->display->save();
+
+ $this->entityTypeManager = \Drupal::entityTypeManager();
}
/**
$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);
$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'));
}
}
/**
* 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() {
* @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', []);
$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;
- }
+ $files[$i] = _file_save_upload_single($file_info, $form_field_name, $validators, $destination, $replace);
+ }
- // 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;
+ // Add files to the cache.
+ $upload_cache[$form_field_name] = $files;
- }
- // 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;
- }
+ return isset($delta) ? $files[$delta] : $files;
+}
- 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));
- }
+/**
+ * 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;
- // 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()]));
+ 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;
}
- }
- // If the destination is not provided, use the temporary directory.
- if (empty($destination)) {
- $destination = 'temporary://';
- }
+ 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;
- // 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;
+ }
+ // 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];
}
-
- $file->source = $form_field_name;
- // A file URI may already have a trailing slash or look like "public://".
- if (substr($destination, -1) != '/') {
- $destination .= '/';
+ 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']);
}
- $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;
+ }
+ 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()]));
}
+ }
- // Add in our check of the file name length.
- $validators['file_validate_name_length'] = [];
+ // If the destination is not provided, use the temporary directory.
+ if (empty($destination)) {
+ $destination = 'temporary://';
+ }
- // Call the validation functions specified by this function's caller.
- $errors = file_validate($file, $validators);
+ // 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;
+ }
- // 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->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;
+ }
- $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;
- }
+ // Add in our check of the file name length.
+ $validators['file_validate_name_length'] = [];
- // Set the permissions on the new file.
- drupal_chmod($file->getFileUri());
+ // 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;
+ }
- // 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());
- }
- }
+ $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;
+ }
- // 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);
+ // 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());
}
}
- // Add files to the cache.
- $upload_cache[$form_field_name] = $files;
+ // If we made it this far it's safe to record this file in the database.
+ $file->save();
- return isset($delta) ? $files[$delta] : $files;
+ // 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;
}
/**
--- /dev/null
+<?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());
+ }
+
+}
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}
$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
--- /dev/null
+<?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');
+ }
+
+}
// 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]);
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;
+}
- 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
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().
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]);
}
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]);
}
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;
+ }
+ }
+ }
+ }
+}
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:
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;
*/
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;
}
/**
*/
public static function create(ContainerInterface $container) {
return new static(
- $container->get('plugin.manager.block')
+ $container->get('plugin.manager.block'),
+ $container->get('entity_type.manager')
);
}
*/
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.
'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 [];
+ }
+
}
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;
use LayoutBuilderContextTrait;
use StringTranslationTrait;
+ use AjaxHelperTrait;
/**
* The layout tempstore repository.
$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);
* 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 = [];
],
'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,
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;
$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);
}
}
* 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();
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;
'#weight' => $event->getComponent()->getWeight(),
'content' => $block->build(),
];
+ if ($event->inPreview() && Element::isEmpty($build['content']) && $block instanceof PreviewFallbackInterface) {
+ $build['content']['#markup'] = $block->getPreviewFallbackString();
+ }
$event->setBuild($build);
}
}
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;
/**
$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) {
$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());
}
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.
return $entity->getSections();
}
elseif ($this->isEntityUsingFieldOverride($entity)) {
- return $entity->get('layout_builder__layout')->getSections();
+ return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
}
return NULL;
}
* 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);
}
}
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}
*/
*/
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.
*
// 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.
*
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;
$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}
*/
$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();
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;
/**
*/
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;
}
/**
*/
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')
);
}
* {@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');
- });
- }
-
}
* 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.
}
}
+ /**
+ * {@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.
*
* 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.
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()));
}
}
+ /**
+ * {@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.
*
--- /dev/null
+<?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);
+
+}
);
}
+ /**
+ * Magic method: Implements a deep clone.
+ */
+ public function __clone() {
+ foreach ($this->components as $uuid => $component) {
+ $this->components[$uuid] = clone $component;
+ }
+ }
+
}
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);
+ }
+
}
--- /dev/null
+# See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
+block.settings.field_block_test:*:*:*:
+ type: block.settings.field_block:*:*:*
--- /dev/null
+name: 'Layout Builder test'
+type: module
+description: 'Support module for testing layout building.'
+package: Testing
+version: VERSION
+core: 8.x
--- /dev/null
+<?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 {
+
+}
'layout_builder_views_test',
'layout_test',
'block',
+ 'block_test',
'node',
'layout_builder_test',
];
// 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');
// 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');
}
/**
- * 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');
}
}
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\BrowserTestBase;
*/
public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test'];
- /**
- * The name of the layout section field.
- *
- * @var string
- */
- protected $fieldName = 'layout_builder__layout';
-
/**
* {@inheritdoc}
*/
]);
$entity->addTranslation('es', [
'title' => 'Translated node title',
- $this->fieldName => [
+ OverridesSectionStorage::FIELD_NAME => [
[
'section' => new Section('layout_twocol', [], [
'foo' => new SectionComponent('foo', 'first', [
'value' => 'The node body',
],
],
- $this->fieldName => $section_values,
+ OverridesSectionStorage::FIELD_NAME => $section_values,
]);
}
/**
* {@inheritdoc}
*/
- public static $modules = ['block', 'datetime', 'layout_builder', 'user'];
+ protected static $modules = [
+ 'block',
+ 'datetime',
+ 'layout_builder',
+ 'user',
+ // See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
+ 'layout_builder_fieldblock_test',
+ ];
/**
* {@inheritdoc}
$assert_session->pageTextNotContains('Initial email');
$assert_session->pageTextContains('Date field');
- $block_url = 'admin/structure/block/add/field_block%3Auser%3Auser%3Afield_date/classy';
+ $block_url = 'admin/structure/block/add/field_block_test%3Auser%3Auser%3Afield_date/classy';
$assert_session->linkByHrefExists($block_url);
$this->drupalGet($block_url);
$page = $this->getSession()->getPage();
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
- $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
- $this->clickLink('Basic block');
+ $this->assertNotEmpty($assert_session->waitForLink('Create custom block'));
+ $this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldValueEquals('Title', '');
$page->findField('Title')->setValue($title);
$assert_session->pageTextNotContains('You are not authorized to access this page');
}
+ /**
+ * Tests the workflow for adding an inline block depending on number of types.
+ *
+ * @throws \Behat\Mink\Exception\ElementNotFoundException
+ * @throws \Behat\Mink\Exception\ExpectationException
+ */
+ public function testAddWorkFlow() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $type_storage = $this->container->get('entity_type.manager')->getStorage('block_content_type');
+ foreach ($type_storage->loadByProperties() as $type) {
+ $type->delete();
+ }
+
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access contextual links',
+ 'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ ]));
+
+ // Enable layout builder and overrides.
+ $this->drupalPostForm(
+ static::FIELD_UI_PREFIX . '/display/default',
+ ['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
+ 'Save'
+ );
+
+ $layout_default_path = 'admin/structure/types/manage/bundle_with_section_field/display-layout/default';
+ $this->drupalGet($layout_default_path);
+ // Add a basic block with the body field set.
+ $page->clickLink('Add Block');
+ $assert_session->assertWaitOnAjaxRequest();
+ // Confirm that with no block content types the link does not appear.
+ $assert_session->linkNotExists('Create custom block');
+
+ $this->createBlockContentType('basic', 'Basic block');
+
+ $this->drupalGet($layout_default_path);
+ // Add a basic block with the body field set.
+ $page->clickLink('Add Block');
+ $assert_session->assertWaitOnAjaxRequest();
+ // Confirm with only 1 type the "Create custom block" link goes directly t
+ // block add form.
+ $assert_session->linkNotExists('Basic block');
+ $this->clickLink('Create custom block');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->fieldExists('Title');
+
+ $this->createBlockContentType('advanced', 'Advanced block');
+
+ $this->drupalGet($layout_default_path);
+ // Add a basic block with the body field set.
+ $page->clickLink('Add Block');
+ // Confirm that, when more than 1 type exists, "Create custom block" shows a
+ // list of block types.
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->linkNotExists('Basic block');
+ $assert_session->linkNotExists('Advanced block');
+ $this->clickLink('Create custom block');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->fieldNotExists('Title');
+ $assert_session->linkExists('Basic block');
+ $assert_session->linkExists('Advanced block');
+
+ $this->clickLink('Advanced block');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->fieldExists('Title');
+ }
+
}
],
],
]);
- $bundle = BlockContentType::create([
- 'id' => 'basic',
- 'label' => 'Basic block',
- 'revision' => 1,
- ]);
- $bundle->save();
- block_content_add_body_field($bundle->id());
+ $this->createBlockContentType('basic', 'Basic block');
$this->blockStorage = $this->container->get('entity_type.manager')->getStorage('block_content');
}
$page = $this->getSession()->getPage();
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
- $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
- $this->clickLink('Basic block');
+ $this->assertNotEmpty($assert_session->waitForLink('Create custom block'));
+ $this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$textarea = $assert_session->waitForElement('css', '[name="settings[block_form][body][0][value]"]');
$this->assertNotEmpty($textarea);
}
}
+ /**
+ * Creates a block content type.
+ *
+ * @param string $id
+ * The block type id.
+ * @param string $label
+ * The block type label.
+ */
+ protected function createBlockContentType($id, $label) {
+ $bundle = BlockContentType::create([
+ 'id' => $id,
+ 'label' => $label,
+ 'revision' => 1,
+ ]);
+ $bundle->save();
+ block_content_add_body_field($bundle->id());
+ }
+
}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\layout_builder\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the Layout Builder UI.
+ *
+ * @group layout_builder
+ */
+class LayoutBuilderUiTest extends WebDriverTestBase {
+
+ /**
+ * Path prefix for the field UI for the test bundle.
+ *
+ * @var string
+ */
+ const FIELD_UI_PREFIX = 'admin/structure/types/manage/bundle_with_section_field';
+
+ public static $modules = [
+ 'layout_builder',
+ 'block',
+ 'node',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // @todo The Layout Builder UI relies on local tasks; fix in
+ // https://www.drupal.org/project/drupal/issues/2917777.
+ $this->drupalPlaceBlock('local_tasks_block');
+
+ $this->createContentType(['type' => 'bundle_with_section_field']);
+
+ $this->drupalLogin($this->drupalCreateUser([
+ 'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ ]));
+ }
+
+ /**
+ * Tests the message indicating unsaved changes.
+ */
+ public function testUnsavedChangesMessage() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Enable layout builder.
+ $this->drupalPostForm(
+ static::FIELD_UI_PREFIX . '/display/default',
+ ['layout[enabled]' => TRUE],
+ 'Save'
+ );
+
+ // Make and then cancel changes.
+ $this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display-layout/default');
+ $page->clickLink('Cancel Layout');
+ $assert_session->pageTextNotContains('You have unsaved changes.');
+
+ // Make and then save changes.
+ $this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display-layout/default');
+ $page->clickLink('Save Layout');
+ $assert_session->pageTextNotContains('You have unsaved changes.');
+ }
+
+ /**
+ * Asserts that modifying a layout works as expected.
+ *
+ * @param string $path
+ * The path to a Layout Builder UI page.
+ */
+ protected function assertModifiedLayout($path) {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ $this->drupalGet($path);
+ $page->clickLink('Add Section');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextNotContains('You have unsaved changes.');
+ $page->clickLink('One column');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextContainsOnce('You have unsaved changes.');
+
+ // Reload the page.
+ $this->drupalGet($path);
+ $assert_session->pageTextContainsOnce('You have unsaved changes.');
+ }
+
+}
* @covers ::build
* @dataProvider providerTestBuild
*/
- public function testBuild(PromiseInterface $promise, $in_preview, $expected_markup, $log_message = '', $log_arguments = []) {
+ public function testBuild(PromiseInterface $promise, $expected_markup, $log_message = '', $log_arguments = []) {
$entity = $this->prophesize(FieldableEntityInterface::class);
$field = $this->prophesize(FieldItemListInterface::class);
$entity->get('the_field_name')->willReturn($field->reveal());
- $entity->in_preview = $in_preview;
$field->view(Argument::type('array'))->will($promise);
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
*/
public function providerTestBuild() {
$data = [];
- $data['array, no preview'] = [
+ $data['array'] = [
new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
- FALSE,
- 'The field value',
- ];
- $data['array, preview'] = [
- new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
- TRUE,
'The field value',
];
- $data['empty array, no preview'] = [
+ $data['empty array'] = [
new ReturnPromise([[]]),
- FALSE,
'',
];
- $data['empty array, preview'] = [
- new ReturnPromise([[]]),
- TRUE,
- 'Placeholder for the "The Field Label" field',
- ];
- $data['exception, no preview'] = [
+ $data['exception'] = [
new ThrowPromise(new \Exception('The exception message')),
- FALSE,
'',
'The field "%field" failed to render with the error of "%error".',
['%field' => 'the_field_name', '%error' => 'The exception message'],
];
- $data['exception, preview'] = [
- new ThrowPromise(new \Exception('The exception message')),
- TRUE,
- 'Placeholder for the "The Field Label" field',
- 'The field "%field" failed to render with the error of "%error".',
- ['%field' => 'the_field_name', '%error' => 'The exception message'],
- ];
return $data;
}
namespace Drupal\Tests\layout_builder\Kernel;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
/**
// Add a layout override.
$this->enableOverrides();
/** @var \Drupal\layout_builder\SectionStorageInterface $field_list */
- $field_list = $this->entity->get('layout_builder__layout');
+ $field_list = $this->entity->get(OverridesSectionStorage::FIELD_NAME);
$field_list->appendSection(new Section('layout_onecol'));
$this->entity->save();
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
/**
// Add a layout override.
$this->enableOverrides();
$this->entity = $this->reloadEntity($this->entity);
- $this->entity->get('layout_builder__layout')->appendSection(new Section('layout_onecol'));
+ $this->entity->get(OverridesSectionStorage::FIELD_NAME)->appendSection(new Section('layout_onecol'));
$this->entity->save();
// The rendered entity has now changed. The non-configurable field is shown
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
// Removing the layout restores the original rendering of the entity.
- $this->entity->get('layout_builder__layout')->removeSection(0);
+ $this->entity->get(OverridesSectionStorage::FIELD_NAME)->removeSection(0);
$this->entity->save();
$this->assertFieldAttributes($this->entity, $expected_fields);
use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Tests the field type for Layout Sections.
}, $section_data);
$entity = EntityTestBaseFieldDisplay::create([
'name' => 'The test entity',
- 'layout_builder__layout' => $section_data,
+ OverridesSectionStorage::FIELD_NAME => $section_data,
]);
$entity->save();
- return $entity->get('layout_builder__layout');
+ return $entity->get(OverridesSectionStorage::FIELD_NAME);
}
}
$this->assertSections($expected);
}
+ /**
+ * Tests __clone().
+ */
+ public function testClone() {
+ $this->assertSame([], $this->sectionStorage->getSection(0)->getLayoutSettings());
+
+ $new_section_storage = clone $this->sectionStorage;
+ $new_section_storage->getSection(0)->setLayoutSettings(['asdf' => 'qwer']);
+ $this->assertSame([], $this->sectionStorage->getSection(0)->getLayoutSettings());
+ }
+
/**
* Asserts that the field list has the expected sections.
*
/**
* @covers ::get
+ * @covers ::has
*/
public function testGetEmptyTempstore() {
$section_storage = $this->prophesize(SectionStorageInterface::class);
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
+ $this->assertFalse($repository->has($section_storage->reveal()));
+
$result = $repository->get($section_storage->reveal());
$this->assertSame($section_storage->reveal(), $result);
}
/**
* @covers ::get
+ * @covers ::has
*/
public function testGetLoadedTempstore() {
$section_storage = $this->prophesize(SectionStorageInterface::class);
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
+ $this->assertTrue($repository->has($section_storage->reveal()));
+
$result = $repository->get($section_storage->reveal());
$this->assertSame($tempstore_section_storage->reveal(), $result);
$this->assertNotSame($section_storage->reveal(), $result);
$entity_storage = $this->prophesize(EntityStorageInterface::class);
$entity_without_layout = $this->prophesize(FieldableEntityInterface::class);
- $entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE);
- $entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled();
+ $entity_without_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(FALSE);
+ $entity_without_layout->get(OverridesSectionStorage::FIELD_NAME)->shouldNotBeCalled();
$entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal());
$entity_with_layout = $this->prophesize(FieldableEntityInterface::class);
- $entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE);
- $entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value');
+ $entity_with_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(TRUE);
+ $entity_with_layout->get(OverridesSectionStorage::FIELD_NAME)->willReturn('the_return_value');
$entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal());
$this->entityTypeManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal());
// Set the thumbnail alt.
$media_source = $this->getSource();
$plugin_definition = $media_source->getPluginDefinition();
+
+ $this->thumbnail->alt = '';
if (!empty($plugin_definition['thumbnail_alt_metadata_attribute'])) {
$this->thumbnail->alt = $media_source->getMetadata($this, $plugin_definition['thumbnail_alt_metadata_attribute']);
}
- else {
- $this->thumbnail->alt = $this->t('Thumbnail', [], ['langcode' => $this->langcode->value]);
- }
-
- // Set the thumbnail title.
- if (!empty($plugin_definition['thumbnail_title_metadata_attribute'])) {
- $this->thumbnail->title = $media_source->getMetadata($this, $plugin_definition['thumbnail_title_metadata_attribute']);
- }
- else {
- $this->thumbnail->title = $this->label();
- }
return $this;
}
use Drupal\Core\Cache\UseCacheBackendTrait;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
-use Symfony\Component\Serializer\Encoder\XmlEncoder;
/**
* Fetches and caches oEmbed resources.
$content = (string) $response->getBody();
if (strstr($format, 'text/xml') || strstr($format, 'application/xml')) {
- $encoder = new XmlEncoder();
- $data = $encoder->decode($content, 'xml');
+ $data = $this->parseResourceXml($content, $url);
}
elseif (strstr($format, 'text/javascript') || strstr($format, 'application/json')) {
$data = Json::decode($content);
}
}
+ /**
+ * Parses XML resource data.
+ *
+ * @param string $data
+ * The raw XML for the resource.
+ * @param string $url
+ * The resource URL.
+ *
+ * @return array
+ * The parsed resource data.
+ *
+ * @throws \Drupal\media\OEmbed\ResourceException
+ * If the resource data could not be parsed.
+ */
+ protected function parseResourceXml($data, $url) {
+ // Enable userspace error handling.
+ $was_using_internal_errors = libxml_use_internal_errors(TRUE);
+ libxml_clear_errors();
+
+ $content = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
+ // Restore the previous error handling behavior.
+ libxml_use_internal_errors($was_using_internal_errors);
+
+ $error = libxml_get_last_error();
+ if ($error) {
+ libxml_clear_errors();
+ throw new ResourceException($error->message, $url);
+ }
+ elseif ($content === FALSE) {
+ throw new ResourceException('The fetched resource could not be parsed.', $url);
+ }
+
+ // Convert XML to JSON so that the parsed resource has a consistent array
+ // structure, regardless of any XML attributes or quirks of the XML parser.
+ $data = Json::encode($content);
+ return Json::decode($data);
+ }
+
}
* label = @Translation("Image"),
* description = @Translation("Use local images for reusable media."),
* allowed_field_types = {"image"},
- * default_thumbnail_filename = "no-thumbnail.png"
+ * default_thumbnail_filename = "no-thumbnail.png",
+ * thumbnail_alt_metadata_attribute = "thumbnail_alt_value"
* )
*/
class Image extends File {
case 'thumbnail_uri':
return $uri;
+
+ case 'thumbnail_alt_value':
+ return $media->get($this->configuration['source_field'])->alt ?: parent::getMetadata($media, $name);
}
return parent::getMetadata($media, $name);
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<oembed>
<type>video</type>
- <version>1.0</version>
+ <version type="float">1.0</version>
<title>Let's Not Get a Drink Sometime</title>
<https/>
<author_name>CollegeHumor</author_name>
],
'thumbnail' => [
[
- 'alt' => 'Thumbnail',
+ 'alt' => '',
'width' => 180,
'height' => 180,
'target_id' => (int) $thumbnail->id(),
'target_type' => 'file',
'target_uuid' => $thumbnail->uuid(),
- 'title' => 'Llama',
+ 'title' => NULL,
'url' => $thumbnail->url(),
],
],
// Make sure the thumbnail is displayed from uploaded image.
$assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'example_1.jpeg');
+ // Ensure the thumbnail has the correct alt attribute.
+ $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'alt', 'Image Alt Text 1');
// Load the media and check that all fields are properly populated.
$media = Media::load(1);
'value' => 'Snowball',
],
'thumbnail_uri' => [
- 'title' => 'Thumbnail',
'value' => 'public://TheSisko.png',
],
]);
// Save a media item and make sure thumbnail was added.
\Drupal::state()->set('media_source_test_attributes', [
- 'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'],
+ 'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
]);
/** @var \Drupal\media\MediaInterface $media */
$media = Media::create([
$this->assertSame('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not added to the media item.');
- $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
- $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+ // We expect the title not to be present on the Thumbnail.
+ $this->assertEmpty($media->thumbnail->title);
+ $this->assertSame('', $media->thumbnail->alt);
// Now change the metadata attribute and make sure that the thumbnail stays
// the same.
\Drupal::state()->set('media_source_test_attributes', [
- 'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail2.jpg'],
+ 'thumbnail_uri' => ['value' => 'public://thumbnail2.jpg'],
]);
$this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not preserved.');
- $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
- $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+ $this->assertEmpty($media->thumbnail->title);
+ $this->assertSame('', $media->thumbnail->alt);
// Remove the thumbnail and make sure that it is auto-updated on save.
$media->thumbnail->target_id = NULL;
$this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media item.');
- $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
- $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+ $this->assertEmpty($media->thumbnail->title);
+ $this->assertSame('', $media->thumbnail->alt);
// Change the metadata attribute again, change the source field value too
// and make sure that the thumbnail updates.
\Drupal::state()->set('media_source_test_attributes', [
- 'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'],
+ 'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
]);
$media->field_media_test->value = 'some_new_value';
$this->assertSame('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media item.');
- $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
- $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+ $this->assertEmpty($media->thumbnail->title);
+ $this->assertSame('', $media->thumbnail->alt);
// Change the thumbnail metadata attribute and make sure that the thumbnail
// is set correctly.
\Drupal::state()->set('media_source_test_attributes', [
- 'thumbnail_uri' => ['title' => 'Should not be used', 'value' => 'public://thumbnail1.jpg'],
- 'alternative_thumbnail_uri' => ['title' => 'Should be used', 'value' => 'public://thumbnail2.jpg'],
+ 'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
+ 'alternative_thumbnail_uri' => ['value' => 'public://thumbnail2.jpg'],
]);
\Drupal::state()->set('media_source_test_definition', ['thumbnail_uri_metadata_attribute' => 'alternative_thumbnail_uri']);
$media = Media::create([
$this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'alternative_thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'Correct metadata attribute was not used for the thumbnail.');
- $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
- $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+ $this->assertEmpty($media->thumbnail->title);
+ $this->assertSame('', $media->thumbnail->alt);
// Enable queued thumbnails and make sure that the entity gets the default
// thumbnail initially.
\Drupal::state()->set('media_source_test_definition', []);
\Drupal::state()->set('media_source_test_attributes', [
- 'thumbnail_uri' => ['title' => 'Should not be used', 'value' => 'public://thumbnail1.jpg'],
+ 'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
]);
$this->testMediaType->setQueueThumbnailDownloadsStatus(TRUE)->save();
$media = Media::create([
$this->assertSame('public://thumbnail1.jpg', $media->getSource()->getMetadata($media, 'thumbnail_uri'), 'Value of the metadata attribute is not correct.');
$media->save();
$this->assertSame('public://media-icons/generic/generic.png', $media->thumbnail->entity->getFileUri(), 'Default thumbnail was not set initially.');
- $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
- $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+ $this->assertEmpty($media->thumbnail->title);
+ $this->assertSame('', $media->thumbnail->alt);
// Process the queue item and make sure that the thumbnail was updated too.
$queue_name = 'media_entity_thumbnail';
$media = Media::load($media->id());
$this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not updated by the queue.');
- $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
- $this->assertSame('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+ $this->assertEmpty($media->thumbnail->title);
+ $this->assertSame('', $media->thumbnail->alt);
- // Set alt and title metadata attributes and make sure they are used for the
- // thumbnail.
+ // Set the alt metadata attribute and make sure it's used for the thumbnail.
\Drupal::state()->set('media_source_test_definition', [
'thumbnail_alt_metadata_attribute' => 'alt',
- 'thumbnail_title_metadata_attribute' => 'title',
]);
\Drupal::state()->set('media_source_test_attributes', [
- 'alt' => ['title' => 'Alt text', 'value' => 'This will be alt.'],
- 'title' => ['title' => 'Title text', 'value' => 'This will be title.'],
+ 'alt' => ['value' => 'This will be alt.'],
]);
$media = Media::create([
'bundle' => $this->testMediaType->id(),
]);
$media->save();
$this->assertSame('Boxer', $media->getName(), 'Correct name was not set on the media item.');
- $this->assertSame('This will be title.', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
- $this->assertSame('This will be alt.', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+ $this->assertEmpty($media->thumbnail->title);
+ $this->assertSame('This will be alt.', $media->thumbnail->alt);
}
/**
* The menu link ID.
*/
protected function getUuid() {
- $this->getDerivativeId();
+ return $this->getDerivativeId();
}
/**
--- /dev/null
+<?php
+
+namespace Drupal\Tests\menu_link_content\Unit;
+
+use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent
+ *
+ * @group Menu
+ */
+class MenuLinkPluginTest extends UnitTestCase {
+
+ /**
+ * @covers ::getUuid
+ */
+ public function testGetInstanceReflection() {
+ /** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $menu_link_content_plugin */
+ $menu_link_content_plugin = $this->prophesize(MenuLinkContent::class);
+ $menu_link_content_plugin->getDerivativeId()->willReturn('test_id');
+ $menu_link_content_plugin = $menu_link_content_plugin->reveal();
+
+ $class = new \ReflectionClass(MenuLinkContent::class);
+ $instance_method = $class->getMethod('getUuid');
+ $instance_method->setAccessible(TRUE);
+
+ $this->assertEquals('test_id', $instance_method->invoke($menu_link_content_plugin));
+ }
+
+}
$migration->set('requirements', $required_dependency_graph[$migration_id]['paths']);
}
}
- array_multisort($weights, SORT_DESC, SORT_NUMERIC, $migrations);
+ // Sort weights, labels, and keys in the same order as each other.
+ array_multisort(
+ // Use the numerical weight as the primary sort.
+ $weights, SORT_DESC, SORT_NUMERIC,
+ // When migrations have the same weight, sort them alphabetically by ID.
+ array_keys($migrations), SORT_ASC, SORT_NATURAL,
+ $migrations
+ );
return $migrations;
}
* @endcode
*
* If the source value was '2004-12-19T10:19:42-0600' the transformed value
- * would be 2004-12-19T10:19:42.
+ * would be 2004-12-19T10:19:42. Set validate_format to false if your source
+ * value is '0000-00-00 00:00:00'.
*
* @see \DateTime::createFromFormat()
* @see \Drupal\Component\Datetime\DateTimePlus::__construct()
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
- if (empty($value)) {
+ if (empty($value) && $value !== '0' && $value !== 0) {
return '';
}
$row_data = $this->getIterator()->current() + $this->configuration;
$this->fetchNextRow();
- $row = new Row($row_data, $this->migration->getSourcePlugin()->getIds(), $this->migration->getDestinationIds());
+ $row = new Row($row_data, $this->getIds());
// Populate the source key for this row.
$this->currentSourceIds = $row->getSourceIdValues();
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->state = $state;
+ // If we are using high water, but haven't yet set a high water mark, skip
+ // joining the map table, as we want to get all available records.
+ if ($this->getHighWaterProperty() && $this->getHighWater() === NULL) {
+ $this->configuration['ignore_map'] = TRUE;
+ }
}
/**
if ($this->getHighWaterProperty()) {
$high_water_field = $this->getHighWaterField();
$high_water = $this->getHighWater();
- if ($high_water) {
+ // We check against NULL because 0 is an acceptable value for the high
+ // water mark.
+ if ($high_water !== NULL) {
$conditions->condition($high_water_field, $high_water, '>');
$condition_added = TRUE;
}
$this->assertNodeDoesNotExist('Item 3');
}
+ /**
+ * Tests that the high water value can be 0.
+ */
+ public function testZeroHighwater() {
+ // Assert all of the nodes have been imported.
+ $this->assertNodeExists('Item 1');
+ $this->assertNodeExists('Item 2');
+ $this->assertNodeExists('Item 3');
+ $migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
+ $source = $migration->getSourcePlugin();
+ $source->rewind();
+ $count = 0;
+ while ($source->valid()) {
+ $count++;
+ $source->next();
+ }
+
+ // Expect no rows as everything is below the high water mark.
+ $this->assertSame(0, $count);
+
+ // Test resetting the high water mark to 0.
+ $this->container->get('keyvalue')->get('migrate:high_water')->set('high_water_test', 0);
+ $migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
+ $source = $migration->getSourcePlugin();
+ $source->rewind();
+ $count = 0;
+ while ($source->valid()) {
+ $count++;
+ $source->next();
+ }
+ $this->assertSame(3, $count);
+ }
+
+ /**
+ * Tests that deleting the high water value causes all rows to be reimported.
+ */
+ public function testNullHighwater() {
+ // Assert all of the nodes have been imported.
+ $this->assertNodeExists('Item 1');
+ $this->assertNodeExists('Item 2');
+ $this->assertNodeExists('Item 3');
+ $migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
+ $source = $migration->getSourcePlugin();
+ $source->rewind();
+ $count = 0;
+ while ($source->valid()) {
+ $count++;
+ $source->next();
+ }
+
+ // Expect no rows as everything is below the high water mark.
+ $this->assertSame(0, $count);
+
+ // Test resetting the high water mark.
+ $this->container->get('keyvalue')->get('migrate:high_water')->delete('high_water_test');
+ $migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
+ $source = $migration->getSourcePlugin();
+ $source->rewind();
+ $count = 0;
+ while ($source->valid()) {
+ $count++;
+ $source->next();
+ }
+ $this->assertSame(3, $count);
+ }
+
/**
* Tests high water property of SqlBase when rows marked for update.
*/
$results = [];
/** @var \Drupal\migrate\Row $row */
foreach ($source as $row) {
+ // The plugin should not mark any rows as stubs. We need to use
+ // assertSame() here because assertFalse() will pass falsy values (e.g.,
+ // empty arrays).
+ $this->assertSame(FALSE, $row->isStub());
+
$data_row = $row->getSource();
// The "data" row returned by getSource() also includes all source
// configuration - we remove it so we see only the data itself.
// converted from Australia/Sydney to America/Managua timezone.
'expected' => '2004-12-18 17:19:42 America/Managua',
],
+ 'integer_0' => [
+ 'configuration' => [
+ 'from_format' => 'U',
+ 'to_format' => 'Y-m-d',
+ ],
+ 'value' => 0,
+ 'expected' => '1970-01-01',
+ ],
+ 'string_0' => [
+ 'configuration' => [
+ 'from_format' => 'U',
+ 'to_format' => 'Y-m-d',
+ ],
+ 'value' => '0',
+ 'expected' => '1970-01-01',
+ ],
+ 'zeros' => [
+ 'configuration' => [
+ 'from_format' => 'Y-m-d H:i:s',
+ 'to_format' => 'Y-m-d H:i:s e',
+ 'settings' => ['validate_format' => FALSE],
+ ],
+ 'value' => '0000-00-00 00:00:00',
+ 'expected' => '-0001-11-30 00:00:00 Australia/Sydney',
+ ],
+ 'zeros_same_timezone' => [
+ 'configuration' => [
+ 'from_format' => 'Y-m-d H:i:s',
+ 'to_format' => 'Y-m-d H:i:s',
+ 'settings' => ['validate_format' => FALSE],
+ 'from_timezone' => 'UTC',
+ 'to_timezone' => 'UTC',
+ ],
+ 'value' => '0000-00-00 00:00:00',
+ 'expected' => '-0001-11-30 00:00:00',
+ ],
];
}
* {@inheritdoc}
*/
public function checkRequirements() {
+ parent::checkRequirements();
if ($this->pluginDefinition['requirements_met'] === TRUE) {
if (isset($this->pluginDefinition['source_module'])) {
if ($this->moduleExists($this->pluginDefinition['source_module'])) {
}
}
}
- parent::checkRequirements();
}
/**
'objectindex' => '0',
'format' => '0',
))
+->values(array(
+ 'lid' => '1692',
+ 'objectid' => '14',
+ 'type' => 'term',
+ 'property' => 'name',
+ 'objectindex' => '14',
+ 'format' => '0',
+))
+->values(array(
+ 'lid' => '1693',
+ 'objectid' => '15',
+ 'type' => 'term',
+ 'property' => 'name',
+ 'objectindex' => '15',
+ 'format' => '0',
+))
+->values(array(
+ 'lid' => '1694',
+ 'objectid' => '14',
+ 'type' => 'term',
+ 'property' => 'description',
+ 'objectindex' => '14',
+ 'format' => '0',
+))
->execute();
$connection->schema()->createTable('i18n_variable', array(
'source' => 'White',
'version' => '1',
))
+->values(array(
+ 'lid' => '1692',
+ 'location' => 'term:14:name',
+ 'textgroup' => 'taxonomy',
+ 'source' => 'Talos IV',
+ 'version' => '1',
+))
+->values(array(
+ 'lid' => '1693',
+ 'location' => 'term:15:name',
+ 'textgroup' => 'taxonomy',
+ 'source' => 'Vulcan',
+ 'version' => '1',
+))
+->values(array(
+ 'lid' => '1694',
+ 'location' => 'term:14:description',
+ 'textgroup' => 'taxonomy',
+ 'source' => 'The home of Captain Christopher Pike.',
+ 'version' => '1',
+))
->execute();
$connection->schema()->createTable('locales_target', array(
'plural' => '0',
'i18n_status' => '0',
))
+->values(array(
+ 'lid' => '1672',
+ 'translation' => 'fr - Type',
+ 'language' => 'fr',
+ 'plid' => '0',
+ 'plural' => '0',
+ 'i18n_status' => '0',
+))
+->values(array(
+ 'lid' => '1692',
+ 'translation' => 'fr - Talos IV',
+ 'language' => 'fr',
+ 'plid' => '0',
+ 'plural' => '0',
+ 'i18n_status' => '0',
+))
+->values(array(
+ 'lid' => '1694',
+ 'translation' => 'fr - The home of Captain Christopher Pike.',
+ 'language' => 'fr',
+ 'plid' => '0',
+ 'plural' => '0',
+ 'i18n_status' => '0',
+))
->values(array(
'lid' => '66',
'translation' => 'zu - CCK - Aucune Intégration aux Vues',
'plural' => '0',
'i18n_status' => '0',
))
+->values(array(
+ 'lid' => '1672',
+ 'translation' => 'zu - Type',
+ 'language' => 'zu',
+ 'plid' => '0',
+ 'plural' => '0',
+ 'i18n_status' => '0',
+))
+->values(array(
+ 'lid' => '1693',
+ 'translation' => 'zu - Vulcan',
+ 'language' => 'zu',
+ 'plid' => '0',
+ 'plural' => '0',
+ 'i18n_status' => '0',
+))
+->values(array(
+ 'lid' => '1694',
+ 'translation' => 'zu - The home of Captain Christopher Pike.',
+ 'language' => 'zu',
+ 'plid' => '0',
+ 'plural' => '0',
+ 'i18n_status' => '0',
+))
->execute();
$connection->schema()->createTable('menu_custom', array(
'weight' => '0',
'file' => 'sites/all/modules/i18n/i18n.admin.inc',
))
+->values(array(
+ 'path' => 'admin/settings/language/i18n/variables',
+ 'load_functions' => '',
+ 'to_arg_functions' => '',
+ 'access_callback' => 'user_access',
+ 'access_arguments' => 'a:1:{i:0;s:29:"administer site configuration";}',
+ 'page_callback' => 'drupal_get_form',
+ 'page_arguments' => 'a:1:{i:0;s:25:"i18n_admin_variables_form";}',
+ 'fit' => '31',
+ 'number_parts' => '5',
+ 'tab_parent' => 'admin/settings/language/i18n',
+ 'tab_root' => 'admin/settings/language',
+ 'title' => 'Variables',
+ 'title_callback' => 't',
+ 'title_arguments' => '',
+ 'type' => '128',
+ 'block_callback' => '',
+ 'description' => 'Multilingual variables.',
+ 'position' => '',
+ 'weight' => '0',
+ 'file' => 'sites/all/modules/i18n/i18n.admin.inc',
+))
->values(array(
'path' => 'admin/settings/language/overview',
'load_functions' => '',
'language' => '',
'trid' => '0',
))
+->values(array(
+ 'tid' => '9',
+ 'vid' => '3',
+ 'name' => 'fr - term 4 of vocabulary 3',
+ 'description' => '',
+ 'weight' => '0',
+ 'language' => 'fr',
+ 'trid' => '1',
+))
+->values(array(
+ 'tid' => '10',
+ 'vid' => '3',
+ 'name' => 'zu - term 4 of vocabulary 3',
+ 'description' => '',
+ 'weight' => '0',
+ 'language' => 'zu',
+ 'trid' => '1',
+))
+->values(array(
+ 'tid' => '11',
+ 'vid' => '3',
+ 'name' => 'term 7 of vocabulary 3',
+ 'description' => '',
+ 'weight' => '0',
+ 'language' => 'en',
+ 'trid' => '2',
+))
+->values(array(
+ 'tid' => '12',
+ 'vid' => '3',
+ 'name' => 'fr - term 7 of vocabulary 3',
+ 'description' => '',
+ 'weight' => '0',
+ 'language' => 'fr',
+ 'trid' => '2',
+))
+->values(array(
+ 'tid' => '13',
+ 'vid' => '3',
+ 'name' => 'zu - term 7 of vocabulary 3',
+ 'description' => '',
+ 'weight' => '0',
+ 'language' => 'zu',
+ 'trid' => '2',
+))
+->values(array(
+ 'tid' => '14',
+ 'vid' => '5',
+ 'name' => 'Talos IV',
+ 'description' => 'The home of Captain Christopher Pike.',
+ 'weight' => '0',
+ 'language' => '',
+ 'trid' => '0',
+))
+->values(array(
+ 'tid' => '15',
+ 'vid' => '5',
+ 'name' => 'Vulcan',
+ 'description' => '',
+ 'weight' => '0',
+ 'language' => '',
+ 'trid' => '0',
+))
->execute();
$connection->schema()->createTable('term_hierarchy', array(
'tid' => '8',
'parent' => '0',
))
+->values(array(
+ 'tid' => '9',
+ 'parent' => '0',
+))
+->values(array(
+ 'tid' => '10',
+ 'parent' => '0',
+))
+->values(array(
+ 'tid' => '11',
+ 'parent' => '0',
+))
+->values(array(
+ 'tid' => '12',
+ 'parent' => '0',
+))
+->values(array(
+ 'tid' => '13',
+ 'parent' => '0',
+))
+->values(array(
+ 'tid' => '14',
+ 'parent' => '0',
+))
+->values(array(
+ 'tid' => '15',
+ 'parent' => '0',
+))
->values(array(
'tid' => '3',
'parent' => '2',
))
->values(array(
'name' => 'i18ntaxonomy_vocabulary',
- 'value' => 'a:3:{i:1;s:1:"3";i:2;s:1:"2";i:3;s:1:"1";}',
+ 'value' => 'a:4:{i:1;s:1:"3";i:2;s:1:"2";i:3;s:1:"3";i:5;s:1:"1";}',
))
->values(array(
'name' => 'i18n_lock_node_article',
$connection = Database::getConnection();
-$connection->schema()->createTable('i18n_block_language', array(
- 'fields' => array(
- 'module' => array(
- 'type' => 'varchar',
- 'not null' => TRUE,
- 'length' => '64',
- ),
- 'delta' => array(
- 'type' => 'varchar',
- 'not null' => TRUE,
- 'length' => '32',
- ),
- 'language' => array(
- 'type' => 'varchar',
- 'not null' => TRUE,
- 'length' => '12',
- 'default' => '',
- ),
- ),
- 'primary key' => array(
- 'module',
- 'delta',
- 'language',
- ),
- 'indexes' => array(
- 'language' => array(
- 'language',
- ),
- ),
- 'mysql_character_set' => 'utf8',
-));
-
-$connection->schema()->createTable('i18n_string', array(
- 'fields' => array(
- 'lid' => array(
- 'type' => 'int',
- 'not null' => TRUE,
- 'size' => 'normal',
- 'default' => '0',
- ),
- 'textgroup' => array(
- 'type' => 'varchar',
- 'not null' => TRUE,
- 'length' => '50',
- 'default' => 'default',
- ),
- 'context' => array(
- 'type' => 'varchar',
- 'not null' => TRUE,
- 'length' => '255',
- 'default' => '',
- ),
- 'objectid' => array(
- 'type' => 'varchar',
- 'not null' => TRUE,
- 'length' => '255',
- 'default' => '',
- ),
- 'type' => array(
- 'type' => 'varchar',
- 'not null' => TRUE,
- 'length' => '255',
- 'default' => '',
- ),
- 'property' => array(
- 'type' => 'varchar',
- 'not null' => TRUE,
- 'length' => '255',
- 'default' => '',
- ),
- 'objectindex' => array(
- 'type' => 'int',
- 'not null' => TRUE,
- 'size' => 'big',
- 'default' => '0',
- ),
- 'format' => array(
- 'type' => 'varchar',
- 'not null' => FALSE,
- 'length' => '255',
- ),
- ),
- 'primary key' => array(
- 'lid',
- ),
- 'indexes' => array(
- 'group_context' => array(
- 'textgroup',
- array(
- 'context',
- '50',
- ),
- ),
- ),
- 'mysql_character_set' => 'utf8',
-));
-
-$connection->insert('i18n_string')
-->fields(array(
- 'lid',
- 'textgroup',
- 'context',
- 'objectid',
- 'type',
- 'property',
- 'objectindex',
- 'format',
-))
-->values(array(
- 'lid' => '57',
- 'textgroup' => 'blocks',
- 'context' => 'block:1:title',
- 'objectid' => '1',
- 'type' => 'block',
- 'property' => 'title',
- 'objectindex' => '1',
- 'format' => '',
-))
-->values(array(
- 'lid' => '60',
- 'textgroup' => 'blocks',
- 'context' => 'block:1:body',
- 'objectid' => '1',
- 'type' => 'block',
- 'property' => 'body',
- 'objectindex' => '1',
- 'format' => 'filtered_html',
-))
-->values(array(
- 'lid' => '61',
- 'textgroup' => 'node',
- 'context' => 'type:article:name',
- 'objectid' => 'article',
- 'type' => 'type',
- 'property' => 'name',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '62',
- 'textgroup' => 'node',
- 'context' => 'type:article:title_label',
- 'objectid' => 'article',
- 'type' => 'type',
- 'property' => 'title_label',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '63',
- 'textgroup' => 'node',
- 'context' => 'type:article:description',
- 'objectid' => 'article',
- 'type' => 'type',
- 'property' => 'description',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '64',
- 'textgroup' => 'node',
- 'context' => 'type:article:help',
- 'objectid' => 'article',
- 'type' => 'type',
- 'property' => 'help',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '65',
- 'textgroup' => 'node',
- 'context' => 'type:book:name',
- 'objectid' => 'book',
- 'type' => 'type',
- 'property' => 'name',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '66',
- 'textgroup' => 'node',
- 'context' => 'type:book:title_label',
- 'objectid' => 'book',
- 'type' => 'type',
- 'property' => 'title_label',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '67',
- 'textgroup' => 'node',
- 'context' => 'type:book:description',
- 'objectid' => 'book',
- 'type' => 'type',
- 'property' => 'description',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '68',
- 'textgroup' => 'node',
- 'context' => 'type:page:name',
- 'objectid' => 'page',
- 'type' => 'type',
- 'property' => 'name',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '69',
- 'textgroup' => 'node',
- 'context' => 'type:page:title_label',
- 'objectid' => 'page',
- 'type' => 'type',
- 'property' => 'title_label',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '70',
- 'textgroup' => 'node',
- 'context' => 'type:page:description',
- 'objectid' => 'page',
- 'type' => 'type',
- 'property' => 'description',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '71',
- 'textgroup' => 'node',
- 'context' => 'type:page:help',
- 'objectid' => 'page',
- 'type' => 'type',
- 'property' => 'help',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '72',
- 'textgroup' => 'node',
- 'context' => 'type:test_content_type:name',
- 'objectid' => 'test_content_type',
- 'type' => 'type',
- 'property' => 'name',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '73',
- 'textgroup' => 'node',
- 'context' => 'type:test_content_type:title_label',
- 'objectid' => 'test_content_type',
- 'type' => 'type',
- 'property' => 'title_label',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '74',
- 'textgroup' => 'node',
- 'context' => 'type:test_content_type:description',
- 'objectid' => 'test_content_type',
- 'type' => 'type',
- 'property' => 'description',
- 'objectindex' => '0',
- 'format' => '',
-))
-->values(array(
- 'lid' => '75',
- 'textgroup' => 'node',
- 'context' => 'type:test_content_type:help',
- 'objectid' => 'test_content_type',
- 'type' => 'type',
- 'property' => 'help',
- 'objectindex' => '0',
- 'format' => '',
-))
-->execute();
-
$connection->schema()->createTable('accesslog', array(
'fields' => array(
'aid' => array(
'size' => 'normal',
'default' => '1',
),
+ 'i18n_mode' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'size' => 'normal',
+ 'default' => '0',
+ ),
),
'primary key' => array(
'bid',
'pages',
'title',
'cache',
+ 'i18n_mode',
))
->values(array(
'bid' => '1',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '2',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '3',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '4',
'custom' => '0',
'visibility' => '0',
'pages' => '',
- 'title' => '',
+ 'title' => 'User login title',
'cache' => '-1',
+ 'i18n_mode' => '1',
))
->values(array(
'bid' => '5',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '6',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '7',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '8',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '9',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '10',
'custom' => '0',
'visibility' => '0',
'pages' => '',
- 'title' => '',
+ 'title' => 'User login title',
'cache' => '-1',
+ 'i18n_mode' => '1',
))
->values(array(
'bid' => '11',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '12',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '13',
'pages' => '',
'title' => '',
'cache' => '1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '14',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '15',
'pages' => '',
'title' => '',
'cache' => '1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '16',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '17',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '18',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '19',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '20',
'pages' => '',
'title' => '',
'cache' => '1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '21',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '22',
'pages' => '',
'title' => '',
'cache' => '1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '23',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '24',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '25',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '26',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '27',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '28',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '29',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '30',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '31',
'pages' => '',
'title' => '',
'cache' => '1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '32',
'pages' => '',
'title' => '',
'cache' => '5',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '33',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '34',
'pages' => '',
'title' => '',
'cache' => '-2',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '35',
'pages' => '',
'title' => '',
'cache' => '-2',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '36',
'pages' => '',
'title' => '',
'cache' => '1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '37',
'pages' => '',
'title' => '',
'cache' => '5',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '38',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '39',
'pages' => '',
'title' => '',
'cache' => '-2',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '40',
'pages' => '',
'title' => '',
'cache' => '-2',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '41',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '42',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '43',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '44',
'pages' => '',
'title' => '',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '45',
'pages' => '',
'title' => 'Mildly amusing limerick of the day',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '46',
'pages' => '',
'title' => 'Mildly amusing limerick of the day',
'cache' => '-1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '47',
'pages' => '',
'title' => '',
'cache' => '1',
+ 'i18n_mode' => '0',
))
->values(array(
'bid' => '48',
'pages' => '',
'title' => '',
'cache' => '1',
+ 'i18n_mode' => '0',
+))
+->values(array(
+ 'bid' => '49',
+ 'module' => 'locale',
+ 'delta' => 'language_content',
+ 'theme' => 'bartik',
+ 'status' => '0',
+ 'weight' => '0',
+ 'region' => '-1',
+ 'custom' => '0',
+ 'visibility' => '0',
+ 'pages' => '',
+ 'title' => '',
+ 'cache' => '-1',
+ 'i18n_mode' => '0',
+))
+->values(array(
+ 'bid' => '50',
+ 'module' => 'locale',
+ 'delta' => 'language_content',
+ 'theme' => 'seven',
+ 'status' => '0',
+ 'weight' => '0',
+ 'region' => '-1',
+ 'custom' => '0',
+ 'visibility' => '0',
+ 'pages' => '',
+ 'title' => '',
+ 'cache' => '-1',
+ 'i18n_mode' => '0',
))
->execute();
'mysql_character_set' => 'utf8',
));
+$connection->schema()->createTable('cache_variable', array(
+ 'fields' => array(
+ 'cid' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '255',
+ 'default' => '',
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ ),
+ 'expire' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'size' => 'normal',
+ 'default' => '0',
+ ),
+ 'created' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'size' => 'normal',
+ 'default' => '0',
+ ),
+ 'serialized' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'size' => 'small',
+ 'default' => '0',
+ ),
+ ),
+ 'primary key' => array(
+ 'cid',
+ ),
+ 'indexes' => array(
+ 'expire' => array(
+ 'expire',
+ ),
+ ),
+ 'mysql_character_set' => 'utf8',
+));
+
$connection->schema()->createTable('cache_views', array(
'fields' => array(
'cid' => array(
'mysql_character_set' => 'utf8',
));
+$connection->schema()->createTable('i18n_block_language', array(
+ 'fields' => array(
+ 'module' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '64',
+ ),
+ 'delta' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '32',
+ ),
+ 'language' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '12',
+ 'default' => '',
+ ),
+ ),
+ 'primary key' => array(
+ 'module',
+ 'delta',
+ 'language',
+ ),
+ 'indexes' => array(
+ 'language' => array(
+ 'language',
+ ),
+ ),
+ 'mysql_character_set' => 'utf8',
+));
+
+$connection->schema()->createTable('i18n_string', array(
+ 'fields' => array(
+ 'lid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'size' => 'normal',
+ 'default' => '0',
+ ),
+ 'textgroup' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '50',
+ 'default' => 'default',
+ ),
+ 'context' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '255',
+ 'default' => '',
+ ),
+ 'objectid' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '255',
+ 'default' => '',
+ ),
+ 'type' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '255',
+ 'default' => '',
+ ),
+ 'property' => array(
+ 'type' => 'varchar',
+ 'not null' => TRUE,
+ 'length' => '255',
+ 'default' => '',
+ ),
+ 'objectindex' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'default' => '0',
+ ),
+ 'format' => array(
+ 'type' => 'varchar',
+ 'not null' => FALSE,
+ 'length' => '255',
+ ),
+ ),
+ 'primary key' => array(
+ 'lid',
+ ),
+ 'indexes' => array(
+ 'group_context' => array(
+ 'textgroup',
+ array(
+ 'context',
+ '50',
+ ),
+ ),
+ ),
+ 'mysql_character_set' => 'utf8',
+));
+
+$connection->insert('i18n_string')
+->fields(array(
+ 'lid',
+ 'textgroup',
+ 'context',
+ 'objectid',
+ 'type',
+ 'property',
+ 'objectindex',
+ 'format',
+))
+->values(array(
+ 'lid' => '57',
+ 'textgroup' => 'blocks',
+ 'context' => 'block:1:title',
+ 'objectid' => '1',
+ 'type' => 'block',
+ 'property' => 'title',
+ 'objectindex' => '1',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '60',
+ 'textgroup' => 'blocks',
+ 'context' => 'block:1:body',
+ 'objectid' => '1',
+ 'type' => 'block',
+ 'property' => 'body',
+ 'objectindex' => '1',
+ 'format' => 'filtered_html',
+))
+->values(array(
+ 'lid' => '61',
+ 'textgroup' => 'node',
+ 'context' => 'type:article:name',
+ 'objectid' => 'article',
+ 'type' => 'type',
+ 'property' => 'name',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '62',
+ 'textgroup' => 'node',
+ 'context' => 'type:article:title_label',
+ 'objectid' => 'article',
+ 'type' => 'type',
+ 'property' => 'title_label',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '63',
+ 'textgroup' => 'node',
+ 'context' => 'type:article:description',
+ 'objectid' => 'article',
+ 'type' => 'type',
+ 'property' => 'description',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '64',
+ 'textgroup' => 'node',
+ 'context' => 'type:article:help',
+ 'objectid' => 'article',
+ 'type' => 'type',
+ 'property' => 'help',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '65',
+ 'textgroup' => 'node',
+ 'context' => 'type:book:name',
+ 'objectid' => 'book',
+ 'type' => 'type',
+ 'property' => 'name',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '66',
+ 'textgroup' => 'node',
+ 'context' => 'type:book:title_label',
+ 'objectid' => 'book',
+ 'type' => 'type',
+ 'property' => 'title_label',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '67',
+ 'textgroup' => 'node',
+ 'context' => 'type:book:description',
+ 'objectid' => 'book',
+ 'type' => 'type',
+ 'property' => 'description',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '68',
+ 'textgroup' => 'node',
+ 'context' => 'type:page:name',
+ 'objectid' => 'page',
+ 'type' => 'type',
+ 'property' => 'name',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '69',
+ 'textgroup' => 'node',
+ 'context' => 'type:page:title_label',
+ 'objectid' => 'page',
+ 'type' => 'type',
+ 'property' => 'title_label',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '70',
+ 'textgroup' => 'node',
+ 'context' => 'type:page:description',
+ 'objectid' => 'page',
+ 'type' => 'type',
+ 'property' => 'description',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '71',
+ 'textgroup' => 'node',
+ 'context' => 'type:page:help',
+ 'objectid' => 'page',
+ 'type' => 'type',
+ 'property' => 'help',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '72',
+ 'textgroup' => 'node',
+ 'context' => 'type:test_content_type:name',
+ 'objectid' => 'test_content_type',
+ 'type' => 'type',
+ 'property' => 'name',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '73',
+ 'textgroup' => 'node',
+ 'context' => 'type:test_content_type:title_label',
+ 'objectid' => 'test_content_type',
+ 'type' => 'type',
+ 'property' => 'title_label',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '74',
+ 'textgroup' => 'node',
+ 'context' => 'type:test_content_type:description',
+ 'objectid' => 'test_content_type',
+ 'type' => 'type',
+ 'property' => 'description',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '75',
+ 'textgroup' => 'node',
+ 'context' => 'type:test_content_type:help',
+ 'objectid' => 'test_content_type',
+ 'type' => 'type',
+ 'property' => 'help',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->values(array(
+ 'lid' => '76',
+ 'textgroup' => 'blocks',
+ 'context' => 'user:login:title',
+ 'objectid' => 'login',
+ 'type' => 'user',
+ 'property' => 'title',
+ 'objectindex' => '0',
+ 'format' => '',
+))
+->execute();
+
$connection->schema()->createTable('image_effects', array(
'fields' => array(
'ieid' => array(
'primary key' => array(
'lid',
),
+ 'indexes' => array(
+ 'textgroup_context' => array(
+ 'textgroup',
+ array(
+ 'context',
+ '50',
+ ),
+ ),
+ ),
'mysql_character_set' => 'utf8',
));
'context' => 'type:test_content_type:help',
'version' => '1',
))
+->values(array(
+ 'lid' => '76',
+ 'location' => 'blocks:user:login:title',
+ 'textgroup' => 'blocks',
+ 'source' => 'User login title',
+ 'context' => 'user:login:title',
+ 'version' => '1',
+))
->execute();
$connection->schema()->createTable('locales_target', array(
'plural' => '0',
'i18n_status' => '0',
))
-->values(array(
+ ->values(array(
'lid' => '57',
'translation' => 'is - Mildly amusing limerick of the day',
'language' => 'is',
'plural' => '0',
'i18n_status' => '0',
))
+->values(array(
+ 'lid' => '76',
+ 'translation' => 'fr - User login title',
+ 'language' => 'fr',
+ 'plid' => '0',
+ 'plural' => '0',
+ 'i18n_status' => '0',
+))
->execute();
$connection->schema()->createTable('menu_custom', array(
'permission' => 'switch shortcut sets',
'module' => 'shortcut',
))
+->values(array(
+ 'rid' => '3',
+ 'permission' => 'translate admin strings',
+ 'module' => 'i18n_string',
+))
+->values(array(
+ 'rid' => '3',
+ 'permission' => 'translate blocks',
+ 'module' => 'i18n_block',
+))
->values(array(
'rid' => '3',
'permission' => 'translate content',
'permission' => 'translate interface',
'module' => 'locale',
))
+->values(array(
+ 'rid' => '3',
+ 'permission' => 'translate user-defined strings',
+ 'module' => 'i18n_string',
+))
->values(array(
'rid' => '3',
'permission' => 'use advanced search',
'name' => 'i18n_block',
'type' => 'module',
'owner' => '',
- 'status' => '0',
+ 'status' => '1',
'bootstrap' => '0',
- 'schema_version' => '-1',
- 'weight' => '0',
- 'info' => 'a:12:{s:4:"name";s:15:"Block languages";s:11:"description";s:68:"Enables language selector for blocks and optional block translation.";s:12:"dependencies";a:2:{i:0;s:5:"block";i:1;s:11:"i18n_string";}s:7:"package";s:35:"Multilingual - Internationalization";s:4:"core";s:3:"7.x";s:5:"files";a:2:{i:0;s:14:"i18n_block.inc";i:1;s:15:"i18n_block.test";}s:7:"version";s:8:"7.x-1.26";s:7:"project";s:4:"i18n";s:9:"datestamp";s:10:"1534531985";s:5:"mtime";i:1534531985;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}',
+ 'schema_version' => '7001',
+ 'weight' => '100',
+ 'info' => 'a:12:{s:4:"name";s:15:"Block languages";s:11:"description";s:68:"Enables language selector for blocks and optional block translation.";s:12:"dependencies";a:2:{i:0;s:5:"block";i:1;s:11:"i18n_string";}s:7:"package";s:35:"Multilingual - Internationalization";s:4:"core";s:3:"7.x";s:5:"files";a:2:{i:0;s:14:"i18n_block.inc";i:1;s:15:"i18n_block.test";}s:7:"version";s:8:"7.x-1.25";s:7:"project";s:4:"i18n";s:9:"datestamp";s:10:"1531342125";s:5:"mtime";i:1537747250;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}',
))
->values(array(
'filename' => 'sites/all/modules/i18n/i18n_contact/i18n_contact.module',
$values = $media_source->current()->getSource();
$this->assertEquals(1, $values['mid']);
$this->assertEquals('Foo media', $values['name'][0]['value']);
- $this->assertEquals('Foo media', $values['thumbnail'][0]['title']);
+ $this->assertNull($values['thumbnail'][0]['title']);
$this->assertEquals(1, $values['uid'][0]['target_id']);
$this->assertEquals('image', $values['bundle'][0]['target_id']);
}
}
}
+ /**
+ * @covers ::checkRequirements
+ */
+ public function testSourceDatabaseError() {
+ $plugin_definition['requirements_met'] = TRUE;
+ $plugin_definition['source_module'] = 'module1';
+ /** @var \Drupal\Core\State\StateInterface $state */
+ $state = $this->getMock('Drupal\Core\State\StateInterface');
+ /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
+ $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+ $plugin = new TestDrupalSqlBase([], 'test', $plugin_definition, $this->getMigration(), $state, $entity_manager);
+ $system_data = $plugin->getSystemData();
+ $this->setExpectedException(RequirementsException::class, 'No database connection configured for source plugin test');
+ $plugin->checkRequirements();
+ }
+
}
namespace Drupal\Tests\migrate_drupal\Unit\source;
'shortcut_set' => 1,
'action' => 23,
'menu' => 8,
- 'taxonomy_term' => 8,
+ 'taxonomy_term' => 15,
'taxonomy_vocabulary' => 7,
'tour' => 5,
'user' => 7,
$counts['file'] = 8;
$counts['menu_link_content'] = 11;
$counts['node'] = 19;
- $counts['taxonomy_term'] = 9;
+ $counts['taxonomy_term'] = 16;
$counts['user'] = 8;
$counts['view'] = 16;
return $counts;
'filter',
'forum',
'image',
+ 'i18n_block',
'language',
'link',
'list',
'entity_translation_i18n_menu',
'entity_translation_upgrade',
'i18n',
- 'i18n_block',
'i18n_contact',
'i18n_field',
'i18n_forum',
])->save();
}
}
+
+/**
+ * Clear caches due to updated views data.
+ */
+function node_post_update_node_revision_views_data() {
+ // Empty post-update hook.
+}
$data['node_field_revision']['nid']['relationship']['base field'] = 'nid';
$data['node_field_revision']['nid']['relationship']['title'] = $this->t('Content');
$data['node_field_revision']['nid']['relationship']['label'] = $this->t('Get the actual content from a content revision.');
+ $data['node_field_revision']['nid']['relationship']['extra'][] = [
+ 'field' => 'langcode',
+ 'left_field' => 'langcode',
+ ];
$data['node_field_revision']['vid'] = [
'argument' => [
'base field' => 'vid',
'title' => $this->t('Content'),
'label' => $this->t('Get the actual content from a content revision.'),
+ 'extra' => [
+ [
+ 'field' => 'langcode',
+ 'left_field' => 'langcode',
+ ],
+ ],
],
] + $data['node_field_revision']['vid'];
* {@inheritdoc}
*/
public function fields() {
- return [
+ $fields = [
'type' => $this->t('Machine name of the node type.'),
'name' => $this->t('Human name of the node type.'),
'description' => $this->t('Description of the node type.'),
plugin_id: field
entity_type: node
entity_field: nid
+ langcode:
+ id: langcode
+ table: node_field_revision
+ field: langcode
+ plugin_id: field
+ entity_type: node
+ entity_field: langcode
arguments:
nid:
id: nid
plugin_id: field
entity_type: node
entity_field: vid
+ langcode:
+ id: langcode
+ table: node_field_revision
+ field: langcode
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: DESC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: langcode
+ plugin_id: standard
+ display_extenders: { }
display_plugin: default
display_title: Master
id: default
plugin_id: field
entity_type: node
entity_field: nid
+ langcode:
+ id: langcode
+ table: node_field_revision
+ field: langcode
+ entity_type: node
+ entity_field: langcode
+ plugin_id: field
arguments:
nid:
id: nid
plugin_id: node_nid
entity_type: node
entity_field: nid
+ display_extenders: { }
+ sorts:
+ langcode:
+ id: langcode
+ table: node_field_revision
+ field: langcode
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: langcode
+ plugin_id: standard
display_plugin: default
display_title: Master
id: default
namespace Drupal\Tests\node\Kernel\Views;
+use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
*
* @var array
*/
- public static $modules = ['node' , 'node_test_views'];
+ public static $modules = [
+ 'node',
+ 'node_test_views',
+ 'language',
+ 'content_translation',
+ ];
/**
* {@inheritdoc}
$this->installEntitySchema('user');
$this->installEntitySchema('node');
+ ConfigurableLanguage::createFromLangcode('fr')->save();
+
ViewTestData::createTestViews(get_class($this), ['node_test_views']);
}
$type->save();
$node = Node::create(['type' => 'page', 'title' => 'test', 'uid' => 1]);
$node->save();
+
+ // Add a translation.
+ $translation = $node->addTranslation('fr', $node->toArray());
+ $translation->save();
// Create revision of the node.
$node->setNewRevision(TRUE);
$node->save();
+
$column_map = [
'vid' => 'vid',
'node_field_data_node_field_revision_nid' => 'node_node_revision_nid',
'nid_1' => 'nid_1',
+ 'node_field_revision_langcode' => 'node_field_revision_langcode',
];
- // Here should be two rows.
+ // Here should be two rows for each translation.
$view_nid = Views::getView('test_node_revision_nid');
$this->executeView($view_nid, [$node->id()]);
$resultset_nid = [
'vid' => '1',
'node_node_revision_nid' => '1',
'nid_1' => '1',
+ 'node_field_revision_langcode' => 'fr',
+ ],
+ [
+ 'vid' => '1',
+ 'node_node_revision_nid' => '1',
+ 'nid_1' => '1',
+ 'node_field_revision_langcode' => 'en',
],
[
'vid' => '2',
'node_revision_nid' => '1',
'node_node_revision_nid' => '1',
'nid_1' => '1',
+ 'node_field_revision_langcode' => 'fr',
+ ],
+ [
+ 'vid' => '2',
+ 'node_revision_nid' => '1',
+ 'node_node_revision_nid' => '1',
+ 'nid_1' => '1',
+ 'node_field_revision_langcode' => 'en',
],
];
$this->assertIdenticalResultset($view_nid, $resultset_nid, $column_map);
- // There should be only one row with active revision 2.
+ // There should be one row with active revision 2 for each translation.
$view_vid = Views::getView('test_node_revision_vid');
$this->executeView($view_vid, [$node->id()]);
$resultset_vid = [
'vid' => '2',
'node_node_revision_nid' => '1',
'nid_1' => '1',
+ 'node_field_revision_langcode' => 'en',
+ ],
+ [
+ 'vid' => '2',
+ 'node_node_revision_nid' => '1',
+ 'nid_1' => '1',
+ 'node_field_revision_langcode' => 'fr',
],
];
$this->assertIdenticalResultset($view_vid, $resultset_vid, $column_map);
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\ComputedItemListTrait;
$entity = $this->getEntity();
if (!$entity->isNew()) {
- // @todo Support loading language neutral aliases in
- // https://www.drupal.org/node/2511968.
- $alias = \Drupal::service('path.alias_storage')->load([
+ $conditions = [
'source' => '/' . $entity->toUrl()->getInternalPath(),
'langcode' => $this->getLangcode(),
- ]);
+ ];
+ $alias = \Drupal::service('path.alias_storage')->load($conditions);
+ if ($alias === FALSE) {
+ // Fall back to non-specific language.
+ if ($this->getLangcode() !== LanguageInterface::LANGCODE_NOT_SPECIFIED) {
+ $conditions['langcode'] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
+ $alias = \Drupal::service('path.alias_storage')->load($conditions);
+ }
+ }
if ($alias) {
$value = $alias;
* {@inheritdoc}
*/
public function postSave($update) {
+ // If specified, rely on the langcode property for the language, so that the
+ // existing language of an alias can be kept. That could for example be
+ // unspecified even if the field/entity has a specific langcode.
+ $alias_langcode = ($this->langcode && $this->pid) ? $this->langcode : $this->getLangcode();
+
if (!$update) {
if ($this->alias) {
$entity = $this->getEntity();
- if ($path = \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode())) {
+ if ($path = \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $alias_langcode)) {
$this->pid = $path['pid'];
}
}
// Only save a non-empty alias.
elseif ($this->alias) {
$entity = $this->getEntity();
- \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode(), $this->pid);
+ \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $alias_langcode, $this->pid);
}
}
}
if ($entity && !$entity->isNew() && !$entity->isDefaultRevision()) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $original */
$original = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
- if ($value->alias != $original->path->alias) {
- $this->context->addViolation($constraint->message);
+ $entity_langcode = $entity->language()->getId();
+
+ // Only add the violation if the current translation does not have the
+ // same path alias.
+ if ($original->hasTranslation($entity_langcode)) {
+ if ($value->alias != $original->getTranslation($entity_langcode)->path->alias) {
+ $this->context->addViolation($constraint->message);
+ }
}
}
}
namespace Drupal\Tests\path\Functional;
-use Drupal\node\Entity\NodeType;
+use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
*
* @var array
*/
- public static $modules = ['node', 'path', 'content_moderation'];
+ public static $modules = [
+ 'node',
+ 'path',
+ 'content_moderation',
+ 'content_translation',
+ ];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
+ ConfigurableLanguage::createFromLangcode('fr')->save();
+ $this->rebuildContainer();
// Created a content type.
- $node_type = NodeType::create(['name' => 'moderated', 'type' => 'moderated']);
- $node_type->save();
+ $this->drupalCreateContentType([
+ 'name' => 'moderated',
+ 'type' => 'moderated',
+ ]);
// Set the content type as moderated.
$workflow = $this->createEditorialWorkflow();
$workflow->save();
$this->drupalLogin($this->rootUser);
+
+ // Enable URL language detection and selection.
+ $edit = ['language_interface[enabled][language-url]' => 1];
+ $this->drupalPostForm('admin/config/regional/language/detection', $edit, 'Save settings');
+
+ // Enable translation for moderated node.
+ $edit = [
+ 'entity_types[node]' => 1,
+ 'settings[node][moderated][translatable]' => 1,
+ 'settings[node][moderated][fields][path]' => 1,
+ 'settings[node][moderated][fields][body]' => 1,
+ 'settings[node][moderated][settings][language][language_alterable]' => 1,
+ ];
+ $this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
+ \Drupal::entityTypeManager()->clearCachedDefinitions();
}
/**
$this->assertSession()->pageTextNotContains('You can only change the URL alias for the published version of this content.');
}
+ /**
+ * Tests that translated and moderated node can get new draft revision.
+ */
+ public function testTranslatedModeratedNodeAlias() {
+ // Create one node with a random alias.
+ $default_node = $this->drupalCreateNode([
+ 'type' => 'moderated',
+ 'langcode' => 'en',
+ 'moderation_state' => 'published',
+ 'path' => '/' . $this->randomMachineName(),
+ ]);
+
+ // Add published translation with another alias.
+ $this->drupalGet('node/' . $default_node->id());
+ $this->drupalGet('node/' . $default_node->id() . '/translations');
+ $this->clickLink('Add');
+ $edit_translation = [
+ 'body[0][value]' => $this->randomMachineName(),
+ 'moderation_state[0][state]' => 'published',
+ 'path[0][alias]' => '/' . $this->randomMachineName(),
+ ];
+ $this->drupalPostForm(NULL, $edit_translation, 'Save (this translation)');
+ // Confirm that the alias works.
+ $this->drupalGet('fr' . $edit_translation['path[0][alias]']);
+ $this->assertSession()->pageTextContains($edit_translation['body[0][value]']);
+
+ $default_path = $default_node->path->alias;
+ $translation_path = 'fr' . $edit_translation['path[0][alias]'];
+
+ $this->assertPathsAreAccessible([$default_path, $translation_path]);
+
+ // Try to create new draft revision for translation with a new path alias.
+ $edit_new_translation_draft_with_alias = [
+ 'moderation_state[0][state]' => 'draft',
+ 'path[0][alias]' => '/' . $this->randomMachineName(),
+ ];
+ $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_alias, 'Save (this translation)');
+ // Confirm the expected error.
+ $this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
+
+ // Create new draft revision for translation without changing path alias.
+ $edit_new_translation_draft = [
+ 'body[0][value]' => $this->randomMachineName(),
+ 'moderation_state[0][state]' => 'draft',
+ ];
+ $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft, t('Save (this translation)'));
+ // Confirm that the new draft revision was created.
+ $this->assertSession()->pageTextNotContains('You can only change the URL alias for the published version of this content.');
+ $this->assertSession()->pageTextContains($edit_new_translation_draft['body[0][value]']);
+ $this->assertPathsAreAccessible([$default_path, $translation_path]);
+
+ // Try to create a new draft revision for translation with path alias from
+ // the original language's default revision.
+ $edit_new_translation_draft_with_defaults_alias = [
+ 'moderation_state[0][state]' => 'draft',
+ 'path[0][alias]' => $default_node->path->alias,
+ ];
+ $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_defaults_alias, 'Save (this translation)');
+ // Verify the expected error.
+ $this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
+
+ // Try to create new draft revision for translation with deleted (empty)
+ // path alias.
+ $edit_new_translation_draft_empty_alias = [
+ 'body[0][value]' => $this->randomMachineName(),
+ 'moderation_state[0][state]' => 'draft',
+ 'path[0][alias]' => '',
+ ];
+ $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_empty_alias, 'Save (this translation)');
+ // Confirm the expected error.
+ $this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
+
+ // Create new default (published) revision for translation with new path
+ // alias.
+ $edit_new_translation = [
+ 'body[0][value]' => $this->randomMachineName(),
+ 'moderation_state[0][state]' => 'published',
+ 'path[0][alias]' => '/' . $this->randomMachineName(),
+ ];
+ $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation, 'Save (this translation)');
+ // Confirm that the new published revision was created.
+ $this->assertSession()->pageTextNotContains('You can only change the URL alias for the published version of this content.');
+ $this->assertSession()->pageTextContains($edit_new_translation['body[0][value]']);
+ $this->assertSession()->addressEquals('fr' . $edit_new_translation['path[0][alias]']);
+ $this->assertPathsAreAccessible([$default_path]);
+ }
+
+ /**
+ * Helper callback to verify paths are responding with status 200.
+ *
+ * @param string[] $paths
+ * An array of paths to check for.
+ */
+ public function assertPathsAreAccessible(array $paths) {
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertSession()->statusCodeEquals(200);
+ }
+ }
+
}
namespace Drupal\Tests\path\Functional;
+use Drupal\Core\Language\LanguageInterface;
+
/**
* Confirm that the Path module user interface works with languages.
*
$this->assertText(t('Filter aliases'), 'Foreign URL alias works');
}
+ /**
+ * Test that language unspecific aliases are shown and saved in the node form.
+ */
+ public function testNotSpecifiedNode() {
+ // Create test node.
+ $node = $this->drupalCreateNode();
+
+ // Create a language-unspecific alias in the admin UI, ensure that is
+ // displayed and the langcode is not changed when saving.
+ $edit = [
+ 'source' => '/node/' . $node->id(),
+ 'alias' => '/' . $this->getRandomGenerator()->word(8),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ ];
+ $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
+
+ $this->drupalGet($node->toUrl('edit-form'));
+ $this->assertSession()->fieldValueEquals('path[0][alias]', $edit['alias']);
+ $this->drupalPostForm(NULL, [], t('Save'));
+
+ $this->drupalGet('admin/config/search/path');
+ $this->assertSession()->pageTextContains('None');
+ $this->assertSession()->pageTextNotContains('English');
+
+ // Create another node, with no alias, to ensure non-language specific
+ // aliases are loaded correctly.
+ $node = $this->drupalCreateNode();
+ $this->drupalget($node->toUrl('edit-form'));
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $this->assertSession()->pageTextNotContains(t('The alias is already in use.'));
+ }
+
}
*/
protected $entity;
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form = parent::form($form, $form_state);
+ $form['#attached']['library'][] = 'core/drupal.form';
+
+ return $form;
+ }
+
/**
* {@inheritdoc}
*/
$attributes = $testcase->attributes();
+ $function = $attributes->class . '->' . $attributes->name . '()';
$record = [
'test_id' => $test_id,
'test_class' => (string) $attributes->class,
'message' => $message,
// @todo: Check on the proper values for this.
'message_group' => 'Other',
- 'function' => $attributes->class . '->' . $attributes->name . '()',
+ 'function' => $function,
'line' => $attributes->line ?: 0,
- 'file' => $attributes->file,
+ // There are situations when the file will not be present because a PHPUnit
+ // @requires has caused a test to be skipped.
+ 'file' => $attributes->file ?: $function,
];
return $record;
}
+++ /dev/null
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-/**
- * Various tests of AJAX behavior.
- *
- * @group Ajax
- */
-class ElementValidationTest extends AjaxTestBase {
-
- /**
- * Tries to post an Ajax change to a form that has a validated element.
- *
- * The drivertext field is Ajax-enabled. An additional field is not, but
- * is set to be a required field. In this test the required field is not
- * filled in, and we want to see if the activation of the "drivertext"
- * Ajax-enabled field fails due to the required field being empty.
- */
- public function testAjaxElementValidation() {
- $edit = ['drivertext' => t('some dumb text')];
-
- // Post with 'drivertext' as the triggering element.
- $this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivertext');
- // Look for a validation failure in the resultant JSON.
- $this->assertNoText(t('Error message'), 'No error message in resultant JSON');
- $this->assertText('ajax_forms_test_validation_form_callback invoked', 'The correct callback was invoked');
-
- $this->drupalGet('ajax_validation_test');
- $edit = ['drivernumber' => 12345];
-
- // Post with 'drivernumber' as the triggering element.
- $this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivernumber');
- // Look for a validation failure in the resultant JSON.
- $this->assertNoText(t('Error message'), 'No error message in resultant JSON');
- $this->assertText('ajax_forms_test_validation_number_form_callback invoked', 'The correct callback was invoked');
- }
-
-}
+++ /dev/null
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-use Drupal\Core\Ajax\DataCommand;
-
-/**
- * Tests that form values are properly delivered to AJAX callbacks.
- *
- * @group Ajax
- */
-class FormValuesTest extends AjaxTestBase {
-
- protected function setUp() {
- parent::setUp();
-
- $this->drupalLogin($this->drupalCreateUser(['access content']));
- }
-
- /**
- * Submits forms with select and checkbox elements via Ajax.
- */
- public function testSimpleAjaxFormValue() {
- // Verify form values of a select element.
- foreach (['red', 'green', 'blue'] as $item) {
- $edit = [
- 'select' => $item,
- ];
- $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'select');
- $expected = new DataCommand('#ajax_selected_color', 'form_state_value_select', $item);
- $this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a selectbox issued with a correct value.');
- }
-
- // Verify form values of a checkbox element.
- foreach ([FALSE, TRUE] as $item) {
- $edit = [
- 'checkbox' => $item,
- ];
- $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'checkbox');
- $expected = new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $item);
- $this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a checkbox issued with a correct value.');
- }
-
- // Verify that AJAX elements with invalid callbacks return error code 500.
- // Ensure the test error log is empty before these tests.
- $this->assertNoErrorsLogged();
- // We don't need to check for the X-Drupal-Ajax-Token header with these
- // invalid requests.
- $this->assertAjaxHeader = FALSE;
- foreach (['null', 'empty', 'nonexistent'] as $key) {
- $element_name = 'select_' . $key . '_callback';
- $edit = [
- $element_name => 'red',
- ];
- $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, $element_name);
- $this->assertResponse(500);
- }
- // Switch this back to the default.
- $this->assertAjaxHeader = TRUE;
- // The exceptions are expected. Do not interpret them as a test failure.
- // Not using File API; a potential error must trigger a PHP warning.
- unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
- }
-
-}
+++ /dev/null
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-use Drupal\Core\Ajax\AddCssCommand;
-use Drupal\Core\Ajax\AlertCommand;
-use Drupal\Core\Ajax\AppendCommand;
-use Drupal\Core\Ajax\HtmlCommand;
-use Drupal\Core\Ajax\PrependCommand;
-use Drupal\Core\Ajax\SettingsCommand;
-use Drupal\Core\Asset\AttachedAssets;
-
-/**
- * Performs tests on AJAX framework functions.
- *
- * @group Ajax
- */
-class FrameworkTest extends AjaxTestBase {
-
- /**
- * Verifies the Ajax rendering of a command in the settings.
- */
- public function testAJAXRender() {
- // Verify that settings command is generated if JavaScript settings exist.
- $commands = $this->drupalGetAjax('ajax-test/render');
- $expected = new SettingsCommand(['ajax' => 'test'], TRUE);
- $this->assertCommand($commands, $expected->render(), 'JavaScript settings command is present.');
- }
-
- /**
- * Tests AjaxResponse::prepare() AJAX commands ordering.
- */
- public function testOrder() {
- $expected_commands = [];
-
- // Expected commands, in a very specific order.
- $asset_resolver = \Drupal::service('asset.resolver');
- $css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
- $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
- $renderer = \Drupal::service('renderer');
- $expected_commands[0] = new SettingsCommand(['ajax' => 'test'], TRUE);
- $build['#attached']['library'][] = 'ajax_test/order-css-command';
- $assets = AttachedAssets::createFromRenderArray($build);
- $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
- $expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array));
- $build['#attached']['library'][] = 'ajax_test/order-header-js-command';
- $build['#attached']['library'][] = 'ajax_test/order-footer-js-command';
- $assets = AttachedAssets::createFromRenderArray($build);
- list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE);
- $js_header_render_array = $js_collection_renderer->render($js_assets_header);
- $js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
- $expected_commands[2] = new PrependCommand('head', $js_header_render_array);
- $expected_commands[3] = new AppendCommand('body', $js_footer_render_array);
- $expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
-
- // Load any page with at least one CSS file, at least one JavaScript file
- // and at least one #ajax-powered element. The latter is an assumption of
- // drupalPostAjaxForm(), the two former are assumptions of the Ajax
- // renderer.
- // @todo refactor AJAX Framework + tests to make less assumptions.
- $this->drupalGet('ajax_forms_test_lazy_load_form');
-
- // Verify AJAX command order — this should always be the order:
- // 1. JavaScript settings
- // 2. CSS files
- // 3. JavaScript files in the header
- // 4. JavaScript files in the footer
- // 5. Any other AJAX commands, in whatever order they were added.
- $commands = $this->drupalPostAjaxForm(NULL, [], NULL, 'ajax-test/order', [], [], NULL, []);
- $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[0]->render(), 'Settings command is first.');
- $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[1]->render(), 'CSS command is second (and CSS files are ordered correctly).');
- $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[2]->render(), 'Header JS command is third.');
- $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[3]->render(), 'Footer JS command is fourth.');
- $this->assertCommand(array_slice($commands, 4, 1), $expected_commands[4]->render(), 'HTML command is fifth.');
- }
-
- /**
- * Tests the behavior of an error alert command.
- */
- public function testAJAXRenderError() {
- // Verify custom error message.
- $edit = [
- 'message' => 'Custom error message.',
- ];
- $commands = $this->drupalGetAjax('ajax-test/render-error', ['query' => $edit]);
- $expected = new AlertCommand($edit['message']);
- $this->assertCommand($commands, $expected->render(), 'Custom error message is output.');
- }
-
- /**
- * Tests that new JavaScript and CSS files are lazy-loaded on an AJAX request.
- */
- public function testLazyLoad() {
- $asset_resolver = \Drupal::service('asset.resolver');
- $css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
- $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
- $renderer = \Drupal::service('renderer');
-
- $expected = [
- 'setting_name' => 'ajax_forms_test_lazy_load_form_submit',
- 'setting_value' => 'executed',
- 'library_1' => 'system/admin',
- 'library_2' => 'system/drupal.system',
- ];
-
- // Get the base page.
- $this->drupalGet('ajax_forms_test_lazy_load_form');
- $original_settings = $this->getDrupalSettings();
- $original_libraries = explode(',', $original_settings['ajaxPageState']['libraries']);
-
- // Verify that the base page doesn't have the settings and files that are to
- // be lazy loaded as part of the next requests.
- $this->assertTrue(!isset($original_settings[$expected['setting_name']]), format_string('Page originally lacks the %setting, as expected.', ['%setting' => $expected['setting_name']]));
- $this->assertTrue(!in_array($expected['library_1'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', ['%library' => $expected['library_1']]));
- $this->assertTrue(!in_array($expected['library_2'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', ['%library' => $expected['library_2']]));
-
- // Calculate the expected CSS and JS.
- $assets = new AttachedAssets();
- $assets->setLibraries([$expected['library_1']])
- ->setAlreadyLoadedLibraries($original_libraries);
- $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
- $expected_css_html = $renderer->renderRoot($css_render_array);
-
- $assets->setLibraries([$expected['library_2']])
- ->setAlreadyLoadedLibraries($original_libraries);
- $js_assets = $asset_resolver->getJsAssets($assets, FALSE)[1];
- unset($js_assets['drupalSettings']);
- $js_render_array = $js_collection_renderer->render($js_assets);
- $expected_js_html = $renderer->renderRoot($js_render_array);
-
- // Submit the AJAX request without triggering files getting added.
- $commands = $this->drupalPostAjaxForm(NULL, ['add_files' => FALSE], ['op' => t('Submit')]);
- $new_settings = $this->getDrupalSettings();
- $new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
-
- // Verify the setting was not added when not expected.
- $this->assertTrue(!isset($new_settings[$expected['setting_name']]), format_string('Page still lacks the %setting, as expected.', ['%setting' => $expected['setting_name']]));
- $this->assertTrue(!in_array($expected['library_1'], $new_libraries), format_string('Page still lacks the %library library, as expected.', ['%library' => $expected['library_1']]));
- $this->assertTrue(!in_array($expected['library_2'], $new_libraries), format_string('Page still lacks the %library library, as expected.', ['%library' => $expected['library_2']]));
- // Verify a settings command does not add CSS or scripts to drupalSettings
- // and no command inserts the corresponding tags on the page.
- $found_settings_command = FALSE;
- $found_markup_command = FALSE;
- foreach ($commands as $command) {
- if ($command['command'] == 'settings' && (array_key_exists('css', $command['settings']['ajaxPageState']) || array_key_exists('js', $command['settings']['ajaxPageState']))) {
- $found_settings_command = TRUE;
- }
- if (isset($command['data']) && ($command['data'] == $expected_js_html || $command['data'] == $expected_css_html)) {
- $found_markup_command = TRUE;
- }
- }
- $this->assertFalse($found_settings_command, format_string('Page state still lacks the %library_1 and %library_2 libraries, as expected.', ['%library_1' => $expected['library_1'], '%library_2' => $expected['library_2']]));
- $this->assertFalse($found_markup_command, format_string('Page still lacks the %library_1 and %library_2 libraries, as expected.', ['%library_1' => $expected['library_1'], '%library_2' => $expected['library_2']]));
-
- // Submit the AJAX request and trigger adding files.
- $commands = $this->drupalPostAjaxForm(NULL, ['add_files' => TRUE], ['op' => t('Submit')]);
- $new_settings = $this->getDrupalSettings();
- $new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
-
- // Verify the expected setting was added, both to drupalSettings, and as
- // the first AJAX command.
- $this->assertIdentical($new_settings[$expected['setting_name']], $expected['setting_value'], format_string('Page now has the %setting.', ['%setting' => $expected['setting_name']]));
- $expected_command = new SettingsCommand([$expected['setting_name'] => $expected['setting_value']], TRUE);
- $this->assertCommand(array_slice($commands, 0, 1), $expected_command->render(), 'The settings command was first.');
-
- // Verify the expected CSS file was added, both to drupalSettings, and as
- // the second AJAX command for inclusion into the HTML.
- $this->assertTrue(in_array($expected['library_1'], $new_libraries), format_string('Page state now has the %library library.', ['%library' => $expected['library_1']]));
- $this->assertCommand(array_slice($commands, 1, 1), ['data' => $expected_css_html], format_string('Page now has the %library library.', ['%library' => $expected['library_1']]));
-
- // Verify the expected JS file was added, both to drupalSettings, and as
- // the third AJAX command for inclusion into the HTML. By testing for an
- // exact HTML string containing the SCRIPT tag, we also ensure that
- // unexpected JavaScript code, such as a jQuery.extend() that would
- // potentially clobber rather than properly merge settings, didn't
- // accidentally get added.
- $this->assertTrue(in_array($expected['library_2'], $new_libraries), format_string('Page state now has the %library library.', ['%library' => $expected['library_2']]));
- $this->assertCommand(array_slice($commands, 2, 1), ['data' => $expected_js_html], format_string('Page now has the %library library.', ['%library' => $expected['library_2']]));
- }
-
- /**
- * Tests that drupalSettings.currentPath is not updated on AJAX requests.
- */
- public function testCurrentPathChange() {
- $commands = $this->drupalPostAjaxForm('ajax_forms_test_lazy_load_form', ['add_files' => FALSE], ['op' => t('Submit')]);
- foreach ($commands as $command) {
- if ($command['command'] == 'settings') {
- $this->assertFalse(isset($command['settings']['currentPath']), 'Value of drupalSettings.currentPath is not updated after an AJAX request.');
- }
- }
- }
-
- /**
- * Tests that overridden CSS files are not added during lazy load.
- */
- public function testLazyLoadOverriddenCSS() {
- // The test theme overrides js.module.css without an implementation,
- // thereby removing it.
- \Drupal::service('theme_handler')->install(['test_theme']);
- $this->config('system.theme')
- ->set('default', 'test_theme')
- ->save();
-
- // This gets the form, and emulates an Ajax submission on it, including
- // adding markup to the HEAD and BODY for any lazy loaded JS/CSS files.
- $this->drupalPostAjaxForm('ajax_forms_test_lazy_load_form', ['add_files' => TRUE], ['op' => t('Submit')]);
-
- // Verify that the resulting HTML does not load the overridden CSS file.
- // We add a "?" to the assertion, because drupalSettings may include
- // information about the file; we only really care about whether it appears
- // in a LINK or STYLE tag, for which Drupal always adds a query string for
- // cache control.
- $this->assertNoText('js.module.css?', 'Ajax lazy loading does not add overridden CSS files.');
- }
-
-}
+++ /dev/null
-<?php
-
-namespace Drupal\system\Tests\Form;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests that FAPI correctly determines the triggering element.
- *
- * @group Form
- */
-class TriggeringElementTest extends WebTestBase {
-
- /**
- * Modules to enable.
- *
- * @var array
- */
- public static $modules = ['form_test'];
-
- /**
- * Test the determination of the triggering element when no button
- * information is included in the POST data, as is sometimes the case when
- * the ENTER key is pressed in a textfield in Internet Explorer.
- */
- public function testNoButtonInfoInPost() {
- $path = 'form-test/clicked-button';
- $edit = [];
- $form_html_id = 'form-test-clicked-button';
-
- // Ensure submitting a form with no buttons results in no triggering element
- // and the form submit handler not running.
- $this->drupalPostForm($path, $edit, NULL, [], [], $form_html_id);
- $this->assertText('There is no clicked button.', '$form_state->getTriggeringElement() set to NULL.');
- $this->assertNoText('Submit handler for form_test_clicked_button executed.', 'Form submit handler did not execute.');
-
- // Ensure submitting a form with one or more submit buttons results in the
- // triggering element being set to the first one the user has access to. An
- // argument with 'r' in it indicates a restricted (#access=FALSE) button.
- $this->drupalPostForm($path . '/s', $edit, NULL, [], [], $form_html_id);
- $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to only button.');
- $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-
- $this->drupalPostForm($path . '/s/s', $edit, NULL, [], [], $form_html_id);
- $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to first button.');
- $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-
- $this->drupalPostForm($path . '/rs/s', $edit, NULL, [], [], $form_html_id);
- $this->assertText('The clicked button is button2.', '$form_state->getTriggeringElement() set to first available button.');
- $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-
- // Ensure submitting a form with buttons of different types results in the
- // triggering element being set to the first button, regardless of type. For
- // the FAPI 'button' type, this should result in the submit handler not
- // executing. The types are 's'(ubmit), 'b'(utton), and 'i'(mage_button).
- $this->drupalPostForm($path . '/s/b/i', $edit, NULL, [], [], $form_html_id);
- $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to first button.');
- $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-
- $this->drupalPostForm($path . '/b/s/i', $edit, NULL, [], [], $form_html_id);
- $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to first button.');
- $this->assertNoText('Submit handler for form_test_clicked_button executed.', 'Form submit handler did not execute.');
-
- $this->drupalPostForm($path . '/i/s/b', $edit, NULL, [], [], $form_html_id);
- $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to first button.');
- $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
- }
-
- /**
- * Test that the triggering element does not get set to a button with
- * #access=FALSE.
- */
- public function testAttemptAccessControlBypass() {
- $path = 'form-test/clicked-button';
- $form_html_id = 'form-test-clicked-button';
-
- // Retrieve a form where 'button1' has #access=FALSE and 'button2' doesn't.
- $this->drupalGet($path . '/rs/s');
-
- // Submit the form with 'button1=button1' in the POST data, which someone
- // trying to get around security safeguards could easily do. We have to do
- // a little trickery here, to work around the safeguards in drupalPostForm(): by
- // renaming the text field that is in the form to 'button1', we can get the
- // data we want into \Drupal::request()->request.
- $elements = $this->xpath('//form[@id="' . $form_html_id . '"]//input[@name="text"]');
- $elements[0]['name'] = 'button1';
- $this->drupalPostForm(NULL, ['button1' => 'button1'], NULL, [], [], $form_html_id);
-
- // Ensure that the triggering element was not set to the restricted button.
- // Do this with both a negative and positive assertion, because negative
- // assertions alone can be brittle. See testNoButtonInfoInPost() for why the
- // triggering element gets set to 'button2'.
- $this->assertNoText('The clicked button is button1.', '$form_state->getTriggeringElement() not set to a restricted button.');
- $this->assertText('The clicked button is button2.', '$form_state->getTriggeringElement() not set to a restricted button.');
- }
-
-}
+++ /dev/null
-<?php
-
-namespace Drupal\system\Tests\Session;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests the stacked session handler functionality.
- *
- * @group Session
- */
-class StackSessionHandlerIntegrationTest extends WebTestBase {
-
- /**
- * Modules to enable.
- *
- * @var array
- */
- public static $modules = ['session_test'];
-
- /**
- * Tests a request.
- */
- public function testRequest() {
- $actual_trace = $this->drupalGetAjax('session-test/trace-handler');
- $expect_trace = [
- ['BEGIN', 'test_argument', 'open'],
- ['BEGIN', NULL, 'open'],
- ['END', NULL, 'open'],
- ['END', 'test_argument', 'open'],
- ['BEGIN', 'test_argument', 'read', $this->sessionId],
- ['BEGIN', NULL, 'read', $this->sessionId],
- ['END', NULL, 'read', $this->sessionId],
- ['END', 'test_argument', 'read', $this->sessionId],
- ['BEGIN', 'test_argument', 'write', $this->sessionId],
- ['BEGIN', NULL, 'write', $this->sessionId],
- ['END', NULL, 'write', $this->sessionId],
- ['END', 'test_argument', 'write', $this->sessionId],
- ['BEGIN', 'test_argument', 'close'],
- ['BEGIN', NULL, 'close'],
- ['END', NULL, 'close'],
- ['END', 'test_argument', 'close'],
- ];
- $this->assertEqual($expect_trace, $actual_trace);
- }
-
-}
*/
public function checkboxCallback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
- $response->addCommand(new HtmlCommand('#ajax_checkbox_value', (int) $form_state->getValue('checkbox')));
+ $response->addCommand(new HtmlCommand('#ajax_checkbox_value', $form_state->getValue('checkbox') ? 'checked' : 'unchecked'));
$response->addCommand(new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $form_state->getValue('checkbox')));
return $response;
}
$form['select_' . $key . '_callback'] = [
'#type' => 'select',
'#title' => $this->t('Test %key callbacks', ['%key' => $key]),
- '#options' => ['red' => 'red'],
+ '#options' => ['red' => 'red', 'green' => 'green'],
'#ajax' => ['callback' => $value],
];
}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\system\Functional\Ajax;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Ajax\AddCssCommand;
+use Drupal\Core\Ajax\AlertCommand;
+use Drupal\Core\Ajax\AppendCommand;
+use Drupal\Core\Ajax\HtmlCommand;
+use Drupal\Core\Ajax\PrependCommand;
+use Drupal\Core\Ajax\SettingsCommand;
+use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Performs tests on AJAX framework functions.
+ *
+ * @group Ajax
+ */
+class FrameworkTest extends BrowserTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+ /**
+ * Verifies the Ajax rendering of a command in the settings.
+ */
+ public function testAJAXRender() {
+ // Verify that settings command is generated if JavaScript settings exist.
+ $commands = $this->drupalGetAjax('ajax-test/render');
+ $expected = new SettingsCommand(['ajax' => 'test'], TRUE);
+ $this->assertCommand($commands, $expected->render(), 'JavaScript settings command is present.');
+ }
+
+ /**
+ * Tests AjaxResponse::prepare() AJAX commands ordering.
+ */
+ public function testOrder() {
+ $expected_commands = [];
+
+ // Expected commands, in a very specific order.
+ $asset_resolver = \Drupal::service('asset.resolver');
+ $css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
+ $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
+ $renderer = \Drupal::service('renderer');
+ $build['#attached']['library'][] = 'ajax_test/order-css-command';
+ $assets = AttachedAssets::createFromRenderArray($build);
+ $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
+ $expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array));
+ $build['#attached']['library'][] = 'ajax_test/order-header-js-command';
+ $build['#attached']['library'][] = 'ajax_test/order-footer-js-command';
+ $assets = AttachedAssets::createFromRenderArray($build);
+ list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE);
+ $js_header_render_array = $js_collection_renderer->render($js_assets_header);
+ $js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
+ $expected_commands[2] = new PrependCommand('head', $js_header_render_array);
+ $expected_commands[3] = new AppendCommand('body', $js_footer_render_array);
+ $expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
+
+ // Load any page with at least one CSS file, at least one JavaScript file
+ // and at least one #ajax-powered element. The latter is an assumption of
+ // drupalPostAjaxForm(), the two former are assumptions of the Ajax
+ // renderer.
+ // @todo refactor AJAX Framework + tests to make less assumptions.
+ $this->drupalGet('ajax_forms_test_lazy_load_form');
+
+ // Verify AJAX command order — this should always be the order:
+ // 1. CSS files
+ // 2. JavaScript files in the header
+ // 3. JavaScript files in the footer
+ // 4. Any other AJAX commands, in whatever order they were added.
+ $commands = $this->drupalGetAjax('ajax-test/order');
+ $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[1]->render());
+ $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[2]->render());
+ $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[3]->render());
+ $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[4]->render());
+ }
+
+ /**
+ * Tests the behavior of an error alert command.
+ */
+ public function testAJAXRenderError() {
+ // Verify custom error message.
+ $edit = [
+ 'message' => 'Custom error message.',
+ ];
+ $commands = $this->drupalGetAjax('ajax-test/render-error', ['query' => $edit]);
+ $expected = new AlertCommand($edit['message']);
+ $this->assertCommand($commands, $expected->render(), 'Custom error message is output.');
+ }
+
+ /**
+ * Asserts the array of Ajax commands contains the searched command.
+ *
+ * An AjaxResponse object stores an array of Ajax commands. This array
+ * sometimes includes commands automatically provided by the framework in
+ * addition to commands returned by a particular controller. During testing,
+ * we're usually interested that a particular command is present, and don't
+ * care whether other commands precede or follow the one we're interested in.
+ * Additionally, the command we're interested in may include additional data
+ * that we're not interested in. Therefore, this function simply asserts that
+ * one of the commands in $haystack contains all of the keys and values in
+ * $needle. Furthermore, if $needle contains a 'settings' key with an array
+ * value, we simply assert that all keys and values within that array are
+ * present in the command we're checking, and do not consider it a failure if
+ * the actual command contains additional settings that aren't part of
+ * $needle.
+ *
+ * @param $haystack
+ * An array of rendered Ajax commands returned by the server.
+ * @param $needle
+ * Array of info we're expecting in one of those commands.
+ */
+ protected function assertCommand($haystack, $needle) {
+ $found = FALSE;
+ foreach ($haystack as $command) {
+ // If the command has additional settings that we're not testing for, do
+ // not consider that a failure.
+ if (isset($command['settings']) && is_array($command['settings']) && isset($needle['settings']) && is_array($needle['settings'])) {
+ $command['settings'] = array_intersect_key($command['settings'], $needle['settings']);
+ }
+ // If the command has additional data that we're not testing for, do not
+ // consider that a failure. Also, == instead of ===, because we don't
+ // require the key/value pairs to be in any particular order
+ // (http://php.net/manual/language.operators.array.php).
+ if (array_intersect_key($command, $needle) == $needle) {
+ $found = TRUE;
+ break;
+ }
+ }
+ $this->assertTrue($found);
+ }
+
+ /**
+ * Requests a path or URL in drupal_ajax format and JSON-decodes the response.
+ *
+ * @param \Drupal\Core\Url|string $path
+ * Drupal path or URL to request from.
+ * @param array $options
+ * Array of URL options.
+ * @param array $headers
+ * Array of headers.
+ *
+ * @return array
+ * Decoded JSON.
+ */
+ protected function drupalGetAjax($path, array $options = [], array $headers = []) {
+ $headers[] = 'X-Requested-With: XMLHttpRequest';
+ if (!isset($options['query'][MainContentViewSubscriber::WRAPPER_FORMAT])) {
+ $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax';
+ }
+ return Json::decode($this->drupalGet($path, $options, $headers));
+ }
+
+}
<?php
-namespace Drupal\system\Tests\Form;
+namespace Drupal\Tests\system\Functional\Form;
use Drupal\Core\Form\FormState;
-use Drupal\simpletest\WebTestBase;
-use Drupal\Tests\system\Functional\Form\StubForm;
+use Drupal\Tests\BrowserTestBase;
/**
* Tests the tableselect form element for expected behavior.
*
* @group Form
*/
-class ElementsTableSelectTest extends WebTestBase {
+class ElementsTableSelectTest extends BrowserTestBase {
/**
* Modules to enable.
$this->drupalGet('form_test/tableselect/multiple-true');
- $this->assertNoText(t('Empty text.'), 'Empty text should not be displayed.');
+ $this->assertSession()->responseNotContains('Empty text.', 'Empty text should not be displayed.');
// Test for the presence of the Select all rows tableheader.
- $this->assertFieldByXPath('//th[@class="select-all"]', NULL, 'Presence of the "Select all" checkbox.');
+ $this->assertNotEmpty($this->xpath('//th[@class="select-all"]'), 'Presence of the "Select all" checkbox.');
$rows = ['row1', 'row2', 'row3'];
foreach ($rows as $row) {
- $this->assertFieldByXPath('//input[@type="checkbox"]', $row, format_string('Checkbox for value @row.', ['@row' => $row]));
- }
- }
-
- /**
- * Test the presence of ajax functionality for all options.
- */
- public function testAjax() {
- $rows = ['row1', 'row2', 'row3'];
- // Test checkboxes (#multiple == TRUE).
- foreach ($rows as $row) {
- $element = 'tableselect[' . $row . ']';
- $edit = [$element => TRUE];
- $result = $this->drupalPostAjaxForm('form_test/tableselect/multiple-true', $edit, $element);
- $this->assertFalse(empty($result), t('Ajax triggers on checkbox for @row.', ['@row' => $row]));
- }
- // Test radios (#multiple == FALSE).
- $element = 'tableselect';
- foreach ($rows as $row) {
- $edit = [$element => $row];
- $result = $this->drupalPostAjaxForm('form_test/tableselect/multiple-false', $edit, $element);
- $this->assertFalse(empty($result), t('Ajax triggers on radio for @row.', ['@row' => $row]));
+ $this->assertNotEmpty($this->xpath('//input[@type="checkbox"]', [$row]), "Checkbox for the value $row.");
}
}
public function testMultipleFalse() {
$this->drupalGet('form_test/tableselect/multiple-false');
- $this->assertNoText(t('Empty text.'), 'Empty text should not be displayed.');
+ $this->assertSession()->pageTextNotContains('Empty text.');
// Test for the absence of the Select all rows tableheader.
- $this->assertNoFieldByXPath('//th[@class="select-all"]', '', 'Absence of the "Select all" checkbox.');
+ $this->assertFalse($this->xpath('//th[@class="select-all"]'));
$rows = ['row1', 'row2', 'row3'];
foreach ($rows as $row) {
- $this->assertFieldByXPath('//input[@type="radio"]', $row, format_string('Radio button for value @row.', ['@row' => $row]));
+ $this->assertNotEmpty($this->xpath('//input[@type="radio"]', [$row], "Radio button value: $row"));
}
}
/**
* Tests the display when #colspan is set.
*/
- public function testTableselectColSpan() {
+ public function testTableSelectColSpan() {
$this->drupalGet('form_test/tableselect/colspan');
- $this->assertText(t('Three'), 'Presence of the third column');
- $this->assertNoText(t('Four'), 'Absence of a fourth column');
+ $this->assertSession()->pageTextContains('Three', 'Presence of the third column');
+ $this->assertSession()->pageTextNotContains('Four', 'Absence of a fourth column');
// There should be three labeled column headers and 1 for the input.
- $table_head = $this->xpath('//thead');
- $this->assertEqual(count($table_head[0]->tr->th), 4, 'There are four column headers');
+ $table_head = $this->xpath('//thead/tr/th');
+ $this->assertEquals(count($table_head), 4, 'There are four column headers');
- $table_body = $this->xpath('//tbody');
// The first two body rows should each have 5 table cells: One for the
// radio, one cell in the first column, one cell in the second column,
// and two cells in the third column which has colspan 2.
for ($i = 0; $i <= 1; $i++) {
- $this->assertEqual(count($table_body[0]->tr[$i]->td), 5, format_string('There are five cells in row @row.', ['@row' => $i]));
+ $this->assertEquals(count($this->xpath('//tbody/tr[' . ($i + 1) . ']/td')), 5, 'There are five cells in row ' . $i);
}
// The third row should have 3 cells, one for the radio, one spanning the
// first and second column, and a third in column 3 (which has colspan 3).
- $this->assertEqual(count($table_body[0]->tr[2]->td), 3, 'There are three cells in row 3.');
+ $this->assertEquals(count($this->xpath('//tbody/tr[3]/td')), 3, 'There are three cells in row 3.');
}
/**
*/
public function testEmptyText() {
$this->drupalGet('form_test/tableselect/empty-text');
- $this->assertText(t('Empty text.'), 'Empty text should be displayed.');
+ $this->assertSession()->pageTextContains('Empty text.', 'Empty text should be displayed.');
}
/**
$edit['tableselect[row1]'] = TRUE;
$this->drupalPostForm('form_test/tableselect/multiple-true', $edit, 'Submit');
- $this->assertText(t('Submitted: row1 = row1'), 'Checked checkbox row1');
- $this->assertText(t('Submitted: row2 = 0'), 'Unchecked checkbox row2.');
- $this->assertText(t('Submitted: row3 = 0'), 'Unchecked checkbox row3.');
+ $assert_session = $this->assertSession();
+ $assert_session->pageTextContains('Submitted: row1 = row1', 'Checked checkbox row1');
+ $assert_session->pageTextContains('Submitted: row2 = 0', 'Unchecked checkbox row2.');
+ $assert_session->pageTextContains('Submitted: row3 = 0', 'Unchecked checkbox row3.');
// Test a submission with multiple checkboxes checked.
$edit['tableselect[row1]'] = TRUE;
$edit['tableselect[row3]'] = TRUE;
$this->drupalPostForm('form_test/tableselect/multiple-true', $edit, 'Submit');
- $this->assertText(t('Submitted: row1 = row1'), 'Checked checkbox row1.');
- $this->assertText(t('Submitted: row2 = 0'), 'Unchecked checkbox row2.');
- $this->assertText(t('Submitted: row3 = row3'), 'Checked checkbox row3.');
+ $assert_session->pageTextContains('Submitted: row1 = row1', 'Checked checkbox row1.');
+ $assert_session->pageTextContains('Submitted: row2 = 0', 'Unchecked checkbox row2.');
+ $assert_session->pageTextContains('Submitted: row3 = row3', 'Checked checkbox row3.');
}
public function testMultipleFalseSubmit() {
$edit['tableselect'] = 'row1';
$this->drupalPostForm('form_test/tableselect/multiple-false', $edit, 'Submit');
- $this->assertText(t('Submitted: row1'), 'Selected radio button');
+ $this->assertSession()->pageTextContains('Submitted: row1', 'Selected radio button');
}
/**
public function testAdvancedSelect() {
// When #multiple = TRUE a Select all checkbox should be displayed by default.
$this->drupalGet('form_test/tableselect/advanced-select/multiple-true-default');
- $this->assertFieldByXPath('//th[@class="select-all"]', NULL, 'Display a "Select all" checkbox by default when #multiple is TRUE.');
+ $this->xpath('//th[@class="select-all"]');
// When #js_select is set to FALSE, a "Select all" checkbox should not be displayed.
$this->drupalGet('form_test/tableselect/advanced-select/multiple-true-no-advanced-select');
- $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #js_select is FALSE.');
+ $this->assertFalse($this->xpath('//th[@class="select-all"]'));
// A "Select all" checkbox never makes sense when #multiple = FALSE, regardless of the value of #js_select.
$this->drupalGet('form_test/tableselect/advanced-select/multiple-false-default');
- $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #multiple is FALSE.');
+ $this->assertFalse($this->xpath('//th[@class="select-all"]'));
$this->drupalGet('form_test/tableselect/advanced-select/multiple-false-advanced-select');
- $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #multiple is FALSE, even when #js_select is TRUE.');
+ $this->assertFalse($this->xpath('//th[@class="select-all"]'));
}
/**
--- /dev/null
+<?php
+
+namespace Drupal\Tests\system\Functional\Form;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests functionality of \Drupal\Core\Form\FormBuilderInterface::rebuildForm().
+ *
+ * @group Form
+ */
+class RebuildTest extends BrowserTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = ['node', 'form_test'];
+
+ /**
+ * A user for testing.
+ *
+ * @var \Drupal\user\UserInterface
+ */
+ protected $webUser;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+
+ $this->webUser = $this->drupalCreateUser(['access content']);
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Tests preservation of values.
+ */
+ public function testRebuildPreservesValues() {
+ $edit = [
+ 'checkbox_1_default_off' => TRUE,
+ 'checkbox_1_default_on' => FALSE,
+ 'text_1' => 'foo',
+ ];
+ $this->drupalPostForm('form-test/form-rebuild-preserve-values', $edit, 'Add more');
+
+ $assert_session = $this->assertSession();
+
+ // Verify that initial elements retained their submitted values.
+ $assert_session->checkboxChecked('edit-checkbox-1-default-off');
+ $assert_session->checkboxNotChecked('edit-checkbox-1-default-on');
+ $assert_session->fieldValueEquals('edit-text-1', 'foo');
+
+ // Verify that newly added elements were initialized with their default values.
+ $assert_session->checkboxChecked('edit-checkbox-2-default-on');
+ $assert_session->checkboxNotChecked('edit-checkbox-2-default-off');
+ $assert_session->fieldValueEquals('edit-text-2', 'DEFAULT 2');
+ }
+
+}
<?php
-namespace Drupal\system\Tests\Form;
+namespace Drupal\Tests\system\Functional\Form;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Core\Database\Database;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Tests\BrowserTestBase;
/**
* Tests a multistep form using form storage and makes sure validation and
*
* @group Form
*/
-class StorageTest extends WebTestBase {
+class StorageTest extends BrowserTestBase {
/**
* Modules to enable.
*/
public static $modules = ['form_test', 'dblog'];
+ /**
+ * {@inheritdoc}
+ */
protected function setUp() {
parent::setUp();
*/
public function testForm() {
$this->drupalGet('form_test/form-storage');
- $this->assertText('Form constructions: 1');
+
+ $assert_session = $this->assertSession();
+ $assert_session->pageTextContains('Form constructions: 1');
$edit = ['title' => 'new', 'value' => 'value_is_set'];
// Use form rebuilding triggered by a submit button.
$this->drupalPostForm(NULL, $edit, 'Continue submit');
- $this->assertText('Form constructions: 2');
- $this->assertText('Form constructions: 3');
+ $assert_session->pageTextContains('Form constructions: 2');
+ $assert_session->pageTextContains('Form constructions: 3');
// Reset the form to the values of the storage, using a form rebuild
// triggered by button of type button.
$this->drupalPostForm(NULL, ['title' => 'changed'], 'Reset');
- $this->assertFieldByName('title', 'new', 'Values have been reset.');
+ $assert_session->fieldValueEquals('title', 'new');
// After rebuilding, the form has been cached.
- $this->assertText('Form constructions: 4');
+ $assert_session->pageTextContains('Form constructions: 4');
$this->drupalPostForm(NULL, $edit, 'Save');
- $this->assertText('Form constructions: 4');
- $this->assertText('Title: new', 'The form storage has stored the values.');
+ $assert_session->pageTextContains('Form constructions: 4');
+ $assert_session->pageTextContains('Title: new', 'The form storage has stored the values.');
}
/**
*/
public function testFormCached() {
$this->drupalGet('form_test/form-storage', ['query' => ['cache' => 1]]);
- $this->assertText('Form constructions: 1');
+ $this->assertSession()->pageTextContains('Form constructions: 1');
$edit = ['title' => 'new', 'value' => 'value_is_set'];
// Use form rebuilding triggered by a submit button.
$this->drupalPostForm(NULL, $edit, 'Continue submit');
// The first one is for the building of the form.
- $this->assertText('Form constructions: 2');
+ $this->assertSession()->pageTextContains('Form constructions: 2');
// The second one is for the rebuilding of the form.
- $this->assertText('Form constructions: 3');
+ $this->assertSession()->pageTextContains('Form constructions: 3');
// Reset the form to the values of the storage, using a form rebuild
// triggered by button of type button.
$this->drupalPostForm(NULL, ['title' => 'changed'], 'Reset');
- $this->assertFieldByName('title', 'new', 'Values have been reset.');
- $this->assertText('Form constructions: 4');
+ $this->assertSession()->fieldValueEquals('title', 'new');
+ $this->assertSession()->pageTextContains('Form constructions: 4');
$this->drupalPostForm(NULL, $edit, 'Save');
- $this->assertText('Form constructions: 4');
- $this->assertText('Title: new', 'The form storage has stored the values.');
+ $this->assertSession()->pageTextContains('Form constructions: 4');
+ $this->assertSession()->pageTextContains('Title: new', 'The form storage has stored the values.');
}
/**
// validation error. Post again and verify that the rebuilt form contains
// the values of the updated form storage.
$this->drupalPostForm(NULL, ['title' => 'foo', 'value' => 'bar'], 'Save');
- $this->assertText("The thing has been changed.", 'The altered form storage value was updated in cache and taken over.');
+ $this->assertSession()->pageTextContains("The thing has been changed.", 'The altered form storage value was updated in cache and taken over.');
}
/**
// Request the form with 'cache' query parameter to enable form caching.
$this->drupalGet('form_test/form-storage', ['query' => ['cache' => 1, 'immutable' => 1]]);
$buildIdFields = $this->xpath('//input[@name="form_build_id"]');
- $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page');
- $buildId = (string) $buildIdFields[0]['value'];
+ $this->assertEquals(count($buildIdFields), 1, 'One form build id field on the page');
+ $buildId = $buildIdFields[0]->getValue();
// Trigger validation error by submitting an empty title.
$edit = ['title' => ''];
$this->drupalPostForm(NULL, $edit, 'Continue submit');
// Verify that the build-id did change.
- $this->assertNoFieldByName('form_build_id', $buildId, 'Build id changes when form validation fails');
+ $this->assertSession()->hiddenFieldValueNotEquals('form_build_id', $buildId);
// Retrieve the new build-id.
$buildIdFields = $this->xpath('//input[@name="form_build_id"]');
- $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page');
- $buildId = (string) $buildIdFields[0]['value'];
+ $this->assertEquals(count($buildIdFields), 1, 'One form build id field on the page');
+ $buildId = (string) $buildIdFields[0]->getValue();
// Trigger validation error by again submitting an empty title.
$edit = ['title' => ''];
$this->drupalPostForm(NULL, $edit, 'Continue submit');
// Verify that the build-id does not change the second time.
- $this->assertFieldByName('form_build_id', $buildId, 'Build id remains the same when form validation fails subsequently');
+ $this->assertSession()->hiddenFieldValueEquals('form_build_id', $buildId);
}
/**
public function testImmutableFormLegacyProtection() {
$this->drupalGet('form_test/form-storage', ['query' => ['cache' => 1, 'immutable' => 1]]);
$build_id_fields = $this->xpath('//input[@name="form_build_id"]');
- $this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page');
- $build_id = (string) $build_id_fields[0]['value'];
+ $this->assertEquals(count($build_id_fields), 1, 'One form build id field on the page');
+ $build_id = $build_id_fields[0]->getValue();
// Try to poison the form cache.
- $original = $this->drupalGetAjax('form-test/form-storage-legacy/' . $build_id);
- $this->assertEqual($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded');
- $this->assertNotEqual($original['form']['#build_id'], $build_id, 'New build_id was generated');
+ $response = $this->drupalGet('form-test/form-storage-legacy/' . $build_id, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']], ['X-Requested-With: XMLHttpRequest']);
+ $original = json_decode($response, TRUE);
+
+ $this->assertEquals($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded');
+ $this->assertNotEquals($original['form']['#build_id'], $build_id, 'New build_id was generated');
// Assert that a watchdog message was logged by
// \Drupal::formBuilder()->setCache().
- $status = (bool) db_query_range('SELECT 1 FROM {watchdog} WHERE message = :message', 0, 1, [':message' => 'Form build-id mismatch detected while attempting to store a form in the cache.']);
- $this->assert($status, 'A watchdog message was logged by \Drupal::formBuilder()->setCache');
+ $status = (bool) Database::getConnection()->queryRange('SELECT 1 FROM {watchdog} WHERE message = :message', 0, 1, [':message' => 'Form build-id mismatch detected while attempting to store a form in the cache.']);
+ $this->assertTrue($status, 'A watchdog message was logged by \Drupal::formBuilder()->setCache');
// Ensure that the form state was not poisoned by the preceding call.
- $original = $this->drupalGetAjax('form-test/form-storage-legacy/' . $build_id);
- $this->assertEqual($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded');
- $this->assertNotEqual($original['form']['#build_id'], $build_id, 'New build_id was generated');
- $this->assert(empty($original['form']['#poisoned']), 'Original form structure was preserved');
- $this->assert(empty($original['form_state']['poisoned']), 'Original form state was preserved');
+ $response = $this->drupalGet('form-test/form-storage-legacy/' . $build_id, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']], ['X-Requested-With: XMLHttpRequest']);
+ $original = json_decode($response, TRUE);
+ $this->assertEquals($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded');
+ $this->assertNotEquals($original['form']['#build_id'], $build_id, 'New build_id was generated');
+ $this->assertTrue(empty($original['form']['#poisoned']), 'Original form structure was preserved');
+ $this->assertTrue(empty($original['form_state']['poisoned']), 'Original form state was preserved');
}
}
$this->assertEquals('Drépal this is a very long test sentence to te <simpletest@example.com>', Unicode::mimeHeaderDecode($sent_message['headers']['From']), 'From header is correctly encoded.');
$this->assertFalse(isset($sent_message['headers']['Reply-to']), 'Message reply-to is not set if not specified.');
$this->assertFalse(isset($sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
+
+ // Test RFC-2822 rules are respected for 'display-name' component of
+ // 'From:' header. Specials characters are not allowed, so randomly add one
+ // of them to the site name and check the string is wrapped in quotes. Also
+ // hardcode some double-quotes and backslash to validate these are escaped
+ // properly too.
+ $specials = '()<>[]:;@\,."';
+ $site_name = 'Drupal' . $specials[rand(0, strlen($specials) - 1)] . ' "si\te"';
+ $this->config('system.site')->set('name', $site_name)->save();
+ // Send an email and check that the From-header contains the site name
+ // within double-quotes. Also make sure double-quotes and "\" are escaped.
+ \Drupal::service('plugin.manager.mail')->mail('simpletest', 'from_test', 'from_test@example.com', $language);
+ $captured_emails = \Drupal::state()->get('system.test_mail_collector');
+ $sent_message = end($captured_emails);
+ $escaped_site_name = str_replace(['\\', '"'], ['\\\\', '\\"'], $site_name);
+ $this->assertEquals('"' . $escaped_site_name . '" <simpletest@example.com>', $sent_message['headers']['From'], 'From header is correctly quoted.');
+
+ // Make sure display-name is not quoted nor escaped if part on an encoding.
+ $site_name = 'Drépal, "si\te"';
+ $this->config('system.site')->set('name', $site_name)->save();
+ // Send an email and check that the From-header contains the site name.
+ \Drupal::service('plugin.manager.mail')->mail('simpletest', 'from_test', 'from_test@example.com', $language);
+ $captured_emails = \Drupal::state()->get('system.test_mail_collector');
+ $sent_message = end($captured_emails);
+ $this->assertEquals('=?UTF-8?B?RHLDqXBhbCwgInNpXHRlIg==?= <simpletest@example.com>', $sent_message['headers']['From'], 'From header is correctly encoded.');
+ $this->assertEquals($site_name . ' <simpletest@example.com>', Unicode::mimeHeaderDecode($sent_message['headers']['From']), 'From header is correctly encoded.');
}
/**
<?php
-namespace Drupal\system\Tests\Routing;
+namespace Drupal\Tests\system\Functional\Routing;
use Drupal\Core\Cache\Cache;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Language\LanguageInterface;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Drupal\Core\Url;
*
* @group Routing
*/
-class RouterTest extends WebTestBase {
+class RouterTest extends BrowserTestBase {
/**
* Modules to enable.
$this->drupalGet('router_test/test1');
$this->assertRaw('test1', 'The correct string was returned because the route was successful.');
// Check expected headers from FinishResponseSubscriber.
- $headers = $this->drupalGetHeaders();
- $this->assertEqual($headers['x-ua-compatible'], 'IE=edge');
- $this->assertEqual($headers['content-language'], 'en');
- $this->assertEqual($headers['x-content-type-options'], 'nosniff');
- $this->assertEqual($headers['x-frame-options'], 'SAMEORIGIN');
+ $headers = $this->getSession()->getResponseHeaders();
+
+ $this->assertEquals($headers['X-UA-Compatible'], ['IE=edge']);
+ $this->assertEquals($headers['Content-language'], ['en']);
+ $this->assertEquals($headers['X-Content-Type-Options'], ['nosniff']);
+ $this->assertEquals($headers['X-Frame-Options'], ['SAMEORIGIN']);
$this->drupalGet('router_test/test2');
$this->assertRaw('test2', 'The correct string was returned because the route was successful.');
// Check expected headers from FinishResponseSubscriber.
$headers = $this->drupalGetHeaders();
- $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', $expected_cache_contexts));
- $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous http_response rendered');
+ $this->assertEqual($headers['X-Drupal-Cache-Contexts'], [implode(' ', $expected_cache_contexts)]);
+ $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['config:user.role.anonymous http_response rendered']);
// Confirm that the page wrapping is being added, so we're not getting a
// raw body returned.
$this->assertRaw('</html>', 'Page markup was found.');
// In some instances, the subrequest handling may get confused and render
// a page inception style. This test verifies that is not happening.
- $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+ $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
// Confirm that route-level access check's cacheability is applied to the
// X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags headers.
// 1. controller result: render array, globally cacheable route access.
$this->drupalGet('router_test/test18');
$headers = $this->drupalGetHeaders();
- $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url'])));
- $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo http_response rendered');
+ $this->assertEqual($headers['X-Drupal-Cache-Contexts'], [implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url']))]);
+ $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['config:user.role.anonymous foo http_response rendered']);
// 2. controller result: render array, per-role cacheable route access.
$this->drupalGet('router_test/test19');
$headers = $this->drupalGetHeaders();
- $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url', 'user.roles'])));
- $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo http_response rendered');
+ $this->assertEqual($headers['X-Drupal-Cache-Contexts'], [implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url', 'user.roles']))]);
+ $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['config:user.role.anonymous foo http_response rendered']);
// 3. controller result: Response object, globally cacheable route access.
$this->drupalGet('router_test/test1');
$headers = $this->drupalGetHeaders();
- $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
- $this->assertFalse(isset($headers['x-drupal-cache-tags']));
+ $this->assertFalse(isset($headers['X-Drupal-Cache-Contexts']));
+ $this->assertFalse(isset($headers['X-Drupal-Cache-Tags']));
// 4. controller result: Response object, per-role cacheable route access.
$this->drupalGet('router_test/test20');
$headers = $this->drupalGetHeaders();
- $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
- $this->assertFalse(isset($headers['x-drupal-cache-tags']));
+ $this->assertFalse(isset($headers['X-Drupal-Cache-Contexts']));
+ $this->assertFalse(isset($headers['X-Drupal-Cache-Tags']));
// 5. controller result: CacheableResponse object, globally cacheable route access.
$this->drupalGet('router_test/test21');
$headers = $this->drupalGetHeaders();
- $this->assertEqual($headers['x-drupal-cache-contexts'], '');
- $this->assertEqual($headers['x-drupal-cache-tags'], 'http_response');
+ $this->assertEqual($headers['X-Drupal-Cache-Contexts'], ['']);
+ $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['http_response']);
// 6. controller result: CacheableResponse object, per-role cacheable route access.
$this->drupalGet('router_test/test22');
$headers = $this->drupalGetHeaders();
- $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles');
- $this->assertEqual($headers['x-drupal-cache-tags'], 'http_response');
+ $this->assertEqual($headers['X-Drupal-Cache-Contexts'], ['user.roles']);
+ $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['http_response']);
// Finally, verify that the X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags
// headers are not sent when their container parameter is set to FALSE.
$this->drupalGet('router_test/test18');
$headers = $this->drupalGetHeaders();
- $this->assertTrue(isset($headers['x-drupal-cache-contexts']));
- $this->assertTrue(isset($headers['x-drupal-cache-tags']));
- $this->setHttpResponseDebugCacheabilityHeaders(FALSE);
+ $this->assertTrue(isset($headers['X-Drupal-Cache-Contexts']));
+ $this->assertTrue(isset($headers['X-Drupal-Cache-Tags']));
+ $this->setContainerParameter('http.response.debug_cacheability_headers', FALSE);
+ $this->rebuildContainer();
+ $this->resetAll();
$this->drupalGet('router_test/test18');
$headers = $this->drupalGetHeaders();
- $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
- $this->assertFalse(isset($headers['x-drupal-cache-tags']));
+ $this->assertFalse(isset($headers['X-Drupal-Cache-Contexts']));
+ $this->assertFalse(isset($headers['X-Drupal-Cache-Tags']));
}
/**
// In some instances, the subrequest handling may get confused and render
// a page inception style. This test verifies that is not happening.
- $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+ $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
}
/**
// In some instances, the subrequest handling may get confused and render
// a page inception style. This test verifies that is not happening.
- $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+ $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
}
/**
// In some instances, the subrequest handling may get confused and render
// a page inception style. This test verifies that is not happening.
- $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+ $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
}
/**
// In some instances, the subrequest handling may get confused and render
// a page inception style. This test verifies that is not happening.
- $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+ $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
}
/**
public function testControllerResolutionAjax() {
// This will fail with a JSON parse error if the request is not routed to
// The correct controller.
- $this->drupalGetAjax('/router_test/test10');
+ $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax';
+ $headers[] = 'X-Requested-With: XMLHttpRequest';
+ $this->drupalGet('/router_test/test10', $options, $headers);
$this->assertEqual($this->drupalGetHeader('Content-Type'), 'application/json', 'Correct mime content type was returned');
$request = $this->container->get('request_stack')->getCurrentRequest();
$url = $request->getUriForPath('//router_test/test1');
$this->drupalGet($url);
- $this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url);
$this->assertUrl($request->getUriForPath('/router_test/test1'));
// It should not matter how many leading slashes are used and query strings
// should be preserved.
$url = $request->getUriForPath('/////////////////////////////////////////////////router_test/test1') . '?qs=test';
$this->drupalGet($url);
- $this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url);
$this->assertUrl($request->getUriForPath('/router_test/test1') . '?qs=test');
// Ensure that external URLs in destination query params are not redirected
// to.
$url = $request->getUriForPath('/////////////////////////////////////////////////router_test/test1') . '?qs=test&destination=http://www.example.com%5c@drupal8alt.test';
$this->drupalGet($url);
- $this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url);
$this->assertUrl($request->getUriForPath('/router_test/test1') . '?qs=test');
}
<?php
-namespace Drupal\system\Tests\Session;
+namespace Drupal\Tests\system\Functional\Session;
use Drupal\Core\Url;
-use Drupal\basic_auth\Tests\BasicAuthTestTrait;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\basic_auth\Traits\BasicAuthTestTrait;
+use Drupal\Tests\BrowserTestBase;
/**
* Tests if sessions are correctly handled when a user authenticates.
*
* @group Session
*/
-class SessionAuthenticationTest extends WebTestBase {
+class SessionAuthenticationTest extends BrowserTestBase {
use BasicAuthTestTrait;
// Test that the route is not accessible as an anonymous user.
$this->drupalGet($protected_url);
+ $session = $this->getSession();
$this->assertResponse(401, 'An anonymous user cannot access a route protected with basic authentication.');
// We should be able to access the route with basic authentication.
- $this->basicAuthGet($protected_url, $this->user->getUsername(), $this->user->pass_raw);
+ $this->basicAuthGet($protected_url, $this->user->getAccountName(), $this->user->passRaw);
$this->assertResponse(200, 'A route protected with basic authentication can be accessed by an authenticated user.');
// Check that the correct user is logged in.
- $this->assertEqual($this->user->id(), json_decode($this->getRawContent())->user, 'The correct user is authenticated on a route with basic authentication.');
+ $this->assertEqual($this->user->id(), json_decode($session->getPage()->getContent())->user, 'The correct user is authenticated on a route with basic authentication.');
+ $session->restart();
// If we now try to access a page without basic authentication then we
// should no longer be logged in.
$this->drupalGet($unprotected_url);
$this->assertResponse(200, 'An unprotected route can be accessed without basic authentication.');
- $this->assertFalse(json_decode($this->getRawContent())->user, 'The user is no longer authenticated after visiting a page without basic authentication.');
+ $this->assertFalse(json_decode($session->getPage()->getContent())->user, 'The user is no longer authenticated after visiting a page without basic authentication.');
// If we access the protected page again without basic authentication we
// should get 401 Unauthorized.
$no_cookie_url = Url::fromRoute('session_test.get_session_basic_auth');
// A route that is authorized with standard cookie authentication.
- $cookie_url = '<front>';
+ $cookie_url = 'user/login';
// If we authenticate with a third party authentication system then no
// session cookie should be set, the third party system is responsible for
// sustaining the session.
- $this->basicAuthGet($no_cookie_url, $this->user->getUsername(), $this->user->pass_raw);
+ $this->basicAuthGet($no_cookie_url, $this->user->getAccountName(), $this->user->passRaw);
$this->assertResponse(200, 'The user is successfully authenticated using basic authentication.');
- $this->assertFalse($this->drupalGetHeader('set-cookie', TRUE), 'No cookie is set on a route protected with basic authentication.');
+ $this->assertEmpty($this->getSessionCookies());
+ // Mink stores some information in the session that breaks the next check if
+ // not reset.
+ $this->getSession()->restart();
// On the other hand, authenticating using Cookie sets a cookie.
- $edit = ['name' => $this->user->getUsername(), 'pass' => $this->user->pass_raw];
+ $this->drupalGet($cookie_url);
+ $this->assertEmpty($this->getSessionCookies());
+ $edit = ['name' => $this->user->getAccountName(), 'pass' => $this->user->passRaw];
$this->drupalPostForm($cookie_url, $edit, t('Log in'));
- $this->assertResponse(200, 'The user is successfully authenticated using cookie authentication.');
- $this->assertTrue($this->drupalGetHeader('set-cookie', TRUE), 'A cookie is set on a route protected with cookie authentication.');
+ $this->assertNotEmpty($this->getSessionCookies());
}
}
<?php
-namespace Drupal\system\Tests\Session;
+namespace Drupal\Tests\system\Functional\Session;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
/**
* Drupal session handling tests.
*
* @group Session
*/
-class SessionTest extends WebTestBase {
+class SessionTest extends BrowserTestBase {
/**
* Modules to enable.
$user = $this->drupalCreateUser();
// Enable sessions.
- $this->sessionReset($user->id());
+ $this->sessionReset();
- // Make sure the session cookie is set as HttpOnly.
- $this->drupalLogin($user);
+ // Make sure the session cookie is set as HttpOnly. We can only test this in
+ // the header, with the test setup
+ // \GuzzleHttp\Cookie\SetCookie::getHttpOnly() always returns FALSE.
+ // Start a new session by setting a message.
+ $this->drupalGet('session-test/set-message');
+ $this->assertSessionCookie(TRUE);
$this->assertTrue(preg_match('/HttpOnly/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as HttpOnly.');
- $this->drupalLogout();
// Verify that the session is regenerated if a module calls exit
// in hook_user_login().
$user->save();
$this->drupalGet('session-test/id');
$matches = [];
- preg_match('/\s*session_id:(.*)\n/', $this->getRawContent(), $matches);
+ preg_match('/\s*session_id:(.*)\n/', $this->getSession()->getPage()->getContent(), $matches);
$this->assertTrue(!empty($matches[1]), 'Found session ID before logging in.');
$original_session = $matches[1];
// We cannot use $this->drupalLogin($user); because we exit in
// session_test_user_login() which breaks a normal assertion.
$edit = [
- 'name' => $user->getUsername(),
- 'pass' => $user->pass_raw,
+ 'name' => $user->getAccountName(),
+ 'pass' => $user->passRaw,
];
$this->drupalPostForm('user/login', $edit, t('Log in'));
$this->drupalGet('user');
$this->drupalGet('session-test/id');
$matches = [];
- preg_match('/\s*session_id:(.*)\n/', $this->getRawContent(), $matches);
+ preg_match('/\s*session_id:(.*)\n/', $this->getSession()->getPage()->getContent(), $matches);
$this->assertTrue(!empty($matches[1]), 'Found session ID after logging in.');
$this->assertTrue($matches[1] != $original_session, 'Session ID changed after login.');
}
// properly, val_1 will still be set.
$value_2 = $this->randomMachineName();
$this->drupalGet('session-test/no-set/' . $value_2);
+ $session = $this->getSession();
$this->assertText($value_2, 'The session value was correctly passed to session-test/no-set.', 'Session');
$this->drupalGet('session-test/get');
$this->assertText($value_1, 'Session data is not saved for drupal_save_session(FALSE).', 'Session');
// Switch browser cookie to anonymous user, then back to user 1.
- $this->sessionReset();
- $this->sessionReset($user->id());
+ $session_cookie_name = $this->getSessionName();
+ $session_cookie_value = $session->getCookie($session_cookie_name);
+ $session->restart();
+ $this->initFrontPage();
+ // Session restart always resets all the cookies by design, so we need to
+ // add the old session cookie again.
+ $session->setCookie($session_cookie_name, $session_cookie_value);
+ $this->drupalGet('session-test/get');
$this->assertText($value_1, 'Session data persists through browser close.', 'Session');
+ $this->mink->setDefaultSessionName('default');
// Logout the user and make sure the stored value no longer persists.
$this->drupalLogout();
$this->assertEqual($times4->timestamp, $times3->timestamp, 'Sessions table was not updated.');
// Force updating of users and sessions table once per second.
- $this->settingsSet('session_write_interval', 0);
- // Write that value also into the test settings.php file.
$settings['settings']['session_write_interval'] = (object) [
'value' => 0,
'required' => TRUE,
// Send a blank sid in the session cookie, and the session should no longer
// be valid. Closing the curl handler will stop the previous session ID
// from persisting.
- $this->curlClose();
- $this->additionalCurlOptions[CURLOPT_COOKIE] = rawurlencode($this->getSessionName()) . '=;';
+ $this->mink->resetSessions();
$this->drupalGet('session-test/id-from-cookie');
$this->assertRaw("session_id:\n", 'Session ID is blank as sent from cookie header.');
// Assert that we have an anonymous session now.
/**
* Reset the cookie file so that it refers to the specified user.
- *
- * @param $uid
- * User id to set as the active session.
*/
- public function sessionReset($uid = 0) {
+ public function sessionReset() {
// Close the internal browser.
- $this->curlClose();
+ $this->mink->resetSessions();
$this->loggedInUser = FALSE;
// Change cookie file for user.
- $this->cookieFile = \Drupal::service('stream_wrapper_manager')->getViaScheme('temporary')->getDirectoryPath() . '/cookie.' . $uid . '.txt';
- $this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile;
- $this->additionalCurlOptions[CURLOPT_COOKIESESSION] = TRUE;
$this->drupalGet('session-test/get');
$this->assertResponse(200, 'Session test module is correctly enabled.', 'Session');
}
*/
public function assertSessionCookie($sent) {
if ($sent) {
- $this->assertNotNull($this->sessionId, 'Session cookie was sent.');
+ $this->assertNotEmpty($this->getSessionCookies()->count(), 'Session cookie was sent.');
}
else {
- $this->assertNull($this->sessionId, 'Session cookie was not sent.');
+ $this->assertEmpty($this->getSessionCookies()->count(), 'Session cookie was not sent.');
}
}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\system\Functional\Session;
+
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the stacked session handler functionality.
+ *
+ * @group Session
+ */
+class StackSessionHandlerIntegrationTest extends BrowserTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['session_test'];
+
+ /**
+ * Tests a request.
+ */
+ public function testRequest() {
+ $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax';
+ $headers[] = 'X-Requested-With: XMLHttpRequest';
+ $actual_trace = json_decode($this->drupalGet('session-test/trace-handler', $options, $headers));
+ $sessionId = $this->getSessionCookies()->getCookieByName($this->getSessionName())->getValue();
+ $expect_trace = [
+ ['BEGIN', 'test_argument', 'open'],
+ ['BEGIN', NULL, 'open'],
+ ['END', NULL, 'open'],
+ ['END', 'test_argument', 'open'],
+ ['BEGIN', 'test_argument', 'read', $sessionId],
+ ['BEGIN', NULL, 'read', $sessionId],
+ ['END', NULL, 'read', $sessionId],
+ ['END', 'test_argument', 'read', $sessionId],
+ ['BEGIN', 'test_argument', 'write', $sessionId],
+ ['BEGIN', NULL, 'write', $sessionId],
+ ['END', NULL, 'write', $sessionId],
+ ['END', 'test_argument', 'write', $sessionId],
+ ['BEGIN', 'test_argument', 'close'],
+ ['BEGIN', NULL, 'close'],
+ ['END', NULL, 'close'],
+ ['END', 'test_argument', 'close'],
+ ];
+ $this->assertEqual($expect_trace, $actual_trace);
+ }
+
+}
$config->set('css.preprocess', 0);
$config->save();
$this->drupalGet('theme-test/suggestion');
- $this->assertNoText('js.module.css', 'The theme\'s .info.yml file is able to override a module CSS file from being added to the page.');
+ // We add a "?" to the assertion, because drupalSettings may include
+ // information about the file; we only really care about whether it appears
+ // in a LINK or STYLE tag, for which Drupal always adds a query string for
+ // cache control.
+ $this->assertSession()->responseNotContains('js.module.css?');
// Also test with aggregation enabled, simply ensuring no PHP errors are
// triggered during drupal_build_css_cache() when a source file doesn't
--- /dev/null
+<?php
+
+namespace Drupal\Tests\system\FunctionalJavascript\Form;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the tableselect form element for expected behavior.
+ *
+ * @group Form
+ */
+class ElementsTableSelectTest extends WebDriverTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['form_test'];
+
+ /**
+ * Test the presence of ajax functionality for all options.
+ */
+ public function testAjax() {
+ // Test checkboxes (#multiple == TRUE).
+ $this->drupalGet('form_test/tableselect/multiple-true');
+ $session = $this->getSession();
+ $page = $session->getPage();
+ for ($i = 1; $i <= 3; $i++) {
+ $row = 'row' . $i;
+ $page->hasUncheckedField($row);
+ $page->checkField($row);
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ // Check current row and previous rows are checked.
+ for ($j = 1; $j <= $i; $j++) {
+ $other_row = 'row' . $j;
+ $page->hasCheckedField($other_row);
+ }
+ }
+
+ // Test radios (#multiple == FALSE).
+ $this->drupalGet('form_test/tableselect/multiple-false');
+ for ($i = 1; $i <= 3; $i++) {
+ $row = 'input[value="row' . $i . '"]';
+ $page->hasUncheckedField($row);
+ $this->click($row);
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $page->hasCheckedField($row);
+ // Check other rows are not checked
+ for ($j = 1; $j <= 3; $j++) {
+ if ($j == $i) {
+ continue;
+ }
+ $other_row = 'edit-tableselect-row' . $j;
+ $page->hasUncheckedField($other_row);
+ }
+ }
+ }
+
+}
<?php
-namespace Drupal\system\Tests\Form;
+namespace Drupal\Tests\system\FunctionalJavascript\Form;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
-use Drupal\simpletest\WebTestBase;
use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests functionality of \Drupal\Core\Form\FormBuilderInterface::rebuildForm().
* @group Form
* @todo Add tests for other aspects of form rebuilding.
*/
-class RebuildTest extends WebTestBase {
+class RebuildTest extends WebDriverTestBase {
/**
- * Modules to enable.
- *
- * @var array
+ * {@inheritdoc}
*/
- public static $modules = ['node', 'form_test'];
+ protected static $modules = ['node', 'form_test'];
/**
* A user for testing.
*/
protected $webUser;
+ /**
+ * {@inheritdoc}
+ */
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->webUser);
}
- /**
- * Tests preservation of values.
- */
- public function testRebuildPreservesValues() {
- $edit = [
- 'checkbox_1_default_off' => TRUE,
- 'checkbox_1_default_on' => FALSE,
- 'text_1' => 'foo',
- ];
- $this->drupalPostForm('form-test/form-rebuild-preserve-values', $edit, 'Add more');
-
- // Verify that initial elements retained their submitted values.
- $this->assertFieldChecked('edit-checkbox-1-default-off', 'A submitted checked checkbox retained its checked state during a rebuild.');
- $this->assertNoFieldChecked('edit-checkbox-1-default-on', 'A submitted unchecked checkbox retained its unchecked state during a rebuild.');
- $this->assertFieldById('edit-text-1', 'foo', 'A textfield retained its submitted value during a rebuild.');
-
- // Verify that newly added elements were initialized with their default values.
- $this->assertFieldChecked('edit-checkbox-2-default-on', 'A newly added checkbox was initialized with a default checked state.');
- $this->assertNoFieldChecked('edit-checkbox-2-default-off', 'A newly added checkbox was initialized with a default unchecked state.');
- $this->assertFieldById('edit-text-2', 'DEFAULT 2', 'A newly added textfield was initialized with its default value.');
- }
-
/**
* Tests that a form's action is retained after an Ajax submission.
*
* followed by a non-Ajax submission, which triggers a validation error.
*/
public function testPreserveFormActionAfterAJAX() {
+ $page = $this->getSession()->getPage();
// Create a multi-valued field for 'page' nodes to use for Ajax testing.
$field_name = 'field_ajax_test';
FieldStorageConfig::create([
'entity_type' => 'node',
'bundle' => 'page',
])->save();
+
+ // Also create a file field to test server side validation error.
+ $field_file_name = 'field_file_test';
+ FieldStorageConfig::create([
+ 'field_name' => $field_file_name,
+ 'entity_type' => 'node',
+ 'type' => 'file',
+ 'cardinality' => 1,
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => $field_file_name,
+ 'entity_type' => 'node',
+ 'bundle' => 'page',
+ 'label' => 'Test file',
+ 'required' => TRUE,
+ ])->save();
+
entity_get_form_display('node', 'page', 'default')
->setComponent($field_name, ['type' => 'text_textfield'])
+ ->setComponent($field_file_name, ['type' => 'file_generic'])
->save();
// Log in a user who can create 'page' nodes.
// submission and verify it worked by ensuring the updated page has two text
// field items in the field for which we just added an item.
$this->drupalGet('node/add/page');
- $this->drupalPostAjaxForm(NULL, [], ['field_ajax_test_add_more' => t('Add another item')], NULL, [], [], 'node-page-form');
- $this->assert(count($this->xpath('//div[contains(@class, "field--name-field-ajax-test")]//input[@type="text"]')) == 2, 'AJAX submission succeeded.');
+ $page->find('css', '[value="Add another item"]')->click();
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertTrue(count($this->xpath('//div[contains(@class, "field--name-field-ajax-test")]//input[@type="text"]')) == 2, 'AJAX submission succeeded.');
- // Submit the form with the non-Ajax "Save" button, leaving the title field
+ // Submit the form with the non-Ajax "Save" button, leaving the file field
// blank to trigger a validation error, and ensure that a validation error
// occurred, because this test is for testing what happens when a form is
// re-rendered without being re-built, which is what happens when there's
- // a validation error.
- $this->drupalPostForm(NULL, [], t('Save'));
- $this->assertText('Title field is required.', 'Non-AJAX submission correctly triggered a validation error.');
+ // a server side validation error.
+ $edit = [
+ 'title[0][value]' => $this->randomString(),
+ ];
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $this->assertSession()->pageTextContains('Test file field is required.', 'Non-AJAX submission correctly triggered a validation error.');
// Ensure that the form contains two items in the multi-valued field, so we
// know we're testing a form that was correctly retrieved from cache.
- $this->assert(count($this->xpath('//form[contains(@id, "node-page-form")]//div[contains(@class, "js-form-item-field-ajax-test")]//input[@type="text"]')) == 2, 'Form retained its state from cache.');
+ $this->assertTrue(count($this->xpath('//form[contains(@id, "node-page-form")]//div[contains(@class, "js-form-item-field-ajax-test")]//input[@type="text"]')) == 2, 'Form retained its state from cache.');
// Ensure that the form's action is correct.
$forms = $this->xpath('//form[contains(@class, "node-page-form")]');
- $this->assertEqual(1, count($forms));
+ $this->assertEquals(1, count($forms));
// Strip query params off the action before asserting.
- $url = parse_url($forms[0]['action'])['path'];
- $this->assertEqual(Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), $url);
+ $url = parse_url($forms[0]->getAttribute('action'))['path'];
+ $this->assertEquals(Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), $url);
}
}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\system\FunctionalJavascript\Form;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests that FAPI correctly determines the triggering element.
+ *
+ * @group Form
+ */
+class TriggeringElementTest extends WebDriverTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['form_test'];
+
+ /**
+ * Tests the the triggering element when no button information is included.
+ *
+ * Test the determination of the triggering element when no button
+ * information is included in the POST data, as is sometimes the case when
+ * the ENTER key is pressed in a textfield in Internet Explorer.
+ */
+ public function testNoButtonInfoInPost() {
+ $path = '/form-test/clicked-button';
+ $form_html_id = 'form-test-clicked-button';
+
+ // Ensure submitting a form with no buttons results in no triggering element
+ // and the form submit handler not running.
+ $this->drupalGet($path);
+
+ $assert_session = $this->assertSession();
+ $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+ $assert_session->pageTextContains('There is no clicked button.');
+ $assert_session->pageTextNotContains('Submit handler for form_test_clicked_button executed.');
+
+ // Ensure submitting a form with one or more submit buttons results in the
+ // triggering element being set to the first one the user has access to. An
+ // argument with 'r' in it indicates a restricted (#access=FALSE) button.
+ $this->drupalGet($path . '/s');
+ $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+ $assert_session->pageTextContains('The clicked button is button1.');
+ $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+
+ $this->drupalGet($path . '/s/s');
+ $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+ $assert_session->pageTextContains('The clicked button is button1.');
+ $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+
+ $this->drupalGet($path . '/rs/s');
+ $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+ $assert_session->pageTextContains('The clicked button is button2.');
+ $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+
+ // Ensure submitting a form with buttons of different types results in the
+ // triggering element being set to the first button, regardless of type. For
+ // the FAPI 'button' type, this should result in the submit handler not
+ // executing. The types are 's'(ubmit), 'b'(utton), and 'i'(mage_button).
+ $this->drupalGet($path . '/s/b/i');
+ $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+ $assert_session->pageTextContains('The clicked button is button1.');
+ $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+
+ $this->drupalGet($path . '/b/s/i');
+ $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+ $assert_session->pageTextContains('The clicked button is button1.');
+ $assert_session->pageTextNotContains('Submit handler for form_test_clicked_button executed.');
+
+ $this->drupalGet($path . '/i/s/b');
+ $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+ $assert_session->pageTextContains('The clicked button is button1.');
+ $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+ }
+
+ /**
+ * Tests attempts to bypass access control.
+ *
+ * Test that the triggering element does not get set to a button with
+ * #access=FALSE.
+ */
+ public function testAttemptAccessControlBypass() {
+ $path = 'form-test/clicked-button';
+ $form_html_id = 'form-test-clicked-button';
+
+ // Retrieve a form where 'button1' has #access=FALSE and 'button2' doesn't.
+ $this->drupalGet($path . '/rs/s');
+
+ // Submit the form with 'button1=button1' in the POST data, which someone
+ // trying to get around security safeguards could easily do. We have to do
+ // a little trickery here, to work around the safeguards in drupalPostForm()
+ // by renaming the text field and value that is in the form to 'button1',
+ // we can get the data we want into \Drupal::request()->request.
+ $page = $this->getSession()->getPage();
+ $input = $page->find('css', 'input[name="text"]');
+ $this->assertNotNull($input, 'text input located.');
+
+ $input->setValue('name', 'button1');
+ $input->setValue('value', 'button1');
+ $this->xpath('//form[@id="' . $form_html_id . '"]//input[@type="submit"]')[0]->click();
+
+ // Ensure that the triggering element was not set to the restricted button.
+ // Do this with both a negative and positive assertion, because negative
+ // assertions alone can be brittle. See testNoButtonInfoInPost() for why the
+ // triggering element gets set to 'button2'.
+ $this->assertSession()->pageTextNotContains('The clicked button is button1.');
+ $this->assertSession()->pageTextContains('The clicked button is button2.');
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\system\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the off-canvas dialog functionality.
+ *
+ * @group system
+ */
+class FrameworkTest extends WebDriverTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+ /**
+ * Tests that new JavaScript and CSS files are lazy-loaded on an AJAX request.
+ */
+ public function testLazyLoad() {
+ $expected = [
+ 'setting_name' => 'ajax_forms_test_lazy_load_form_submit',
+ 'setting_value' => 'executed',
+ 'library_1' => 'system/admin',
+ 'library_2' => 'system/drupal.system',
+ ];
+
+ // Get the base page.
+ $this->drupalGet('ajax_forms_test_lazy_load_form');
+ $page = $this->getSession()->getPage();
+ $assert = $this->assertSession();
+
+ $original_settings = $this->getDrupalSettings();
+ $original_libraries = explode(',', $original_settings['ajaxPageState']['libraries']);
+
+ // Verify that the base page doesn't have the settings and files that are to
+ // be lazy loaded as part of the next requests.
+ $this->assertTrue(!isset($original_settings[$expected['setting_name']]), format_string('Page originally lacks the %setting, as expected.', ['%setting' => $expected['setting_name']]));
+ $this->assertTrue(!in_array($expected['library_1'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', ['%library' => $expected['library_1']]));
+ $this->assertTrue(!in_array($expected['library_2'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', ['%library' => $expected['library_2']]));
+
+ // Submit the AJAX request without triggering files getting added.
+ $page->pressButton('Submit');
+ $assert->assertWaitOnAjaxRequest();
+ $new_settings = $this->getDrupalSettings();
+ $new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
+
+ // Verify the setting was not added when not expected.
+ $this->assertTrue(!isset($new_settings[$expected['setting_name']]), format_string('Page still lacks the %setting, as expected.', ['%setting' => $expected['setting_name']]));
+ $this->assertTrue(!in_array($expected['library_1'], $new_libraries), format_string('Page still lacks the %library library, as expected.', ['%library' => $expected['library_1']]));
+ $this->assertTrue(!in_array($expected['library_2'], $new_libraries), format_string('Page still lacks the %library library, as expected.', ['%library' => $expected['library_2']]));
+
+ // Submit the AJAX request and trigger adding files.
+ $page->checkField('add_files');
+ $page->pressButton('Submit');
+ $assert->assertWaitOnAjaxRequest();
+ $new_settings = $this->getDrupalSettings();
+ $new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
+
+ // Verify the expected setting was added, both to drupalSettings, and as
+ // the first AJAX command.
+ $this->assertIdentical($new_settings[$expected['setting_name']], $expected['setting_value'], format_string('Page now has the %setting.', ['%setting' => $expected['setting_name']]));
+
+ // Verify the expected CSS file was added, both to drupalSettings, and as
+ // the second AJAX command for inclusion into the HTML.
+ $this->assertTrue(in_array($expected['library_1'], $new_libraries), format_string('Page state now has the %library library.', ['%library' => $expected['library_1']]));
+
+ // Verify the expected JS file was added, both to drupalSettings, and as
+ // the third AJAX command for inclusion into the HTML. By testing for an
+ // exact HTML string containing the SCRIPT tag, we also ensure that
+ // unexpected JavaScript code, such as a jQuery.extend() that would
+ // potentially clobber rather than properly merge settings, didn't
+ // accidentally get added.
+ $this->assertTrue(in_array($expected['library_2'], $new_libraries), format_string('Page state now has the %library library.', ['%library' => $expected['library_2']]));
+ }
+
+ /**
+ * Tests that drupalSettings.currentPath is not updated on AJAX requests.
+ */
+ public function testCurrentPathChange() {
+ $this->drupalGet('ajax_forms_test_lazy_load_form');
+ $page = $this->getSession()->getPage();
+ $assert = $this->assertSession();
+
+ $old_settings = $this->getDrupalSettings();
+ $page->pressButton('Submit');
+ $assert->assertWaitOnAjaxRequest();
+ $new_settings = $this->getDrupalSettings();
+ $this->assertEquals($old_settings['path']['currentPath'], $new_settings['path']['currentPath']);
+ }
+
+ /**
+ * Tests that overridden CSS files are not added during lazy load.
+ */
+ public function testLazyLoadOverriddenCSS() {
+ // The test theme overrides js.module.css without an implementation,
+ // thereby removing it.
+ \Drupal::service('theme_handler')->install(['test_theme']);
+ $this->config('system.theme')
+ ->set('default', 'test_theme')
+ ->save();
+
+ // This gets the form, and does an Ajax submission on it.
+ $this->drupalGet('ajax_forms_test_lazy_load_form');
+ $page = $this->getSession()->getPage();
+ $assert = $this->assertSession();
+
+ $page->checkField('add_files');
+ $page->pressButton('Submit');
+ $assert->assertWaitOnAjaxRequest();
+
+ // Verify that the resulting HTML does not load the overridden CSS file.
+ // We add a "?" to the assertion, because drupalSettings may include
+ // information about the file; we only really care about whether it appears
+ // in a LINK or STYLE tag, for which Drupal always adds a query string for
+ // cache control.
+ $assert->responseNotContains('js.module.css?', 'Ajax lazy loading does not add overridden CSS files.');
+ }
+
+}
$result = $this->moduleInstaller()->uninstall([$non_dependency]);
$this->assertTrue($result, 'ModuleInstaller::uninstall() returns TRUE.');
$this->assertFalse($this->moduleHandler()->moduleExists($non_dependency));
- $this->assertEquals(drupal_get_installed_schema_version($non_dependency), SCHEMA_UNINSTALLED, "$dependency module was uninstalled.");
+ $this->assertEquals(drupal_get_installed_schema_version($non_dependency), SCHEMA_UNINSTALLED, "$non_dependency module was uninstalled.");
// Verify that the installation profile itself was not uninstalled.
$uninstalled_modules = \Drupal::state()->get('module_test.uninstall_order') ?: [];
- $this->assertContains($non_dependency, $uninstalled_modules, "$dependency module is in the list of uninstalled modules.");
+ $this->assertContains($non_dependency, $uninstalled_modules, "$non_dependency module is in the list of uninstalled modules.");
$this->assertNotContains($profile, $uninstalled_modules, 'The installation profile is not in the list of uninstalled modules.');
// Try uninstalling the required module.
core: 8.x
logo: images/logo2.svg
stylesheets-remove:
- - '@system/css/js.module.css'
+ - '@stable/css/system/components/js.module.css'
libraries:
- test_theme/global-styling
libraries-override:
sequence:
type: integer
label: 'fids'
+ multi_file:
+ type: sequence
+ label: 'Multiple file field with all file extensions'
+ sequence:
+ type: integer
+ label: 'fids'
],
];
+ $form['multi_file'] = [
+ '#type' => 'managed_file',
+ '#title' => t('Multiple file field with all file extensions'),
+ '#multiple' => TRUE,
+ '#default_value' => theme_get_setting('multi_file'),
+ '#upload_location' => 'public://test',
+ '#upload_validators' => [
+ 'file_validate_extensions' => [],
+ ],
+ ];
+
$form['#submit'][] = 'test_theme_settings_form_system_theme_settings_submit';
}
--- /dev/null
+<?php
+
+namespace Drupal\taxonomy\Plugin\migrate\source\d6;
+
+use Drupal\migrate\Row;
+
+/**
+ * Gets i18n taxonomy terms from source database.
+ *
+ * @MigrateSource(
+ * id = "d6_term_localized_translation",
+ * source_module = "i18ntaxonomy"
+ * )
+ */
+class TermLocalizedTranslation extends Term {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query() {
+ // Ideally, the query would return rows for each language for each taxonomy
+ // term with the translations for both the name and description or just the
+ // name translation or just the description translation. That query quickly
+ // became complex and would be difficult to maintain.
+ // Therefore, build a query based on i18nstrings table where each row has
+ // the translation for only one property, either name or description. The
+ // method prepareRow() is then used to obtain the translation for the other
+ // property.
+ $query = parent::query();
+ $query->addField('td', 'language', 'td.language');
+
+ // Add in the property, which is either name or description.
+ // Cast td.tid as char for PostgreSQL compatibility.
+ $query->leftJoin('i18n_strings', 'i18n', 'CAST(td.tid AS CHAR(255)) = i18n.objectid');
+ $query->isNotNull('i18n.lid');
+ $query->addField('i18n', 'lid');
+ $query->addField('i18n', 'property');
+
+ // Add in the translation for the property.
+ $query->innerJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
+ $query->addField('lt', 'language', 'lt.language');
+ $query->addField('lt', 'translation');
+ return $query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function prepareRow(Row $row) {
+ $language = $row->getSourceProperty('ltlanguage');
+ $row->setSourceProperty('language', $language);
+ $tid = $row->getSourceProperty('tid');
+
+ // If this row has been migrated it is a duplicate then skip it.
+ if ($this->idMap->lookupDestinationIds(['tid' => $tid, 'language' => $language])) {
+ return FALSE;
+ }
+
+ // Save the translation for this property.
+ $property = $row->getSourceProperty('property');
+ $row->setSourceProperty($property . '_translated', $row->getSourceProperty('translation'));
+
+ // Get the translation, if one exists, for the property not already in the
+ // row.
+ $other_property = ($property == 'name') ? 'description' : 'name';
+ $query = $this->select('i18n_strings', 'i18n')
+ ->fields('i18n', ['lid'])
+ ->condition('i18n.property', $other_property)
+ ->condition('i18n.objectid', $tid);
+ $query->leftJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
+ $query->condition('lt.language', $language);
+ $query->addField('lt', 'translation');
+ $results = $query->execute()->fetchAssoc();
+ $row->setSourceProperty($other_property . '_translated', $results['translation']);
+
+ parent::prepareRow($row);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fields() {
+ $fields = [
+ 'language' => $this->t('Language for this term.'),
+ 'name_translated' => $this->t('Term name translation.'),
+ 'description_translated' => $this->t('Term description translation.'),
+ ];
+ return parent::fields() + $fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIds() {
+ $ids['language']['type'] = 'string';
+ $ids['language']['alias'] = 'lt';
+ return parent::getIds() + $ids;
+ }
+
+}
$this->assertArrayHasKey($tid, $tree_terms, "Term $tid exists in vocabulary tree");
$tree_term = $tree_terms[$tid];
- $this->assertEquals($values['parent'], $tree_term->parents, "Term $tid has correct parents in vocabulary tree");
+
+ // PostgreSQL, MySQL and SQLite may not return the parent terms in the
+ // same order so sort before testing.
+ $expected_parents = $values['parent'];
+ sort($expected_parents);
+ $actual_parents = $tree_term->parents;
+ sort($actual_parents);
+ $this->assertEquals($expected_parents, $actual_parents, "Term $tid has correct parents in vocabulary tree");
}
}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\taxonomy\Kernel\Migrate\d6;
+
+use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\TermInterface;
+
+/**
+ * Tests migration of localized translated taxonomy terms.
+ *
+ * @group migrate_drupal_6
+ */
+class MigrateTermLocalizedTranslationTest extends MigrateDrupal6TestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'content_translation',
+ 'language',
+ 'menu_ui',
+ 'node',
+ 'taxonomy',
+ // Required for translation migrations.
+ 'migrate_drupal_multilingual',
+ ];
+
+ /**
+ * The cached taxonomy tree items, keyed by vid and tid.
+ *
+ * @var array
+ */
+ protected $treeData = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->installEntitySchema('taxonomy_term');
+ $this->installConfig(static::$modules);
+ $this->executeMigrations([
+ 'language',
+ 'd6_node_type',
+ 'd6_field',
+ 'd6_taxonomy_vocabulary',
+ 'd6_field_instance',
+ 'd6_taxonomy_term',
+ 'd6_taxonomy_term_localized_translation',
+ ]);
+ }
+
+ /**
+ * Validates a migrated term contains the expected values.
+ *
+ * @param int $id
+ * Entity ID to load and check.
+ * @param string $expected_language
+ * The language code for this term.
+ * @param string $expected_label
+ * The label the migrated entity should have.
+ * @param string $expected_vid
+ * The parent vocabulary the migrated entity should have.
+ * @param string $expected_description
+ * The description the migrated entity should have.
+ * @param string $expected_format
+ * The format the migrated entity should have.
+ * @param int $expected_weight
+ * The weight the migrated entity should have.
+ * @param array $expected_parents
+ * The parent terms the migrated entity should have.
+ * @param int $expected_field_integer_value
+ * The value the migrated entity field should have.
+ * @param int $expected_term_reference_tid
+ * The term reference ID the migrated entity field should have.
+ */
+ protected function assertEntity($id, $expected_language, $expected_label, $expected_vid, $expected_description = '', $expected_format = NULL, $expected_weight = 0, array $expected_parents = [], $expected_field_integer_value = NULL, $expected_term_reference_tid = NULL) {
+ /** @var \Drupal\taxonomy\TermInterface $entity */
+ $entity = Term::load($id);
+ $this->assertInstanceOf(TermInterface::class, $entity);
+ $this->assertSame($expected_language, $entity->language()->getId());
+ $this->assertSame($expected_label, $entity->label());
+ $this->assertSame($expected_vid, $entity->bundle());
+ $this->assertSame($expected_description, $entity->getDescription());
+ $this->assertSame($expected_format, $entity->getFormat());
+ $this->assertSame($expected_weight, $entity->getWeight());
+ $this->assertHierarchy($expected_vid, $id, $expected_parents);
+ }
+
+ /**
+ * Asserts that a term is present in the tree storage, with the right parents.
+ *
+ * @param string $vid
+ * Vocabulary ID.
+ * @param int $tid
+ * ID of the term to check.
+ * @param array $parent_ids
+ * The expected parent term IDs.
+ */
+ protected function assertHierarchy($vid, $tid, array $parent_ids) {
+ if (!isset($this->treeData[$vid])) {
+ $tree = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($vid);
+ $this->treeData[$vid] = [];
+ foreach ($tree as $item) {
+ $this->treeData[$vid][$item->tid] = $item;
+ }
+ }
+
+ $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");
+ }
+
+ /**
+ * Tests the Drupal 6 i18n localized taxonomy term to Drupal 8 migration.
+ */
+ public function testTranslatedLocalizedTaxonomyTerms() {
+ $this->assertEntity(14, 'en', 'Talos IV', 'vocabulary_name_much_longer_than', 'The home of Captain Christopher Pike.', NULL, '0', []);
+ $this->assertEntity(15, 'en', 'Vulcan', 'vocabulary_name_much_longer_than', NULL, NULL, '0', []);
+
+ /** @var \Drupal\taxonomy\TermInterface $entity */
+ $entity = Term::load(14);
+ $this->assertTrue($entity->hasTranslation('fr'));
+ $translation = $entity->getTranslation('fr');
+ $this->assertSame('fr - Talos IV', $translation->label());
+ $this->assertSame('fr - The home of Captain Christopher Pike.', $translation->getDescription());
+
+ $this->assertTrue($entity->hasTranslation('zu'));
+ $translation = $entity->getTranslation('zu');
+ $this->assertSame('Talos IV', $translation->label());
+ $this->assertSame('zu - The home of Captain Christopher Pike.', $translation->getDescription());
+
+ $entity = Term::load(15);
+ $this->assertFalse($entity->hasTranslation('fr'));
+ $this->assertTrue($entity->hasTranslation('zu'));
+ $translation = $entity->getTranslation('zu');
+ $this->assertSame('zu - Vulcan', $translation->label());
+ $this->assertSame('', $translation->getDescription());
+ }
+
+}
$field_id = 'node.story.field_vocabulary_3_i_2_';
$field = FieldConfig::load($field_id);
$this->assertFalse($field->isRequired(), 'Field is not required');
- $this->assertFalse($field->isTranslatable());
+ $this->assertTrue($field->isTranslatable());
// Tests that a vocabulary named like a D8 base field will be migrated and
// prefixed with 'field_' to avoid conflicts.
$field_type = FieldConfig::load('node.sponsor.field_type');
$this->assertInstanceOf(FieldConfig::class, $field_type);
- $this->assertFalse($field->isTranslatable());
+ $this->assertTrue($field->isTranslatable());
}
/**
--- /dev/null
+<?php
+
+namespace Drupal\Tests\taxonomy\Kernel\Plugin\migrate\source\d6;
+
+use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
+
+/**
+ * Tests D6 i18n term localized source plugin.
+ *
+ * @covers \Drupal\taxonomy\Plugin\migrate\source\d6\TermLocalizedTranslation
+ * @group taxonomy
+ */
+class TermLocalizedTranslationTest extends MigrateSqlSourceTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['taxonomy', 'migrate_drupal'];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function providerSource() {
+ $tests = [];
+
+ // The source data.
+ $tests[0]['source_data']['term_data'] = [
+ [
+ 'tid' => 1,
+ 'vid' => 5,
+ 'name' => 'name value 1',
+ 'description' => 'description value 1',
+ 'weight' => 0,
+ 'language' => NULL,
+ ],
+ [
+ 'tid' => 2,
+ 'vid' => 6,
+ 'name' => 'name value 2',
+ 'description' => 'description value 2',
+ 'weight' => 0,
+ 'language' => NULL,
+ ],
+ [
+ 'tid' => 3,
+ 'vid' => 6,
+ 'name' => 'name value 3',
+ 'description' => 'description value 3',
+ 'weight' => 0,
+ 'language' => NULL,
+ ],
+ [
+ 'tid' => 4,
+ 'vid' => 5,
+ 'name' => 'name value 4',
+ 'description' => 'description value 4',
+ 'weight' => 1,
+ 'language' => NULL,
+ ],
+ ];
+ $tests[0]['source_data']['term_hierarchy'] = [
+ [
+ 'tid' => 1,
+ 'parent' => 0,
+ ],
+ [
+ 'tid' => 2,
+ 'parent' => 0,
+ ],
+ [
+ 'tid' => 3,
+ 'parent' => 0,
+ ],
+ [
+ 'tid' => 4,
+ 'parent' => 1,
+ ],
+ ];
+ $tests[0]['source_data']['i18n_strings'] = [
+ [
+ 'lid' => 6,
+ 'objectid' => 1,
+ 'type' => 'term',
+ 'property' => 'name',
+ 'objectindex' => '1',
+ 'format' => 0,
+ ],
+ [
+ 'lid' => 7,
+ 'objectid' => 1,
+ 'type' => 'term',
+ 'property' => 'description',
+ 'objectindex' => '1',
+ 'format' => 0,
+ ],
+ [
+ 'lid' => 8,
+ 'objectid' => 3,
+ 'type' => 'term',
+ 'property' => 'name',
+ 'objectindex' => '3',
+ 'format' => 0,
+ ],
+ ];
+ $tests[0]['source_data']['locales_target'] = [
+ [
+ 'lid' => 6,
+ 'language' => 'fr',
+ 'translation' => 'fr - name value 1 translation',
+ 'plid' => 0,
+ 'plural' => 0,
+ 'i18n_status' => 0,
+ ],
+ [
+ 'lid' => 7,
+ 'language' => 'fr',
+ 'translation' => 'fr - description value 1 translation',
+ 'plid' => 0,
+ 'plural' => 0,
+ 'i18n_status' => 0,
+ ],
+ [
+ 'lid' => 8,
+ 'language' => 'zu',
+ 'translation' => 'zu - description value 2 translation',
+ 'plid' => 0,
+ 'plural' => 0,
+ 'i18n_status' => 0,
+ ],
+ ];
+
+ // The expected results.
+ $tests[0]['expected_data'] = [
+ [
+ 'tid' => 1,
+ 'vid' => 5,
+ 'name' => 'name value 1',
+ 'description' => 'description value 1',
+ 'weight' => 0,
+ 'parent' => [0],
+ 'property' => 'name',
+ 'language' => 'fr',
+ 'name_translated' => 'fr - name value 1 translation',
+ 'description_translated' => 'fr - description value 1 translation',
+ ],
+ [
+ 'tid' => 1,
+ 'vid' => 5,
+ 'name' => 'name value 1',
+ 'description' => 'description value 1',
+ 'weight' => 0,
+ 'parent' => [0],
+ 'property' => 'description',
+ 'language' => 'fr',
+ 'name_translated' => 'fr - name value 1 translation',
+ 'description_translated' => 'fr - description value 1 translation',
+ ],
+ [
+ 'tid' => 3,
+ 'vid' => 6,
+ 'name' => 'name value 3',
+ 'description' => 'description value 3',
+ 'weight' => 0,
+ 'parent' => [0],
+ 'property' => 'name',
+ 'language' => 'zu',
+ 'name_translated' => 'zu - description value 2 translation',
+ 'description_translated' => NULL,
+ ],
+ ];
+
+ $tests[0]['expected_count'] = NULL;
+ // Empty configuration will return terms for all vocabularies.
+ $tests[0]['configuration'] = [];
+
+ return $tests;
+ }
+
+}
*
* Called by $account->getDisplayName() to allow modules to alter the username
* that is displayed. Can be used to ensure user privacy in situations where
- * $account->getDisplayName() is too revealing.
+ * $account->getDisplayName() is too revealing. This hook is invoked both for
+ * user entities and the anonymous user session object.
*
* @param string|Drupal\Component\Render\MarkupInterface $name
* The username that is displayed for a user. If a hook implementation changes
* the implementation to ensure the user's name is escaped properly. String
* values will be autoescaped.
* @param \Drupal\Core\Session\AccountInterface $account
- * The user object on which the operation is being performed.
+ * The object on which the operation is being performed. This object may be a
+ * user entity. If the object is an implementation of UserInterface you can
+ * use instanceof operator before accessing user entity methods. For example:
+ * @code
+ * if ($account instanceof UserInterface) {
+ * // Access user entity methods.
+ * }
+ * @endcode
*
* @see \Drupal\Core\Session\AccountInterface::getDisplayName()
* @see sanitization
return ['views_label' => ''];
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getPreviewFallbackString() {
+ return $this->t('Placeholder for the "@view" views block', ['@view' => $this->view->storage->label()]);
+ }
+
/**
* {@inheritdoc}
*/
--- /dev/null
+langcode: en
+status: true
+dependencies: { }
+id: test_click_sort_ajax
+module: views
+description: ''
+tag: ''
+base_table: views_test_data
+base_field: nid
+core: '8'
+display:
+ default:
+ display_options:
+ use_ajax: true
+ fields:
+ id:
+ id: id
+ table: views_test_data
+ field: id
+ label: ID
+ plugin_id: numeric
+ name:
+ id: name
+ table: views_test_data
+ field: name
+ label: Name
+ plugin_id: string
+ created:
+ id: created
+ table: views_test_data
+ field: created
+ label: created
+ plugin_id: field
+ type: timestamp
+ settings:
+ date_format: medium
+ custom_date_format: ''
+ timezone: ''
+ access:
+ type: none
+ cache:
+ type: tag
+ style:
+ type: table
+ options:
+ info:
+ id:
+ sortable: true
+ default_sort_order: asc
+ name:
+ sortable: true
+ default_sort_order: desc
+ created:
+ sortable: false
+ display_plugin: default
+ display_title: Master
+ id: default
+ position: 0
+ page_1:
+ display_options:
+ path: test_click_sort
+ display_plugin: page
+ display_title: Page
+ id: page_1
+ position: 0
--- /dev/null
+langcode: en
+status: true
+dependencies:
+ module:
+ - node
+ - user
+id: test_mini_pager_ajax
+label: test_mini_pager
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+core: 8.x
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: null
+ display_options:
+ use_ajax: true
+ access:
+ type: perm
+ cache:
+ type: tag
+ query:
+ type: views_query
+ exposed_form:
+ type: basic
+ pager:
+ type: mini
+ options:
+ items_per_page: 3
+ offset: 0
+ id: 0
+ total_pages: null
+ tags:
+ previous: '‹‹ test'
+ next: '›› test'
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ style:
+ type: default
+ row:
+ type: 'entity:node'
+ options:
+ view_mode: teaser
+ fields:
+ title:
+ id: title
+ table: node_field_data
+ field: title
+ label: ''
+ alter:
+ alter_text: false
+ make_link: false
+ absolute: false
+ trim: false
+ word_boundary: false
+ ellipsis: false
+ strip_tags: false
+ html: false
+ hide_empty: false
+ empty_zero: false
+ plugin_id: field
+ entity_type: node
+ entity_field: title
+ filters: { }
+ sorts:
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ plugin_id: standard
+ order: ASC
+ entity_type: node
+ entity_field: nid
+ title: test_mini_pager
+ filter_groups:
+ operator: AND
+ groups: { }
--- /dev/null
+langcode: en
+status: true
+dependencies:
+ module:
+ - node
+id: test_pager_full_ajax
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+core: '8'
+display:
+ default:
+ display_options:
+ use_ajax: true
+ access:
+ type: none
+ cache:
+ type: tag
+ exposed_form:
+ type: basic
+ pager:
+ options:
+ id: 0
+ items_per_page: 5
+ offset: 0
+ type: full
+ style:
+ type: default
+ row:
+ type: 'entity:node'
+ display_plugin: default
+ display_title: Master
+ id: default
+ position: 0
--- /dev/null
+langcode: en
+status: true
+dependencies:
+ module:
+ - user
+id: test_user_path
+label: 'user break'
+module: views
+description: ''
+tag: ''
+base_table: users_field_data
+base_field: uid
+core: 8.x
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: perm
+ options:
+ perm: 'access user profiles'
+ cache:
+ type: tag
+ options: { }
+ query:
+ type: views_query
+ options:
+ disable_sql_rewrite: false
+ distinct: false
+ replica: false
+ query_comment: ''
+ query_tags: { }
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Toepassen
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sorteren op'
+ expose_sort_order: true
+ sort_asc_label: Oplopend
+ sort_desc_label: Aflopend
+ pager:
+ type: mini
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per pagina'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- Alle -'
+ offset: false
+ offset_label: Startpunt
+ tags:
+ previous: ‹‹
+ next: ››
+ style:
+ type: default
+ row:
+ type: fields
+ fields:
+ name:
+ id: name
+ table: users_field_data
+ field: name
+ entity_type: user
+ entity_field: name
+ label: ''
+ alter:
+ alter_text: false
+ make_link: false
+ absolute: false
+ trim: false
+ word_boundary: false
+ ellipsis: false
+ strip_tags: false
+ html: false
+ hide_empty: false
+ empty_zero: false
+ plugin_id: field
+ relationship: none
+ group_type: group
+ admin_label: ''
+ exclude: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_alter_empty: true
+ click_sort_column: value
+ type: user_name
+ settings: { }
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ filters:
+ status:
+ value: '1'
+ table: users_field_data
+ field: status
+ plugin_id: boolean
+ entity_type: user
+ entity_field: status
+ id: status
+ expose:
+ operator: ''
+ group: 1
+ sorts: { }
+ title: 'user break'
+ header: { }
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - user.permissions
+ tags: { }
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: user/%
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - user.permissions
+ tags: { }
--- /dev/null
+<?php
+
+namespace Drupal\Tests\views\Functional;
+
+/**
+ * Tests overriding user paths using wildcards.
+ *
+ * @group views
+ */
+class UserPathTest extends ViewTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['views', 'user'];
+
+ /**
+ * The test views to use.
+ *
+ * @var array
+ */
+ public static $testViews = ['test_user_path'];
+
+ /**
+ * Tests if the login page is still available when using a wildcard path.
+ */
+ public function testUserLoginPage() {
+ $this->drupalGet('user/login');
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+}
+++ /dev/null
-<?php
-
-namespace Drupal\views_ui\Tests;
-
-use Drupal\Component\Serialization\Json;
-use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
-
-/**
- * Tests the UI preview functionality.
- *
- * @group views_ui
- */
-class PreviewTest extends UITestBase {
-
- /**
- * Views used by this test.
- *
- * @var array
- */
- public static $testViews = ['test_preview', 'test_preview_error', 'test_pager_full', 'test_mini_pager', 'test_click_sort'];
-
- /**
- * Tests contextual links in the preview form.
- */
- public function testPreviewContextual() {
- \Drupal::service('module_installer')->install(['contextual']);
- $this->resetAll();
-
- $this->drupalGet('admin/structure/views/view/test_preview/edit');
- $this->assertResponse(200);
- $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
-
- $elements = $this->xpath('//div[@id="views-live-preview"]//ul[contains(@class, :ul-class)]/li[contains(@class, :li-class)]', [':ul-class' => 'contextual-links', ':li-class' => 'filter-add']);
- $this->assertEqual(count($elements), 1, 'The contextual link to add a new field is shown.');
-
- $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
-
- // Test that area text and exposed filters are present and rendered.
- $this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
- $this->assertText('Test header text', 'Rendered header text found');
- $this->assertText('Test footer text', 'Rendered footer text found.');
- $this->assertText('Test empty text', 'Rendered empty text found.');
- }
-
- /**
- * Tests arguments in the preview form.
- */
- public function testPreviewUI() {
- $this->drupalGet('admin/structure/views/view/test_preview/edit');
- $this->assertResponse(200);
-
- $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
-
- $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
- $this->assertEqual(count($elements), 5);
-
- // Filter just the first result.
- $this->drupalPostForm(NULL, $edit = ['view_args' => '1'], t('Update preview'));
-
- $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
- $this->assertEqual(count($elements), 1);
-
- // Filter for no results.
- $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
-
- $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
- $this->assertEqual(count($elements), 0);
-
- // Test that area text and exposed filters are present and rendered.
- $this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
- $this->assertText('Test header text', 'Rendered header text found');
- $this->assertText('Test footer text', 'Rendered footer text found.');
- $this->assertText('Test empty text', 'Rendered empty text found.');
-
- // Test feed preview.
- $view = [];
- $view['label'] = $this->randomMachineName(16);
- $view['id'] = strtolower($this->randomMachineName(16));
- $view['page[create]'] = 1;
- $view['page[title]'] = $this->randomMachineName(16);
- $view['page[path]'] = $this->randomMachineName(16);
- $view['page[feed]'] = 1;
- $view['page[feed_properties][path]'] = $this->randomMachineName(16);
- $this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
- $this->clickLink(t('Feed'));
- $this->drupalPostForm(NULL, [], t('Update preview'));
- $result = $this->xpath('//div[@id="views-live-preview"]/pre');
- $this->assertTrue(strpos($result[0], '<title>' . $view['page[title]'] . '</title>'), 'The Feed RSS preview was rendered.');
-
- // Test the non-default UI display options.
- // Statistics only, no query.
- $settings = \Drupal::configFactory()->getEditable('views.settings');
- $settings->set('ui.show.performance_statistics', TRUE)->save();
- $this->drupalGet('admin/structure/views/view/test_preview/edit');
- $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
- $this->assertText(t('Query build time'));
- $this->assertText(t('Query execute time'));
- $this->assertText(t('View render time'));
- $this->assertNoRaw('<strong>Query</strong>');
-
- // Statistics and query.
- $settings->set('ui.show.sql_query.enabled', TRUE)->save();
- $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
- $this->assertText(t('Query build time'));
- $this->assertText(t('Query execute time'));
- $this->assertText(t('View render time'));
- $this->assertRaw('<strong>Query</strong>');
- $query_string = <<<SQL
-SELECT views_test_data.name AS views_test_data_name
-FROM
-{views_test_data} views_test_data
-WHERE (views_test_data.id = '100')
-SQL;
- $this->assertEscaped($query_string);
-
- // Test that the statistics and query are rendered above the preview.
- $this->assertTrue(strpos($this->getRawContent(), 'views-query-info') < strpos($this->getRawContent(), 'view-test-preview'), 'Statistics shown above the preview.');
-
- // Test that statistics and query rendered below the preview.
- $settings->set('ui.show.sql_query.where', 'below')->save();
- $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
- $this->assertTrue(strpos($this->getRawContent(), 'view-test-preview') < strpos($this->getRawContent(), 'views-query-info'), 'Statistics shown below the preview.');
-
- // Test that the preview title isn't double escaped.
- $this->drupalPostForm("admin/structure/views/nojs/display/test_preview/default/title", $edit = ['title' => 'Double & escaped'], t('Apply'));
- $this->drupalPostForm(NULL, [], t('Update preview'));
- $elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', [':text' => t('Double & escaped')]);
- $this->assertEqual(1, count($elements));
- }
-
- /**
- * Tests the taxonomy term preview AJAX.
- *
- * This tests a specific regression in the taxonomy term view preview.
- *
- * @see https://www.drupal.org/node/2452659
- */
- public function testTaxonomyAJAX() {
- \Drupal::service('module_installer')->install(['taxonomy']);
- $this->getPreviewAJAX('taxonomy_term', 'page_1', 0);
- }
-
- /**
- * Tests pagers in the preview form.
- */
- public function testPreviewWithPagersUI() {
-
- // Create 11 nodes and make sure that everyone is returned.
- $this->drupalCreateContentType(['type' => 'page']);
- for ($i = 0; $i < 11; $i++) {
- $this->drupalCreateNode();
- }
-
- // Test Full Pager.
- $this->getPreviewAJAX('test_pager_full', 'default', 5);
-
- // Test that the pager is present and rendered.
- $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
- $this->assertTrue(!empty($elements), 'Full pager found.');
-
- // Verify elements and links to pages.
- // We expect to find 5 elements: current page == 1, links to pages 2 and
- // and 3, links to 'next >' and 'last >>' pages.
- $this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
- $this->assertTrue($elements[0]->a, 'Element for current page has link.');
-
- $this->assertClass($elements[1], 'pager__item', 'Element for page 2 has .pager__item class.');
- $this->assertTrue($elements[1]->a, 'Link to page 2 found.');
-
- $this->assertClass($elements[2], 'pager__item', 'Element for page 3 has .pager__item class.');
- $this->assertTrue($elements[2]->a, 'Link to page 3 found.');
-
- $this->assertClass($elements[3], 'pager__item--next', 'Element for next page has .pager__item--next class.');
- $this->assertTrue($elements[3]->a, 'Link to next page found.');
-
- $this->assertClass($elements[4], 'pager__item--last', 'Element for last page has .pager__item--last class.');
- $this->assertTrue($elements[4]->a, 'Link to last page found.');
-
- // Navigate to next page.
- $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--next']);
- $this->clickPreviewLinkAJAX($elements[0]['href'], 5);
-
- // Test that the pager is present and rendered.
- $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
- $this->assertTrue(!empty($elements), 'Full pager found.');
-
- // Verify elements and links to pages.
- // We expect to find 7 elements: links to '<< first' and '< previous'
- // pages, link to page 1, current page == 2, link to page 3 and links
- // to 'next >' and 'last >>' pages.
- $this->assertClass($elements[0], 'pager__item--first', 'Element for first page has .pager__item--first class.');
- $this->assertTrue($elements[0]->a, 'Link to first page found.');
-
- $this->assertClass($elements[1], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
- $this->assertTrue($elements[1]->a, 'Link to previous page found.');
-
- $this->assertClass($elements[2], 'pager__item', 'Element for page 1 has .pager__item class.');
- $this->assertTrue($elements[2]->a, 'Link to page 1 found.');
-
- $this->assertClass($elements[3], 'is-active', 'Element for current page has .is-active class.');
- $this->assertTrue($elements[3]->a, 'Element for current page has link.');
-
- $this->assertClass($elements[4], 'pager__item', 'Element for page 3 has .pager__item class.');
- $this->assertTrue($elements[4]->a, 'Link to page 3 found.');
-
- $this->assertClass($elements[5], 'pager__item--next', 'Element for next page has .pager__item--next class.');
- $this->assertTrue($elements[5]->a, 'Link to next page found.');
-
- $this->assertClass($elements[6], 'pager__item--last', 'Element for last page has .pager__item--last class.');
- $this->assertTrue($elements[6]->a, 'Link to last page found.');
-
- // Test Mini Pager.
- $this->getPreviewAJAX('test_mini_pager', 'default', 3);
-
- // Test that the pager is present and rendered.
- $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
- $this->assertTrue(!empty($elements), 'Mini pager found.');
-
- // Verify elements and links to pages.
- // We expect to find current pages element with no link, next page element
- // with a link, and not to find previous page element.
- $this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
-
- $this->assertClass($elements[1], 'pager__item--next', 'Element for next page has .pager__item--next class.');
- $this->assertTrue($elements[1]->a, 'Link to next page found.');
-
- // Navigate to next page.
- $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--next']);
- $this->clickPreviewLinkAJAX($elements[0]['href'], 3);
-
- // Test that the pager is present and rendered.
- $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
- $this->assertTrue(!empty($elements), 'Mini pager found.');
-
- // Verify elements and links to pages.
- // We expect to find 3 elements: previous page with a link, current
- // page with no link, and next page with a link.
- $this->assertClass($elements[0], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
- $this->assertTrue($elements[0]->a, 'Link to previous page found.');
-
- $this->assertClass($elements[1], 'is-active', 'Element for current page has .is-active class.');
- $this->assertFalse(isset($elements[1]->a), 'Element for current page has no link.');
-
- $this->assertClass($elements[2], 'pager__item--next', 'Element for next page has .pager__item--next class.');
- $this->assertTrue($elements[2]->a, 'Link to next page found.');
- }
-
- /**
- * Tests the additional information query info area.
- */
- public function testPreviewAdditionalInfo() {
- \Drupal::service('module_installer')->install(['views_ui_test']);
- $this->resetAll();
-
- $this->drupalGet('admin/structure/views/view/test_preview/edit');
- $this->assertResponse(200);
-
- $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
-
- // Check for implementation of hook_views_preview_info_alter().
- // @see views_ui_test.module
- $elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', [':text' => t('Test row count')]);
- $this->assertEqual(count($elements), 1, 'Views Query Preview Info area altered.');
- // Check that additional assets are attached.
- $this->assertTrue(strpos($this->getDrupalSettings()['ajaxPageState']['libraries'], 'views_ui_test/views_ui_test.test') !== FALSE, 'Attached library found.');
- $this->assertRaw('css/views_ui_test.test.css', 'Attached CSS asset found.');
- }
-
- /**
- * Tests view validation error messages in the preview.
- */
- public function testPreviewError() {
- $this->drupalGet('admin/structure/views/view/test_preview_error/edit');
- $this->assertResponse(200);
-
- $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
-
- $this->assertText('Unable to preview due to validation errors.', 'Preview error text found.');
- }
-
- /**
- * Tests the link to sort in the preview form.
- */
- public function testPreviewSortLink() {
-
- // Get the preview.
- $this->getPreviewAJAX('test_click_sort', 'page_1', 0);
-
- // Test that the header label is present.
- $elements = $this->xpath('//th[contains(@class, :class)]/a', [':class' => 'views-field views-field-name']);
- $this->assertTrue(!empty($elements), 'The header label is present.');
-
- // Verify link.
- $this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=desc', 0, 'The output URL is as expected.');
-
- // Click link to sort.
- $this->clickPreviewLinkAJAX($elements[0]['href'], 0);
-
- // Test that the header label is present.
- $elements = $this->xpath('//th[contains(@class, :class)]/a', [':class' => 'views-field views-field-name is-active']);
- $this->assertTrue(!empty($elements), 'The header label is present.');
-
- // Verify link.
- $this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=asc', 0, 'The output URL is as expected.');
- }
-
- /**
- * Get the preview form and force an AJAX preview update.
- *
- * @param string $view_name
- * The view to test.
- * @param string $panel_id
- * The view panel to test.
- * @param int $row_count
- * The expected number of rows in the preview.
- */
- protected function getPreviewAJAX($view_name, $panel_id, $row_count) {
- $this->drupalGet('admin/structure/views/view/' . $view_name . '/preview/' . $panel_id);
- $result = $this->drupalPostAjaxForm(NULL, [], ['op' => t('Update preview')]);
- $this->assertPreviewAJAX($result, $row_count);
- }
-
- /**
- * Mimic clicking on a preview link.
- *
- * @param string $url
- * The url to navigate to.
- * @param int $row_count
- * The expected number of rows in the preview.
- */
- protected function clickPreviewLinkAJAX($url, $row_count) {
- $content = $this->content;
- $drupal_settings = $this->drupalSettings;
- $ajax_settings = [
- 'wrapper' => 'views-preview-wrapper',
- 'method' => 'replaceWith',
- ];
- $url = $this->getAbsoluteUrl($url);
- $post = ['js' => 'true'] + $this->getAjaxPageStatePostData();
- $result = Json::decode($this->drupalPost($url, '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]));
- if (!empty($result)) {
- $this->drupalProcessAjaxResponse($content, $result, $ajax_settings, $drupal_settings);
- }
- $this->assertPreviewAJAX($result, $row_count);
- }
-
- /**
- * Assert that the AJAX response contains expected data.
- *
- * @param array $result
- * An array of AJAX commands.
- * @param int $row_count
- * The expected number of rows in the preview.
- */
- protected function assertPreviewAJAX($result, $row_count) {
- // Has AJAX callback replied with an insert command? If so, we can
- // assume that the page content was updated with AJAX returned data.
- $result_commands = [];
- foreach ($result as $command) {
- $result_commands[$command['command']] = $command;
- }
- $this->assertTrue(isset($result_commands['insert']), 'AJAX insert command received.');
-
- // Test if preview contains the expected number of rows.
- $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
- $this->assertEqual(count($elements), $row_count, 'Expected items found on page.');
- }
-
- /**
- * Asserts that an element has a given class.
- *
- * @param \SimpleXMLElement $element
- * The element to test.
- * @param string $class
- * The class to assert.
- * @param string $message
- * (optional) A verbose message to output.
- */
- protected function assertClass(\SimpleXMLElement $element, $class, $message = NULL) {
- if (!isset($message)) {
- $message = "Class .$class found.";
- }
- $this->assertTrue(strpos($element['class'], $class) !== FALSE, $message);
- }
-
-}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\views_ui\Functional;
+
+/**
+ * Tests the UI preview functionality.
+ *
+ * @group views_ui
+ */
+class PreviewTest extends UITestBase {
+
+ /**
+ * Views used by this test.
+ *
+ * @var array
+ */
+ public static $testViews = ['test_preview', 'test_preview_error', 'test_pager_full', 'test_mini_pager', 'test_click_sort'];
+
+ /**
+ * Tests contextual links in the preview form.
+ */
+ public function testPreviewContextual() {
+ \Drupal::service('module_installer')->install(['contextual']);
+ $this->resetAll();
+
+ $this->drupalGet('admin/structure/views/view/test_preview/edit');
+ $this->assertResponse(200);
+ $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
+
+ $elements = $this->xpath('//div[@id="views-live-preview"]//ul[contains(@class, :ul-class)]/li[contains(@class, :li-class)]', [':ul-class' => 'contextual-links', ':li-class' => 'filter-add']);
+ $this->assertEqual(count($elements), 1, 'The contextual link to add a new field is shown.');
+
+ $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+
+ // Test that area text and exposed filters are present and rendered.
+ $this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
+ $this->assertText('Test header text', 'Rendered header text found');
+ $this->assertText('Test footer text', 'Rendered footer text found.');
+ $this->assertText('Test empty text', 'Rendered empty text found.');
+ }
+
+ /**
+ * Tests arguments in the preview form.
+ */
+ public function testPreviewUI() {
+ $this->drupalGet('admin/structure/views/view/test_preview/edit');
+ $this->assertResponse(200);
+
+ $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
+
+ $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
+ $this->assertEqual(count($elements), 5);
+
+ // Filter just the first result.
+ $this->drupalPostForm(NULL, $edit = ['view_args' => '1'], t('Update preview'));
+
+ $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
+ $this->assertEqual(count($elements), 1);
+
+ // Filter for no results.
+ $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+
+ $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
+ $this->assertEqual(count($elements), 0);
+
+ // Test that area text and exposed filters are present and rendered.
+ $this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
+ $this->assertText('Test header text', 'Rendered header text found');
+ $this->assertText('Test footer text', 'Rendered footer text found.');
+ $this->assertText('Test empty text', 'Rendered empty text found.');
+
+ // Test feed preview.
+ $view = [];
+ $view['label'] = $this->randomMachineName(16);
+ $view['id'] = strtolower($this->randomMachineName(16));
+ $view['page[create]'] = 1;
+ $view['page[title]'] = $this->randomMachineName(16);
+ $view['page[path]'] = $this->randomMachineName(16);
+ $view['page[feed]'] = 1;
+ $view['page[feed_properties][path]'] = $this->randomMachineName(16);
+ $this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
+ $this->clickLink(t('Feed'));
+ $this->drupalPostForm(NULL, [], t('Update preview'));
+ $result = $this->xpath('//div[@id="views-live-preview"]/pre');
+ $this->assertTrue(strpos($result[0]->getText(), '<title>' . $view['page[title]'] . '</title>'), 'The Feed RSS preview was rendered.');
+
+ // Test the non-default UI display options.
+ // Statistics only, no query.
+ $settings = \Drupal::configFactory()->getEditable('views.settings');
+ $settings->set('ui.show.performance_statistics', TRUE)->save();
+ $this->drupalGet('admin/structure/views/view/test_preview/edit');
+ $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+ $this->assertText(t('Query build time'));
+ $this->assertText(t('Query execute time'));
+ $this->assertText(t('View render time'));
+ $this->assertNoRaw('<strong>Query</strong>');
+
+ // Statistics and query.
+ $settings->set('ui.show.sql_query.enabled', TRUE)->save();
+ $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+ $this->assertText(t('Query build time'));
+ $this->assertText(t('Query execute time'));
+ $this->assertText(t('View render time'));
+ $this->assertRaw('<strong>Query</strong>');
+ $query_string = <<<SQL
+SELECT views_test_data.name AS views_test_data_name
+FROM
+{views_test_data} views_test_data
+WHERE (views_test_data.id = '100')
+SQL;
+ $this->assertEscaped($query_string);
+
+ // Test that the statistics and query are rendered above the preview.
+ $this->assertTrue(strpos($this->getSession()->getPage()->getContent(), 'views-query-info') < strpos($this->getSession()->getPage()->getContent(), 'view-test-preview'), 'Statistics shown above the preview.');
+
+ // Test that statistics and query rendered below the preview.
+ $settings->set('ui.show.sql_query.where', 'below')->save();
+ $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+ $this->assertTrue(strpos($this->getSession()->getPage()->getContent(), 'view-test-preview') < strpos($this->getSession()->getPage()->getContent(), 'views-query-info'), 'Statistics shown below the preview.');
+
+ // Test that the preview title isn't double escaped.
+ $this->drupalPostForm("admin/structure/views/nojs/display/test_preview/default/title", $edit = ['title' => 'Double & escaped'], t('Apply'));
+ $this->drupalPostForm(NULL, [], t('Update preview'));
+ $elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', [':text' => 'Double & escaped']);
+ $this->assertEqual(1, count($elements));
+ }
+
+ /**
+ * Tests the additional information query info area.
+ */
+ public function testPreviewAdditionalInfo() {
+ \Drupal::service('module_installer')->install(['views_ui_test']);
+ $this->resetAll();
+
+ $this->drupalGet('admin/structure/views/view/test_preview/edit');
+ $this->assertResponse(200);
+
+ $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
+
+ // Check for implementation of hook_views_preview_info_alter().
+ // @see views_ui_test.module
+ $elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', [':text' => 'Test row count']);
+ $this->assertEqual(count($elements), 1, 'Views Query Preview Info area altered.');
+ // Check that additional assets are attached.
+ $this->assertTrue(strpos($this->getDrupalSettings()['ajaxPageState']['libraries'], 'views_ui_test/views_ui_test.test') !== FALSE, 'Attached library found.');
+ $this->assertRaw('css/views_ui_test.test.css', 'Attached CSS asset found.');
+ }
+
+ /**
+ * Tests view validation error messages in the preview.
+ */
+ public function testPreviewError() {
+ $this->drupalGet('admin/structure/views/view/test_preview_error/edit');
+ $this->assertResponse(200);
+
+ $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
+
+ $this->assertText('Unable to preview due to validation errors.', 'Preview error text found.');
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\views_ui\FunctionalJavascript;
+
+use Behat\Mink\Element\NodeElement;
+use Drupal\Core\Database\Database;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\views\Tests\ViewTestData;
+
+/**
+ * Tests the UI preview functionality.
+ *
+ * @group views_ui
+ */
+class PreviewTest extends WebDriverTestBase {
+
+ /**
+ * Views used by this test.
+ *
+ * @var array
+ */
+ public static $testViews = ['test_preview', 'test_pager_full_ajax', 'test_mini_pager_ajax', 'test_click_sort_ajax'];
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'node',
+ 'views',
+ 'views_ui',
+ 'views_test_config',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp();
+
+ ViewTestData::createTestViews(self::class, ['views_test_config']);
+
+ $this->enableViewsTestModule();
+
+ $admin_user = $this->drupalCreateUser([
+ 'administer site configuration',
+ 'administer views',
+ 'administer nodes',
+ 'access content overview',
+ ]);
+
+ // Disable automatic live preview to make the sequence of calls clearer.
+ \Drupal::configFactory()->getEditable('views.settings')->set('ui.always_live_preview', FALSE)->save();
+ $this->drupalLogin($admin_user);
+ }
+
+ /**
+ * Sets up the views_test_data.module.
+ *
+ * Because the schema of views_test_data.module is dependent on the test
+ * using it, it cannot be enabled normally.
+ */
+ protected function enableViewsTestModule() {
+ // Define the schema and views data variable before enabling the test module.
+ \Drupal::state()->set('views_test_data_schema', $this->schemaDefinition());
+ \Drupal::state()->set('views_test_data_views_data', $this->viewsData());
+
+ \Drupal::service('module_installer')->install(['views_test_data']);
+ $this->resetAll();
+ $this->rebuildContainer();
+ $this->container->get('module_handler')->reload();
+
+ // Load the test dataset.
+ $data_set = $this->dataSet();
+ $query = Database::getConnection()->insert('views_test_data')
+ ->fields(array_keys($data_set[0]));
+ foreach ($data_set as $record) {
+ $query->values($record);
+ }
+ $query->execute();
+ }
+
+ /**
+ * Returns the schema definition.
+ *
+ * @internal
+ */
+ protected function schemaDefinition() {
+ return ViewTestData::schemaDefinition();
+ }
+
+ /**
+ * Returns the views data definition.
+ */
+ protected function viewsData() {
+ return ViewTestData::viewsData();
+ }
+
+ /**
+ * Returns a very simple test dataset.
+ */
+ protected function dataSet() {
+ return ViewTestData::dataSet();
+ }
+
+ /**
+ * Tests the taxonomy term preview AJAX.
+ *
+ * This tests a specific regression in the taxonomy term view preview.
+ *
+ * @see https://www.drupal.org/node/2452659
+ */
+ public function testTaxonomyAJAX() {
+ \Drupal::service('module_installer')->install(['taxonomy']);
+ $this->getPreviewAJAX('taxonomy_term', 'page_1', 0);
+ }
+
+ /**
+ * Tests pagers in the preview form.
+ */
+ public function testPreviewWithPagersUI() {
+ // Create 11 nodes and make sure that everyone is returned.
+ $this->drupalCreateContentType(['type' => 'page']);
+ for ($i = 0; $i < 11; $i++) {
+ $this->drupalCreateNode();
+ }
+
+ // Test Full Pager.
+ $this->getPreviewAJAX('test_pager_full_ajax', 'default', 5);
+
+ // Test that the pager is present and rendered.
+ $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
+ $this->assertTrue(!empty($elements), 'Full pager found.');
+
+ // Verify elements and links to pages.
+ // We expect to find 5 elements: current page == 1, links to pages 2 and
+ // and 3, links to 'next >' and 'last >>' pages.
+ $this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
+ $this->assertTrue($elements[0]->find('css', 'a'), 'Element for current page has link.');
+
+ $this->assertClass($elements[1], 'pager__item', 'Element for page 2 has .pager__item class.');
+ $this->assertTrue($elements[1]->find('css', 'a'), 'Link to page 2 found.');
+
+ $this->assertClass($elements[2], 'pager__item', 'Element for page 3 has .pager__item class.');
+ $this->assertTrue($elements[2]->find('css', 'a'), 'Link to page 3 found.');
+
+ $this->assertClass($elements[3], 'pager__item--next', 'Element for next page has .pager__item--next class.');
+ $this->assertTrue($elements[3]->find('css', 'a'), 'Link to next page found.');
+
+ $this->assertClass($elements[4], 'pager__item--last', 'Element for last page has .pager__item--last class.');
+ $this->assertTrue($elements[4]->find('css', 'a'), 'Link to last page found.');
+
+ // Navigate to next page.
+ $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--next']);
+ $this->clickPreviewLinkAJAX($elements[0], 5);
+
+ // Test that the pager is present and rendered.
+ $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
+ $this->assertTrue(!empty($elements), 'Full pager found.');
+
+ // Verify elements and links to pages.
+ // We expect to find 7 elements: links to '<< first' and '< previous'
+ // pages, link to page 1, current page == 2, link to page 3 and links
+ // to 'next >' and 'last >>' pages.
+ $this->assertClass($elements[0], 'pager__item--first', 'Element for first page has .pager__item--first class.');
+ $this->assertTrue($elements[0]->find('css', 'a'), 'Link to first page found.');
+
+ $this->assertClass($elements[1], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
+ $this->assertTrue($elements[1]->find('css', 'a'), 'Link to previous page found.');
+
+ $this->assertClass($elements[2], 'pager__item', 'Element for page 1 has .pager__item class.');
+ $this->assertTrue($elements[2]->find('css', 'a'), 'Link to page 1 found.');
+
+ $this->assertClass($elements[3], 'is-active', 'Element for current page has .is-active class.');
+ $this->assertTrue($elements[3]->find('css', 'a'), 'Element for current page has link.');
+
+ $this->assertClass($elements[4], 'pager__item', 'Element for page 3 has .pager__item class.');
+ $this->assertTrue($elements[4]->find('css', 'a'), 'Link to page 3 found.');
+
+ $this->assertClass($elements[5], 'pager__item--next', 'Element for next page has .pager__item--next class.');
+ $this->assertTrue($elements[5]->find('css', 'a'), 'Link to next page found.');
+
+ $this->assertClass($elements[6], 'pager__item--last', 'Element for last page has .pager__item--last class.');
+ $this->assertTrue($elements[6]->find('css', 'a'), 'Link to last page found.');
+
+ // Test Mini Pager.
+ $this->getPreviewAJAX('test_mini_pager_ajax', 'default', 3);
+
+ // Test that the pager is present and rendered.
+ $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
+ $this->assertTrue(!empty($elements), 'Mini pager found.');
+
+ // Verify elements and links to pages.
+ // We expect to find current pages element with no link, next page element
+ // with a link, and not to find previous page element.
+ $this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
+
+ $this->assertClass($elements[1], 'pager__item--next', 'Element for next page has .pager__item--next class.');
+ $this->assertTrue($elements[1]->find('css', 'a'), 'Link to next page found.');
+
+ // Navigate to next page.
+ $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--next']);
+ $this->clickPreviewLinkAJAX($elements[0], 3);
+
+ // Test that the pager is present and rendered.
+ $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
+ $this->assertTrue(!empty($elements), 'Mini pager found.');
+
+ // Verify elements and links to pages.
+ // We expect to find 3 elements: previous page with a link, current
+ // page with no link, and next page with a link.
+ $this->assertClass($elements[0], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
+ $this->assertTrue($elements[0]->find('css', 'a'), 'Link to previous page found.');
+
+ $this->assertClass($elements[1], 'is-active', 'Element for current page has .is-active class.');
+ $this->assertEmpty($elements[1]->find('css', 'a'), 'Element for current page has no link.');
+
+ $this->assertClass($elements[2], 'pager__item--next', 'Element for next page has .pager__item--next class.');
+ $this->assertTrue($elements[2]->find('css', 'a'), 'Link to next page found.');
+ }
+
+ /**
+ * Tests the link to sort in the preview form.
+ */
+ public function testPreviewSortLink() {
+ // Get the preview.
+ $this->getPreviewAJAX('test_click_sort_ajax', 'page_1', 0);
+
+ // Test that the header label is present.
+ $elements = $this->xpath('//th[contains(@class, :class)]/a', [':class' => 'views-field views-field-name']);
+ $this->assertTrue(!empty($elements), 'The header label is present.');
+
+ // Verify link.
+ $this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=desc', 0, 'The output URL is as expected.');
+
+ // Click link to sort.
+ $elements[0]->click();
+ $sort_link = $this->assertSession()->waitForElement('xpath', '//th[contains(@class, \'views-field views-field-name is-active\')]/a');
+
+ $this->assertNotEmpty($sort_link);
+
+ // Verify link.
+ $this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=asc', 0, 'The output URL is as expected.');
+ }
+
+ /**
+ * Get the preview form and force an AJAX preview update.
+ *
+ * @param string $view_name
+ * The view to test.
+ * @param string $panel_id
+ * The view panel to test.
+ * @param int $row_count
+ * The expected number of rows in the preview.
+ */
+ protected function getPreviewAJAX($view_name, $panel_id, $row_count) {
+ $this->drupalGet('admin/structure/views/view/' . $view_name . '/edit/' . $panel_id);
+ $this->getSession()->getPage()->pressButton('Update preview');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertPreviewAJAX($row_count);
+ }
+
+ /**
+ * Click on a preview link.
+ *
+ * @param \Behat\Mink\Element\NodeElement $element
+ * The element to click.
+ * @param int $row_count
+ * The expected number of rows in the preview.
+ */
+ protected function clickPreviewLinkAJAX(NodeElement $element, $row_count) {
+ $element->click();
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertPreviewAJAX($row_count);
+ }
+
+ /**
+ * Assert that the preview contains expected data.
+ *
+ * @param int $row_count
+ * The expected number of rows in the preview.
+ */
+ protected function assertPreviewAJAX($row_count) {
+ $elements = $this->getSession()->getPage()->findAll('css', '.view-content .views-row');
+ $this->assertCount($row_count, $elements, 'Expected items found on page.');
+ }
+
+ /**
+ * Asserts that an element has a given class.
+ *
+ * @param \Behat\Mink\Element\NodeElement $element
+ * The element to test.
+ * @param string $class
+ * The class to assert.
+ * @param string $message
+ * (optional) A verbose message to output.
+ */
+ protected function assertClass(NodeElement $element, $class, $message = NULL) {
+ if (!isset($message)) {
+ $message = "Class .$class found.";
+ }
+ $this->assertTrue(strpos($element->getAttribute('class'), $class) !== FALSE, $message);
+ }
+
+}
}
}
+ /**
+ * Alters field plugin definitions.
+ *
+ * @param array[] $definitions
+ * An array of field plugin definitions.
+ *
+ * @see hook_field_info_alter()
+ */
+ public function fieldInfoAlter(&$definitions) {
+ if (isset($definitions['entity_reference'])) {
+ $definitions['entity_reference']['constraints']['EntityReferenceSupportedNewEntities'] = [];
+ }
+ }
+
}
--- /dev/null
+<?php
+
+namespace Drupal\workspaces\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * The entity reference supported new entities constraint.
+ *
+ * @Constraint(
+ * id = "EntityReferenceSupportedNewEntities",
+ * label = @Translation("Entity Reference Supported New Entities", context = "Validation"),
+ * )
+ */
+class EntityReferenceSupportedNewEntitiesConstraint extends Constraint {
+
+ /**
+ * The default violation message.
+ *
+ * @var string
+ */
+ public $message = '%collection_label can only be created in the default workspace.';
+
+}
--- /dev/null
+<?php
+
+namespace Drupal\workspaces\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\workspaces\WorkspaceManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Checks if new entities created for entity reference fields are supported.
+ */
+class EntityReferenceSupportedNewEntitiesConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+ /**
+ * The workspace manager.
+ *
+ * @var \Drupal\workspaces\WorkspaceManagerInterface
+ */
+ protected $workspaceManager;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Creates a new EntityReferenceSupportedNewEntitiesConstraintValidator instance.
+ */
+ public function __construct(WorkspaceManagerInterface $workspaceManager, EntityTypeManagerInterface $entityTypeManager) {
+ $this->workspaceManager = $workspaceManager;
+ $this->entityTypeManager = $entityTypeManager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('workspaces.manager'),
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value, Constraint $constraint) {
+ if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
+ return;
+ }
+
+ $target_entity_type_id = $value->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
+ $target_entity_type = $this->entityTypeManager->getDefinition($target_entity_type_id);
+
+ if ($value->hasNewEntity() && !$this->workspaceManager->isEntityTypeSupported($target_entity_type)) {
+ $this->context->addViolation($constraint->message, ['%collection_label' => $target_entity_type->getCollectionLabel()]);
+ }
+ }
+
+}
namespace Drupal\workspaces;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
*/
protected $entityTypeManager;
+ /**
+ * The entity memory cache service.
+ *
+ * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
+ */
+ protected $entityMemoryCache;
+
/**
* The current user.
*
* The request stack.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
+ * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $entity_memory_cache
+ * The entity memory cache service.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
* @param \Drupal\Core\State\StateInterface $state
* @param array $negotiator_ids
* The workspace negotiator service IDs.
*/
- public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, array $negotiator_ids) {
+ public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, MemoryCacheInterface $entity_memory_cache, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, array $negotiator_ids) {
$this->requestStack = $request_stack;
$this->entityTypeManager = $entity_type_manager;
+ $this->entityMemoryCache = $entity_memory_cache;
$this->currentUser = $current_user;
$this->state = $state;
$this->logger = $logger;
* {@inheritdoc}
*/
public function setActiveWorkspace(WorkspaceInterface $workspace) {
+ $this->doSwitchWorkspace($workspace);
+
+ // Set the workspace on the proper negotiator.
+ $request = $this->requestStack->getCurrentRequest();
+ foreach ($this->negotiatorIds as $negotiator_id) {
+ $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
+ if ($negotiator->applies($request)) {
+ $negotiator->setActiveWorkspace($workspace);
+ break;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Switches the current workspace.
+ *
+ * @param \Drupal\workspaces\WorkspaceInterface $workspace
+ * The workspace to set as active.
+ *
+ * @throws \Drupal\workspaces\WorkspaceAccessException
+ * Thrown when the current user doesn't have access to view the workspace.
+ */
+ protected function doSwitchWorkspace(WorkspaceInterface $workspace) {
// If the current user doesn't have access to view the workspace, they
// shouldn't be allowed to switch to it.
if (!$workspace->access('view') && !$workspace->isDefaultWorkspace()) {
$this->activeWorkspace = $workspace;
- // Set the workspace on the proper negotiator.
- $request = $this->requestStack->getCurrentRequest();
- foreach ($this->negotiatorIds as $negotiator_id) {
- $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
- if ($negotiator->applies($request)) {
- $negotiator->setActiveWorkspace($workspace);
- break;
- }
- }
+ // Clear the static entity cache for the supported entity types.
+ $cache_tags_to_invalidate = array_map(function ($entity_type_id) {
+ return 'entity.memory_cache:' . $entity_type_id;
+ }, array_keys($this->getSupportedEntityTypes()));
+ $this->entityMemoryCache->invalidateTags($cache_tags_to_invalidate);
+ }
- $supported_entity_types = $this->getSupportedEntityTypes();
- foreach ($supported_entity_types as $supported_entity_type) {
- $this->entityTypeManager->getStorage($supported_entity_type->id())->resetCache();
+ /**
+ * {@inheritdoc}
+ */
+ public function executeInWorkspace($workspace_id, callable $function) {
+ /** @var \Drupal\workspaces\WorkspaceInterface $workspace */
+ $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
+
+ if (!$workspace) {
+ throw new \InvalidArgumentException('The ' . $workspace_id . ' workspace does not exist.');
}
- return $this;
+ $previous_active_workspace = $this->getActiveWorkspace();
+ $this->doSwitchWorkspace($workspace);
+ $result = $function();
+ $this->doSwitchWorkspace($previous_active_workspace);
+
+ return $result;
}
/**
*/
public function setActiveWorkspace(WorkspaceInterface $workspace);
+ /**
+ * Executes the given callback function in the context of a workspace.
+ *
+ * @param string $workspace_id
+ * The ID of a workspace.
+ * @param callable $function
+ * The callback to be executed.
+ *
+ * @return mixed
+ * The callable's return value.
+ */
+ public function executeInWorkspace($workspace_id, callable $function);
+
/**
* Determines whether runtime entity operations should be altered.
*
/**
* {@inheritdoc}
*/
- public static $modules = ['workspaces'];
+ public static $modules = ['workspaces', 'toolbar'];
/**
* A test user.
'create workspace',
'edit own workspace',
'edit any workspace',
+ 'view own workspace',
+ 'access toolbar',
];
$this->editor1 = $this->drupalCreateUser($permissions);
$page->hasContent("This value is not valid");
}
+ /**
+ * Test that the toolbar correctly shows the active workspace.
+ */
+ public function testWorkspaceToolbar() {
+ $this->drupalLogin($this->editor1);
+
+ $this->drupalPostForm('/admin/config/workflow/workspaces/add', [
+ 'id' => 'test_workspace',
+ 'label' => 'Test workspace',
+ ], 'Save');
+
+ // Activate the test workspace.
+ $this->drupalPostForm('/admin/config/workflow/workspaces/manage/test_workspace/activate', [], 'Confirm');
+
+ $this->drupalGet('<front>');
+ $page = $this->getSession()->getPage();
+ // Toolbar should show the correct label.
+ $this->assertTrue($page->hasLink('Test workspace'));
+
+ // Change the workspace label.
+ $this->drupalPostForm('/admin/config/workflow/workspaces/manage/test_workspace/edit', [
+ 'label' => 'New name',
+ ], 'Save');
+
+ $this->drupalGet('<front>');
+ $page = $this->getSession()->getPage();
+ // Toolbar should show the new label.
+ $this->assertTrue($page->hasLink('New name'));
+ }
+
/**
* Test changing the owner of a workspace.
*/
--- /dev/null
+<?php
+
+namespace Drupal\Tests\workspaces\Kernel;
+
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\entity_test\Entity\EntityTestMulRevPub;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+
+/**
+ * @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityReferenceSupportedNewEntitiesConstraintValidator
+ * @group workspaces
+ */
+class EntityReferenceSupportedNewEntitiesConstraintValidatorTest extends KernelTestBase {
+
+ use UserCreationTrait;
+ use WorkspaceTestTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'system',
+ 'user',
+ 'workspaces',
+ 'entity_test',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->installEntitySchema('user');
+ $this->installSchema('system', ['sequences']);
+ $this->createUser();
+
+ $fields['supported_reference'] = BaseFieldDefinition::create('entity_reference')->setSetting('target_type', 'entity_test_mulrevpub');
+ $fields['unsupported_reference'] = BaseFieldDefinition::create('entity_reference')->setSetting('target_type', 'entity_test');
+ $this->container->get('state')->set('entity_test_mulrevpub.additional_base_field_definitions', $fields);
+
+ $this->installEntitySchema('entity_test_mulrevpub');
+ $this->initializeWorkspacesModule();
+ }
+
+ /**
+ * @covers ::validate
+ */
+ public function testNewEntitiesAllowedInDefaultWorkspace() {
+ $entity = EntityTestMulRevPub::create([
+ 'unsupported_reference' => [
+ 'entity' => EntityTest::create([]),
+ ],
+ 'supported_reference' => [
+ 'entity' => EntityTest::create([]),
+ ],
+ ]);
+ $this->assertCount(0, $entity->validate());
+ }
+
+ /**
+ * @covers ::validate
+ */
+ public function testNewEntitiesForbiddenInNonDefaultWorkspace() {
+ $this->switchToWorkspace('stage');
+ $entity = EntityTestMulRevPub::create([
+ 'unsupported_reference' => [
+ 'entity' => EntityTest::create([]),
+ ],
+ 'supported_reference' => [
+ 'entity' => EntityTestMulRevPub::create([]),
+ ],
+ ]);
+ $violations = $entity->validate();
+ $this->assertCount(1, $violations);
+ $this->assertEquals('<em class="placeholder">Test entity entities</em> can only be created in the default workspace.', $violations[0]->getMessage());
+ }
+
+}
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\views\Tests\ViewResultAssertionTrait;
use Drupal\views\Views;
-use Drupal\workspaces\Entity\Workspace;
/**
* Tests a complete deployment scenario across different workspaces.
use NodeCreationTrait;
use UserCreationTrait;
use ViewResultAssertionTrait;
+ use WorkspaceTestTrait;
/**
* The entity type manager.
*/
protected $entityTypeManager;
- /**
- * An array of test workspaces, keyed by workspace ID.
- *
- * @var \Drupal\workspaces\WorkspaceInterface[]
- */
- protected $workspaces = [];
-
/**
* Creation timestamp that should be incremented for each new entity.
*
$this->createNode(['title' => 'live - 2 - r2 - unpublished', 'created' => $this->createdTimestamp++, 'status' => FALSE]);
}
- /**
- * Enables the Workspaces module and creates two workspaces.
- */
- protected function initializeWorkspacesModule() {
- // Enable the Workspaces module here instead of the static::$modules array
- // so we can test it with default content.
- $this->enableModules(['workspaces']);
- $this->container = \Drupal::getContainer();
- $this->entityTypeManager = \Drupal::entityTypeManager();
-
- $this->installEntitySchema('workspace');
- $this->installEntitySchema('workspace_association');
-
- // Create two workspaces by default, 'live' and 'stage'.
- $this->workspaces['live'] = Workspace::create(['id' => 'live']);
- $this->workspaces['live']->save();
- $this->workspaces['stage'] = Workspace::create(['id' => 'stage']);
- $this->workspaces['stage']->save();
-
- $permissions = [
- 'administer nodes',
- 'create workspace',
- 'edit any workspace',
- 'view any workspace',
- ];
- $this->setCurrentUser($this->createUser($permissions));
- }
-
/**
* Tests various scenarios for creating and deploying content in workspaces.
*/
$entity_test->delete();
}
+ /**
+ * @covers \Drupal\workspaces\WorkspaceManager::executeInWorkspace
+ */
+ public function testExecuteInWorkspaceContext() {
+ $this->initializeWorkspacesModule();
+
+ // Create an entity in the default workspace.
+ $this->switchToWorkspace('live');
+ $node = $this->createNode([
+ 'title' => 'live node 1',
+ ]);
+ $node->save();
+
+ // Switch to the 'stage' workspace and change some values for the referenced
+ // entities.
+ $this->switchToWorkspace('stage');
+ $node->title->value = 'stage node 1';
+ $node->save();
+
+ // Switch back to the default workspace and run the baseline assertions.
+ $this->switchToWorkspace('live');
+ $storage = $this->entityTypeManager->getStorage('node');
+
+ $this->assertEquals('live', $this->workspaceManager->getActiveWorkspace()->id());
+
+ $live_node = $storage->loadUnchanged($node->id());
+ $this->assertEquals('live node 1', $live_node->title->value);
+
+ $result = $storage->getQuery()
+ ->condition('title', 'live node 1')
+ ->execute();
+ $this->assertEquals([$live_node->getRevisionId() => $node->id()], $result);
+
+ // Try the same assertions in the context of the 'stage' workspace.
+ $this->workspaceManager->executeInWorkspace('stage', function () use ($node, $storage) {
+ $this->assertEquals('stage', $this->workspaceManager->getActiveWorkspace()->id());
+
+ $stage_node = $storage->loadUnchanged($node->id());
+ $this->assertEquals('stage node 1', $stage_node->title->value);
+
+ $result = $storage->getQuery()
+ ->condition('title', 'stage node 1')
+ ->execute();
+ $this->assertEquals([$stage_node->getRevisionId() => $stage_node->id()], $result);
+ });
+
+ // Check that the 'stage' workspace was not persisted by the workspace
+ // manager.
+ $this->assertEquals('live', $this->workspaceManager->getActiveWorkspace()->id());
+ }
+
/**
* Checks entity load, entity queries and views results for a test scenario.
*
}
}
- /**
- * Sets a given workspace as active.
- *
- * @param string $workspace_id
- * The ID of the workspace to switch to.
- */
- protected function switchToWorkspace($workspace_id) {
- // Switch the test runner's context to the specified workspace.
- $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
- \Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
- }
-
/**
* Flattens the expectations array defined by testWorkspaces().
*
--- /dev/null
+<?php
+
+namespace Drupal\Tests\workspaces\Kernel;
+
+use Drupal\workspaces\Entity\Workspace;
+
+/**
+ * A trait with common workspaces testing functionality.
+ */
+trait WorkspaceTestTrait {
+
+ /**
+ * The workspaces manager.
+ *
+ * @var \Drupal\workspaces\WorkspaceManagerInterface
+ */
+ protected $workspaceManager;
+
+ /**
+ * An array of test workspaces, keyed by workspace ID.
+ *
+ * @var \Drupal\workspaces\WorkspaceInterface[]
+ */
+ protected $workspaces = [];
+
+ /**
+ * Enables the Workspaces module and creates two workspaces.
+ */
+ protected function initializeWorkspacesModule() {
+ // Enable the Workspaces module here instead of the static::$modules array
+ // so we can test it with default content.
+ $this->enableModules(['workspaces']);
+ $this->container = \Drupal::getContainer();
+ $this->entityTypeManager = \Drupal::entityTypeManager();
+ $this->workspaceManager = \Drupal::service('workspaces.manager');
+
+ $this->installEntitySchema('workspace');
+ $this->installEntitySchema('workspace_association');
+
+ // Create two workspaces by default, 'live' and 'stage'.
+ $this->workspaces['live'] = Workspace::create(['id' => 'live']);
+ $this->workspaces['live']->save();
+ $this->workspaces['stage'] = Workspace::create(['id' => 'stage']);
+ $this->workspaces['stage']->save();
+
+ $permissions = array_intersect([
+ 'administer nodes',
+ 'create workspace',
+ 'edit any workspace',
+ 'view any workspace',
+ ], array_keys($this->container->get('user.permissions')->getPermissions()));
+ $this->setCurrentUser($this->createUser($permissions));
+ }
+
+ /**
+ * Sets a given workspace as active.
+ *
+ * @param string $workspace_id
+ * The ID of the workspace to switch to.
+ */
+ protected function switchToWorkspace($workspace_id) {
+ // Switch the test runner's context to the specified workspace.
+ $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
+ \Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
+ }
+
+}
->formAlter($form, $form_state, $form_id);
}
+/**
+ * Implements hook_field_info_alter().
+ */
+function workspaces_field_info_alter(&$definitions) {
+ \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityTypeInfo::class)
+ ->fieldInfoAlter($definitions);
+}
+
/**
* Implements hook_entity_load().
*/
$current_user = \Drupal::currentUser();
if (!$current_user->hasPermission('administer workspaces')
- || !$current_user->hasPermission('view own workspace')
- || !$current_user->hasPermission('view any workspace')) {
+ && !$current_user->hasPermission('view own workspace')
+ && !$current_user->hasPermission('view any workspace')) {
return $items;
}
/** @var \Drupal\workspaces\WorkspaceInterface $active_workspace */
$active_workspace = \Drupal::service('workspaces.manager')->getActiveWorkspace();
- $items['workspace'] = [
+ $items['workspace'] += [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'link',
],
]),
],
+ '#cache' => ['tags' => $active_workspace->getCacheTags()],
],
'#wrapper_attributes' => [
'class' => ['workspaces-toolbar-tab'],
services:
workspaces.manager:
class: Drupal\workspaces\WorkspaceManager
- arguments: ['@request_stack', '@entity_type.manager', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver']
+ arguments: ['@request_stack', '@entity_type.manager', '@entity.memory_cache', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver']
tags:
- { name: service_id_collector, tag: workspace_negotiator }
workspaces.operation_factory:
expose:
label: ''
granularity: second
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: nid
+ plugin_id: standard
title: 'More featured articles'
header: { }
footer: { }
expose:
label: ''
granularity: second
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: nid
+ plugin_id: standard
title: Articles
header: { }
footer: { }
granularity: second
entity_type: node
entity_field: created
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: nid
+ plugin_id: standard
style:
type: default
options:
expose:
label: ''
granularity: second
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: nid
+ plugin_id: standard
title: Recipes
header: { }
footer: { }
background: url('../../../../images/svg/pointer--white.svg') no-repeat center center;
vertical-align: middle;
}
+[dir=rtl] .block-type-footer-promo-block .footer-promo-content a:after {
+ transform: rotate(180deg);
+}
@media screen and (min-width: 60rem) {
.block-type-footer-promo-block {
.search-form #edit-basic {
display: flex;
align-items: flex-end;
+ flex: 1 1;
}
.search-form .form-type-search {
margin: 0;
.search-form .search-help-link {
padding: 1.28rem;
margin: 0 1rem 1rem 0;
+ flex: 1 1;
}
.search-form #edit-advanced {
background: #fff;
border: 1px solid #fcece7;
padding: 1.28rem;
- margin: 0 0 1rem 0;
+ margin: 0 0 1rem 0; /* LTR */
+}
+/* Apply right margin to keep aligned with title and exposed filter. */
+[dir=rtl] .search-results li {
+ margin: 0 1rem 1rem 0;
}
.search-results .search-result__snippet {
*/
.search-form + h2 {
- margin: 0 14px 1rem;
+ margin: 0 1rem 1rem;
}
.search-block-form {
width: 20em;
max-width: calc(100vw - 6.25em);
height: auto;
- margin: 0 -2px 0 0;
+ margin: 0 -2px 0 0; /* LTR */
padding: 7px 8px 7px 32px;
color: #464646;
- border: 1px solid #dbdbdb;
- border-right: none;
+ border: 1px solid #dbdbdb; /* LTR */
+ border-right: none; /* LTR */
border-radius: 3px;
background: url(../../../../images/svg/search.svg) no-repeat 0.5em center #fff;
font-size: 0.875rem;
line-height: normal;
}
+[dir=rtl] .form-search {
+ margin: 0 0 0 -2px;
+ border-left: none;
+ border-right: 1px solid #dbdbdb;
+}
.form-search:focus {
- margin: 0 0 -2px -2px;
+ margin: 0 0 -2px -2px; /* LTR */
padding: 5px 8px 5px 32px;
outline: none;
}
+[dir=rtl] .form-search:focus {
+ margin: 0 -2px -2px 0;
+ background-position: 0.35em;
+ border: 3px solid #00836d;
+}
.form-search::placeholder {
opacity: 1;
.search-block-form .form-submit,
.search-form .form-submit {
/* Take off the border radius on the left side as it bumps into the search field */
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
+ border-top-left-radius: 0; /* LTR */
+ border-bottom-left-radius: 0; /* LTR */
+}
+[dir=rtl] .search-block-form .form-submit,
+[dir=rtl] .search-form .form-submit {
+ /* Take off the border radius on the left side as it bumps into the search field */
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
}
.search-block-form .form-submit:focus,
.search-form .form-submit:focus,
.search-form .form-submit:hover {
margin: 0;
- border-top-left-radius: 4px;
- border-bottom-left-radius: 4px;
+ border-top-left-radius: 4px; /* LTR */
+ border-bottom-left-radius: 4px; /* LTR */
+}
+/* Apply border radius to all corners to override LTR and RTL (normal state) changes. */
+[dir=rtl] .search-block-form .form-submit:focus,
+[dir=rtl] .search-block-form .form-submit:hover,
+[dir=rtl] .search-form .form-submit:focus,
+[dir=rtl] .search-form .form-submit:hover {
+ margin: 0;
+ border-radius: 4px;
}
align-items: center;
min-height: 40px;
margin-bottom: 0.96em;
- padding-left: 48px;
+ padding-left: 48px; /* LTR */
background-repeat: no-repeat;
- background-position: left center;
+ background-position: left center; /* LTR */
background-size: 40px 40px;
}
+[dir=rtl] .node--type-recipe.node--view-mode-full .field--name-field-preparation-time,
+[dir=rtl] .node--type-recipe.node--view-mode-full .field--name-field-cooking-time,
+[dir=rtl] .node--type-recipe.node--view-mode-full .field--name-field-number-of-servings,
+[dir=rtl] .node--type-recipe.node--view-mode-full .field--name-field-difficulty {
+ background-position: right center;
+ padding-left: 0;
+ padding-right: 48px;
+}
.node--type-recipe.node--view-mode-full .field--name-field-preparation-time {
background-image: url(../../../../images/svg/knife.svg);
}
.field--name-field-recipe-instruction ol > li {
position: relative;
min-height: 1.5em;
- padding: 0 0 0.6em 2.5em;
+ padding: 0 0 0.6em 2.5em; /* LTR */
list-style: none;
counter-increment: step-counter;
}
+[dir=rtl] .field--name-field-recipe-instruction ol > li {
+ padding: 0 2.5em 0.6em 0;
+}
.field--name-field-recipe-instruction ol > li::before {
position: absolute;
top: 0;
- left: 0;
+ left: 0; /* LTR */
content: counter(step-counter);
color: #cc2a00;
font-size: 1.5rem;
}
+[dir=rtl] .field--name-field-recipe-instruction ol > li::before {
+ right: 0;
+ left: auto;
+}
@media screen and (min-width: 30rem) { /* 480px */
.contact-form .form-actions .button {
- margin-left: 1em;
+ margin-left: 1em; /* LTR */
+ }
+ [dir=rtl] .contact-form .form-actions .button + .button {
+ margin-right: 1em;
+ margin-left: 0;
}
}
margin-top: 1.538em;
}
.messages__content {
- background: no-repeat 0 center;
+ background: no-repeat 0 center; /* LTR */
+}
+[dir=rtl] .messages__content {
+ background-position: right;
}
.messages--status {
background-color: #e6eee0;
margin: 0;
}
.messages__item {
- margin-left: 24px;
+ margin-left: 24px; /* LTR */
+}
+[dir=rtl] .messages__item {
+ margin-left: 0;
+ margin-right: 24px;
}
.messages__item + .messages__item {
margin-top: 0.769em;
.breadcrumb {
padding: 0.79rem 1.266rem;
}
+.breadcrumb li {
+ display: inline-block;
+}
/* Large */
@media screen and (min-width: 60rem) { /* 960px */
.breadcrumb {
.menu--account {
display: block;
flex: 0 1 50%;
- text-align: right;
+ text-align: right; /* LTR */
+ }
+ [dir="rtl"] .menu--account {
+ text-align: left;
}
}
line-height: 1.5;
}
.menu-account__item + .menu-account__item {
- margin-left: 1em;
+ margin-left: 1em; /* LTR */
+}
+[dir="rtl"] .menu-account__item + .menu-account__item {
+ margin-left: 0;
+ margin-right: 1em;
}
.menu-account__link,
.menu-account__link:hover {
margin-bottom: 0;
}
.menu-main__item + .menu-main__item {
- margin-left: 2.5em;
+ margin-left: 2.5em; /* LTR */
+ }
+ [dir="rtl"] .menu-main__item + .menu-main__item {
+ margin-left: 0;
+ margin-right: 2.5em;
}
}
padding: 0;
}
.tabs .tab {
- margin: 0;
+ margin: 0; /* LTR */
background-color: #e6eee0;
}
+[dir=rtl] .tabs .tab {
+ margin: 0;
+}
.tabs .tab.is-active {
background-color: #fff;
}
@media screen and (min-width: 60rem) { /* 960px */
.view-promoted-items--single > .view-content {
flex: 0 0 50%;
- margin-right: 14px;
+ margin-right: 14px; /* LTR */
display: flex;
}
+ [dir=rtl] .view-promoted-items--single > .view-content {
+ margin-right: 0;
+ margin-left: 14px;
+ }
}
.view-promoted-items--single > .view-content .views-row {
/* Large */
@media screen and (min-width: 60rem) { /* 960px */
.view-promoted-items--single > .attachment-after {
- margin-left: 14px;
+ margin-left: 14px; /* LTR */
display: flex;
}
+ [dir=rtl] .view-promoted-items--single > .attachment-after {
+ margin-left: 0;
+ margin-right: 14px;
+ }
}
/* Large */
--- /dev/null
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Various tests of AJAX behavior.
+ *
+ * @group Ajax
+ */
+class ElementValidationTest extends WebDriverTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['ajax_test', 'ajax_forms_test'];
+
+ /**
+ * Tries to post an Ajax change to a form that has a validated element.
+ *
+ * Drupal AJAX commands update the DOM echoing back the validated values in
+ * the form of messages that appear on the page.
+ */
+ public function testAjaxElementValidation() {
+ $this->drupalGet('ajax_validation_test');
+ $page = $this->getSession()->getPage();
+ $assert = $this->assertSession();
+
+ // Partially complete the form with a string.
+ $page->fillField('drivertext', 'some dumb text');
+ // Move focus away from this field to trigger AJAX.
+ $page->findField('spare_required_field')->focus();
+
+ // When the AJAX command updates the DOM a <ul> unsorted list
+ // "message__list" structure will appear on the page echoing back the
+ // "some dumb text" message.
+ $placeholder_text = $assert->waitForElement('css', "ul.messages__list li.messages__item em:contains('some dumb text')");
+ $this->assertNotNull($placeholder_text, 'A callback successfully echoed back a string.');
+
+ $this->drupalGet('ajax_validation_test');
+ // Partialy complete the form with a number.
+ $page->fillField('drivernumber', '12345');
+ $page->findField('spare_required_field')->focus();
+
+ // The AJAX request/resonse will complete successfully when a InsertCommand
+ // injects a message with a placeholder element into the DOM with the
+ // submitted number.
+ $placeholder_number = $assert->waitForElement('css', "ul.messages__list li.messages__item em:contains('12345')");
+ $this->assertNotNull($placeholder_number, 'A callback successfully echoed back a number.');
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests that form values are properly delivered to AJAX callbacks.
+ *
+ * @group Ajax
+ */
+class FormValuesTest extends WebDriverTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->drupalLogin($this->drupalCreateUser(['access content']));
+ }
+
+ /**
+ * Submits forms with select and checkbox elements via Ajax.
+ */
+ public function testSimpleAjaxFormValue() {
+
+ $this->drupalGet('ajax_forms_test_get_form');
+
+ $session = $this->getSession();
+ $assertSession = $this->assertSession();
+
+ // Verify form values of a select element.
+ foreach (['green', 'blue', 'red'] as $item) {
+ // Updating the field will trigger a AJAX request/response.
+ $session->getPage()->selectFieldOption('select', $item);
+
+ // The AJAX command in the response will update the DOM
+ $select = $assertSession->waitForElement('css', "div#ajax_selected_color:contains('$item')");
+ $this->assertNotNull($select, "DataCommand has updated the page with a value of $item.");
+ }
+
+ // Verify form values of a checkbox element.
+ $session->getPage()->checkField('checkbox');
+ $div0 = $this->assertSession()->waitForElement('css', "div#ajax_checkbox_value:contains('checked')");
+ $this->assertNotNull($div0, 'DataCommand updates the DOM as expected when a checkbox is selected');
+
+ $session->getPage()->uncheckField('checkbox');
+ $div1 = $this->assertSession()->waitForElement('css', "div#ajax_checkbox_value:contains('unchecked')");
+ $this->assertNotNull($div1, 'DataCommand updates the DOM as expected when a checkbox is de-selected');
+
+ // Verify that AJAX elements with invalid callbacks return error code 500.
+ // Ensure the test error log is empty before these tests.
+ $this->assertFalse(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is empty.');
+ // We don't need to check for the X-Drupal-Ajax-Token header with these
+ // invalid requests.
+ $this->assertAjaxHeader = FALSE;
+ foreach (['null', 'empty', 'nonexistent'] as $key) {
+ $element_name = 'select_' . $key . '_callback';
+ // Updating the field will trigger a AJAX request/response.
+ $session->getPage()->selectFieldOption($element_name, 'green');
+
+ // The select element is disabled as the AJAX request is issued.
+ $this->assertSession()->waitForElement('css', "select[name=\"$element_name\"]:disabled");
+
+ // The select element is enabled as the response is receieved.
+ $this->assertSession()->waitForElement('css', "select[name=\"$element_name\"]:enabled");
+ $this->assertTrue(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is not empty.');
+ $this->assertContains('"The specified #ajax callback is empty or not callable."', file_get_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'));
+ // The exceptions are expected. Do not interpret them as a test failure.
+ // Not using File API; a potential error must trigger a PHP warning.
+ unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
+ }
+ // We need to reload the page to kill any unfinished AJAX calls before
+ // tearDown() is called.
+ $this->drupalGet('ajax_forms_test_get_form');
+ }
+
+}
// Test drupalPostForm().
$edit = ['bananas' => 'red'];
+ // Submit the form using the button label.
$result = $this->drupalPostForm('form-test/object-builder', $edit, 'Save');
$this->assertSame($this->getSession()->getPage()->getContent(), $result);
$value = $config_factory->get('form_test.object')->get('bananas');
$value = $config_factory->get('form_test.object')->get('bananas');
$this->assertSame('', $value);
+ // Submit the form using the button id.
+ $edit = ['bananas' => 'blue'];
+ $result = $this->drupalPostForm('form-test/object-builder', $edit, 'edit-submit');
+ $this->assertSame($this->getSession()->getPage()->getContent(), $result);
+ $value = $config_factory->get('form_test.object')->get('bananas');
+ $this->assertSame('blue', $value);
+
+ // Submit the form using the button name.
+ $edit = ['bananas' => 'purple'];
+ $result = $this->drupalPostForm('form-test/object-builder', $edit, 'op');
+ $this->assertSame($this->getSession()->getPage()->getContent(), $result);
+ $value = $config_factory->get('form_test.object')->get('bananas');
+ $this->assertSame('purple', $value);
+
// Test drupalPostForm() with no-html response.
$values = Json::decode($this->drupalPostForm('form_test/form-state-values-clean', [], t('Submit')));
$this->assertTrue(1000, $values['beer']);
// it will try and create the drupal_install_test table as this is part of
// the standard database tests performed by the installer in
// Drupal\Core\Database\Install\Tasks.
- Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NULL)');
+ Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)');
parent::setUpSettings();
}
// it will try and create the drupal_install_test table as this is part of
// the standard database tests performed by the installer in
// Drupal\Core\Database\Install\Tasks.
- Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NULL)');
+ Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)');
parent::setUpSettings();
// Ensure that the error message translation is working.
$this->assertRaw('Beheben Sie alle Probleme unten, um die Installation fortzusetzen. Informationen zur Konfiguration der Datenbankserver finden Sie in der <a href="https://www.drupal.org/getting-started/install">Installationshandbuch</a>, oder kontaktieren Sie Ihren Hosting-Anbieter.');
- $this->assertRaw('<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl <em class="placeholder">CREATE TABLE {drupal_install_test} (id int NULL)</em> fehlgeschlagen.');
+ $this->assertRaw('<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl <em class="placeholder">CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)</em> fehlgeschlagen.');
// Now do it successfully.
Database::getConnection('default')->query('DROP TABLE {drupal_install_test}');
// Allow to skip entire config files.
if ($skipped_config[$config_name] === TRUE) {
- continue;
+ break;
}
// Allow to skip some specific lines of imported config files.
case 'Drupal\Component\Diff\Engine\DiffOpAdd':
// The _core property does not exist in the default config.
if ($op->closing[0] === '_core:') {
- continue;
+ break;
}
foreach ($op->closing as $closing) {
// The UUIDs don't exist in the default config.
if (strpos($closing, 'uuid: ') === 0) {
- continue;
+ break;
}
throw new \Exception($config_name . ': ' . var_export($op, TRUE));
}
public function providerTestCommonFormatSize() {
$kb = Bytes::KILOBYTE;
return [
+ ['0 bytes', 0],
['1 byte', 1],
+ ['-1 bytes', -1],
['2 bytes', 2],
+ ['-2 bytes', -2],
+ ['1023 bytes', $kb - 1],
['1 KB', $kb],
['1 MB', pow($kb, 2)],
['1 GB', pow($kb, 3)],
['1 EB', pow($kb, 6)],
['1 ZB', pow($kb, 7)],
['1 YB', pow($kb, 8)],
- // Rounded to 1 MB - not 1000 or 1024 kilobyte
+ ['1024 YB', pow($kb, 9)],
+ // Rounded to 1 MB - not 1000 or 1024 kilobytes
['1 MB', ($kb * $kb) - 1],
+ ['-1 MB', -(($kb * $kb) - 1)],
// Decimal Megabytes
['3.46 MB', 3623651],
+ ['3.77 GB', 4053371676],
// Decimal Petabytes
['59.72 PB', 67234178751368124],
// Decimal Yottabytes
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
$this->assertEqual($entity->field_test_text->processed, $target, format_string('%entity_type: Text is processed with the default filter.', ['%entity_type' => $entity_type]));
}
+ /**
+ * Tests explicit entity ID assignment.
+ */
+ public function testEntityIdAssignment() {
+ $entity_type = 'entity_test';
+ /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+ $storage = $this->container->get('entity_type.manager')->getStorage($entity_type);
+
+ // Check that an ID can be explicitly assigned on creation.
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ $entity = $this->createTestEntity($entity_type);
+ $entity_id = 3;
+ $entity->set('id', $entity_id);
+ $this->assertSame($entity_id, $entity->id());
+ $storage->save($entity);
+ $entity = $storage->loadUnchanged($entity->id());
+ $this->assertTrue($entity);
+
+ // Check that an explicitly-assigned ID is preserved on update.
+ $storage->save($entity);
+ $entity = $storage->loadUnchanged($entity->id());
+ $this->assertTrue($entity);
+
+ // Check that an ID cannot be explicitly assigned on update.
+ $this->setExpectedException(EntityStorageException::class);
+ $entity->set('id', $entity_id + 1);
+ $storage->save($entity);
+ }
+
}
}
foreach (array_reverse(str_split(decbin($i))) as $key => $bit) {
if ($bit) {
- list($field_name, $langcode, $values) = $units[$key];
- $entity->getTranslation($langcode)->{$field_name}[] = $values;
+ // @todo https://www.drupal.org/project/drupal/issues/3001920 Doing
+ // list($field_name, $langcode, $values) = $units[$key]; causes
+ // problems in PHP 7.3. Revert to better variable names once
+ // https://bugs.php.net/bug.php?id=76937 is fixed.
+ $entity->getTranslation($units[$key][1])->{$units[$key][0]}[] = $units[$key][2];
}
}
$entity->save();
$this->assertEquals($entity->id(), reset($result));
}
+ /**
+ * Tests entity queries with condition on the revision metadata keys.
+ */
+ public function testConditionOnRevisionMetadataKeys() {
+ $this->installModule('entity_test_revlog');
+ $this->installEntitySchema('entity_test_revlog');
+
+ /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+ $entity_type_manager = $this->container->get('entity_type.manager');
+ /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+ $entity_type = $entity_type_manager->getDefinition('entity_test_revlog');
+ /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+ $storage = $entity_type_manager->getStorage('entity_test_revlog');
+
+ $revision_created_timestamp = time();
+ $revision_created_field_name = $entity_type->getRevisionMetadataKey('revision_created');
+ $entity = $storage->create([
+ 'type' => 'entity_test',
+ $revision_created_field_name => $revision_created_timestamp,
+ ]);
+ $entity->save();
+
+ // Query only the default revision.
+ $result = $storage->getQuery()
+ ->condition($revision_created_field_name, $revision_created_timestamp)
+ ->execute();
+ $this->assertCount(1, $result);
+ $this->assertEquals($entity->id(), reset($result));
+
+ // Query all revisions.
+ $result = $storage->getQuery()
+ ->condition($revision_created_field_name, $revision_created_timestamp)
+ ->allRevisions()
+ ->execute();
+ $this->assertCount(1, $result);
+ $this->assertEquals($entity->id(), reset($result));
+ }
+
}
--- /dev/null
+<?php
+
+namespace Drupal\KernelTests\Core\Plugin;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\Core\Plugin\Context\EntityContext;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the interaction between entity context and typed data.
+ *
+ * @group Context
+ */
+class EntityContextTypedDataTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['entity_test'];
+
+ /**
+ * Tests that entity contexts wrapping a config entity can be validated.
+ */
+ public function testValidateConfigEntityContext() {
+ $display = EntityViewDisplay::create([
+ 'targetEntityType' => 'entity_test',
+ 'bundle' => 'entity_test',
+ 'mode' => 'default',
+ 'status' => TRUE,
+ ]);
+ $display->save();
+
+ $violations = EntityContext::fromEntity($display)->validate();
+ $this->assertCount(0, $violations);
+ }
+
+}
$this->render($messages);
$this->assertRaw('messages messages--error');
$this->assertRaw('messages messages--status');
+ // Tests display of only one type of messages.
+ \Drupal::messenger()->addError('An error occurred');
+ $messages = [
+ '#type' => 'status_messages',
+ '#display' => 'error',
+ ];
+ $this->render($messages);
+ $this->assertRaw('messages messages--error');
}
}
* The JSON decoded drupalSettings value from the current page.
*/
protected function getDrupalSettings() {
- $html = $this->getSession()->getPage()->getHtml();
+ $html = $this->getSession()->getPage()->getContent();
if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) {
return Json::decode($matches[1]);
}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\Component\Utility;
+
+use Drupal\Component\Utility\Mail;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Test mail helpers implemented in Mail component.
+ *
+ * @group Utility
+ *
+ * @coversDefaultClass \Drupal\Component\Utility\Mail
+ */
+class MailTest extends TestCase {
+
+ /**
+ * Tests RFC-2822 'display-name' formatter.
+ *
+ * @dataProvider providerTestDisplayName
+ * @covers ::formatDisplayName
+ */
+ public function testFormatDisplayName($string, $safe_display_name) {
+ $this->assertEquals($safe_display_name, Mail::formatDisplayName($string));
+ }
+
+ /**
+ * Data provider for testFormatDisplayName().
+ *
+ * @see testFormatDisplayName()
+ *
+ * @return array
+ * An array containing a string and its 'display-name' safe value.
+ */
+ public function providerTestDisplayName() {
+ return [
+ // Simple ASCII characters.
+ ['Test site', 'Test site'],
+ // ASCII with html entity.
+ ['Test & site', 'Test & site'],
+ // Non-ASCII characters.
+ ['Tést site', '=?UTF-8?B?VMOpc3Qgc2l0ZQ==?='],
+ // Non-ASCII with special characters.
+ ['Tést; site', '=?UTF-8?B?VMOpc3Q7IHNpdGU=?='],
+ // Non-ASCII with html entity.
+ ['Tést; site', '=?UTF-8?B?VMOpc3Q7IHNpdGU=?='],
+ // ASCII with special characters.
+ ['Test; site', '"Test; site"'],
+ // ASCII with special characters as html entity.
+ ['Test < site', '"Test < site"'],
+ // ASCII with special characters and '\'.
+ ['Test; \ "site"', '"Test; \\\\ \"site\""'],
+ // String already RFC-2822 compliant.
+ ['"Test; site"', '"Test; site"'],
+ // String already RFC-2822 compliant.
+ ['"Test; \\\\ \"site\""', '"Test; \\\\ \"site\""'],
+ ];
+ }
+
+}
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Layout\LayoutDefinition;
use Drupal\Core\Layout\LayoutPluginManager;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
use Prophecy\Argument;
$theme_a_path = vfsStream::url('root/themes/theme_a');
$layout_definition = $this->layoutPluginManager->getDefinition('theme_a_provided_layout');
$this->assertSame('theme_a_provided_layout', $layout_definition->id());
- $this->assertSame('2 column layout', $layout_definition->getLabel());
- $this->assertSame('Columns: 2', $layout_definition->getCategory());
+ $this->assertSame('2 column layout', (string) $layout_definition->getLabel());
+ $this->assertSame('Columns: 2', (string) $layout_definition->getCategory());
+ $this->assertSame('A theme provided layout', (string) $layout_definition->getDescription());
+ $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
+ $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
+ $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
$this->assertSame('twocol', $layout_definition->getTemplate());
$this->assertSame("$theme_a_path/templates", $layout_definition->getPath());
$this->assertSame('theme_a/twocol', $layout_definition->getLibrary());
$this->assertSame(LayoutDefault::class, $layout_definition->getClass());
$expected_regions = [
'left' => [
- 'label' => 'Left region',
+ 'label' => new TranslatableMarkup('Left region', [], ['context' => 'layout_region']),
],
'right' => [
- 'label' => 'Right region',
+ 'label' => new TranslatableMarkup('Right region', [], ['context' => 'layout_region']),
],
];
- $this->assertSame($expected_regions, $layout_definition->getRegions());
+ $regions = $layout_definition->getRegions();
+ $this->assertEquals($expected_regions, $regions);
+ $this->assertTrue($regions['left']['label'] instanceof TranslatableMarkup);
+ $this->assertTrue($regions['right']['label'] instanceof TranslatableMarkup);
$module_a_path = vfsStream::url('root/modules/module_a');
$layout_definition = $this->layoutPluginManager->getDefinition('module_a_provided_layout');
$this->assertSame('module_a_provided_layout', $layout_definition->id());
- $this->assertSame('1 column layout', $layout_definition->getLabel());
- $this->assertSame('Columns: 1', $layout_definition->getCategory());
+ $this->assertSame('1 column layout', (string) $layout_definition->getLabel());
+ $this->assertSame('Columns: 1', (string) $layout_definition->getCategory());
+ $this->assertSame('A module provided layout', (string) $layout_definition->getDescription());
+ $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
+ $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
+ $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
$this->assertSame(NULL, $layout_definition->getTemplate());
$this->assertSame("$module_a_path/layouts", $layout_definition->getPath());
$this->assertSame('module_a/onecol', $layout_definition->getLibrary());
$this->assertSame(LayoutDefault::class, $layout_definition->getClass());
$expected_regions = [
'top' => [
- 'label' => 'Top region',
+ 'label' => new TranslatableMarkup('Top region', [], ['context' => 'layout_region']),
],
'bottom' => [
- 'label' => 'Bottom region',
+ 'label' => new TranslatableMarkup('Bottom region', [], ['context' => 'layout_region']),
],
];
- $this->assertSame($expected_regions, $layout_definition->getRegions());
+ $regions = $layout_definition->getRegions();
+ $this->assertEquals($expected_regions, $regions);
+ $this->assertTrue($regions['top']['label'] instanceof TranslatableMarkup);
+ $this->assertTrue($regions['bottom']['label'] instanceof TranslatableMarkup);
$core_path = '/core/lib/Drupal/Core';
$layout_definition = $this->layoutPluginManager->getDefinition('plugin_provided_layout');
$this->assertSame('plugin_provided_layout', $layout_definition->id());
$this->assertEquals('Layout plugin', $layout_definition->getLabel());
$this->assertEquals('Columns: 1', $layout_definition->getCategory());
+ $this->assertEquals('Test layout', $layout_definition->getDescription());
+ $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
+ $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
+ $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
$this->assertSame('plugin-provided-layout', $layout_definition->getTemplate());
$this->assertSame($core_path, $layout_definition->getPath());
$this->assertSame(NULL, $layout_definition->getLibrary());
$this->assertSame('Drupal\Core\Plugin\Layout\TestLayout', $layout_definition->getClass());
$expected_regions = [
'main' => [
- 'label' => 'Main Region',
+ 'label' => new TranslatableMarkup('Main Region', [], ['context' => 'layout_region']),
],
];
- $this->assertEquals($expected_regions, $layout_definition->getRegions());
+ $regions = $layout_definition->getRegions();
+ $this->assertEquals($expected_regions, $regions);
+ $this->assertTrue($regions['main']['label'] instanceof TranslatableMarkup);
}
/**
module_a_provided_layout:
label: 1 column layout
category: 'Columns: 1'
+ description: 'A module provided layout'
theme_hook: onecol
path: layouts
library: module_a/onecol
class: '\Drupal\Core\Layout\LayoutDefault'
label: 2 column layout
category: 'Columns: 2'
+ description: 'A theme provided layout'
template: twocol
path: templates
library: theme_a/twocol
* template = "templates/plugin-provided-layout",
* regions = {
* "main" = {
- * "label" = @Translation("Main Region")
+ * "label" = @Translation("Main Region", context = "layout_region")
* }
* }
* )
--- /dev/null
+<?php
+
+namespace Drupal\Tests\Core\Plugin\Context;
+
+use Drupal\Component\Plugin\Context\ContextInterface as ComponentContextInterface;
+use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionInterface;
+use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionTrait;
+use Drupal\Component\Plugin\Definition\PluginDefinition;
+use Drupal\Component\Plugin\Exception\ContextException;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\Plugin\ContextAwarePluginBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\Plugin\DataType\StringData;
+use Drupal\Core\TypedData\TypedDataManagerInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Plugin\ContextAwarePluginBase
+ * @group Plugin
+ */
+class ContextAwarePluginBaseTest extends UnitTestCase {
+
+ /**
+ * The plugin instance under test.
+ *
+ * @var \Drupal\Core\Plugin\ContextAwarePluginBase
+ */
+ private $plugin;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp();
+ $this->plugin = new TestContextAwarePlugin([], 'the_sisko', new TestPluginDefinition());
+ }
+
+ /**
+ * @covers ::getContextDefinitions
+ */
+ public function testGetContextDefinitions() {
+ $this->assertInternalType('array', $this->plugin->getContextDefinitions());
+ }
+
+ /**
+ * @covers ::getContextDefinition
+ */
+ public function testGetContextDefinition() {
+ // The context is not defined, so an exception will be thrown.
+ $this->setExpectedException(ContextException::class, 'The person context is not a valid context.');
+ $this->plugin->getContextDefinition('person');
+ }
+
+ /**
+ * @covers ::setContextValue
+ */
+ public function testSetContextValue() {
+ $typed_data_manager = $this->prophesize(TypedDataManagerInterface::class);
+ $container = new ContainerBuilder();
+ $container->set('typed_data_manager', $typed_data_manager->reveal());
+ \Drupal::setContainer($container);
+
+ $this->plugin->getPluginDefinition()->addContextDefinition('foo', new ContextDefinition('string'));
+
+ $this->assertFalse($this->plugin->setContextCalled);
+ $this->plugin->setContextValue('foo', new StringData(new DataDefinition(), 'bar'));
+ $this->assertTrue($this->plugin->setContextCalled);
+ }
+
+}
+
+class TestPluginDefinition extends PluginDefinition implements ContextAwarePluginDefinitionInterface {
+
+ use ContextAwarePluginDefinitionTrait;
+
+}
+
+class TestContextAwarePlugin extends ContextAwarePluginBase {
+
+ /**
+ * Indicates if ::setContext() has been called or not.
+ *
+ * @var bool
+ */
+ public $setContextCalled = FALSE;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContext($name, ComponentContextInterface $context) {
+ parent::setContext($name, $context);
+ $this->setContextCalled = TRUE;
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Drupal\Tests\Core\Routing;
+
+use Drupal\Core\Path\CurrentPathStack;
+use Drupal\Core\Routing\RequestContext;
+use Drupal\Core\Routing\RouteCompiler;
+use Drupal\Core\Routing\RouteProviderInterface;
+use Drupal\Core\Routing\Router;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Routing\Router
+ * @group Routing
+ */
+class RouterTest extends UnitTestCase {
+
+ /**
+ * @covers ::applyFitOrder
+ */
+ public function testMatchesWithDifferentFitOrder() {
+ $route_provider = $this->prophesize(RouteProviderInterface::class);
+
+ $route_collection = new RouteCollection();
+
+ $route = new Route('/user/{user}');
+ $route->setOption('compiler_class', RouteCompiler::class);
+ $route_collection->add('user_view', $route);
+
+ $route = new Route('/user/login');
+ $route->setOption('compiler_class', RouteCompiler::class);
+ $route_collection->add('user_login', $route);
+
+ $route_provider->getRouteCollectionForRequest(Argument::any())
+ ->willReturn($route_collection);
+
+ $url_generator = $this->prophesize(UrlGeneratorInterface::class);
+ $current_path_stack = $this->prophesize(CurrentPathStack::class);
+ $router = new Router($route_provider->reveal(), $current_path_stack->reveal(), $url_generator->reveal());
+
+ $request_context = $this->prophesize(RequestContext::class);
+ $request_context->getScheme()->willReturn('http');
+ $router->setContext($request_context->reveal());
+
+ $current_path_stack->getPath(Argument::any())->willReturn('/user/1');
+ $result = $router->match('/user/1');
+
+ $this->assertEquals('user_view', $result['_route']);
+
+ $current_path_stack->getPath(Argument::any())->willReturn('/user/login');
+ $result = $router->match('/user/login');
+
+ $this->assertEquals('user_login', $result['_route']);
+ }
+
+}
* @todo change $edit to disallow NULL as a value for Drupal 9.
* https://www.drupal.org/node/2802401
* @param string $submit
- * Value of the submit button whose click is to be emulated. For example,
- * 'Save'. The processing of the request depends on this value. For example,
- * a form may have one button with the value 'Save' and another button with
- * the value 'Delete', and execute different code depending on which one is
- * clicked.
- *
- * This function can also be called to emulate an Ajax submission. In this
- * case, this value needs to be an array with the following keys:
- * - path: A path to submit the form values to for Ajax-specific processing.
- * - triggering_element: If the value for the 'path' key is a generic Ajax
- * processing path, this needs to be set to the name of the element. If
- * the name doesn't identify the element uniquely, then this should
- * instead be an array with a single key/value pair, corresponding to the
- * element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder
- * uses this to find the #ajax information for the element, including
- * which specific callback to use for processing the request.
- *
- * This can also be set to NULL in order to emulate an Internet Explorer
- * submission of a form with a single text field, and pressing ENTER in that
- * textfield: under these conditions, no button information is added to the
- * POST data.
+ * The id, name, label or value of the submit button which is to be clicked.
+ * For example, 'Save'. The first element matched by
+ * \Drupal\Tests\WebAssert::buttonExists() will be used. The processing of
+ * the request depends on this value. For example, a form may have one
+ * button with the value 'Save' and another button with the value 'Delete',
+ * and execute different code depending on which one is clicked.
* @param array $options
* Options to be forwarded to the url generator.
* @param string|null $form_html_id
* (deprecated) The response content after submit form. It is necessary for
* backwards compatibility and will be removed before Drupal 9.0. You should
* just use the webAssert object for your assertions.
+ *
+ * @see \Drupal\Tests\WebAssert::buttonExists()
*/
protected function drupalPostForm($path, $edit, $submit, array $options = [], $form_html_id = NULL) {
if (is_object($submit)) {
namespace Drupal\Tests;
use Behat\Mink\Exception\ExpectationException;
+use Behat\Mink\Exception\ResponseTextException;
use Behat\Mink\WebAssert as MinkWebAssert;
use Behat\Mink\Element\TraversableElement;
use Behat\Mink\Exception\ElementNotFoundException;
$this->assert(!preg_match($regex, $actual), $message);
}
+ /**
+ * Checks that current page contains text only once.
+ *
+ * @param string $text
+ * The string to look for.
+ *
+ * @see \Behat\Mink\WebAssert::pageTextContains()
+ */
+ public function pageTextContainsOnce($text) {
+ $actual = $this->session->getPage()->getText();
+ $actual = preg_replace('/\s+/u', ' ', $actual);
+ $regex = '/' . preg_quote($text, '/') . '/ui';
+ $count = preg_match_all($regex, $actual);
+ if ($count === 1) {
+ return;
+ }
+
+ if ($count > 1) {
+ $message = sprintf('The text "%s" appears in the text of this page more than once, but it should not.', $text);
+ }
+ else {
+ $message = sprintf('The text "%s" was not found anywhere in the text of the current page.', $text);
+ }
+
+ throw new ResponseTextException($message, $this->session->getDriver());
+ }
+
}
}
/**
- * Implements hook_preprocess_HOOK() for page templates.
+ * Implements hook_preprocess_HOOK() for page title templates.
*/
function bartik_preprocess_page_title(&$variables) {
// Since the title and the shortcut link are both block level elements,
# Paths (clean URLs)
Disallow: /admin/
Disallow: /comment/reply/
-Disallow: /filter/tips/
+Disallow: /filter/tips
Disallow: /node/add/
Disallow: /search/
Disallow: /user/register/
# Paths (no clean URLs)
Disallow: /index.php/admin/
Disallow: /index.php/comment/reply/
-Disallow: /index.php/filter/tips/
+Disallow: /index.php/filter/tips
Disallow: /index.php/node/add/
Disallow: /index.php/search/
Disallow: /index.php/user/password/
* example, to map https://www.drupal.org:8080/mysite/test to the configuration
* directory sites/example.com, the array should be defined as:
* @code
- * $sites = array(
+ * $sites = [
* '8080.www.drupal.org.mysite.test' => 'example.com',
- * );
+ * ];
* @endcode
* The URL, https://www.drupal.org:8080/mysite/test/, could be a symbolic link
* or an Apache Alias directive that points to the Drupal root containing