diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index c3d5f5622..adc61a74c 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -3,6 +3,9 @@ encouraged to submit issues and changes (patches) to improve Drupal, and to contribute in other ways -- see https://www.drupal.org/contribute to find out how. +This file lists the active maintainers. For a list of past maintainers, see: +https://www.drupal.org/core/maintainers/past + Core committers --------------- @@ -53,11 +56,10 @@ Actions - ? Aggregator -- Paris Liakos 'ParisLiakos' https://www.drupal.org/u/parisliakos +- ? Ajax - Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia -- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos - Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett Asset Library API @@ -73,19 +75,17 @@ Ban - ? Bartik -- Jen Simmons 'jensimmons' https://www.drupal.org/u/jensimmons - Emma Maria Karayiannis 'emma.maria' https://www.drupal.org/u/emma.maria Base system -- Damien Tournoud 'damien-tournoud' https://www.drupal.org/u/damien-tournoud -- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman +- ? Basic Auth - Klaus Purer 'klausi' https://www.drupal.org/u/klausi - Juampy Novillo Requena 'juampy' https://www.drupal.org/u/juampy Batch API -- Yves Chedemois 'yched' https://www.drupal.org/u/yched +- ? BigPipe - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers @@ -109,9 +109,7 @@ Breakpoint - Marc Drummond 'mdrummond' https://www.drupal.org/u/mdrummond Cache -- Damien Tournoud 'damien-tournoud' https://www.drupal.org/u/damien-tournoud - Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch -- Mark Sonnabaum 'msonnabaum' https://www.drupal.org/u/msonnabaum CKEditor - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers @@ -130,13 +128,11 @@ Comment - Andrey Postnikov 'andypost' https://www.drupal.org/u/andypost Configuration API -- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun - Alex Pott 'alexpott' https://www.drupal.org/u/alexpott - Matthew Tift 'mtift' https://www.drupal.org/u/mtift Configuration Entity API - Alex Pott 'alexpott' https://www.drupal.org/u/alexpott -- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun - Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett Configuration UI @@ -159,10 +155,10 @@ Content Translation - Francesco Placella 'plach' https://www.drupal.org/u/plach Contextual -- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun +- ? Cron -- Derek Wright 'dww' https://www.drupal.org/u/dww +- ? CSS - John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin @@ -175,14 +171,13 @@ Database API - David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss PostgreSQL DB driver - - Damien Tournoud 'damien-tournoud' https://www.drupal.org/u/damien-tournoud - - Josh Waihi 'fiasco' https://www.drupal.org/u/fiasco + - ? Sqlite DB driver - - Damien Tournoud 'damien-tournoud' https://www.drupal.org/u/damien-tournoud + - ? Database Logging -- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey +- ? Database Update API - ? @@ -203,7 +198,6 @@ Editor - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers Entity API -- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago - Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch - Sascha Grossenbacher 'Berdir' https://www.drupal.org/u/berdir - Francesco Placella 'plach' https://www.drupal.org/u/plach @@ -213,7 +207,6 @@ Extension API - ? Field API -- Yves Chedemois 'yched' https://www.drupal.org/u/yched - Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu Field UI @@ -221,19 +214,16 @@ Field UI - Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu File -- Andrew Morton 'drewish' https://www.drupal.org/u/drewish -- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron +- ? Filter -- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun +- ? Forum - Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan Form API - Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia -- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago -- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun - Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett History @@ -266,23 +256,25 @@ Link Field - Weber Macedo 'Mac_Weber' https://www.drupal.org/u/mac_weber Lock -- Damien Tournoud 'damien-tournoud' https://www.drupal.org/u/damien-tournoud +- ? Mail - ? Markup -- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun +- ? Migrate - Adam Globus-Hoenich 'phenaproxima' https://www.drupal.org/u/phenaproxima - Ben Dougherty 'benjy' https://www.drupal.org/u/benjy +- Lucas Hedding 'heddn' https://www.drupal.org/u/heddn - Michael Anello 'ultimike' https://www.drupal.org/u/ultimike - Mike Ryan 'mikeryan' https://www.drupal.org/u/mikeryan - Vicki Spagnolo 'quietone' https://www.drupal.org/u/quietone Migrate (Drupal) - Ben Dougherty 'benjy' https://www.drupal.org/u/benjy +- Lucas Hedding 'heddn' https://www.drupal.org/u/heddn - Michael Anello 'ultimike' https://www.drupal.org/u/ultimike - Mike Ryan 'mikeryan' https://www.drupal.org/u/mikeryan - Vicki Spagnolo 'quietone' https://www.drupal.org/u/quietone @@ -299,16 +291,15 @@ Menu UI - ? Node -- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman -- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss +- ? Node Access -- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman - Ken Rickard 'agentrickard' https://www.drupal.org/u/agentrickard +- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman - Jess Myrbo 'xjm' https://www.drupal.org/u/xjm Options -- Yves Chedemois 'yched' https://www.drupal.org/u/yched +- ? Outside In - Ted Bowman 'tedbow' https://www.drupal.org/u/tedbow @@ -327,7 +318,6 @@ Plugin Queue - James Gilliland 'neclimdul' https://www.drupal.org/u/neclimdul -- Mark Sonnabaum 'msonnabaum' https://www.drupal.org/u/msonnabaum Quick Edit - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers @@ -337,8 +327,8 @@ RDF - Stéphane Corlosquet 'scor' https://www.drupal.org/u/scor Render API -- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman - Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia +- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman Request Processing - Larry Garfield 'Crell' https://www.drupal.org/u/crell @@ -388,10 +378,9 @@ System (module) Taxonomy - Jess Myrbo 'xjm' https://www.drupal.org/u/xjm - Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch -- Benjamin Doherty 'bangpound' https://www.drupal.org/u/bangpound Telephone -- Dave Reid 'dave-reid' https://www.drupal.org/u/dave-reid +- ? Testing framework - Alex Pott 'alexpott' https://www.drupal.org/u/alexpott @@ -409,7 +398,7 @@ Theme API - Lauri Eskola 'lauriii' https://www.drupal.org/u/lauriii Token -- Dave Reid 'davereid' https://www.drupal.org/u/davereid +- ? Toolbar - Théodore Biadala 'nod_' https://www.drupal.org/u/nod_ @@ -418,22 +407,19 @@ Tour - Nick Schuch 'nick_schuch' https://www.drupal.org/u/nick_schuch Tracker -- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss +- ? Transliteration - Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu -- Damien Tournoud 'damien-tournoud' https://www.drupal.org/u/damien-tournoud -- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun Typed Data - Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago Update UI -- Derek Wright 'dww' https://www.drupal.org/u/dww +- ? User - Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman -- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss Views - Daniel Wehner 'dawehner' https://www.drupal.org/u/dawehner @@ -442,18 +428,11 @@ Views - Jess Myrbo 'xjm' https://www.drupal.org/u/xjm - Len Swaneveld 'Lendude' https://www.drupal.org/u/lendude -Views UI -- Daniel Wehner 'dawehner' https://www.drupal.org/u/dawehner -- Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett -- Damian Lee 'damiankloip' https://www.drupal.org/u/damiankloip -- Len Swaneveld 'Lendude' https://www.drupal.org/u/lendude - Topic maintainers ----------------- Accessibility - Mike Gifford 'mgifford' https://www.drupal.org/u/mgifford -- Jesse Renée Beach 'jessebeach' https://www.drupal.org/u/jessebeach - Andrew Macpherson 'andrewmacpherson' https://www.drupal.org/u/andrewmacpherson Documentation @@ -492,24 +471,16 @@ re-architect or otherwise improve large areas of Drupal core. See https://www.drupal.org/community-initiatives/drupal-core for more information on their responsibilities. The initiative coordinators for Drupal 8 are: -Configuration management -- Greg Dunlap 'heyrocker' https://www.drupal.org/u/heyrocker - -Design -- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz - -Mobile -- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin - Multi-lingual - Gábor Hojtsy 'Gábor Hojtsy' https://www.drupal.org/u/gábor-hojtsy -Web services -- Larry Garfield 'Crell' https://www.drupal.org/u/crell - Workflow Initiative - Dick Olsson 'dixon_' https://www.drupal.org/u/dixon_ +PHPUnit Initiative +- Klaus Purer 'klausi' https://www.drupal.org/u/klausi +- Daniel Wehner 'dawehner' https://www.drupal.org/u/dawehner + Provisional membership: None at this time. diff --git a/core/composer.json b/core/composer.json index 53ab8fa68..b9a3fad8b 100644 --- a/core/composer.json +++ b/core/composer.json @@ -31,7 +31,7 @@ "symfony/psr-http-message-bridge": "v0.2", "zendframework/zend-diactoros": "~1.1", "composer/semver": "~1.0", - "paragonie/random_compat": "~1.0", + "paragonie/random_compat": "^1|^2", "asm89/stack-cors": "~1.0" }, "require-dev": { diff --git a/core/core.api.php b/core/core.api.php index 639e55dd9..eb16ee098 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -1073,8 +1073,9 @@ * yourmodule/tests/src/Unit directory, according to the PSR-4 standard. * - Your test class needs a phpDoc comment block with a description and * a @group annotation, which gives information about the test. - * - Methods in your test class whose names start with 'test' are the actual - * test cases. Each one should test a logical subset of the functionality. + * - Add test cases by adding method names that start with 'test' and have no + * arguments, for example testYourTestCase(). Each one should test a logical + * subset of the functionality. * For more details, see: * - https://www.drupal.org/phpunit for full documentation on how to write * PHPUnit tests for Drupal. @@ -1110,9 +1111,9 @@ * set up content types and similar procedures. * - In some cases, you may need to write a test module to support your test; * put such modules under the yourmodule/tests/modules directory. - * - Methods in your test class whose names start with 'test', and which have - * no arguments, are the actual test cases. Each one should test a logical - * subset of the functionality, and each one runs in a new, isolated test + * - Add test cases by adding method names that start with 'test' and have no + * arguments, for example testYourTestCase(). Each one should test a logical + * subset of the functionality. Each method runs in a new, isolated test * environment, so it can only rely on the setUp() method, not what has * been set up by other test methods. * For more details, see: @@ -1121,6 +1122,52 @@ * - @link oo_conventions Object-oriented programming topic @endlink for more * on PSR-4, namespaces, and where to place classes. * + * @section write_functional_phpunit Write functional PHP tests (phpunit) + * Functional tests extend the BrowserTestBase base class, and use PHPUnit as + * their underlying framework. They use a simulated browser, in which the test + * can click links, visit URLs, post to forms, etc. To write a functional test: + * - Extend \Drupal\Tests\BrowserTestBase. + * - Place the test in the yourmodule/tests/src/Functional/ directory and use + * the \Drupal\Tests\yourmodule\Functional namespace. + * - Add a @group annotation. For example, if the test is for a Drupal 6 + * migration process, the group core uses is migrate_drupal_6. Use yourmodule + * as the group name if the test does not belong to another larger group. + * - You may also override the default setUp() method, which can be used to set + * up content types and similar procedures. Don't forget to call the parent + * method. + * - In some cases, you may need to write a test module to support your test; + * put such modules under the yourmodule/tests/modules directory. + * - Add test cases by adding method names that start with 'test' and have no + * arguments, for example testYourTestCase(). Each one should test a logical + * subset of the functionality. Each method runs in a new, isolated test + * environment, so it can only rely on the setUp() method, not what has + * been set up by other test methods. + * For more details, see: + * - https://www.drupal.org/docs/8/phpunit/phpunit-browser-test-tutorial for + * a full tutorial on how to write functional PHPUnit tests for Drupal. + * - https://www.drupal.org/phpunit for the full documentation on how to write + * PHPUnit tests for Drupal. + * + * @section write_jsfunctional_phpunit Write functional JavaScript tests (phpunit) + * To write a functional test that relies on JavaScript: + * - Extend \Drupal\FunctionalJavaScriptTests\JavascriptTestBase. + * - Place the test into the yourmodule/tests/src/FunctionalJavascript/ + * directory and use the \Drupal\Tests\yourmodule\FunctionalJavascript + * namespace. + * - Add a @group annotation. Use yourmodule as the group name if the test does + * not belong to another larger group. + * - Set up PhantomJS; see http://phantomjs.org/download.html. + * - To run tests, see core/tests/README.md. + * - When clicking a link/button with Ajax behavior attached, keep in mind that + * the underlying browser might take time to deliver changes to the HTML. Use + * $this->assertSession()->assertWaitOnAjaxRequest() to wait for the Ajax + * request to finish. + * For more details, see: + * - https://www.drupal.org/docs/8/phpunit/phpunit-javascript-testing-tutorial + * for a full tutorial on how to write PHPUnit JavaScript tests for Drupal. + * - https://www.drupal.org/phpunit for the full documentation on how to write + * PHPUnit tests for Drupal. + * * @section running Running tests * You can run both Simpletest and PHPUnit tests by enabling the core Testing * module (core/modules/simpletest). Once that module is enabled, tests can be @@ -2493,7 +2540,7 @@ function hook_validation_constraint_alter(array &$definitions) { * this class is subscribed to, and which methods on the class should be * called for each one. Example: * @code - * public function getSubscribedEvents() { + * public static function getSubscribedEvents() { * // Subscribe to kernel terminate with priority 100. * $events[KernelEvents::TERMINATE][] = array('onTerminate', 100); * // Subscribe to kernel request with default priority of 0. diff --git a/core/includes/entity.inc b/core/includes/entity.inc index b0e5088f9..8b4eb22f0 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -539,12 +539,12 @@ function entity_get_display($entity_type, $bundle, $view_mode) { * When the entity form display is not available in configuration, you can * create a new EntityFormDisplay object using: * @code - * $values = ('entity_form_display', array( + * $values = array( * 'targetEntityType' => $entity_type, * 'bundle' => $bundle, * 'mode' => $form_mode, * 'status' => TRUE, - * )); + * ); * \Drupal::entityTypeManager() * ->getStorage('entity_form_display') * ->create($values); diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 0d961c12f..1e0da8eb8 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -81,7 +81,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '8.2.3'; + const VERSION = '8.2.4'; /** * Core API compatibility. diff --git a/core/lib/Drupal/Component/Diff/DiffFormatter.php b/core/lib/Drupal/Component/Diff/DiffFormatter.php index ce08b004d..edcb84df7 100644 --- a/core/lib/Drupal/Component/Diff/DiffFormatter.php +++ b/core/lib/Drupal/Component/Diff/DiffFormatter.php @@ -36,6 +36,16 @@ class DiffFormatter { */ public $trailing_context_lines = 0; + /** + * The line stats. + * + * @var array + */ + protected $line_stats = array( + 'counter' => array('x' => 0, 'y' => 0), + 'offset' => array('x' => 0, 'y' => 0), + ); + /** * Format a diff. * diff --git a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php index c72b116fa..753a00899 100644 --- a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php +++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php @@ -54,7 +54,7 @@ class CsrfAccessCheck implements RoutingAccessInterface { $path = str_replace("{{$param}}", $value, $path); } - if ($this->csrfToken->validate($request->query->get('token'), $path)) { + if ($this->csrfToken->validate($request->query->get('token', ''), $path)) { $result = AccessResult::allowed(); } else { diff --git a/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php b/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php index 23d610ce9..4fd215e38 100644 --- a/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php +++ b/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php @@ -87,7 +87,7 @@ class CsrfTokenGenerator { return FALSE; } - return $token === $this->computeToken($seed, $value); + return Crypt::hashEquals($this->computeToken($seed, $value), $token); } /** diff --git a/core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php b/core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php index dc0ac923d..3aed1ece8 100644 --- a/core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php +++ b/core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php @@ -34,6 +34,11 @@ abstract class DraggableListBuilder extends ConfigEntityListBuilder implements F */ protected $weightKey = FALSE; + /** + * {@inheritdoc} + */ + protected $limit = FALSE; + /** * The form builder. * diff --git a/core/lib/Drupal/Core/Diff/DiffFormatter.php b/core/lib/Drupal/Core/Diff/DiffFormatter.php index 52f289491..8ebe2f479 100644 --- a/core/lib/Drupal/Core/Diff/DiffFormatter.php +++ b/core/lib/Drupal/Core/Diff/DiffFormatter.php @@ -19,16 +19,6 @@ class DiffFormatter extends DiffFormatterBase { */ protected $rows = array(); - /** - * The line stats. - * - * @var array - */ - protected $line_stats = array( - 'counter' => array('x' => 0, 'y' => 0), - 'offset' => array('x' => 0, 'y' => 0), - ); - /** * Creates a DiffFormatter to render diffs in a table. * diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 51adf91b7..9c3e705bd 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -348,13 +348,10 @@ class EntityAutocomplete extends Textfield { public static function extractEntityIdFromAutocompleteInput($input) { $match = NULL; - // Take "label (entity id)', match the ID from parenthesis when it's a - // number. - if (preg_match("/.+\s\((\d+)\)/", $input, $matches)) { - $match = $matches[1]; - } - // Match the ID when it's a string (e.g. for config entity types). - elseif (preg_match("/.+\s\(([\w.]+)\)/", $input, $matches)) { + // Take "label (entity id)', match the ID from inside the parentheses. + // @todo Add support for entities containing parentheses in their ID. + // @see https://www.drupal.org/node/2520416 + if (preg_match("/.+\s\(([^\)]+)\)/", $input, $matches)) { $match = $matches[1]; } diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index 4de891a82..f8683e3da 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -94,7 +94,7 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl protected $plugins = array(); /** - * Context in which this entity will be used (e.g. 'display', 'form'). + * Context in which this entity will be used (e.g. 'view', 'form'). * * @var string */ diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index 8bfb3a3e2..7097b85df 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Theme\Registry; use Drupal\Core\TypedData\TranslatableInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -54,6 +55,13 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf */ protected $languageManager; + /** + * The theme registry. + * + * @var \Drupal\Core\Theme\Registry + */ + protected $themeRegistry; + /** * The EntityViewDisplay objects created for individual field rendering. * @@ -72,12 +80,15 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf * The entity manager service. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. + * @param \Drupal\Core\Theme\Registry $theme_registry + * The theme registry. */ - public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) { + public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, Registry $theme_registry = NULL) { $this->entityTypeId = $entity_type->id(); $this->entityType = $entity_type; $this->entityManager = $entity_manager; $this->languageManager = $language_manager; + $this->themeRegistry = $theme_registry ?: \Drupal::service('theme.registry'); } /** @@ -87,7 +98,8 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf return new static( $entity_type, $container->get('entity.manager'), - $container->get('language_manager') + $container->get('language_manager'), + $container->get('theme.registry') ); } @@ -148,7 +160,6 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf $this->moduleHandler()->alter('entity_view_mode', $view_mode, $entity, $context); $build = array( - '#theme' => $this->entityTypeId, "#{$this->entityTypeId}" => $entity, '#view_mode' => $view_mode, // Collect cache defaults for this entity. @@ -159,6 +170,11 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf ), ); + // Add the default #theme key if a template exists for it. + if ($this->themeRegistry->getRuntime()->has($this->entityTypeId)) { + $build['#theme'] = $this->entityTypeId; + } + // Cache the rendered output if permitted by the view mode and global entity // type configuration. if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) { diff --git a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php index 630d17673..9c05dfa4d 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php @@ -35,13 +35,18 @@ interface QueryInterface extends AlterableInterface { * * @param $field * Name of the field being queried. It must contain a field name, optionally - * followed by a column name. The column can be "entity" for reference - * fields and that can be followed similarly by a field name and so on. Some - * examples: + * followed by a column name. The column can be the reference property, + * usually "entity", for reference fields and that can be followed + * similarly by a field name and so on. Additionally, the target entity type + * can be specified by appending the ":target_entity_type_id" to "entity". + * Some examples: * - nid * - tags.value * - tags + * - tags.entity.name + * - tags.entity:taxonomy_term.name * - uid.entity.name + * - uid.entity:user.name * "tags" "is the same as "tags.value" as value is the default column. * If two or more conditions have the same field names they apply to the * same delta within that field. In order to limit the condition to a diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php index c8bf59d15..1a1509cc4 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php @@ -6,6 +6,8 @@ use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\Query\QueryException; use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; use Drupal\Core\Entity\Sql\TableMappingInterface; +use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface; +use Drupal\Core\TypedData\DataReferenceDefinitionInterface; /** * Adds tables and fields to the SQL entity query. @@ -253,10 +255,20 @@ class Tables implements TablesInterface { $relationship_specifier = $specifiers[$key + 1]; $next_index_prefix = $relationship_specifier; } + $entity_type_id = NULL; + // Relationship specifier can also contain the entity type ID, i.e. + // entity:node, entity:user or entity:taxonomy. + if (strpos($relationship_specifier, ':') !== FALSE) { + list($relationship_specifier, $entity_type_id) = explode(':', $relationship_specifier, 2); + } // Check for a valid relationship. - if (isset($propertyDefinitions[$relationship_specifier]) && $field_storage->getPropertyDefinition('entity')->getDataType() == 'entity_reference' ) { - // If it is, use the entity type. - $entity_type_id = $propertyDefinitions[$relationship_specifier]->getTargetDefinition()->getEntityTypeId(); + if (isset($propertyDefinitions[$relationship_specifier]) && $propertyDefinitions[$relationship_specifier] instanceof DataReferenceDefinitionInterface) { + // If it is, use the entity type if specified already, otherwise use + // the definition. + $target_definition = $propertyDefinitions[$relationship_specifier]->getTargetDefinition(); + if (!$entity_type_id && $target_definition instanceof EntityDataDefinitionInterface) { + $entity_type_id = $target_definition->getEntityTypeId(); + } $entity_type = $this->entityManager->getDefinition($entity_type_id); $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); // Add the new entity base table using the table and sql column. diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index 2ea450d73..d4945cf3f 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -1257,7 +1257,11 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt foreach ($storage_definition->getColumns() as $column => $attributes) { $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); // Serialize the value if specified in the column schema. - $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column; + $value = $item->$column; + if (!empty($attributes['serialize'])) { + $value = serialize($value); + } + $record[$column_name] = drupal_schema_get_field_value($attributes, $value); } $query->values($record); if ($this->entityType->isRevisionable()) { diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php index fa83359c0..2692d84e1 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php @@ -168,6 +168,10 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase implem $recursive_render_id = $items->getFieldDefinition()->getTargetEntityTypeId() . $items->getFieldDefinition()->getTargetBundle() . $items->getName() + // We include the referencing entity, so we can render default images + // without hitting recursive protections. + . $items->getEntity()->id() + . $entity->getEntityTypeId() . $entity->id(); if (isset(static::$recursiveRenderDepth[$recursive_render_id])) { diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UuidItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UuidItem.php index 94aa8aae7..8bd33ce44 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UuidItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UuidItem.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Field\Plugin\Field\FieldType; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Field\FieldDefinitionInterface; /** * Defines the 'uuid' entity field type. @@ -48,4 +49,12 @@ class UuidItem extends StringItem { return $schema; } + /** + * {@inheritdoc} + */ + public static function generateSampleValue(FieldDefinitionInterface $field_definition) { + $values['value'] = \Drupal::service('uuid')->generate(); + return $values; + } + } diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index 5f7ce3c14..3d0db4ce4 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -36,7 +36,7 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface /** * Constructs a WidgetBase object. * - * @param array $plugin_id + * @param string $plugin_id * The plugin_id for the widget. * @param mixed $plugin_definition * The plugin implementation definition. diff --git a/core/lib/Drupal/Core/Language/LanguageManager.php b/core/lib/Drupal/Core/Language/LanguageManager.php index 9016d1e45..ef5421713 100644 --- a/core/lib/Drupal/Core/Language/LanguageManager.php +++ b/core/lib/Drupal/Core/Language/LanguageManager.php @@ -318,7 +318,7 @@ class LanguageManager implements LanguageManagerInterface { 'th' => array('Thai', 'ภาษาไทย'), 'tr' => array('Turkish', 'Türkçe'), 'tyv' => array('Tuvan', 'Тыва дыл'), - 'ug' => array('Uyghur', 'Уйғур'), + 'ug' => array('Uyghur', /* Left-to-right marker "‭" */ 'ئۇيغۇرچە', LanguageInterface::DIRECTION_RTL), 'uk' => array('Ukrainian', 'Українська'), 'ur' => array('Urdu', /* Left-to-right marker "‭" */ 'اردو', LanguageInterface::DIRECTION_RTL), 'vi' => array('Vietnamese', 'Tiếng Việt'), diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php b/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php index 71cfaa104..1c252e296 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Plugin\Context; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\TypedData\TypedDataTrait; /** @@ -9,6 +10,8 @@ use Drupal\Core\TypedData\TypedDataTrait; */ class ContextDefinition implements ContextDefinitionInterface { + use DependencySerializationTrait; + use TypedDataTrait; /** diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php index 2f9aeb366..358b1f1b7 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php @@ -419,9 +419,13 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn if ($should_add_header) { // Also add a HTTP header "Link:". - $href = '<' . Html::escape($attributes['href'] . '>'); + $href = '<' . Html::escape($attributes['href']) . '>'; unset($attributes['href']); - $attached['http_header'][] = ['Link', $href . drupal_http_header_attributes($attributes), TRUE]; + if ($param = drupal_http_header_attributes($attributes)) { + $href .= ';' . $param; + } + + $attached['http_header'][] = ['Link', $href, FALSE]; } } return $attached; diff --git a/core/lib/Drupal/Core/StringTranslation/Translator/CustomStrings.php b/core/lib/Drupal/Core/StringTranslation/Translator/CustomStrings.php index 9d9a3b326..8e733d548 100644 --- a/core/lib/Drupal/Core/StringTranslation/Translator/CustomStrings.php +++ b/core/lib/Drupal/Core/StringTranslation/Translator/CustomStrings.php @@ -3,6 +3,7 @@ namespace Drupal\Core\StringTranslation\Translator; use Drupal\Core\Site\Settings; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; /** * String translator using overrides from variables. @@ -12,6 +13,8 @@ use Drupal\Core\Site\Settings; */ class CustomStrings extends StaticTranslation { + use DependencySerializationTrait; + /** * The settings read only object. * diff --git a/core/misc/autocomplete.js b/core/misc/autocomplete.js index 254e7e509..5a1c156d0 100644 --- a/core/misc/autocomplete.js +++ b/core/misc/autocomplete.js @@ -77,6 +77,11 @@ */ function searchHandler(event) { var options = autocomplete.options; + + if (options.isComposing) { + return false; + } + var term = autocomplete.extractLastTerm(event.target.value); // Abort search if the first character is in firstCharacterBlacklist. if (term.length > 0 && options.firstCharacterBlacklist.indexOf(term[0]) !== -1) { @@ -225,6 +230,14 @@ .each(function () { $(this).data('ui-autocomplete')._renderItem = autocomplete.options.renderItem; }); + + // Use CompositionEvent to handle IME inputs. It requests remote server on "compositionend" event only. + $autocomplete.on('compositionstart.autocomplete', function () { + autocomplete.options.isComposing = true; + }); + $autocomplete.on('compositionend.autocomplete', function () { + autocomplete.options.isComposing = false; + }); } }, detach: function (context, settings, trigger) { @@ -261,7 +274,9 @@ renderItem: renderItem, minLength: 1, // Custom options, used by Drupal.autocomplete. - firstCharacterBlacklist: '' + firstCharacterBlacklist: '', + // Custom options, indicate IME usage status. + isComposing: false }, ajax: { dataType: 'json' diff --git a/core/modules/block/block.api.php b/core/modules/block/block.api.php index 2d61bb004..4c7faf598 100644 --- a/core/modules/block/block.api.php +++ b/core/modules/block/block.api.php @@ -151,7 +151,7 @@ function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\B function hook_block_build_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) { // Add the 'user' cache context to some blocks. if ($some_condition) { - $build['#contexts'][] = 'user'; + $build['#cache']['contexts'][] = 'user'; } } diff --git a/core/modules/book/tests/src/FunctionalJavascript/BookJavascriptTest.php b/core/modules/book/tests/src/FunctionalJavascript/BookJavascriptTest.php index e895b6824..d8d406b44 100644 --- a/core/modules/book/tests/src/FunctionalJavascript/BookJavascriptTest.php +++ b/core/modules/book/tests/src/FunctionalJavascript/BookJavascriptTest.php @@ -73,7 +73,7 @@ class BookJavascriptTest extends JavascriptTestBase { $dragged->dragTo($target); // Give javascript some time to manipulate the DOM. - $this->getSession()->wait(1000, 'jQuery(".tabledrag-changed-warning").is(":visible")'); + $this->assertJsCondition('jQuery(".tabledrag-changed-warning").is(":visible")'); // Check that the 'unsaved changes' text appeared in the message area. $this->assertSession()->pageTextContains('You have unsaved changes.'); diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Language.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Language.php index 145f74147..980d5e909 100644 --- a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Language.php +++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Language.php @@ -8,7 +8,6 @@ use Drupal\ckeditor\CKEditorPluginCssInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageManager; use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Url; use Drupal\editor\Entity\Editor; /** @@ -115,7 +114,7 @@ class Language extends CKEditorPluginBase implements CKEditorPluginConfigurableI ], '#default_value' => $config['language_list'], '#description' => $this->t('The list of languages to show in the language dropdown. The basic list will only show the six official languages of the UN. The extended list will show all @count languages that are available in Drupal.', [ - ':url' => Url::fromUri('http://www.un.org/en/aboutun/languages.shtml/')->toString(), + ':url' => 'https://www.un.org/en/sections/about-un/official-languages', '@count' => count($predefined_languages), ]), '#attached' => ['library' => ['ckeditor/drupal.ckeditor.language.admin']], diff --git a/core/modules/comment/migration_templates/d6_comment.yml b/core/modules/comment/migration_templates/d6_comment.yml index a7ffc9dad..f4dff98e2 100644 --- a/core/modules/comment/migration_templates/d6_comment.yml +++ b/core/modules/comment/migration_templates/d6_comment.yml @@ -7,6 +7,8 @@ source: constants: entity_type: node process: + # If you are using this file to build a custom migration consider removing + # the cid field to allow incremental migrations. cid: cid pid: plugin: migration diff --git a/core/modules/comment/migration_templates/d6_comment_field.yml b/core/modules/comment/migration_templates/d6_comment_field.yml index 469d604d0..d14d1aa4f 100644 --- a/core/modules/comment/migration_templates/d6_comment_field.yml +++ b/core/modules/comment/migration_templates/d6_comment_field.yml @@ -13,7 +13,10 @@ process: type: 'constants/type' 'settings/comment_type': comment_type destination: - plugin: md_entity:field_storage_config + plugin: entity:field_storage_config + dependencies: + module: + - comment migration_dependencies: required: - d6_comment_type diff --git a/core/modules/comment/migration_templates/d7_comment.yml b/core/modules/comment/migration_templates/d7_comment.yml index 5845999da..d4e3c57cb 100644 --- a/core/modules/comment/migration_templates/d7_comment.yml +++ b/core/modules/comment/migration_templates/d7_comment.yml @@ -7,6 +7,8 @@ source: constants: entity_type: node process: + # If you are using this file to build a custom migration consider removing + # the cid field to allow incremental migrations. cid: cid pid: plugin: migration diff --git a/core/modules/comment/src/CommentAccessControlHandler.php b/core/modules/comment/src/CommentAccessControlHandler.php index ff6b3b96b..fc0d4b2d6 100644 --- a/core/modules/comment/src/CommentAccessControlHandler.php +++ b/core/modules/comment/src/CommentAccessControlHandler.php @@ -110,12 +110,13 @@ class CommentAccessControlHandler extends EntityAccessControlHandler { // access. return AccessResult::forbidden(); } + $is_name = $field_definition->getName() === 'name'; /** @var \Drupal\comment\CommentInterface $entity */ $entity = $items->getEntity(); $commented_entity = $entity->getCommentedEntity(); $anonymous_contact = $commented_entity->get($entity->getFieldName())->getFieldDefinition()->getSetting('anonymous'); $admin_access = AccessResult::allowedIfHasPermission($account, 'administer comments'); - $anonymous_access = AccessResult::allowedIf($entity->isNew() && $account->isAnonymous() && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT && $account->hasPermission('post comments')) + $anonymous_access = AccessResult::allowedIf($entity->isNew() && $account->isAnonymous() && ($anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT || $is_name) && $account->hasPermission('post comments')) ->cachePerPermissions() ->addCacheableDependency($entity) ->addCacheableDependency($field_definition->getConfig($commented_entity->bundle())) diff --git a/core/modules/comment/src/Tests/CommentAnonymousTest.php b/core/modules/comment/src/Tests/CommentAnonymousTest.php index 660d75406..2a5feb2f1 100644 --- a/core/modules/comment/src/Tests/CommentAnonymousTest.php +++ b/core/modules/comment/src/Tests/CommentAnonymousTest.php @@ -65,6 +65,16 @@ class CommentAnonymousTest extends CommentTestBase { $anonymous_comment1 = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName()); $this->assertTrue($this->commentExists($anonymous_comment1), 'Anonymous comment without contact info found.'); + // Ensure anonymous users cannot post in the name of registered users. + $edit = array( + 'name' => $this->adminUser->getUsername(), + 'comment_body[0][value]' => $this->randomMachineName(), + ); + $this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit, t('Save')); + $this->assertRaw(t('The name you used (%name) belongs to a registered user.', [ + '%name' => $this->adminUser->getUsername(), + ])); + // Allow contact info. $this->drupalLogin($this->adminUser); $this->setCommentAnonymous(COMMENT_ANONYMOUS_MAY_CONTACT); diff --git a/core/modules/config/tests/src/Functional/ConfigDraggableListBuilderTest.php b/core/modules/config/tests/src/Functional/ConfigDraggableListBuilderTest.php new file mode 100644 index 000000000..417e9f13c --- /dev/null +++ b/core/modules/config/tests/src/Functional/ConfigDraggableListBuilderTest.php @@ -0,0 +1,49 @@ +drupalLogin($this->drupalCreateUser(array('administer permissions'))); + + // Create more than 50 roles. + for ($i = 0; $i < 51; $i++) { + $role = Role::create([ + 'id' => 'role_' . $i, + 'label' => "Role $i", + ]); + $role->save(); + } + + // Navigate to Roles page + $this->drupalGet('admin/people/roles'); + + // Test for the page title. + $this->assertSession()->titleEquals(t('Roles') . ' | Drupal'); + + // Count the number of rows in table. + $rows = $this->xpath('//form[@class="user-admin-roles-form"]/table/tbody/tr'); + $this->assertGreaterThan(50, count($rows)); + for ($i = 0; $i < 51; $i++) { + $this->assertSession()->pageTextContains("Role $i"); + } + } + +} diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php index 51ad7d685..d76a35b98 100644 --- a/core/modules/content_moderation/src/EntityOperations.php +++ b/core/modules/content_moderation/src/EntityOperations.php @@ -152,7 +152,6 @@ class EntityOperations implements ContainerInjectionInterface { $entity_type_id = $entity->getEntityTypeId(); $entity_id = $entity->id(); $entity_revision_id = $entity->getRevisionId(); - $entity_langcode = $entity->language()->getId(); $storage = $this->entityTypeManager->getStorage('content_moderation_state'); $entities = $storage->loadByProperties([ @@ -174,11 +173,14 @@ class EntityOperations implements ContainerInjectionInterface { } // Sync translations. - 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')) { + $entity_langcode = $entity->language()->getId(); + 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); + } } // Create the ContentModerationState entity for the inserted entity. diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index ab7e918e4..047bc23ca 100644 --- a/core/modules/content_moderation/src/EntityTypeInfo.php +++ b/core/modules/content_moderation/src/EntityTypeInfo.php @@ -292,8 +292,8 @@ class EntityTypeInfo implements ContainerInjectionInterface { $fields = []; $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Moderation state')) - ->setDescription(t('The moderation state of this piece of content.')) + ->setLabel($this->t('Moderation state')) + ->setDescription($this->t('The moderation state of this piece of content.')) ->setComputed(TRUE) ->setClass(ModerationStateFieldItemList::class) ->setSetting('target_type', 'moderation_state') @@ -310,6 +310,7 @@ class EntityTypeInfo implements ContainerInjectionInterface { ->addConstraint('ModerationState', []) ->setDisplayConfigurable('form', FALSE) ->setDisplayConfigurable('view', FALSE) + ->setReadOnly(FALSE) ->setTranslatable(TRUE); return $fields; diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php index 80820b4b4..c32521c44 100644 --- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php +++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php @@ -43,12 +43,14 @@ class ModerationStateFieldItemList extends EntityReferenceFieldItemList { ->loadRevision($revision_to_load); // Return the correct translation. - $langcode = $entity->language()->getId(); - if (!$content_moderation_state->hasTranslation($langcode)) { - $content_moderation_state->addTranslation($langcode); - } - if ($content_moderation_state->language()->getId() !== $langcode) { - $content_moderation_state = $content_moderation_state->getTranslation($langcode); + if ($entity->getEntityType()->hasKey('langcode')) { + $langcode = $entity->language()->getId(); + if (!$content_moderation_state->hasTranslation($langcode)) { + $content_moderation_state->addTranslation($langcode); + } + if ($content_moderation_state->language()->getId() !== $langcode) { + $content_moderation_state = $content_moderation_state->getTranslation($langcode); + } } return $content_moderation_state->get('moderation_state')->entity; diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php index 3ba37a294..67a7a7498 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php @@ -4,6 +4,8 @@ namespace Drupal\Tests\content_moderation\Kernel; use Drupal\content_moderation\Entity\ContentModerationState; use Drupal\content_moderation\Entity\ModerationState; +use Drupal\entity_test\Entity\EntityTestBundle; +use Drupal\entity_test\Entity\EntityTestWithBundle; use Drupal\KernelTests\KernelTestBase; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; @@ -21,12 +23,14 @@ class ContentModerationStateTest extends KernelTestBase { * {@inheritdoc} */ public static $modules = [ + 'entity_test', 'node', 'content_moderation', 'user', 'system', 'language', 'content_translation', + 'text', ]; /** @@ -38,6 +42,7 @@ class ContentModerationStateTest extends KernelTestBase { $this->installSchema('node', 'node_access'); $this->installEntitySchema('node'); $this->installEntitySchema('user'); + $this->installEntitySchema('entity_test_with_bundle'); $this->installEntitySchema('content_moderation_state'); $this->installConfig('content_moderation'); } @@ -210,6 +215,91 @@ class ContentModerationStateTest extends KernelTestBase { $this->assertEquals(6, $english_node->getRevisionId()); } + /** + * Tests that a non-translatable entity type with a langcode can be moderated. + */ + public function testNonTranslatableEntityTypeModeration() { + // Make the 'entity_test_with_bundle' entity type revisionable. + $entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle'); + $keys = $entity_type->getKeys(); + $keys['revision'] = 'revision_id'; + $entity_type->set('entity_keys', $keys); + \Drupal::state()->set('entity_test_with_bundle.entity_type', $entity_type); + \Drupal::entityDefinitionUpdateManager()->applyUpdates(); + + // Create a test bundle. + $entity_test_bundle = EntityTestBundle::create([ + 'id' => 'example', + ]); + $entity_test_bundle->setThirdPartySetting('content_moderation', 'enabled', TRUE); + $entity_test_bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', [ + 'draft', + 'published' + ]); + $entity_test_bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); + $entity_test_bundle->save(); + + // Check that the tested entity type is not translatable. + $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle'); + $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.'); + + // Create a test entity. + $entity_test_with_bundle = EntityTestWithBundle::create([ + 'type' => 'example' + ]); + $entity_test_with_bundle->save(); + $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->entity->id()); + + $entity_test_with_bundle->moderation_state->target_id = 'published'; + $entity_test_with_bundle->save(); + + $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->entity->id()); + } + + /** + * Tests that a non-translatable entity type without a langcode can be + * moderated. + */ + public function testNonLangcodeEntityTypeModeration() { + // Make the 'entity_test_with_bundle' entity type revisionable and unset + // the langcode entity key. + $entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle'); + $keys = $entity_type->getKeys(); + $keys['revision'] = 'revision_id'; + unset($keys['langcode']); + $entity_type->set('entity_keys', $keys); + \Drupal::state()->set('entity_test_with_bundle.entity_type', $entity_type); + \Drupal::entityDefinitionUpdateManager()->applyUpdates(); + + // Create a test bundle. + $entity_test_bundle = EntityTestBundle::create([ + 'id' => 'example', + ]); + $entity_test_bundle->setThirdPartySetting('content_moderation', 'enabled', TRUE); + $entity_test_bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', [ + 'draft', + 'published' + ]); + $entity_test_bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); + $entity_test_bundle->save(); + + // Check that the tested entity type is not translatable. + $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle'); + $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.'); + + // Create a test entity. + $entity_test_with_bundle = EntityTestWithBundle::create([ + 'type' => 'example' + ]); + $entity_test_with_bundle->save(); + $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->entity->id()); + + $entity_test_with_bundle->moderation_state->target_id = 'published'; + $entity_test_with_bundle->save(); + + $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->entity->id()); + } + /** * Reloads the node after clearing the static cache. * diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityTypeInfoTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityTypeInfoTest.php new file mode 100644 index 000000000..1c45721d0 --- /dev/null +++ b/core/modules/content_moderation/tests/src/Kernel/EntityTypeInfoTest.php @@ -0,0 +1,63 @@ +entityTypeInfo = $this->container->get('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class); + $this->entityTypeManager = $this->container->get('entity_type.manager'); + } + + /** + * @covers ::entityBaseFieldInfo + */ + public function testEntityBaseFieldInfo() { + $definition = $this->entityTypeManager->getDefinition('entity_test'); + $definition->setHandlerClass('moderation', ModerationHandler::class); + + $base_fields = $this->entityTypeInfo->entityBaseFieldInfo($definition); + + $this->assertFalse($base_fields['moderation_state']->isReadOnly()); + $this->assertTrue($base_fields['moderation_state']->isComputed()); + $this->assertTrue($base_fields['moderation_state']->isTranslatable()); + } + +} diff --git a/core/modules/datetime/tests/src/Kernel/DateTimeFormInjectionTest.php b/core/modules/datetime/tests/src/Kernel/DateTimeFormInjectionTest.php new file mode 100644 index 000000000..a65d43928 --- /dev/null +++ b/core/modules/datetime/tests/src/Kernel/DateTimeFormInjectionTest.php @@ -0,0 +1,113 @@ +installSchema('system', ['key_value_expire', 'sequences']); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'datetime_test_injection_form'; + } + + /** + * Process callback. + * + * @param array $element + * Form element. + * + * @return array + * Processed element. + */ + public function process($element) { + return $element; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['datelist_element'] = [ + '#title' => 'datelist test', + '#type' => 'datelist', + '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'), + '#date_part_order' => [ + 'month', + 'day', + 'year', + 'hour', + 'minute', 'ampm', + ], + '#date_text_parts' => ['year'], + '#date_year_range' => '2010:2020', + '#date_increment' => 15, + ]; + $form['#process'][] = [$this, 'process']; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) {} + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->assertTrue(TRUE); + $form_state->setRebuild(); + } + + /** + * Tests custom string injection serialization. + */ + public function testDatetimeSerialization() { + $form_state = new FormState(); + $form_state->setRequestMethod('POST'); + $form_state->setCached(); + $form_builder = $this->container->get('form_builder'); + $form_id = $form_builder->getFormId($this, $form_state); + $form = $form_builder->retrieveForm($form_id, $form_state); + $form_builder->prepareForm($form_id, $form, $form_state); + $form_builder->processForm($form_id, $form, $form_state); + } + +} diff --git a/core/modules/field/migration_templates/d6_field.yml b/core/modules/field/migration_templates/d6_field.yml index 3157808f5..1205b67ef 100644 --- a/core/modules/field/migration_templates/d6_field.yml +++ b/core/modules/field/migration_templates/d6_field.yml @@ -123,4 +123,4 @@ process: - '@type' - global_settings destination: - plugin: md_entity:field_storage_config + plugin: entity:field_storage_config diff --git a/core/modules/field/migration_templates/d7_field.yml b/core/modules/field/migration_templates/d7_field.yml index 18135affb..069334322 100644 --- a/core/modules/field/migration_templates/d7_field.yml +++ b/core/modules/field/migration_templates/d7_field.yml @@ -22,6 +22,7 @@ process: datestamp: datetime datetime: datetime email: email + entityreference: entity_reference file: file image: image link_field: link diff --git a/core/modules/field/migration_templates/d7_field_formatter_settings.yml b/core/modules/field/migration_templates/d7_field_formatter_settings.yml index ee07ec4be..14cdd661a 100644 --- a/core/modules/field/migration_templates/d7_field_formatter_settings.yml +++ b/core/modules/field/migration_templates/d7_field_formatter_settings.yml @@ -51,7 +51,7 @@ process: - plugin: static_map bypass: true - source: type + source: formatter_type map: date_default: datetime_default email_default: email_mailto @@ -61,6 +61,9 @@ process: link_default: link phone: basic_string taxonomy_term_reference_link: entity_reference_label + entityreference_label: entity_reference_label + entityreference_entity_id: entity_reference_entity_id + entityreference_entity_view: entity_reference_entity_view - plugin: skip_on_empty method: row diff --git a/core/modules/field/migration_templates/d7_field_instance.yml b/core/modules/field/migration_templates/d7_field_instance.yml index b6d2497e5..6f9d07426 100644 --- a/core/modules/field/migration_templates/d7_field_instance.yml +++ b/core/modules/field/migration_templates/d7_field_instance.yml @@ -21,6 +21,7 @@ process: source: - instance_settings - widget_settings + - field_settings default_value_function: '' default_value: plugin: d7_field_instance_defaults diff --git a/core/modules/field/migration_templates/d7_field_instance_widget_settings.yml b/core/modules/field/migration_templates/d7_field_instance_widget_settings.yml index f88f2d205..b5ee41701 100644 --- a/core/modules/field/migration_templates/d7_field_instance_widget_settings.yml +++ b/core/modules/field/migration_templates/d7_field_instance_widget_settings.yml @@ -46,6 +46,7 @@ process: phone_textfield: telephone_default options_onoff: boolean_checkbox entityreference_autocomplete: entity_reference_autocomplete + entityreference_autocomplete_tags: entity_reference_autocomplete_tags taxonomy_autocomplete: entity_reference_autocomplete 'options/settings': plugin: field_instance_widget_settings diff --git a/core/modules/field/src/Plugin/migrate/process/d7/FieldInstanceSettings.php b/core/modules/field/src/Plugin/migrate/process/d7/FieldInstanceSettings.php index e8d59ad31..d28007986 100644 --- a/core/modules/field/src/Plugin/migrate/process/d7/FieldInstanceSettings.php +++ b/core/modules/field/src/Plugin/migrate/process/d7/FieldInstanceSettings.php @@ -17,9 +17,38 @@ class FieldInstanceSettings extends ProcessPluginBase { * {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { - list($instance_settings, $widget_settings) = $value; + list($instance_settings, $widget_settings, $field_settings) = $value; $widget_type = $widget_settings['type']; + // Get entityreference handler settings from source field configuration. + if ($row->getSourceProperty('type') == "entityreference") { + $instance_settings['handler'] = 'default:' . $field_settings['target_type']; + // Transform the sort settings to D8 structure. + $sort = [ + 'field' => '_none', + 'direction' => 'ASC', + ]; + if (!empty(array_filter($field_settings['handler_settings']['sort']))) { + if ($field_settings['handler_settings']['sort']['type'] == "property") { + $sort = [ + 'field' => $field_settings['handler_settings']['sort']['property'], + 'direction' => $field_settings['handler_settings']['sort']['direction'], + ]; + } + elseif ($field_settings['handler_settings']['sort']['type'] == "field") { + $sort = [ + 'field' => $field_settings['handler_settings']['sort']['field'], + 'direction' => $field_settings['handler_settings']['sort']['direction'], + ]; + } + } + if (empty($field_settings['handler_settings']['target_bundles'])) { + $field_settings['handler_settings']['target_bundles'] = NULL; + } + $field_settings['handler_settings']['sort'] = $sort; + $instance_settings['handler_settings'] = $field_settings['handler_settings']; + } + switch ($widget_type) { case 'image_image': $settings = $instance_settings; diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php index 7aa281f4e..5d3d6cc06 100644 --- a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php +++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php @@ -28,6 +28,7 @@ class FieldInstance extends DrupalSqlBase { ->fields('fc', array('type')); $query->innerJoin('field_config', 'fc', 'fci.field_id = fc.id'); + $query->addField('fc', 'data', 'field_data'); // Optionally filter by entity type and bundle. if (isset($this->configuration['entity_type'])) { @@ -53,6 +54,7 @@ class FieldInstance extends DrupalSqlBase { 'instance_settings' => $this->t('Field instance settings.'), 'widget_settings' => $this->t('Widget settings.'), 'display_settings' => $this->t('Display settings.'), + 'field_settings' => $this->t('Field settings.'), ); } @@ -81,6 +83,9 @@ class FieldInstance extends DrupalSqlBase { // This is for parity with the d6_field_instance plugin. $row->setSourceProperty('widget_type', $data['widget']['type']); + $field_data = unserialize($row->getSourceProperty('field_data')); + $row->setSourceProperty('field_settings', $field_data['settings']); + return parent::prepareRow($row); } diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstancePerViewMode.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstancePerViewMode.php index 17d4e183c..72a845bca 100644 --- a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstancePerViewMode.php +++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstancePerViewMode.php @@ -24,8 +24,15 @@ class FieldInstancePerViewMode extends DrupalSqlBase { $data = unserialize($field_instance['data']); // We don't need to include the serialized data in the returned rows. unset($field_instance['data']); + foreach ($data['display'] as $view_mode => $info) { - $rows[] = array_merge($field_instance, $info, array('view_mode' => $view_mode)); + // Rename type to formatter_type in the info array. + $info['formatter_type'] = $info['type']; + unset($info['type']); + + $rows[] = array_merge($field_instance, $info, [ + 'view_mode' => $view_mode, + ]); } } return new \ArrayIterator($rows); @@ -35,8 +42,11 @@ class FieldInstancePerViewMode extends DrupalSqlBase { * {@inheritdoc} */ public function query() { - return $this->select('field_config_instance', 'fci') - ->fields('fci', array('entity_type', 'bundle', 'field_name', 'data')); + $query = $this->select('field_config_instance', 'fci') + ->fields('fci', ['entity_type', 'bundle', 'field_name', 'data']) + ->fields('fc', ['type']); + $query->join('field_config', 'fc', 'fc.field_name = fci.field_name'); + return $query; } /** @@ -49,7 +59,8 @@ class FieldInstancePerViewMode extends DrupalSqlBase { 'field_name' => $this->t('Machine name of the field.'), 'view_mode' => $this->t('The original machine name of the view mode.'), 'label' => $this->t('The display label of the field.'), - 'type' => $this->t('The formatter ID.'), + 'type' => $this->t('The field ID.'), + 'formatter_type' => $this->t('The formatter ID.'), 'settings' => $this->t('Array of formatter-specific settings.'), 'module' => $this->t('The module providing the formatter.'), 'weight' => $this->t('Display weight of the field.'), diff --git a/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php b/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php index 30f67d1e6..5fa491b20 100644 --- a/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php +++ b/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php @@ -20,7 +20,12 @@ class BooleanFieldTest extends WebTestBase { * * @var array */ - public static $modules = array('entity_test', 'field_ui', 'options'); + public static $modules = [ + 'entity_test', + 'field_ui', + 'options', + 'field_test_boolean_access_denied', + ]; /** * A field to use in this test class. @@ -179,4 +184,66 @@ class BooleanFieldTest extends WebTestBase { $this->assertFieldById('edit-settings-off-label', $off); } + /** + * Test field access. + */ + public function testFormAccess() { + $on = 'boolean_on'; + $off = 'boolean_off'; + $label = 'boolean_label'; + $field_name = 'boolean_name'; + $this->fieldStorage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'boolean', + ]); + $this->fieldStorage->save(); + $this->field = FieldConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + 'label' => $label, + 'settings' => [ + 'on_label' => $on, + 'off_label' => $off, + ], + ]); + $this->field->save(); + + // Create a form display for the default form mode. + entity_get_form_display('entity_test', 'entity_test', 'default') + ->setComponent($field_name, [ + 'type' => 'boolean_checkbox', + ]) + ->save(); + + // Create a display for the full view mode. + entity_get_display('entity_test', 'entity_test', 'full') + ->setComponent($field_name, [ + 'type' => 'boolean', + ]) + ->save(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + $this->assertFieldByName("{$field_name}[value]"); + + // Should be posted OK. + $this->drupalPostForm(NULL, [], t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); + + // Tell the test module to disable access to the field. + \Drupal::state()->set('field.test_boolean_field_access_field', $field_name); + $this->drupalGet('entity_test/add'); + // Field should not be there anymore. + $this->assertNoFieldByName("{$field_name}[value]"); + // Should still be able to post the form. + $this->drupalPostForm(NULL, [], t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); + } + } diff --git a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestObjectItem.php b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestObjectItem.php new file mode 100644 index 000000000..a20265e55 --- /dev/null +++ b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestObjectItem.php @@ -0,0 +1,62 @@ +setLabel(t('Value')) + ->setRequired(TRUE); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + return [ + 'columns' => [ + 'value' => [ + 'description' => 'The object item value.', + 'type' => 'blob', + 'not null' => TRUE, + 'serialize' => TRUE, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function setValue($values, $notify = TRUE) { + if (isset($values['value'])) { + // @todo Remove this in https://www.drupal.org/node/2788637. + if (is_string($values['value'])) { + $values['value'] = unserialize($values['value']); + } + } + parent::setValue($values, $notify); + } + +} diff --git a/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.info.yml b/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.info.yml new file mode 100644 index 000000000..d1b578f46 --- /dev/null +++ b/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.info.yml @@ -0,0 +1,8 @@ +name: 'Boolean field Test' +type: module +description: 'Support module for the field and entity display tests.' +core: 8.x +package: Testing +version: VERSION +dependencies: + - field diff --git a/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.module b/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.module new file mode 100644 index 000000000..e1ae2f737 --- /dev/null +++ b/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.module @@ -0,0 +1,18 @@ +getName() === \Drupal::state()->get('field.test_boolean_field_access_field')); +} diff --git a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php index d4be1dadc..aaee7d080 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php @@ -277,6 +277,49 @@ class EntityReferenceFormatterTest extends EntityKernelTestBase { $this->assertEqual($actual_occurrences, $expected_occurrences); } + /** + * Renders the same entity referenced from different places. + */ + public function testEntityReferenceRecursiveProtectionWithManyRenderedEntities() { + $formatter = 'entity_reference_entity_view'; + $view_builder = $this->entityManager->getViewBuilder($this->entityType); + + // Set the default view mode to use the 'entity_reference_entity_view' + // formatter. + entity_get_display($this->entityType, $this->bundle, 'default') + ->setComponent($this->fieldName, [ + 'type' => $formatter, + ]) + ->save(); + + $storage = $this->entityManager->getStorage($this->entityType); + /** @var \Drupal\Core\Entity\ContentEntityInterface $referenced_entity */ + $referenced_entity = $storage->create(['name' => $this->randomMachineName()]); + + $range = range(0, 30); + $referencing_entities = array_map(function () use ($storage, $referenced_entity) { + $referencing_entity = $storage->create([ + 'name' => $this->randomMachineName(), + $this->fieldName => $referenced_entity, + ]); + $referencing_entity->save(); + return $referencing_entity; + }, $range); + + $build = $view_builder->viewMultiple($referencing_entities, 'default'); + $output = $this->render($build); + + // The title of entity_test entities is printed twice by default, so we have + // to multiply the formatter's recursive rendering protection limit by 2. + // Additionally, we have to take into account 2 additional occurrences of + // the entity title because we're rendering the full entity, not just the + // reference field. + $expected_occurrences = 30 * 2 + 2; + $actual_occurrences = substr_count($output, $referenced_entity->get('name')->value); + $this->assertEquals($expected_occurrences, $actual_occurrences); + } + + /** * Tests the label formatter. */ diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldFormatterSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldFormatterSettingsTest.php index 21cc3aa67..14f37cb41 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldFormatterSettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldFormatterSettingsTest.php @@ -276,6 +276,9 @@ class MigrateFieldFormatterSettingsTest extends MigrateDrupal7TestBase { $this->assertComponent('node.test_content_type.default', 'field_text_list', 'list_default', 'above', 10); $this->assertComponent('node.test_content_type.default', 'field_integer_list', 'list_default', 'above', 11); $this->assertComponent('node.test_content_type.default', 'field_long_text', 'text_default', 'above', 12); + $this->assertComponent('node.test_content_type.default', 'field_node_entityreference', 'entity_reference_label', 'above', 15); + $this->assertComponent('node.test_content_type.default', 'field_user_entityreference', 'entity_reference_label', 'above', 16); + $this->assertComponent('node.test_content_type.default', 'field_term_entityreference', 'entity_reference_label', 'above', 17); $this->assertComponentNotExists('node.test_content_type.default', 'field_term_reference'); $this->assertComponentNotExists('node.test_content_type.default', 'field_text'); diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceTest.php index 2fa6d5cc0..7c2115fbe 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceTest.php @@ -141,6 +141,9 @@ class MigrateFieldInstanceTest extends MigrateDrupal7TestBase { $this->assertEntity('node.test_content_type.field_integer_list', 'Integer List', 'list_integer', FALSE); $this->assertEntity('node.test_content_type.field_long_text', 'Long text', 'text_with_summary', FALSE); $this->assertEntity('node.test_content_type.field_term_reference', 'Term Reference', 'entity_reference', FALSE); + $this->assertEntity('node.test_content_type.field_node_entityreference', 'Node Entity Reference', 'entity_reference', FALSE); + $this->assertEntity('node.test_content_type.field_user_entityreference', 'User Entity Reference', 'entity_reference', FALSE); + $this->assertEntity('node.test_content_type.field_term_entityreference', 'Term Entity Reference', 'entity_reference', FALSE); $this->assertEntity('node.test_content_type.field_text', 'Text', 'text', FALSE); $this->assertEntity('comment.comment_node_test_content_type.field_integer', 'Integer', 'integer', FALSE); $this->assertEntity('user.user.field_file', 'File', 'file', FALSE); diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceWidgetSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceWidgetSettingsTest.php index dd4decd00..7f2e0244f 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceWidgetSettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceWidgetSettingsTest.php @@ -125,6 +125,9 @@ class MigrateFieldInstanceWidgetSettingsTest extends MigrateDrupal7TestBase { $this->assertComponent('node.test_content_type.default', 'field_long_text', 'text_textarea_with_summary', 13); $this->assertComponent('node.test_content_type.default', 'field_phone', 'telephone_default', 6); $this->assertComponent('node.test_content_type.default', 'field_term_reference', 'entity_reference_autocomplete', 14); + $this->assertComponent('node.test_content_type.default', 'field_node_entityreference', 'entity_reference_autocomplete', 16); + $this->assertComponent('node.test_content_type.default', 'field_user_entityreference', 'options_buttons', 17); + $this->assertComponent('node.test_content_type.default', 'field_term_entityreference', 'entity_reference_autocomplete_tags', 18); $this->assertComponent('node.test_content_type.default', 'field_text', 'text_textfield', 15); $this->assertComponent('node.test_content_type.default', 'field_text_list', 'options_select', 11); diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php index b1c2fb8b3..fb275c67c 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php @@ -100,6 +100,9 @@ class MigrateFieldTest extends MigrateDrupal7TestBase { $this->assertEntity('node.field_phone', 'telephone', FALSE, 1); $this->assertEntity('node.field_date', 'datetime', FALSE, 1); $this->assertEntity('node.field_date_with_end_time', 'datetime', FALSE, 1); + $this->assertEntity('node.field_node_entityreference', 'entity_reference', FALSE, -1); + $this->assertEntity('node.field_user_entityreference', 'entity_reference', FALSE, 1); + $this->assertEntity('node.field_term_entityreference', 'entity_reference', FALSE, -1); // Assert that the taxonomy term reference fields are referencing the // correct entity type. @@ -108,6 +111,15 @@ class MigrateFieldTest extends MigrateDrupal7TestBase { $field = FieldStorageConfig::load('node.taxonomy_forums'); $this->assertIdentical('taxonomy_term', $field->getSetting('target_type')); + // Assert that the entityreference fields are referencing the correct + // entity type. + $field = FieldStorageConfig::load('node.field_node_entityreference'); + $this->assertIdentical('node', $field->getSetting('target_type')); + $field = FieldStorageConfig::load('node.field_user_entityreference'); + $this->assertIdentical('user', $field->getSetting('target_type')); + $field = FieldStorageConfig::load('node.field_term_entityreference'); + $this->assertIdentical('taxonomy_term', $field->getSetting('target_type')); + // Validate that the source count and processed count match up. /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ $migration = $this->getMigration('d7_field'); diff --git a/core/modules/field/tests/src/Kernel/Plugin/migrate/source/d7/FieldInstancePerViewModeTest.php b/core/modules/field/tests/src/Kernel/Plugin/migrate/source/d7/FieldInstancePerViewModeTest.php index 6ed72e43c..ff5993e62 100644 --- a/core/modules/field/tests/src/Kernel/Plugin/migrate/source/d7/FieldInstancePerViewModeTest.php +++ b/core/modules/field/tests/src/Kernel/Plugin/migrate/source/d7/FieldInstancePerViewModeTest.php @@ -41,6 +41,24 @@ class FieldInstancePerViewModeTest extends MigrateSqlSourceTestBase { ], ]; + $tests[0]['source_data']['field_config'] = [ + [ + 'id' => '2', + 'field_name' => 'body', + 'type' => 'text_with_summary', + 'module' => 'text', + 'active' => '1', + 'storage_type' => 'field_sql_storage', + 'storage_module' => 'field_sql_storage', + 'storage_active' => '1', + 'locked' => '0', + 'data' => 'a:7:{s:12:"entity_types";a:1:{i:0;s:4:"node";}s:7:"indexes";a:1:{s:6:"format";a:1:{i:0;s:6:"format";}}s:8:"settings";a:0:{}s:12:"translatable";i:0;s:12:"foreign keys";a:1:{s:6:"format";a:2:{s:5:"table";s:13:"filter_format";s:7:"columns";a:1:{s:6:"format";s:6:"format";}}}s:7:"storage";a:4:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";}s:2:"id";s:2:"25";}', + 'cardinality' => '1', + 'translatable' => '0', + 'deleted' => '0', + ], + ]; + // The expected results. $tests[0]['expected_data'] = [ [ @@ -48,7 +66,8 @@ class FieldInstancePerViewModeTest extends MigrateSqlSourceTestBase { 'bundle' => 'page', 'field_name' => 'body', 'label' => 'hidden', - 'type' => 'text_default', + 'type' => 'text_with_summary', + 'formatter_type' => 'text_default', 'settings' => [], 'module' => 'text', 'weight' => 0, @@ -59,7 +78,8 @@ class FieldInstancePerViewModeTest extends MigrateSqlSourceTestBase { 'bundle' => 'page', 'field_name' => 'body', 'label' => 'hidden', - 'type' => 'text_summary_or_trimmed', + 'type' => 'text_with_summary', + 'formatter_type' => 'text_summary_or_trimmed', 'settings' => [ 'trim_length' => 600, ], diff --git a/core/modules/field/tests/src/Kernel/String/UuidItemTest.php b/core/modules/field/tests/src/Kernel/String/UuidItemTest.php new file mode 100644 index 000000000..480057b90 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/String/UuidItemTest.php @@ -0,0 +1,30 @@ +save(); + + $uuid_field = $entity->get('uuid'); + + // Test the generateSampleValue() method. + $uuid_field->generateSampleItems(); + $this->assertTrue(Uuid::isValid($uuid_field->value)); + } + +} diff --git a/core/modules/field/tests/src/Kernel/TestObjectItemTest.php b/core/modules/field/tests/src/Kernel/TestObjectItemTest.php new file mode 100644 index 000000000..8a7875581 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/TestObjectItemTest.php @@ -0,0 +1,59 @@ + 'field_test', + 'entity_type' => 'entity_test', + 'type' => 'test_object_field', + ))->save(); + FieldConfig::create([ + 'entity_type' => 'entity_test', + 'field_name' => 'field_test', + 'bundle' => 'entity_test', + ])->save(); + } + + /** + * Tests the serialization of a field type that has an object. + */ + public function testTestObjectItem() { + $object = new \stdClass(); + $object->foo = 'bar'; + $entity = EntityTest::create(); + $entity->field_test->value = $object; + $entity->save(); + + // Verify that the entity has been created properly. + $id = $entity->id(); + $entity = EntityTest::load($id); + $this->assertTrue($entity->field_test->value instanceof \stdClass); + $this->assertEquals($object, $entity->field_test->value); + } + +} diff --git a/core/modules/field/tests/src/Unit/Plugin/migrate/process/d7/FieldInstanceSettingsTest.php b/core/modules/field/tests/src/Unit/Plugin/migrate/process/d7/FieldInstanceSettingsTest.php index 174271a1b..8c405b04b 100644 --- a/core/modules/field/tests/src/Unit/Plugin/migrate/process/d7/FieldInstanceSettingsTest.php +++ b/core/modules/field/tests/src/Unit/Plugin/migrate/process/d7/FieldInstanceSettingsTest.php @@ -28,7 +28,7 @@ class FieldInstanceSettingsTest extends MigrateTestCase { ->disableOriginalConstructor() ->getMock(); - $value = $plugin->transform([[], ['type' => 'image_image']], $executable, $row, 'foo'); + $value = $plugin->transform([[], ['type' => 'image_image'], []], $executable, $row, 'foo'); $this->assertInternalType('array', $value['default_image']); $this->assertSame('', $value['default_image']['alt']); $this->assertSame('', $value['default_image']['title']); diff --git a/core/modules/file/migration_templates/d6_file.yml b/core/modules/file/migration_templates/d6_file.yml index 0fdcd8403..6df20b522 100644 --- a/core/modules/file/migration_templates/d6_file.yml +++ b/core/modules/file/migration_templates/d6_file.yml @@ -13,6 +13,9 @@ source: # configuration in this migration's process pipeline as an example. source_base_path: '' process: + # If you are using both this migration and d6_user_picture_file in a custom + # migration and executing migrations incrementally, it is recommended that + # you remove the fid mapping here to avoid potential ID conflicts. fid: fid filename: filename source_full_path: diff --git a/core/modules/file/migration_templates/d6_upload_field.yml b/core/modules/file/migration_templates/d6_upload_field.yml index c0b4569a6..a919f918f 100644 --- a/core/modules/file/migration_templates/d6_upload_field.yml +++ b/core/modules/file/migration_templates/d6_upload_field.yml @@ -20,4 +20,7 @@ process: cardinality: 'constants/cardinality' 'settings/display_field': 'constants/display_field' destination: - plugin: md_entity:field_storage_config + plugin: entity:field_storage_config + dependencies: + module: + - file diff --git a/core/modules/file/migration_templates/d7_file.yml b/core/modules/file/migration_templates/d7_file.yml index ffd85ab55..7ebf83bf3 100644 --- a/core/modules/file/migration_templates/d7_file.yml +++ b/core/modules/file/migration_templates/d7_file.yml @@ -13,6 +13,8 @@ source: # configuration in this migration's process pipeline as an example. source_base_path: '' process: + # If you are using this file to build a custom migration consider removing + # the fid field to allow incremental migrations. fid: fid filename: filename source_full_path: diff --git a/core/modules/file/src/Plugin/migrate/process/d6/CckFile.php b/core/modules/file/src/Plugin/migrate/process/d6/CckFile.php index 0415c7ffb..8cf2d19a4 100644 --- a/core/modules/file/src/Plugin/migrate/process/d6/CckFile.php +++ b/core/modules/file/src/Plugin/migrate/process/d6/CckFile.php @@ -5,7 +5,6 @@ namespace Drupal\file\Plugin\migrate\process\d6; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\MigrateExecutableInterface; -use Drupal\migrate\MigrateSkipRowException; use Drupal\migrate\Plugin\MigrateProcessInterface; use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\Row; @@ -76,18 +75,7 @@ class CckFile extends ProcessPluginBase implements ContainerFactoryPluginInterfa // some reason -- file migration is notoriously brittle -- and we do NOT // want to send invalid file references into the field system (it causes // fatals), so return an empty item instead. - try { - $fid = $this->migrationPlugin->transform($value['fid'], $migrate_executable, $row, $destination_property); - } - // If the migration plugin completely fails its lookup process, it will - // throw a MigrateSkipRowException. It shouldn't, but that is being dealt - // with at https://www.drupal.org/node/2487568. Until that lands, return - // an empty item. - catch (MigrateSkipRowException $e) { - return []; - } - - if ($fid) { + if ($fid = $this->migrationPlugin->transform($value['fid'], $migrate_executable, $row, $destination_property)) { return [ 'target_id' => $fid, 'display' => $value['list'], diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Block/BlockHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Block/BlockHalJsonAnonTest.php new file mode 100644 index 000000000..d0758f347 --- /dev/null +++ b/core/modules/hal/tests/src/Functional/EntityResource/Block/BlockHalJsonAnonTest.php @@ -0,0 +1,35 @@ +applyHalFieldNormalization($default_normalization); + + // Because \Drupal\comment\Entity\Comment::getOwner() generates an in-memory + // User entity without a UUID, we cannot use it. + $author = User::load($this->entity->getOwnerId()); + $commented_entity = EntityTest::load(1); + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/comment/1?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/comment/comment', + ], + $this->baseUrl . '/rest/relation/comment/comment/entity_id' => [ + [ + 'href' => $this->baseUrl . '/entity_test/1?_format=hal_json', + ], + ], + $this->baseUrl . '/rest/relation/comment/comment/uid' => [ + [ + 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json', + 'lang' => 'en', + ], + ], + ], + '_embedded' => [ + $this->baseUrl . '/rest/relation/comment/comment/entity_id' => [ + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/entity_test/1?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/entity_test/bar', + ], + ], + 'uuid' => [ + ['value' => $commented_entity->uuid()] + ], + ], + ], + $this->baseUrl . '/rest/relation/comment/comment/uid' => [ + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/user/user', + ], + ], + 'uuid' => [ + ['value' => $author->uuid()] + ], + 'lang' => 'en', + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + '_links' => [ + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/comment/comment', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + // The 'url.site' cache context is added for '_links' in the response. + return Cache::mergeTags(parent::getExpectedCacheContexts(), ['url.site']); + } + +} diff --git a/core/modules/hal/tests/src/Functional/EntityResource/ConfigTest/ConfigTestHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/ConfigTest/ConfigTestHalJsonAnonTest.php new file mode 100644 index 000000000..45cedb2a9 --- /dev/null +++ b/core/modules/hal/tests/src/Functional/EntityResource/ConfigTest/ConfigTestHalJsonAnonTest.php @@ -0,0 +1,35 @@ +applyHalFieldNormalization($default_normalization); + + $author = User::load(0); + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/entity_test/1?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/entity_test/entity_test', + ], + $this->baseUrl . '/rest/relation/entity_test/entity_test/user_id' => [ + [ + 'href' => $this->baseUrl . '/user/0?_format=hal_json', + 'lang' => 'en', + ], + ], + ], + '_embedded' => [ + $this->baseUrl . '/rest/relation/entity_test/entity_test/user_id' => [ + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/user/0?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/user/user', + ], + ], + 'uuid' => [ + ['value' => $author->uuid()] + ], + 'lang' => 'en', + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + '_links' => [ + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/entity_test/entity_test', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + // The 'url.site' cache context is added for '_links' in the response. + return Cache::mergeTags(parent::getExpectedCacheContexts(), ['url.site']); + } + +} diff --git a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonBasicAuthTest.php b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonBasicAuthTest.php new file mode 100644 index 000000000..5604e3b54 --- /dev/null +++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonBasicAuthTest.php @@ -0,0 +1,30 @@ +entity instanceof FieldableEntityInterface) { + throw new \LogicException('This trait should only be used for fieldable entity types.'); + } + + // In the HAL normalization, all translatable fields get a 'lang' attribute. + $translatable_non_reference_fields = array_keys(array_filter($this->entity->getTranslatableFields(), function (FieldItemListInterface $field) { + return !$field instanceof EntityReferenceFieldItemListInterface; + })); + foreach ($translatable_non_reference_fields as $field_name) { + if (isset($normalization[$field_name])) { + $normalization[$field_name][0]['lang'] = 'en'; + } + } + + // In the HAL normalization, reference fields are omitted, except for the + // bundle field. + $bundle_key = $this->entity->getEntityType()->getKey('bundle'); + $reference_fields = array_keys(array_filter($this->entity->getFields(), function (FieldItemListInterface $field) use ($bundle_key) { + return $field instanceof EntityReferenceFieldItemListInterface && $field->getName() !== $bundle_key; + })); + foreach ($reference_fields as $field_name) { + unset($normalization[$field_name]); + } + + // In the HAL normalization, the bundle field omits the 'target_type' and + // 'target_uuid' properties, because it's encoded in the '_links' section. + if ($bundle_key) { + unset($normalization[$bundle_key][0]['target_type']); + unset($normalization[$bundle_key][0]['target_uuid']); + } + + // In the HAL normalization, empty fields are omitted. + $empty_fields = array_keys(array_filter($this->entity->getFields(), function (FieldItemListInterface $field) { + return $field->isEmpty(); + })); + foreach ($empty_fields as $field_name) { + unset($normalization[$field_name]); + } + + return $normalization; + } + + /** + * {@inheritdoc} + */ + protected function removeFieldsFromNormalization(array $normalization, $field_names) { + $normalization = parent::removeFieldsFromNormalization($normalization, $field_names); + foreach ($field_names as $field_name) { + $relation_url = Url::fromUri('base:rest/relation/' . static::$entityTypeId . '/' . $this->entity->bundle() . '/' . $field_name) + ->setAbsolute(TRUE) + ->toString(); + $normalization['_links'] = array_diff_key($normalization['_links'], [$relation_url => TRUE]); + if (isset($normalization['_embedded'])) { + $normalization['_embedded'] = array_diff_key($normalization['_embedded'], [$relation_url => TRUE]); + } + } + + return array_diff_key($normalization, array_flip($field_names)); + } + + /** + * {@inheritdoc} + */ + protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) { + // \Drupal\serialization\Normalizer\EntityNormalizer::denormalize(): entity + // types with bundles MUST send their bundle field to be denormalizable. + if ($this->entity->getEntityType()->hasKey('bundle')) { + $normalization = $this->getNormalizedPostEntity(); + + // @todo Uncomment this in https://www.drupal.org/node/2824827. + // @codingStandardsIgnoreStart +/* + $normalization['_links']['type'] = Url::fromUri('base:rest/type/' . static::$entityTypeId . '/bad_bundle_name'); + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + // DX: 400 when incorrect entity type bundle is specified. + $response = $this->request($method, $url, $request_options); + // @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813853. +// $this->assertResourceErrorResponse(400, 'The type link relation must be specified.', $response); + $this->assertSame(400, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['error' => 'The type link relation must be specified.'], static::$format), (string) $response->getBody()); +*/ + // @codingStandardsIgnoreEnd + + unset($normalization['_links']['type']); + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // DX: 400 when no entity type bundle is specified. + $response = $this->request($method, $url, $request_options); + // @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813853. + // $this->assertResourceErrorResponse(400, 'The type link relation must be specified.', $response); + $this->assertSame(400, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['error' => 'The type link relation must be specified.'], static::$format), (string) $response->getBody()); + } + } + +} diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php new file mode 100644 index 000000000..c7f242812 --- /dev/null +++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php @@ -0,0 +1,137 @@ +applyHalFieldNormalization($default_normalization); + + $author = User::load($this->entity->getOwnerId()); + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/node/1?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/node/camelids', + ], + $this->baseUrl . '/rest/relation/node/camelids/uid' => [ + [ + 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json', + 'lang' => 'en', + ], + ], + $this->baseUrl . '/rest/relation/node/camelids/revision_uid' => [ + [ + 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json', + ], + ], + ], + '_embedded' => [ + $this->baseUrl . '/rest/relation/node/camelids/uid' => [ + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/user/user', + ], + ], + 'uuid' => [ + ['value' => $author->uuid()] + ], + 'lang' => 'en', + ], + ], + $this->baseUrl . '/rest/relation/node/camelids/revision_uid' => [ + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/user/user', + ], + ], + 'uuid' => [ + ['value' => $author->uuid()] + ], + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + '_links' => [ + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/node/camelids', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + // The 'url.site' cache context is added for '_links' in the response. + return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']); + } + +} diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonBasicAuthTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonBasicAuthTest.php new file mode 100644 index 000000000..1d7bb6234 --- /dev/null +++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonBasicAuthTest.php @@ -0,0 +1,30 @@ +applyHalFieldNormalization($default_normalization); + + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/taxonomy/term/1?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + '_links' => [ + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + // The 'url.site' cache context is added for '_links' in the response. + return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']); + } + +} diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonBasicAuthTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonBasicAuthTest.php new file mode 100644 index 000000000..8c7b04b65 --- /dev/null +++ b/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonBasicAuthTest.php @@ -0,0 +1,30 @@ +applyHalFieldNormalization($default_normalization); + + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/user/3?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/user/user', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + '_links' => [ + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/user/user', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + // The 'url.site' cache context is added for '_links' in the response. + return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']); + } + +} diff --git a/core/modules/hal/tests/src/Functional/EntityResource/User/UserHalJsonBasicAuthTest.php b/core/modules/hal/tests/src/Functional/EntityResource/User/UserHalJsonBasicAuthTest.php new file mode 100644 index 000000000..dbf17cb54 --- /dev/null +++ b/core/modules/hal/tests/src/Functional/EntityResource/User/UserHalJsonBasicAuthTest.php @@ -0,0 +1,30 @@ +markTestSkipped(); + } + +} diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Vocabulary/VocabularyHalJsonBasicAuthTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Vocabulary/VocabularyHalJsonBasicAuthTest.php new file mode 100644 index 000000000..4f7896e9b --- /dev/null +++ b/core/modules/hal/tests/src/Functional/EntityResource/Vocabulary/VocabularyHalJsonBasicAuthTest.php @@ -0,0 +1,46 @@ +assertSame(401, $response->getStatusCode()); + // @todo this works fine locally, but on testbot it comes back with + // 'text/plain; charset=UTF-8'. WTF. + // $this->assertSame(['application/hal+json'], $response->getHeader('Content-Type')); + $this->assertSame('No authentication credentials provided.', (string) $response->getBody()); + } + +} diff --git a/core/modules/hal/tests/src/Kernel/DenormalizeTest.php b/core/modules/hal/tests/src/Kernel/DenormalizeTest.php index 64c8311f7..8f6404991 100644 --- a/core/modules/hal/tests/src/Kernel/DenormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/DenormalizeTest.php @@ -7,7 +7,7 @@ use Drupal\field\Entity\FieldConfig; use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** - * Tests that entities can be denormalized from HAL. + * Tests HAL denormalization edge cases for EntityResource. * * @group hal */ @@ -110,98 +110,4 @@ class DenormalizeTest extends NormalizerTestBase { $this->assertEqual($entity->field_test_text->count(), 0); } - /** - * Test that non-reference fields can be denormalized. - */ - public function testBasicFieldDenormalization() { - $data = array( - '_links' => array( - 'type' => array( - 'href' => Url::fromUri('base:rest/type/entity_test/entity_test', array('absolute' => TRUE))->toString(), - ), - ), - 'uuid' => array( - array( - 'value' => 'e5c9fb96-3acf-4a8d-9417-23de1b6c3311', - ), - ), - 'field_test_text' => array( - array( - 'value' => $this->randomMachineName(), - 'format' => 'full_html', - ), - ), - 'field_test_translatable_text' => array( - array( - 'value' => $this->randomMachineName(), - 'format' => 'full_html', - ), - array( - 'value' => $this->randomMachineName(), - 'format' => 'filtered_html', - ), - array( - 'value' => $this->randomMachineName(), - 'format' => 'filtered_html', - 'lang' => 'de', - ), - array( - 'value' => $this->randomMachineName(), - 'format' => 'full_html', - 'lang' => 'de', - ), - ), - ); - - $expected_value_default = array( - array ( - 'value' => $data['field_test_translatable_text'][0]['value'], - 'format' => 'full_html', - ), - array ( - 'value' => $data['field_test_translatable_text'][1]['value'], - 'format' => 'filtered_html', - ), - ); - $expected_value_de = array( - array ( - 'value' => $data['field_test_translatable_text'][2]['value'], - 'format' => 'filtered_html', - ), - array ( - 'value' => $data['field_test_translatable_text'][3]['value'], - 'format' => 'full_html', - ), - ); - $denormalized = $this->serializer->denormalize($data, $this->entityClass, $this->format); - $this->assertEqual($data['uuid'], $denormalized->get('uuid')->getValue(), 'A preset value (e.g. UUID) is overridden by incoming data.'); - $this->assertEqual($data['field_test_text'], $denormalized->get('field_test_text')->getValue(), 'A basic text field is denormalized.'); - $this->assertEqual($expected_value_default, $denormalized->get('field_test_translatable_text')->getValue(), 'Values in the default language are properly handled for a translatable field.'); - $this->assertEqual($expected_value_de, $denormalized->getTranslation('de')->get('field_test_translatable_text')->getValue(), 'Values in a translation language are properly handled for a translatable field.'); - } - - /** - * Verifies that the denormalized entity is correct in the PATCH context. - */ - public function testPatchDenormalization() { - $data = array( - '_links' => array( - 'type' => array( - 'href' => Url::fromUri('base:rest/type/entity_test/entity_test', array('absolute' => TRUE))->toString(), - ), - ), - 'field_test_text' => array( - array( - 'value' => $this->randomMachineName(), - 'format' => 'full_html', - ), - ), - ); - $denormalized = $this->serializer->denormalize($data, $this->entityClass, $this->format, array('request_method' => 'patch')); - // Check that the one field got populated as expected. - $this->assertEqual($data['field_test_text'], $denormalized->get('field_test_text')->getValue()); - // Check the custom property that contains the list of fields to merge. - $this->assertEqual($denormalized->_restSubmittedFields, ['field_test_text']); - } - } diff --git a/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php b/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php deleted file mode 100644 index 88db403da..000000000 --- a/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php +++ /dev/null @@ -1,206 +0,0 @@ -rebuild(); - $this->installSchema('system', array('sequences')); - $this->installSchema('comment', array('comment_entity_statistics')); - $this->installEntitySchema('taxonomy_term'); - $this->installConfig(['node', 'comment']); - } - - /** - * Tests the normalization of nodes. - */ - public function testNode() { - $node_type = NodeType::create(['type' => 'example_type']); - $node_type->save(); - - $user = User::create(['name' => $this->randomMachineName()]); - $user->save(); - - // Add comment type. - $this->container->get('entity.manager')->getStorage('comment_type')->create(array( - 'id' => 'comment', - 'label' => 'comment', - 'target_entity_type_id' => 'node', - ))->save(); - - $this->addDefaultCommentField('node', 'example_type'); - - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'uid' => $user->id(), - 'type' => $node_type->id(), - 'status' => NODE_PUBLISHED, - 'promote' => 1, - 'sticky' => 0, - 'body' => [ - 'value' => $this->randomMachineName(), - 'format' => $this->randomMachineName() - ], - 'revision_log' => $this->randomString(), - ]); - $node->save(); - - $original_values = $node->toArray(); - - $normalized = $this->serializer->normalize($node, $this->format); - - /** @var \Drupal\node\NodeInterface $denormalized_node */ - $denormalized_node = $this->serializer->denormalize($normalized, 'Drupal\node\Entity\Node', $this->format); - - $this->assertEqual($original_values, $denormalized_node->toArray(), 'Node values are restored after normalizing and denormalizing.'); - } - - /** - * Tests the normalization of terms. - */ - public function testTerm() { - $vocabulary = Vocabulary::create(['vid' => 'example_vocabulary']); - $vocabulary->save(); - - $account = User::create(['name' => $this->randomMachineName()]); - $account->save(); - - // @todo Until https://www.drupal.org/node/2327935 is fixed, if no parent is - // set, the test fails because target_id => 0 is reserialized to NULL. - $term_parent = Term::create([ - 'name' => $this->randomMachineName(), - 'vid' => $vocabulary->id(), - ]); - $term_parent->save(); - $term = Term::create([ - 'name' => $this->randomMachineName(), - 'vid' => $vocabulary->id(), - 'description' => array( - 'value' => $this->randomMachineName(), - 'format' => $this->randomMachineName(), - ), - 'parent' => $term_parent->id(), - ]); - $term->save(); - - $original_values = $term->toArray(); - - $normalized = $this->serializer->normalize($term, $this->format, ['account' => $account]); - - /** @var \Drupal\taxonomy\TermInterface $denormalized_term */ - $denormalized_term = $this->serializer->denormalize($normalized, 'Drupal\taxonomy\Entity\Term', $this->format, ['account' => $account]); - - $this->assertEqual($original_values, $denormalized_term->toArray(), 'Term values are restored after normalizing and denormalizing.'); - } - - /** - * Tests the normalization of comments. - */ - public function testComment() { - $node_type = NodeType::create(['type' => 'example_type']); - $node_type->save(); - - $account = User::create(['name' => $this->randomMachineName()]); - $account->save(); - - // Add comment type. - $this->container->get('entity.manager')->getStorage('comment_type')->create(array( - 'id' => 'comment', - 'label' => 'comment', - 'target_entity_type_id' => 'node', - ))->save(); - - $this->addDefaultCommentField('node', 'example_type'); - - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'uid' => $account->id(), - 'type' => $node_type->id(), - 'status' => NODE_PUBLISHED, - 'promote' => 1, - 'sticky' => 0, - 'body' => [[ - 'value' => $this->randomMachineName(), - 'format' => $this->randomMachineName() - ]], - ]); - $node->save(); - - $parent_comment = Comment::create(array( - 'uid' => $account->id(), - 'subject' => $this->randomMachineName(), - 'comment_body' => [ - 'value' => $this->randomMachineName(), - 'format' => NULL, - ], - 'entity_id' => $node->id(), - 'entity_type' => 'node', - 'field_name' => 'comment', - )); - $parent_comment->save(); - - $comment = Comment::create(array( - 'uid' => $account->id(), - 'subject' => $this->randomMachineName(), - 'comment_body' => [ - 'value' => $this->randomMachineName(), - 'format' => NULL, - ], - 'entity_id' => $node->id(), - 'entity_type' => 'node', - 'field_name' => 'comment', - 'pid' => $parent_comment->id(), - 'mail' => 'dries@drupal.org', - 'homepage' => 'http://buytaert.net', - )); - $comment->save(); - - $original_values = $comment->toArray(); - // Hostname will always be denied view access. - // No value will exist for name as this is only for anonymous users. - unset($original_values['hostname'], $original_values['name']); - - $normalized = $this->serializer->normalize($comment, $this->format, ['account' => $account]); - - // Assert that the hostname field does not appear at all in the normalized - // data. - $this->assertFalse(array_key_exists('hostname', $normalized), 'Hostname was not found in normalized comment data.'); - - /** @var \Drupal\comment\CommentInterface $denormalized_comment */ - $denormalized_comment = $this->serializer->denormalize($normalized, 'Drupal\comment\Entity\Comment', $this->format, ['account' => $account]); - - // Before comparing, unset values that are expected to differ. - $denormalized_comment_values = $denormalized_comment->toArray(); - unset($denormalized_comment_values['hostname'], $denormalized_comment_values['name']); - $this->assertEqual($original_values, $denormalized_comment_values, 'The expected comment values are restored after normalizing and denormalizing.'); - } - -} diff --git a/core/modules/hal/tests/src/Kernel/NormalizeTest.php b/core/modules/hal/tests/src/Kernel/NormalizeTest.php index 6ed064c3f..cfffb4a86 100644 --- a/core/modules/hal/tests/src/Kernel/NormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/NormalizeTest.php @@ -7,7 +7,7 @@ use Drupal\Core\Url; use Drupal\entity_test\Entity\EntityTest; /** - * Tests that entities can be normalized in HAL. + * Tests HAL normalization edge cases for EntityResource. * * @group hal */ diff --git a/core/modules/language/migration_templates/d6_language_negotiation_settings.yml b/core/modules/language/migration_templates/d6_language_negotiation_settings.yml new file mode 100644 index 000000000..abc71f68f --- /dev/null +++ b/core/modules/language/migration_templates/d6_language_negotiation_settings.yml @@ -0,0 +1,34 @@ +id: d6_language_negotiation_settings +label: Language negotiation settings +migration_tags: + - Drupal 6 +source: + plugin: variable + variables: + - language_negotiation +process: + session/parameter: + plugin: default_value + default_value: 'language' + selected_langcode: + plugin: default_value + default_value: 'site_default' + url/source: + plugin: static_map + source: language_negotiation + default_value: path_prefix + map: + # LANGUAGE_NEGOTIATION_NONE = 0 + # LANGUAGE_NEGOTIATION_PATH_DEFAULT = 1 + # LANGUAGE_NEGOTIATION_PATH = 2 + # LANGUAGE_NEGOTIATION_DOMAIN = 3 + 0: path_prefix + 1: path_prefix + 2: path_prefix + 3: domain +destination: + plugin: config + config_name: language.negotiation +migration_dependencies: + required: + - language diff --git a/core/modules/language/migration_templates/d6_language_types.yml b/core/modules/language/migration_templates/d6_language_types.yml new file mode 100644 index 000000000..05ce3001e --- /dev/null +++ b/core/modules/language/migration_templates/d6_language_types.yml @@ -0,0 +1,52 @@ +id: d6_language_types +label: Language types +migration_tags: + - Drupal 6 +source: + plugin: variable + variables: + - language_negotiation +process: + all: + plugin: default_value + default_value: + - 'language_interface' + - 'language_content' + - 'language_url' + configurable: + plugin: default_value + default_value: + - 'language_interface' + negotiation/language_content/enabled: + plugin: default_value + default_value: + 'language-interface': 0 + negotiation/language_url/enabled: + plugin: default_value + default_value: + 'language-url': 0 + 'language-url-fallback': 1 + negotiation/language_interface/enabled: + plugin: static_map + source: language_negotiation + map: + # LANGUAGE_NEGOTIATION_NONE = 0 + # LANGUAGE_NEGOTIATION_PATH_DEFAULT = 1 + # LANGUAGE_NEGOTIATION_PATH = 2 + # LANGUAGE_NEGOTIATION_DOMAIN = 3 + 0: + 'language-selected': 0 + 1: + 'language-url': 0 + 'language-selected': 1 + 2: + 'language-url': 0 + 'language-user': 1 + 'language-browser': 2 + 'language-selected': 3 + 3: + 'language-url': 0 + 'language-selected': 1 +destination: + plugin: config + config_name: language.types diff --git a/core/modules/language/migration_templates/d7_language_negotiation_settings.yml b/core/modules/language/migration_templates/d7_language_negotiation_settings.yml index f03be789e..775996533 100644 --- a/core/modules/language/migration_templates/d7_language_negotiation_settings.yml +++ b/core/modules/language/migration_templates/d7_language_negotiation_settings.yml @@ -8,8 +8,25 @@ source: - locale_language_negotiation_session_param - locale_language_negotiation_url_part process: - 'session/parameter': locale_language_negotiation_session_param - 'url/source': locale_language_negotiation_url_part + session/parameter: + plugin: default_value + source: locale_language_negotiation_session_param + default_value: 'language' + selected_langcode: + plugin: default_value + default_value: 'site_default' + url/source: + plugin: static_map + source: locale_language_negotiation_url_part + default_value: path_prefix + map: + # LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX = 0 + # LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN = 1 + 0: path_prefix + 1: domain destination: plugin: config config_name: language.negotiation +migration_dependencies: + required: + - language diff --git a/core/modules/language/migration_templates/d7_language_types.yml b/core/modules/language/migration_templates/d7_language_types.yml new file mode 100644 index 000000000..3a634a2e6 --- /dev/null +++ b/core/modules/language/migration_templates/d7_language_types.yml @@ -0,0 +1,40 @@ +id: d7_language_types +label: Language types +migration_tags: + - Drupal 7 +source: + plugin: variable + variables: + - language_types + - language_negotiation_language + - language_negotiation_language_content + - language_negotiation_language_url + - locale_language_providers_weight_language + - locale_language_providers_weight_language_content + - locale_language_providers_weight_language_url +process: + all: + plugin: language_types + source: language_types + configurable: + plugin: language_types + source: language_types + filter_configurable: true + negotiation/language_content: + plugin: language_negotiation + source: + - language_negotiation_language_content + - locale_language_providers_weight_language_content + negotiation/language_url: + plugin: language_negotiation + source: + - language_negotiation_language_url + - locale_language_providers_weight_language_url + negotiation/language_interface: + plugin: language_negotiation + source: + - language_negotiation_language + - locale_language_providers_weight_language +destination: + plugin: config + config_name: language.types diff --git a/core/modules/language/migration_templates/language_prefixes_and_domains.yml b/core/modules/language/migration_templates/language_prefixes_and_domains.yml new file mode 100644 index 000000000..edc5b549d --- /dev/null +++ b/core/modules/language/migration_templates/language_prefixes_and_domains.yml @@ -0,0 +1,26 @@ +id: language_prefixes_and_domains +label: Language prefixes and domains +migration_tags: + - Drupal 6 + - Drupal 7 +source: + plugin: language + fetch_all: true + domain_negotiation: true +process: + url/prefixes: + plugin: array_build + source: languages + key: language + value: prefix + url/domains: + plugin: language_domains + source: languages + key: language + value: domain +destination: + plugin: config + config_name: language.negotiation +migration_dependencies: + required: + - language diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php index a1d4da410..25b5484a9 100644 --- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php @@ -192,6 +192,7 @@ class LanguageNegotiationUrl extends LanguageNegotiationMethodBase implements In */ public function getLanguageSwitchLinks(Request $request, $type, Url $url) { $links = array(); + $query = $request->query->all(); foreach ($this->languageManager->getNativeLanguages() as $language) { $links[$language->getId()] = array( @@ -202,6 +203,7 @@ class LanguageNegotiationUrl extends LanguageNegotiationMethodBase implements In 'title' => $language->getName(), 'language' => $language, 'attributes' => array('class' => array('language-link')), + 'query' => $query, ); } diff --git a/core/modules/language/src/Plugin/migrate/process/LanguageDomains.php b/core/modules/language/src/Plugin/migrate/process/LanguageDomains.php new file mode 100644 index 000000000..2fcf7d759 --- /dev/null +++ b/core/modules/language/src/Plugin/migrate/process/LanguageDomains.php @@ -0,0 +1,44 @@ +getSourceProperty('domain_negotiation')) { + global $base_url; + + foreach ($value as $old_key => $old_value) { + if (empty($old_value['domain'])) { + // The default language domain might be empty. + // If it is, use the current domain. + $value[$old_key]['domain'] = parse_url($base_url, PHP_URL_HOST); + } + else { + // Ensure we have a protocol when checking for the hostname. + $domain = 'http://' . str_replace(['http://', 'https://'], '', $old_value['domain']); + // Only keep the host part of the domain. + $value[$old_key]['domain'] = parse_url($domain, PHP_URL_HOST); + } + } + } + + return parent::transform($value, $migrate_executable, $row, $destination_property); + } + +} diff --git a/core/modules/language/src/Plugin/migrate/process/LanguageNegotiation.php b/core/modules/language/src/Plugin/migrate/process/LanguageNegotiation.php new file mode 100644 index 000000000..837dbbbc6 --- /dev/null +++ b/core/modules/language/src/Plugin/migrate/process/LanguageNegotiation.php @@ -0,0 +1,81 @@ + [], + 'method_weights' => [], + ]; + + if (!is_array($value)) { + throw new MigrateException('The input should be an array'); + } + + // If no weights are provided, use the keys by flipping the array. + if (empty($value[1])) { + $new_value['enabled'] = array_flip(array_map([$this, 'mapNewMethods'], array_keys($value[0]))); + unset($new_value['method_weights']); + } + else { + foreach ($value[1] as $method => $weight) { + $new_method = $this->mapNewMethods($method); + $new_value['method_weights'][$new_method] = $weight; + if (in_array($method, array_keys($value[0]))) { + $new_value['enabled'][$new_method] = $weight; + } + } + } + + return $new_value; + } + + /** + * Maps old negotiation method names to the new ones. + * + * @param string $value + * The old negotiation method name. + * + * @return string + * The new negotiation method name. + */ + protected function mapNewMethods($value) { + switch ($value) { + case 'language-default': + return 'language-selected'; + case 'locale-browser': + return 'language-browser'; + case 'locale-interface': + return 'language-interface'; + case 'locale-session': + return 'language-session'; + case 'locale-url': + return 'language-url'; + case 'locale-url-fallback': + return 'language-url-fallback'; + case 'locale-user': + return 'language-user'; + default: + return $value; + } + } + +} diff --git a/core/modules/language/src/Plugin/migrate/process/LanguageTypes.php b/core/modules/language/src/Plugin/migrate/process/LanguageTypes.php new file mode 100644 index 000000000..bc09dd77d --- /dev/null +++ b/core/modules/language/src/Plugin/migrate/process/LanguageTypes.php @@ -0,0 +1,40 @@ +configuration['filter_configurable'])) { + $value = array_filter($value); + } + + return array_keys($value); + } + +} diff --git a/core/modules/language/src/Plugin/migrate/source/Language.php b/core/modules/language/src/Plugin/migrate/source/Language.php index 01a0ccf3c..dc2e0ce38 100644 --- a/core/modules/language/src/Plugin/migrate/source/Language.php +++ b/core/modules/language/src/Plugin/migrate/source/Language.php @@ -2,6 +2,7 @@ namespace Drupal\language\Plugin\migrate\source; +use Drupal\migrate\Row; use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; /** @@ -49,4 +50,27 @@ class Language extends DrupalSqlBase { return $this->select('languages')->fields('languages'); } + /** + * {@inheritdoc} + */ + public function prepareRow(Row $row) { + if (!empty($this->configuration['fetch_all'])) { + // Get an array of all languages. + $languages = $this->query()->execute()->fetchAll(); + $row->setSourceProperty('languages', $languages); + } + + if (!empty($this->configuration['domain_negotiation'])) { + // Check if domain negotiation is used to be able to fill in the default + // language domain, which may be empty. In D6, domain negotiation is used + // when the 'language_negotiation' variable is set to '3', and in D7, when + // the 'locale_language_negotiation_url_part' variable is set to '1'. + if ($this->variableGet('language_negotiation', 0) == 3 || $this->variableGet('locale_language_negotiation_url_part', 0) == 1) { + $row->setSourceProperty('domain_negotiation', TRUE); + } + } + + return parent::prepareRow($row); + } + } diff --git a/core/modules/language/src/Tests/LanguageSwitchingTest.php b/core/modules/language/src/Tests/LanguageSwitchingTest.php index c076e6f1e..5d4469d3a 100644 --- a/core/modules/language/src/Tests/LanguageSwitchingTest.php +++ b/core/modules/language/src/Tests/LanguageSwitchingTest.php @@ -118,8 +118,9 @@ class LanguageSwitchingTest extends WebTestBase { protected function doTestLanguageBlockAnonymous($block_label) { $this->drupalLogout(); - // Assert that the language switching block is displayed on the frontpage. - $this->drupalGet(''); + // Assert that the language switching block is displayed on the frontpage + // and ensure that the active class is added when query params are present. + $this->drupalGet('', ['query' => ['foo' => 'bar']]); $this->assertText($block_label, 'Language switcher block found.'); // Assert that only the current language is marked as active. diff --git a/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateLanguageNegotiationSettingsTest.php b/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateLanguageNegotiationSettingsTest.php new file mode 100644 index 000000000..0e74f494a --- /dev/null +++ b/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateLanguageNegotiationSettingsTest.php @@ -0,0 +1,167 @@ +executeMigrations([ + 'language', + 'd6_language_negotiation_settings', + 'language_prefixes_and_domains', + 'd6_language_types', + ]); + + $config = $this->config('language.negotiation'); + $this->assertSame($config->get('session.parameter'), 'language'); + $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); + $this->assertSame($config->get('selected_langcode'), 'site_default'); + $expected_prefixes = [ + 'en' => '', + 'fr' => 'fr', + 'zu' => 'zu', + ]; + $this->assertSame($config->get('url.prefixes'), $expected_prefixes); + + $config = $this->config('language.types'); + $this->assertSame($config->get('all'), ['language_interface', 'language_content', 'language_url']); + $this->assertSame($config->get('configurable'), ['language_interface']); + $this->assertSame($config->get('negotiation.language_content.enabled'), ['language-interface' => 0]); + $this->assertSame($config->get('negotiation.language_url.enabled'), ['language-url' => 0, 'language-url-fallback' => 1]); + $expected_language_interface = [ + 'language-url' => 0, + 'language-selected' => 1, + ]; + $this->assertSame($config->get('negotiation.language_interface.enabled'), $expected_language_interface); + } + + /** + * Tests the migration with LANGUAGE_NEGOTIATION_NONE. + */ + public function testLanguageNegotiationWithNoNegotiation() { + $this->sourceDatabase->update('variable') + ->fields(array('value' => serialize(0))) + ->condition('name', 'language_negotiation') + ->execute(); + + $this->executeMigrations([ + 'language', + 'd6_language_negotiation_settings', + 'language_prefixes_and_domains', + 'd6_language_types', + ]); + + $config = $this->config('language.negotiation'); + $this->assertSame($config->get('session.parameter'), 'language'); + $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); + $this->assertSame($config->get('selected_langcode'), 'site_default'); + + $config = $this->config('language.types'); + $this->assertSame($config->get('all'), ['language_interface', 'language_content', 'language_url']); + $this->assertSame($config->get('configurable'), ['language_interface']); + $this->assertSame($config->get('negotiation.language_content.enabled'), ['language-interface' => 0]); + $this->assertSame($config->get('negotiation.language_url.enabled'), ['language-url' => 0, 'language-url-fallback' => 1]); + $expected_language_interface = [ + 'language-selected' => 0, + ]; + $this->assertSame($config->get('negotiation.language_interface.enabled'), $expected_language_interface); + } + + /** + * Tests the migration with LANGUAGE_NEGOTIATION_PATH. + */ + public function testLanguageNegotiationWithPathPrefix() { + $this->sourceDatabase->update('variable') + ->fields(array('value' => serialize(2))) + ->condition('name', 'language_negotiation') + ->execute(); + + $this->executeMigrations([ + 'language', + 'd6_language_negotiation_settings', + 'language_prefixes_and_domains', + 'd6_language_types', + ]); + + $config = $this->config('language.negotiation'); + $this->assertSame($config->get('session.parameter'), 'language'); + $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); + $this->assertSame($config->get('selected_langcode'), 'site_default'); + $expected_prefixes = [ + 'en' => '', + 'fr' => 'fr', + 'zu' => 'zu', + ]; + $this->assertSame($config->get('url.prefixes'), $expected_prefixes); + + $config = $this->config('language.types'); + $this->assertSame($config->get('all'), ['language_interface', 'language_content', 'language_url']); + $this->assertSame($config->get('configurable'), ['language_interface']); + $this->assertSame($config->get('negotiation.language_content.enabled'), ['language-interface' => 0]); + $this->assertSame($config->get('negotiation.language_url.enabled'), ['language-url' => 0, 'language-url-fallback' => 1]); + $expected_language_interface = [ + 'language-url' => 0, + 'language-user' => 1, + 'language-browser' => 2, + 'language-selected' => 3, + ]; + $this->assertSame($config->get('negotiation.language_interface.enabled'), $expected_language_interface); + } + + /** + * Tests the migration with LANGUAGE_NEGOTIATION_DOMAIN. + */ + public function testLanguageNegotiationWithDomain() { + $this->sourceDatabase->update('variable') + ->fields(array('value' => serialize(3))) + ->condition('name', 'language_negotiation') + ->execute(); + + $this->executeMigrations([ + 'language', + 'd6_language_negotiation_settings', + 'language_prefixes_and_domains', + 'd6_language_types', + ]); + + global $base_url; + $config = $this->config('language.negotiation'); + $this->assertSame($config->get('session.parameter'), 'language'); + $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_DOMAIN); + $this->assertSame($config->get('selected_langcode'), 'site_default'); + $expected_domains = [ + 'en' => parse_url($base_url, PHP_URL_HOST), + 'fr' => 'fr.drupal.org', + 'zu' => 'zu.drupal.org', + ]; + $this->assertSame($config->get('url.domains'), $expected_domains); + + $config = $this->config('language.types'); + $this->assertSame($config->get('all'), ['language_interface', 'language_content', 'language_url']); + $this->assertSame($config->get('configurable'), ['language_interface']); + $this->assertSame($config->get('negotiation.language_content.enabled'), ['language-interface' => 0]); + $this->assertSame($config->get('negotiation.language_url.enabled'), ['language-url' => 0, 'language-url-fallback' => 1]); + $expected_language_interface = [ + 'language-url' => 0, + 'language-selected' => 1, + ]; + $this->assertSame($config->get('negotiation.language_interface.enabled'), $expected_language_interface); + } + +} diff --git a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php index f9ac1c85a..269bac697 100644 --- a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php +++ b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php @@ -2,37 +2,124 @@ namespace Drupal\Tests\language\Kernel\Migrate\d7; +use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl; use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase; /** - * Tests migration of language negotiation variables. + * Tests the migration of language negotiation. * - * @group language + * @group migrate_drupal_7 */ class MigrateLanguageNegotiationSettingsTest extends MigrateDrupal7TestBase { /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ public static $modules = ['language']; /** - * {@inheritdoc} + * Tests migration of language types variables to language.types.yml. */ - protected function setUp() { - parent::setUp(); - $this->executeMigration('d7_language_negotiation_settings'); + public function testLanguageTypes() { + $this->executeMigrations([ + 'language', + 'd7_language_negotiation_settings', + 'd7_language_types', + ]); + + $config = $this->config('language.types'); + $this->assertSame($config->get('all'), ['language_content', 'language_url', 'language_interface']); + $this->assertSame($config->get('configurable'), ['language_interface']); + $this->assertSame($config->get('negotiation.language_content'), ['enabled' => ['language-interface' => 0]]); + $this->assertSame($config->get('negotiation.language_url'), ['enabled' => ['language-url' => 0, 'language-url-fallback' => 1]]); + $expected_language_interface = [ + 'enabled' => [ + 'language-url' => -9, + 'language-user' => -10, + 'language-selected' => -6, + ], + 'method_weights' => [ + 'language-url' => -9, + 'language-session' => -8, + 'language-user' => -10, + 'language-browser' => -7, + 'language-selected' => -6, + ], + ]; + $this->assertSame($config->get('negotiation.language_interface'), $expected_language_interface); } /** - * Tests migration of language negotiation variables to language.negotiation.yml. + * Tests the migration with prefix negotiation. */ - public function testLanguageNegotiation() { + public function testLanguageNegotiationWithPrefix() { + $this->executeMigrations([ + 'language', + 'd7_language_negotiation_settings', + 'language_prefixes_and_domains', + ]); + $config = $this->config('language.negotiation'); - $this->assertIdentical($config->get('session.parameter'), 'language'); - $this->assertIdentical($config->get('url.source'), 'domain'); + $this->assertSame($config->get('session.parameter'), 'language'); + $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); + $this->assertSame($config->get('selected_langcode'), 'site_default'); + $expected_prefixes = [ + 'en' => '', + 'is' => 'is', + ]; + $this->assertSame($config->get('url.prefixes'), $expected_prefixes); + } + + /** + * Tests the migration with domain negotiation. + */ + public function testLanguageNegotiationWithDomain() { + $this->sourceDatabase->update('variable') + ->fields(array('value' => serialize(1))) + ->condition('name', 'locale_language_negotiation_url_part') + ->execute(); + + $this->executeMigrations([ + 'language', + 'd7_language_negotiation_settings', + 'language_prefixes_and_domains', + ]); + + global $base_url; + $config = $this->config('language.negotiation'); + $this->assertSame($config->get('session.parameter'), 'language'); + $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_DOMAIN); + $this->assertSame($config->get('selected_langcode'), 'site_default'); + $expected_domains = [ + 'en' => parse_url($base_url, PHP_URL_HOST), + 'is' => 'is.drupal.org', + ]; + $this->assertSame($config->get('url.domains'), $expected_domains); + } + + /** + * Tests the migration with non-existent variables. + */ + public function testLanguageNegotiationWithNonExistentVariables() { + $this->sourceDatabase->delete('variable') + ->condition('name', ['local_language_negotiation_url_part', 'local_language_negotiation_session_param'], 'IN') + ->execute(); + + $this->executeMigrations([ + 'language', + 'd6_language_negotiation_settings', + 'language_prefixes_and_domains', + ]); + + $config = $this->config('language.negotiation'); + $this->assertSame($config->get('session.parameter'), 'language'); + $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); + $this->assertSame($config->get('selected_langcode'), 'site_default'); + $expected_prefixes = [ + 'en' => '', + 'is' => 'is', + ]; + $this->assertSame($config->get('url.prefixes'), $expected_prefixes); } } diff --git a/core/modules/language/tests/src/Unit/process/LanguageDomainsTest.php b/core/modules/language/tests/src/Unit/process/LanguageDomainsTest.php new file mode 100644 index 000000000..022fc4e6c --- /dev/null +++ b/core/modules/language/tests/src/Unit/process/LanguageDomainsTest.php @@ -0,0 +1,62 @@ + 'language', + 'value' => 'domain', + ]; + $this->plugin = new LanguageDomains($configuration, 'map', []); + parent::setUp(); + + // The language_domains plugin calls getSourceProperty() to check if domain + // negotiation is used. If it is the values will be processed so we need it + // to return TRUE to be able to test the process. + $this->row->expects($this->once()) + ->method('getSourceProperty') + ->will($this->returnValue(TRUE)); + + // The language_domains plugin use $base_url to fill empty domains. + global $base_url; + $base_url = 'http://example.com'; + } + + /** + * @covers ::transform + */ + public function testTransform() { + $source = [ + ['language' => 'en', 'domain' => ''], + ['language' => 'fr', 'domain' => 'fr.example.com'], + ['language' => 'es', 'domain' => 'http://es.example.com'], + ['language' => 'hu', 'domain' => 'https://hu.example.com'], + ]; + $expected = [ + 'en' => 'example.com', + 'fr' => 'fr.example.com', + 'es' => 'es.example.com', + 'hu' => 'hu.example.com', + ]; + $value = $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($value, $expected); + } + +} diff --git a/core/modules/language/tests/src/Unit/process/LanguageNegotiationTest.php b/core/modules/language/tests/src/Unit/process/LanguageNegotiationTest.php new file mode 100644 index 000000000..c6e62fbc4 --- /dev/null +++ b/core/modules/language/tests/src/Unit/process/LanguageNegotiationTest.php @@ -0,0 +1,87 @@ +plugin = new LanguageNegotiation([], 'map', []); + parent::setUp(); + } + + /** + * Tests successful transformation without weights. + */ + public function testTransformWithWeights() { + $source = [ + [ + 'locale-url' => [], + 'language-default' => [], + ], + [ + 'locale-url' => -10, + 'locale-session' => -9, + 'locale-user' => -8, + 'locale-browser' => -7, + 'language-default' => -6, + ], + ]; + $expected = [ + 'enabled' => [ + 'language-url' => -10, + 'language-selected' => -6, + ], + 'method_weights' => [ + 'language-url' => -10, + 'language-session' => -9, + 'language-user' => -8, + 'language-browser' => -7, + 'language-selected' => -6, + ], + ]; + $value = $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($value, $expected); + } + + /** + * Tests successful transformation without weights. + */ + public function testTransformWithoutWeights() { + $source = [ + [ + 'locale-url' => [], + 'locale-url-fallback' => [], + ], + ]; + $expected = [ + 'enabled' => [ + 'language-url' => 0, + 'language-url-fallback' => 1, + ], + ]; + $value = $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($value, $expected); + } + + /** + * Tests string input. + * + * @expectedException \Drupal\migrate\MigrateException + * @expectedExceptionMessage The input should be an array + */ + public function testStringInput() { + $this->plugin = new LanguageNegotiation([], 'map', []); + $this->plugin->transform('foo', $this->migrateExecutable, $this->row, 'destinationproperty'); + } + +} diff --git a/core/modules/language/tests/src/Unit/process/LanguageTypesTest.php b/core/modules/language/tests/src/Unit/process/LanguageTypesTest.php new file mode 100644 index 000000000..17cf412a3 --- /dev/null +++ b/core/modules/language/tests/src/Unit/process/LanguageTypesTest.php @@ -0,0 +1,61 @@ +plugin = new LanguageTypes([], 'map', []); + $source = [ + 'language' => TRUE, + 'language_url' => FALSE, + 'language_content' => FALSE, + ]; + $expected = [ + 0 => 'language_url', + 1 => 'language_content', + 2 => 'language_interface', + ]; + $value = $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($value, $expected); + } + + /** + * Tests successful transformation of configurable language types. + */ + public function testTransformConfigurable() { + $this->plugin = new LanguageTypes(['filter_configurable' => TRUE], 'map', []); + $source = [ + 'language' => TRUE, + 'language_url' => FALSE, + 'language_content' => FALSE, + ]; + $expected = [ + 0 => 'language_interface', + ]; + $value = $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($value, $expected); + } + + /** + * Tests string input. + * + * @expectedException \Drupal\migrate\MigrateException + * @expectedExceptionMessage The input should be an array + */ + public function testStringInput() { + $this->plugin = new LanguageTypes([], 'map', []); + $this->plugin->transform('foo', $this->migrateExecutable, $this->row, 'destinationproperty'); + } + +} diff --git a/core/modules/menu_link_content/src/Plugin/migrate/process/LinkUri.php b/core/modules/menu_link_content/src/Plugin/migrate/process/LinkUri.php new file mode 100644 index 000000000..93656c847 --- /dev/null +++ b/core/modules/menu_link_content/src/Plugin/migrate/process/LinkUri.php @@ -0,0 +1,84 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + list($path) = $value; + $path = ltrim($path, '/'); + + if (parse_url($path, PHP_URL_SCHEME) === NULL) { + $path = 'internal:/' . $path; + + // Convert entity URIs to the entity scheme, if the path matches a route + // of the form "entity.$entity_type_id.canonical". + // @see \Drupal\Core\Url::fromEntityUri() + $url = Url::fromUri($path); + if ($url->isRouted()) { + $route_name = $url->getRouteName(); + foreach (array_keys($this->entityTypeManager->getDefinitions()) as $entity_type_id) { + if ($route_name == "entity.$entity_type_id.canonical" && isset($url->getRouteParameters()[$entity_type_id])) { + return "entity:$entity_type_id/" . $url->getRouteParameters()[$entity_type_id]; + } + } + } + } + return $path; + } + +} diff --git a/core/modules/menu_link_content/src/Plugin/migrate/process/d6/LinkUri.php b/core/modules/menu_link_content/src/Plugin/migrate/process/d6/LinkUri.php index 08c45d812..46b53a04e 100644 --- a/core/modules/menu_link_content/src/Plugin/migrate/process/d6/LinkUri.php +++ b/core/modules/menu_link_content/src/Plugin/migrate/process/d6/LinkUri.php @@ -2,83 +2,12 @@ namespace Drupal\menu_link_content\Plugin\migrate\process\d6; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Url; -use Drupal\migrate\MigrateExecutableInterface; -use Drupal\migrate\ProcessPluginBase; -use Drupal\migrate\Row; -use Symfony\Component\DependencyInjection\ContainerInterface; +use \Drupal\menu_link_content\Plugin\migrate\process\LinkUri as RealLinkUri; /** * Processes a link path into an 'internal:' or 'entity:' URI. * - * @MigrateProcessPlugin( - * id = "link_uri" - * ) + * @deprecated in Drupal 8.2.0, will be removed before Drupal 9.0.0. Use + * \Drupal\menu_link_content\Plugin\migrate\process\LinkUri instead. */ -class LinkUri extends ProcessPluginBase implements ContainerFactoryPluginInterface { - - /** - * The entity type manager, used to fetch entity link templates. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a LinkUri object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager, used to fetch entity link templates. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity_type.manager') - ); - } - - /** - * {@inheritdoc} - */ - public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { - list($path) = $value; - $path = ltrim($path, '/'); - - if (parse_url($path, PHP_URL_SCHEME) === NULL) { - $path = 'internal:/' . $path; - - // Convert entity URIs to the entity scheme, if the path matches a route - // of the form "entity.$entity_type_id.canonical". - // @see \Drupal\Core\Url::fromEntityUri() - $url = Url::fromUri($path); - if ($url->isRouted()) { - $route_name = $url->getRouteName(); - foreach (array_keys($this->entityTypeManager->getDefinitions()) as $entity_type_id) { - if ($route_name == "entity.$entity_type_id.canonical" && isset($url->getRouteParameters()[$entity_type_id])) { - return "entity:$entity_type_id/" . $url->getRouteParameters()[$entity_type_id]; - } - } - } - } - return $path; - } - -} +class LinkUri extends RealLinkUri {} diff --git a/core/modules/menu_link_content/tests/src/Unit/Plugin/migrate/process/d6/LinkUriTest.php b/core/modules/menu_link_content/tests/src/Unit/Plugin/migrate/process/LinkUriTest.php similarity index 94% rename from core/modules/menu_link_content/tests/src/Unit/Plugin/migrate/process/d6/LinkUriTest.php rename to core/modules/menu_link_content/tests/src/Unit/Plugin/migrate/process/LinkUriTest.php index 93a72802a..073180f6b 100644 --- a/core/modules/menu_link_content/tests/src/Unit/Plugin/migrate/process/d6/LinkUriTest.php +++ b/core/modules/menu_link_content/tests/src/Unit/Plugin/migrate/process/LinkUriTest.php @@ -1,10 +1,10 @@ multiple(); + $multiple = $plugin->multiple(); } } // No plugins or no value means do not set. diff --git a/core/modules/migrate/src/Plugin/MigrateDestinationInterface.php b/core/modules/migrate/src/Plugin/MigrateDestinationInterface.php index b6e86a09f..9c83baf29 100644 --- a/core/modules/migrate/src/Plugin/MigrateDestinationInterface.php +++ b/core/modules/migrate/src/Plugin/MigrateDestinationInterface.php @@ -83,11 +83,8 @@ interface MigrateDestinationInterface extends PluginInspectionInterface { * Derived classes must implement fields(), returning a list of available * destination fields. * - * @todo Review the cases where we need the Migration parameter, can we avoid - * that? To be resolved with https://www.drupal.org/node/2543568. - * * @param \Drupal\migrate\Plugin\MigrationInterface $migration - * (optional) The migration containing this destination. Defaults to NULL. + * Unused, will be removed before Drupal 9.0.x. Defaults to NULL. * * @return array * - Keys: machine names of the fields diff --git a/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php b/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php index 1aaf4f180..2d1275023 100644 --- a/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php +++ b/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php @@ -20,13 +20,6 @@ use Drupal\migrate\Plugin\Discovery\ProviderFilterDecorator; */ class MigrateSourcePluginManager extends MigratePluginManager { - /** - * The class loader. - * - * @var object - */ - protected $classLoader; - /** * MigrateSourcePluginManager constructor. * diff --git a/core/modules/migrate/src/Plugin/migrate/process/ArrayBuild.php b/core/modules/migrate/src/Plugin/migrate/process/ArrayBuild.php new file mode 100644 index 000000000..d5f9d2feb --- /dev/null +++ b/core/modules/migrate/src/Plugin/migrate/process/ArrayBuild.php @@ -0,0 +1,107 @@ + Array + * ( + * [language] => en + * ... + * [domain] => http://example.com + * ) + * [1] => Array + * ( + * [language] => fr + * ... + * [domain] => http://fr.example.com + * ) + * ... + * @endcode + * + * The destination should be an array of all the domains keyed by their + * language code: + * + * @code + * domains: Array + * ( + * [en] => http://example.com + * [fr] => http://fr.example.com + * ... + * @endcode + * + * The array_build process plugin would be used like this: + * + * @code + * process: + * domains: + * plugin: array_build + * key: language + * value: domain + * source: languages + * @endcode + * + * @see \Drupal\migrate\Plugin\MigrateProcessInterface + * + * @MigrateProcessPlugin( + * id = "array_build", + * handle_multiples = TRUE + * ) + */ +class ArrayBuild extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + $new_value = []; + + foreach ((array) $value as $old_key => $old_value) { + // Checks that $old_value is an array. + if (!is_array($old_value)) { + throw new MigrateException("The input should be an array of arrays"); + } + + // Checks that the key exists. + if (!array_key_exists($this->configuration['key'], $old_value)) { + throw new MigrateException("The key '" . $this->configuration['key'] . "' does not exist"); + } + + // Checks that the value exists. + if (!array_key_exists($this->configuration['value'], $old_value)) { + throw new MigrateException("The key '" . $this->configuration['value'] . "' does not exist"); + } + + $new_value[$old_value[$this->configuration['key']]] = $old_value[$this->configuration['value']]; + } + + return $new_value; + } + +} diff --git a/core/modules/migrate/src/Plugin/migrate/process/DedupeEntity.php b/core/modules/migrate/src/Plugin/migrate/process/DedupeEntity.php index d2afe8f58..a616c86c3 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/DedupeEntity.php +++ b/core/modules/migrate/src/Plugin/migrate/process/DedupeEntity.php @@ -10,6 +10,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Ensures value is not duplicated against an entity field. * + * If the 'migrated' configuration value is true, an entity will only be + * considered a duplicate if it was migrated by the current migration. + * * @link https://www.drupal.org/node/2135325 Online handbook documentation for dedupe_entity process plugin @endlink * * @MigrateProcessPlugin( @@ -25,11 +28,19 @@ class DedupeEntity extends DedupeBase implements ContainerFactoryPluginInterface */ protected $entityQueryFactory; + /** + * The current migration. + * + * @var \Drupal\migrate\Plugin\MigrationInterface + */ + protected $migration; + /** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, QueryFactory $entity_query_factory) { parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->migration = $migration; $this->entityQueryFactory = $entity_query_factory; } @@ -51,12 +62,25 @@ class DedupeEntity extends DedupeBase implements ContainerFactoryPluginInterface */ protected function exists($value) { // Plugins are cached so for every run we need a new query object. - return $this + $query = $this ->entityQueryFactory ->get($this->configuration['entity_type'], 'AND') - ->condition($this->configuration['field'], $value) - ->count() - ->execute(); + ->condition($this->configuration['field'], $value); + if (!empty($this->configuration['migrated'])) { + // Check if each entity is in the ID map. + $idMap = $this->migration->getIdMap(); + foreach ($query->execute() as $id) { + $dest_id_values[$this->configuration['field']] = $id; + if ($idMap->lookupSourceID($dest_id_values)) { + return TRUE; + } + } + return FALSE; + } + else { + // Just check if any such entity exists. + return $query->count()->execute(); + } } } diff --git a/core/modules/migrate/src/Plugin/migrate/process/Migration.php b/core/modules/migrate/src/Plugin/migrate/process/Migration.php index a8d5b601b..8520a5ec3 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Migration.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Migration.php @@ -38,6 +38,13 @@ class Migration extends ProcessPluginBase implements ContainerFactoryPluginInter */ protected $migrationPluginManager; + /** + * The migration to be executed. + * + * @var \Drupal\migrate\Plugin\MigrationInterface + */ + protected $migration; + /** * {@inheritdoc} */ @@ -70,9 +77,7 @@ class Migration extends ProcessPluginBase implements ContainerFactoryPluginInter if (!is_array($migration_ids)) { $migration_ids = array($migration_ids); } - $scalar = FALSE; if (!is_array($value)) { - $scalar = TRUE; $value = array($value); } $this->skipOnEmpty($value); @@ -145,10 +150,8 @@ class Migration extends ProcessPluginBase implements ContainerFactoryPluginInter } } if ($destination_ids) { - if ($scalar) { - if (count($destination_ids) == 1) { - return reset($destination_ids); - } + if (count($destination_ids) == 1) { + return reset($destination_ids); } else { return $destination_ids; diff --git a/core/modules/migrate/src/Plugin/migrate/process/Route.php b/core/modules/migrate/src/Plugin/migrate/process/Route.php index 2c0144bcb..7ff472048 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Route.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Route.php @@ -3,10 +3,10 @@ namespace Drupal\migrate\Plugin\migrate\process; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\migrate\Plugin\MigrationInterface; use Drupal\Core\Path\PathValidatorInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\Row; @@ -55,7 +55,14 @@ class Route extends ProcessPluginBase implements ContainerFactoryPluginInterface * Set the destination route information based on the source link_path. */ public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { - list($link_path, $options) = $value; + if (is_string($value)) { + $link_path = $value; + $options = []; + } + else { + list($link_path, $options) = $value; + } + $extracted = $this->pathValidator->getUrlIfValidWithoutAccessCheck($link_path); $route = array(); diff --git a/core/modules/migrate/tests/src/Kernel/process/DownloadTest.php b/core/modules/migrate/tests/src/Kernel/process/DownloadTest.php index 69e3aa002..576539eb3 100644 --- a/core/modules/migrate/tests/src/Kernel/process/DownloadTest.php +++ b/core/modules/migrate/tests/src/Kernel/process/DownloadTest.php @@ -60,7 +60,7 @@ class DownloadTest extends FileTestBase { /** * Tests that an exception is thrown if the destination URI is not writable. */ - public function testWriteProectedDestination() { + public function testWriteProtectedDestination() { // Create a pre-existing file at the destination, to test overwrite behavior. $destination_uri = $this->createUri('not-writable.txt'); diff --git a/core/modules/migrate/tests/src/Kernel/process/HandleMultiplesTest.php b/core/modules/migrate/tests/src/Kernel/process/HandleMultiplesTest.php new file mode 100644 index 000000000..b4cc0e82b --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/process/HandleMultiplesTest.php @@ -0,0 +1,138 @@ + [ + 'plugin' => 'embedded_data', + 'data_rows' => [], + 'ids' => [ + 'id' => ['type' => 'string'], + ], + ], + 'process' => [ + // Process pipeline for testing values from string to array to string. + 'first' => [ + // Expects a string and returns an array. + [ + 'plugin' => 'explode', + 'source' => 'scalar', + 'delimiter' => '/', + ], + // Expects an array and returns a string. + [ + 'plugin' => 'extract', + 'index' => [1], + ], + // Expects a string and returns a string. + [ + 'plugin' => 'callback', + 'callable' => 'strtoupper', + ], + ], + // Process pipeline for testing values from array to string to array. + 'second' => [ + // Expects an array and returns a string. + [ + 'plugin' => 'extract', + 'source' => 'multiple', + 'index' => [1], + ], + // Expects a string and returns a string. + [ + 'plugin' => 'callback', + 'callable' => 'strtoupper', + ], + // Expects a string and returns an array. + [ + 'plugin' => 'explode', + 'delimiter' => '/', + ], + ], + ], + 'destination' => [ + 'plugin' => 'config', + 'config_name' => 'migrate_test.settings', + ], + ]; + } + + /** + * Tests process pipelines with scalar and multiple values handling. + * + * @dataProvider scalarAndMultipleValuesProviderSource + * + * @param array $source_data + * @param array $expected_data + */ + public function testScalarAndMultipleValues(array $source_data, array $expected_data) { + $definition = $this->getDefinition(); + $definition['source']['data_rows'] = [$source_data]; + + $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition); + + $executable = new MigrateExecutable($migration, new MigrateMessage()); + $result = $executable->import(); + + // Migration needs to succeed before further assertions are made. + $this->assertSame(MigrationInterface::RESULT_COMPLETED, $result); + + // Compare with expected data. + $this->assertEquals($expected_data, \Drupal::config('migrate_test.settings')->get()); + } + + /** + * Provides the source data with scalar and multiple values. + * + * @return array + */ + public function scalarAndMultipleValuesProviderSource() { + return [ + [ + 'source_data' => [ + 'id' => '1', + // Source value for the first pipeline. + 'scalar' => 'foo/bar', + // Source value for the second pipeline. + 'multiple' => [ + 'foo', + 'bar/baz', + ], + ], + 'expected_data' => [ + // Expected value from the first pipeline. + 'first' => 'BAR', + // Expected value from the second pipeline. + 'second' => [ + 'BAR', + 'BAZ', + ], + ], + ], + ]; + } + +} diff --git a/core/modules/migrate/tests/src/Kernel/process/RouteTest.php b/core/modules/migrate/tests/src/Kernel/process/RouteTest.php new file mode 100644 index 000000000..3de8d2d6a --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/process/RouteTest.php @@ -0,0 +1,278 @@ +doTransform($value); + $this->assertSame($expected, $actual); + } + + /** + * Data provider for testRoute(). + * + * @return array + * An array of arrays, where the first element is the input to the Route + * process plugin, and the second is the expected results. + */ + public function providerTestRoute() { + // Internal link tests. + // Valid link path and options. + $values[0] = [ + 'user/login', + [ + 'attributes' => [ + 'title' => 'Test menu link 1', + ], + ], + ]; + $expected[0] = [ + 'route_name' => 'user.login', + 'route_parameters' => [], + 'options' => [ + 'query' => [], + 'attributes' => [ + 'title' => 'Test menu link 1', + ], + ], + 'url' => NULL, + ]; + + // Valid link path and empty options. + $values[1] = [ + 'user/login', + [], + ]; + $expected[1] = [ + 'route_name' => 'user.login', + 'route_parameters' => [], + 'options' => [ + 'query' => [], + ], + 'url' => NULL, + ]; + + // Valid link path and no options. + $values[2] = 'user/login'; + $expected[2] = [ + 'route_name' => 'user.login', + 'route_parameters' => [], + 'options' => [ + 'query' => [], + ], + 'url' => NULL, + ]; + + // Invalid link path. + $values[3] = 'users'; + $expected[3] = []; + + // Valid link path with parameter. + $values[4] = [ + 'system/timezone/nzdt', + [ + 'attributes' => [ + 'title' => 'Show NZDT', + ], + ], + ]; + $expected[4] = [ + 'route_name' => 'system.timezone', + 'route_parameters' => [ + 'abbreviation' => 'nzdt', + 'offset' => -1, + 'is_daylight_saving_time' => NULL, + ], + 'options' => [ + 'query' => [], + 'attributes' => [ + 'title' => 'Show NZDT', + ], + ], + 'url' => NULL, + ]; + + // External link tests. + // Valid external link path and options. + $values[5] = [ + 'https://www.drupal.org', + [ + 'attributes' => [ + 'title' => 'Drupal', + ], + ], + ]; + $expected[5] = [ + 'route_name' => NULL, + 'route_parameters' => [], + 'options' => [ + 'attributes' => [ + 'title' => 'Drupal', + ], + ], + 'url' => 'https://www.drupal.org', + ]; + + // Valid external link path and options. + $values[6] = [ + 'https://www.drupal.org/user/1/edit?pass-reset-token=QgtDKcRV4e4fjg6v2HTa6CbWx-XzMZ5XBZTufinqsM73qIhscIuU_BjZ6J2tv4dQI6N50ZJOag', + [ + 'attributes' => [ + 'title' => 'Drupal password reset', + ], + ], + ]; + $expected[6] = [ + 'route_name' => NULL, + 'route_parameters' => [], + 'options' => [ + 'attributes' => [ + 'title' => 'Drupal password reset', + ], + ], + 'url' => 'https://www.drupal.org/user/1/edit?pass-reset-token=QgtDKcRV4e4fjg6v2HTa6CbWx-XzMZ5XBZTufinqsM73qIhscIuU_BjZ6J2tv4dQI6N50ZJOag', + ]; + + return [ + // Test data for internal paths. + // Test with valid link path and options. + [$values[0], $expected[0]], + // Test with valid link path and empty options. + [$values[1], $expected[1]], + // Test with valid link path and no options. + [$values[2], $expected[2]], + // Test with Invalid link path. + [$values[3], $expected[3]], + // Test with Valid link path with query options and parameters. + [$values[4], $expected[4]], + + // Test data for external paths. + // Test with external link path and options. + [$values[5], $expected[5]], + // Test with valid link path and query options. + [$values[6], $expected[6]], + ]; + } + + /** + * Tests Route plugin based on providerTestRoute() values. + * + * @param mixed $value + * Input value for the Route process plugin. + * @param array $expected + * The expected results from the Route transform process. + * + * @dataProvider providerTestRouteWithParamQuery + */ + public function testRouteWithParamQuery($value, $expected) { + $this->installSchema('system', ['sequences']); + $this->installEntitySchema('user'); + $this->installConfig(['user']); + + // Create a user so that user/1/edit is a valid path. + $adminUser = User::create([ + 'name' => $this->randomMachineName(), + ]); + $adminUser->save(); + + $actual = $this->doTransform($value); + $this->assertSame($expected, $actual); + } + + /** + * Data provider for testRouteWithParamQuery(). + * + * @return array + * An array of arrays, where the first element is the input to the Route + * process plugin, and the second is the expected results. + */ + public function providerTestRouteWithParamQuery() { + $values = []; + $expected = []; + // Valid link path with query options and parameters. + $values[0] = [ + 'user/1/edit', + [ + 'attributes' => [ + 'title' => 'Edit admin', + ], + 'query' => [ + 'destination' => '/admin/people', + ], + ], + ]; + $expected[0] = [ + 'route_name' => 'entity.user.edit_form', + 'route_parameters' => [ + 'user' => '1', + ], + 'options' => [ + 'attributes' => [ + 'title' => 'Edit admin', + ], + 'query' => [ + 'destination' => '/admin/people', + ], + ], + 'url' => NULL, + ]; + + return [ + // Test with valid link path with parameters and options. + [$values[0], $expected[0]], + ]; + } + + /** + * Transforms link path data to a route. + * + * @param array|string $value + * Source link path information. + * + * @return array $actual + * The route information based on the source link_path. + */ + protected function doTransform($value) { + // Rebuild the routes. + $this->container->get('router.builder')->rebuild(); + $pathValidator = $this->container->get('path.validator'); + $row = new Row(); + $migration = $this->prophesize(MigrationInterface::class)->reveal(); + $executable = $this->prophesize(MigrateExecutableInterface::class)->reveal(); + + $plugin = new Route([], 'route', [], $migration, $pathValidator); + $actual = $plugin->transform($value, $executable, $row, 'destinationproperty'); + return $actual; + } + +} diff --git a/core/modules/migrate/tests/src/Unit/process/ArrayBuildTest.php b/core/modules/migrate/tests/src/Unit/process/ArrayBuildTest.php new file mode 100644 index 000000000..bbfef8b19 --- /dev/null +++ b/core/modules/migrate/tests/src/Unit/process/ArrayBuildTest.php @@ -0,0 +1,82 @@ + 'foo', + 'value' => 'bar', + ]; + $this->plugin = new ArrayBuild($configuration, 'map', []); + parent::setUp(); + } + + /** + * Tests successful transformation. + */ + public function testTransform() { + $source = [ + ['foo' => 'Foo', 'bar' => 'Bar'], + ['foo' => 'foo bar', 'bar' => 'bar foo'], + ]; + $expected = [ + 'Foo' => 'Bar', + 'foo bar' => 'bar foo', + ]; + $value = $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($value, $expected); + } + + /** + * Tests non-existent key for the key configuration. + */ + public function testNonExistentKey() { + $source = [ + ['bar' => 'foo'], + ]; + $this->setExpectedException(MigrateException::class, "The key 'foo' does not exist"); + $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * Tests non-existent key for the value configuration. + */ + public function testNonExistentValue() { + $source = [ + ['foo' => 'bar'], + ]; + $this->setExpectedException(MigrateException::class, "The key 'bar' does not exist"); + $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * Tests one-dimensional array input. + */ + public function testOneDimensionalArrayInput() { + $source = ['foo' => 'bar']; + $this->setExpectedException(MigrateException::class, 'The input should be an array of arrays'); + $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * Tests string input. + */ + public function testStringInput() { + $source = 'foo'; + $this->setExpectedException(MigrateException::class, 'The input should be an array of arrays'); + $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + +} diff --git a/core/modules/migrate/tests/src/Unit/process/DedupeEntityTest.php b/core/modules/migrate/tests/src/Unit/process/DedupeEntityTest.php index a1960deab..4fe8db3bc 100644 --- a/core/modules/migrate/tests/src/Unit/process/DedupeEntityTest.php +++ b/core/modules/migrate/tests/src/Unit/process/DedupeEntityTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\migrate\Unit\process; +use Drupal\Core\Entity\Query\QueryInterface; use Drupal\migrate\Plugin\migrate\process\DedupeEntity; use Drupal\Component\Utility\Unicode; @@ -161,4 +162,47 @@ class DedupeEntityTest extends MigrateProcessTestCase { ->will($this->returnCallback(function () use (&$count) { return $count--;})); } + /** + * Test deduplicating only migrated entities. + */ + public function testDedupeMigrated() { + $configuration = array( + 'entity_type' => 'test_entity_type', + 'field' => 'test_field', + 'migrated' => TRUE, + ); + $plugin = new DedupeEntity($configuration, 'dedupe_entity', array(), $this->getMigration(), $this->entityQueryFactory); + + // Setup the entityQuery used in DedupeEntity::exists. The map, $map, is + // an array consisting of the four input parameters to the query condition + // method and then the query to return. Both 'forum' and + // 'test_vocab' are existing entities. There is no 'test_vocab1'. + $map = []; + foreach (['forums', 'test_vocab', 'test_vocab1'] as $id) { + $query = $this->prophesize(QueryInterface::class); + $query->willBeConstructedWith([]); + $query->execute()->willReturn($id === 'test_vocab1' ? [] : [$id]); + $map[] = ['test_field', $id, NULL, NULL, $query->reveal()]; + } + $this->entityQuery + ->method('condition') + ->will($this->returnValueMap($map)); + + // Entity 'forums' is pre-existing, entity 'test_vocab' was migrated. + $this->idMap + ->method('lookupSourceID') + ->will($this->returnValueMap([ + [['test_field' => 'forums'], FALSE], + [['test_field' => 'test_vocab'], ['source_id' => 42]], + ])); + + // Existing entity 'forums' was not migrated, it should not be deduplicated. + $actual = $plugin->transform('forums', $this->migrateExecutable, $this->row, 'testproperty'); + $this->assertEquals('forums', $actual, 'Pre-existing name is re-used'); + + // Entity 'test_vocab' was migrated, should be deduplicated. + $actual = $plugin->transform('test_vocab', $this->migrateExecutable, $this->row, 'testproperty'); + $this->assertEquals('test_vocab1', $actual, 'Migrated name is deduplicated'); + } + } diff --git a/core/modules/migrate/tests/src/Unit/process/MigrationTest.php b/core/modules/migrate/tests/src/Unit/process/MigrationTest.php index 4eb52da98..b62c1f934 100644 --- a/core/modules/migrate/tests/src/Unit/process/MigrationTest.php +++ b/core/modules/migrate/tests/src/Unit/process/MigrationTest.php @@ -89,7 +89,7 @@ class MigrationTest extends MigrateProcessTestCase { /** * Tests that processing is skipped when the input value is empty. - * + * * @expectedException \Drupal\migrate\MigrateSkipProcessException */ public function testSkipOnEmpty() { @@ -107,8 +107,19 @@ class MigrationTest extends MigrateProcessTestCase { /** * Tests a successful lookup. + * + * @dataProvider successfulLookupDataProvider + * + * @param array $source_id_values + * The source id(s) of the migration map. + * @param array $destination_id_values + * The destination id(s) of the migration map. + * @param string|array $source_value + * The source value(s) for the migration process plugin. + * @param string|array $expected_value + * The expected value(s) of the migration process plugin. */ - public function testSuccessfulLookup() { + public function testSuccessfulLookup($source_id_values, $destination_id_values, $source_value, $expected_value) { $migration_plugin = $this->prophesize(MigrationInterface::class); $migration_plugin_manager = $this->prophesize(MigrationPluginManagerInterface::class); $process_plugin_manager = $this->prophesize(MigratePluginManager::class); @@ -119,7 +130,7 @@ class MigrationTest extends MigrateProcessTestCase { $migration_plugin->id()->willReturn(uniqid()); $id_map = $this->prophesize(MigrateIdMapInterface::class); - $id_map->lookupDestinationId([1])->willReturn([3]); + $id_map->lookupDestinationId($source_id_values)->willReturn($destination_id_values); $migration_plugin->getIdMap()->willReturn($id_map->reveal()); $migration_plugin_manager->createInstances(['foobaz']) @@ -131,7 +142,61 @@ class MigrationTest extends MigrateProcessTestCase { ->willReturn([$migration_plugin->reveal()]); $migration = new Migration($configuration, 'migration', [], $migration_plugin->reveal(), $migration_plugin_manager->reveal(), $process_plugin_manager->reveal()); - $this->assertSame(3, $migration->transform(1, $this->migrateExecutable, $this->row, 'foo')); + $this->assertSame($expected_value, $migration->transform($source_value, $this->migrateExecutable, $this->row, 'foo')); + } + + /** + * Provides data for the successful lookup test. + * + * @return array + */ + public function successfulLookupDataProvider() { + return [ + // Test data for scalar to scalar. + [ + // Source ID of the migration map. + [1], + // Destination ID of the migration map. + [3], + // Input value for the migration plugin. + 1, + // Expected output value of the migration plugin. + 3, + ], + // Test data for scalar to array. + [ + // Source ID of the migration map. + [1], + // Destination IDs of the migration map. + [3, 'foo'], + // Input value for the migration plugin. + 1, + // Expected output values of the migration plugin. + [3, 'foo'], + ], + // Test data for array to scalar. + [ + // Source IDs of the migration map. + [1, 3], + // Destination ID of the migration map. + ['foo'], + // Input values for the migration plugin. + [1, 3], + // Expected output value of the migration plugin. + 'foo', + ], + // Test data for array to array. + [ + // Source IDs of the migration map. + [1, 3], + // Destination IDs of the migration map. + [3, 'foo'], + // Input values for the migration plugin. + [1, 3], + // Expected output values of the migration plugin. + [3, 'foo'], + ], + ]; } } diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/destination/EntityFieldStorageConfig.php b/core/modules/migrate_drupal/src/Plugin/migrate/destination/EntityFieldStorageConfig.php index cc8431de3..169390a6a 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/destination/EntityFieldStorageConfig.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/destination/EntityFieldStorageConfig.php @@ -9,11 +9,17 @@ use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\migrate\destination\EntityFieldStorageConfig as BaseEntityFieldStorageConfig; /** - * Destination with Drupal specific config dependencies. + * Deprecated. Destination with Drupal specific config dependencies. * * @MigrateDestination( * id = "md_entity:field_storage_config" * ) + * + * @deprecated in Drupal 8.2.x and will be removed in Drupal 9.0.x. Use + * \Drupal\migrate\Plugin\migrate\destination\EntityFieldStorageConfig + * instead. + * + * @see \Drupal\migrate\Plugin\migrate\destination\EntityFieldStorageConfig */ class EntityFieldStorageConfig extends BaseEntityFieldStorageConfig { diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal6.php b/core/modules/migrate_drupal/tests/fixtures/drupal6.php index 9debae008..91251aab7 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal6.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal6.php @@ -9812,7 +9812,7 @@ $connection->insert('languages') 'enabled' => '1', 'plurals' => '2', 'formula' => '($n>1)', - 'domain' => '', + 'domain' => 'http://fr.drupal.org', 'prefix' => 'fr', 'weight' => '0', 'javascript' => '047746d30d76aa44a54db9923c7c5fb0', @@ -9825,7 +9825,7 @@ $connection->insert('languages') 'enabled' => '1', 'plurals' => '0', 'formula' => '', - 'domain' => '', + 'domain' => 'http://zu.drupal.org', 'prefix' => 'zu', 'weight' => '0', 'javascript' => '', @@ -41378,6 +41378,40 @@ $connection->insert('node') 'tnid' => '10', 'translate' => '0', )) +->values(array( + 'nid' => '12', + 'vid' => '15', + 'type' => 'page', + 'language' => 'zu', + 'title' => 'Abantu zulu', + 'uid' => '1', + 'status' => '1', + 'created' => '1444238800', + 'changed' => '1444238808', + 'comment' => '0', + 'promote' => '0', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '12', + 'translate' => '0', +)) +->values(array( + 'nid' => '13', + 'vid' => '16', + 'type' => 'page', + 'language' => 'en', + 'title' => 'The Zulu People', + 'uid' => '1', + 'status' => '1', + 'created' => '1444239050', + 'changed' => '1444239050', + 'comment' => '0', + 'promote' => '0', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '12', + 'translate' => '0', +)) ->execute(); $connection->schema()->createTable('node_access', array( @@ -41807,6 +41841,28 @@ $connection->insert('node_revisions') 'timestamp' => '1444239050', 'format' => '1', )) +->values(array( + 'nid' => '12', + 'vid' => '15', + 'uid' => '1', + 'title' => 'Abantu zulu', + 'body' => "Mr. Crusher, ready a collision course with the Borg ship.", + 'teaser' => "Mr. Crusher, ready a collision course with the Borg ship.", + 'log' => '', + 'timestamp' => '1444238808', + 'format' => '1', +)) +->values(array( + 'nid' => '13', + 'vid' => '16', + 'uid' => '1', + 'title' => 'The Zulu People', + 'body' => 'Mr. Crusher, ready a collision course with the Borg ship.', + 'teaser' => 'Mr. Crusher, ready a collision course with the Borg ship.', + 'log' => '', + 'timestamp' => '1444239050', + 'format' => '1', +)) ->execute(); $connection->schema()->createTable('node_type', array( diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal7.php b/core/modules/migrate_drupal/tests/fixtures/drupal7.php index 98e56382f..6853e1e87 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal7.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal7.php @@ -3381,6 +3381,51 @@ $connection->insert('field_config') 'translatable' => '0', 'deleted' => '0', )) +->values(array( + 'id' => '22', + 'field_name' => 'field_node_entityreference', + 'type' => 'entityreference', + 'module' => 'entityreference', + 'active' => '1', + 'storage_type' => 'field_sql_storage', + 'storage_module' => 'field_sql_storage', + 'storage_active' => '1', + 'locked' => '0', + 'data' => 'a:7:{s:12:"translatable";s:1:"0";s:12:"entity_types";a:0:{}s:8:"settings";a:3:{s:11:"target_type";s:4:"node";s:7:"handler";s:4:"base";s:16:"handler_settings";a:2:{s:14:"target_bundles";a:1:{s:7:"article";s:7:"article";}s:4:"sort";a:1:{s:4:"type";s:4:"none";}}}s:7:"storage";a:5:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";s:7:"details";a:1:{s:3:"sql";a:2:{s:18:"FIELD_LOAD_CURRENT";a:1:{s:37:"field_data_field_node_entityreference";a:1:{s:9:"target_id";s:36:"field_node_entityreference_target_id";}}s:19:"FIELD_LOAD_REVISION";a:1:{s:41:"field_revision_field_node_entityreference";a:1:{s:9:"target_id";s:36:"field_node_entityreference_target_id";}}}}}s:12:"foreign keys";a:1:{s:4:"node";a:2:{s:5:"table";s:4:"node";s:7:"columns";a:1:{s:9:"target_id";s:3:"nid";}}}s:7:"indexes";a:1:{s:9:"target_id";a:1:{i:0;s:9:"target_id";}}s:2:"id";s:2:"22";}', + 'cardinality' => '-1', + 'translatable' => '0', + 'deleted' => '0', +)) +->values(array( + 'id' => '23', + 'field_name' => 'field_user_entityreference', + 'type' => 'entityreference', + 'module' => 'entityreference', + 'active' => '1', + 'storage_type' => 'field_sql_storage', + 'storage_module' => 'field_sql_storage', + 'storage_active' => '1', + 'locked' => '0', + 'data' => 'a:7:{s:12:"translatable";s:1:"0";s:12:"entity_types";a:0:{}s:8:"settings";a:3:{s:11:"target_type";s:4:"user";s:7:"handler";s:4:"base";s:16:"handler_settings";a:2:{s:14:"target_bundles";a:0:{}s:4:"sort";a:3:{s:4:"type";s:8:"property";s:8:"property";s:7:"created";s:9:"direction";s:4:"DESC";}}}s:7:"storage";a:5:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";s:7:"details";a:1:{s:3:"sql";a:2:{s:18:"FIELD_LOAD_CURRENT";a:1:{s:37:"field_data_field_user_entityreference";a:1:{s:9:"target_id";s:36:"field_user_entityreference_target_id";}}s:19:"FIELD_LOAD_REVISION";a:1:{s:41:"field_revision_field_user_entityreference";a:1:{s:9:"target_id";s:36:"field_user_entityreference_target_id";}}}}}s:12:"foreign keys";a:1:{s:5:"users";a:2:{s:5:"table";s:5:"users";s:7:"columns";a:1:{s:9:"target_id";s:3:"uid";}}}s:7:"indexes";a:1:{s:9:"target_id";a:1:{i:0;s:9:"target_id";}}s:2:"id";s:2:"23";}', + 'cardinality' => '1', + 'translatable' => '0', + 'deleted' => '0', +)) +->values(array( + 'id' => '24', + 'field_name' => 'field_term_entityreference', + 'type' => 'entityreference', + 'module' => 'entityreference', + 'active' => '1', + 'storage_type' => 'field_sql_storage', + 'storage_module' => 'field_sql_storage', + 'storage_active' => '1', + 'locked' => '0', + 'data' => 'a:7:{s:12:"translatable";s:1:"0";s:12:"entity_types";a:0:{}s:8:"settings";a:3:{s:11:"target_type";s:13:"taxonomy_term";s:7:"handler";s:4:"base";s:16:"handler_settings";a:2:{s:14:"target_bundles";a:1:{s:4:"tags";s:4:"tags";}s:4:"sort";a:1:{s:4:"type";s:4:"none";}}}s:7:"storage";a:5:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";s:7:"details";a:1:{s:3:"sql";a:2:{s:18:"FIELD_LOAD_CURRENT";a:1:{s:37:"field_data_field_term_entityreference";a:1:{s:9:"target_id";s:36:"field_term_entityreference_target_id";}}s:19:"FIELD_LOAD_REVISION";a:1:{s:41:"field_revision_field_term_entityreference";a:1:{s:9:"target_id";s:36:"field_term_entityreference_target_id";}}}}}s:12:"foreign keys";a:1:{s:18:"taxonomy_term_data";a:2:{s:5:"table";s:18:"taxonomy_term_data";s:7:"columns";a:1:{s:9:"target_id";s:3:"tid";}}}s:7:"indexes";a:1:{s:9:"target_id";a:1:{i:0;s:9:"target_id";}}s:2:"id";s:2:"24";}', + 'cardinality' => '-1', + 'translatable' => '0', + 'deleted' => '0', +)) ->execute(); $connection->schema()->createTable('field_config_instance', array( @@ -3756,6 +3801,33 @@ $connection->insert('field_config_instance') 'data' => 'a:7:{s:5:"label";s:4:"Link";s:6:"widget";a:5:{s:6:"weight";s:2:"10";s:4:"type";s:10:"link_field";s:6:"module";s:4:"link";s:6:"active";i:0;s:8:"settings";a:0:{}}s:8:"settings";a:12:{s:12:"absolute_url";i:1;s:12:"validate_url";i:1;s:3:"url";i:0;s:5:"title";s:8:"required";s:11:"title_value";s:19:"Unused Static Title";s:27:"title_label_use_field_label";i:0;s:15:"title_maxlength";s:3:"128";s:7:"display";a:1:{s:10:"url_cutoff";s:2:"81";}s:10:"attributes";a:6:{s:6:"target";s:6:"_blank";s:3:"rel";s:8:"nofollow";s:18:"configurable_class";i:0;s:5:"class";s:7:"classes";s:18:"configurable_title";i:1;s:5:"title";s:0:"";}s:10:"rel_remove";s:19:"rel_remove_external";s:13:"enable_tokens";i:1;s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:12:"link_default";s:6:"weight";s:1:"9";s:8:"settings";a:0:{}s:6:"module";s:4:"link";}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}', 'deleted' => '0', )) +->values(array( + 'id' => '38', + 'field_id' => '22', + 'field_name' => 'field_node_entityreference', + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'data' => 'a:7:{s:5:"label";s:21:"Node Entity Reference";s:6:"widget";a:5:{s:6:"weight";s:2:"16";s:4:"type";s:28:"entityreference_autocomplete";s:6:"module";s:15:"entityreference";s:6:"active";i:1;s:8:"settings";a:3:{s:14:"match_operator";s:8:"CONTAINS";s:4:"size";s:2:"60";s:4:"path";s:0:"";}}s:8:"settings";a:1:{s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:21:"entityreference_label";s:8:"settings";a:1:{s:4:"link";b:0;}s:6:"module";s:15:"entityreference";s:6:"weight";i:15;}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}', + 'deleted' => '0', +)) +->values(array( + 'id' => '39', + 'field_id' => '23', + 'field_name' => 'field_user_entityreference', + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'data' => 'a:7:{s:5:"label";s:21:"User Entity Reference";s:6:"widget";a:5:{s:6:"weight";s:2:"17";s:4:"type";s:15:"options_buttons";s:6:"module";s:7:"options";s:6:"active";i:1;s:8:"settings";a:0:{}}s:8:"settings";a:1:{s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:21:"entityreference_label";s:8:"settings";a:1:{s:4:"link";b:0;}s:6:"module";s:15:"entityreference";s:6:"weight";i:16;}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}', + 'deleted' => '0', +)) +->values(array( + 'id' => '40', + 'field_id' => '24', + 'field_name' => 'field_term_entityreference', + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'data' => 'a:7:{s:5:"label";s:21:"Term Entity Reference";s:6:"widget";a:5:{s:6:"weight";s:2:"18";s:4:"type";s:33:"entityreference_autocomplete_tags";s:6:"module";s:15:"entityreference";s:6:"active";i:1;s:8:"settings";a:3:{s:14:"match_operator";s:8:"CONTAINS";s:4:"size";s:2:"60";s:4:"path";s:0:"";}}s:8:"settings";a:2:{s:9:"behaviors";a:1:{s:14:"taxonomy-index";a:1:{s:6:"status";b:1;}}s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:21:"entityreference_label";s:8:"settings";a:1:{s:4:"link";b:0;}s:6:"module";s:15:"entityreference";s:6:"weight";i:17;}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}', + 'deleted' => '0', +)) ->execute(); $connection->schema()->createTable('field_data_body', array( @@ -5054,6 +5126,113 @@ $connection->schema()->createTable('field_data_field_long_text', array( 'mysql_character_set' => 'utf8', )); +$connection->schema()->createTable('field_data_field_node_entityreference', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_node_entityreference_target_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + ), + 'primary key' => array( + 'entity_type', + 'entity_id', + 'deleted', + 'delta', + 'language', + ), + 'indexes' => array( + 'entity_type' => array( + 'entity_type', + ), + 'bundle' => array( + 'bundle', + ), + 'deleted' => array( + 'deleted', + ), + 'entity_id' => array( + 'entity_id', + ), + 'revision_id' => array( + 'revision_id', + ), + 'language' => array( + 'language', + ), + 'field_node_entityreference_target_id' => array( + 'field_node_entityreference_target_id', + ), + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_data_field_node_entityreference') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_node_entityreference_target_id', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_node_entityreference_target_id' => '2', +)) +->execute(); + $connection->schema()->createTable('field_data_field_phone', array( 'fields' => array( 'entity_type' => array( @@ -5219,6 +5398,26 @@ $connection->insert('field_data_field_tags') 'delta' => '0', 'field_tags_tid' => '9', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '2', + 'revision_id' => '2', + 'language' => 'und', + 'delta' => '1', + 'field_tags_tid' => '14', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '2', + 'revision_id' => '2', + 'language' => 'und', + 'delta' => '2', + 'field_tags_tid' => '17', +)) ->values(array( 'entity_type' => 'node', 'bundle' => 'article', @@ -5233,8 +5432,8 @@ $connection->insert('field_data_field_tags') 'entity_type' => 'node', 'bundle' => 'article', 'deleted' => '0', - 'entity_id' => '2', - 'revision_id' => '2', + 'entity_id' => '3', + 'revision_id' => '3', 'language' => 'und', 'delta' => '1', 'field_tags_tid' => '14', @@ -5246,28 +5445,125 @@ $connection->insert('field_data_field_tags') 'entity_id' => '3', 'revision_id' => '3', 'language' => 'und', + 'delta' => '2', + 'field_tags_tid' => '17', +)) +->execute(); + +$connection->schema()->createTable('field_data_field_term_entityreference', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_term_entityreference_target_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + ), + 'primary key' => array( + 'entity_type', + 'entity_id', + 'deleted', + 'delta', + 'language', + ), + 'indexes' => array( + 'entity_type' => array( + 'entity_type', + ), + 'bundle' => array( + 'bundle', + ), + 'deleted' => array( + 'deleted', + ), + 'entity_id' => array( + 'entity_id', + ), + 'revision_id' => array( + 'revision_id', + ), + 'language' => array( + 'language', + ), + 'field_term_entityreference_target_id' => array( + 'field_term_entityreference_target_id', + ), + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_data_field_term_entityreference') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_term_entityreference_target_id', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_term_entityreference_target_id' => '17', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', 'delta' => '1', - 'field_tags_tid' => '14', -)) -->values(array( - 'entity_type' => 'node', - 'bundle' => 'article', - 'deleted' => '0', - 'entity_id' => '2', - 'revision_id' => '2', - 'language' => 'und', - 'delta' => '2', - 'field_tags_tid' => '17', -)) -->values(array( - 'entity_type' => 'node', - 'bundle' => 'article', - 'deleted' => '0', - 'entity_id' => '3', - 'revision_id' => '3', - 'language' => 'und', - 'delta' => '2', - 'field_tags_tid' => '17', + 'field_term_entityreference_target_id' => '15', )) ->execute(); @@ -5528,6 +5824,113 @@ $connection->insert('field_data_field_text_list') )) ->execute(); +$connection->schema()->createTable('field_data_field_user_entityreference', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_user_entityreference_target_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + ), + 'primary key' => array( + 'entity_type', + 'entity_id', + 'deleted', + 'delta', + 'language', + ), + 'indexes' => array( + 'entity_type' => array( + 'entity_type', + ), + 'bundle' => array( + 'bundle', + ), + 'deleted' => array( + 'deleted', + ), + 'entity_id' => array( + 'entity_id', + ), + 'revision_id' => array( + 'revision_id', + ), + 'language' => array( + 'language', + ), + 'field_user_entityreference_target_id' => array( + 'field_user_entityreference_target_id', + ), + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_data_field_user_entityreference') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_user_entityreference_target_id', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_user_entityreference_target_id' => '2', +)) +->execute(); + $connection->schema()->createTable('field_data_taxonomy_forums', array( 'fields' => array( 'entity_type' => array( @@ -6899,6 +7302,114 @@ $connection->schema()->createTable('field_revision_field_long_text', array( 'mysql_character_set' => 'utf8', )); +$connection->schema()->createTable('field_revision_field_node_entityreference', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_node_entityreference_target_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + ), + 'primary key' => array( + 'entity_type', + 'entity_id', + 'revision_id', + 'deleted', + 'delta', + 'language', + ), + 'indexes' => array( + 'entity_type' => array( + 'entity_type', + ), + 'bundle' => array( + 'bundle', + ), + 'deleted' => array( + 'deleted', + ), + 'entity_id' => array( + 'entity_id', + ), + 'revision_id' => array( + 'revision_id', + ), + 'language' => array( + 'language', + ), + 'field_node_entityreference_target_id' => array( + 'field_node_entityreference_target_id', + ), + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_revision_field_node_entityreference') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_node_entityreference_target_id', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_node_entityreference_target_id' => '2', +)) +->execute(); + $connection->schema()->createTable('field_revision_field_phone', array( 'fields' => array( 'entity_type' => array( @@ -7066,6 +7577,26 @@ $connection->insert('field_revision_field_tags') 'delta' => '0', 'field_tags_tid' => '9', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '2', + 'revision_id' => '2', + 'language' => 'und', + 'delta' => '1', + 'field_tags_tid' => '14', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '2', + 'revision_id' => '2', + 'language' => 'und', + 'delta' => '2', + 'field_tags_tid' => '17', +)) ->values(array( 'entity_type' => 'node', 'bundle' => 'article', @@ -7080,8 +7611,8 @@ $connection->insert('field_revision_field_tags') 'entity_type' => 'node', 'bundle' => 'article', 'deleted' => '0', - 'entity_id' => '2', - 'revision_id' => '2', + 'entity_id' => '3', + 'revision_id' => '3', 'language' => 'und', 'delta' => '1', 'field_tags_tid' => '14', @@ -7093,28 +7624,126 @@ $connection->insert('field_revision_field_tags') 'entity_id' => '3', 'revision_id' => '3', 'language' => 'und', + 'delta' => '2', + 'field_tags_tid' => '17', +)) +->execute(); + +$connection->schema()->createTable('field_revision_field_term_entityreference', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_term_entityreference_target_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + ), + 'primary key' => array( + 'entity_type', + 'entity_id', + 'revision_id', + 'deleted', + 'delta', + 'language', + ), + 'indexes' => array( + 'entity_type' => array( + 'entity_type', + ), + 'bundle' => array( + 'bundle', + ), + 'deleted' => array( + 'deleted', + ), + 'entity_id' => array( + 'entity_id', + ), + 'revision_id' => array( + 'revision_id', + ), + 'language' => array( + 'language', + ), + 'field_term_entityreference_target_id' => array( + 'field_term_entityreference_target_id', + ), + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_revision_field_term_entityreference') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_term_entityreference_target_id', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_term_entityreference_target_id' => '17', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', 'delta' => '1', - 'field_tags_tid' => '14', -)) -->values(array( - 'entity_type' => 'node', - 'bundle' => 'article', - 'deleted' => '0', - 'entity_id' => '2', - 'revision_id' => '2', - 'language' => 'und', - 'delta' => '2', - 'field_tags_tid' => '17', -)) -->values(array( - 'entity_type' => 'node', - 'bundle' => 'article', - 'deleted' => '0', - 'entity_id' => '3', - 'revision_id' => '3', - 'language' => 'und', - 'delta' => '2', - 'field_tags_tid' => '17', + 'field_term_entityreference_target_id' => '15', )) ->execute(); @@ -7378,6 +8007,114 @@ $connection->insert('field_revision_field_text_list') )) ->execute(); +$connection->schema()->createTable('field_revision_field_user_entityreference', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_user_entityreference_target_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + ), + 'primary key' => array( + 'entity_type', + 'entity_id', + 'revision_id', + 'deleted', + 'delta', + 'language', + ), + 'indexes' => array( + 'entity_type' => array( + 'entity_type', + ), + 'bundle' => array( + 'bundle', + ), + 'deleted' => array( + 'deleted', + ), + 'entity_id' => array( + 'entity_id', + ), + 'revision_id' => array( + 'revision_id', + ), + 'language' => array( + 'language', + ), + 'field_user_entityreference_target_id' => array( + 'field_user_entityreference_target_id', + ), + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_revision_field_user_entityreference') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_user_entityreference_target_id', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_user_entityreference_target_id' => '2', +)) +->execute(); + $connection->schema()->createTable('field_revision_taxonomy_forums', array( 'fields' => array( 'entity_type' => array( @@ -8335,7 +9072,7 @@ $connection->insert('languages') 'enabled' => '1', 'plurals' => '0', 'formula' => '', - 'domain' => '', + 'domain' => 'is.drupal.org', 'prefix' => 'is', 'weight' => '0', 'javascript' => '', @@ -28082,6 +28819,181 @@ $connection->insert('menu_router') 'weight' => '0', 'include_file' => 'modules/contact/contact.pages.inc', )) +->values(array( + 'path' => 'ctools/autocomplete/%', + 'load_functions' => 'a:1:{i:2;N;}', + 'to_arg_functions' => '', + 'access_callback' => 'user_access', + 'access_arguments' => 'a:1:{i:0;s:14:"access content";}', + 'page_callback' => 'ctools_content_autocomplete_entity', + 'page_arguments' => 'a:1:{i:0;i:2;}', + 'delivery_callback' => '', + 'fit' => '6', + 'number_parts' => '3', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'ctools/autocomplete/%', + 'title' => '', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => '', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => 'sites/all/modules/ctools/includes/content.menu.inc', +)) +->values(array( + 'path' => 'ctools/context/ajax/access/add', + 'load_functions' => '', + 'to_arg_functions' => '', + 'access_callback' => 'user_access', + 'access_arguments' => 'a:1:{i:0;s:14:"access content";}', + 'page_callback' => 'ctools_access_ajax_add', + 'page_arguments' => 'a:0:{}', + 'delivery_callback' => '', + 'fit' => '31', + 'number_parts' => '5', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'ctools/context/ajax/access/add', + 'title' => '', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => 'ajax_base_page_theme', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => 'sites/all/modules/ctools/includes/context-access-admin.inc', +)) +->values(array( + 'path' => 'ctools/context/ajax/access/configure', + 'load_functions' => '', + 'to_arg_functions' => '', + 'access_callback' => 'user_access', + 'access_arguments' => 'a:1:{i:0;s:14:"access content";}', + 'page_callback' => 'ctools_access_ajax_edit', + 'page_arguments' => 'a:0:{}', + 'delivery_callback' => '', + 'fit' => '31', + 'number_parts' => '5', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'ctools/context/ajax/access/configure', + 'title' => '', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => 'ajax_base_page_theme', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => 'sites/all/modules/ctools/includes/context-access-admin.inc', +)) +->values(array( + 'path' => 'ctools/context/ajax/access/delete', + 'load_functions' => '', + 'to_arg_functions' => '', + 'access_callback' => 'user_access', + 'access_arguments' => 'a:1:{i:0;s:14:"access content";}', + 'page_callback' => 'ctools_access_ajax_delete', + 'page_arguments' => 'a:0:{}', + 'delivery_callback' => '', + 'fit' => '31', + 'number_parts' => '5', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'ctools/context/ajax/access/delete', + 'title' => '', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => 'ajax_base_page_theme', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => 'sites/all/modules/ctools/includes/context-access-admin.inc', +)) +->values(array( + 'path' => 'ctools/context/ajax/add', + 'load_functions' => '', + 'to_arg_functions' => '', + 'access_callback' => 'user_access', + 'access_arguments' => 'a:1:{i:0;s:14:"access content";}', + 'page_callback' => 'ctools_context_ajax_item_add', + 'page_arguments' => 'a:0:{}', + 'delivery_callback' => '', + 'fit' => '15', + 'number_parts' => '4', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'ctools/context/ajax/add', + 'title' => '', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => 'ajax_base_page_theme', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => 'sites/all/modules/ctools/includes/context-admin.inc', +)) +->values(array( + 'path' => 'ctools/context/ajax/configure', + 'load_functions' => '', + 'to_arg_functions' => '', + 'access_callback' => 'user_access', + 'access_arguments' => 'a:1:{i:0;s:14:"access content";}', + 'page_callback' => 'ctools_context_ajax_item_edit', + 'page_arguments' => 'a:0:{}', + 'delivery_callback' => '', + 'fit' => '15', + 'number_parts' => '4', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'ctools/context/ajax/configure', + 'title' => '', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => 'ajax_base_page_theme', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => 'sites/all/modules/ctools/includes/context-admin.inc', +)) +->values(array( + 'path' => 'ctools/context/ajax/delete', + 'load_functions' => '', + 'to_arg_functions' => '', + 'access_callback' => 'user_access', + 'access_arguments' => 'a:1:{i:0;s:14:"access content";}', + 'page_callback' => 'ctools_context_ajax_item_delete', + 'page_arguments' => 'a:0:{}', + 'delivery_callback' => '', + 'fit' => '15', + 'number_parts' => '4', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'ctools/context/ajax/delete', + 'title' => '', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => 'ajax_base_page_theme', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => 'sites/all/modules/ctools/includes/context-admin.inc', +)) ->values(array( 'path' => 'email/%/%/%', 'load_functions' => 'a:3:{i:1;N;i:2;N;i:3;N;}', @@ -28107,6 +29019,56 @@ $connection->insert('menu_router') 'weight' => '0', 'include_file' => '', )) +->values(array( + 'path' => 'entityreference/autocomplete/single/%/%/%', + 'load_functions' => 'a:3:{i:3;N;i:4;N;i:5;N;}', + 'to_arg_functions' => '', + 'access_callback' => 'entityreference_autocomplete_access_callback', + 'access_arguments' => 'a:4:{i:0;i:2;i:1;i:3;i:2;i:4;i:3;i:5;}', + 'page_callback' => 'entityreference_autocomplete_callback', + 'page_arguments' => 'a:4:{i:0;i:2;i:1;i:3;i:2;i:4;i:3;i:5;}', + 'delivery_callback' => '', + 'fit' => '56', + 'number_parts' => '6', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'entityreference/autocomplete/single/%/%/%', + 'title' => 'Entity Reference Autocomplete', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => '', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => '', +)) +->values(array( + 'path' => 'entityreference/autocomplete/tags/%/%/%', + 'load_functions' => 'a:3:{i:3;N;i:4;N;i:5;N;}', + 'to_arg_functions' => '', + 'access_callback' => 'entityreference_autocomplete_access_callback', + 'access_arguments' => 'a:4:{i:0;i:2;i:1;i:3;i:2;i:4;i:3;i:5;}', + 'page_callback' => 'entityreference_autocomplete_callback', + 'page_arguments' => 'a:4:{i:0;i:2;i:1;i:3;i:2;i:4;i:3;i:5;}', + 'delivery_callback' => '', + 'fit' => '56', + 'number_parts' => '6', + 'context' => '0', + 'tab_parent' => '', + 'tab_root' => 'entityreference/autocomplete/tags/%/%/%', + 'title' => 'Entity Reference Autocomplete', + 'title_callback' => 't', + 'title_arguments' => '', + 'theme_callback' => '', + 'theme_arguments' => 'a:0:{}', + 'type' => '0', + 'description' => '', + 'position' => '', + 'weight' => '0', + 'include_file' => '', +)) ->values(array( 'path' => 'file/ajax', 'load_functions' => '', @@ -31227,6 +32189,111 @@ $connection->insert('registry') 'module' => 'phone', 'weight' => '0', )) +->values(array( + 'name' => 'CtoolsContextKeywordsSubstitutionTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/tests/context.test', + 'module' => 'ctools_plugin_test', + 'weight' => '0', +)) +->values(array( + 'name' => 'CToolsCssCache', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/includes/css-cache.inc', + 'module' => 'ctools', + 'weight' => '0', +)) +->values(array( + 'name' => 'CtoolsCssTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/tests/css.test', + 'module' => 'ctools_plugin_test', + 'weight' => '0', +)) +->values(array( + 'name' => 'CtoolsExportCrudTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/tests/ctools_export_test/ctools_export.test', + 'module' => 'ctools_export_test', + 'weight' => '0', +)) +->values(array( + 'name' => 'CtoolsMathExpressionStackTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/tests/math_expression_stack.test', + 'module' => 'ctools_plugin_test', + 'weight' => '0', +)) +->values(array( + 'name' => 'CtoolsMathExpressionTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/tests/math_expression.test', + 'module' => 'ctools_plugin_test', + 'weight' => '0', +)) +->values(array( + 'name' => 'CtoolsObjectCache', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/tests/object_cache.test', + 'module' => 'ctools_plugin_test', + 'weight' => '0', +)) +->values(array( + 'name' => 'CtoolsPluginsGetInfoTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/tests/ctools.plugins.test', + 'module' => 'ctools_plugin_test', + 'weight' => '0', +)) +->values(array( + 'name' => 'ctools_context', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/includes/context.inc', + 'module' => 'ctools', + 'weight' => '0', +)) +->values(array( + 'name' => 'ctools_context_optional', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/includes/context.inc', + 'module' => 'ctools', + 'weight' => '0', +)) +->values(array( + 'name' => 'ctools_context_required', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/includes/context.inc', + 'module' => 'ctools', + 'weight' => '0', +)) +->values(array( + 'name' => 'ctools_export_ui', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/plugins/export_ui/ctools_export_ui.class.php', + 'module' => 'ctools', + 'weight' => '0', +)) +->values(array( + 'name' => 'ctools_math_expr', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/includes/math-expr.inc', + 'module' => 'ctools', + 'weight' => '0', +)) +->values(array( + 'name' => 'ctools_math_expr_stack', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/includes/math-expr.inc', + 'module' => 'ctools', + 'weight' => '0', +)) +->values(array( + 'name' => 'ctools_stylizer_image_processor', + 'type' => 'class', + 'filename' => 'sites/all/modules/ctools/includes/stylizer.inc', + 'module' => 'ctools', + 'weight' => '0', +)) ->values(array( 'name' => 'CSPhoneNumberTestCase', 'type' => 'class', @@ -31234,6 +32301,7 @@ $connection->insert('registry') 'module' => 'phone', 'weight' => '0', )) + ->values(array( 'name' => 'DashboardBlocksTestCase', 'type' => 'class', @@ -32130,6 +33198,83 @@ $connection->insert('registry') 'module' => 'system', 'weight' => '0', )) +->values(array( + 'name' => 'Entity', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityAPICommentNodeAccessTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityAPIController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.controller.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityAPIControllerExportable', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.controller.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityAPIControllerInterface', + 'type' => 'interface', + 'filename' => 'sites/all/modules/entity/includes/entity.controller.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityAPIControllerRevisionableInterface', + 'type' => 'interface', + 'filename' => 'sites/all/modules/entity/includes/entity.controller.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityAPIi18nItegrationTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityAPIRulesIntegrationTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityAPITestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityBundleableUIController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.ui.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityContentUIController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.ui.inc', + 'module' => 'entity', + 'weight' => '0', +)) ->values(array( 'name' => 'EntityCrudHookTestCase', 'type' => 'class', @@ -32137,6 +33282,97 @@ $connection->insert('registry') 'module' => 'simpletest', 'weight' => '0', )) +->values(array( + 'name' => 'EntityDB', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDBExtendable', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDefaultExtraFieldsController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.info.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDefaultFeaturesController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.features.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDefaultI18nStringController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.i18n.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDefaultMetadataController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.info.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDefaultRulesController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.rules.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDefaultUIController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.ui.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDefaultViewsController', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/entity.views.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityDrupalWrapper', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.wrapper.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityExtendable', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityExtraFieldsControllerInterface', + 'type' => 'interface', + 'filename' => 'sites/all/modules/entity/entity.info.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityFieldHandlerHelper', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_field_handler_helper.inc', + 'module' => 'entity', + 'weight' => '0', +)) ->values(array( 'name' => 'EntityFieldQuery', 'type' => 'class', @@ -32172,6 +33408,69 @@ $connection->insert('registry') 'module' => '', 'weight' => '0', )) +->values(array( + 'name' => 'EntityMetadataArrayObject', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.wrapper.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityMetadataIntegrationTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityMetadataNodeAccessTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityMetadataNodeCreateAccessTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityMetadataNodeRevisionAccessTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityMetadataTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityMetadataWrapper', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.wrapper.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityMetadataWrapperException', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.wrapper.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityMetadataWrapperIterator', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.wrapper.inc', + 'module' => 'entity', + 'weight' => '0', +)) ->values(array( 'name' => 'EntityPropertiesTestCase', 'type' => 'class', @@ -32179,6 +33478,251 @@ $connection->insert('registry') 'module' => 'field', 'weight' => '0', )) +->values(array( + 'name' => 'EntityReferenceAdminTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/tests/entityreference.admin.test', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReferenceBehavior_TaxonomyIndex', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/behavior/EntityReferenceBehavior_TaxonomyIndex.class.php', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReferenceHandlersTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/tests/entityreference.handlers.test', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReferenceTaxonomyTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/tests/entityreference.taxonomy.test', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_BehaviorHandler', + 'type' => 'interface', + 'filename' => 'sites/all/modules/entityreference/plugins/behavior/abstract.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_BehaviorHandler_Abstract', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/behavior/abstract.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_BehaviorHandler_Broken', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/behavior/abstract.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'entityreference_plugin_display', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/views/entityreference_plugin_display.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'entityreference_plugin_row_fields', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/views/entityreference_plugin_row_fields.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'entityreference_plugin_style', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/views/entityreference_plugin_style.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_SelectionHandler', + 'type' => 'interface', + 'filename' => 'sites/all/modules/entityreference/plugins/selection/abstract.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_SelectionHandler_Broken', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/selection/abstract.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_SelectionHandler_Generic', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/selection/EntityReference_SelectionHandler_Generic.class.php', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_SelectionHandler_Generic_comment', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/selection/EntityReference_SelectionHandler_Generic.class.php', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_SelectionHandler_Generic_file', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/selection/EntityReference_SelectionHandler_Generic.class.php', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_SelectionHandler_Generic_node', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/selection/EntityReference_SelectionHandler_Generic.class.php', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_SelectionHandler_Generic_taxonomy_term', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/selection/EntityReference_SelectionHandler_Generic.class.php', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityReference_SelectionHandler_Generic_user', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/plugins/selection/EntityReference_SelectionHandler_Generic.class.php', + 'module' => 'entityreference', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityStructureWrapper', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.wrapper.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityTokenTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityValueWrapper', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/includes/entity.wrapper.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'EntityWebTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/entity.test', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_area_entity', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_area_entity.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_boolean', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_boolean.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_date', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_date.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_duration', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_duration.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_entity', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_entity.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_field', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_field.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_numeric', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_numeric.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_options', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_options.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_text', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_text.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_field_uri', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_uri.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_relationship', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_relationship.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_handler_relationship_by_bundle', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_relationship_by_bundle.inc', + 'module' => 'entity', + 'weight' => '0', +)) +->values(array( + 'name' => 'entity_views_plugin_row_entity_view', + 'type' => 'class', + 'filename' => 'sites/all/modules/entity/views/plugins/entity_views_plugin_row_entity_view.inc', + 'module' => 'entity', + 'weight' => '0', +)) ->values(array( 'name' => 'ESPhoneNumberTestCase', 'type' => 'class', @@ -32207,6 +33751,13 @@ $connection->insert('registry') 'module' => 'aggregator', 'weight' => '0', )) +->values(array( + 'name' => 'FeedsMapperFieldTestCase', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/tests/entityreference.feeds.test', + 'module' => 'entityreference', + 'weight' => '0', +)) ->values(array( 'name' => 'FieldAttachOtherTestCase', 'type' => 'class', @@ -33558,6 +35109,13 @@ $connection->insert('registry') 'module' => 'email', 'weight' => '0', )) +->values(array( + 'name' => 'MigrateEntityReferenceFieldHandler', + 'type' => 'class', + 'filename' => 'sites/all/modules/entityreference/entityreference.migrate.inc', + 'module' => 'entityreference', + 'weight' => '0', +)) ->values(array( 'name' => 'MigrateLinkFieldHandler', 'type' => 'class', @@ -36912,6 +38470,58 @@ $connection->insert('registry_file') 'filename' => 'modules/user/user.test', 'hash' => '178320fdb9a0c8754f1fa7272f68f536dcb94ae82ce7d0fc6a0f8a476c1f6922', )) +->values(array( + 'filename' => 'sites/all/modules/ctools/includes/context.inc', + 'hash' => '4cec11a71872eb916c4315c9f727a184d46758aa64bb950d86877a60b9007157', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/includes/css-cache.inc', + 'hash' => 'db90ff67669d9fa445e91074ac67fb97cdb191a19e68d42744f0fd4158649cfa', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/includes/math-expr.inc', + 'hash' => '601db581743dd22d67f7aaf228bd8d26298d72033fc675d02385a1fd6d31888f', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/includes/stylizer.inc', + 'hash' => '3f91f5ed42fb6ee1b65ddef7ac22577b07a5d75ca1eb2df60041243ced5c7079', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/plugins/export_ui/ctools_export_ui.class.php', + 'hash' => '2fd87a7d80689e4d44673b31c07b762144eb8ac57324fd0b9cd9ede5f4ea34b5', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/context.test', + 'hash' => '3a8dd81dd1b99da05a28425f9a017cb611e0470ba88cf000c8b74339c8c91d91', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/css.test', + 'hash' => '20ba7d780a8bdd8f512472a8becef11240da74b96599f6968fecea0ad7ae49c6', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/css_cache.test', + 'hash' => '0dbc038efedb1fa06d2617b7c72b3a45d6ee5b5b791dcb1134876f174a2a7733', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/ctools.plugins.test', + 'hash' => '6af9d9caa3afe93faf5051d3d42c0ce33a1ff6e3a18a09f281df1260d43337d6', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/ctools_export_test/ctools_export.test', + 'hash' => '65e96eabc5c62d7ad29f63309671e2761f221a1c15bb6836d7eadfd263653abc', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/math_expression.test', + 'hash' => 'b99d5c3096857de944dba0e88e21628c7268d8056f42903786172b7a95563dbf', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/math_expression_stack.test', + 'hash' => '797d9e0844f9e214799d96b33d55dd041720d27b11ceb96c3790634d93a62be6', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/object_cache.test', + 'hash' => '18e03c7760a2fc8858d801479ec5471b8c93bd4044c80db557c5e77e7ab20d79', +)) ->values(array( 'filename' => 'sites/all/modules/date/date.migrate.inc', 'hash' => '47ffb48daf97c13ef154cf2ffff577018f02a7091b85dfb39e9c2c89e1da6a5d', @@ -36972,6 +38582,154 @@ $connection->insert('registry_file') 'filename' => 'sites/all/modules/email/email.migrate.inc', 'hash' => 'bf3859ca39a3e5570e4ac862858f066668caab33841d65bdfa229c8445e12d5a', )) +->values(array( + 'filename' => 'sites/all/modules/entity/entity.features.inc', + 'hash' => '47261e1f4f39ac3707a16fdea8a8147c09df1281bcb4b9e46b0c8120603137e8', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/entity.i18n.inc', + 'hash' => '41e0e62af7e2774f62b162d597bb3244551c280296b692b29d039a2c243d7059', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/entity.info.inc', + 'hash' => '8799080b9393c9560e64feae1276fb7d26fef4d92bb0edacc863ee3e7b67bf04', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/entity.rules.inc', + 'hash' => '774199059d1b3ebe6d3fe7a49dbb1550df489055a3d066b5de54edda8dd7ba84', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/entity.test', + 'hash' => 'df253128e41f152b45ef30b5674009c51cf4112450e5dad8e815f39ced280db5', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/includes/entity.controller.inc', + 'hash' => '342db185e6170b63c59a9b360a196eb322edb9a5b8c7819f66b0eae48ed13ebd', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/includes/entity.inc', + 'hash' => '57411fa3d7b5cd2afe8b84f20c1741f48c32673a9da07bd2c35d4a11c50c640e', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/includes/entity.ui.inc', + 'hash' => '65739b31af0e6b422919c17805799dc99143fd89cacfb56b9186e26ece2d0df2', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/includes/entity.wrapper.inc', + 'hash' => '0db08cbb6b730035e3e9a483e6e5c06a744a73f19e4ca83936446b44f0c3d158', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/entity.views.inc', + 'hash' => 'de657f42389ed6832df787e4b618d8d7117b60d145d34ce5dcf3a5b65db29df9', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_field_handler_helper.inc', + 'hash' => '4ec395881109a71327ab8d7c5b5702bef30288ca66557e44e8539cc15a2135bb', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_area_entity.inc', + 'hash' => '7b7bb88e53861739b7279f705f0492fc83ce95f5b20d89339480f546422ebf25', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_boolean.inc', + 'hash' => 'b28b8eee8761ba7a6af35d97ab7aaee28406e6c227271f9769818560626c5791', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_date.inc', + 'hash' => 'b0f5be5b399de94934b24e84c8cf6053a043f6b00c60dcffa752daeafdd38778', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_duration.inc', + 'hash' => 'ed7bb64cb63b94a20c8cde98cfb053b5ea252804396cf61ac562faf1d850266b', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_entity.inc', + 'hash' => '4f255918a22fefebe9c7734f200751457a7ca4d3648e32a98511bb51968d7521', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_field.inc', + 'hash' => '893121efbce2a7181e31147bade260c9cc657cbd33b0d254cb28b2650e57566d', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_numeric.inc', + 'hash' => 'f14e2b063930e8820af381b4f5e83c7278440e7804ab88cfde865b6c94e7c0f6', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_options.inc', + 'hash' => '27ef31b8ee7b9999930380d6a5fdb477772329c4ddbd5c70cc34bcdc7543ce56', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_text.inc', + 'hash' => '5fb0a85d35d29944c699ceaf6efed5eda2df757009e44caba8ff2be397568b60', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_field_uri.inc', + 'hash' => '79ecaa3eb17dfdd0ca077351b75a2c0adf411ebc04720e7cc0e2397674225f24', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_relationship.inc', + 'hash' => 'b69bc538d1e1e0f91f8485ca54c3b6e2be025caa47619734c467377cf89041b9', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/handlers/entity_views_handler_relationship_by_bundle.inc', + 'hash' => '65300c793c4591d60ad908dc02cf3148fe4e899fa6ad218e875fd92d411374dc', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/views/plugins/entity_views_plugin_row_entity_view.inc', + 'hash' => 'ba557790215f2658146424d933e0d17787a0b15180c5815f23428448ccf056a0', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/entityreference.migrate.inc', + 'hash' => '617c6c49e6e0fa4d106cfb49b61a6994b5520934ac3b64a8400a9d969eab7c59', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/plugins/behavior/abstract.inc', + 'hash' => 'a7959ddece2ce3490f92d916162e07aed313e28ca299ca0375bad633b42d93e3', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/plugins/behavior/EntityReferenceBehavior_TaxonomyIndex.class.php', + 'hash' => '92fa0cf46ecdf6200659646e6666c562ea506c40efa41a8edd4758dc0c551b92', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/plugins/selection/abstract.inc', + 'hash' => '7ecf94f5dc3456e4a5c87117d19deb98c368617fb07d610505b1dfa351f14a0b', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/plugins/selection/EntityReference_SelectionHandler_Generic.class.php', + 'hash' => 'e9a8a3c693ed24218d00c10c445cdb21daed10a26e6b55e5c9d6a8c616cfd871', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/plugins/selection/views.inc', + 'hash' => '7bbe8900b6b71c2d41e370deaccca869884d0fe9ca81772d7d5bca5f58ec1cd8', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/tests/entityreference.admin.test', + 'hash' => 'bcd6516be3099ae87a4c3d41add08edd17eafb4244db8442c5dc15f19ebde7ae', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/tests/entityreference.feeds.test', + 'hash' => '320c7480b1758e4d80e91c0a6ea3d43b6b35d1adfe00b6155b61ef786510bb7c', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/tests/entityreference.handlers.test', + 'hash' => '2fa170925ac5303c519378f1763e918cc2f111205220d90998b547a08db90d8c', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/tests/entityreference.taxonomy.test', + 'hash' => '8e4f7d9ae621df0f587b6fcbf139adea2a35c69305ef018ced88447a41164c5f', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/views/entityreference_plugin_display.inc', + 'hash' => '9216a065ea4fdb2daacb1280e5c9549e3400b8553b5293534cf65a0d703ab189', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/views/entityreference_plugin_row_fields.inc', + 'hash' => '7f5a58c099c2df6fd1c3ae285197a4648841d44fa107bcb2064bc1edf435ea8b', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/views/entityreference_plugin_style.inc', + 'hash' => 'ad9a7ea5a37c2d9658c2b1d19ade3011c27ed5d9959423ebf7a390372507e6b0', +)) ->values(array( 'filename' => 'sites/all/modules/link/link.migrate.inc', 'hash' => '0a17ff0daa79813174fff92e9db787e75e710fe757b6924eec193c66fe13f3df', @@ -40064,6 +41822,17 @@ $connection->insert('system') 'weight' => '1000', 'info' => 'a:15:{s:4:"name";s:8:"Standard";s:11:"description";s:51:"Install with commonly used features pre-configured.";s:7:"version";s:4:"7.40";s:4:"core";s:3:"7.x";s:12:"dependencies";a:21:{i:0;s:5:"block";i:1;s:5:"color";i:2;s:7:"comment";i:3;s:10:"contextual";i:4;s:9:"dashboard";i:5;s:4:"help";i:6;s:5:"image";i:7;s:4:"list";i:8;s:4:"menu";i:9;s:6:"number";i:10;s:7:"options";i:11;s:4:"path";i:12;s:8:"taxonomy";i:13;s:5:"dblog";i:14;s:6:"search";i:15;s:8:"shortcut";i:16;s:7:"toolbar";i:17;s:7:"overlay";i:18;s:8:"field_ui";i:19;s:4:"file";i:20;s:3:"rdf";}s:7:"project";s:6:"drupal";s:9:"datestamp";s:10:"1444866674";s:5:"mtime";i:1444866674;s:7:"package";s:5:"Other";s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;s:6:"hidden";b:1;s:8:"required";b:1;s:17:"distribution_name";s:6:"Drupal";}', )) +->values(array( + 'filename' => 'sites/all/modules/ctools/bulk_export/bulk_export.module', + 'name' => 'bulk_export', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:11:"Bulk Export";s:11:"description";s:67:"Performs bulk exporting of data objects known about by Chaos tools.";s:4:"core";s:3:"7.x";s:12:"dependencies";a:1:{i:0;s:6:"ctools";}s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) ->values(array( 'filename' => 'sites/all/modules/ctools/ctools.module', 'name' => 'ctools', @@ -40075,6 +41844,116 @@ $connection->insert('system') 'weight' => '0', 'info' => 'a:12:{s:4:"name";s:11:"Chaos tools";s:11:"description";s:46:"A library of helpful tools by Merlin of Chaos.";s:4:"core";s:3:"7.x";s:7:"package";s:16:"Chaos tool suite";s:5:"files";a:3:{i:0;s:20:"includes/context.inc";i:1;s:22:"includes/math-expr.inc";i:2;s:21:"includes/stylizer.inc";}s:7:"version";s:7:"7.x-1.4";s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1392220730";s:5:"mtime";i:1392220730;s:12:"dependencies";a:0:{}s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', )) +->values(array( + 'filename' => 'sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.module', + 'name' => 'ctools_access_ruleset', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:15:"Custom rulesets";s:11:"description";s:81:"Create custom, exportable, reusable access rulesets for applications like Panels.";s:4:"core";s:3:"7.x";s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:12:"dependencies";a:1:{i:0;s:6:"ctools";}s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/ctools_ajax_sample/ctools_ajax_sample.module', + 'name' => 'ctools_ajax_sample', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:33:"Chaos Tools (CTools) AJAX Example";s:11:"description";s:41:"Shows how to use the power of Chaos AJAX.";s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:12:"dependencies";a:1:{i:0;s:6:"ctools";}s:4:"core";s:3:"7.x";s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.module', + 'name' => 'ctools_custom_content', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:20:"Custom content panes";s:11:"description";s:79:"Create custom, exportable, reusable content panes for applications like Panels.";s:4:"core";s:3:"7.x";s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:12:"dependencies";a:1:{i:0;s:6:"ctools";}s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.module', + 'name' => 'ctools_plugin_example', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:35:"Chaos Tools (CTools) Plugin Example";s:11:"description";s:75:"Shows how an external module can provide ctools plugins (for Panels, etc.).";s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:12:"dependencies";a:4:{i:0;s:6:"ctools";i:1;s:6:"panels";i:2;s:12:"page_manager";i:3;s:13:"advanced_help";}s:4:"core";s:3:"7.x";s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/page_manager/page_manager.module', + 'name' => 'page_manager', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:12:"Page manager";s:11:"description";s:54:"Provides a UI and API to manage pages within the site.";s:4:"core";s:3:"7.x";s:12:"dependencies";a:1:{i:0;s:6:"ctools";}s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/stylizer/stylizer.module', + 'name' => 'stylizer', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:8:"Stylizer";s:11:"description";s:53:"Create custom styles for applications such as Panels.";s:4:"core";s:3:"7.x";s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:12:"dependencies";a:2:{i:0;s:6:"ctools";i:1;s:5:"color";}s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/term_depth/term_depth.module', + 'name' => 'term_depth', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:17:"Term Depth access";s:11:"description";s:48:"Controls access to context based upon term depth";s:4:"core";s:3:"7.x";s:12:"dependencies";a:1:{i:0;s:6:"ctools";}s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/ctools_export_test/ctools_export_test.module', + 'name' => 'ctools_export_test', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:13:{s:4:"name";s:18:"CTools export test";s:11:"description";s:25:"CTools export test module";s:4:"core";s:3:"7.x";s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:12:"dependencies";a:1:{i:0;s:6:"ctools";}s:6:"hidden";b:1;s:5:"files";a:1:{i:0;s:18:"ctools_export.test";}s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/tests/ctools_plugin_test.module', + 'name' => 'ctools_plugin_test', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:13:{s:4:"name";s:24:"Chaos tools plugins test";s:11:"description";s:42:"Provides hooks for testing ctools plugins.";s:7:"package";s:16:"Chaos tool suite";s:7:"version";s:7:"7.x-1.9";s:4:"core";s:3:"7.x";s:12:"dependencies";a:1:{i:0;s:6:"ctools";}s:5:"files";a:6:{i:0;s:19:"ctools.plugins.test";i:1;s:17:"object_cache.test";i:2;s:8:"css.test";i:3;s:12:"context.test";i:4;s:20:"math_expression.test";i:5;s:26:"math_expression_stack.test";}s:6:"hidden";b:1;s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/ctools/views_content/views_content.module', + 'name' => 'views_content', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:19:"Views content panes";s:11:"description";s:104:"Allows Views content to be used in Panels, Dashboard and other modules which use the CTools Content API.";s:7:"package";s:16:"Chaos tool suite";s:12:"dependencies";a:2:{i:0;s:6:"ctools";i:1;s:5:"views";}s:4:"core";s:3:"7.x";s:7:"version";s:7:"7.x-1.9";s:5:"files";a:3:{i:0;s:61:"plugins/views/views_content_plugin_display_ctools_context.inc";i:1;s:57:"plugins/views/views_content_plugin_display_panel_pane.inc";i:2;s:59:"plugins/views/views_content_plugin_style_ctools_context.inc";}s:7:"project";s:6:"ctools";s:9:"datestamp";s:10:"1440020680";s:5:"mtime";i:1440020680;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', +)) ->values(array( 'filename' => 'sites/all/modules/date/date.module', 'name' => 'date', @@ -40207,6 +42086,94 @@ $connection->insert('system') 'weight' => '0', 'info' => 'a:14:{s:4:"name";s:5:"Email";s:11:"description";s:28:"Defines an email field type.";s:4:"core";s:3:"7.x";s:7:"package";s:6:"Fields";s:5:"files";a:1:{i:0;s:17:"email.migrate.inc";}s:7:"version";s:7:"7.x-1.3";s:7:"project";s:5:"email";s:9:"datestamp";s:10:"1397134155";s:5:"mtime";i:1397134155;s:12:"dependencies";a:0:{}s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;s:8:"required";b:1;s:11:"explanation";s:73:"Field type(s) in use - see Field list";}', )) +->values(array( + 'filename' => 'sites/all/modules/entity/entity.module', + 'name' => 'entity', + 'type' => 'module', + 'owner' => '', + 'status' => '1', + 'bootstrap' => '0', + 'schema_version' => '7003', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:10:"Entity API";s:11:"description";s:69:"Enables modules to work with any entity type and to provide entities.";s:4:"core";s:3:"7.x";s:5:"files";a:24:{i:0;s:19:"entity.features.inc";i:1;s:15:"entity.i18n.inc";i:2;s:15:"entity.info.inc";i:3;s:16:"entity.rules.inc";i:4;s:11:"entity.test";i:5;s:19:"includes/entity.inc";i:6;s:30:"includes/entity.controller.inc";i:7;s:22:"includes/entity.ui.inc";i:8;s:27:"includes/entity.wrapper.inc";i:9;s:22:"views/entity.views.inc";i:10;s:52:"views/handlers/entity_views_field_handler_helper.inc";i:11;s:51:"views/handlers/entity_views_handler_area_entity.inc";i:12;s:53:"views/handlers/entity_views_handler_field_boolean.inc";i:13;s:50:"views/handlers/entity_views_handler_field_date.inc";i:14;s:54:"views/handlers/entity_views_handler_field_duration.inc";i:15;s:52:"views/handlers/entity_views_handler_field_entity.inc";i:16;s:51:"views/handlers/entity_views_handler_field_field.inc";i:17;s:53:"views/handlers/entity_views_handler_field_numeric.inc";i:18;s:53:"views/handlers/entity_views_handler_field_options.inc";i:19;s:50:"views/handlers/entity_views_handler_field_text.inc";i:20;s:49:"views/handlers/entity_views_handler_field_uri.inc";i:21;s:62:"views/handlers/entity_views_handler_relationship_by_bundle.inc";i:22;s:52:"views/handlers/entity_views_handler_relationship.inc";i:23;s:53:"views/plugins/entity_views_plugin_row_entity_view.inc";}s:7:"version";s:7:"7.x-1.6";s:7:"project";s:6:"entity";s:9:"datestamp";s:10:"1424876582";s:5:"mtime";i:1424876582;s:12:"dependencies";a:0:{}s:7:"package";s:5:"Other";s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/entity_token.module', + 'name' => 'entity_token', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:13:"Entity tokens";s:11:"description";s:99:"Provides token replacements for all properties that have no tokens and are known to the entity API.";s:4:"core";s:3:"7.x";s:5:"files";a:2:{i:0;s:23:"entity_token.tokens.inc";i:1;s:19:"entity_token.module";}s:12:"dependencies";a:1:{i:0;s:6:"entity";}s:7:"version";s:7:"7.x-1.6";s:7:"project";s:6:"entity";s:9:"datestamp";s:10:"1424876582";s:5:"mtime";i:1424876582;s:7:"package";s:5:"Other";s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/tests/entity_feature.module', + 'name' => 'entity_feature', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:13:{s:4:"name";s:21:"Entity feature module";s:11:"description";s:31:"Provides some entities in code.";s:7:"version";s:7:"7.x-1.6";s:4:"core";s:3:"7.x";s:5:"files";a:1:{i:0;s:21:"entity_feature.module";}s:12:"dependencies";a:1:{i:0;s:11:"entity_test";}s:6:"hidden";b:1;s:7:"project";s:6:"entity";s:9:"datestamp";s:10:"1424876582";s:5:"mtime";i:1424876582;s:7:"package";s:5:"Other";s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/tests/entity_test.module', + 'name' => 'entity_test', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:13:{s:4:"name";s:23:"Entity CRUD test module";s:11:"description";s:46:"Provides entity types based upon the CRUD API.";s:7:"version";s:7:"7.x-1.6";s:4:"core";s:3:"7.x";s:5:"files";a:2:{i:0;s:18:"entity_test.module";i:1;s:19:"entity_test.install";}s:12:"dependencies";a:1:{i:0;s:6:"entity";}s:6:"hidden";b:1;s:7:"project";s:6:"entity";s:9:"datestamp";s:10:"1424876582";s:5:"mtime";i:1424876582;s:7:"package";s:5:"Other";s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/entity/tests/entity_test_i18n.module', + 'name' => 'entity_test_i18n', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:13:{s:4:"name";s:28:"Entity-test type translation";s:11:"description";s:37:"Allows translating entity-test types.";s:12:"dependencies";a:2:{i:0;s:11:"entity_test";i:1;s:11:"i18n_string";}s:7:"package";s:35:"Multilingual - Internationalization";s:4:"core";s:3:"7.x";s:6:"hidden";b:1;s:7:"version";s:7:"7.x-1.6";s:7:"project";s:6:"entity";s:9:"datestamp";s:10:"1424876582";s:5:"mtime";i:1424876582;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/entityreference.module', + 'name' => 'entityreference', + 'type' => 'module', + 'owner' => '', + 'status' => '1', + 'bootstrap' => '0', + 'schema_version' => '7002', + 'weight' => '0', + 'info' => 'a:14:{s:4:"name";s:16:"Entity Reference";s:11:"description";s:51:"Provides a field that can reference other entities.";s:4:"core";s:3:"7.x";s:7:"package";s:6:"Fields";s:12:"dependencies";a:2:{i:0;s:6:"entity";i:1;s:6:"ctools";}s:5:"files";a:11:{i:0;s:27:"entityreference.migrate.inc";i:1;s:30:"plugins/selection/abstract.inc";i:2;s:27:"plugins/selection/views.inc";i:3;s:29:"plugins/behavior/abstract.inc";i:4;s:40:"views/entityreference_plugin_display.inc";i:5;s:38:"views/entityreference_plugin_style.inc";i:6;s:43:"views/entityreference_plugin_row_fields.inc";i:7;s:35:"tests/entityreference.handlers.test";i:8;s:35:"tests/entityreference.taxonomy.test";i:9;s:32:"tests/entityreference.admin.test";i:10;s:32:"tests/entityreference.feeds.test";}s:7:"version";s:7:"7.x-1.1";s:7:"project";s:15:"entityreference";s:9:"datestamp";s:10:"1384973110";s:5:"mtime";i:1384973110;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;s:8:"required";b:1;s:11:"explanation";s:73:"Field type(s) in use - see Field list";}', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/examples/entityreference_behavior_example/entityreference_behavior_example.module', + 'name' => 'entityreference_behavior_example', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:33:"Entity Reference Behavior Example";s:11:"description";s:71:"Provides some example code for implementing Entity Reference behaviors.";s:4:"core";s:3:"7.x";s:7:"package";s:6:"Fields";s:12:"dependencies";a:1:{i:0;s:15:"entityreference";}s:7:"version";s:7:"7.x-1.1";s:7:"project";s:15:"entityreference";s:9:"datestamp";s:10:"1384973110";s:5:"mtime";i:1384973110;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) +->values(array( + 'filename' => 'sites/all/modules/entityreference/tests/modules/entityreference_feeds_test/entityreference_feeds_test.module', + 'name' => 'entityreference_feeds_test', + 'type' => 'module', + 'owner' => '', + 'status' => '0', + 'bootstrap' => '0', + 'schema_version' => '-1', + 'weight' => '0', + 'info' => 'a:13:{s:4:"name";s:41:"Entityreference - Feeds integration tests";s:11:"description";s:65:"Support module for the Entityreference - Feeds integration tests.";s:7:"package";s:7:"Testing";s:4:"core";s:3:"7.x";s:6:"hidden";b:1;s:12:"dependencies";a:3:{i:0;s:5:"feeds";i:1;s:8:"feeds_ui";i:2;s:15:"entityreference";}s:7:"version";s:7:"7.x-1.1";s:7:"project";s:15:"entityreference";s:9:"datestamp";s:10:"1384973110";s:5:"mtime";i:1384973110;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}s:9:"bootstrap";i:0;}', +)) ->values(array( 'filename' => 'sites/all/modules/link/link.module', 'name' => 'link', @@ -40378,6 +42345,24 @@ $connection->insert('taxonomy_index') 'sticky' => '0', 'created' => '1471428152', )) +->values(array( + 'nid' => '1', + 'tid' => '4', + 'sticky' => '0', + 'created' => '1421727515', +)) +->values(array( + 'nid' => '1', + 'tid' => '17', + 'sticky' => '0', + 'created' => '1421727515', +)) +->values(array( + 'nid' => '1', + 'tid' => '15', + 'sticky' => '0', + 'created' => '1421727515', +)) ->execute(); $connection->schema()->createTable('taxonomy_term_data', array( @@ -41608,6 +43593,10 @@ $connection->insert('variable') 'name' => 'feed_item_length', 'value' => 's:8:"fulltext";', )) +->values(array( + 'name' => 'entityreference:base-tables', + 'value' => 'a:6:{s:7:"comment";a:2:{i:0;s:7:"comment";i:1;s:3:"cid";}s:4:"node";a:2:{i:0;s:4:"node";i:1;s:3:"nid";}s:4:"file";a:2:{i:0;s:12:"file_managed";i:1;s:3:"fid";}s:13:"taxonomy_term";a:2:{i:0;s:18:"taxonomy_term_data";i:1;s:3:"tid";}s:19:"taxonomy_vocabulary";a:2:{i:0;s:19:"taxonomy_vocabulary";i:1;s:3:"vid";}s:4:"user";a:2:{i:0;s:5:"users";i:1;s:3:"uid";}}', +)) ->values(array( 'name' => 'field_bundle_settings_comment__comment_node_test_content_type', 'value' => 'a:2:{s:10:"view_modes";a:0:{}s:12:"extra_fields";a:2:{s:4:"form";a:2:{s:6:"author";a:1:{s:6:"weight";s:2:"-2";}s:7:"subject";a:1:{s:6:"weight";s:2:"-1";}}s:7:"display";a:0:{}}}', @@ -41750,7 +43739,7 @@ $connection->insert('variable') )) ->values(array( 'name' => 'language_negotiation_language', - 'value' => 'a:0:{}', + 'value' => 'a:3:{s:11:"locale-user";a:2:{s:9:"callbacks";a:1:{s:8:"language";s:25:"locale_language_from_user";}s:4:"file";s:19:"includes/locale.inc";}s:10:"locale-url";a:2:{s:9:"callbacks";a:3:{s:8:"language";s:24:"locale_language_from_url";s:8:"switcher";s:28:"locale_language_switcher_url";s:11:"url_rewrite";s:31:"locale_language_url_rewrite_url";}s:4:"file";s:19:"includes/locale.inc";}s:16:"language-default";a:1:{s:9:"callbacks";a:1:{s:8:"language";s:21:"language_from_default";}}}', )) ->values(array( 'name' => 'language_negotiation_language_content', @@ -41770,7 +43759,11 @@ $connection->insert('variable') )) ->values(array( 'name' => 'locale_language_negotiation_url_part', - 'value' => 's:6:"domain";', + 'value' => 'i:0;', +)) +->values(array( + 'name' => 'locale_language_providers_weight_language', + 'value' => 'a:5:{s:10:"locale-url";s:2:"-9";s:14:"locale-session";s:2:"-8";s:11:"locale-user";s:3:"-10";s:14:"locale-browser";s:2:"-7";s:16:"language-default";s:2:"-6";}', )) ->values(array( 'name' => 'mail_system', @@ -42100,6 +44093,10 @@ $connection->insert('variable') 'name' => 'theme_default', 'value' => 's:6:"bartik";', )) +->values(array( + 'name' => 'theme_settings', + 'value' => 'a:16:{s:11:"toggle_logo";i:0;s:11:"toggle_name";i:1;s:13:"toggle_slogan";i:0;s:24:"toggle_node_user_picture";i:0;s:27:"toggle_comment_user_picture";i:0;s:32:"toggle_comment_user_verification";i:0;s:14:"toggle_favicon";i:0;s:16:"toggle_main_menu";i:0;s:21:"toggle_secondary_menu";i:0;s:12:"default_logo";i:1;s:9:"logo_path";s:23:"public://customlogo.png";s:11:"logo_upload";s:0:"";s:15:"default_favicon";i:0;s:12:"favicon_path";s:24:"public://somefavicon.png";s:14:"favicon_upload";s:0:"";s:16:"favicon_mimetype";s:9:"image/png";}', +)) ->values(array( 'name' => 'tracker_batch_size', 'value' => 'i:999;', diff --git a/core/modules/migrate_drupal_ui/src/MigrateMessageCapture.php b/core/modules/migrate_drupal_ui/src/Batch/MigrateMessageCapture.php similarity index 94% rename from core/modules/migrate_drupal_ui/src/MigrateMessageCapture.php rename to core/modules/migrate_drupal_ui/src/Batch/MigrateMessageCapture.php index 72e1ace15..efa0a57a4 100644 --- a/core/modules/migrate_drupal_ui/src/MigrateMessageCapture.php +++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateMessageCapture.php @@ -1,6 +1,6 @@ addListener(MigrateEvents::POST_ROW_SAVE, [static::class, 'onPostRowSave']); - $event_dispatcher->addListener(MigrateEvents::MAP_SAVE, [static::class, 'onMapSave']); - $event_dispatcher->addListener(MigrateEvents::IDMAP_MESSAGE, [static::class, 'onIdMapMessage']); - } + $event_dispatcher->addListener(MigrateEvents::POST_ROW_SAVE, [static::class, 'onPostRowSave']); + $event_dispatcher->addListener(MigrateEvents::MAP_SAVE, [static::class, 'onMapSave']); + $event_dispatcher->addListener(MigrateEvents::IDMAP_MESSAGE, [static::class, 'onIdMapMessage']); + static::$maxExecTime = ini_get('max_execution_time'); if (static::$maxExecTime <= 0) { static::$maxExecTime = 60; @@ -98,7 +92,6 @@ class MigrateUpgradeRunBatch { $context['sandbox']['messages'] = []; $context['results']['failures'] = 0; $context['results']['successes'] = 0; - $context['results']['operation'] = $operation; } // Number processed in this batch. @@ -124,12 +117,10 @@ class MigrateUpgradeRunBatch { $migration_name = $migration->label() ? $migration->label() : $migration_id; try { - if ($operation == 'import') { - $migration_status = $executable->import(); - } + $migration_status = $executable->import(); } catch (\Exception $e) { - static::logger()->error($e->getMessage()); + \Drupal::logger('migrate_drupal_ui')->error($e->getMessage()); $migration_status = MigrationInterface::RESULT_FAILED; } @@ -137,13 +128,11 @@ class MigrateUpgradeRunBatch { case MigrationInterface::RESULT_COMPLETED: // Store the number processed in the sandbox. $context['sandbox']['num_processed'] += static::$numProcessed; - if ($operation == 'import') { - $message = new PluralTranslatableMarkup( - $context['sandbox']['num_processed'], 'Upgraded @migration (processed 1 item total)', 'Upgraded @migration (processed @count items total)', - ['@migration' => $migration_name]); - } + $message = new PluralTranslatableMarkup( + $context['sandbox']['num_processed'], 'Upgraded @migration (processed 1 item total)', 'Upgraded @migration (processed @count items total)', + ['@migration' => $migration_name]); $context['sandbox']['messages'][] = (string) $message; - static::logger()->notice($message); + \Drupal::logger('migrate_drupal_ui')->notice($message); $context['sandbox']['num_processed'] = 0; $context['results']['successes']++; break; @@ -162,12 +151,12 @@ class MigrateUpgradeRunBatch { case MigrationInterface::RESULT_FAILED: $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration failed', ['@migration' => $migration_name]); $context['results']['failures']++; - static::logger()->error('Operation on @migration failed', ['@migration' => $migration_name]); + \Drupal::logger('migrate_drupal_ui')->error('Operation on @migration failed', ['@migration' => $migration_name]); break; case MigrationInterface::RESULT_SKIPPED: $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]); - static::logger()->error('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]); + \Drupal::logger('migrate_drupal_ui')->error('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]); break; case MigrationInterface::RESULT_DISABLED: @@ -184,7 +173,7 @@ class MigrateUpgradeRunBatch { // Add and log any captured messages. foreach (static::$messages->getMessages() as $message) { $context['sandbox']['messages'][] = (string) $message; - static::logger()->error($message); + \Drupal::logger('migrate_drupal_ui')->error($message); } // Only display the last MESSAGE_LENGTH messages, in reverse order. @@ -203,13 +192,11 @@ class MigrateUpgradeRunBatch { $migration_id = reset($context['sandbox']['migration_ids']); $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id); $migration_name = $migration->label() ? $migration->label() : $migration_id; - if ($operation == 'import') { - $context['message'] = (string) new TranslatableMarkup('Currently upgrading @migration (@current of @max total tasks)', [ + $context['message'] = (string) new TranslatableMarkup('Currently upgrading @migration (@current of @max total tasks)', [ '@migration' => $migration_name, '@current' => $context['sandbox']['current'], '@max' => $context['sandbox']['max'], ]) . "
\n" . $context['message']; - } } } else { @@ -221,52 +208,36 @@ class MigrateUpgradeRunBatch { } /** - * Returns the logger using the migrate_drupal_ui channel. + * Callback executed when the Migrate Upgrade Import batch process completes. * - * @return \Psr\Log\LoggerInterface - * The logger instance. - */ - protected static function logger() { - return \Drupal::logger('migrate_drupal_ui'); - } - - /** - * Implements the Batch API finished method. + * @param bool $success + * TRUE if batch successfully completed. + * @param array $results + * Batch results. + * @param array $operations + * An array of methods run in the batch. + * @param string $elapsed + * The time to run the batch. */ public static function finished($success, $results, $operations, $elapsed) { - static::displayResults($results); - } - - /** - * Displays counts of success/failures on the migration upgrade complete page. - * - * @param array $results - * An array of result data built during the batch. - */ - protected static function displayResults($results) { $successes = $results['successes']; $failures = $results['failures']; // If we had any successes log that for the user. if ($successes > 0) { - if ($results['operation'] == 'import') { - drupal_set_message(new PluralTranslatableMarkup($successes, 'Completed 1 upgrade task successfully', 'Completed @count upgrade tasks successfully')); - } + drupal_set_message(\Drupal::translation() + ->formatPlural($successes, 'Completed 1 upgrade task successfully', 'Completed @count upgrade tasks successfully')); } - // If we had failures, log them and show the migration failed. if ($failures > 0) { - if ($results['operation'] == 'import') { - drupal_set_message(new PluralTranslatableMarkup($failures, '1 upgrade failed', '@count upgrades failed')); - drupal_set_message(new TranslatableMarkup('Upgrade process not completed'), 'error'); - } + drupal_set_message(\Drupal::translation() + ->formatPlural($failures, '1 upgrade failed', '@count upgrades failed')); + drupal_set_message(t('Upgrade process not completed'), 'error'); } else { - if ($results['operation'] == 'import') { - // Everything went off without a hitch. We may not have had successes - // but we didn't have failures so this is fine. - drupal_set_message(new TranslatableMarkup('Congratulations, you upgraded Drupal!')); - } + // Everything went off without a hitch. We may not have had successes + // but we didn't have failures so this is fine. + drupal_set_message(t('Congratulations, you upgraded Drupal!')); } if (\Drupal::moduleHandler()->moduleExists('dblog')) { @@ -335,7 +306,7 @@ class MigrateUpgradeRunBatch { $type = 'error'; } $source_id_string = implode(',', $event->getSourceIdValues()); - $message = new TranslatableMarkup('Source ID @source_id: @message', ['@source_id' => $source_id_string, '@message' => $event->getMessage()]); + $message = t('Source ID @source_id: @message', ['@source_id' => $source_id_string, '@message' => $event->getMessage()]); static::$messages->display($message, $type); } diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php index 2d60c4694..0b98237b7 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -9,7 +9,7 @@ use Drupal\Core\Render\RendererInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Url; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; -use Drupal\migrate_drupal_ui\MigrateUpgradeRunBatch; +use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch; use Drupal\migrate_drupal\MigrationConfigurationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -258,6 +258,10 @@ class MigrateUpgradeForm extends ConfirmFormBase { 'source_module' => 'forum', 'destination_module' => 'forum', ], + 'd7_global_theme_settings' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], 'd6_imagecache_presets' => [ 'source_module' => 'imagecache', 'destination_module' => 'image', @@ -278,14 +282,30 @@ class MigrateUpgradeForm extends ConfirmFormBase { 'source_module' => 'locale', 'destination_module' => 'language', ], + 'd6_language_negotiation_settings' => [ + 'source_module' => 'locale', + 'destination_module' => 'language', + ], 'd7_language_negotiation_settings' => [ 'source_module' => 'locale', 'destination_module' => 'language', ], + 'language_prefixes_and_domains' => [ + 'source_module' => 'locale', + 'destination_module' => 'language', + ], + 'd6_language_types' => [ + 'source_module' => 'locale', + 'destination_module' => 'language', + ], 'language' => [ 'source_module' => 'locale', 'destination_module' => 'language', ], + 'd7_language_types' => [ + 'source_module' => 'locale', + 'destination_module' => 'language', + ], 'locale_settings' => [ 'source_module' => 'locale', 'destination_module' => 'locale', @@ -744,7 +764,7 @@ class MigrateUpgradeForm extends ConfirmFormBase { $form['upgrade_option_item'] = [ '#type' => 'item', '#prefix' => $this->t('An upgrade has already been performed on this site. To perform a new migration, create a clean and empty new install of Drupal 8. Rollbacks and incremental migrations are not yet supported through the user interface. For more information, see the upgrading handbook.', [':url' => 'https://www.drupal.org/upgrade/migrate']), - '#description' => $this->t('

Last upgrade: @date

', ['@date' => $this->dateFormatter->format($date_performed)]), + '#description' => $this->t('Last upgrade: @date', ['@date' => $this->dateFormatter->format($date_performed)]), ]; return $form; } @@ -961,16 +981,10 @@ class MigrateUpgradeForm extends ConfirmFormBase { } catch (\Exception $e) { $error_message = [ - '#type' => 'inline_template', - '#template' => '{% trans %}Resolve the issue below to continue the upgrade.{% endtrans%}{{ errors }}', - '#context' => [ - 'errors' => [ - '#theme' => 'item_list', - '#items' => [$e->getMessage()], - ], - ], + '#title' => $this->t('Resolve the issue below to continue the upgrade.'), + '#theme' => 'item_list', + '#items' => [$e->getMessage()], ]; - $form_state->setErrorByName($database['driver'] . '][0', $this->renderer->renderPlain($error_message)); } } @@ -1083,8 +1097,12 @@ class MigrateUpgradeForm extends ConfirmFormBase { ]; } $form['counts'] = [ - '#type' => 'item', - '#title' => '', + '#title' => 'Upgrade analysis report', + '#theme' => 'item_list', + '#items' => [ + $this->formatPlural($available_count, '@count available upgrade path', '@count available upgrade paths'), + $this->formatPlural($missing_count, '@count missing upgrade path', '@count missing upgrade paths'), + ], '#weight' => -15, ]; @@ -1109,13 +1127,12 @@ class MigrateUpgradeForm extends ConfirmFormBase { 'progress_message' => '', 'operations' => [ [ - [MigrateUpgradeRunBatch::class, 'run'], - [array_keys($migrations), 'import', $config], + [MigrateUpgradeImportBatch::class, 'run'], + [array_keys($migrations), $config], ], ], 'finished' => [ - MigrateUpgradeRunBatch::class, - 'finished', + MigrateUpgradeImportBatch::class, 'finished', ], ]; batch_set($batch); @@ -1153,7 +1170,9 @@ class MigrateUpgradeForm extends ConfirmFormBase { * {@inheritdoc} */ public function getDescription() { - return $this->t('

Upgrade analysis report

'); + // The description is added by the buildConfirmForm() method. + // @see \Drupal\migrate_drupal_ui\Form\MigrateUpgradeForm::buildConfirmForm() + return; } /** diff --git a/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php index 2a806dedf..50829f026 100644 --- a/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php +++ b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php @@ -49,7 +49,7 @@ class MigrateUpgrade6Test extends MigrateUpgradeTestBase { 'image_style' => 5, 'language_content_settings' => 2, 'migration' => 105, - 'node' => 10, + 'node' => 11, 'node_type' => 11, 'rdf_mapping' => 5, 'search_page' => 2, @@ -58,7 +58,7 @@ class MigrateUpgrade6Test extends MigrateUpgradeTestBase { 'action' => 22, 'menu' => 8, 'taxonomy_term' => 6, - 'taxonomy_vocabulary' => 6, + 'taxonomy_vocabulary' => 5, 'tour' => 4, 'user' => 7, 'user_role' => 6, diff --git a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php index 74d4237ca..d69308580 100644 --- a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php +++ b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php @@ -43,8 +43,8 @@ class MigrateUpgrade7Test extends MigrateUpgradeTestBase { 'configurable_language' => 4, 'contact_form' => 3, 'editor' => 2, - 'field_config' => 45, - 'field_storage_config' => 33, + 'field_config' => 48, + 'field_storage_config' => 36, 'file' => 1, 'filter_format' => 7, 'image_style' => 6, diff --git a/core/modules/node/migration_templates/d6_node.yml b/core/modules/node/migration_templates/d6_node.yml index 58aea356d..4e19bb407 100644 --- a/core/modules/node/migration_templates/d6_node.yml +++ b/core/modules/node/migration_templates/d6_node.yml @@ -9,6 +9,8 @@ process: # In D6, nodes always have a tnid, but it's zero for untranslated nodes. # We normalize it to equal the nid in that case. # @see \Drupal\node\Plugin\migrate\source\d6\Node::prepareRow(). + # If you are using this file to build a custom migration consider removing + # the nid and vid fields to allow incremental migrations. nid: tnid vid: vid langcode: diff --git a/core/modules/node/migration_templates/d6_node_revision.yml b/core/modules/node/migration_templates/d6_node_revision.yml index b7826a18b..046e9bbe4 100644 --- a/core/modules/node/migration_templates/d6_node_revision.yml +++ b/core/modules/node/migration_templates/d6_node_revision.yml @@ -6,6 +6,8 @@ deriver: Drupal\node\Plugin\migrate\D6NodeDeriver source: plugin: d6_node_revision process: + # If you are using this file to build a custom migration consider removing + # the nid and vid fields to allow incremental migrations. nid: nid vid: vid langcode: diff --git a/core/modules/node/migration_templates/d6_node_translation.yml b/core/modules/node/migration_templates/d6_node_translation.yml index b4a240217..3b923d181 100644 --- a/core/modules/node/migration_templates/d6_node_translation.yml +++ b/core/modules/node/migration_templates/d6_node_translation.yml @@ -7,6 +7,8 @@ source: plugin: d6_node translations: true process: + # If you are using this file to build a custom migration consider removing + # the nid field to allow incremental migrations. nid: tnid type: type langcode: @@ -29,6 +31,7 @@ process: revision_uid: revision_uid revision_log: log revision_timestamp: timestamp + content_translation_source: source_langcode # unmapped d6 fields. # translate diff --git a/core/modules/node/migration_templates/d7_node.yml b/core/modules/node/migration_templates/d7_node.yml index b763534dd..381f7ce70 100644 --- a/core/modules/node/migration_templates/d7_node.yml +++ b/core/modules/node/migration_templates/d7_node.yml @@ -6,6 +6,8 @@ deriver: Drupal\node\Plugin\migrate\D7NodeDeriver source: plugin: d7_node process: + # If you are using this file to build a custom migration consider removing + # the nid and vid fields to allow incremental migrations. nid: nid vid: vid langcode: diff --git a/core/modules/node/migration_templates/d7_node_revision.yml b/core/modules/node/migration_templates/d7_node_revision.yml index 0ee8bca6e..c6081ef11 100644 --- a/core/modules/node/migration_templates/d7_node_revision.yml +++ b/core/modules/node/migration_templates/d7_node_revision.yml @@ -6,6 +6,8 @@ deriver: Drupal\node\Plugin\migrate\D7NodeDeriver source: plugin: d7_node_revision process: + # If you are using this file to build a custom migration consider removing + # the nid and vid fields to allow incremental migrations. nid: nid vid: vid langcode: diff --git a/core/modules/node/src/Plugin/migrate/source/d6/Node.php b/core/modules/node/src/Plugin/migrate/source/d6/Node.php index 86864156c..2e4d1a605 100644 --- a/core/modules/node/src/Plugin/migrate/source/d6/Node.php +++ b/core/modules/node/src/Plugin/migrate/source/d6/Node.php @@ -3,8 +3,13 @@ namespace Drupal\node\Plugin\migrate\source\d6; use Drupal\Core\Database\Query\SelectInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Extension\ModuleHandler; +use Drupal\Core\State\StateInterface; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Drupal 6 node source from database. @@ -34,6 +39,36 @@ class Node extends DrupalSqlBase { */ protected $fieldInfo; + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandler + */ + protected $moduleHandler; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityManagerInterface $entity_manager, ModuleHandler $module_handler) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_manager); + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->get('state'), + $container->get('entity.manager'), + $container->get('module_handler') + ); + } + /** * {@inheritdoc} */ @@ -68,6 +103,13 @@ class Node extends DrupalSqlBase { $query->addField('n', 'uid', 'node_uid'); $query->addField('nr', 'uid', 'revision_uid'); + // If the content_translation module is enabled, get the source langcode + // to fill the content_translation_source field. + if ($this->moduleHandler->moduleExists('content_translation')) { + $query->leftJoin('node', 'nt', 'n.tnid = nt.nid'); + $query->addField('nt', 'language', 'source_langcode'); + } + if (isset($this->configuration['node_type'])) { $query->condition('n.type', $this->configuration['node_type']); } diff --git a/core/modules/node/src/Plugin/views/filter/Access.php b/core/modules/node/src/Plugin/views/filter/Access.php index 4b34afeff..73844e11d 100644 --- a/core/modules/node/src/Plugin/views/filter/Access.php +++ b/core/modules/node/src/Plugin/views/filter/Access.php @@ -25,7 +25,7 @@ class Access extends FilterPluginBase { */ public function query() { $account = $this->view->getUser(); - if (!$account->hasPermission('administer nodes')) { + if (!$account->hasPermission('bypass node access')) { $table = $this->ensureMyTable(); $grants = db_or(); foreach (node_access_grants('view', $account) as $realm => $gids) { diff --git a/core/modules/node/src/Tests/NodeViewTest.php b/core/modules/node/src/Tests/NodeViewTest.php index fae056c6f..3a27ade0d 100644 --- a/core/modules/node/src/Tests/NodeViewTest.php +++ b/core/modules/node/src/Tests/NodeViewTest.php @@ -2,6 +2,8 @@ namespace Drupal\node\Tests; +use Drupal\Component\Utility\Html; + /** * Tests the node/{node} page. * @@ -63,6 +65,24 @@ class NodeViewTest extends NodeTestBase { $this->assertEqual($result[0]['href'], $node->url('edit-form')); } + /** + * Tests the Link header. + */ + public function testLinkHeader() { + $node = $this->drupalCreateNode(); + + $expected = [ + '<' . Html::escape($node->url('canonical')) . '>; rel="canonical"', + '<' . Html::escape($node->url('canonical'), ['alias' => TRUE]) . '>; rel="shortlink"', + '<' . Html::escape($node->url('revision')) . '>; rel="revision"', + ]; + + $this->drupalGet($node->urlInfo()); + + $links = explode(',', $this->drupalGetHeader('Link')); + $this->assertEqual($links, $expected); + } + /** * Tests that we store and retrieve multi-byte UTF-8 characters correctly. */ diff --git a/core/modules/node/src/Tests/Views/FilterNodeAccessTest.php b/core/modules/node/src/Tests/Views/FilterNodeAccessTest.php new file mode 100644 index 000000000..dba441957 --- /dev/null +++ b/core/modules/node/src/Tests/Views/FilterNodeAccessTest.php @@ -0,0 +1,111 @@ +drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + + node_access_test_add_field(NodeType::load('article')); + + node_access_rebuild(); + \Drupal::state()->set('node_access_test.private', TRUE); + + $num_simple_users = 2; + $this->users = []; + + for ($i = 0; $i < $num_simple_users; $i++) { + $this->users[$i] = $this->drupalCreateUser(['access content', 'create article content']); + } + foreach ($this->users as $web_user) { + $this->drupalLogin($web_user); + foreach ([0 => 'Public', 1 => 'Private'] as $is_private => $type) { + $settings = [ + 'body' => [[ + 'value' => $type . ' node', + 'format' => filter_default_format(), + ]], + 'title' => t('@private_public Article created by @user', ['@private_public' => $type, '@user' => $web_user->getUsername()]), + 'type' => 'article', + 'uid' => $web_user->id(), + 'private' => (bool) $is_private, + ]; + + $node = $this->drupalCreateNode($settings); + $this->assertEqual($is_private, (int) $node->private->value, 'The private status of the node was properly set in the node_access_test table.'); + } + } + } + + /** + * Tests the node access filter. + */ + public function testFilterNodeAccess() { + $this->drupalLogin($this->users[0]); + $this->drupalGet('test_filter_node_access'); + // Test that the private node of the current user is shown. + $this->assertText('Private Article created by ' . $this->users[0]->getUsername()); + // Test that the private node of the other use isn't shown. + $this->assertNoText('Private Article created by ' . $this->users[1]->getUsername()); + // Test that both public nodes are shown. + $this->assertText('Public Article created by ' . $this->users[0]->getUsername()); + $this->assertText('Public Article created by ' . $this->users[1]->getUsername()); + + // Switch users and test the other private node is shown. + $this->drupalLogin($this->users[1]); + $this->drupalGet('test_filter_node_access'); + // Test that the private node of the current user is shown. + $this->assertText('Private Article created by ' . $this->users[1]->getUsername()); + // Test that the private node of the other use isn't shown. + $this->assertNoText('Private Article created by ' . $this->users[0]->getUsername()); + + // Test that a user with administer nodes permission can't see all nodes. + $administer_nodes_user = $this->drupalCreateUser(['access content', 'administer nodes']); + $this->drupalLogin($administer_nodes_user); + $this->drupalGet('test_filter_node_access'); + $this->assertNoText('Private Article created by ' . $this->users[0]->getUsername()); + $this->assertNoText('Private Article created by ' . $this->users[1]->getUsername()); + $this->assertText('Public Article created by ' . $this->users[0]->getUsername()); + $this->assertText('Public Article created by ' . $this->users[1]->getUsername()); + + // Test that a user with bypass node access can see all nodes. + $bypass_access_user = $this->drupalCreateUser(['access content', 'bypass node access']); + $this->drupalLogin($bypass_access_user); + $this->drupalGet('test_filter_node_access'); + $this->assertText('Private Article created by ' . $this->users[0]->getUsername()); + $this->assertText('Private Article created by ' . $this->users[1]->getUsername()); + $this->assertText('Public Article created by ' . $this->users[0]->getUsername()); + $this->assertText('Public Article created by ' . $this->users[1]->getUsername()); + } + +} diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_filter_node_access.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_filter_node_access.yml new file mode 100644 index 000000000..6663fa826 --- /dev/null +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_filter_node_access.yml @@ -0,0 +1,203 @@ +langcode: en +status: true +dependencies: + module: + - node + - user +id: test_filter_node_access +label: test_filter_node_access +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: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: true + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: some + options: + items_per_page: 10 + offset: 0 + style: + type: default + row: + type: fields + options: + default_field_elements: true + inline: { } + separator: '' + hide_empty: false + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_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 + settings: + link_to_entity: true + 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: string + 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: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + nid: + id: nid + table: node_access + field: nid + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: node_access + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: test_filter_node_access + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test_filter_node_access + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php index 122199fe5..7a0a272a1 100644 --- a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php +++ b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php @@ -28,7 +28,12 @@ class MigrateNodeTest extends MigrateNodeTestBase { parent::setUp(); $this->setUpMigratedFiles(); $this->installSchema('file', ['file_usage']); - $this->executeMigrations(['language', 'd6_node', 'd6_node_translation']); + $this->executeMigrations([ + 'language', + 'd6_language_content_settings', + 'd6_node', + 'd6_node_translation', + ]); } /** @@ -96,6 +101,14 @@ class MigrateNodeTest extends MigrateNodeTestBase { $this->assertIdentical('The Real McCoy', $node->title->value); $this->assertTrue($node->hasTranslation('fr'), "Node 10 has french translation"); + // Test that content_translation_source is set. + $manager = $this->container->get('content_translation.manager'); + $this->assertIdentical('en', $manager->getTranslationMetadata($node->getTranslation('fr'))->getSource()); + + // Test that content_translation_source for a source other than English. + $node = Node::load(12); + $this->assertIdentical('zu', $manager->getTranslationMetadata($node->getTranslation('en'))->getSource()); + // Node 11 is a translation of node 10, and should not be imported separately. $this->assertNull(Node::load(11), "Node 11 doesn't exist in D8, it was a translation"); diff --git a/core/modules/outside_in/css/outside_in.module.css b/core/modules/outside_in/css/outside_in.module.css index 9e360372d..a534dd159 100644 --- a/core/modules/outside_in/css/outside_in.module.css +++ b/core/modules/outside_in/css/outside_in.module.css @@ -38,12 +38,3 @@ [dir="rtl"] .toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab { float: right; } - -.offcanvas-lining { - background: #333; - position: absolute; - top: 0; - height: 100%; - width: 100%; - z-index: -1; -} diff --git a/core/modules/outside_in/css/outside_in.motion.css b/core/modules/outside_in/css/outside_in.motion.css index 8fbf5cb34..8e1981150 100644 --- a/core/modules/outside_in/css/outside_in.motion.css +++ b/core/modules/outside_in/css/outside_in.motion.css @@ -32,8 +32,8 @@ /* Transition the editables on the page, their contextual links and their hover states. */ #main-canvas-wrapper .contextual, -#main-canvas-wrapper .outside-in-editable, -#main-canvas-wrapper.js-tray-open .outside-in-editable { +#main-canvas-wrapper .js-outside-in-edit-mode .outside-in-editable, +#main-canvas-wrapper.js-tray-open .js-outside-in-edit-mode .outside-in-editable { -webkit-transition: all .7s ease; -moz-transition: all .7s ease; transition: all .7s ease; diff --git a/core/modules/outside_in/js/offcanvas.js b/core/modules/outside_in/js/offcanvas.js index b3c4237e4..ffcb5e896 100644 --- a/core/modules/outside_in/js/offcanvas.js +++ b/core/modules/outside_in/js/offcanvas.js @@ -120,6 +120,9 @@ of: window }; settings.dialogClass = 'ui-dialog-offcanvas'; + // Applies initial height to dialog based on window height. + // See http://api.jqueryui.com/dialog for all dialog options. + settings.height = $(window).height(); } }, 'dialog:beforeclose': function (event, dialog, $element) { diff --git a/core/modules/outside_in/js/outside_in.js b/core/modules/outside_in/js/outside_in.js index d079664cd..e802a6cc9 100644 --- a/core/modules/outside_in/js/outside_in.js +++ b/core/modules/outside_in/js/outside_in.js @@ -84,6 +84,17 @@ event.preventDefault(); } + /** + * Close any active toolbar tray before entering edit mode. + */ + function closeToolbarTrays() { + $('#toolbar-bar') + .find('.toolbar-tab') + .not('.contextual-toolbar-tab') + .has('.toolbar-tray.is-active') + .find('.toolbar-item').click(); + } + /** * Helper to switch edit mode state. * @@ -97,10 +108,7 @@ // Turn on edit mode. if (editMode) { $editButton.text(Drupal.t('Editing')); - // Close the Manage tray if open when entering edit mode. - if ($('#toolbar-item-administration-tray').hasClass('is-active')) { - $('#toolbar-item-administration').trigger('click'); - } + closeToolbarTrays(); $editables = $('[data-drupal-outsidein="editable"]').once('outsidein'); if ($editables.length) { diff --git a/core/modules/outside_in/templates/outside-in-page-wrapper.html.twig b/core/modules/outside_in/templates/outside-in-page-wrapper.html.twig index 588918334..d28cb4fae 100644 --- a/core/modules/outside_in/templates/outside-in-page-wrapper.html.twig +++ b/core/modules/outside_in/templates/outside-in-page-wrapper.html.twig @@ -17,5 +17,4 @@ {{ children }} -
{% endif %} diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php index dd79fc2c4..9388ec8d7 100644 --- a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php @@ -51,18 +51,24 @@ class OutsideInBlockFormTest extends OutsideInJavascriptTestBase { * Tests opening Offcanvas tray by click blocks and elements in the blocks. */ public function testBlocks() { + // @todo: re-enable once https://www.drupal.org/node/2830485 is resolved. + $this->markTestSkipped('Test skipped due to random failures in DrupalCI, see https://www.drupal.org/node/2830485'); + + $web_assert = $this->assertSession(); $blocks = [ [ 'id' => 'block-powered', 'new_page_text' => 'Can you imagine anyone showing the label on this block?', 'element_selector' => '.content a', 'button_text' => 'Save Powered by Drupal', + 'toolbar_item' => '#toolbar-item-user', ], [ 'id' => 'block-branding', 'new_page_text' => 'The site that will live a very short life.', 'element_selector' => 'a[rel="home"]:nth-child(2)', 'button_text' => 'Save Site branding', + 'toolbar_item' => '#toolbar-item-administration', ], [ 'id' => 'block-search', @@ -74,7 +80,22 @@ class OutsideInBlockFormTest extends OutsideInJavascriptTestBase { foreach ($blocks as $block) { $block_selector = '#' . $block['id']; $this->drupalGet('user'); + if (isset($block['toolbar_item'])) { + // Check that you can open a toolbar tray and it will be closed after + // entering edit mode. + if ($element = $page->find('css', "#toolbar-administration a.is-active")) { + // If a tray was open from page load close it. + $element->click(); + $this->waitForNoElement("#toolbar-administration a.is-active"); + } + $page->find('css', $block['toolbar_item'])->click(); + $this->waitForElement("{$block['toolbar_item']}.is-active"); + } $this->toggleEditingMode(); + if (isset($block['toolbar_item'])) { + $this->waitForNoElement("{$block['toolbar_item']}.is-active"); + } + $this->openBlockForm($block_selector); switch ($block['id']) { @@ -93,8 +114,7 @@ class OutsideInBlockFormTest extends OutsideInJavascriptTestBase { if (isset($block['new_page_text'])) { $page->pressButton($block['button_text']); // Make sure the changes are present. - $this->getSession()->wait(500); - $web_assert = $this->assertSession(); + $this->assertSession()->assertWaitOnAjaxRequest(); $web_assert->pageTextContains($block['new_page_text']); } @@ -124,7 +144,7 @@ class OutsideInBlockFormTest extends OutsideInJavascriptTestBase { protected function toggleEditingMode() { $this->waitForElement('div[data-contextual-id="block:block=powered:langcode=en|outside_in::langcode=en"] .contextual-links a'); - $this->waitForElement('#toolbar-bar', 3000); + $this->waitForElement('#toolbar-bar'); $edit_button = $this->getSession()->getPage()->find('css', '#toolbar-bar div.contextual-toolbar-tab button'); @@ -155,4 +175,5 @@ class OutsideInBlockFormTest extends OutsideInJavascriptTestBase { $this->waitForOffCanvasToOpen(); $this->assertOffCanvasBlockFormIsValid(); } + } diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php index da0be1b2b..fe8fcf405 100644 --- a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php @@ -36,8 +36,7 @@ abstract class OutsideInJavascriptTestBase extends JavascriptTestBase { * Waits for Off-canvas tray to close. */ protected function waitForOffCanvasToClose() { - $condition = "(jQuery('#drupal-offcanvas').length == 0)"; - $this->assertJsCondition($condition); + $this->waitForNoElement('#drupal-offcanvas'); } /** @@ -46,9 +45,9 @@ abstract class OutsideInJavascriptTestBase extends JavascriptTestBase { * @param string $selector * CSS selector. * @param int $timeout - * (optional) Timeout in milliseconds, defaults to 1000. + * (optional) Timeout in milliseconds, defaults to 10000. */ - protected function waitForElement($selector, $timeout = 1000) { + protected function waitForElement($selector, $timeout = 10000) { $condition = "(jQuery('$selector').length > 0)"; $this->assertJsCondition($condition, $timeout); } @@ -64,4 +63,17 @@ abstract class OutsideInJavascriptTestBase extends JavascriptTestBase { return $tray; } + /** + * Waits for an element to be removed from the page. + * + * @param string $selector + * CSS selector. + * @param int $timeout + * (optional) Timeout in milliseconds, defaults to 10000. + */ + protected function waitForNoElement($selector, $timeout = 10000) { + $condition = "(jQuery('$selector').length == 0)"; + $this->assertJsCondition($condition, $timeout); + } + } diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 15ec5375a..8c7bf8ddd 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -158,7 +158,6 @@ function template_preprocess_responsive_image(&$variables) { unset($variables['height']); } - $image = \Drupal::service('image.factory')->get($variables['uri']); $responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']); // If a responsive image style is not selected, log the error and stop // execution. @@ -176,7 +175,7 @@ function template_preprocess_responsive_image(&$variables) { $breakpoints = array_reverse(\Drupal::service('breakpoint.manager')->getBreakpointsByGroup($responsive_image_style->getBreakpointGroup())); foreach ($responsive_image_style->getKeyedImageStyleMappings() as $breakpoint_id => $multipliers) { if (isset($breakpoints[$breakpoint_id])) { - $variables['sources'][] = responsive_image_build_source_attributes($image, $variables, $breakpoints[$breakpoint_id], $multipliers); + $variables['sources'][] = _responsive_image_build_source_attributes($variables, $breakpoints[$breakpoint_id], $multipliers); } } @@ -192,7 +191,7 @@ function template_preprocess_responsive_image(&$variables) { } $variables['img_element'] = array( '#theme' => 'image', - '#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $image->getSource()), + '#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']), ); } else { @@ -205,7 +204,7 @@ function template_preprocess_responsive_image(&$variables) { '#theme' => 'image', '#srcset' => array( array( - 'uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $image->getSource()), + 'uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']), ), ), ); @@ -224,6 +223,32 @@ function template_preprocess_responsive_image(&$variables) { } } +/** + * Helper function for template_preprocess_responsive_image(). + * + * @param \Drupal\Core\Image\ImageInterface $image + * The image to build the tags for. + * @param array $variables + * An array with the following keys: + * - responsive_image_style_id: The \Drupal\responsive_image\Entity\ResponsiveImageStyle + * ID. + * - width: The width of the image (if known). + * - height: The height of the image (if known). + * - uri: The URI of the image file. + * @param \Drupal\breakpoint\BreakpointInterface $breakpoint + * The breakpoint for this source tag. + * @param array $multipliers + * An array with multipliers as keys and image style mappings as values. + * + * @return \Drupal\Core\Template\Attribute[] + * An array of attributes for the source tag. + * + * @deprecated in Drupal 8.3.x and will be removed before 9.0.0. + */ +function responsive_image_build_source_attributes(ImageInterface $image, array $variables, BreakpointInterface $breakpoint, array $multipliers) { + return _responsive_image_build_source_attributes($variables, $breakpoint, $multipliers); +} + /** * Helper function for template_preprocess_responsive_image(). * @@ -346,8 +371,6 @@ function template_preprocess_responsive_image(&$variables) { * See http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#image-candidate-string * for further information. * - * @param \Drupal\Core\Image\ImageInterface $image - * The image to build the tags for. * @param array $variables * An array with the following keys: * - responsive_image_style_id: The \Drupal\responsive_image\Entity\ResponsiveImageStyle @@ -363,10 +386,17 @@ function template_preprocess_responsive_image(&$variables) { * @return \Drupal\Core\Template\Attribute[] * An array of attributes for the source tag. */ -function responsive_image_build_source_attributes(ImageInterface $image, array $variables, BreakpointInterface $breakpoint, array $multipliers) { - $width = isset($variables['width']) && !empty($variables['width']) ? $variables['width'] : $image->getWidth(); - $height = isset($variables['height']) && !empty($variables['height']) ? $variables['height'] : $image->getHeight(); - $extension = pathinfo($image->getSource(), PATHINFO_EXTENSION); +function _responsive_image_build_source_attributes(array $variables, BreakpointInterface $breakpoint, array $multipliers) { + if ((empty($variables['width']) || empty($variables['height']))) { + $image = \Drupal::service('image.factory')->get($variables['uri']); + $width = $image->getWidth(); + $height = $image->getHeight(); + } + else { + $width = $variables['width']; + $height = $variables['height']; + } + $extension = pathinfo($variables['uri'], PATHINFO_EXTENSION); $sizes = array(); $srcset = array(); $derivative_mime_types = array(); @@ -388,12 +418,12 @@ function responsive_image_build_source_attributes(ImageInterface $image, array $ // this breakpoint should be merged into one srcset and the sizes // attribute should be merged as well. if (is_null($dimensions['width'])) { - throw new \LogicException("Could not determine image width for '{$image->getSource()}' using image style with ID: $image_style_name. This image style can not be used for a responsive image style mapping using the 'sizes' attribute."); + throw new \LogicException("Could not determine image width for '{$variables['uri']}' using image style with ID: $image_style_name. This image style can not be used for a responsive image style mapping using the 'sizes' attribute."); } // Use the image width as key so we can sort the array later on. // Images within a srcset should be sorted from small to large, since // the first matching source will be used. - $srcset[intval($dimensions['width'])] = _responsive_image_image_style_url($image_style_name, $image->getSource()) . ' ' . $dimensions['width'] . 'w'; + $srcset[intval($dimensions['width'])] = _responsive_image_image_style_url($image_style_name, $variables['uri']) . ' ' . $dimensions['width'] . 'w'; $sizes = array_merge(explode(',', $image_style_mapping['image_mapping']['sizes']), $sizes); } break; @@ -407,7 +437,7 @@ function responsive_image_build_source_attributes(ImageInterface $image, array $ // be sorted from small to large, since the first matching source will // be used. We multiply it by 100 so multipliers with up to two decimals // can be used. - $srcset[intval(Unicode::substr($multiplier, 0, -1) * 100)] = _responsive_image_image_style_url($image_style_mapping['image_mapping'], $image->getSource()) . ' ' . $multiplier; + $srcset[intval(Unicode::substr($multiplier, 0, -1) * 100)] = _responsive_image_image_style_url($image_style_mapping['image_mapping'], $variables['uri']) . ' ' . $multiplier; break; } } diff --git a/core/modules/rest/src/Entity/RestResourceConfig.php b/core/modules/rest/src/Entity/RestResourceConfig.php index a31a1a4cf..bb8358416 100644 --- a/core/modules/rest/src/Entity/RestResourceConfig.php +++ b/core/modules/rest/src/Entity/RestResourceConfig.php @@ -243,24 +243,16 @@ class RestResourceConfig extends ConfigEntityBase implements RestResourceConfigI } /** - * Normalizes the method to upper case and check validity. + * Normalizes the method. * * @param string $method * The request method. * * @return string - * The normalised request method. - * - * @throws \InvalidArgumentException - * If the method is not supported. + * The normalized request method. */ protected function normalizeRestMethod($method) { - $valid_methods = ['GET', 'POST', 'PATCH', 'DELETE']; - $normalised_method = strtoupper($method); - if (!in_array($normalised_method, $valid_methods)) { - throw new \InvalidArgumentException('The method is not supported.'); - } - return $normalised_method; + return strtoupper($method); } } diff --git a/core/modules/rest/src/Plugin/views/row/DataFieldRow.php b/core/modules/rest/src/Plugin/views/row/DataFieldRow.php index c92726f2e..8cebb738d 100644 --- a/core/modules/rest/src/Plugin/views/row/DataFieldRow.php +++ b/core/modules/rest/src/Plugin/views/row/DataFieldRow.php @@ -137,10 +137,6 @@ class DataFieldRow extends RowPluginBase { $output = array(); foreach ($this->view->field as $id => $field) { - // Don't render anything if this field is excluded. - if (!empty($field->options['exclude'])) { - continue; - } // If the raw output option has been set, just get the raw value. if (!empty($this->rawOutputOptions[$id])) { $value = $field->getValue($row); @@ -150,7 +146,10 @@ class DataFieldRow extends RowPluginBase { $value = $field->advancedRender($row); } - $output[$this->getFieldKeyAlias($id)] = $value; + // Omit excluded fields from the rendered output. + if (empty($field->options['exclude'])) { + $output[$this->getFieldKeyAlias($id)] = $value; + } } return $output; diff --git a/core/modules/rest/src/Tests/AuthTest.php b/core/modules/rest/src/Tests/AuthTest.php deleted file mode 100644 index 9f4224f64..000000000 --- a/core/modules/rest/src/Tests/AuthTest.php +++ /dev/null @@ -1,103 +0,0 @@ -enableService('entity:' . $entity_type, 'GET', NULL, array('basic_auth')); - - // Create an entity programmatically. - $entity = $this->entityCreate($entity_type); - $entity->save(); - - // Try to read the resource as an anonymous user, which should not work. - $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); - $this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.'); - $this->assertRaw(json_encode(['message' => 'A fatal error occurred: No authentication credentials provided.'])); - - // Ensure that cURL settings/headers aren't carried over to next request. - unset($this->curlHandle); - - // Create a user account that has the required permissions to read - // resources via the REST API, but the request is authenticated - // with session cookies. - $permissions = $this->entityPermissions($entity_type, 'view'); - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - - // Try to read the resource with session cookie authentication, which is - // not enabled and should not work. - $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); - $this->assertResponse('403', 'HTTP response code is 403 when the request was authenticated by the wrong authentication provider.'); - - // Ensure that cURL settings/headers aren't carried over to next request. - unset($this->curlHandle); - - // Now read it with the Basic authentication which is enabled and should - // work. - $this->basicAuthGet($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), $account->getUsername(), $account->pass_raw); - $this->assertResponse('200', 'HTTP response code is 200 for successfully authenticated requests.'); - $this->curlClose(); - } - - /** - * Performs a HTTP request with Basic authentication. - * - * We do not use \Drupal\simpletest\WebTestBase::drupalGet because we need to - * set curl settings for basic authentication. - * - * @param \Drupal\Core\Url $url - * A Url object. - * @param string $username - * The user name to authenticate with. - * @param string $password - * The password. - * @param string $mime_type - * The MIME type for the Accept header. - * - * @return string - * Curl output. - */ - protected function basicAuthGet(Url $url, $username, $password, $mime_type = NULL) { - if (!isset($mime_type)) { - $mime_type = $this->defaultMimeType; - } - $out = $this->curlExec( - array( - CURLOPT_HTTPGET => TRUE, - CURLOPT_URL => $url->setAbsolute()->toString(), - CURLOPT_NOBODY => FALSE, - CURLOPT_HTTPAUTH => CURLAUTH_BASIC, - CURLOPT_USERPWD => $username . ':' . $password, - CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type), - ) - ); - - $this->verbose('GET request to: ' . $url->toString() . - '
' . $out); - - return $out; - } - -} diff --git a/core/modules/rest/src/Tests/CreateTest.php b/core/modules/rest/src/Tests/CreateTest.php deleted file mode 100644 index 2454d2a0c..000000000 --- a/core/modules/rest/src/Tests/CreateTest.php +++ /dev/null @@ -1,484 +0,0 @@ -addDefaultCommentField('node', 'resttest'); - // Get the 'serializer' service. - $this->serializer = $this->container->get('serializer'); - } - - /** - * Try to create a resource which is not REST API enabled. - */ - public function testCreateResourceRestApiNotEnabled() { - $entity_type = 'entity_test'; - // Enables the REST service for a specific entity type. - $this->enableService('entity:' . $entity_type, 'POST'); - - // Get the necessary user permissions to create the current entity type. - $permissions = $this->entityPermissions($entity_type, 'create'); - - // Create the user. - $account = $this->drupalCreateUser($permissions); - // Populate some entity properties before create the entity. - $entity_values = $this->entityValues($entity_type); - $entity = EntityTest::create($entity_values); - - // Serialize the entity before the POST request. - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - - // Disable all resource types. - $this->enableService(FALSE); - $this->drupalLogin($account); - - // POST request to create the current entity. GET request for CSRF token - // is included into the httpRequest() method. - $this->httpRequest('entity/entity_test', 'POST', $serialized, $this->defaultMimeType); - - // The resource is not enabled. So, we receive a 'not found' response. - $this->assertResponse(404); - $this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.'); - } - - /** - * Ensure that an entity cannot be created without the restful permission. - */ - public function testCreateWithoutPermissionIfBcFlagIsOn() { - $rest_settings = $this->config('rest.settings'); - $rest_settings->set('bc_entity_resource_permissions', TRUE) - ->save(TRUE); - - $entity_type = 'entity_test'; - // Enables the REST service for 'entity_test' entity type. - $this->enableService('entity:' . $entity_type, 'POST'); - $permissions = $this->entityPermissions($entity_type, 'create'); - // Create a user without the 'restful post entity:entity_test permission. - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - // Populate some entity properties before create the entity. - $entity_values = $this->entityValues($entity_type); - $entity = EntityTest::create($entity_values); - - // Serialize the entity before the POST request. - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - - // Create the entity over the REST API. - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(403); - $this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.'); - - // Create a user with the 'restful post entity:entity_test permission and - // try again. This time, we should be able to create an entity. - $permissions[] = 'restful post entity:' . $entity_type; - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(201); - } - - /** - * Tests valid and invalid create requests for 'entity_test' entity type. - */ - public function testCreateEntityTest() { - $entity_type = 'entity_test'; - // Enables the REST service for 'entity_test' entity type. - $this->enableService('entity:' . $entity_type, 'POST'); - // Create two accounts with the required permissions to create resources. - // The second one has administrative permissions. - $accounts = $this->createAccountPerEntity($entity_type); - - // Verify create requests per user. - foreach ($accounts as $key => $account) { - $this->drupalLogin($account); - // Populate some entity properties before create the entity. - $entity_values = $this->entityValues($entity_type); - $entity = EntityTest::create($entity_values); - - // Serialize the entity before the POST request. - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - - // Create the entity over the REST API. - $this->assertCreateEntityOverRestApi($entity_type, $serialized); - // Get the entity ID from the location header and try to read it from the - // database. - $this->assertReadEntityIdFromHeaderAndDb($entity_type, $entity, $entity_values); - - // Try to create an entity with an access protected field. - // @see entity_test_entity_field_access() - $normalized = $this->serializer->normalize($entity, $this->defaultFormat, ['account' => $account]); - $normalized['field_test_text'][0]['value'] = 'no access value'; - $this->httpRequest('entity/' . $entity_type, 'POST', $this->serializer->serialize($normalized, $this->defaultFormat, ['account' => $account]), $this->defaultMimeType); - $this->assertResponse(403); - $this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.'); - - // Try to create a field with a text format this user has no access to. - $entity->field_test_text->value = $entity_values['field_test_text'][0]['value']; - $entity->field_test_text->format = 'full_html'; - - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - // The value selected is not a valid choice because the format must be - // 'plain_txt'. - $this->assertResponse(422); - $this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.'); - - // Restore the valid test value. - $entity->field_test_text->format = 'plain_text'; - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - - // Try to send invalid data that cannot be correctly deserialized. - $this->assertCreateEntityInvalidData($entity_type); - - // Try to send no data at all, which does not make sense on POST requests. - $this->assertCreateEntityNoData($entity_type); - - // Try to send invalid data to trigger the entity validation constraints. - // Send a UUID that is too long. - $this->assertCreateEntityInvalidSerialized($entity, $entity_type); - - // Try to create an entity without proper permissions. - $this->assertCreateEntityWithoutProperPermissions($entity_type, $serialized, ['account' => $account]); - - } - - } - - /** - * Tests several valid and invalid create requests for 'node' entity type. - */ - public function testCreateNode() { - $entity_type = 'node'; - // Enables the REST service for 'node' entity type. - $this->enableService('entity:' . $entity_type, 'POST'); - // Create two accounts that have the required permissions to create - // resources. The second one has administrative permissions. - $accounts = $this->createAccountPerEntity($entity_type); - - // Verify create requests per user. - foreach ($accounts as $key => $account) { - $this->drupalLogin($account); - // Populate some entity properties before create the entity. - $entity_values = $this->entityValues($entity_type); - $entity = Node::create($entity_values); - - // Verify that user cannot create content when trying to write to fields - // where it is not possible. - if (!$account->hasPermission('administer nodes')) { - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(403); - // Remove fields where non-administrative users cannot write. - $entity = $this->removeNodeFieldsForNonAdminUsers($entity); - } - else { - // Changed and revision_timestamp fields can never be added. - $entity->set('changed', NULL); - $entity->set('revision_timestamp', NULL); - } - - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - - // Create the entity over the REST API. - $this->assertCreateEntityOverRestApi($entity_type, $serialized); - - // Get the new entity ID from the location header and try to read it from - // the database. - $this->assertReadEntityIdFromHeaderAndDb($entity_type, $entity, $entity_values); - - // Try to send invalid data that cannot be correctly deserialized. - $this->assertCreateEntityInvalidData($entity_type); - - // Try to send no data at all, which does not make sense on POST requests. - $this->assertCreateEntityNoData($entity_type); - - // Try to send invalid data to trigger the entity validation constraints. Send a UUID that is too long. - $this->assertCreateEntityInvalidSerialized($entity, $entity_type); - - // Try to create an entity without proper permissions. - $this->assertCreateEntityWithoutProperPermissions($entity_type, $serialized); - - } - - } - - /** - * Test comment creation. - */ - protected function testCreateComment() { - $node = Node::create([ - 'type' => 'resttest', - 'title' => 'some node', - ]); - $node->save(); - $entity_type = 'comment'; - // Enable the REST service for 'comment' entity type. - $this->enableService('entity:' . $entity_type, 'POST'); - // Create two accounts that have the required permissions to create - // resources, The second one has administrative permissions. - $accounts = $this->createAccountPerEntity($entity_type); - $account = end($accounts); - - $this->drupalLogin($account); - $entity_values = $this->entityValues($entity_type); - $entity_values['entity_id'] = $node->id(); - - $entity = Comment::create($entity_values); - - // Changed field can never be added. - unset($entity->changed); - - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - - // Create the entity over the REST API. - $this->assertCreateEntityOverRestApi($entity_type, $serialized); - - // Get the new entity ID from the location header and try to read it from - // the database. - $this->assertReadEntityIdFromHeaderAndDb($entity_type, $entity, $entity_values); - - // Try to send invalid data that cannot be correctly deserialized. - $this->assertCreateEntityInvalidData($entity_type); - - // Try to send no data at all, which does not make sense on POST requests. - $this->assertCreateEntityNoData($entity_type); - - // Try to send invalid data to trigger the entity validation constraints. - // Send a UUID that is too long. - $this->assertCreateEntityInvalidSerialized($entity, $entity_type); - } - - /** - * Tests several valid and invalid create requests for 'user' entity type. - */ - public function testCreateUser() { - $entity_type = 'user'; - // Enables the REST service for 'user' entity type. - $this->enableService('entity:' . $entity_type, 'POST'); - // Create two accounts that have the required permissions to create - // resources. The second one has administrative permissions. - $accounts = $this->createAccountPerEntity($entity_type); - - foreach ($accounts as $key => $account) { - $this->drupalLogin($account); - $entity_values = $this->entityValues($entity_type); - $entity = User::create($entity_values); - - // Verify that only administrative users can create users. - if (!$account->hasPermission('administer users')) { - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(403); - continue; - } - - // Changed field can never be added. - $entity->set('changed', NULL); - - $serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); - - // Create the entity over the REST API. - $this->assertCreateEntityOverRestApi($entity_type, $serialized); - - // Get the new entity ID from the location header and try to read it from - // the database. - $this->assertReadEntityIdFromHeaderAndDb($entity_type, $entity, $entity_values); - - // Try to send invalid data that cannot be correctly deserialized. - $this->assertCreateEntityInvalidData($entity_type); - - // Try to send no data at all, which does not make sense on POST requests. - $this->assertCreateEntityNoData($entity_type); - - // Try to send invalid data to trigger the entity validation constraints. - // Send a UUID that is too long. - $this->assertCreateEntityInvalidSerialized($entity, $entity_type); - } - - } - - /** - * Creates user accounts that have the required permissions to create - * resources via the REST API. The second one has administrative permissions. - * - * @param string $entity_type - * Entity type needed to apply user permissions. - * @return array - * An array that contains user accounts. - */ - public function createAccountPerEntity($entity_type) { - $accounts = array(); - // Get the necessary user permissions for the current $entity_type creation. - $permissions = $this->entityPermissions($entity_type, 'create'); - // Create user without administrative permissions. - $accounts[] = $this->drupalCreateUser($permissions); - // Add administrative permissions for nodes and users. - $permissions[] = 'administer nodes'; - $permissions[] = 'administer users'; - $permissions[] = 'administer comments'; - // Create an administrative user. - $accounts[] = $this->drupalCreateUser($permissions); - - return $accounts; - } - - /** - * Creates the entity over the REST API. - * - * @param string $entity_type - * The type of the entity that should be created. - * @param string $serialized - * The body for the POST request. - */ - public function assertCreateEntityOverRestApi($entity_type, $serialized = NULL) { - // Note: this will fail with PHP 5.6 when always_populate_raw_post_data is - // set to something other than -1. See https://www.drupal.org/node/2456025. - // Try first without the CSRF token, which should fail. - $url = Url::fromUri('internal:/entity/' . $entity_type)->setOption('query', ['_format' => $this->defaultFormat]); - $this->httpRequest($url, 'POST', $serialized, $this->defaultMimeType, FALSE); - $this->assertResponse(403); - $this->assertRaw('X-CSRF-Token request header is missing'); - // Then try with an invalid CSRF token. - $this->httpRequest($url, 'POST', $serialized, $this->defaultMimeType, 'invalid-csrf-token'); - $this->assertResponse(403); - $this->assertRaw('X-CSRF-Token request header is invalid'); - // Then try with the CSRF token. - $response = $this->httpRequest($url, 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(201); - - // Make sure that the response includes an entity in the body and check the - // UUID as an example. - $request = Json::decode($serialized); - $response = Json::decode($response); - $this->assertEqual($request['uuid'][0]['value'], $response['uuid'][0]['value'], 'Got new entity created as response after successful POST over Rest API'); - } - - /** - * Gets the new entity ID from the location header and tries to read it from - * the database. - * - * @param string $entity_type - * Entity type we need to load the entity from DB. - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity we want to check that was inserted correctly. - * @param array $entity_values - * The values of $entity. - */ - public function assertReadEntityIdFromHeaderAndDb($entity_type, EntityInterface $entity, array $entity_values = array()) { - // Get the location from the HTTP response header. - $location_url = $this->drupalGetHeader('location'); - $url_parts = explode('/', $location_url); - $id = end($url_parts); - - // Get the entity using the ID found. - $loaded_entity = \Drupal::entityManager()->getStorage($entity_type)->load($id); - $this->assertNotIdentical(FALSE, $loaded_entity, 'The new ' . $entity_type . ' was found in the database.'); - $this->assertEqual($entity->uuid(), $loaded_entity->uuid(), 'UUID of created entity is correct.'); - - // Verify that the field values sent and received from DB are the same. - foreach ($entity_values as $property => $value) { - $actual_value = $loaded_entity->get($property)->value; - $send_value = $entity->get($property)->value; - $this->assertEqual($send_value, $actual_value, 'Created property ' . $property . ' expected: ' . $send_value . ', actual: ' . $actual_value); - } - - // Delete the entity loaded from DB. - $loaded_entity->delete(); - } - - /** - * Try to send invalid data that cannot be correctly deserialized. - * - * @param string $entity_type - * The type of the entity that should be created. - */ - public function assertCreateEntityInvalidData($entity_type) { - $this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', $this->defaultMimeType); - $this->assertResponse(400); - } - - /** - * Try to send no data at all, which does not make sense on POST requests. - * - * @param string $entity_type - * The type of the entity that should be created. - */ - public function assertCreateEntityNoData($entity_type) { - $this->httpRequest('entity/' . $entity_type, 'POST', NULL, $this->defaultMimeType); - $this->assertResponse(400); - } - - /** - * Send an invalid UUID to trigger the entity validation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity we want to check that was inserted correctly. - * @param string $entity_type - * The type of the entity that should be created. - * @param array $context - * Options normalizers/encoders have access to. - */ - public function assertCreateEntityInvalidSerialized(EntityInterface $entity, $entity_type, array $context = array()) { - // Add a UUID that is too long. - $entity->set('uuid', $this->randomMachineName(129)); - $invalid_serialized = $this->serializer->serialize($entity, $this->defaultFormat, $context); - - $response = $this->httpRequest(Url::fromRoute("rest.entity.$entity_type.POST")->setRouteParameter('_format', $this->defaultFormat), 'POST', $invalid_serialized, $this->defaultMimeType); - - // Unprocessable Entity as response. - $this->assertResponse(422); - - // Verify that the text of the response is correct. - $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n"); - } - - /** - * Try to create an entity without proper permissions. - * - * @param string $entity_type - * The type of the entity that should be created. - * @param string $serialized - * The body for the POST request. - */ - public function assertCreateEntityWithoutProperPermissions($entity_type, $serialized = NULL) { - $this->drupalLogout(); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - // Forbidden Error as response. - $this->assertResponse(403); - $this->assertFalse(\Drupal::entityManager()->getStorage($entity_type)->loadMultiple(), 'No entity has been created in the database.'); - } - -} diff --git a/core/modules/rest/src/Tests/CsrfTest.php b/core/modules/rest/src/Tests/CsrfTest.php deleted file mode 100644 index f0063ad8e..000000000 --- a/core/modules/rest/src/Tests/CsrfTest.php +++ /dev/null @@ -1,124 +0,0 @@ -enableService('entity:' . $this->testEntityType, 'POST', 'hal_json', array('basic_auth', 'cookie')); - - // Create a user account that has the required permissions to create - // resources via the REST API. - $permissions = $this->entityPermissions($this->testEntityType, 'create'); - $this->account = $this->drupalCreateUser($permissions); - - // Serialize an entity to a string to use in the content body of the POST - // request. - $serializer = $this->container->get('serializer'); - $entity_values = $this->entityValues($this->testEntityType); - $entity = $this->container->get('entity_type.manager') - ->getStorage($this->testEntityType) - ->create($entity_values); - $this->serialized = $serializer->serialize($entity, $this->defaultFormat); - } - - /** - * Tests that CSRF check is not triggered for Basic Auth requests. - */ - public function testBasicAuth() { - $curl_options = $this->getCurlOptions(); - $curl_options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; - $curl_options[CURLOPT_USERPWD] = $this->account->getUsername() . ':' . $this->account->pass_raw; - $this->curlExec($curl_options); - $this->assertResponse(201); - // Ensure that the entity was created. - $loaded_entity = $this->loadEntityFromLocationHeader($this->drupalGetHeader('location')); - $this->assertTrue($loaded_entity, 'An entity was created in the database'); - } - - /** - * Tests that CSRF check is triggered for Cookie Auth requests. - * - * @deprecated as of Drupal 8.2.x, will be removed before Drupal 9.0.0. Use - * \Drupal\Tests\system\Functional\CsrfRequestHeaderTest::testRouteAccess - * instead. - */ - public function testCookieAuth() { - $this->drupalLogin($this->account); - - $curl_options = $this->getCurlOptions(); - - // Try to create an entity without the CSRF token. - // Note: this will fail with PHP 5.6 when always_populate_raw_post_data is - // set to something other than -1. See https://www.drupal.org/node/2456025. - $this->curlExec($curl_options); - $this->assertResponse(403); - // Ensure that the entity was not created. - $storage = $this->container->get('entity_type.manager') - ->getStorage($this->testEntityType); - $storage->resetCache(); - $this->assertFalse($storage->loadMultiple(), 'No entity has been created in the database.'); - - // Create an entity with the CSRF token. - $token = $this->drupalGet('rest/session/token'); - $curl_options[CURLOPT_HTTPHEADER][] = "X-CSRF-Token: $token"; - $this->curlExec($curl_options); - $this->assertResponse(201); - // Ensure that the entity was created. - $loaded_entity = $this->loadEntityFromLocationHeader($this->drupalGetHeader('location')); - $this->assertTrue($loaded_entity, 'An entity was created in the database'); - } - - /** - * Gets the cURL options to create an entity with POST. - * - * @return array - * The array of cURL options. - */ - protected function getCurlOptions() { - return array( - CURLOPT_HTTPGET => FALSE, - CURLOPT_POST => TRUE, - CURLOPT_POSTFIELDS => $this->serialized, - CURLOPT_URL => Url::fromRoute('rest.entity.' . $this->testEntityType . '.POST')->setAbsolute()->toString(), - CURLOPT_NOBODY => FALSE, - CURLOPT_HTTPHEADER => array( - "Content-Type: {$this->defaultMimeType}", - ), - ); - } - -} diff --git a/core/modules/rest/src/Tests/DeleteTest.php b/core/modules/rest/src/Tests/DeleteTest.php deleted file mode 100644 index 88db0fd9d..000000000 --- a/core/modules/rest/src/Tests/DeleteTest.php +++ /dev/null @@ -1,90 +0,0 @@ -enableService('entity:' . $entity_type, 'DELETE'); - // Create a user account that has the required permissions to delete - // resources via the REST API. - $permissions = $this->entityPermissions($entity_type, 'delete'); - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - - // Create an entity programmatically. - $entity = $this->entityCreate($entity_type); - $entity->save(); - // Try first to delete over REST API without the CSRF token. - $url = $entity->toUrl()->setRouteParameter('_format', $this->defaultFormat); - $this->httpRequest($url, 'DELETE', NULL, 'application/hal+json', FALSE); - $this->assertResponse(403); - $this->assertRaw('X-CSRF-Token request header is missing'); - // Then try with an invalid CSRF token. - $this->httpRequest($url, 'DELETE', NULL, 'application/hal+json', 'invalid-csrf-token'); - $this->assertResponse(403); - $this->assertRaw('X-CSRF-Token request header is invalid'); - // Delete it over the REST API. - $response = $this->httpRequest($url, 'DELETE'); - $this->assertResponse(204); - // Clear the static cache with entity_load(), otherwise we won't see the - // update. - $storage = $this->container->get('entity_type.manager') - ->getStorage($entity_type); - $storage->resetCache([$entity->id()]); - $entity = $storage->load($entity->id()); - $this->assertFalse($entity, $entity_type . ' entity is not in the DB anymore.'); - $this->assertResponse('204', 'HTTP response code is correct.'); - $this->assertEqual($response, '', 'Response body is empty.'); - - // Try to delete an entity that does not exist. - $response = $this->httpRequest(Url::fromRoute('entity.' . $entity_type . '.canonical', [$entity_type => 9999]), 'DELETE'); - $this->assertResponse(404); - $this->assertText('The requested page could not be found.'); - - // Try to delete an entity without proper permissions. - $this->drupalLogout(); - // Re-save entity to the database. - $entity = $this->entityCreate($entity_type); - $entity->save(); - $this->httpRequest($entity->urlInfo(), 'DELETE'); - $this->assertResponse(403); - $storage->resetCache([$entity->id()]); - $this->assertNotIdentical(FALSE, $storage->load($entity->id()), - 'The ' . $entity_type . ' entity is still in the database.'); - } - // Try to delete a resource which is not REST API enabled. - $this->enableService(FALSE); - $account = $this->drupalCreateUser(); - $this->drupalLogin($account); - $this->httpRequest($account->urlInfo(), 'DELETE'); - $user_storage = $this->container->get('entity.manager')->getStorage('user'); - $user_storage->resetCache(array($account->id())); - $user = $user_storage->load($account->id()); - $this->assertEqual($account->id(), $user->id(), 'User still exists in the database.'); - $this->assertResponse(405); - } - -} diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php deleted file mode 100644 index 95dc475e0..000000000 --- a/core/modules/rest/src/Tests/NodeTest.php +++ /dev/null @@ -1,197 +0,0 @@ -enableService('entity:node', $method); - $permissions = $this->entityPermissions('node', $operation); - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - } - - /** - * Serializes and attempts to create a node via a REST "post" http request. - * - * @param array $data - */ - protected function postNode($data) { - // Enable node creation via POST. - $this->enableNodeConfiguration('POST', 'create'); - $this->enableService('entity:node', 'POST', 'json'); - - // Create a JSON version of a simple node with the title. - $serialized = $this->container->get('serializer')->serialize($data, 'json'); - - // Post to the REST service to create the node. - $this->httpRequest('/entity/node', 'POST', $serialized, 'application/json'); - } - - /** - * Tests the title on a newly created node. - * - * @param array $data - * @return \Drupal\node\Entity\Node - */ - protected function assertNodeTitleMatch($data) { - /** @var \Drupal\node\Entity\Node $node */ - // Load the newly created node. - $node = Node::load(1); - - // Test that the title is the same as what we posted. - $this->assertEqual($node->title->value, $data['title'][0]['value']); - - return $node; - } - - /** - * Performs various tests on nodes and their REST API. - */ - public function testNodes() { - $node_storage = $this->container->get('entity.manager')->getStorage('node'); - $this->enableNodeConfiguration('GET', 'view'); - - $node = $this->entityCreate('node'); - $node->save(); - $this->httpRequest($node->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); - $this->assertResponse(200); - $this->assertHeader('Content-type', $this->defaultMimeType); - - // Also check that JSON works and the routing system selects the correct - // REST route. - $this->enableService('entity:node', 'GET', 'json'); - $this->httpRequest($node->urlInfo()->setRouteParameter('_format', 'json'), 'GET'); - $this->assertResponse(200); - $this->assertHeader('Content-type', 'application/json'); - - // Check that a simple PATCH update to the node title works as expected. - $this->enableNodeConfiguration('PATCH', 'update'); - - // Create a PATCH request body that only updates the title field. - $new_title = $this->randomString(); - $data = array( - '_links' => array( - 'type' => array( - 'href' => Url::fromUri('base:rest/type/node/resttest', array('absolute' => TRUE))->toString(), - ), - ), - 'title' => array( - array( - 'value' => $new_title, - ), - ), - ); - $serialized = $this->container->get('serializer')->serialize($data, $this->defaultFormat); - $this->httpRequest($node->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(200); - - // Reload the node from the DB and check if the title was correctly updated. - $node_storage->resetCache(array($node->id())); - $updated_node = $node_storage->load($node->id()); - $this->assertEqual($updated_node->getTitle(), $new_title); - // Make sure that the UUID of the node has not changed. - $this->assertEqual($node->get('uuid')->getValue(), $updated_node->get('uuid')->getValue(), 'UUID was not changed.'); - } - - /** - * Test creating a node using json serialization. - */ - public function testCreate() { - // Data to be used for serialization. - $data = [ - 'type' => [['target_id' => 'resttest']], - 'title' => [['value' => $this->randomString() ]], - ]; - - $this->postNode($data); - - // Make sure the response is "CREATED". - $this->assertResponse(201); - - // Make sure the node was created and the title matches. - $node = $this->assertNodeTitleMatch($data); - - // Make sure the request returned a redirect header to view the node. - $this->assertHeader('Location', $node->url('canonical', ['absolute' => TRUE])); - } - - /** - * Test bundle normalization when posting bundle as a simple string. - */ - public function testBundleNormalization() { - // Data to be used for serialization. - $data = [ - 'type' => 'resttest', - 'title' => [['value' => $this->randomString() ]], - ]; - - $this->postNode($data); - - // Make sure the response is "CREATED". - $this->assertResponse(201); - - // Make sure the node was created and the title matches. - $this->assertNodeTitleMatch($data); - } - - /** - * Test bundle normalization when posting using a simple string. - */ - public function testInvalidBundle() { - // Data to be used for serialization. - $data = [ - 'type' => 'bad_bundle_name', - 'title' => [['value' => $this->randomString() ]], - ]; - - $this->postNode($data); - - // Make sure the response is "Bad Request". - $this->assertResponse(400); - $this->assertResponseBody('{"error":"\"bad_bundle_name\" is not a valid bundle type for denormalization."}'); - } - - /** - * Test when the bundle is missing. - */ - public function testMissingBundle() { - // Data to be used for serialization. - $data = [ - 'title' => [['value' => $this->randomString() ]], - ]; - - // testing - $this->postNode($data); - - // Make sure the response is "Bad Request". - $this->assertResponse(400); - $this->assertResponseBody('{"error":"A string must be provided as a bundle value."}'); - } - -} diff --git a/core/modules/rest/src/Tests/PageCacheTest.php b/core/modules/rest/src/Tests/PageCacheTest.php deleted file mode 100644 index 66e57f5a5..000000000 --- a/core/modules/rest/src/Tests/PageCacheTest.php +++ /dev/null @@ -1,162 +0,0 @@ -serializer = $this->container->get('serializer'); - } - - /** - * Tests that configuration changes also clear the page cache. - */ - public function testConfigChangePageCache() { - // Allow anonymous users to issue GET requests. - user_role_grant_permissions('anonymous', ['view test entity', 'restful get entity:entity_test']); - - $this->enableService('entity:entity_test', 'POST'); - $permissions = [ - 'administer entity_test content', - ]; - $account = $this->drupalCreateUser($permissions); - - // Create an entity and test that the response from a post request is not - // cacheable. - $entity = $this->entityCreate('entity_test'); - $entity->set('field_test_text', 'custom cache tag value'); - $serialized = $this->serializer->serialize($entity, $this->defaultFormat); - // Log in for creating the entity. - $this->drupalLogin($account); - $this->httpRequest('entity/entity_test', 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(201); - - if ($this->getCacheHeaderValues('x-drupal-cache')) { - $this->fail('Post request is cached.'); - } - $this->drupalLogout(); - - $url = Url::fromUri('internal:/entity_test/1?_format=' . $this->defaultFormat); - - // Read it over the REST API. - $this->enableService('entity:entity_test', 'GET'); - $this->httpRequest($url, 'GET', NULL, $this->defaultMimeType); - $this->assertResponse(200, 'HTTP response code is correct.'); - $this->assertHeader('x-drupal-cache', 'MISS'); - $this->assertCacheTag('config:rest.resource.entity.entity_test'); - $this->assertCacheTag('entity_test:1'); - $this->assertCacheTag('entity_test_access:field_test_text'); - - // Read it again, should be page-cached now. - $this->httpRequest($url, 'GET', NULL, $this->defaultMimeType); - $this->assertResponse(200, 'HTTP response code is correct.'); - $this->assertHeader('x-drupal-cache', 'HIT'); - $this->assertCacheTag('config:rest.resource.entity.entity_test'); - $this->assertCacheTag('entity_test:1'); - $this->assertCacheTag('entity_test_access:field_test_text'); - - // Trigger a resource config save which should clear the page cache, so we - // should get a cache miss now for the same request. - $this->resourceConfigStorage->load('entity.entity_test')->save(); - $this->httpRequest($url, 'GET', NULL, $this->defaultMimeType); - $this->assertResponse(200, 'HTTP response code is correct.'); - $this->assertHeader('x-drupal-cache', 'MISS'); - $this->assertCacheTag('config:rest.resource.entity.entity_test'); - $this->assertCacheTag('entity_test:1'); - $this->assertCacheTag('entity_test_access:field_test_text'); - - // Log in for deleting / updating entity. - $this->drupalLogin($account); - - // Test that updating an entity is not cacheable. - $this->enableService('entity:entity_test', 'PATCH'); - - // Create a second stub entity for overwriting a field. - $patch_values['field_test_text'] = [0 => [ - 'value' => 'patched value', - 'format' => 'plain_text', - ]]; - $patch_entity = $this->container->get('entity_type.manager') - ->getStorage('entity_test') - ->create($patch_values); - // We don't want to overwrite the UUID. - $patch_entity->set('uuid', NULL); - $serialized = $this->container->get('serializer') - ->serialize($patch_entity, $this->defaultFormat); - - // Update the entity over the REST API. - $this->httpRequest($url, 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(200); - - if ($this->getCacheHeaderValues('x-drupal-cache')) { - $this->fail('Patch request is cached.'); - } - - // Test that the response from a delete request is not cacheable. - $this->enableService('entity:entity_test', 'DELETE'); - $this->httpRequest($url, 'DELETE'); - $this->assertResponse(204); - - if ($this->getCacheHeaderValues('x-drupal-cache')) { - $this->fail('Patch request is cached.'); - } - } - - /** - * Tests HEAD support when a REST resource supports GET. - */ - public function testHeadSupport() { - user_role_grant_permissions('anonymous', ['view test entity', 'restful get entity:entity_test']); - - // Create an entity programatically. - $this->entityCreate('entity_test')->save(); - - $url = Url::fromUri('internal:/entity_test/1?_format=' . $this->defaultFormat); - - $this->enableService('entity:entity_test', 'GET'); - - $this->httpRequest($url, 'HEAD', NULL, $this->defaultMimeType); - $this->assertResponse(200, 'HTTP response code is correct.'); - $this->assertHeader('X-Drupal-Cache', 'MISS'); - $this->assertResponseBody(''); - - $response = $this->httpRequest($url, 'GET', NULL, $this->defaultMimeType); - $this->assertResponse(200, 'HTTP response code is correct.'); - $this->assertHeader('X-Drupal-Cache', 'HIT'); - $this->assertCacheTag('config:rest.resource.entity.entity_test'); - $this->assertCacheTag('entity_test:1'); - $data = Json::decode($response); - $this->assertEqual($data['type'][0]['value'], 'entity_test'); - } - -} diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php index 7cb0f1bde..c755967ea 100644 --- a/core/modules/rest/src/Tests/RESTTestBase.php +++ b/core/modules/rest/src/Tests/RESTTestBase.php @@ -9,6 +9,8 @@ use Drupal\simpletest\WebTestBase; /** * Test helper class that provides a REST client method to send HTTP requests. + * + * @deprecated in Drupal 8.3.x-dev and will be removed before Drupal 9.0.0. Use \Drupal\Tests\rest\Functional\ResourceTestBase and \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase instead. Only retained for contributed module tests that may be using this base class. */ abstract class RESTTestBase extends WebTestBase { diff --git a/core/modules/rest/src/Tests/ReadTest.php b/core/modules/rest/src/Tests/ReadTest.php deleted file mode 100644 index dc6f57437..000000000 --- a/core/modules/rest/src/Tests/ReadTest.php +++ /dev/null @@ -1,205 +0,0 @@ -enableService('entity:' . $entity_type, 'GET'); - // Create a user account that has the required permissions to read - // resources via the REST API. - $permissions = $this->entityPermissions($entity_type, 'view'); - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - - // Create an entity programmatically. - $entity = $this->entityCreate($entity_type); - $entity->save(); - - // Verify that it exists: use a HEAD request. - $this->httpRequest($this->getReadUrl($entity), 'HEAD'); - $this->assertResponseBody(''); - $head_headers = $this->drupalGetHeaders(); - - // Read it over the REST API. - $response = $this->httpRequest($this->getReadUrl($entity), 'GET'); - $get_headers = $this->drupalGetHeaders(); - $this->assertResponse('200', 'HTTP response code is correct.'); - - // Verify that the GET and HEAD responses are the same, that the only - // difference is that there's no body. - unset($get_headers['date']); - unset($head_headers['date']); - unset($get_headers['content-length']); - unset($head_headers['content-length']); - unset($get_headers['x-drupal-dynamic-cache']); - unset($head_headers['x-drupal-dynamic-cache']); - $this->assertIdentical($get_headers, $head_headers); - $this->assertResponse('200', 'HTTP response code is correct.'); - - $this->assertHeader('content-type', $this->defaultMimeType); - $data = Json::decode($response); - // Only assert one example property here, other properties should be - // checked in serialization tests. - if ($entity instanceof ConfigEntityInterface) { - $this->assertEqual($data['uuid'], $entity->uuid(), 'Entity UUID is correct'); - } - else { - $this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct'); - } - - // Try to read the entity with an unsupported mime format. - $this->httpRequest($this->getReadUrl($entity, 'wrongformat'), 'GET'); - $this->assertResponse(406); - $this->assertHeader('Content-type', 'application/json'); - - // Try to read an entity that does not exist. - $response = $this->httpRequest($this->getReadUrl($entity, $this->defaultFormat, 9999), 'GET'); - $this->assertResponse(404); - switch ($entity_type) { - case 'node': - $path = '/node/{node}'; - break; - - case 'entity_test': - $path = '/entity_test/{entity_test}'; - break; - - default: - $path = "/entity/$entity_type/{" . $entity_type . '}'; - } - $expected_message = Json::encode(['message' => 'The "' . $entity_type . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . $entity_type . '.GET.hal_json")']); - $this->assertIdentical($expected_message, $response, 'Response message is correct.'); - - // Make sure that field level access works and that the according field is - // not available in the response. Only applies to entity_test. - // @see entity_test_entity_field_access() - if ($entity_type == 'entity_test') { - $entity->field_test_text->value = 'no access value'; - $entity->save(); - $response = $this->httpRequest($this->getReadUrl($entity), 'GET'); - $this->assertResponse(200); - $this->assertHeader('content-type', $this->defaultMimeType); - $data = Json::decode($response); - $this->assertFalse(isset($data['field_test_text']), 'Field access protected field is not visible in the response.'); - } - } - // Try to read a resource, the user entity, which is not REST API enabled. - $account = $this->drupalCreateUser(); - $this->drupalLogin($account); - $response = $this->httpRequest($this->getReadUrl($account), 'GET'); - - // \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical, - // non-REST route a match, but a lower quality one: no format restrictions - // means there's always a match and hence when there is no matching REST - // route, the non-REST route is used, but can't render into - // application/hal+json, so it returns a 406. - $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.'); - $this->assertEqual($response, Json::encode([ - 'message' => 'Not acceptable format: hal_json', - ])); - } - - /** - * Tests the resource structure. - */ - public function testResourceStructure() { - // Enable a service with a format restriction but no authentication. - $this->enableService('entity:node', 'GET', 'json'); - // Create a user account that has the required permissions to read - // resources via the REST API. - $permissions = $this->entityPermissions('node', 'view'); - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - - // Create an entity programmatically. - $entity = $this->entityCreate('node'); - $entity->save(); - - // Read it over the REST API. - $this->httpRequest($this->getReadUrl($entity, 'json'), 'GET'); - $this->assertResponse('200', 'HTTP response code is correct.'); - } - - /** - * Gets the read URL object for the entity. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to get the URL for. - * @param string $format - * The format to request the entity in. - * @param string $entity_id - * The entity ID to use in the URL, defaults to the entity's ID if know - * given. - * - * @return \Drupal\Core\Url - * The Url object. - */ - protected function getReadUrl(EntityInterface $entity, $format = NULL, $entity_id = NULL) { - if (!$format) { - $format = $this->defaultFormat; - } - if (!$entity_id) { - $entity_id = $entity->id(); - } - $entity_type = $entity->getEntityTypeId(); - if ($entity->hasLinkTemplate('canonical')) { - $url = $entity->toUrl('canonical'); - } - else { - $route_name = 'rest.entity.' . $entity_type . ".GET."; - // If testing unsupported format don't use the format to construct route - // name. This would give a RouteNotFoundException. - if ($format == 'wrongformat') { - $route_name .= $this->defaultFormat; - } - else { - $route_name .= $format; - } - $url = Url::fromRoute($route_name); - } - $url->setRouteParameter($entity_type, $entity_id); - $url->setRouteParameter('_format', $format); - return $url; - } - -} diff --git a/core/modules/rest/src/Tests/UpdateTest.php b/core/modules/rest/src/Tests/UpdateTest.php deleted file mode 100644 index d3784ed66..000000000 --- a/core/modules/rest/src/Tests/UpdateTest.php +++ /dev/null @@ -1,396 +0,0 @@ -addDefaultCommentField('entity_test', 'entity_test'); - } - - /** - * Tests several valid and invalid partial update requests on test entities. - */ - public function testPatchUpdate() { - $serializer = $this->container->get('serializer'); - // @todo Test all other entity types here as well. - $entity_type = 'entity_test'; - - $this->enableService('entity:' . $entity_type, 'PATCH'); - // Create a user account that has the required permissions to create - // resources via the REST API. - $permissions = $this->entityPermissions($entity_type, 'update'); - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - - $context = ['account' => $account]; - - // Create an entity and save it to the database. - $entity = $this->entityCreate($entity_type); - $entity->save(); - - // Create a second stub entity for overwriting a field. - $patch_values['field_test_text'] = array(0 => array( - 'value' => $this->randomString(), - 'format' => 'plain_text', - )); - $patch_entity = $this->container->get('entity_type.manager') - ->getStorage($entity_type) - ->create($patch_values); - // We don't want to overwrite the UUID. - $patch_entity->set('uuid', NULL); - $serialized = $serializer->serialize($patch_entity, $this->defaultFormat, $context); - - // Update the entity over the REST API but forget to specify a Content-Type - // header, this should throw the proper exception. - $this->httpRequest($entity->toUrl(), 'PATCH', $serialized, 'none'); - $this->assertResponse(Response::HTTP_UNSUPPORTED_MEDIA_TYPE); - $this->assertRaw('No route found that matches "Content-Type: none"'); - - // Update the entity over the REST API. - $response = $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(200); - - // Make sure that the response includes an entity in the body, check the - // updated field as an example. - $request = Json::decode($serialized); - $response = Json::decode($response); - $this->assertEqual($request['field_test_text'][0]['value'], $response['field_test_text'][0]['value']); - unset($request['_links']); - unset($response['_links']); - unset($response['id']); - unset($response['uuid']); - unset($response['name']); - $this->assertEqual($request, $response); - - // Re-load updated entity from the database. - $storage = $this->container->get('entity_type.manager') - ->getStorage($entity_type); - $storage->resetCache([$entity->id()]); - $entity = $storage->load($entity->id()); - $this->assertEqual($entity->field_test_text->value, $patch_entity->field_test_text->value, 'Field was successfully updated.'); - - // Make sure that the field does not get deleted if it is not present in the - // PATCH request. - $normalized = $serializer->normalize($patch_entity, $this->defaultFormat, $context); - unset($normalized['field_test_text']); - $serialized = $serializer->encode($normalized, $this->defaultFormat); - $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(200); - - $storage->resetCache([$entity->id()]); - $entity = $storage->load($entity->id()); - $this->assertNotNull($entity->field_test_text->value . 'Test field has not been deleted.'); - - // Try to empty a field. - $normalized['field_test_text'] = array(); - $serialized = $serializer->encode($normalized, $this->defaultFormat); - - // Update the entity over the REST API. - $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(200); - - // Re-load updated entity from the database. - $storage->resetCache([$entity->id()]); - $entity = $storage->load($entity->id(), TRUE); - $this->assertNull($entity->field_test_text->value, 'Test field has been cleared.'); - - // Enable access protection for the text field. - // @see entity_test_entity_field_access() - $entity->field_test_text->value = 'no edit access value'; - $entity->field_test_text->format = 'plain_text'; - $entity->save(); - - // Try to empty a field that is access protected. - $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(403); - - // Re-load the entity from the database. - $storage->resetCache([$entity->id()]); - $entity = $storage->load($entity->id()); - $this->assertEqual($entity->field_test_text->value, 'no edit access value', 'Text field was not deleted.'); - - // Try to update an access protected field. - $normalized = $serializer->normalize($patch_entity, $this->defaultFormat, $context); - $normalized['field_test_text'][0]['value'] = 'no access value'; - $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(403); - - // Re-load the entity from the database. - $storage->resetCache([$entity->id()]); - $entity = $storage->load($entity->id()); - $this->assertEqual($entity->field_test_text->value, 'no edit access value', 'Text field was not updated.'); - - // Try to update the field with a text format this user has no access to. - // First change the original field value so we're allowed to edit it again. - $entity->field_test_text->value = 'test'; - $entity->save(); - $patch_entity->set('field_test_text', array( - 'value' => 'test', - 'format' => 'full_html', - )); - $serialized = $serializer->serialize($patch_entity, $this->defaultFormat, $context); - $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(422); - - // Re-load the entity from the database. - $storage->resetCache([$entity->id()]); - $entity = $storage->load($entity->id()); - $this->assertEqual($entity->field_test_text->format, 'plain_text', 'Text format was not updated.'); - - // Restore the valid test value. - $entity->field_test_text->value = $this->randomString(); - $entity->save(); - - // Try to send no data at all, which does not make sense on PATCH requests. - $this->httpRequest($entity->urlInfo(), 'PATCH', NULL, $this->defaultMimeType); - $this->assertResponse(400); - - // Try to update a non-existing entity with ID 9999. - $this->httpRequest($entity_type . '/9999', 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(404); - $storage->resetCache([9999]); - $loaded_entity = $storage->load(9999); - $this->assertFalse($loaded_entity, 'Entity 9999 was not created.'); - - // Try to send invalid data to trigger the entity validation constraints. - // Send a UUID that is too long. - $entity->set('uuid', $this->randomMachineName(129)); - $invalid_serialized = $serializer->serialize($entity, $this->defaultFormat, $context); - $response = $this->httpRequest($entity->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $invalid_serialized, $this->defaultMimeType); - $this->assertResponse(422); - $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n"); - - // Try to update an entity without proper permissions. - $this->drupalLogout(); - $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(403); - - // Try to update a resource which is not REST API enabled. - $this->enableService(FALSE); - $this->drupalLogin($account); - $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(405); - } - - /** - * Tests several valid and invalid update requests for the 'user' entity type. - */ - public function testUpdateUser() { - $serializer = $this->container->get('serializer'); - $entity_type = 'user'; - // Enables the REST service for 'user' entity type. - $this->enableService('entity:' . $entity_type, 'PATCH'); - $permissions = $this->entityPermissions($entity_type, 'update'); - $account = $this->drupalCreateUser($permissions); - $account->set('mail', 'old-email@example.com'); - $this->drupalLogin($account); - - // Create an entity and save it to the database. - $account->save(); - $account->set('changed', NULL); - - // Try and set a new email without providing the password. - $account->set('mail', 'new-email@example.com'); - $context = ['account' => $account]; - $normalized = $serializer->normalize($account, $this->defaultFormat, $context); - $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(422); - $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"); - - // Try and send the new email with a password. - $normalized['pass'][0]['existing'] = 'wrong'; - $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(422); - $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"); - - // Try again with the password. - $normalized['pass'][0]['existing'] = $account->pass_raw; - $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(200); - - // Try to change the password without providing the current password. - $new_password = $this->randomString(); - $normalized = $serializer->normalize($account, $this->defaultFormat, $context); - $normalized['pass'][0]['value'] = $new_password; - $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(422); - $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n"); - - // Try again with the password. - $normalized['pass'][0]['existing'] = $account->pass_raw; - $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); - $this->assertResponse(200); - - // Verify that we can log in with the new password. - $account->pass_raw = $new_password; - $this->drupalLogin($account); - } - - /** - * Test patching a comment using both HAL+JSON and JSON. - */ - public function testUpdateComment() { - $entity_type = 'comment'; - // Enables the REST service for 'comment' entity type. - $this->enableService('entity:' . $entity_type, 'PATCH', ['hal_json', 'json']); - $permissions = $this->entityPermissions($entity_type, 'update'); - $account = $this->drupalCreateUser($permissions); - $account->set('mail', 'old-email@example.com'); - $this->drupalLogin($account); - - // Create & save an entity to comment on, plus a comment. - $entity_test = EntityTest::create(); - $entity_test->save(); - $entity_values = $this->entityValues($entity_type); - $entity_values['entity_id'] = $entity_test->id(); - $entity_values['uid'] = $account->id(); - $comment = Comment::create($entity_values); - $comment->save(); - - $this->pass('Test case 1: PATCH comment using HAL+JSON.'); - $comment->setSubject('Initial subject')->save(); - $read_only_fields = [ - 'name', - 'created', - 'changed', - 'status', - 'thread', - 'entity_type', - 'field_name', - 'entity_id', - 'uid', - ]; - $this->assertNotEqual('Updated subject', $comment->getSubject()); - $comment->setSubject('Updated subject'); - $this->patchEntity($comment, $read_only_fields, $account, 'hal_json', 'application/hal+json'); - $comment = Comment::load($comment->id()); - $this->assertEqual('Updated subject', $comment->getSubject()); - - $this->pass('Test case 1: PATCH comment using JSON.'); - $comment->setSubject('Initial subject')->save(); - $read_only_fields = [ - 'pid', // Extra compared to HAL+JSON. - 'entity_id', - 'uid', - 'name', - 'homepage', // Extra compared to HAL+JSON. - 'created', - 'changed', - 'status', - 'thread', - 'entity_type', - 'field_name', - ]; - $this->assertNotEqual('Updated subject', $comment->getSubject()); - $comment->setSubject('Updated subject'); - $this->patchEntity($comment, $read_only_fields, $account, 'json', 'application/json'); - $comment = Comment::load($comment->id()); - $this->assertEqual('Updated subject', $comment->getSubject()); - } - - /** - * Patches an existing entity using the passed in (modified) entity. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The updated entity to send. - * @param string[] $read_only_fields - * Names of the fields that are read-only, in validation order. - * @param \Drupal\Core\Session\AccountInterface $account - * The account to use for serialization. - * @param string $format - * A serialization format. - * @param string $mime_type - * The MIME type corresponding to the specified serialization format. - */ - protected function patchEntity(EntityInterface $entity, array $read_only_fields, AccountInterface $account, $format, $mime_type) { - $serializer = $this->container->get('serializer'); - - $url = $entity->toUrl()->setRouteParameter('_format', $this->defaultFormat); - $context = ['account' => $account]; - // Certain fields are always read-only, others this user simply is not - // allowed to modify. For all of them, ensure they are not serialized, else - // we'll get a 403 plus an error message. - for ($i = 0; $i < count($read_only_fields); $i++) { - $field = $read_only_fields[$i]; - - $normalized = $serializer->normalize($entity, $format, $context); - if ($format !== 'hal_json') { - // The default normalizer always keeps fields, even if they are unset - // here because they should be omitted during a PATCH request. Therefore - // manually strip them - // @see \Drupal\Core\Entity\ContentEntityBase::__unset() - // @see \Drupal\serialization\Normalizer\EntityNormalizer::normalize() - // @see \Drupal\hal\Normalizer\ContentEntityNormalizer::normalize() - $read_only_fields_so_far = array_slice($read_only_fields, 0, $i); - $normalized = array_diff_key($normalized, array_flip($read_only_fields_so_far)); - } - $serialized = $serializer->serialize($normalized, $format, $context); - - $this->httpRequest($url, 'PATCH', $serialized, $mime_type); - $this->assertResponse(403); - $this->assertResponseBody('{"message":"Access denied on updating field \\u0027' . $field . '\\u0027."}'); - - if ($format === 'hal_json') { - // We've just tried with this read-only field, now unset it. - $entity->set($field, NULL); - } - } - - // Finally, with all read-only fields unset, the request should succeed. - $normalized = $serializer->normalize($entity, $format, $context); - if ($format !== 'hal_json') { - $normalized = array_diff_key($normalized, array_combine($read_only_fields, $read_only_fields)); - } - $serialized = $serializer->serialize($normalized, $format, $context); - - // Try first without CSRF token which should fail. - $this->httpRequest($url, 'PATCH', $serialized, $mime_type, FALSE); - $this->assertResponse(403); - $this->assertRaw('X-CSRF-Token request header is missing'); - // Then try with an invalid CSRF token. - $this->httpRequest($url, 'PATCH', $serialized, $mime_type, 'invalid-csrf-token'); - $this->assertResponse(403); - $this->assertRaw('X-CSRF-Token request header is invalid'); - // Then try with CSRF token. - $this->httpRequest($url, 'PATCH', $serialized, $mime_type); - $this->assertResponse(200); - } - -} diff --git a/core/modules/rest/src/Tests/Views/ExcludedFieldTokenTest.php b/core/modules/rest/src/Tests/Views/ExcludedFieldTokenTest.php new file mode 100644 index 000000000..2a910fe28 --- /dev/null +++ b/core/modules/rest/src/Tests/Views/ExcludedFieldTokenTest.php @@ -0,0 +1,88 @@ + 'article', + 'title' => 'Article test ' . $i, + ])->save(); + } + + $this->enableViewsTestModule(); + + $this->view = Views::getView('test_excluded_field_token_display'); + $this->view->setDisplay('rest_export_1'); + } + + /** + * Tests the display of an excluded title field when used as a token. + */ + public function testExcludedTitleTokenDisplay() { + $actual_json = $this->drupalGetWithFormat($this->view->getPath(), 'json'); + $this->assertResponse(200); + + $expected = [ + ['nothing' => 'Article test 10'], + ['nothing' => 'Article test 9'], + ['nothing' => 'Article test 8'], + ['nothing' => 'Article test 7'], + ['nothing' => 'Article test 6'], + ['nothing' => 'Article test 5'], + ['nothing' => 'Article test 4'], + ['nothing' => 'Article test 3'], + ['nothing' => 'Article test 2'], + ['nothing' => 'Article test 1'], + ]; + $this->assertIdentical($actual_json, json_encode($expected)); + } + + +} diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.info.yml b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.info.yml new file mode 100644 index 000000000..cf9efee46 --- /dev/null +++ b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.info.yml @@ -0,0 +1,7 @@ +name: 'Configuration test REST' +type: module +package: Testing +version: VERSION +core: 8.x +dependencies: + - config_test diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module new file mode 100644 index 000000000..fcd9979a1 --- /dev/null +++ b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module @@ -0,0 +1,30 @@ +hasPermission('view config_test'))->cachePerPermissions(); +} diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.permissions.yml b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.permissions.yml new file mode 100644 index 000000000..b8fd229fd --- /dev/null +++ b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.permissions.yml @@ -0,0 +1,2 @@ +view config_test: + title: 'View ConfigTest entities' diff --git a/core/modules/rest/tests/modules/rest_test/rest_test.module b/core/modules/rest/tests/modules/rest_test/rest_test.module index 272603d15..01511ea1e 100644 --- a/core/modules/rest/tests/modules/rest_test/rest_test.module +++ b/core/modules/rest/tests/modules/rest_test/rest_test.module @@ -5,6 +5,11 @@ * Contains hook implementations for testing REST module. */ +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Access\AccessResult; + /** * Implements hook_rest_type_uri_alter(). */ @@ -22,3 +27,24 @@ function rest_test_rest_relation_uri_alter(&$uri, $context = array()) { $uri = 'rest_test_relation'; } } + +/** + * Implements hook_entity_field_access(). + * + * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::setUp() + * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPost() + */ +function rest_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { + if ($field_definition->getName() === 'field_rest_test') { + switch ($operation) { + case 'view': + // Never ever allow this field to be viewed: this lets EntityResourceTestBase::testGet() test in a "vanilla" way. + return AccessResult::forbidden(); + case 'edit': + return AccessResult::forbidden(); + } + } + + // No opinion. + return AccessResult::neutral(); +} diff --git a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_excluded_field_token_display.yml b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_excluded_field_token_display.yml new file mode 100644 index 000000000..e95f95337 --- /dev/null +++ b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_excluded_field_token_display.yml @@ -0,0 +1,277 @@ +langcode: en +status: true +dependencies: + config: + - node.type.article + module: + - node + - rest + - user +id: test_excluded_field_token_display +label: 'Test Excluded Field Token Display' +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: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + 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: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + 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 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 + tags: + previous: ‹‹ + next: ›› + style: + type: serializer + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: true + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: false + ellipsis: false + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: false + 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 + entity_type: node + entity_field: title + plugin_id: field + nothing: + id: nothing + table: views + field: nothing + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: true + text: '{{ title }}' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: false + plugin_id: custom + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + article: article + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + nid: + id: nid + table: node_field_data + field: nid + order: DESC + entity_type: node + entity_field: nid + plugin_id: standard + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - request_format + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + rest_export_1: + display_plugin: rest_export + id: rest_export_1 + display_title: 'REST export' + position: 1 + display_options: + display_extenders: { } + path: rest/test/excluded-field-token + pager: + type: some + options: + items_per_page: 10 + offset: 0 + style: + type: serializer + options: + formats: + json: json + row: + type: data_field + options: + field_options: + title: + alias: '' + raw_output: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - request_format + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php b/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php new file mode 100644 index 000000000..b05ddf2a9 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php @@ -0,0 +1,36 @@ + [ + 'Authorization' => 'Basic ' . base64_encode($this->account->name->value . ':' . $this->account->passRaw), + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) { + $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response); + } + + /** + * {@inheritdoc} + */ + protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) {} + +} diff --git a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php new file mode 100644 index 000000000..18dc296b2 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php @@ -0,0 +1,129 @@ +setRouteParameter('_format', 'json'); + + $request_body = [ + 'name' => $this->account->name->value, + 'pass' => $this->account->passRaw, + ]; + + $request_options[RequestOptions::BODY] = $this->serializer->encode($request_body, 'json'); + $request_options[RequestOptions::HEADERS]['Accept'] = 'application/json'; + $response = $this->request('POST', $user_login_url, $request_options); + + // Parse and store the session cookie. + $this->sessionCookie = explode(';', $response->getHeader('Set-Cookie')[0], 2)[0]; + + // Parse and store the CSRF token and logout token. + $data = $this->serializer->decode((string)$response->getBody(), static::$format); + $this->csrfToken = $data['csrf_token']; + $this->logoutToken = $data['logout_token']; + } + + /** + * {@inheritdoc} + */ + protected function getAuthenticationRequestOptions($method) { + $request_options[RequestOptions::HEADERS]['Cookie'] = $this->sessionCookie; + // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html + if (!in_array($method, ['HEAD', 'GET', 'OPTIONS', 'TRACE'])) { + $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = $this->csrfToken; + } + return $request_options; + } + + /** + * {@inheritdoc} + */ + protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) { + $this->assertResourceErrorResponse(403, '', $response); + } + + /** + * {@inheritdoc} + */ + protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) { + // X-CSRF-Token request header is unnecessary for safe and side effect-free + // HTTP methods. No need for additional assertions. + // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html + if (in_array($method, ['HEAD', 'GET', 'OPTIONS', 'TRACE'])) { + return; + } + + + unset($request_options[RequestOptions::HEADERS]['X-CSRF-Token']); + + + // DX: 403 when missing X-CSRF-Token request header. + $response = $this->request($method, $url, $request_options); + $this->assertResourceErrorResponse(403, 'X-CSRF-Token request header is missing', $response); + + + $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = 'this-is-not-the-token-you-are-looking-for'; + + + // DX: 403 when invalid X-CSRF-Token request header. + $response = $this->request($method, $url, $request_options); + $this->assertResourceErrorResponse(403, 'X-CSRF-Token request header is invalid', $response); + + + $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = $this->csrfToken; + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonAnonTest.php new file mode 100644 index 000000000..9c764bda7 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonAnonTest.php @@ -0,0 +1,29 @@ +entity->setVisibilityConfig('user_role', [])->save(); + break; + case 'POST': + $this->grantPermissionsToTestedRole(['administer blocks']); + break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['administer blocks']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $block = Block::create([ + 'plugin' => 'llama_block', + 'region' => 'header', + 'id' => 'llama', + 'theme' => 'classy', + ]); + // All blocks can be viewed by the anonymous user by default. An interesting + // side effect of this is that any anonymous user is also able to read the + // corresponding block config entity via REST, even if an authentication + // provider is configured for the block config entity REST resource! In + // other words: Block entities do not distinguish between 'view' as in + // "render on a page" and 'view' as in "read the configuration". + // This prevents that. + // @todo Fix this in https://www.drupal.org/node/2820315. + $block->setVisibilityConfig('user_role', [ + 'id' => 'user_role', + 'roles' => ['non-existing-role' => 'non-existing-role'], + 'negate' => FALSE, + 'context_mapping' => [ + 'user' => '@user.current_user_context:current_user', + ], + ]); + $block->save(); + + return $block; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $normalization = [ + 'uuid' => $this->entity->uuid(), + 'id' => 'llama', + 'weight' => NULL, + 'langcode' => 'en', + 'status' => TRUE, + 'dependencies' => [ + 'theme' => [ + 'classy', + ], + ], + 'theme' => 'classy', + 'region' => 'header', + 'provider' => NULL, + 'plugin' => 'llama_block', + 'settings' => [ + 'id' => 'broken', + 'label' => '', + 'provider' => 'core', + 'label_display' => 'visible', + ], + 'visibility' => [], + ]; + + return $normalization; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + // @todo Update in https://www.drupal.org/node/2300677. + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + // @see ::createEntity() + return []; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + // Because the 'user.permissions' cache context is missing, the cache tag + // for the anonymous user role is never added automatically. + return array_filter(parent::getExpectedCacheTags(), function ($tag) { + return $tag !== 'config:user.role.anonymous'; + }); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentJsonAnonTest.php new file mode 100644 index 000000000..6ce580d03 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentJsonAnonTest.php @@ -0,0 +1,50 @@ +grantPermissionsToTestedRole(['access comments', 'view test entity']); + break; + case 'POST': + $this->grantPermissionsToTestedRole(['post comments']); + break; + case 'PATCH': + // Anononymous users are not ever allowed to edit their own comments. To + // be able to test PATCHing comments as the anonymous user, the more + // permissive 'administer comments' permission must be granted. + // @see \Drupal\comment\CommentAccessControlHandler::checkAccess + if (static::$auth) { + $this->grantPermissionsToTestedRole(['edit own comments']); + } + else { + $this->grantPermissionsToTestedRole(['administer comments']); + } + break; + case 'DELETE': + $this->grantPermissionsToTestedRole(['administer comments']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + // Create a "bar" bundle for the "entity_test" entity type and create. + $bundle = 'bar'; + entity_test_create_bundle($bundle, NULL, 'entity_test'); + + // Create a comment field on this bundle. + $this->addDefaultCommentField('entity_test', 'bar', 'comment'); + + // Create a "Camelids" test entity that the comment will be assigned to. + $commented_entity = EntityTest::create(array( + 'name' => 'Camelids', + 'type' => 'bar', + )); + $commented_entity->save(); + + // Create a "Llama" comment. + $comment = Comment::create([ + 'comment_body' => [ + 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', + 'format' => 'plain_text', + ], + 'entity_id' => $commented_entity->id(), + 'entity_type' => 'entity_test', + 'field_name' => 'comment', + ]); + $comment->setSubject('Llama') + ->setOwnerId(static::$auth ? $this->account->id() : 0) + ->setPublished(TRUE) + ->setCreatedTime(123456789) + ->setChangedTime(123456789); + $comment->save(); + + return $comment; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $author = User::load($this->entity->getOwnerId()); + return [ + 'cid' => [ + ['value' => 1], + ], + 'uuid' => [ + ['value' => $this->entity->uuid()], + ], + 'langcode' => [ + [ + 'value' => 'en', + ], + ], + 'comment_type' => [ + [ + 'target_id' => 'comment', + 'target_type' => 'comment_type', + 'target_uuid' => CommentType::load('comment')->uuid(), + ], + ], + 'subject' => [ + [ + 'value' => 'Llama', + ], + ], + 'status' => [ + [ + 'value' => 1, + ], + ], + 'created' => [ + [ + 'value' => '123456789', + ], + ], + 'changed' => [ + [ + 'value' => '123456789', + ], + ], + 'default_langcode' => [ + [ + 'value' => TRUE, + ], + ], + 'uid' => [ + [ + 'target_id' => $author->id(), + 'target_type' => 'user', + 'target_uuid' => $author->uuid(), + 'url' => base_path() . 'user/' . $author->id(), + ], + ], + 'pid' => [], + 'entity_type' => [ + [ + 'value' => 'entity_test', + ], + ], + 'entity_id' => [ + [ + 'target_id' => '1', + 'target_type' => 'entity_test', + 'target_uuid' => EntityTest::load(1)->uuid(), + 'url' => base_path() . 'entity_test/1', + ], + ], + 'field_name' => [ + [ + 'value' => 'comment', + ], + ], + 'name' => [], + 'homepage' => [], + 'thread' => [ + [ + 'value' => '01/', + ], + ], + 'comment_body' => [ + [ + 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', + 'format' => 'plain_text', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return [ + 'comment_type' => [ + [ + 'target_id' => 'comment', + ], + ], + 'entity_type' => [ + [ + 'value' => 'entity_test', + ], + ], + 'entity_id' => [ + [ + 'target_id' => EntityTest::load(1)->id(), + ], + ], + 'field_name' => [ + [ + 'value' => 'comment', + ], + ], + 'subject' => [ + [ + 'value' => 'Dramallama', + ], + ], + 'comment_body' => [ + [ + 'value' => 'Llamas are awesome.', + 'format' => 'plain_text', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPatchEntity() { + return array_diff_key($this->getNormalizedPostEntity(), ['entity_type' => TRUE, 'entity_id' => TRUE, 'field_name' => TRUE]); + } + + /** + * Tests POSTing a comment without critical base fields. + * + * testPost() is testing with the most minimal normalization possible: the one + * returned by ::getNormalizedPostEntity(). + * + * But Comment entities have some very special edge cases: + * - base fields that are not marked as required in + * \Drupal\comment\Entity\Comment::baseFieldDefinitions() yet in fact are + * required. + * - base fields that are marked as required, but yet can still result in + * validation errors other than "missing required field". + */ + public function testPostDxWithoutCriticalBaseFields() { + $this->initAuthentication(); + $this->provisionEntityResource(); + $this->setUpAuthorization('POST'); + + $url = $this->getPostUrl()->setOption('query', ['_format' => static::$format]); + $request_options = []; + $request_options[RequestOptions::HEADERS]['Accept'] = static::$mimeType; + $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; + $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('POST')); + + // DX: 422 when missing 'entity_type' field. + $request_options[RequestOptions::BODY] = $this->serializer->encode(array_diff_key($this->getNormalizedPostEntity(), ['entity_type' => TRUE]), static::$format); + $response = $this->request('POST', $url, $request_options); + // @todo Uncomment, remove next line in https://www.drupal.org/node/2820364. + $this->assertResourceErrorResponse(500, 'A fatal error occurred: Internal Server Error', $response); + //$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nentity_type: This value should not be null.\n", $response); + + // DX: 422 when missing 'entity_id' field. + $request_options[RequestOptions::BODY] = $this->serializer->encode(array_diff_key($this->getNormalizedPostEntity(), ['entity_id' => TRUE]), static::$format); + // @todo Remove the try/catch in favor of the two commented lines in + // https://www.drupal.org/node/2820364. + try { + $response = $this->request('POST', $url, $request_options); + // This happens on DrupalCI. + //$this->assertSame(500, $response->getStatusCode()); + } + catch (\Exception $e) { + // This happens on Wim's local machine. + //$this->assertSame("Error: Call to a member function get() on null\nDrupal\\comment\\Plugin\\Validation\\Constraint\\CommentNameConstraintValidator->getAnonymousContactDetailsSetting()() (Line: 96)\n", $e->getMessage()); + } + //$response = $this->request('POST', $url, $request_options); + //$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nentity_type: This value should not be null.\n", $response); + + // DX: 422 when missing 'entity_type' field. + $request_options[RequestOptions::BODY] = $this->serializer->encode(array_diff_key($this->getNormalizedPostEntity(), ['field_name' => TRUE]), static::$format); + $response = $this->request('POST', $url, $request_options); + // @todo Uncomment, remove next line in https://www.drupal.org/node/2820364. + $this->assertResourceErrorResponse(500, 'A fatal error occurred: Field is unknown.', $response); + //$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nfield_name: This value should not be null.\n", $response); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestJsonAnonTest.php new file mode 100644 index 000000000..db79e6c09 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestJsonAnonTest.php @@ -0,0 +1,29 @@ +grantPermissionsToTestedRole(['view config_test']); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $config_test = ConfigTest::create([ + 'id' => 'llama', + 'label' => 'Llama', + ]); + $config_test->save(); + + return $config_test; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $normalization = [ + 'uuid' => $this->entity->uuid(), + 'id' => 'llama', + 'weight' => 0, + 'langcode' => 'en', + 'status' => TRUE, + 'dependencies' => [], + 'label' => 'Llama', + 'style' => NULL, + 'size' => NULL, + 'size_value' => NULL, + 'protected_property' => NULL, + ]; + + return $normalization; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + // @todo Update in https://www.drupal.org/node/2300677. + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php new file mode 100644 index 000000000..c946967d3 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -0,0 +1,1090 @@ +provisionResource('entity.' . static::$entityTypeId, [static::$format], $auth); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->serializer = $this->container->get('serializer'); + $this->entityStorage = $this->container->get('entity_type.manager') + ->getStorage(static::$entityTypeId); + + // Set up a HTTP client that accepts relative URLs. + $this->httpClient = $this->container->get('http_client_factory') + ->fromOptions(['base_uri' => $this->baseUrl]); + + // Create an entity. + $this->entity = $this->createEntity(); + + if ($this->entity instanceof FieldableEntityInterface) { + // Add access-protected field. + FieldStorageConfig::create([ + 'entity_type' => static::$entityTypeId, + 'field_name' => 'field_rest_test', + 'type' => 'text', + ]) + ->setCardinality(1) + ->save(); + FieldConfig::create([ + 'entity_type' => static::$entityTypeId, + 'field_name' => 'field_rest_test', + 'bundle' => $this->entity->bundle(), + ]) + ->setLabel('Test field') + ->setTranslatable(FALSE) + ->save(); + + // Reload entity so that it has the new field. + $this->entity = $this->entityStorage->loadUnchanged($this->entity->id()); + + // Set a default value on the field. + $this->entity->set('field_rest_test', ['value' => 'All the faith he had had had had no effect on the outcome of his life.']); + // @todo Remove in this if-test in https://www.drupal.org/node/2808335. + if ($this->entity instanceof EntityChangedInterface) { + $changed = $this->entity->getChangedTime(); + $this->entity->setChangedTime(42); + $this->entity->save(); + $this->entity->setChangedTime($changed); + } + $this->entity->save(); + } + + // @todo Remove this in https://www.drupal.org/node/2815845. + drupal_flush_all_caches(); + } + + /** + * Creates the entity to be tested. + * + * @return \Drupal\Core\Entity\EntityInterface + * The entity to be tested. + */ + abstract protected function createEntity(); + + /** + * Returns the expected normalization of the entity. + * + * @see ::createEntity() + * + * @return array + */ + abstract protected function getExpectedNormalizedEntity(); + + /** + * Returns the normalized POST entity. + * + * @see ::testPost + * + * @return array + */ + abstract protected function getNormalizedPostEntity(); + + /** + * Returns the normalized PATCH entity. + * + * By default, reuses ::getNormalizedPostEntity(), which works fine for most + * entity types. A counterexample: the 'comment' entity type. + * + * @see ::testPatch + * + * @return array + */ + protected function getNormalizedPatchEntity() { + return $this->getNormalizedPostEntity(); + } + + /** + * The expected cache tags for the GET/HEAD response of the test entity. + * + * @see ::testGet + * + * @return string[] + */ + protected function getExpectedCacheTags() { + $expected_cache_tags = [ + 'config:rest.resource.entity.' . static::$entityTypeId, + ]; + if (!static::$auth) { + $expected_cache_tags[] = 'config:user.role.anonymous'; + } + return Cache::mergeTags($expected_cache_tags, $this->entity->getCacheTags()); + } + + /** + * The expected cache contexts for the GET/HEAD response of the test entity. + * + * @see ::testGet + * + * @return string[] + */ + protected function getExpectedCacheContexts() { + return [ + 'user.permissions', + ]; + } + + /** + * Test a GET request for an entity, plus edge cases to ensure good DX. + */ + public function testGet() { + $this->initAuthentication(); + $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); + + // The URL and Guzzle request options that will be used in this test. The + // request options will be modified/expanded throughout this test: + // - to first test all mistakes a developer might make, and assert that the + // error responses provide a good DX + // - to eventually result in a well-formed request that succeeds. + $url = $this->getUrl(); + $request_options = []; + + + // DX: 404 when resource not provisioned, 403 if canonical route. HTML + // response because missing ?_format query string. + $response = $this->request('GET', $url, $request_options); + $this->assertSame($has_canonical_url ? 403 : 404, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 404 when resource not provisioned, 403 if canonical route. Non-HTML + // response because ?_format query string is present. + $response = $this->request('GET', $url, $request_options); + if ($has_canonical_url) { + $this->assertResourceErrorResponse(403, '', $response); + } + else { + $this->assertResourceErrorResponse(404, 'No route found for "GET ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response); + } + + + $this->provisionEntityResource(); + // Simulate the developer again forgetting the ?_format query string. + $url->setOption('query', []); + + + + // DX: 406 when ?_format is missing, except when requesting a canonical HTML + // route. + $response = $this->request('GET', $url, $request_options); + if ($has_canonical_url && (!static::$auth || static::$auth === 'cookie')) { + $this->assertSame(403, $response->getStatusCode()); + } + else { + $this->assert406Response($response); + } + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: forgetting authentication: authentication provider-specific error + // response. + if (static::$auth) { + $response = $this->request('GET', $url, $request_options); + $this->assertResponseWhenMissingAuthentication($response); + } + + $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('GET')); + + + // DX: 403 when unauthorized. + $response = $this->request('GET', $url, $request_options); + // @todo Update the message in https://www.drupal.org/node/2808233. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->setUpAuthorization('GET'); + + + // 200 for well-formed HEAD request. + $response = $this->request('HEAD', $url, $request_options); + $this->assertResourceResponse(200, '', $response); + if (!$this->account) { + $this->assertSame(['MISS'], $response->getHeader('X-Drupal-Cache')); + } + else { + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + } + $head_headers = $response->getHeaders(); + + // 200 for well-formed GET request. Page Cache hit because of HEAD request. + $response = $this->request('GET', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + if (!static::$auth) { + $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Cache')); + } + else { + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + } + $cache_tags_header_value = $response->getHeader('X-Drupal-Cache-Tags')[0]; + $this->assertEquals($this->getExpectedCacheTags(), empty($cache_tags_header_value) ? [] : explode(' ', $cache_tags_header_value)); + $cache_contexts_header_value = $response->getHeader('X-Drupal-Cache-Contexts')[0]; + $this->assertEquals($this->getExpectedCacheContexts(), empty($cache_contexts_header_value) ? [] : explode(' ', $cache_contexts_header_value)); + // Comparing the exact serialization is pointless, because the order of + // fields does not matter (at least not yet). That's why we only compare the + // normalized entity with the decoded response: it's comparing PHP arrays + // instead of strings. + $this->assertEquals($this->getExpectedNormalizedEntity(), $this->serializer->decode((string) $response->getBody(), static::$format)); + // Not only assert the normalization, also assert deserialization of the + // response results in the expected object. + $unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format); + $this->assertSame($unserialized->uuid(), $this->entity->uuid()); + $get_headers = $response->getHeaders(); + + // Verify that the GET and HEAD responses are the same. The only difference + // is that there's no body. + $ignored_headers = ['Date', 'Content-Length', 'X-Drupal-Cache', 'X-Drupal-Dynamic-Cache']; + foreach ($ignored_headers as $ignored_header) { + unset($head_headers[$ignored_header]); + unset($get_headers[$ignored_header]); + } + $this->assertSame($get_headers, $head_headers); + + + $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE); + // @todo Remove this in https://www.drupal.org/node/2815845. + drupal_flush_all_caches(); + + + // DX: 403 when unauthorized. + $response = $this->request('GET', $url, $request_options); + // @todo Update the message in https://www.drupal.org/node/2808233. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->grantPermissionsToTestedRole(['restful get entity:' . static::$entityTypeId]); + + + // 200 for well-formed request. + $response = $this->request('GET', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + + + $url->setOption('query', ['_format' => 'non_existing_format']); + + + // DX: 406 when requesting unsupported format. + $response = $this->request('GET', $url, $request_options); + $this->assert406Response($response); + $this->assertNotSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); + + + $request_options[RequestOptions::HEADERS]['Accept'] = static::$mimeType; + + + // DX: 406 when requesting unsupported format but specifying Accept header. + // @todo Update in https://www.drupal.org/node/2825347. + $response = $this->request('GET', $url, $request_options); + $this->assert406Response($response); + $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); + + + $url = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . static::$format); + $url->setRouteParameter(static::$entityTypeId, 987654321); + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 404 when GETting non-existing entity. + $response = $this->request('GET', $url, $request_options); + $path = str_replace('987654321', '{' . static::$entityTypeId . '}', $url->setAbsolute()->setOptions(['base_url' => '', 'query' => []])->toString()); + $message = 'The "' . static::$entityTypeId . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityTypeId . '.GET.' . static::$format . '")'; + $this->assertResourceErrorResponse(404, $message, $response); + } + + /** + * Tests a POST request for an entity, plus edge cases to ensure good DX. + */ + public function testPost() { + // @todo Remove this in https://www.drupal.org/node/2300677. + if ($this->entity instanceof ConfigEntityInterface) { + $this->assertTrue(TRUE, 'POSTing config entities is not yet supported.'); + return; + } + + $this->initAuthentication(); + $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); + + // Try with all of the following request bodies. + $unparseable_request_body = '!{>}<'; + $parseable_valid_request_body = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); + $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); + $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity()), static::$format); + // @todo Change to ['uuid' => UUID] in https://www.drupal.org/node/2820743. + $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [['value' => $this->randomMachineName(129)]]], static::$format); + $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPostEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format); + + // The URL and Guzzle request options that will be used in this test. The + // request options will be modified/expanded throughout this test: + // - to first test all mistakes a developer might make, and assert that the + // error responses provide a good DX + // - to eventually result in a well-formed request that succeeds. + $url = $this->getPostUrl(); + $request_options = []; + + + // DX: 404 when resource not provisioned, but HTML if canonical route. + $response = $this->request('POST', $url, $request_options); + if ($has_canonical_url) { + $this->assertSame(404, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + } + else { + $this->assertResourceErrorResponse(404, 'No route found for "GET ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response); + } + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 404 when resource not provisioned. + $response = $this->request('POST', $url, $request_options); + $this->assertResourceErrorResponse(404, 'No route found for "POST ' . str_replace($this->baseUrl, '', $this->getPostUrl()->setAbsolute()->toString()) . '"', $response); + + + $this->provisionEntityResource(); + // Simulate the developer again forgetting the ?_format query string. + $url->setOption('query', []); + + + // DX: 415 when no Content-Type request header, but HTML if canonical route. + $response = $this->request('POST', $url, $request_options); + if ($has_canonical_url) { + $this->assertSame(415, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertContains(htmlspecialchars('No "Content-Type" request header specified'), (string) $response->getBody()); + } + else { + $this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response); + } + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 415 when no Content-Type request header. + $response = $this->request('POST', $url, $request_options); + $this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response); + + + $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; + + + // DX: 400 when no request body. + $response = $this->request('POST', $url, $request_options); + $this->assertResourceErrorResponse(400, 'No entity content received.', $response); + + + $request_options[RequestOptions::BODY] = $unparseable_request_body; + + + // DX: 400 when unparseable request body. + $response = $this->request('POST', $url, $request_options); + // @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813853. + // $this->assertResourceErrorResponse(400, 'Syntax error', $response); + $this->assertSame(400, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['error' => 'Syntax error'], static::$format), (string) $response->getBody()); + + + + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body; + + + if (static::$auth) { + // DX: forgetting authentication: authentication provider-specific error + // response. + $response = $this->request('POST', $url, $request_options); + $this->assertResponseWhenMissingAuthentication($response); + } + + + $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST')); + + + // DX: 403 when unauthorized. + $response = $this->request('POST', $url, $request_options); + // @todo Update the message in https://www.drupal.org/node/2808233. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->setUpAuthorization('POST'); + + + // DX: 422 when invalid entity: multiple values sent for single-value field. + $response = $this->request('POST', $url, $request_options); + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; + $label_field_capitalized = ucfirst($label_field); + // @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813755. + // $this->assertErrorResponse(422, "Unprocessable Entity: validation failed.\ntitle: Title: this field cannot hold more than 1 values.\n", $response); + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n"], static::$format), (string) $response->getBody()); + + + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; + + + // DX: 422 when invalid entity: UUID field too long. + $response = $this->request('POST', $url, $request_options); + // @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813755. + // $this->assertErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response); + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n"], static::$format), (string) $response->getBody()); + + + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3; + + + // DX: 403 when entity contains field without 'edit' access. + $response = $this->request('POST', $url, $request_options); + // @todo Add trailing period in https://www.drupal.org/node/2821013. + $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'", $response); + + + $request_options[RequestOptions::BODY] = $parseable_valid_request_body; + + + // Before sending a well-formed request, allow the normalization and + // authentication provider edge cases to also be tested. + $this->assertNormalizationEdgeCases('POST', $url, $request_options); + $this->assertAuthenticationEdgeCases('POST', $url, $request_options); + + + $request_options[RequestOptions::HEADERS]['Content-Type'] = 'text/xml'; + + + // DX: 415 when request body in existing but not allowed format. + $response = $this->request('POST', $url, $request_options); + // @todo Update this in https://www.drupal.org/node/2826407. Also move it + // higher, before the "no request body" test. That's impossible right now, + // because the format validation happens too late. + $this->assertResourceErrorResponse(415, '', $response); + + + $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; + + + // 201 for well-formed request. + $response = $this->request('POST', $url, $request_options); + $this->assertResourceResponse(201, FALSE, $response); + $this->assertSame([str_replace($this->entity->id(), static::$firstCreatedEntityId, $this->entity->toUrl('canonical')->setAbsolute(TRUE)->toString())], $response->getHeader('Location')); + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + + + $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE); + $request_options[RequestOptions::BODY] = $parseable_valid_request_body_2; + // @todo Remove this in https://www.drupal.org/node/2815845. + drupal_flush_all_caches(); + + + // DX: 403 when unauthorized. + $response = $this->request('POST', $url, $request_options); + // @todo Update the message in https://www.drupal.org/node/2808233. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->grantPermissionsToTestedRole(['restful post entity:' . static::$entityTypeId]); + + + // 201 for well-formed request. + $response = $this->request('POST', $url, $request_options); + $this->assertResourceResponse(201, FALSE, $response); + $this->assertSame([str_replace($this->entity->id(), static::$secondCreatedEntityId, $this->entity->toUrl('canonical')->setAbsolute(TRUE)->toString())], $response->getHeader('Location')); + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + } + + /** + * Tests a PATCH request for an entity, plus edge cases to ensure good DX. + */ + public function testPatch() { + // @todo Remove this in https://www.drupal.org/node/2300677. + if ($this->entity instanceof ConfigEntityInterface) { + $this->assertTrue(TRUE, 'PATCHing config entities is not yet supported.'); + return; + } + + $this->initAuthentication(); + $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); + + // Try with all of the following request bodies. + $unparseable_request_body = '!{>}<'; + $parseable_valid_request_body = $this->serializer->encode($this->getNormalizedPatchEntity(), static::$format); + $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPatchEntity(), static::$format); + $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPatchEntity()), static::$format); + $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPatchEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format); + + // The URL and Guzzle request options that will be used in this test. The + // request options will be modified/expanded throughout this test: + // - to first test all mistakes a developer might make, and assert that the + // error responses provide a good DX + // - to eventually result in a well-formed request that succeeds. + $url = $this->getUrl(); + $request_options = []; + + + // DX: 405 when resource not provisioned, but HTML if canonical route. + $response = $this->request('PATCH', $url, $request_options); + if ($has_canonical_url) { + $this->assertSame(405, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + } + else { + $this->assertResourceErrorResponse(404, 'No route found for "PATCH ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response); + } + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 405 when resource not provisioned. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(405, 'No route found for "PATCH ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response); + + + $this->provisionEntityResource(); + // Simulate the developer again forgetting the ?_format query string. + $url->setOption('query', []); + + + // DX: 415 when no Content-Type request header, but HTML if canonical route. + $response = $this->request('PATCH', $url, $request_options); + if ($has_canonical_url) { + $this->assertSame(415, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertTrue(FALSE !== strpos((string) $response->getBody(), htmlspecialchars('No "Content-Type" request header specified'))); + } + else { + $this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response); + } + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 415 when no Content-Type request header. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response); + + + $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; + + + // DX: 400 when no request body. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(400, 'No entity content received.', $response); + + + $request_options[RequestOptions::BODY] = $unparseable_request_body; + + + // DX: 400 when unparseable request body. + $response = $this->request('PATCH', $url, $request_options); + // @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813853. + // $this->assertResourceErrorResponse(400, 'Syntax error', $response); + $this->assertSame(400, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['error' => 'Syntax error'], static::$format), (string) $response->getBody()); + + + + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body; + + + if (static::$auth) { + // DX: forgetting authentication: authentication provider-specific error + // response. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResponseWhenMissingAuthentication($response); + } + + + $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('PATCH')); + + + // DX: 403 when unauthorized. + $response = $this->request('PATCH', $url, $request_options); + // @todo Update the message in https://www.drupal.org/node/2808233. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->setUpAuthorization('PATCH'); + + + // DX: 422 when invalid entity: multiple values sent for single-value field. + $response = $this->request('PATCH', $url, $request_options); + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; + $label_field_capitalized = ucfirst($label_field); + // @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813755. + // $this->assertErrorResponse(422, "Unprocessable Entity: validation failed.\ntitle: Title: this field cannot hold more than 1 values.\n", $response); + // $this->assertSame(422, $response->getStatusCode()); + // $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n"], static::$format), (string) $response->getBody()); + + + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; + + + // DX: 403 when entity contains field without 'edit' access. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response); + + + // DX: 403 when sending PATCH request with read-only fields. + // First send all fields (the "maximum normalization"). Assert the expected + // error message for the first PATCH-protected field. Remove that field from + // the normalization, send another request, assert the next PATCH-protected + // field error message. And so on. + $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format); + for ($i = 0; $i < count(static::$patchProtectedFieldNames); $i++) { + $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFieldNames, 0, $i)); + $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format); + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(403, "Access denied on updating field '" . static::$patchProtectedFieldNames[$i] . "'.", $response); + } + + // 200 for well-formed request that sends the maximum number of fields. + $max_normalization = $this->removeFieldsFromNormalization($max_normalization, static::$patchProtectedFieldNames); + $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format); + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + + + $request_options[RequestOptions::BODY] = $parseable_valid_request_body; + + + // Before sending a well-formed request, allow the normalization and + // authentication provider edge cases to also be tested. + $this->assertNormalizationEdgeCases('PATCH', $url, $request_options); + $this->assertAuthenticationEdgeCases('PATCH', $url, $request_options); + + + $request_options[RequestOptions::HEADERS]['Content-Type'] = 'text/xml'; + + + // DX: 415 when request body in existing but not allowed format. + $response = $this->request('PATCH', $url, $request_options); + // @todo Update this in https://www.drupal.org/node/2826407. Also move it + // higher, before the "no request body" test. That's impossible right now, + // because the format validation happens too late. + $this->assertResourceErrorResponse(415, '', $response); + + + $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; + + + // 200 for well-formed request. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + // Ensure that fields do not get deleted if they're not present in the PATCH + // request. Test this using the configurable field that we added, but which + // is not sent in the PATCH request. + $this->assertSame('All the faith he had had had had no effect on the outcome of his life.', $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test')->value); + + + $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE); + $request_options[RequestOptions::BODY] = $parseable_valid_request_body_2; + // @todo Remove this in https://www.drupal.org/node/2815845. + drupal_flush_all_caches(); + + + // DX: 403 when unauthorized. + $response = $this->request('PATCH', $url, $request_options); + // @todo Update the message in https://www.drupal.org/node/2808233. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->grantPermissionsToTestedRole(['restful patch entity:' . static::$entityTypeId]); + + + // 200 for well-formed request. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + } + + /** + * Tests a DELETE request for an entity, plus edge cases to ensure good DX. + */ + public function testDelete() { + // @todo Remove this in https://www.drupal.org/node/2300677. + if ($this->entity instanceof ConfigEntityInterface) { + $this->assertTrue(TRUE, 'DELETEing config entities is not yet supported.'); + return; + } + + $this->initAuthentication(); + $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); + + // The URL and Guzzle request options that will be used in this test. The + // request options will be modified/expanded throughout this test: + // - to first test all mistakes a developer might make, and assert that the + // error responses provide a good DX + // - to eventually result in a well-formed request that succeeds. + $url = $this->getUrl(); + $request_options = []; + + + // DX: 405 when resource not provisioned, but HTML if canonical route. + $response = $this->request('DELETE', $url, $request_options); + if ($has_canonical_url) { + $this->assertSame(405, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + } + else { + $this->assertResourceErrorResponse(404, 'No route found for "DELETE ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response); + } + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 405 when resource not provisioned. + $response = $this->request('DELETE', $url, $request_options); + $this->assertResourceErrorResponse(405, 'No route found for "DELETE ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response); + + + $this->provisionEntityResource(); + + + if (static::$auth) { + // DX: forgetting authentication: authentication provider-specific error + // response. + $response = $this->request('DELETE', $url, $request_options); + $this->assertResponseWhenMissingAuthentication($response); + } + + + $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('PATCH')); + + + // DX: 403 when unauthorized. + $response = $this->request('DELETE', $url, $request_options); + // @todo Update the message in https://www.drupal.org/node/2808233. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->setUpAuthorization('DELETE'); + + + // Before sending a well-formed request, allow the authentication provider's + // edge cases to also be tested. + $this->assertAuthenticationEdgeCases('DELETE', $url, $request_options); + + + // 204 for well-formed request. + $response = $this->request('DELETE', $url, $request_options); + $this->assertSame(204, $response->getStatusCode()); + // @todo Uncomment the following line when https://www.drupal.org/node/2821711 is fixed. + // $this->assertSame(FALSE, $response->hasHeader('Content-Type')); + $this->assertSame('', (string) $response->getBody()); + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + + + $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE); + // @todo Remove this in https://www.drupal.org/node/2815845. + drupal_flush_all_caches(); + $this->entity = $this->createEntity(); + $url = $this->getUrl()->setOption('query', $url->getOption('query')); + + + // DX: 403 when unauthorized. + $response = $this->request('DELETE', $url, $request_options); + // @todo Update the message in https://www.drupal.org/node/2808233. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->grantPermissionsToTestedRole(['restful delete entity:' . static::$entityTypeId]); + + + // 204 for well-formed request. + $response = $this->request('DELETE', $url, $request_options); + $this->assertSame(204, $response->getStatusCode()); + // @todo Uncomment the following line when https://www.drupal.org/node/2821711 is fixed. + // $this->assertSame(FALSE, $response->hasHeader('Content-Type')); + $this->assertSame('', (string) $response->getBody()); + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + } + + /** + * {@inheritdoc} + */ + protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) { + // \Drupal\serialization\Normalizer\EntityNormalizer::denormalize(): entity + // types with bundles MUST send their bundle field to be denormalizable. + $entity_type = $this->entity->getEntityType(); + if ($entity_type->hasKey('bundle')) { + $bundle_field_name = $this->entity->getEntityType()->getKey('bundle'); + $normalization = $this->getNormalizedPostEntity(); + + // The bundle type itself can be validated only if there's a bundle entity + // type. + if ($entity_type->getBundleEntityType()) { + $normalization[$bundle_field_name] = 'bad_bundle_name'; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // DX: 400 when incorrect entity type bundle is specified. + // @todo Change to 422 in https://www.drupal.org/node/2827084. + $response = $this->request($method, $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813853 lands. + // $this->assertResourceErrorResponse(400, '"bad_bundle_name" is not a valid bundle type for denormalization.', $response); + $this->assertSame(400, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['error' => '"bad_bundle_name" is not a valid bundle type for denormalization.'], static::$format), (string) $response->getBody()); + } + + + unset($normalization[$bundle_field_name]); + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // DX: 400 when no entity type bundle is specified. + // @todo Change to 422 in https://www.drupal.org/node/2827084. + $response = $this->request($method, $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813853 lands. + // $this->assertResourceErrorResponse(400, 'A string must be provided as a bundle value.', $response); + $this->assertSame(400, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['error' => 'A string must be provided as a bundle value.'], static::$format), (string) $response->getBody()); + } + } + + /** + * Gets an entity resource's GET/PATCH/DELETE URL. + * + * @return \Drupal\Core\Url + * The URL to GET/PATCH/DELETE. + */ + protected function getUrl() { + $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); + return $has_canonical_url ? $this->entity->toUrl() : Url::fromUri('base:entity/' . static::$entityTypeId . '/' . $this->entity->id()); + } + + /** + * Gets an entity resource's POST URL. + * + * @return \Drupal\Core\Url + * The URL to POST to. + */ + protected function getPostUrl() { + $has_canonical_url = $this->entity->hasLinkTemplate('https://www.drupal.org/link-relations/create'); + return $has_canonical_url ? $this->entity->toUrl() : Url::fromUri('base:entity/' . static::$entityTypeId); + } + + /** + * Makes the given entity normalization invalid. + * + * @param array $normalization + * An entity normalization. + * + * @return array + * The updated entity normalization, now invalid. + */ + protected function makeNormalizationInvalid(array $normalization) { + // Add a second label to this entity to make it invalid. + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; + $normalization[$label_field][1]['value'] = 'Second Title'; + + return $normalization; + } + + /** + * Removes fields from a normalization. + * + * @param array $normalization + * An entity normalization. + * @param string[] $field_names + * The field names to remove from the entity normalization. + * + * @return array + * The updated entity normalization. + * + * @see ::testPatch + */ + protected function removeFieldsFromNormalization(array $normalization, $field_names) { + return array_diff_key($normalization, array_flip($field_names)); + } + + /** + * Asserts a 406 response… or in some cases a 403 response, because weirdness. + * + * Asserting a 406 response should be easy, but it's not, due to bugs. + * + * Drupal returns a 403 response instead of a 406 response when: + * - there is a canonical route, i.e. one that serves HTML + * - unless the user is logged in with any non-global authentication provider, + * because then they tried to access a route that requires the user to be + * authenticated, but they used an authentication provider that is only + * accepted for specific routes, and HTML routes never have such specific + * authentication providers specified. (By default, only 'cookie' is a + * global authentication provider.) + * + * @todo Remove this in https://www.drupal.org/node/2805279. + * + * @param \Psr\Http\Message\ResponseInterface $response + * The response to assert. + */ + protected function assert406Response(ResponseInterface $response) { + if ($this->entity->hasLinkTemplate('canonical') && ($this->account && static::$auth !== 'cookie')) { + $this->assertSame(403, $response->getStatusCode()); + } + else { + // This is the desired response. + $this->assertSame(406, $response->getStatusCode()); + } + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonAnonTest.php new file mode 100644 index 000000000..a7e442055 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonAnonTest.php @@ -0,0 +1,29 @@ +grantPermissionsToTestedRole(['view test entity']); + break; + case 'POST': + $this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities']); + break; + case 'PATCH': + case 'DELETE': + $this->grantPermissionsToTestedRole(['administer entity_test content']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $entity_test = EntityTest::create([ + 'name' => 'Llama', + 'type' => 'entity_test', + ]); + $entity_test->setOwnerId(0); + $entity_test->save(); + + return $entity_test; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $author = User::load(0); + $normalization = [ + 'uuid' => [ + [ + 'value' => $this->entity->uuid() + ] + ], + 'id' => [ + [ + 'value' => '1', + ], + ], + 'langcode' => [ + [ + 'value' => 'en', + ], + ], + 'type' => [ + [ + 'value' => 'entity_test', + ] + ], + 'name' => [ + [ + 'value' => 'Llama', + ] + ], + 'created' => [ + [ + 'value' => $this->entity->get('created')->value, + ] + ], + 'user_id' => [ + [ + 'target_id' => $author->id(), + 'target_type' => 'user', + 'target_uuid' => $author->uuid(), + 'url' => $author->toUrl()->toString(), + ] + ], + 'field_test_text' => [], + ]; + + return $normalization; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return [ + 'type' => 'entity_test', + 'name' => [ + [ + 'value' => 'Dramallama', + ], + ], + ]; + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonAnonTest.php new file mode 100644 index 000000000..24d47c495 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonAnonTest.php @@ -0,0 +1,29 @@ +grantPermissionsToTestedRole(['access content']); + break; + case 'POST': + $this->grantPermissionsToTestedRole(['access content', 'create camelids content']); + break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['access content', 'edit any camelids content']); + break; + case 'DELETE': + $this->grantPermissionsToTestedRole(['access content', 'delete any camelids content']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + if (!NodeType::load('camelids')) { + // Create a "Camelids" node type. + NodeType::create([ + 'name' => 'Camelids', + 'type' => 'camelids', + ])->save(); + } + + // Create a "Llama" node. + $node = Node::create(['type' => 'camelids']); + $node->setTitle('Llama') + ->setOwnerId(static::$auth ? $this->account->id() : 0) + ->setPublished(TRUE) + ->setCreatedTime(123456789) + ->setChangedTime(123456789) + ->setRevisionCreationTime(123456789) + ->save(); + + return $node; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $author = User::load($this->entity->getOwnerId()); + return [ + 'nid' => [ + ['value' => 1], + ], + 'uuid' => [ + ['value' => $this->entity->uuid()], + ], + 'vid' => [ + ['value' => 1], + ], + 'langcode' => [ + [ + 'value' => 'en', + ], + ], + 'type' => [ + [ + 'target_id' => 'camelids', + 'target_type' => 'node_type', + 'target_uuid' => NodeType::load('camelids')->uuid(), + ], + ], + 'title' => [ + [ + 'value' => 'Llama', + ], + ], + 'status' => [ + [ + 'value' => 1, + ], + ], + 'created' => [ + [ + 'value' => '123456789', + ], + ], + 'changed' => [ + [ + 'value' => '123456789', + ], + ], + 'promote' => [ + [ + 'value' => 1, + ], + ], + 'sticky' => [ + [ + 'value' => '0', + ], + ], + 'revision_timestamp' => [ + [ + 'value' => '123456789', + ], + ], + 'revision_translation_affected' => [ + [ + 'value' => TRUE, + ], + ], + 'default_langcode' => [ + [ + 'value' => TRUE, + ], + ], + 'uid' => [ + [ + 'target_id' => $author->id(), + 'target_type' => 'user', + 'target_uuid' => $author->uuid(), + 'url' => base_path() . 'user/' . $author->id(), + ], + ], + 'revision_uid' => [ + [ + 'target_id' => $author->id(), + 'target_type' => 'user', + 'target_uuid' => $author->uuid(), + 'url' => base_path() . 'user/' . $author->id(), + ], + ], + 'revision_log' => [ + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return [ + 'type' => [ + [ + 'target_id' => 'camelids', + ], + ], + 'title' => [ + [ + 'value' => 'Dramallama', + ], + ], + ]; + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonAnonTest.php new file mode 100644 index 000000000..3ec96eb7a --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonAnonTest.php @@ -0,0 +1,29 @@ +assertSame(401, $response->getStatusCode()); + $this->assertSame('{"message":"A fatal error occurred: No authentication credentials provided."}', (string) $response->getBody()); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonCookieTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonCookieTest.php new file mode 100644 index 000000000..9f4ec073c --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonCookieTest.php @@ -0,0 +1,34 @@ +grantPermissionsToTestedRole(['administer permissions']); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $role = Role::create([ + 'id' => 'llama', + 'name' => $this->randomString(), + ]); + $role->save(); + + return $role; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return [ + 'uuid' => $this->entity->uuid(), + 'weight' => 2, + 'langcode' => 'en', + 'status' => TRUE, + 'dependencies' => [], + 'id' => 'llama', + 'label' => NULL, + 'is_admin' => NULL, + 'permissions' => [], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + // @todo Update in https://www.drupal.org/node/2300677. + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonAnonTest.php new file mode 100644 index 000000000..6e01c0314 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonAnonTest.php @@ -0,0 +1,29 @@ +grantPermissionsToTestedRole(['access content']); + break; + case 'POST': + case 'PATCH': + case 'DELETE': + // @todo Update once https://www.drupal.org/node/2824408 lands. + $this->grantPermissionsToTestedRole(['administer taxonomy']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $vocabulary = Vocabulary::load('camelids'); + if (!$vocabulary) { + // Create a "Camelids" vocabulary. + $vocabulary = Vocabulary::create([ + 'name' => 'Camelids', + 'vid' => 'camelids', + ]); + $vocabulary->save(); + } + + // Create a "Llama" taxonomy term. + $term = Term::create(['vid' => $vocabulary->id()]) + ->setName('Llama') + ->setChangedTime(123456789); + $term->save(); + + return $term; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return [ + 'tid' => [ + ['value' => 1], + ], + 'uuid' => [ + ['value' => $this->entity->uuid()], + ], + 'vid' => [ + [ + 'target_id' => 'camelids', + 'target_type' => 'taxonomy_vocabulary', + 'target_uuid' => Vocabulary::load('camelids')->uuid(), + ], + ], + 'name' => [ + ['value' => 'Llama'], + ], + 'description' => [ + [ + 'value' => NULL, + 'format' => NULL, + ], + ], + 'parent' => [], + 'weight' => [ + ['value' => 0], + ], + 'langcode' => [ + [ + 'value' => 'en', + ], + ], + 'changed' => [ + [ + 'value' => '123456789', + ], + ], + 'default_langcode' => [ + [ + 'value' => TRUE, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return [ + 'vid' => [ + [ + 'target_id' => 'camelids', + ], + ], + 'name' => [ + [ + 'value' => 'Dramallama', + ], + ], + ]; + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/User/UserJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/User/UserJsonAnonTest.php new file mode 100644 index 000000000..a20aff834 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserJsonAnonTest.php @@ -0,0 +1,29 @@ +grantPermissionsToTestedRole(['access user profiles']); + break; + case 'POST': + case 'PATCH': + case 'DELETE': + $this->grantPermissionsToTestedRole(['administer users']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + // Create a "Llama" user. + $user = User::create(['created' => 123456789]); + $user->setUsername('Llama') + ->setChangedTime(123456789) + ->activate() + ->save(); + + return $user; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return [ + 'uid' => [ + ['value' => '3'], + ], + 'uuid' => [ + ['value' => $this->entity->uuid()], + ], + 'langcode' => [ + [ + 'value' => 'en', + ], + ], + 'name' => [ + [ + 'value' => 'Llama', + ], + ], + 'created' => [ + [ + 'value' => '123456789', + ], + ], + 'changed' => [ + [ + 'value' => '123456789', + ], + ], + 'default_langcode' => [ + [ + 'value' => TRUE, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return [ + 'name' => [ + [ + 'value' => 'Dramallama ' . $this->randomMachineName(), + ], + ], + ]; + } + + /** + * Tests PATCHing security-sensitive base fields of the logged in account. + */ + public function testPatchDxForSecuritySensitiveBaseFields() { + // The anonymous user is never allowed to modify itself. + if (!static::$auth) { + $this->markTestSkipped(); + } + + $this->initAuthentication(); + $this->provisionEntityResource(); + $this->setUpAuthorization('PATCH'); + + /** @var \Drupal\user\UserInterface $user */ + $user = static::$auth ? $this->account : User::load(0); + $original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['changed' => TRUE]); + + + // Since this test must be performed by the user that is being modified, + // we cannot use $this->getUrl(). + $url = $user->toUrl()->setOption('query', ['_format' => static::$format]); + $request_options = [ + RequestOptions::HEADERS => ['Content-Type' => static::$mimeType], + ]; + $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH')); + + + // Test case 1: changing email. + $normalization = $original_normalization; + $normalization['mail'] = [['value' => 'new-email@example.com']]; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // DX: 422 when changing email without providing the password. + $response = $this->request('PATCH', $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813755 lands. + // $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response); + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"], static::$format), (string) $response->getBody()); + + + $normalization['pass'] = [['existing' => 'wrong']]; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + // DX: 422 when changing email while providing a wrong password. + $response = $this->request('PATCH', $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813755 lands. + // $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response); + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"], static::$format), (string) $response->getBody()); + + + $normalization['pass'] = [['existing' => $this->account->passRaw]]; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // 200 for well-formed request. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + + + // Test case 2: changing password. + $normalization = $original_normalization; + $new_password = $this->randomString(); + $normalization['pass'] = [['value' => $new_password]]; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // DX: 422 when changing password without providing the current password. + $response = $this->request('PATCH', $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813755 lands. + // $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response); + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n"], static::$format), (string) $response->getBody()); + + + $normalization['pass'][0]['existing'] = $this->account->pass_raw; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // 200 for well-formed request. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + + + // Verify that we can log in with the new password. + $request_body = [ + 'name' => $user->getAccountName(), + 'pass' => $new_password, + ]; + $request_options = [ + RequestOptions::HEADERS => [], + RequestOptions::BODY => $this->serializer->encode($request_body, 'json'), + ]; + $response = $this->httpClient->request('POST', Url::fromRoute('user.login.http')->setRouteParameter('_format', 'json')->toString(), $request_options); + $this->assertSame(200, $response->getStatusCode()); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyJsonAnonTest.php new file mode 100644 index 000000000..4e2910699 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyJsonAnonTest.php @@ -0,0 +1,37 @@ +markTestSkipped(); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyJsonBasicAuthTest.php new file mode 100644 index 000000000..106005006 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyJsonBasicAuthTest.php @@ -0,0 +1,45 @@ +grantPermissionsToTestedRole(['administer taxonomy']); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $vocabulary = Vocabulary::create([ + 'name' => 'Llama', + 'vid' => 'llama', + ]); + $vocabulary->save(); + + return $vocabulary; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return [ + 'uuid' => $this->entity->uuid(), + 'vid' => 'llama', + 'langcode' => 'en', + 'status' => TRUE, + 'dependencies' => [], + 'name' => 'Llama', + 'description' => NULL, + 'hierarchy' => 0, + 'weight' => 0, + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + // @todo Update in https://www.drupal.org/node/2300677. + } + +} diff --git a/core/modules/rest/tests/src/Functional/JsonBasicAuthWorkaroundFor2805281Trait.php b/core/modules/rest/tests/src/Functional/JsonBasicAuthWorkaroundFor2805281Trait.php new file mode 100644 index 000000000..495bf5ae7 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/JsonBasicAuthWorkaroundFor2805281Trait.php @@ -0,0 +1,25 @@ +assertSame(401, $response->getStatusCode()); + $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); + // Note that strange 'A fatal error occurred: ' prefix, that should not + // exist. + // @todo Fix in https://www.drupal.org/node/2805281. + $this->assertSame('{"message":"A fatal error occurred: No authentication credentials provided."}', (string) $response->getBody()); + } + +} diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php new file mode 100644 index 000000000..f1f0458dc --- /dev/null +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php @@ -0,0 +1,349 @@ +getPermissions() as $permission) { + $user_role->revokePermission($permission); + } + $user_role->save(); + assert('[] === $user_role->getPermissions()', 'The anonymous user role has no permissions at all.'); + + if (static::$auth !== FALSE) { + // Ensure the authenticated user role has no permissions at all. + $user_role = Role::load(RoleInterface::AUTHENTICATED_ID); + foreach ($user_role->getPermissions() as $permission) { + $user_role->revokePermission($permission); + } + $user_role->save(); + assert('[] === $user_role->getPermissions()', 'The authenticated user role has no permissions at all.'); + + // Create an account. + $this->account = $this->createUser(); + } + else { + // Otherwise, also create an account, so that any test involving User + // entities will have the same user IDs regardless of authentication. + $this->createUser(); + } + + $this->resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config'); + + // Ensure there's a clean slate: delete all REST resource config entities. + $this->resourceConfigStorage->delete($this->resourceConfigStorage->loadMultiple()); + } + + /** + * Provisions a REST resource. + * + * @param string $resource_type + * The resource type (REST resource plugin ID). + * @param string[] $formats + * The allowed formats for this resource. + * @param string[] $authentication + * The allowed authentication providers for this resource. + */ + protected function provisionResource($resource_type, $formats = [], $authentication = []) { + $this->resourceConfigStorage->create([ + 'id' => $resource_type, + 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY, + 'configuration' => [ + 'methods' => ['GET', 'POST', 'PATCH', 'DELETE'], + 'formats' => $formats, + 'authentication' => $authentication, + ] + ])->save(); + // @todo Remove this in https://www.drupal.org/node/2815845. + drupal_flush_all_caches(); + } + + /** + * Sets up the necessary authorization. + * + * In case of a test verifying publicly accessible REST resources: grant + * permissions to the anonymous user role. + * + * In case of a test verifying behavior when using a particular authentication + * provider: create a user with a particular set of permissions. + * + * Because of the $method parameter, it's possible to first set up + * authentication for only GET, then add POST, et cetera. This then also + * allows for verifying a 403 in case of missing authorization. + * + * @param string $method + * The HTTP method for which to set up authentication. + * + * @see ::grantPermissionsToAnonymousRole() + * @see ::grantPermissionsToAuthenticatedRole() + */ + abstract protected function setUpAuthorization($method); + + /** + * Verifies the error response in case of missing authentication. + */ + abstract protected function assertResponseWhenMissingAuthentication(ResponseInterface $response); + + /** + * Asserts normalization-specific edge cases. + * + * (Should be called before sending a well-formed request.) + * + * @see \GuzzleHttp\ClientInterface::request() + * + * @param string $method + * HTTP method. + * @param \Drupal\Core\Url $url + * URL to request. + * @param array $request_options + * Request options to apply. + */ + abstract protected function assertNormalizationEdgeCases($method, Url $url, array $request_options); + + /** + * Asserts authentication provider-specific edge cases. + * + * (Should be called before sending a well-formed request.) + * + * @see \GuzzleHttp\ClientInterface::request() + * + * @param string $method + * HTTP method. + * @param \Drupal\Core\Url $url + * URL to request. + * @param array $request_options + * Request options to apply. + */ + abstract protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options); + + /** + * Initializes authentication. + * + * E.g. for cookie authentication, we first need to get a cookie. + */ + protected function initAuthentication() {} + + /** + * Returns Guzzle request options for authentication. + * + * @param string $method + * The HTTP method for this authenticated request. + * + * @return array + * Guzzle request options to use for authentication. + * + * @see \GuzzleHttp\ClientInterface::request() + */ + protected function getAuthenticationRequestOptions($method) { + return []; + } + + /** + * Grants permissions to the anonymous role. + * + * @param string[] $permissions + * Permissions to grant. + */ + protected function grantPermissionsToAnonymousRole(array $permissions) { + $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), $permissions); + } + + /** + * Grants permissions to the authenticated role. + * + * @param string[] $permissions + * Permissions to grant. + */ + protected function grantPermissionsToAuthenticatedRole(array $permissions) { + $this->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), $permissions); + } + + /** + * Grants permissions to the tested role: anonymous or authenticated. + * + * @param string[] $permissions + * Permissions to grant. + * + * @see ::grantPermissionsToAuthenticatedRole() + * @see ::grantPermissionsToAnonymousRole() + */ + protected function grantPermissionsToTestedRole(array $permissions) { + if (static::$auth) { + $this->grantPermissionsToAuthenticatedRole($permissions); + } + else { + $this->grantPermissionsToAnonymousRole($permissions); + } + } + + /** + * Performs a HTTP request. Wraps the Guzzle HTTP client. + * + * Why wrap the Guzzle HTTP client? Because we want to keep the actual test + * code as simple as possible, and hence not require them to specify the + * 'http_errors = FALSE' request option, nor do we want them to have to + * convert Drupal Url objects to strings. + * + * @see \GuzzleHttp\ClientInterface::request() + * + * @param string $method + * HTTP method. + * @param \Drupal\Core\Url $url + * URL to request. + * @param array $request_options + * Request options to apply. + * + * @return \Psr\Http\Message\ResponseInterface + */ + protected function request($method, Url $url, array $request_options) { + $request_options[RequestOptions::HTTP_ERRORS] = FALSE; + return $this->httpClient->request($method, $url->toString(), $request_options); + } + + /** + * Asserts that a resource response has the given status code and body. + * + * (Also asserts that the expected error MIME type is present, but this is + * defined globally for the test via static::$expectedErrorMimeType, because + * all error responses should use the same MIME type.) + * + * @param int $expected_status_code + * The expected response status. + * @param string|false $expected_body + * The expected response body. FALSE in case this should not be asserted. + * @param \Psr\Http\Message\ResponseInterface $response + * The response to assert. + */ + protected function assertResourceResponse($expected_status_code, $expected_body, ResponseInterface $response) { + $this->assertSame($expected_status_code, $response->getStatusCode()); + if ($expected_status_code < 400) { + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + } + else { + $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); + } + if ($expected_body !== FALSE) { + $this->assertSame($expected_body, (string) $response->getBody()); + } + } + + /** + * Asserts that a resource error response has the given message. + * + * (Also asserts that the expected error MIME type is present, but this is + * defined globally for the test via static::$expectedErrorMimeType, because + * all error responses should use the same MIME type.) + * + * @param int $expected_status_code + * The expected response status. + * @param string $expected_message + * The expected error message. + * @param \Psr\Http\Message\ResponseInterface $response + * The error response to assert. + */ + protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response) { + // @todo Fix this in https://www.drupal.org/node/2813755. + $encode_options = ['json_encode_options' => JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT]; + $expected_body = $this->serializer->encode(['message' => $expected_message], static::$format, $encode_options); + $this->assertResourceResponse($expected_status_code, $expected_body, $response); + } + +} diff --git a/core/modules/rest/tests/src/Unit/Entity/RestResourceConfigTest.php b/core/modules/rest/tests/src/Unit/Entity/RestResourceConfigTest.php new file mode 100644 index 000000000..f60225e5e --- /dev/null +++ b/core/modules/rest/tests/src/Unit/Entity/RestResourceConfigTest.php @@ -0,0 +1,42 @@ + ['cookie'], + 'supported_formats' => ['json'], + ]; + } + + $entity = new RestResourceConfig([ + 'plugin_id' => 'entity:entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => $configuration, + ], 'rest_resource_config'); + + $this->assertArrayEquals($expected, $entity->getMethods()); + } + +} diff --git a/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php index a8b71c7bb..013402c5c 100644 --- a/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php +++ b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php @@ -63,7 +63,7 @@ class UserRouteAlterSubscriber implements EventSubscriberInterface { foreach ($route_names as $route_name) { if ($route = $routes->get($route_name)) { $formats = explode('|', $route->getRequirement('_format')); - $formats = array_unique($formats + $this->serializerFormats); + $formats = array_unique(array_merge($formats, $this->serializerFormats)); $route->setRequirement('_format', implode('|', $formats)); } } diff --git a/core/modules/simpletest/tests/src/FunctionalJavascript/JavascriptGetDrupalSettingsTest.php b/core/modules/simpletest/tests/src/FunctionalJavascript/JavascriptGetDrupalSettingsTest.php new file mode 100644 index 000000000..db8b3acd5 --- /dev/null +++ b/core/modules/simpletest/tests/src/FunctionalJavascript/JavascriptGetDrupalSettingsTest.php @@ -0,0 +1,46 @@ +drupalLogin($this->drupalCreateUser()); + $this->drupalGet('test-page'); + + // Check that we can read the JS settings. + $js_settings = $this->getDrupalSettings(); + $this->assertSame('azAZ09();.,\\\/-_{}', $js_settings['test-setting']); + + // Dynamically change the setting using Javascript. + $script = <<getSession()->evaluateScript($script); + + // Check that the setting has been changed. + $js_settings = $this->getDrupalSettings(); + $this->assertSame('foo', $js_settings['test-setting']); + } + +} diff --git a/core/modules/statistics/src/Tests/StatisticsAdminTest.php b/core/modules/statistics/src/Tests/StatisticsAdminTest.php index 565925c0e..6d87fd975 100644 --- a/core/modules/statistics/src/Tests/StatisticsAdminTest.php +++ b/core/modules/statistics/src/Tests/StatisticsAdminTest.php @@ -52,8 +52,7 @@ class StatisticsAdminTest extends WebTestBase { $this->privilegedUser = $this->drupalCreateUser(array('administer statistics', 'view post access counter', 'create page content')); $this->drupalLogin($this->privilegedUser); $this->testNode = $this->drupalCreateNode(array('type' => 'page', 'uid' => $this->privilegedUser->id())); - $this->client = \Drupal::service('http_client_factory') - ->fromOptions(['config/curl' => [CURLOPT_TIMEOUT => 10]]); + $this->client = \Drupal::httpClient(); } /** diff --git a/core/modules/statistics/src/Tests/StatisticsLoggingTest.php b/core/modules/statistics/src/Tests/StatisticsLoggingTest.php index 7cfec7cd8..9aa3cc3f9 100644 --- a/core/modules/statistics/src/Tests/StatisticsLoggingTest.php +++ b/core/modules/statistics/src/Tests/StatisticsLoggingTest.php @@ -82,9 +82,7 @@ class StatisticsLoggingTest extends WebTestBase { // Clear the logs. db_truncate('node_counter'); - - $this->client = \Drupal::service('http_client_factory') - ->fromOptions(['config/curl' => [CURLOPT_TIMEOUT => 10]]); + $this->client = \Drupal::httpClient(); } /** diff --git a/core/modules/statistics/src/Tests/StatisticsReportsTest.php b/core/modules/statistics/src/Tests/StatisticsReportsTest.php index 9c0d26c59..402807412 100644 --- a/core/modules/statistics/src/Tests/StatisticsReportsTest.php +++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php @@ -25,8 +25,7 @@ class StatisticsReportsTest extends StatisticsTestBase { $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; - $client = \Drupal::service('http_client_factory') - ->fromOptions(['config/curl' => [CURLOPT_TIMEOUT => 10]]); + $client = \Drupal::httpClient(); $client->post($stats_path, array('headers' => $headers, 'body' => $post)); // Configure and save the block. diff --git a/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php b/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php index b7d22b8fe..e2e4533d6 100644 --- a/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php +++ b/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php @@ -28,8 +28,7 @@ class StatisticsTokenReplaceTest extends StatisticsTestBase { $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; - $client = \Drupal::service('http_client_factory') - ->fromOptions(['config/curl' => [CURLOPT_TIMEOUT => 10]]); + $client = \Drupal::httpClient(); $client->post($stats_path, array('headers' => $headers, 'body' => $post)); $statistics = statistics_get($node->id()); diff --git a/core/modules/statistics/src/Tests/Views/IntegrationTest.php b/core/modules/statistics/src/Tests/Views/IntegrationTest.php index 07380c889..7fe99f93e 100644 --- a/core/modules/statistics/src/Tests/Views/IntegrationTest.php +++ b/core/modules/statistics/src/Tests/Views/IntegrationTest.php @@ -75,7 +75,7 @@ class IntegrationTest extends ViewTestBase { // @see \Drupal\statistics\Tests\StatisticsLoggingTest::testLogging(). global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; - $client = \Drupal::service('http_client_factory')->fromOptions(['config/curl', array(CURLOPT_TIMEOUT => 10)]); + $client = \Drupal::httpClient(); $client->post($stats_path, array('form_params' => array('nid' => $this->node->id()))); $this->drupalGet('test_statistics_integration'); diff --git a/core/modules/system/migration_templates/d7_global_theme_settings.yml b/core/modules/system/migration_templates/d7_global_theme_settings.yml new file mode 100644 index 000000000..42c5177d4 --- /dev/null +++ b/core/modules/system/migration_templates/d7_global_theme_settings.yml @@ -0,0 +1,29 @@ +id: d7_global_theme_settings +label: D7 global theme settings +migration_tags: + - Drupal 7 +source: + plugin: variable + variables: + - theme_settings +process: + 'features/logo': theme_settings/toggle_logo + 'features/name': theme_settings/toggle_name + 'features/slogan': theme_settings/toggle_slogan + 'features/node_user_picture': theme_settings/toggle_node_user_picture + 'features/comment_user_picture': theme_settings/toggle_comment_user_picture + 'features/comment_user_verification': theme_settings/toggle_comment_user_verification + 'features/favicon': theme_settings/toggle_favicon + 'logo/use_default': theme_settings/default_logo + 'logo/path': theme_settings/logo_path + 'favicon/use_default': theme_settings/default_favicon + 'favicon/path': theme_settings/favicon_path + 'favicon/mimetype': theme_settings/favicon_mimetype +# Ignore settings not present in Drupal 8 +# theme_settings/logo_upload +# theme_settings/favicon_upload +# theme_settings/toggle_main_menu +# theme_settings/toggle_secondary_menu +destination: + plugin: config + config_name: system.theme.global diff --git a/core/modules/system/src/MachineNameController.php b/core/modules/system/src/MachineNameController.php index 239e5b3ec..d6310f459 100644 --- a/core/modules/system/src/MachineNameController.php +++ b/core/modules/system/src/MachineNameController.php @@ -6,9 +6,9 @@ use Drupal\Component\Transliteration\TransliterationInterface; use Drupal\Component\Utility\Unicode; use Drupal\Core\Access\CsrfTokenGenerator; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -78,10 +78,10 @@ class MachineNameController implements ContainerInjectionInterface { if (isset($replace_pattern) && isset($replace)) { if (!isset($replace_token)) { - throw new AccessDeniedException("Missing 'replace_token' query parameter."); + throw new AccessDeniedHttpException("Missing 'replace_token' query parameter."); } elseif (!$this->tokenGenerator->validate($replace_token, $replace_pattern)) { - throw new AccessDeniedException("Invalid 'replace_token' query parameter."); + throw new AccessDeniedHttpException("Invalid 'replace_token' query parameter."); } // Quote the pattern delimiter and remove null characters to avoid the e diff --git a/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryErrorTest.php b/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryErrorTest.php index dbb15be52..eb50a0d88 100644 --- a/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryErrorTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryErrorTest.php @@ -44,7 +44,7 @@ class InstallerConfigDirectorySetNoDirectoryErrorTest extends InstallerTestBase } /** - * @{inheritdoc} + * {@inheritdoc} */ protected function setUpSite() { // This step should not appear as we had a failure prior to the settings diff --git a/core/modules/system/src/Tests/Installer/InstallerDatabaseErrorMessagesTest.php b/core/modules/system/src/Tests/Installer/InstallerDatabaseErrorMessagesTest.php index 32829c833..c98a5de0b 100644 --- a/core/modules/system/src/Tests/Installer/InstallerDatabaseErrorMessagesTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerDatabaseErrorMessagesTest.php @@ -13,7 +13,7 @@ use Drupal\simpletest\InstallerTestBase; class InstallerDatabaseErrorMessagesTest extends InstallerTestBase { /** - * @{inheritdoc} + * {@inheritdoc} */ protected function setUpSettings() { // We are creating a table here to force an error in the installer because @@ -25,7 +25,7 @@ class InstallerDatabaseErrorMessagesTest extends InstallerTestBase { } /** - * @{inheritdoc} + * {@inheritdoc} */ protected function setUpSite() { // This step should not appear as we had a failure on the settings screen. diff --git a/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php b/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php index 330d61ff6..fd6e0019b 100644 --- a/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php @@ -42,7 +42,7 @@ class InstallerTranslationTest extends InstallerTestBase { } /** - * @{inheritdoc} + * {@inheritdoc} */ protected function setUpSettings() { // We are creating a table here to force an error in the installer because diff --git a/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php b/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php index 06af7c726..6060fcc30 100644 --- a/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php +++ b/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php @@ -54,6 +54,14 @@ class HtmlResponseAttachmentsTest extends WebTestBase { // Repeat for the cache. $this->drupalGet('/render_attached_test/head'); $this->assertHeader('X-Drupal-Cache', 'HIT'); + + // Test ['#attached']['html_head_link'] when outputted as HTTP header. + $this->drupalGet('/render_attached_test/html_header_link'); + $expected_link_headers = [ + '; rel="alternate"', + '; hreflang="nl"; rel="alternate"', + ]; + $this->assertEqual($this->drupalGetHeader('link'), implode(',', $expected_link_headers)); } /** diff --git a/core/modules/system/templates/links.html.twig b/core/modules/system/templates/links.html.twig index 77e726008..62e9ed0b5 100644 --- a/core/modules/system/templates/links.html.twig +++ b/core/modules/system/templates/links.html.twig @@ -8,11 +8,11 @@ * - links: Links to be output. * Each link will have the following elements: * - title: The link text. - * - href: The link URL. If omitted, the 'title' is shown as a plain text - * item in the links list. If 'href' is supplied, the entire link is passed + * - url: The link URL. If omitted, the 'title' is shown as a plain text + * item in the links list. If 'url' is supplied, the entire link is passed * to l() as its $options parameter. * - attributes: (optional) HTML attributes for the anchor, or for the - * tag if no 'href' is supplied. + * tag if no 'url' is supplied. * - heading: (optional) A heading to precede the links. * - text: The heading text. * - level: The heading level (e.g. 'h2', 'h3'). diff --git a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml index d34d94940..464a007ac 100644 --- a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml +++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml @@ -27,3 +27,19 @@ entity_test.entity_test_bundle.*: description: type: text label: 'Description' + +entity_test.entity_test_bundle.*.third_party.content_moderation: + type: mapping + label: 'Enable moderation states for this entity test type' + mapping: + enabled: + type: boolean + label: 'Moderation states enabled' + allowed_moderation_states: + type: sequence + sequence: + type: string + label: 'Moderation state' + default_moderation_state: + type: string + label: 'Moderation state for new entity test' diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index b0c8cbf4d..f8f56ebbb 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -95,12 +95,26 @@ function entity_test_entity_type_alter(array &$entity_types) { // Allow entity_test_update tests to override the entity type definition. $entity_types['entity_test_update'] = $state->get('entity_test_update.entity_type', $entity_types['entity_test_update']); + // Allow entity_test_with_bundle tests to override the entity type definition. + $entity_types['entity_test_with_bundle'] = $state->get('entity_test_with_bundle.entity_type', $entity_types['entity_test_with_bundle']); + // Enable the entity_test_new only when needed. if (!$state->get('entity_test_new')) { unset($entity_types['entity_test_new']); } } +/** + * Implements hook_module_implements_alter(). + */ +function entity_test_module_implements_alter(&$implementations, $hook) { + // Move our hook_entity_type_alter() implementation to the beginning of the + // list in order to run before content_moderation_entity_type_alter(). + if ($hook === 'entity_type_alter') { + $implementations = ['entity_test' => $implementations['entity_test']] + $implementations; + } +} + /** * Implements hook_entity_base_field_info(). */ diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php index b339f5b85..fb69ccb34 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php @@ -19,7 +19,7 @@ namespace Drupal\entity_test\Entity; class EntityTestNoLabel extends EntityTest { /** - * @{inheritdoc} + * {@inheritdoc} */ public function label() { return $this->getName(); diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php index a63abada0..342cbb9b1 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php @@ -25,7 +25,8 @@ use Drupal\Core\Entity\EntityTypeInterface; * entity_keys = { * "id" = "id", * "uuid" = "uuid", - * "bundle" = "type" + * "bundle" = "type", + * "label" = "name", * }, * links = { * "canonical" = "/entity_test_string_id/manage/{entity_test_string_id}", diff --git a/core/modules/system/tests/modules/entity_test/src/EntityTestViewBuilder.php b/core/modules/system/tests/modules/entity_test/src/EntityTestViewBuilder.php index 90eaf3017..720e3c3b8 100644 --- a/core/modules/system/tests/modules/entity_test/src/EntityTestViewBuilder.php +++ b/core/modules/system/tests/modules/entity_test/src/EntityTestViewBuilder.php @@ -2,7 +2,6 @@ namespace Drupal\entity_test; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityViewBuilder; /** @@ -12,15 +11,6 @@ use Drupal\Core\Entity\EntityViewBuilder; */ class EntityTestViewBuilder extends EntityViewBuilder { - /** - * {@inheritdoc} - */ - protected function getBuildDefaults(EntityInterface $entity, $view_mode) { - $build = parent::getBuildDefaults($entity, $view_mode); - unset($build['#theme']); - return $build; - } - /** * {@inheritdoc} */ diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldWidget/ShapeOnlyColorEditableWidget.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldWidget/ShapeOnlyColorEditableWidget.php new file mode 100644 index 000000000..bcff1ec33 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldWidget/ShapeOnlyColorEditableWidget.php @@ -0,0 +1,40 @@ + 'hidden', + '#value' => $items[$delta]->shape + ]; + + $element['color'] = [ + '#type' => 'textfield', + '#default_value' => isset($items[$delta]->color) ? $items[$delta]->color : NULL, + '#size' => 255, + ]; + + return $element; + } + +} diff --git a/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml b/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml index 918209b54..a14bdc611 100644 --- a/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml +++ b/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml @@ -19,6 +19,13 @@ render_attached.head: requirements: _access: 'TRUE' +render_attached.html_header_link: + path: '/render_attached_test/html_header_link' + defaults: + _controller: '\Drupal\render_attached_test\Controller\RenderAttachedTestController::htmlHeaderLink' + requirements: + _access: 'TRUE' + render_attached.feed_single: path: '/render_attached_test/feed' defaults: diff --git a/core/modules/system/tests/modules/render_attached_test/src/Controller/RenderAttachedTestController.php b/core/modules/system/tests/modules/render_attached_test/src/Controller/RenderAttachedTestController.php index 4405196fd..013f100b8 100644 --- a/core/modules/system/tests/modules/render_attached_test/src/Controller/RenderAttachedTestController.php +++ b/core/modules/system/tests/modules/render_attached_test/src/Controller/RenderAttachedTestController.php @@ -69,4 +69,18 @@ class RenderAttachedTestController { return $render; } + /** + * Test HTTP header rendering for link. + * + * @return array + * A render array using the 'html_head_link' directive. + */ + public function htmlHeaderLink() { + $render = []; + $render['#attached']['html_head_link'][] = [['href' => '/foo?bar=&baz=false', 'rel' => 'alternate'], TRUE]; + $render['#attached']['html_head_link'][] = [['href' => '/not-added-to-http-headers', 'rel' => 'alternate'], FALSE]; + $render['#attached']['html_head_link'][] = [['href' => '/foo/bar', 'hreflang' => 'nl', 'rel' => 'alternate'], TRUE]; + return $render; + } + } diff --git a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateGlobalThemeSettingsTest.php b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateGlobalThemeSettingsTest.php new file mode 100644 index 000000000..56ee5b631 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateGlobalThemeSettingsTest.php @@ -0,0 +1,49 @@ +executeMigration('d7_global_theme_settings'); + } + + /** + * Tests migration of global theme settings to configuration. + */ + public function testMigrateThemeSettings() { + $config = $this->config('system.theme.global'); + + $this->assertSame('image/png', $config->get('favicon.mimetype')); + $this->assertSame('public://somefavicon.png', $config->get('favicon.path')); + $this->assertFalse($config->get('favicon.use_default')); + + $this->assertFalse($config->get('features.comment_user_picture')); + $this->assertFalse($config->get('features.comment_user_verification')); + $this->assertFalse($config->get('features.favicon')); + $this->assertFalse($config->get('features.node_user_picture')); + $this->assertFalse($config->get('features.logo')); + $this->assertTrue($config->get('features.name')); + $this->assertFalse($config->get('features.slogan')); + + $this->assertSame('public://customlogo.png', $config->get('logo.path')); + $this->assertTrue($config->get('logo.use_default')); + } + +} diff --git a/core/modules/system/tests/src/Unit/Transliteration/MachineNameControllerTest.php b/core/modules/system/tests/src/Unit/Transliteration/MachineNameControllerTest.php index dbd1f1a3b..90696fda4 100644 --- a/core/modules/system/tests/src/Unit/Transliteration/MachineNameControllerTest.php +++ b/core/modules/system/tests/src/Unit/Transliteration/MachineNameControllerTest.php @@ -7,8 +7,8 @@ use Drupal\Tests\UnitTestCase; use Drupal\Component\Transliteration\PhpTransliteration; use Drupal\system\MachineNameController; use Prophecy\Argument; -use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Tests that the machine name controller can transliterate strings as expected. @@ -103,7 +103,7 @@ class MachineNameControllerTest extends UnitTestCase { public function testMachineNameControllerWithInvalidReplacePattern() { $request = Request::create('', 'GET', ['text' => 'Bob', 'langcode' => 'en', 'replace' => 'Alice', 'replace_pattern' => 'Bob', 'replace_token' => 'invalid']); - $this->setExpectedException(AccessDeniedException::class, "Invalid 'replace_token' query parameter."); + $this->setExpectedException(AccessDeniedHttpException::class, "Invalid 'replace_token' query parameter."); $this->machineNameController->transliterate($request); } @@ -113,7 +113,7 @@ class MachineNameControllerTest extends UnitTestCase { public function testMachineNameControllerWithMissingToken() { $request = Request::create('', 'GET', ['text' => 'Bob', 'langcode' => 'en', 'replace' => 'Alice', 'replace_pattern' => 'Bob']); - $this->setExpectedException(AccessDeniedException::class, "Missing 'replace_token' query parameter."); + $this->setExpectedException(AccessDeniedHttpException::class, "Missing 'replace_token' query parameter."); $this->machineNameController->transliterate($request); } diff --git a/core/modules/taxonomy/migration_templates/d6_taxonomy_term.yml b/core/modules/taxonomy/migration_templates/d6_taxonomy_term.yml index 44f9f7fb6..909fb4c11 100644 --- a/core/modules/taxonomy/migration_templates/d6_taxonomy_term.yml +++ b/core/modules/taxonomy/migration_templates/d6_taxonomy_term.yml @@ -5,6 +5,8 @@ migration_tags: source: plugin: taxonomy_term process: + # If you are using this file to build a custom migration consider removing + # the tid field to allow incremental migrations. tid: tid vid: plugin: migration diff --git a/core/modules/taxonomy/migration_templates/d6_taxonomy_vocabulary.yml b/core/modules/taxonomy/migration_templates/d6_taxonomy_vocabulary.yml index 654a07640..f7f588406 100644 --- a/core/modules/taxonomy/migration_templates/d6_taxonomy_vocabulary.yml +++ b/core/modules/taxonomy/migration_templates/d6_taxonomy_vocabulary.yml @@ -14,6 +14,7 @@ process: entity_type: taxonomy_vocabulary field: vid length: 32 + migrated: true label: name name: name description: description diff --git a/core/modules/taxonomy/migration_templates/d6_vocabulary_field.yml b/core/modules/taxonomy/migration_templates/d6_vocabulary_field.yml index a1c97356f..0e1a48728 100644 --- a/core/modules/taxonomy/migration_templates/d6_vocabulary_field.yml +++ b/core/modules/taxonomy/migration_templates/d6_vocabulary_field.yml @@ -22,7 +22,10 @@ process: 'settings/target_type': 'constants/target_entity_type' cardinality: cardinality destination: - plugin: md_entity:field_storage_config + plugin: entity:field_storage_config + dependencies: + module: + - entity_reference migration_dependencies: required: - d6_taxonomy_vocabulary diff --git a/core/modules/taxonomy/migration_templates/d7_taxonomy_term.yml b/core/modules/taxonomy/migration_templates/d7_taxonomy_term.yml index 8d4adbbed..a47ab46b9 100644 --- a/core/modules/taxonomy/migration_templates/d7_taxonomy_term.yml +++ b/core/modules/taxonomy/migration_templates/d7_taxonomy_term.yml @@ -5,6 +5,8 @@ migration_tags: source: plugin: taxonomy_term process: + # If you are using this file to build a custom migration consider removing + # the tid field to allow incremental migrations. tid: tid vid: plugin: migration diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php index 1842a4382..8d69571a5 100644 --- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php +++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php @@ -74,7 +74,7 @@ class TextTrimmedFormatter extends FormatterBase { // because text_pre_render_summary() must run last. $element += \Drupal::service('element_info')->getInfo($element['#type']); // Add the #pre_render callback that renders the text into a summary. - $element['#pre_render'][] = '\Drupal\text\Plugin\field\FieldFormatter\TextTrimmedFormatter::preRenderSummary'; + $element['#pre_render'][] = [TextTrimmedFormatter::class, 'preRenderSummary']; // Pass on the trim length to the #pre_render callback via a property. $element['#text_summary_trim_length'] = $this->getSetting('trim_length'); }; diff --git a/core/modules/user/migration_templates/d6_user.yml b/core/modules/user/migration_templates/d6_user.yml index fa6326e93..c82c65750 100644 --- a/core/modules/user/migration_templates/d6_user.yml +++ b/core/modules/user/migration_templates/d6_user.yml @@ -5,6 +5,8 @@ migration_tags: source: plugin: d6_user process: + # If you are using this file to build a custom migration consider removing + # the uid field to allow incremental migrations. uid: uid name: name pass: pass diff --git a/core/modules/user/migration_templates/d6_user_picture_file.yml b/core/modules/user/migration_templates/d6_user_picture_file.yml index 3518d7a6e..d367f33b9 100644 --- a/core/modules/user/migration_templates/d6_user_picture_file.yml +++ b/core/modules/user/migration_templates/d6_user_picture_file.yml @@ -11,6 +11,9 @@ source: # table are specified, and must end with a /. source_base_path: '' process: + # If you are using both this migration and d6_file in a custom migration + # and executing migrations incrementally, it is recommended that you + # remove the fid mapping from d6_file to avoid potential ID conflicts. filename: filename uid: uid source_full_path: diff --git a/core/modules/user/migration_templates/d7_user.yml b/core/modules/user/migration_templates/d7_user.yml index b72e8c728..3910659fa 100644 --- a/core/modules/user/migration_templates/d7_user.yml +++ b/core/modules/user/migration_templates/d7_user.yml @@ -6,6 +6,8 @@ class: Drupal\user\Plugin\migrate\User source: plugin: d7_user process: + # If you are using this file to build a custom migration consider removing + # the uid field to allow incremental migrations. uid: uid name: name pass: pass diff --git a/core/modules/user/migration_templates/user_picture_field.yml b/core/modules/user/migration_templates/user_picture_field.yml index a484ab8d1..ff8bd8274 100644 --- a/core/modules/user/migration_templates/user_picture_field.yml +++ b/core/modules/user/migration_templates/user_picture_field.yml @@ -18,4 +18,7 @@ process: type: 'constants/type' cardinality: 'constants/cardinality' destination: - plugin: md_entity:field_storage_config + plugin: entity:field_storage_config + dependencies: + module: + - image diff --git a/core/modules/user/migration_templates/user_profile_field.yml b/core/modules/user/migration_templates/user_profile_field.yml index bf8189822..3ba0eee48 100644 --- a/core/modules/user/migration_templates/user_profile_field.yml +++ b/core/modules/user/migration_templates/user_profile_field.yml @@ -32,4 +32,4 @@ process: map: list: -1 destination: - plugin: md_entity:field_storage_config + plugin: entity:field_storage_config diff --git a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php index 356544119..29b97277c 100644 --- a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php +++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php @@ -10,6 +10,7 @@ use GuzzleHttp\Cookie\CookieJar; use Psr\Http\Message\ResponseInterface; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; +use Drupal\hal\Encoder\JsonEncoder as HALJsonEncoder; use Symfony\Component\Serializer\Serializer; /** @@ -19,6 +20,13 @@ use Symfony\Component\Serializer\Serializer; */ class UserLoginHttpTest extends BrowserTestBase { + /** + * Modules to install. + * + * @var array + */ + public static $modules = ['hal']; + /** * The cookie jar. * @@ -39,7 +47,7 @@ class UserLoginHttpTest extends BrowserTestBase { protected function setUp() { parent::setUp(); $this->cookies = new CookieJar(); - $encoders = [new JsonEncoder(), new XmlEncoder()]; + $encoders = [new JsonEncoder(), new XmlEncoder(), new HALJsonEncoder()]; $this->serializer = new Serializer([], $encoders); } @@ -90,7 +98,7 @@ class UserLoginHttpTest extends BrowserTestBase { /** @var \Drupal\Core\Extension\ModuleInstaller $module_installer */ $module_installer = $this->container->get('module_installer'); $module_installer->install(['serialization']); - $formats = ['json', 'xml']; + $formats = ['json', 'xml', 'hal_json']; } else { // Without the serialization module only JSON is supported. diff --git a/core/modules/views/src/Plugin/Block/ViewsBlock.php b/core/modules/views/src/Plugin/Block/ViewsBlock.php index 43ff658f9..ad8d7c914 100644 --- a/core/modules/views/src/Plugin/Block/ViewsBlock.php +++ b/core/modules/views/src/Plugin/Block/ViewsBlock.php @@ -27,11 +27,6 @@ class ViewsBlock extends ViewsBlockBase { // entry for the view output by passing FALSE, because we're going to cache // the whole block instead. if ($output = $this->view->buildRenderable($this->displayID, [], FALSE)) { - // Override the label to the dynamic title configured in the view. - if (empty($this->configuration['views_label']) && $this->view->getTitle()) { - $output['#title'] = ['#markup' => $this->view->getTitle(), '#allowed_tags' => Xss::getHtmlTagList()]; - } - // Before returning the block output, convert it to a renderable array // with contextual links. $this->addContextualLinks($output); @@ -41,6 +36,11 @@ class ViewsBlock extends ViewsBlockBase { // #pre_render callback has already been applied. $output = View::preRenderViewElement($output); + // Override the label to the dynamic title configured in the view. + if (empty($this->configuration['views_label']) && $this->view->getTitle()) { + $output['#title'] = ['#markup' => $this->view->getTitle(), '#allowed_tags' => Xss::getHtmlTagList()]; + } + // When view_build is empty, the actual render array output for this View // is going to be empty. In that case, return just #cache, so that the // render system knows the reasons (cache contexts & tags) why this Views diff --git a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php index 927c1727b..e8833787c 100644 --- a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php +++ b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php @@ -79,7 +79,7 @@ abstract class TokenizeAreaPluginBase extends AreaPluginBase { foreach ($options[$type] as $key => $value) { $items[] = $key . ' == ' . $value; } - $form['tokens']['tokens'] = array( + $form['tokens'][$type]['tokens'] = array( '#theme' => 'item_list', '#items' => $items, ); diff --git a/core/modules/views/src/Plugin/views/display/EntityReference.php b/core/modules/views/src/Plugin/views/display/EntityReference.php index 22c8c8cf8..bf2d46034 100644 --- a/core/modules/views/src/Plugin/views/display/EntityReference.php +++ b/core/modules/views/src/Plugin/views/display/EntityReference.php @@ -122,9 +122,12 @@ class EntityReference extends DisplayPluginBase { // Restrict the autocomplete options based on what's been typed already. if (isset($options['match'])) { $style_options = $this->getOption('style'); - $value = db_like($options['match']) . '%'; - if ($options['match_operator'] != 'STARTS_WITH') { - $value = '%' . $value; + $value = db_like($options['match']); + if ($options['match_operator'] !== '=') { + $value = $value . '%'; + if ($options['match_operator'] != 'STARTS_WITH') { + $value = '%' . $value; + } } // Multiple search fields are OR'd together. diff --git a/core/modules/views/src/Plugin/views/field/EntityOperations.php b/core/modules/views/src/Plugin/views/field/EntityOperations.php index 974d3fbd8..f2a3ba997 100644 --- a/core/modules/views/src/Plugin/views/field/EntityOperations.php +++ b/core/modules/views/src/Plugin/views/field/EntityOperations.php @@ -165,4 +165,11 @@ class EntityOperations extends FieldPluginBase { return $this->view; } + /** + * {@inheritdoc} + */ + public function clickSortable() { + return FALSE; + } + } diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index 103dd9dee..bdb44fafd 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -101,6 +101,13 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf */ protected $renderer; + /** + * Keeps track of the last render index. + * + * @var int|NULL + */ + protected $lastRenderIndex; + /** * {@inheritdoc} */ @@ -1122,6 +1129,10 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf * {@inheritdoc} */ public function advancedRender(ResultRow $values) { + // Clean up values from previous render calls. + if ($this->lastRenderIndex != $values->index) { + $this->last_render_text = ''; + } if ($this->allowAdvancedRender() && $this instanceof MultiItemsFieldHandlerInterface) { $raw_items = $this->getItems($values); // If there are no items, set the original value to NULL. @@ -1181,7 +1192,10 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf $this->last_render = $this->renderText($alter); } } - + // If we rendered something, update the last render index. + if ((string) $this->last_render !== '') { + $this->lastRenderIndex = $values->index; + } return $this->last_render; } @@ -1222,6 +1236,12 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf if (!empty($alter['alter_text']) && $alter['text'] !== '') { $tokens = $this->getRenderTokens($alter); $value = $this->renderAltered($alter, $tokens); + // $alter['text'] is entered through the views admin UI and will be safe + // because the output of $this->renderAltered() is run through + // Xss::filterAdmin(). + // @see \Drupal\views\Plugin\views\PluginBase::viewsTokenReplace() + // @see \Drupal\Component\Utility\Xss::filterAdmin() + $value_is_safe = TRUE; } if (!empty($this->options['alter']['trim_whitespace'])) { diff --git a/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php b/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php index 068ed89b2..3f64626f6 100644 --- a/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php +++ b/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php @@ -1091,7 +1091,8 @@ abstract class WizardPluginBase extends PluginBase implements WizardInterface { $block = $form_state->getValue('block'); $display_options['title'] = $block['title']; $display_options['style'] = array('type' => $block['style']['style_plugin']); - $display_options['row'] = array('type' => isset($block['style']['row_plugin']) ? $block['style']['row_plugin'] : 'fields'); + $options = $this->rowStyleOptions(); + $display_options['row'] = array('type' => (isset($block['style']['row_plugin']) && isset($options[$block['style']['row_plugin']])) ? $block['style']['row_plugin'] : 'fields'); $display_options['pager']['type'] = $block['pager'] ? 'full' : (empty($block['items_per_page']) ? 'none' : 'some'); $display_options['pager']['options']['items_per_page'] = $block['items_per_page']; return $display_options; diff --git a/core/modules/views/src/Tests/Handler/FieldEntityOperationsTest.php b/core/modules/views/src/Tests/Handler/FieldEntityOperationsTest.php index d5adcb409..e76d54e34 100644 --- a/core/modules/views/src/Tests/Handler/FieldEntityOperationsTest.php +++ b/core/modules/views/src/Tests/Handler/FieldEntityOperationsTest.php @@ -25,7 +25,7 @@ class FieldEntityOperationsTest extends HandlerTestBase { * * @var array */ - public static $modules = array('node', 'language'); + public static $modules = array('node', 'language', 'views_ui'); function setUp() { parent::setUp(); @@ -83,6 +83,11 @@ class FieldEntityOperationsTest extends HandlerTestBase { } } } + + // Test that we can't enable click sorting on the operation field. + $this->drupalGet('admin/structure/views/nojs/display/test_entity_operations/page_2/style_options'); + $this->assertField('style_options[info][title][sortable]'); + $this->assertNoField('style_options[info][operations][sortable]'); } } diff --git a/core/modules/views/src/Tests/Plugin/DisplayEntityReferenceTest.php b/core/modules/views/src/Tests/Plugin/DisplayEntityReferenceTest.php index 67d4d4516..96d036926 100644 --- a/core/modules/views/src/Tests/Plugin/DisplayEntityReferenceTest.php +++ b/core/modules/views/src/Tests/Plugin/DisplayEntityReferenceTest.php @@ -103,6 +103,21 @@ class DisplayEntityReferenceTest extends PluginTestBase { $this->fieldName => 'text' . $i, ])->save(); } + EntityTest::create([ + 'bundle' => 'entity_test', + 'name' => 'name', + $this->fieldName => 'tex', + ])->save(); + EntityTest::create([ + 'bundle' => 'entity_test', + 'name' => 'name', + $this->fieldName => 'TEX', + ])->save(); + EntityTest::create([ + 'bundle' => 'entity_test', + 'name' => 'name', + $this->fieldName => 'sometext', + ])->save(); } /** @@ -140,6 +155,48 @@ class DisplayEntityReferenceTest extends PluginTestBase { $this->assertEqual(count($view->result), 2, 'Search returned two rows'); $view->destroy(); + // Test the 'CONTAINS' match_operator. + $view = Views::getView('test_display_entity_reference'); + $view->setDisplay('entity_reference_1'); + $options = [ + 'match' => 'tex', + 'match_operator' => 'CONTAINS', + 'limit' => 0, + 'ids' => NULL, + ]; + $view->display_handler->setOption('entity_reference_options', $options); + $this->executeView($view); + $this->assertEqual(count($view->result), 13, 'Search returned thirteen rows'); + $view->destroy(); + + // Test the 'STARTS_WITH' match_operator. + $view = Views::getView('test_display_entity_reference'); + $view->setDisplay('entity_reference_1'); + $options = [ + 'match' => 'tex', + 'match_operator' => 'STARTS_WITH', + 'limit' => 0, + 'ids' => NULL, + ]; + $view->display_handler->setOption('entity_reference_options', $options); + $this->executeView($view); + $this->assertEqual(count($view->result), 12, 'Search returned twelve rows'); + $view->destroy(); + + // Test the '=' match_operator. + $view = Views::getView('test_display_entity_reference'); + $view->setDisplay('entity_reference_1'); + $options = [ + 'match' => 'tex', + 'match_operator' => '=', + 'limit' => 0, + 'ids' => NULL, + ]; + $view->display_handler->setOption('entity_reference_options', $options); + $this->executeView($view); + $this->assertEqual(count($view->result), 2, 'Search returned two rows'); + $view->destroy(); + // Add a relationship and a field using that relationship. $this->drupalPostForm('admin/structure/views/nojs/add-handler/test_display_entity_reference/default/relationship', ['name[entity_test.user_id]' => TRUE], t('Add and configure relationships')); $this->drupalPostForm(NULL, [], t('Apply')); diff --git a/core/modules/views/src/Tests/Wizard/BasicTest.php b/core/modules/views/src/Tests/Wizard/BasicTest.php index 1e2d05185..00237d79a 100644 --- a/core/modules/views/src/Tests/Wizard/BasicTest.php +++ b/core/modules/views/src/Tests/Wizard/BasicTest.php @@ -170,26 +170,6 @@ class BasicTest extends WizardTestBase { $this->assertEqual($node['nid'][0]['value'], $node1->id(), 'The node of type page is exported.'); } - /** - * Tests the actual wizard form. - * - * @see \Drupal\views_ui\ViewAddForm::form() - */ - public function testWizardForm() { - $this->drupalGet('admin/structure/views/add'); - - $result = $this->xpath('//small[@id = "edit-label-machine-name-suffix"]'); - $this->assertTrue(count($result), 'Ensure that the machine name is applied to the name field.'); - - $this->drupalPostAjaxForm(NULL, array('show[wizard_key]' => 'users'), 'show[wizard_key]'); - $this->assertNoFieldByName('show[type]', NULL, 'The "of type" filter is not added for users.'); - $this->drupalPostAjaxForm(NULL, array('show[wizard_key]' => 'node'), 'show[wizard_key]'); - $this->assertNoFieldByName('show[type]', 'all', 'The "of type" filter is not added for nodes when there are no node types.'); - $this->drupalCreateContentType(array('type' => 'page')); - $this->drupalPostAjaxForm(NULL, array('show[wizard_key]' => 'node'), 'show[wizard_key]'); - $this->assertFieldByName('show[type]', 'all', 'The "of type" filter is added for nodes when there is at least one node type.'); - } - /** * Tests default plugin values are populated from the wizard form. * diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_dropbutton.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_dropbutton.yml index 8605dce89..2c0bb91c3 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_dropbutton.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_dropbutton.yml @@ -21,6 +21,8 @@ display: display_options: access: type: perm + options: + perm: 'access content overview' cache: type: tag query: @@ -159,6 +161,108 @@ display: hide_empty: false empty_zero: false hide_alter_empty: false + edit_node: + id: edit_node + table: node + field: edit_node + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: true + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + text: edit + entity_type: node + plugin_id: entity_link_edit + delete_node: + id: delete_node + table: node + field: delete_node + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: true + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + text: delete + entity_type: node + plugin_id: entity_link_delete dropbutton: id: dropbutton table: views @@ -211,6 +315,8 @@ display: fields: title: title nothing: nothing + edit_node: edit_node + delete_node: delete_node nid: '0' destination: true filters: diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_operations.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_operations.yml index 8276613a3..bb84119f0 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_operations.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_operations.yml @@ -123,3 +123,46 @@ display: position: null display_options: path: test-entity-operations + page_2: + display_plugin: page + id: page_2 + display_title: 'Page 2' + position: null + display_options: + path: test-entity-operations-table + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + title: title + operations: operations + info: + title: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + align: '' + separator: '' + empty_column: false + responsive: '' + default: '-1' + empty_table: false + row: + type: fields + options: { } + display_extenders: { } + defaults: + style: false + row: false diff --git a/core/modules/views/tests/src/Kernel/Handler/FieldDropbuttonTest.php b/core/modules/views/tests/src/Kernel/Handler/FieldDropbuttonTest.php new file mode 100644 index 000000000..b789bd62a --- /dev/null +++ b/core/modules/views/tests/src/Kernel/Handler/FieldDropbuttonTest.php @@ -0,0 +1,167 @@ +installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installSchema('node', 'node_access'); + $this->installConfig('node'); + $this->installConfig('filter'); + + ViewTestData::createTestViews(get_class($this), ['views_test_config']); + // Create two node types. + $this->createContentType(['type' => 'foo']); + $this->createContentType(['type' => 'bar']); + + // Create user 1. + $admin = $this->createUser(); + + // And three nodes. + $this->node1 = $this->createNode([ + 'type' => 'bar', + 'title' => 'bazs', + 'status' => 1, + 'uid' => $admin->id(), + 'created' => REQUEST_TIME - 10, + ]); + $this->node2 = $this->createNode([ + 'type' => 'foo', + 'title' => 'foos', + 'status' => 1, + 'uid' => $admin->id(), + 'created' => REQUEST_TIME - 5, + ]); + $this->node3 = $this->createNode([ + 'type' => 'bar', + 'title' => 'bars', + 'status' => 1, + 'uid' => $admin->id(), + 'created' => REQUEST_TIME, + ]); + + // Now create a user with the ability to edit bar but not foo. + $this->testUser = $this->createUser([ + 'access content overview', + 'access content', + 'edit any bar content', + 'delete any bar content', + ]); + // And switch to that user. + $this->container->get('account_switcher')->switchTo($this->testUser); + } + + /** + * Tests that dropbutton markup doesn't leak between rows. + */ + public function testDropbuttonMarkupShouldNotLeakBetweenRows() { + $view = Views::getView('test_dropbutton'); + $view->setDisplay(); + $view->preExecute([]); + $view->execute(); + + $renderer = $this->container->get('renderer'); + $dropbutton_output = []; + + // Render each row and field in turn - the dropbutton plugin relies on + // output being set in previous versions. + foreach ($view->result as $index => $row) { + foreach (array_keys($view->field) as $field) { + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row, $field) { + return $view->field[$field]->advancedRender($row); + }); + if ($field === 'dropbutton') { + $dropbutton_output[] = $output; + } + } + } + + // The first row should contain edit links to node 3, as the user has + // access. + $this->assertContains($this->node3->toUrl('edit-form')->toString(), (string) $dropbutton_output[0]); + $this->assertContains($this->node3->toUrl('delete-form')->toString(), (string) $dropbutton_output[0]); + + // Second row should be not contain links to edit/delete any content as user + // has no edit/delete permissions. + // It most certainly should not contain links to node 3, as node 2 is the + // entity that forms this row. + $this->assertNotContains($this->node3->toUrl('edit-form')->toString(), (string) $dropbutton_output[1]); + $this->assertNotContains($this->node3->toUrl('delete-form')->toString(), (string) $dropbutton_output[1]); + $this->assertNotContains($this->node2->toUrl('edit-form')->toString(), (string) $dropbutton_output[1]); + $this->assertNotContains($this->node2->toUrl('delete-form')->toString(), (string) $dropbutton_output[1]); + + // Third row should contain links for node 1. + $this->assertContains($this->node1->toUrl('edit-form')->toString(), (string) $dropbutton_output[2]); + $this->assertContains($this->node1->toUrl('delete-form')->toString(), (string) $dropbutton_output[2]); + } + +} diff --git a/core/modules/views/tests/src/Kernel/Handler/FieldKernelTest.php b/core/modules/views/tests/src/Kernel/Handler/FieldKernelTest.php index a57e47869..be8a4f133 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FieldKernelTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FieldKernelTest.php @@ -168,6 +168,67 @@ class FieldKernelTest extends ViewsKernelTestBase { $this->assertSubString($output, $random_text); } + /** + * Tests rewriting of the output with HTML. + */ + public function testRewriteHtmlWithTokens() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + + $view = Views::getView('test_view'); + $view->initHandlers(); + $this->executeView($view); + $row = $view->result[0]; + $id_field = $view->field['id']; + + $id_field->options['alter']['text'] = '

{{ id }}

'; + $id_field->options['alter']['alter_text'] = TRUE; + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); + $this->assertSubString($output, '

1

'); + + // Add a non-safe HTML tag and make sure this gets removed. + $id_field->options['alter']['text'] = '

{{ id }}

'; + $id_field->options['alter']['alter_text'] = TRUE; + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); + $this->assertSubString($output, '

1 alert("Script removed")

'); + } + + /** + * Tests rewriting of the output with HTML and aggregation. + */ + public function testRewriteHtmlWithTokensAndAggregation() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + + $view = Views::getView('test_view'); + $view->setDisplay(); + $view->displayHandlers->get('default')->options['fields']['id']['group_type'] = 'sum'; + $view->displayHandlers->get('default')->setOption('group_by', TRUE); + $view->initHandlers(); + $this->executeView($view); + $row = $view->result[0]; + $id_field = $view->field['id']; + + $id_field->options['alter']['text'] = '

{{ id }}

'; + $id_field->options['alter']['alter_text'] = TRUE; + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); + $this->assertSubString($output, '

1

'); + + // Add a non-safe HTML tag and make sure this gets removed. + $id_field->options['alter']['text'] = '

{{ id }}

'; + $id_field->options['alter']['alter_text'] = TRUE; + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); + $this->assertSubString($output, '

1 alert("Script removed")

'); + } + /** * Tests the arguments tokens on field level. */ diff --git a/core/modules/views/tests/src/Kernel/Plugin/ViewsBlockTest.php b/core/modules/views/tests/src/Kernel/Plugin/ViewsBlockTest.php index f30db33a2..d06ae66ba 100644 --- a/core/modules/views/tests/src/Kernel/Plugin/ViewsBlockTest.php +++ b/core/modules/views/tests/src/Kernel/Plugin/ViewsBlockTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\views\Kernel\Plugin; use Drupal\views\Plugin\Block\ViewsBlock; use Drupal\views\Tests\ViewTestData; use Drupal\Tests\views\Kernel\ViewsKernelTestBase; +use Drupal\views\Views; /** * Tests native behaviors of the block views plugin. @@ -51,4 +52,79 @@ class ViewsBlockTest extends ViewsKernelTestBase { $this->assertEqual($views_block->getMachineNameSuggestion(), 'views_block__test_view_block_block_1'); } + /** + * Tests that ViewsBlock::build() produces the right output with title tokens. + * + * @see \Drupal\views\Plugin\Block::build() + */ + public function testBuildWithTitleToken() { + $view = Views::getView('test_view_block'); + $view->setDisplay(); + + $sorts = [ + 'name' => [ + 'id' => 'name', + 'field' => 'name', + 'table' => 'views_test_data', + 'plugin_id' => 'standard', + 'order' => 'asc', + ], + ]; + // Set the title to the 'name' field in the first row and add a sort order + // for consistent results on different databases. + $view->display_handler->setOption('title', '{{ name }}'); + $view->display_handler->setOption('sorts', $sorts); + $view->save(); + + $plugin_definition = [ + 'provider' => 'views', + ]; + $plugin_id = 'views_block:test_view_block-block_1'; + $views_block = ViewsBlock::create($this->container, [], $plugin_id, $plugin_definition); + + $build = $views_block->build(); + $this->assertEquals('George', $build['#title']['#markup']); + } + + /** + * Tests ViewsBlock::build() with a title override. + * + * @see \Drupal\views\Plugin\Block::build() + */ + public function testBuildWithTitleOverride() { + $view = Views::getView('test_view_block'); + $view->setDisplay(); + + // Add a fixed argument that sets a title and save the view. + $view->displayHandlers->get('default')->overrideOption('arguments', array( + 'name' => array( + 'default_action' => 'default', + 'title_enable' => TRUE, + 'title' => 'Overridden title', + 'default_argument_type' => 'fixed', + 'default_argument_options' => [ + 'argument' => 'fixed' + ], + 'validate' => array( + 'type' => 'none', + 'fail' => 'not found', + ), + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'plugin_id' => 'string', + ) + )); + $view->save(); + + $plugin_definition = [ + 'provider' => 'views', + ]; + $plugin_id = 'views_block:test_view_block-block_1'; + $views_block = ViewsBlock::create($this->container, [], $plugin_id, $plugin_definition); + + $build = $views_block->build(); + $this->assertEquals('Overridden title', $build['#title']['#markup']); + } + } diff --git a/core/modules/views_ui/js/views-admin.js b/core/modules/views_ui/js/views-admin.js index 3cc0f4c1a..761185ed6 100644 --- a/core/modules/views_ui/js/views-admin.js +++ b/core/modules/views_ui/js/views-admin.js @@ -733,12 +733,7 @@ * The event triggered. */ clickAddGroupButton: function (event) { - // Due to conflicts between Drupal core's AJAX system and the Views AJAX - // system, the only way to get this to work seems to be to trigger both - // the mousedown and submit events. - this.addGroupButton - .trigger('mousedown') - .trigger('submit'); + this.addGroupButton.trigger('mousedown'); event.preventDefault(); }, @@ -750,7 +745,7 @@ * form button that should be clicked. */ clickRemoveGroupButton: function (event) { - this.table.find('#' + event.data.buttonId).trigger('mousedown').trigger('submit'); + this.table.find('#' + event.data.buttonId).trigger('mousedown'); event.preventDefault(); }, diff --git a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php index e8eedd750..ea2235b7b 100644 --- a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php +++ b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php @@ -129,6 +129,7 @@ class RearrangeFilter extends ViewsFormBase { 'class' => array('views-remove-group'), ), '#group' => $id, + '#ajax' => ['url' => NULL], ); } $group_options[$id] = $id == 1 ? $this->t('Default group') : $this->t('Group @group', array('@group' => $id)); diff --git a/core/modules/views_ui/src/Tests/TokenizeAreaUITest.php b/core/modules/views_ui/src/Tests/TokenizeAreaUITest.php new file mode 100644 index 000000000..38eb47567 --- /dev/null +++ b/core/modules/views_ui/src/Tests/TokenizeAreaUITest.php @@ -0,0 +1,45 @@ + 'entity_test']); + $entity_test->save(); + + $default = $this->randomView([]); + $id = $default['id']; + $view = View::load($id); + + $this->drupalGet($view->toUrl('edit-form')); + + // Add a global NULL argument to the view for testing argument tokens. + $this->drupalPostForm("admin/structure/views/nojs/add-handler/$id/page_1/argument", ['name[views.null]' => 1], 'Add and configure contextual filters'); + $this->drupalPostForm(NULL, [], 'Apply'); + + $this->drupalPostForm("admin/structure/views/nojs/add-handler/$id/page_1/header", ['name[views.area]' => 'views.area'], 'Add and configure header'); + // Test that field tokens are shown. + $this->assertText('{{ title }} == Content: Title'); + // Test that argument tokens are shown. + $this->assertText('{{ arguments.null }} == Global: Null title'); + } + +} diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php new file mode 100644 index 000000000..d67870d5f --- /dev/null +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php @@ -0,0 +1,71 @@ +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); + } + + /** + * Tests dialog for filter criteria. + */ + public function testFilterCriteriaDialog() { + $this->drupalGet('admin/structure/views/view/content_recent'); + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Use the 'And/Or Rearrange' link for fields to open a dialog. + $dropbutton = $page->find('css', '.views-ui-display-tab-bucket.filter .dropbutton-toggle button'); + $dropbutton->click(); + $add_link = $page->findById('views-rearrange-filter'); + $this->assertTrue($add_link->isVisible(), 'And/Or Rearrange button found.'); + $add_link->click(); + $assert_session->assertWaitOnAjaxRequest(); + + // Add a new filter group. + $create_new_filter_group = $page->findById('views-add-group-link'); + $this->assertTrue($create_new_filter_group->isVisible(), 'Add group link found.'); + $create_new_filter_group->click(); + $assert_session->assertWaitOnAjaxRequest(); + + // Assert the existence of the new filter group by checking the remove group + // link. + $remove_link = $page->findLink('Remove group'); + $this->assertTrue($remove_link->isVisible(), 'New group found.'); + + // Remove the group again and assert the group is not present anymore. + $remove_link->click(); + $assert_session->assertWaitOnAjaxRequest(); + $remove_link = $page->findLink('Remove group'); + $this->assertEmpty($remove_link, 'Remove button not available'); + } + +} diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/LibraryCachingTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/LibraryCachingTest.php index fc98e87ff..4cf51d57f 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/LibraryCachingTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/LibraryCachingTest.php @@ -39,12 +39,12 @@ class LibraryCachingTest extends JavascriptTestBase { $add_link = $page->findById('views-add-field'); $this->assertTrue($add_link->isVisible(), 'Add fields button found.'); $add_link->click(); - $this->getSession()->wait(5000, "jQuery('.ui-dialog-titlebar').length > 0"); + $this->assertJsCondition("jQuery('.ui-dialog-titlebar').length > 0"); // Close the dialog and open it again. No no libraries will be loaded, but a // cache entry will be made for not loading any libraries. $page->pressButton('Close'); $add_link->click(); - $this->getSession()->wait(5000, "jQuery('.ui-dialog-titlebar').length > 0"); + $this->assertJsCondition("jQuery('.ui-dialog-titlebar').length > 0"); $page->pressButton('Close'); // Reload the page. @@ -55,14 +55,14 @@ class LibraryCachingTest extends JavascriptTestBase { $preview = $page->findById('preview-submit'); // The first click will load all the libraries. $preview->click(); - $this->getSession()->wait(5000, "jQuery('.ajax-progress').length === 0"); + $this->assertJsCondition("jQuery('.ajax-progress').length === 0"); // The second click will not load any new libraries. $preview->click(); - $this->getSession()->wait(5000, "jQuery('.ajax-progress').length === 0"); + $this->assertJsCondition("jQuery('.ajax-progress').length === 0"); // Check to see if the dialogs still open. $add_link = $page->findById('views-add-field'); $add_link->click(); - $this->getSession()->wait(5000, "jQuery('.ui-dialog-titlebar').length > 0"); + $this->assertJsCondition("jQuery('.ui-dialog-titlebar').length > 0"); $page->pressButton('Close'); } diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsWizardTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsWizardTest.php index 6320f69ed..3fb1167e1 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsWizardTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsWizardTest.php @@ -15,7 +15,7 @@ class ViewsWizardTest extends JavascriptTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'views', 'views_ui', 'block']; + public static $modules = ['node', 'views', 'views_ui', 'block', 'user']; /** * {@inheritdoc} @@ -56,6 +56,27 @@ class ViewsWizardTest extends JavascriptTestBase { // Add a block display. $page->findField('block[create]')->click(); $this->assertEquals($label_value, $page->findField('block[title]')->getValue()); + + // Select the entity type to display and test that the type selector is + // shown when expected. + $page->selectFieldOption('show[wizard_key]', 'node'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertNull($page->findField('show[type]'), 'The "of type" filter is not added for nodes when there are no node types.'); + $this->assertEquals('teasers', $page->findField('page[style][row_plugin]')->getValue(), 'The page display format shows the expected default value.'); + $this->assertEquals('titles_linked', $page->findField('block[style][row_plugin]')->getValue(), 'The block display format shows the expected default value.'); + + $page->selectFieldOption('show[wizard_key]', 'users'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertNull($page->findField('show[type]'), 'The "of type" filter is not added for users.'); + $this->assertEquals('fields', $page->findField('page[style][row_plugin]')->getValue(), 'The page display format was updated to a valid value.'); + $this->assertEquals('fields', $page->findField('block[style][row_plugin]')->getValue(), 'The block display format was updated to a valid value.'); + + $this->drupalCreateContentType(['type' => 'page']); + $page->selectFieldOption('show[wizard_key]', 'node'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertNotNull($page->findField('show[type]'), 'The "of type" filter is added for nodes when there is at least one node type.'); + $this->assertEquals('fields', $page->findField('page[style][row_plugin]')->getValue(), 'The page display format was not changed from a valid value.'); + $this->assertEquals('fields', $page->findField('block[style][row_plugin]')->getValue(), 'The block display format was not changed from a valid value.'); } } diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php index 23c2f0bb9..10aa88e92 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php @@ -86,7 +86,7 @@ class MachineNameTest extends JavascriptTestBase { $title_1->setValue($test_info['input']); // Wait the set timeout for fetching the machine name. - $this->getSession()->wait(1000, 'jQuery("#edit-machine-name-1-label-machine-name-suffix .machine-name-value").html() == "' . $test_info['expected'] . '"'); + $this->assertJsCondition('jQuery("#edit-machine-name-1-label-machine-name-suffix .machine-name-value").html() == "' . $test_info['expected'] . '"'); // Validate the generated machine name. $this->assertEquals($test_info['expected'], $machine_name_1_value->getHtml(), $test_info['message']); diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php index 092d0cdb8..d5156eeb5 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php @@ -47,16 +47,18 @@ abstract class JavascriptTestBase extends BrowserTestBase { * {@inheritdoc} */ protected function tearDown() { - // Wait for all requests to finish. It is possible that an AJAX request is - // still on-going. - $result = $this->getSession()->wait(5000, '(typeof(jQuery)=="undefined" || (0 === jQuery.active && 0 === jQuery(\':animated\').length))'); - if (!$result) { - // If the wait is unsuccessful, there may still be an AJAX request in - // progress. If we tear down now, then this AJAX request may fail with - // missing database tables, because tear down will have removed them. Rather - // than allow it to fail, throw an explicit exception now explaining what - // the problem is. - throw new \RuntimeException('Unfinished AJAX requests whilst tearing down a test'); + if ($this->mink) { + // Wait for all requests to finish. It is possible that an AJAX request is + // still on-going. + $result = $this->getSession()->wait(5000, '(typeof(jQuery)=="undefined" || (0 === jQuery.active && 0 === jQuery(\':animated\').length))'); + if (!$result) { + // If the wait is unsuccessful, there may still be an AJAX request in + // progress. If we tear down now, then this AJAX request may fail with + // missing database tables, because tear down will have removed them. + // Rather than allow it to fail, throw an explicit exception now + // explaining what the problem is. + throw new \RuntimeException('Unfinished AJAX requests while tearing down a test'); + } } parent::tearDown(); } @@ -97,7 +99,7 @@ abstract class JavascriptTestBase extends BrowserTestBase { * @param string $condition * JS condition to wait until it becomes TRUE. * @param int $timeout - * (Optional) Timeout in milliseconds, defaults to 1000. + * (Optional) Timeout in milliseconds, defaults to 10000. * @param string $message * (optional) A message to display with the assertion. If left blank, a * default message will be displayed. @@ -106,7 +108,7 @@ abstract class JavascriptTestBase extends BrowserTestBase { * * @see \Behat\Mink\Driver\DriverInterface::evaluateScript() */ - protected function assertJsCondition($condition, $timeout = 1000, $message = '') { + protected function assertJsCondition($condition, $timeout = 10000, $message = '') { $message = $message ?: "Javascript condition met:\n" . $condition; $result = $this->getSession()->getDriver()->wait($timeout, $condition); $this->assertTrue($result, $message); @@ -143,4 +145,28 @@ abstract class JavascriptTestBase extends BrowserTestBase { return new JSWebAssert($this->getSession($name), $this->baseUrl); } + /** + * Gets the current Drupal javascript settings and parses into an array. + * + * Unlike BrowserTestBase::getDrupalSettings(), this implementation reads the + * current values of drupalSettings, capturing all changes made via javascript + * after the page was loaded. + * + * @return array + * The Drupal javascript settings array. + * + * @see \Drupal\Tests\BrowserTestBase::getDrupalSettings() + */ + protected function getDrupalSettings() { + $script = <<getSession()->evaluateScript($script) ?: []; + } + } diff --git a/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormCorrectUserInputMappingOnFieldDeltaElementsTest.php b/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormCorrectUserInputMappingOnFieldDeltaElementsTest.php new file mode 100644 index 000000000..9f1468ba6 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormCorrectUserInputMappingOnFieldDeltaElementsTest.php @@ -0,0 +1,117 @@ +drupalCreateUser(['administer entity_test content']); + $this->drupalLogin($web_user); + + // Create a field of field type "shape" with unlimited cardinality on the + // entity type "entity_test". + $this->entityTypeId = 'entity_test'; + $this->fieldName = 'shape'; + + FieldStorageConfig::create([ + 'field_name' => $this->fieldName, + 'entity_type' => $this->entityTypeId, + 'type' => 'shape', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ]) + ->save(); + FieldConfig::create([ + 'entity_type' => $this->entityTypeId, + 'field_name' => $this->fieldName, + 'bundle' => $this->entityTypeId, + 'label' => 'Shape', + 'translatable' => FALSE, + ]) + ->save(); + + entity_get_form_display($this->entityTypeId, $this->entityTypeId, 'default') + ->setComponent($this->fieldName, ['type' => 'shape_only_color_editable_widget']) + ->save(); + } + + /** + * Tests the correct user input mapping on complex fields. + */ + public function testCorrectUserInputMappingOnComplexFields() { + /** @var ContentEntityStorageInterface $storage */ + $storage = $this->container->get('entity_type.manager')->getStorage($this->entityTypeId); + + /** @var ContentEntityInterface $entity */ + $entity = $storage->create([$this->fieldName => [ + ['shape' => 'rectangle', 'color' => 'green'], + ['shape' => 'circle', 'color' => 'blue'], + ]]); + $entity->save(); + + $this->drupalGet($this->entityTypeId . '/manage/' . $entity->id() . '/edit'); + + // Rearrange the field items. + $edit = array( + "$this->fieldName[0][_weight]" => 0, + "$this->fieldName[1][_weight]" => -1, + ); + // Executing an ajax call is important before saving as it will trigger + // form state caching and so if for any reasons the form is rebuilt with + // the entity built based on the user submitted values with already + // reordered field items then the correct mapping will break after the form + // builder maps over the new form the user submitted values based on the + // previous delta ordering. + // + // This is how currently the form building process works and this test + // ensures the correct behavior no matter what changes would be made to the + // form builder or the content entity forms. + $this->drupalPostForm(NULL, $edit, t('Add another item')); + $this->drupalPostForm(NULL, [], t('Save')); + + // Reload the entity. + $entity = $storage->load($entity->id()); + + // Assert that after rearranging the field items the user input will be + // mapped on the correct delta field items. + $this->assertEquals($entity->get($this->fieldName)->getValue(), [ + ['shape' => 'circle', 'color' => 'blue'], + ['shape' => 'rectangle', 'color' => 'green'], + ]); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/Element/EntityAutocompleteElementFormTest.php b/core/tests/Drupal/KernelTests/Core/Entity/Element/EntityAutocompleteElementFormTest.php index ec81f7b38..c2d737bfb 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/Element/EntityAutocompleteElementFormTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/Element/EntityAutocompleteElementFormTest.php @@ -8,6 +8,7 @@ use Drupal\Core\Form\FormInterface; use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; use Drupal\entity_test\Entity\EntityTest; +use Drupal\entity_test\Entity\EntityTestStringId; use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; use Drupal\user\Entity\User; @@ -46,6 +47,7 @@ class EntityAutocompleteElementFormTest extends EntityKernelTestBase implements parent::setUp(); $this->installSchema('system', ['key_value_expire']); + $this->installEntitySchema('entity_test_string_id'); \Drupal::service('router.builder')->rebuild(); $this->testUser = User::create(array( @@ -68,6 +70,17 @@ class EntityAutocompleteElementFormTest extends EntityKernelTestBase implements $entity->save(); $this->referencedEntities[] = $entity; } + + // Use special characters in the ID of some of the test entities so we can + // test if these are handled correctly. + for ($i = 0; $i < 2; $i++) { + $entity = EntityTestStringId::create([ + 'name' => $this->randomMachineName(), + 'id' => $this->randomMachineName() . '&save(); + $this->referencedEntities[] = $entity; + } } /** @@ -150,6 +163,16 @@ class EntityAutocompleteElementFormTest extends EntityKernelTestBase implements '#default_value' => array($this->referencedEntities[0], $this->referencedEntities[1]), ); + $form['single_string_id'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test_string_id', + ); + $form['tags_string_id'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test_string_id', + '#tags' => TRUE, + ); + return $form; } @@ -181,6 +204,8 @@ class EntityAutocompleteElementFormTest extends EntityKernelTestBase implements $this->getAutocompleteInput($this->referencedEntities[0]) . ', tags - autocreated entity label with specific uid, ' . $this->getAutocompleteInput($this->referencedEntities[1]), + 'single_string_id' => $this->getAutocompleteInput($this->referencedEntities[2]), + 'tags_string_id' => $this->getAutocompleteInput($this->referencedEntities[2]) . ', ' . $this->getAutocompleteInput($this->referencedEntities[3]), ]); $form_builder = $this->container->get('form_builder'); $form_builder->submitForm($this, $form_state); @@ -231,6 +256,16 @@ class EntityAutocompleteElementFormTest extends EntityKernelTestBase implements $this->assertEqual($value[1]['entity']->getOwnerId(), $this->testAutocreateUser->id()); // Third value is an existing entity. $this->assertEqual($value[2]['target_id'], $this->referencedEntities[1]->id()); + + // Test the 'single_string_id' element. + $this->assertEquals($this->referencedEntities[2]->id(), $form_state->getValue('single_string_id')); + + // Test the 'tags_string_id' element. + $expected = [ + ['target_id' => $this->referencedEntities[2]->id()], + ['target_id' => $this->referencedEntities[3]->id()], + ]; + $this->assertEquals($expected, $form_state->getValue('tags_string_id')); } /** diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryRelationshipTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryRelationshipTest.php index 846543820..60dc13c67 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryRelationshipTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryRelationshipTest.php @@ -1,6 +1,8 @@ queryResults = $this->factory->get('entity_test') ->condition("user_id.entity.name", $this->accounts[0]->getUsername()) @@ -154,6 +156,56 @@ class EntityQueryRelationshipTest extends EntityKernelTestBase { ->condition("$this->fieldName.entity.name", $this->terms[0]->name->value, '<>') ->execute(); $this->assertResults(array(1, 2)); + // This returns the 0th entity as that's only one pointing to the 0th + // account. + $this->queryResults = $this->factory->get('entity_test') + ->condition("user_id.entity:user.name", $this->accounts[0]->getUsername()) + ->execute(); + $this->assertResults(array(0)); + // This returns the 1st and 2nd entity as those point to the 1st account. + $this->queryResults = $this->factory->get('entity_test') + ->condition("user_id.entity:user.name", $this->accounts[0]->getUsername(), '<>') + ->execute(); + $this->assertResults(array(1, 2)); + // This returns all three entities because all of them point to an + // account. + $this->queryResults = $this->factory->get('entity_test') + ->exists("user_id.entity:user.name") + ->execute(); + $this->assertResults(array(0, 1, 2)); + // This returns no entities because all of them point to an account. + $this->queryResults = $this->factory->get('entity_test') + ->notExists("user_id.entity:user.name") + ->execute(); + $this->assertEqual(count($this->queryResults), 0); + // This returns the 0th entity as that's only one pointing to the 0th + // term (test without specifying the field column). + $this->queryResults = $this->factory->get('entity_test') + ->condition("$this->fieldName.entity:taxonomy_term.name", $this->terms[0]->name->value) + ->execute(); + $this->assertResults(array(0)); + // This returns the 0th entity as that's only one pointing to the 0th + // term (test with specifying the column name). + $this->queryResults = $this->factory->get('entity_test') + ->condition("$this->fieldName.target_id.entity:taxonomy_term.name", $this->terms[0]->name->value) + ->execute(); + $this->assertResults(array(0)); + // This returns the 1st and 2nd entity as those point to the 1st term. + $this->queryResults = $this->factory->get('entity_test') + ->condition("$this->fieldName.entity:taxonomy_term.name", $this->terms[0]->name->value, '<>') + ->execute(); + $this->assertResults(array(1, 2)); + } + + /** + * Tests the invalid specifier in the query relationship. + */ + public function testInvalidSpecifier() { + $this->setExpectedException(PluginNotFoundException::class); + $this->factory + ->get('taxonomy_term') + ->condition('langcode.language.foo', 'bar') + ->execute(); } /** diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewBuilderTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewBuilderTest.php index 156b799d9..0143128f1 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewBuilderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewBuilderTest.php @@ -2,6 +2,7 @@ namespace Drupal\KernelTests\Core\Entity; +use Drupal\Core\Entity\EntityViewBuilder; use Drupal\Core\Language\LanguageInterface; use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; use Drupal\Core\Cache\Cache; @@ -210,4 +211,23 @@ class EntityViewBuilderTest extends EntityKernelTestBase { return $this->container->get('entity.manager')->getStorage($entity_type)->create($data); } + /** + * Tests that viewing an entity without template does not specify #theme. + */ + public function testNoTemplate() { + // Ensure that an entity type without explicit view builder uses the + // default. + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_type = $entity_type_manager->getDefinition('entity_test_base_field_display'); + $this->assertTrue($entity_type->hasViewBuilderClass()); + $this->assertEquals(EntityViewBuilder::class, $entity_type->getViewBuilderClass()); + + // Ensure that an entity without matching template does not have a #theme + // key. + $entity = $this->createTestEntity('entity_test'); + $build = $entity_type_manager->getViewBuilder('entity_test')->view($entity); + $this->assertEquals($entity, $build['#entity_test']); + $this->assertFalse(array_key_exists('#theme', $build)); + } + } diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index 8d0cd39a0..44908bb2f 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -258,7 +258,9 @@ abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase { /** * Mink session manager. * - * @var \Behat\Mink\Mink + * This will not be initialized if there was an error during the test setup. + * + * @var \Behat\Mink\Mink|null */ protected $mink; diff --git a/core/tests/Drupal/Tests/Component/Diff/DiffFormatterTest.php b/core/tests/Drupal/Tests/Component/Diff/DiffFormatterTest.php new file mode 100644 index 000000000..70fb0dcf1 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Diff/DiffFormatterTest.php @@ -0,0 +1,57 @@ + ['', [], []], + 'add' => [ + "3a3\n> line2a\n", + ['line1', 'line2', 'line3'], + ['line1', 'line2', 'line2a', 'line3'], + ], + 'delete' => [ + "3d3\n< line2a\n", + ['line1', 'line2', 'line2a', 'line3'], + ['line1', 'line2', 'line3'], + ], + 'change' => [ + "3c3\n< line2a\n---\n> line2b\n", + ['line1', 'line2', 'line2a', 'line3'], + ['line1', 'line2', 'line2b', 'line3'], + ], + ]; + } + + /** + * Tests whether op classes returned by DiffEngine::diff() match expectations. + * + * @covers ::format + * @dataProvider provideTestDiff + */ + public function testDiff($expected, $from, $to) { + $diff = new Diff($from, $to); + $formatter = new DiffFormatter(); + $output = $formatter->format($diff); + $this->assertEquals($expected, $output); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php index 6f020237f..62310a190 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php @@ -2,11 +2,27 @@ namespace Drupal\Tests\Core\Config\Entity; +use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Drupal\Core\Config\Config; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Config\Entity\ConfigEntityStorage; +use Drupal\Core\Config\Entity\ConfigEntityType; +use Drupal\Core\Config\ImmutableConfig; +use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\Query\QueryFactoryInterface; +use Drupal\Core\Entity\Query\QueryInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; use Symfony\Component\DependencyInjection\ContainerBuilder; /** @@ -15,13 +31,6 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; */ class ConfigEntityStorageTest extends UnitTestCase { - /** - * The entity type. - * - * @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $entityType; - /** * The type ID of the entity under test. * @@ -32,21 +41,21 @@ class ConfigEntityStorageTest extends UnitTestCase { /** * The module handler. * - * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface */ protected $moduleHandler; /** * The UUID service. * - * @var \Drupal\Component\Uuid\UuidInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Component\Uuid\UuidInterface|\Prophecy\Prophecy\ProphecyInterface */ protected $uuidService; /** * The language manager. * - * @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Language\LanguageManagerInterface|\Prophecy\Prophecy\ProphecyInterface */ protected $languageManager; @@ -60,52 +69,31 @@ class ConfigEntityStorageTest extends UnitTestCase { /** * The config factory service. * - * @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Config\ConfigFactoryInterface|\Prophecy\Prophecy\ProphecyInterface */ protected $configFactory; /** * The entity query. * - * @var \Drupal\Core\Entity\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Entity\Query\QueryInterface|\Prophecy\Prophecy\ProphecyInterface */ protected $entityQuery; - /** - * The entity manager used for testing. - * - * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $entityManager; - /** * The mocked cache backend. * - * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface|\Prophecy\Prophecy\ProphecyInterface */ protected $cacheTagsInvalidator; - /** - * The mocked typed config manager. - * - * @var \Drupal\Core\Config\TypedConfigManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $typedConfigManager; - /** * The configuration manager. * - * @var \Drupal\Core\Config\ConfigManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Config\ConfigManagerInterface|\Prophecy\Prophecy\ProphecyInterface */ protected $configManager; - /** - * The cache contexts manager. - * - * @var \Drupal\Core\Cache\Context\CacheContextsManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cacheContextsManager; - /** * {@inheritdoc} * @@ -114,76 +102,56 @@ class ConfigEntityStorageTest extends UnitTestCase { protected function setUp() { parent::setUp(); - $this->entityType = $this->getMock('Drupal\Core\Config\Entity\ConfigEntityTypeInterface'); $this->entityTypeId = 'test_entity_type'; - $this->entityType->expects($this->any()) - ->method('getKey') - ->will($this->returnValueMap(array( - array('id', 'id'), - array('uuid', 'uuid'), - array('langcode', 'langcode'), - ))); - $this->entityType->expects($this->any()) - ->method('id') - ->will($this->returnValue($this->entityTypeId)); - $this->entityType->expects($this->any()) - ->method('getConfigPrefix') - ->will($this->returnValue('the_config_prefix')); - $this->entityType->expects($this->any()) - ->method('getClass') - ->will($this->returnValue(get_class($this->getMockEntity()))); - $this->entityType->expects($this->any()) - ->method('getListCacheTags') - ->willReturn(array('test_entity_type_list')); - $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $entity_type = new ConfigEntityType([ + 'id' => $this->entityTypeId, + 'class' => get_class($this->getMockEntity()), + 'provider' => 'the_provider', + 'config_prefix' => 'the_config_prefix', + 'entity_keys' => [ + 'id' => 'id', + 'uuid' => 'uuid', + 'langcode' => 'langcode', + ], + 'list_cache_tags' => [$this->entityTypeId . '_list'], + ]); - $this->uuidService = $this->getMock('Drupal\Component\Uuid\UuidInterface'); + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); - $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); - $this->languageManager->expects($this->any()) - ->method('getCurrentLanguage') - ->willReturn(new Language(array('id' => 'hu'))); + $this->uuidService = $this->prophesize(UuidInterface::class); - $this->configFactory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface'); + $this->languageManager = $this->prophesize(LanguageManagerInterface::class); + $this->languageManager->getCurrentLanguage()->willReturn(new Language(['id' => 'hu'])); - $this->entityQuery = $this->getMock('Drupal\Core\Entity\Query\QueryInterface'); + $this->configFactory = $this->prophesize(ConfigFactoryInterface::class); - $this->entityStorage = $this->getMockBuilder('Drupal\Core\Config\Entity\ConfigEntityStorage') - ->setConstructorArgs(array($this->entityType, $this->configFactory, $this->uuidService, $this->languageManager)) - ->setMethods(array('getQuery')) - ->getMock(); - $this->entityStorage->expects($this->any()) - ->method('getQuery') - ->will($this->returnValue($this->entityQuery)); - $this->entityStorage->setModuleHandler($this->moduleHandler); + $this->entityQuery = $this->prophesize(QueryInterface::class); + $entity_query_factory = $this->prophesize(QueryFactoryInterface::class); + $entity_query_factory->get($entity_type, 'AND')->willReturn($this->entityQuery->reveal()); - $this->entityManager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface'); - $this->entityManager->expects($this->any()) - ->method('getDefinition') - ->with('test_entity_type') - ->will($this->returnValue($this->entityType)); + $this->entityStorage = new ConfigEntityStorage($entity_type, $this->configFactory->reveal(), $this->uuidService->reveal(), $this->languageManager->reveal()); + $this->entityStorage->setModuleHandler($this->moduleHandler->reveal()); - $this->cacheTagsInvalidator = $this->getMock('Drupal\Core\Cache\CacheTagsInvalidatorInterface'); + $entity_manager = $this->prophesize(EntityManagerInterface::class); + $entity_manager->getDefinition('test_entity_type')->willReturn($entity_type); - $this->typedConfigManager = $this->getMock('Drupal\Core\Config\TypedConfigManagerInterface'); - $this->typedConfigManager->expects($this->any()) - ->method('getDefinition') - ->will($this->returnValue(array('mapping' => array('id' => '', 'uuid' => '', 'dependencies' => '')))); + $this->cacheTagsInvalidator = $this->prophesize(CacheTagsInvalidatorInterface::class); - $this->configManager = $this->getMock('Drupal\Core\Config\ConfigManagerInterface'); + $typed_config_manager = $this->prophesize(TypedConfigManagerInterface::class); + $typed_config_manager + ->getDefinition(Argument::containingString('the_provider.the_config_prefix.')) + ->willReturn(['mapping' => ['id' => '', 'uuid' => '', 'dependencies' => '']]); - $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager') - ->disableOriginalConstructor() - ->getMock(); + $this->configManager = $this->prophesize(ConfigManagerInterface::class); $container = new ContainerBuilder(); - $container->set('entity.manager', $this->entityManager); - $container->set('config.typed', $this->typedConfigManager); - $container->set('cache_tags.invalidator', $this->cacheTagsInvalidator); - $container->set('config.manager', $this->configManager); - $container->set('language_manager', $this->languageManager); - $container->set('cache_contexts_manager', $this->cacheContextsManager); + $container->set('entity.manager', $entity_manager->reveal()); + $container->set('entity.query.config', $entity_query_factory->reveal()); + $container->set('config.typed', $typed_config_manager->reveal()); + $container->set('cache_tags.invalidator', $this->cacheTagsInvalidator->reveal()); + $container->set('config.manager', $this->configManager->reveal()); + $container->set('language_manager', $this->languageManager->reveal()); \Drupal::setContainer($container); } @@ -193,20 +161,24 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::doCreate */ public function testCreateWithPredefinedUuid() { - $this->cacheTagsInvalidator->expects($this->never()) - ->method('invalidateTags'); + $this->cacheTagsInvalidator->invalidateTags(Argument::cetera())->shouldNotBeCalled(); - $this->moduleHandler->expects($this->at(0)) - ->method('invokeAll') - ->with('test_entity_type_create'); - $this->moduleHandler->expects($this->at(1)) - ->method('invokeAll') - ->with('entity_create'); - $this->uuidService->expects($this->never()) - ->method('generate'); + $entity = $this->getMockEntity(); + $entity->set('id', 'foo'); + $entity->set('langcode', 'hu'); + $entity->set('uuid', 'baz'); + $entity->setOriginalId('foo'); + $entity->enforceIsNew(); + + $this->moduleHandler->invokeAll('test_entity_type_create', [$entity]) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('entity_create', [$entity, 'test_entity_type']) + ->shouldBeCalled(); + + $this->uuidService->generate()->shouldNotBeCalled(); $entity = $this->entityStorage->create(array('id' => 'foo', 'uuid' => 'baz')); - $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); + $this->assertInstanceOf(EntityInterface::class, $entity); $this->assertSame('foo', $entity->id()); $this->assertSame('baz', $entity->uuid()); } @@ -218,21 +190,24 @@ class ConfigEntityStorageTest extends UnitTestCase { * @return \Drupal\Core\Entity\EntityInterface */ public function testCreate() { - $this->cacheTagsInvalidator->expects($this->never()) - ->method('invalidateTags'); + $this->cacheTagsInvalidator->invalidateTags(Argument::cetera())->shouldNotBeCalled(); - $this->moduleHandler->expects($this->at(0)) - ->method('invokeAll') - ->with('test_entity_type_create'); - $this->moduleHandler->expects($this->at(1)) - ->method('invokeAll') - ->with('entity_create'); - $this->uuidService->expects($this->once()) - ->method('generate') - ->will($this->returnValue('bar')); + $entity = $this->getMockEntity(); + $entity->set('id', 'foo'); + $entity->set('langcode', 'hu'); + $entity->set('uuid', 'bar'); + $entity->setOriginalId('foo'); + $entity->enforceIsNew(); + + $this->moduleHandler->invokeAll('test_entity_type_create', [$entity]) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('entity_create', [$entity, 'test_entity_type']) + ->shouldBeCalled(); + + $this->uuidService->generate()->willReturn('bar'); $entity = $this->entityStorage->create(array('id' => 'foo')); - $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); + $this->assertInstanceOf(EntityInterface::class, $entity); $this->assertSame('foo', $entity->id()); $this->assertSame('bar', $entity->uuid()); return $entity; @@ -243,10 +218,7 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::doCreate */ public function testCreateWithCurrentLanguage() { - $this->languageManager->expects($this->any()) - ->method('getLanguage') - ->with('hu') - ->willReturn(new Language(array('id' => 'hu'))); + $this->languageManager->getLanguage('hu')->willReturn(new Language(['id' => 'hu'])); $entity = $this->entityStorage->create(array('id' => 'foo')); $this->assertSame('hu', $entity->language()->getId()); @@ -257,10 +229,7 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::doCreate */ public function testCreateWithExplicitLanguage() { - $this->languageManager->expects($this->any()) - ->method('getLanguage') - ->with('en') - ->willReturn(new Language(array('id' => 'en'))); + $this->languageManager->getLanguage('en')->willReturn(new Language(['id' => 'en'])); $entity = $this->entityStorage->create(array('id' => 'foo', 'langcode' => 'en')); $this->assertSame('en', $entity->language()->getId()); @@ -277,56 +246,34 @@ class ConfigEntityStorageTest extends UnitTestCase { * @depends testCreate */ public function testSaveInsert(EntityInterface $entity) { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->atLeastOnce()) - ->method('isNew') - ->will($this->returnValue(TRUE)); - $config_object->expects($this->exactly(1)) - ->method('setData'); - $config_object->expects($this->once()) - ->method('save'); - $config_object->expects($this->atLeastOnce()) - ->method('get') - ->willReturn([]); + $immutable_config_object = $this->prophesize(ImmutableConfig::class); + $immutable_config_object->isNew()->willReturn(TRUE); - $this->cacheTagsInvalidator->expects($this->once()) - ->method('invalidateTags') - ->with(array( - $this->entityTypeId . '_list', // List cache tag. - )); + $config_object = $this->prophesize(Config::class); + $config_object->setData(['id' => 'foo', 'uuid' => 'bar', 'dependencies' => []]) + ->shouldBeCalled(); + $config_object->save(FALSE)->shouldBeCalled(); + $config_object->get()->willReturn([]); - $this->configFactory->expects($this->exactly(1)) - ->method('get') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); + $this->cacheTagsInvalidator->invalidateTags([$this->entityTypeId . '_list']) + ->shouldBeCalled(); - $this->configFactory->expects($this->exactly(1)) - ->method('getEditable') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); + $this->configFactory->get('the_provider.the_config_prefix.foo') + ->willReturn($immutable_config_object->reveal()); + $this->configFactory->getEditable('the_provider.the_config_prefix.foo') + ->willReturn($config_object->reveal()); - $this->moduleHandler->expects($this->at(0)) - ->method('invokeAll') - ->with('test_entity_type_presave'); - $this->moduleHandler->expects($this->at(1)) - ->method('invokeAll') - ->with('entity_presave'); - $this->moduleHandler->expects($this->at(2)) - ->method('invokeAll') - ->with('test_entity_type_insert'); - $this->moduleHandler->expects($this->at(3)) - ->method('invokeAll') - ->with('entity_insert'); + $this->moduleHandler->invokeAll('test_entity_type_presave', [$entity]) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('entity_presave', [$entity, 'test_entity_type']) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('test_entity_type_insert', [$entity]) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('entity_insert', [$entity, 'test_entity_type']) + ->shouldBeCalled(); - $this->entityQuery->expects($this->once()) - ->method('condition') - ->with('uuid', 'bar') - ->will($this->returnSelf()); - $this->entityQuery->expects($this->once()) - ->method('execute') - ->will($this->returnValue(array())); + $this->entityQuery->condition('uuid', 'bar')->willReturn($this->entityQuery); + $this->entityQuery->execute()->willReturn([]); $return = $this->entityStorage->save($entity); $this->assertSame(SAVED_NEW, $return); @@ -344,61 +291,41 @@ class ConfigEntityStorageTest extends UnitTestCase { * @depends testSaveInsert */ public function testSaveUpdate(EntityInterface $entity) { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->atLeastOnce()) - ->method('isNew') - ->will($this->returnValue(FALSE)); - $config_object->expects($this->exactly(1)) - ->method('setData'); - $config_object->expects($this->once()) - ->method('save'); - $config_object->expects($this->atLeastOnce()) - ->method('get') - ->willReturn([]); + $immutable_config_object = $this->prophesize(ImmutableConfig::class); + $immutable_config_object->isNew()->willReturn(FALSE); - $this->cacheTagsInvalidator->expects($this->once()) - ->method('invalidateTags') - ->with(array( - // List cache tag only; the own cache tag is invalidated by the config - // system. - $this->entityTypeId . '_list', - )); + $config_object = $this->prophesize(Config::class); + $config_object->setData(['id' => 'foo', 'uuid' => 'bar', 'dependencies' => []]) + ->shouldBeCalled(); + $config_object->save(FALSE)->shouldBeCalled(); + $config_object->get()->willReturn([]); - $this->configFactory->expects($this->exactly(2)) - ->method('loadMultiple') - ->with(array('the_config_prefix.foo')) - ->will($this->returnValue(array())); - $this->configFactory->expects($this->exactly(1)) - ->method('get') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); - $this->configFactory->expects($this->exactly(1)) - ->method('getEditable') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); + $this->cacheTagsInvalidator->invalidateTags([$this->entityTypeId . '_list']) + ->shouldBeCalled(); - $this->moduleHandler->expects($this->at(0)) - ->method('invokeAll') - ->with('test_entity_type_presave'); - $this->moduleHandler->expects($this->at(1)) - ->method('invokeAll') - ->with('entity_presave'); - $this->moduleHandler->expects($this->at(2)) - ->method('invokeAll') - ->with('test_entity_type_update'); - $this->moduleHandler->expects($this->at(3)) - ->method('invokeAll') - ->with('entity_update'); + $this->configFactory->loadMultiple(['the_provider.the_config_prefix.foo']) + ->willReturn([]) + ->shouldBeCalledTimes(2); + $this->configFactory + ->get('the_provider.the_config_prefix.foo') + ->willReturn($immutable_config_object->reveal()) + ->shouldBeCalledTimes(1); + $this->configFactory + ->getEditable('the_provider.the_config_prefix.foo') + ->willReturn($config_object->reveal()) + ->shouldBeCalledTimes(1); - $this->entityQuery->expects($this->once()) - ->method('condition') - ->with('uuid', 'bar') - ->will($this->returnSelf()); - $this->entityQuery->expects($this->once()) - ->method('execute') - ->will($this->returnValue(array($entity->id()))); + $this->moduleHandler->invokeAll('test_entity_type_presave', [$entity]) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('entity_presave', [$entity, 'test_entity_type']) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('test_entity_type_update', [$entity]) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('entity_update', [$entity, 'test_entity_type']) + ->shouldBeCalled(); + + $this->entityQuery->condition('uuid', 'bar')->willReturn($this->entityQuery); + $this->entityQuery->execute()->willReturn([$entity->id()]); $return = $this->entityStorage->save($entity); $this->assertSame(SAVED_UPDATED, $return); @@ -412,56 +339,35 @@ class ConfigEntityStorageTest extends UnitTestCase { * @depends testSaveInsert */ public function testSaveRename(ConfigEntityInterface $entity) { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->atLeastOnce()) - ->method('isNew') - ->will($this->returnValue(FALSE)); - $config_object->expects($this->exactly(1)) - ->method('setData'); - $config_object->expects($this->once()) - ->method('save'); - $config_object->expects($this->atLeastOnce()) - ->method('get') + $immutable_config_object = $this->prophesize(ImmutableConfig::class); + $immutable_config_object->isNew()->willReturn(FALSE); + + $config_object = $this->prophesize(Config::class); + $config_object->setData(['id' => 'bar', 'uuid' => 'bar', 'dependencies' => []]) + ->shouldBeCalled(); + $config_object->save(FALSE) + ->shouldBeCalled(); + $config_object->get()->willReturn([]); + + $this->cacheTagsInvalidator->invalidateTags([$this->entityTypeId . '_list']) + ->shouldBeCalled(); + + $this->configFactory->get('the_provider.the_config_prefix.foo') + ->willReturn($immutable_config_object->reveal()); + $this->configFactory->loadMultiple(['the_provider.the_config_prefix.foo']) ->willReturn([]); - - $this->cacheTagsInvalidator->expects($this->once()) - ->method('invalidateTags') - ->with(array( - // List cache tag only; the own cache tag is invalidated by the config - // system. - $this->entityTypeId . '_list', - )); - - $this->configFactory->expects($this->once()) - ->method('rename') - ->willReturn($this->configFactory); - $this->configFactory->expects($this->exactly(1)) - ->method('getEditable') - ->with('the_config_prefix.bar') - ->will($this->returnValue($config_object)); - $this->configFactory->expects($this->exactly(2)) - ->method('loadMultiple') - ->with(array('the_config_prefix.foo')) - ->will($this->returnValue(array())); - $this->configFactory->expects($this->once()) - ->method('get') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); + $this->configFactory->rename('the_provider.the_config_prefix.foo', 'the_provider.the_config_prefix.bar') + ->shouldBeCalled(); + $this->configFactory->getEditable('the_provider.the_config_prefix.bar') + ->willReturn($config_object->reveal()); // Performing a rename does not change the original ID until saving. $this->assertSame('foo', $entity->getOriginalId()); $entity->set('id', 'bar'); $this->assertSame('foo', $entity->getOriginalId()); - $this->entityQuery->expects($this->once()) - ->method('condition') - ->with('uuid', 'bar') - ->will($this->returnSelf()); - $this->entityQuery->expects($this->once()) - ->method('execute') - ->will($this->returnValue(array($entity->id()))); + $this->entityQuery->condition('uuid', 'bar')->willReturn($this->entityQuery); + $this->entityQuery->execute()->willReturn([$entity->id()]); $return = $this->entityStorage->save($entity); $this->assertSame(SAVED_UPDATED, $return); @@ -475,8 +381,8 @@ class ConfigEntityStorageTest extends UnitTestCase { * @expectedExceptionMessage The entity does not have an ID. */ public function testSaveInvalid() { - $this->cacheTagsInvalidator->expects($this->never()) - ->method('invalidateTags'); + $this->cacheTagsInvalidator->invalidateTags(Argument::cetera()) + ->shouldNotBeCalled(); $entity = $this->getMockEntity(); $this->entityStorage->save($entity); @@ -489,24 +395,14 @@ class ConfigEntityStorageTest extends UnitTestCase { * @expectedException \Drupal\Core\Entity\EntityStorageException */ public function testSaveDuplicate() { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->atLeastOnce()) - ->method('isNew') - ->will($this->returnValue(FALSE)); - $config_object->expects($this->never()) - ->method('set'); - $config_object->expects($this->never()) - ->method('save'); + $config_object = $this->prophesize(ImmutableConfig::class); + $config_object->isNew()->willReturn(FALSE); - $this->cacheTagsInvalidator->expects($this->never()) - ->method('invalidateTags'); + $this->cacheTagsInvalidator->invalidateTags(Argument::cetera()) + ->shouldNotBeCalled(); - $this->configFactory->expects($this->once()) - ->method('get') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); + $this->configFactory->get('the_provider.the_config_prefix.foo') + ->willReturn($config_object->reveal()); $entity = $this->getMockEntity(array('id' => 'foo')); $entity->enforceIsNew(); @@ -522,29 +418,17 @@ class ConfigEntityStorageTest extends UnitTestCase { * @expectedExceptionMessage when this UUID is already used for */ public function testSaveMismatch() { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->atLeastOnce()) - ->method('isNew') - ->will($this->returnValue(TRUE)); - $config_object->expects($this->never()) - ->method('save'); + $config_object = $this->prophesize(ImmutableConfig::class); + $config_object->isNew()->willReturn(TRUE); - $this->cacheTagsInvalidator->expects($this->never()) - ->method('invalidateTags'); + $this->cacheTagsInvalidator->invalidateTags(Argument::cetera()) + ->shouldNotBeCalled(); - $this->configFactory->expects($this->once()) - ->method('get') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); + $this->configFactory->get('the_provider.the_config_prefix.foo') + ->willReturn($config_object->reveal()); - $this->entityQuery->expects($this->once()) - ->method('condition') - ->will($this->returnSelf()); - $this->entityQuery->expects($this->once()) - ->method('execute') - ->will($this->returnValue(array('baz'))); + $this->entityQuery->condition('uuid', NULL)->willReturn($this->entityQuery); + $this->entityQuery->execute()->willReturn(['baz']); $entity = $this->getMockEntity(array('id' => 'foo')); $this->entityStorage->save($entity); @@ -555,41 +439,29 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::doSave */ public function testSaveNoMismatch() { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->atLeastOnce()) - ->method('isNew') - ->will($this->returnValue(TRUE)); - $config_object->expects($this->once()) - ->method('save'); - $config_object->expects($this->atLeastOnce()) - ->method('get') - ->willReturn([]); + $immutable_config_object = $this->prophesize(ImmutableConfig::class); + $immutable_config_object->isNew()->willReturn(TRUE); - $this->cacheTagsInvalidator->expects($this->once()) - ->method('invalidateTags') - ->with(array( - $this->entityTypeId . '_list', // List cache tag. - )); + $config_object = $this->prophesize(Config::class); + $config_object->get()->willReturn([]); + $config_object->setData(['id' => 'foo', 'uuid' => NULL, 'dependencies' => []]) + ->shouldBeCalled(); + $config_object->save(FALSE)->shouldBeCalled(); - $this->configFactory->expects($this->once()) - ->method('get') - ->with('the_config_prefix.baz') - ->will($this->returnValue($config_object)); - $this->configFactory->expects($this->once()) - ->method('rename') - ->willReturn($this->configFactory); - $this->configFactory->expects($this->exactly(1)) - ->method('getEditable') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); - $this->entityQuery->expects($this->once()) - ->method('condition') - ->will($this->returnSelf()); - $this->entityQuery->expects($this->once()) - ->method('execute') - ->will($this->returnValue(array('baz'))); + $this->cacheTagsInvalidator->invalidateTags([$this->entityTypeId . '_list']) + ->shouldBeCalled(); + + $this->configFactory->get('the_provider.the_config_prefix.baz') + ->willReturn($immutable_config_object->reveal()) + ->shouldBeCalled(); + $this->configFactory->rename('the_provider.the_config_prefix.baz', 'the_provider.the_config_prefix.foo') + ->shouldBeCalled(); + $this->configFactory->getEditable('the_provider.the_config_prefix.foo') + ->willReturn($config_object->reveal()) + ->shouldBeCalled(); + + $this->entityQuery->condition('uuid', NULL)->willReturn($this->entityQuery); + $this->entityQuery->execute()->willReturn(['baz']); $entity = $this->getMockEntity(array('id' => 'foo')); $entity->setOriginalId('baz'); @@ -605,62 +477,29 @@ class ConfigEntityStorageTest extends UnitTestCase { * @expectedExceptionMessage when this entity already exists with UUID */ public function testSaveChangedUuid() { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->atLeastOnce()) - ->method('isNew') - ->will($this->returnValue(FALSE)); - $config_object->expects($this->never()) - ->method('save'); - $config_object->expects($this->exactly(2)) - ->method('get') - ->will($this->returnValueMap(array( - array('', array('id' => 'foo')), - array('id', 'foo'), - ))); - $config_object->expects($this->exactly(1)) - ->method('getCacheContexts') - ->willReturn([]); - $config_object->expects($this->exactly(1)) - ->method('getCacheTags') - ->willReturn(['config:foo']); - $config_object->expects($this->exactly(1)) - ->method('getCacheMaxAge') - ->willReturn(Cache::PERMANENT); - $config_object->expects($this->exactly(1)) - ->method('getName') - ->willReturn('foo'); + $config_object = $this->prophesize(ImmutableConfig::class); + $config_object->get()->willReturn(['id' => 'foo']); + $config_object->get('id')->willReturn('foo'); + $config_object->isNew()->willReturn(FALSE); + $config_object->getName()->willReturn('foo'); + $config_object->getCacheContexts()->willReturn([]); + $config_object->getCacheTags()->willReturn(['config:foo']); + $config_object->getCacheMaxAge()->willReturn(Cache::PERMANENT); - $this->cacheTagsInvalidator->expects($this->never()) - ->method('invalidateTags'); + $this->cacheTagsInvalidator->invalidateTags(Argument::cetera()) + ->shouldNotBeCalled(); - $this->configFactory->expects($this->at(1)) - ->method('loadMultiple') - ->with(array('the_config_prefix.foo')) - ->will($this->returnValue(array())); - $this->configFactory->expects($this->at(2)) - ->method('loadMultiple') - ->with(array('the_config_prefix.foo')) - ->will($this->returnValue(array($config_object))); - $this->configFactory->expects($this->once()) - ->method('get') - ->with('the_config_prefix.foo') - ->will($this->returnValue($config_object)); - $this->configFactory->expects($this->never()) - ->method('rename') - ->will($this->returnValue($config_object)); + $this->configFactory->loadMultiple(['the_provider.the_config_prefix.foo']) + ->willReturn([$config_object->reveal()]); + $this->configFactory->get('the_provider.the_config_prefix.foo') + ->willReturn($config_object->reveal()); + $this->configFactory->rename(Argument::cetera())->shouldNotBeCalled(); - $this->moduleHandler->expects($this->exactly(2)) - ->method('getImplementations') - ->will($this->returnValue(array())); + $this->moduleHandler->getImplementations('entity_load')->willReturn([]); + $this->moduleHandler->getImplementations('test_entity_type_load')->willReturn([]); - $this->entityQuery->expects($this->once()) - ->method('condition') - ->will($this->returnSelf()); - $this->entityQuery->expects($this->once()) - ->method('execute') - ->will($this->returnValue(array('foo'))); + $this->entityQuery->condition('uuid', 'baz')->willReturn($this->entityQuery); + $this->entityQuery->execute()->willReturn(['foo']); $entity = $this->getMockEntity(array('id' => 'foo')); @@ -675,38 +514,22 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::doLoadMultiple */ public function testLoad() { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->exactly(2)) - ->method('get') - ->will($this->returnValueMap(array( - array('', array('id' => 'foo')), - array('id', 'foo'), - ))); - $config_object->expects($this->exactly(1)) - ->method('getCacheContexts') - ->willReturn([]); - $config_object->expects($this->exactly(1)) - ->method('getCacheTags') - ->willReturn(['config:foo']); - $config_object->expects($this->exactly(1)) - ->method('getCacheMaxAge') - ->willReturn(Cache::PERMANENT); - $config_object->expects($this->exactly(1)) - ->method('getName') - ->willReturn('foo'); + $config_object = $this->prophesize(ImmutableConfig::class); + $config_object->get()->willReturn(['id' => 'foo']); + $config_object->get('id')->willReturn('foo'); + $config_object->getCacheContexts()->willReturn([]); + $config_object->getCacheTags()->willReturn(['config:foo']); + $config_object->getCacheMaxAge()->willReturn(Cache::PERMANENT); + $config_object->getName()->willReturn('foo'); - $this->configFactory->expects($this->once()) - ->method('loadMultiple') - ->with(array('the_config_prefix.foo')) - ->will($this->returnValue(array($config_object))); - $this->moduleHandler->expects($this->exactly(2)) - ->method('getImplementations') - ->will($this->returnValue(array())); + $this->configFactory->loadMultiple(['the_provider.the_config_prefix.foo']) + ->willReturn([$config_object->reveal()]); + + $this->moduleHandler->getImplementations('entity_load')->willReturn([]); + $this->moduleHandler->getImplementations('test_entity_type_load')->willReturn([]); $entity = $this->entityStorage->load('foo'); - $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); + $this->assertInstanceOf(EntityInterface::class, $entity); $this->assertSame('foo', $entity->id()); } @@ -717,67 +540,35 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::doLoadMultiple */ public function testLoadMultipleAll() { - $foo_config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $foo_config_object->expects($this->exactly(2)) - ->method('get') - ->will($this->returnValueMap(array( - array('', array('id' => 'foo')), - array('id', 'foo'), - ))); - $foo_config_object->expects($this->exactly(1)) - ->method('getCacheContexts') - ->willReturn([]); - $foo_config_object->expects($this->exactly(1)) - ->method('getCacheTags') - ->willReturn(['config:foo']); - $foo_config_object->expects($this->exactly(1)) - ->method('getCacheMaxAge') - ->willReturn(Cache::PERMANENT); - $foo_config_object->expects($this->exactly(1)) - ->method('getName') - ->willReturn('foo'); + $foo_config_object = $this->prophesize(ImmutableConfig::class); + $foo_config_object->get()->willReturn(['id' => 'foo']); + $foo_config_object->get('id')->willReturn('foo'); + $foo_config_object->getCacheContexts()->willReturn([]); + $foo_config_object->getCacheTags()->willReturn(['config:foo']); + $foo_config_object->getCacheMaxAge()->willReturn(Cache::PERMANENT); + $foo_config_object->getName()->willReturn('foo'); - $bar_config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $bar_config_object->expects($this->exactly(2)) - ->method('get') - ->will($this->returnValueMap(array( - array('', array('id' => 'bar')), - array('id', 'bar'), - ))); - $bar_config_object->expects($this->exactly(1)) - ->method('getCacheContexts') - ->willReturn([]); - $bar_config_object->expects($this->exactly(1)) - ->method('getCacheTags') - ->willReturn(['config:bar']); - $bar_config_object->expects($this->exactly(1)) - ->method('getCacheMaxAge') - ->willReturn(Cache::PERMANENT); - $bar_config_object->expects($this->exactly(1)) - ->method('getName') - ->willReturn('foo'); + $bar_config_object = $this->prophesize(ImmutableConfig::class); + $bar_config_object->get()->willReturn(['id' => 'bar']); + $bar_config_object->get('id')->willReturn('bar'); + $bar_config_object->getCacheContexts()->willReturn([]); + $bar_config_object->getCacheTags()->willReturn(['config:bar']); + $bar_config_object->getCacheMaxAge()->willReturn(Cache::PERMANENT); + $bar_config_object->getName()->willReturn('foo'); - $this->configFactory->expects($this->once()) - ->method('listAll') - ->with('the_config_prefix.') - ->will($this->returnValue(array('the_config_prefix.foo' , 'the_config_prefix.bar'))); - $this->configFactory->expects($this->once()) - ->method('loadMultiple') - ->with(array('the_config_prefix.foo' , 'the_config_prefix.bar')) - ->will($this->returnValue(array($foo_config_object, $bar_config_object))); - $this->moduleHandler->expects($this->exactly(2)) - ->method('getImplementations') - ->will($this->returnValue(array())); + $this->configFactory->listAll('the_provider.the_config_prefix.') + ->willReturn(['the_provider.the_config_prefix.foo' , 'the_provider.the_config_prefix.bar']); + $this->configFactory->loadMultiple(['the_provider.the_config_prefix.foo', 'the_provider.the_config_prefix.bar']) + ->willReturn([$foo_config_object->reveal(), $bar_config_object->reveal()]); + + $this->moduleHandler->getImplementations('entity_load')->willReturn([]); + $this->moduleHandler->getImplementations('test_entity_type_load')->willReturn([]); $entities = $this->entityStorage->loadMultiple(); $expected['foo'] = 'foo'; $expected['bar'] = 'bar'; + $this->assertContainsOnlyInstancesOf(EntityInterface::class, $entities); foreach ($entities as $id => $entity) { - $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); $this->assertSame($id, $entity->id()); $this->assertSame($expected[$id], $entity->id()); } @@ -790,39 +581,23 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::doLoadMultiple */ public function testLoadMultipleIds() { - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->exactly(2)) - ->method('get') - ->will($this->returnValueMap(array( - array('', array('id' => 'foo')), - array('id', 'foo'), - ))); - $config_object->expects($this->exactly(1)) - ->method('getCacheContexts') - ->willReturn([]); - $config_object->expects($this->exactly(1)) - ->method('getCacheTags') - ->willReturn(['config:foo']); - $config_object->expects($this->exactly(1)) - ->method('getCacheMaxAge') - ->willReturn(Cache::PERMANENT); - $config_object->expects($this->exactly(1)) - ->method('getName') - ->willReturn('foo'); + $config_object = $this->prophesize(ImmutableConfig::class); + $config_object->get()->willReturn(['id' => 'foo']); + $config_object->get('id')->willReturn('foo'); + $config_object->getCacheContexts()->willReturn([]); + $config_object->getCacheTags()->willReturn(['config:foo']); + $config_object->getCacheMaxAge()->willReturn(Cache::PERMANENT); + $config_object->getName()->willReturn('foo'); - $this->configFactory->expects($this->once()) - ->method('loadMultiple') - ->with(array('the_config_prefix.foo')) - ->will($this->returnValue(array($config_object))); - $this->moduleHandler->expects($this->exactly(2)) - ->method('getImplementations') - ->will($this->returnValue(array())); + $this->configFactory->loadMultiple(['the_provider.the_config_prefix.foo']) + ->willReturn([$config_object->reveal()]); + + $this->moduleHandler->getImplementations('entity_load')->willReturn([]); + $this->moduleHandler->getImplementations('test_entity_type_load')->willReturn([]); $entities = $this->entityStorage->loadMultiple(array('foo')); + $this->assertContainsOnlyInstancesOf(EntityInterface::class, $entities); foreach ($entities as $id => $entity) { - $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); $this->assertSame($id, $entity->id()); } } @@ -838,8 +613,8 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::deleteRevision */ public function testDeleteRevision() { - $this->cacheTagsInvalidator->expects($this->never()) - ->method('invalidateTags'); + $this->cacheTagsInvalidator->invalidateTags(Argument::cetera()) + ->shouldNotBeCalled(); $this->assertSame(NULL, $this->entityStorage->deleteRevision(1)); } @@ -851,60 +626,37 @@ class ConfigEntityStorageTest extends UnitTestCase { public function testDelete() { // Dependencies are tested in // \Drupal\Tests\config\Kernel\ConfigDependencyTest. - $this->configManager->expects($this->any()) - ->method('getConfigEntitiesToChangeOnDependencyRemoval') + $this->configManager + ->getConfigEntitiesToChangeOnDependencyRemoval('config', ['the_provider.the_config_prefix.foo'], FALSE) ->willReturn(['update' => [], 'delete' => [], 'unchanged' => []]); + $this->configManager + ->getConfigEntitiesToChangeOnDependencyRemoval('config', ['the_provider.the_config_prefix.bar'], FALSE) + ->willReturn(['update' => [], 'delete' => [], 'unchanged' => []]); + $entities = array(); - $configs = array(); - $config_map = array(); foreach (array('foo', 'bar') as $id) { $entity = $this->getMockEntity(array('id' => $id)); $entities[] = $entity; - $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') - ->disableOriginalConstructor() - ->getMock(); - $config_object->expects($this->once()) - ->method('delete'); - $configs[] = $config_object; - $config_map[] = array("the_config_prefix.$id", $config_object); + + $config_object = $this->prophesize(Config::class); + $config_object->delete()->shouldBeCalled(); + + $this->configFactory->getEditable("the_provider.the_config_prefix.$id") + ->willReturn($config_object->reveal()); + + $this->moduleHandler->invokeAll('test_entity_type_predelete', [$entity]) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('entity_predelete', [$entity, 'test_entity_type']) + ->shouldBeCalled(); + + $this->moduleHandler->invokeAll('test_entity_type_delete', [$entity]) + ->shouldBeCalled(); + $this->moduleHandler->invokeAll('entity_delete', [$entity, 'test_entity_type']) + ->shouldBeCalled(); } - $this->cacheTagsInvalidator->expects($this->once()) - ->method('invalidateTags') - ->with(array( - // List cache tag only; the own cache tag is invalidated by the config - // system. - $this->entityTypeId . '_list', - )); - - $this->configFactory->expects($this->exactly(2)) - ->method('getEditable') - ->will($this->returnValueMap($config_map)); - - $this->moduleHandler->expects($this->at(0)) - ->method('invokeAll') - ->with('test_entity_type_predelete'); - $this->moduleHandler->expects($this->at(1)) - ->method('invokeAll') - ->with('entity_predelete'); - $this->moduleHandler->expects($this->at(2)) - ->method('invokeAll') - ->with('test_entity_type_predelete'); - $this->moduleHandler->expects($this->at(3)) - ->method('invokeAll') - ->with('entity_predelete'); - $this->moduleHandler->expects($this->at(4)) - ->method('invokeAll') - ->with('test_entity_type_delete'); - $this->moduleHandler->expects($this->at(5)) - ->method('invokeAll') - ->with('entity_delete'); - $this->moduleHandler->expects($this->at(6)) - ->method('invokeAll') - ->with('test_entity_type_delete'); - $this->moduleHandler->expects($this->at(7)) - ->method('invokeAll') - ->with('entity_delete'); + $this->cacheTagsInvalidator->invalidateTags([$this->entityTypeId . '_list']) + ->shouldBeCalled(); $this->entityStorage->delete($entities); } @@ -914,13 +666,13 @@ class ConfigEntityStorageTest extends UnitTestCase { * @covers ::doDelete */ public function testDeleteNothing() { - $this->moduleHandler->expects($this->never()) - ->method($this->anything()); - $this->configFactory->expects($this->never()) - ->method('get'); + $this->moduleHandler->getImplementations(Argument::cetera())->shouldNotBeCalled(); + $this->moduleHandler->invokeAll(Argument::cetera())->shouldNotBeCalled(); - $this->cacheTagsInvalidator->expects($this->never()) - ->method('invalidateTags'); + $this->configFactory->get(Argument::cetera())->shouldNotBeCalled(); + $this->configFactory->getEditable(Argument::cetera())->shouldNotBeCalled(); + + $this->cacheTagsInvalidator->invalidateTags(Argument::cetera())->shouldNotBeCalled(); $this->entityStorage->delete(array()); } @@ -936,7 +688,7 @@ class ConfigEntityStorageTest extends UnitTestCase { * @return \Drupal\Core\Entity\EntityInterface|\PHPUnit_Framework_MockObject_MockObject */ public function getMockEntity(array $values = array(), $methods = array()) { - return $this->getMockForAbstractClass('Drupal\Core\Config\Entity\ConfigEntityBase', array($values, 'test_entity_type'), '', TRUE, TRUE, TRUE, $methods); + return $this->getMockForAbstractClass(ConfigEntityBase::class, [$values, 'test_entity_type'], '', TRUE, TRUE, TRUE, $methods); } } diff --git a/core/themes/seven/css/theme/ckeditor-dialog.css b/core/themes/seven/css/theme/ckeditor-dialog.css index f3d2d86a7..b7d8dbbcc 100644 --- a/core/themes/seven/css/theme/ckeditor-dialog.css +++ b/core/themes/seven/css/theme/ckeditor-dialog.css @@ -139,7 +139,7 @@ [dir="rtl"] .cke_reset_all .cke_dialog .cke_dialog_footer { text-align: right; } -.cke_reset_all .cke_resizer { +.cke_reset_all .cke_dialog .cke_resizer { display: none; } .cke_reset_all .cke_dialog_footer_buttons { diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index f31046651..03ee279d4 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -667,7 +667,7 @@ if ($settings['hash_salt']) { /** * Load services definition file. */ -$settings['container_yamls'][] = __DIR__ . '/services.yml'; +$settings['container_yamls'][] = $app_root . '/' . $site_path . '/services.yml'; /** * Override the default service container class. @@ -762,6 +762,13 @@ if (file_exists(__DIR__ . '/settings.pantheon.php')) { * * Keep this code block at the end of this file to take full effect. */ +<<<<<<< HEAD if (file_exists(__DIR__ . '/settings.local.php')) { include __DIR__ . '/settings.local.php'; } +======= +# +# if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) { +# include $app_root . '/' . $site_path . '/settings.local.php'; +# } +>>>>>>> b96f629ea4b63fb16b11b89c0a88fb8524e82359 diff --git a/sites/development.services.yml b/sites/development.services.yml index 73cc99825..d2857c66f 100644 --- a/sites/development.services.yml +++ b/sites/development.services.yml @@ -2,6 +2,8 @@ # # To activate this feature, follow the instructions at the top of the # 'example.settings.local.php' file, which sits next to this file. +parameters: + http.response.debug_cacheability_headers: true services: cache.backend.null: class: Drupal\Core\Cache\NullBackendFactory