diff --git a/core/INSTALL.txt b/core/INSTALL.txt index 8c5ee8723..c83976126 100644 --- a/core/INSTALL.txt +++ b/core/INSTALL.txt @@ -90,6 +90,19 @@ INSTALLATION mv drupal-x.y.z/* drupal-x.y.z/.htaccess drupal-x.y.z/.csslintrc drupal-x.y.z/.editorconfig drupal-x.y.z/.eslintignore drupal-x.y.z/.eslintrc drupal-x.y.z/.gitattributes /path/to/your/installation + You can also download the latest version of Drupal using Git on the command + line and set up a repository by following the instructions at + https://www.drupal.org/project/drupal/git-instructions for "Setting up + repository for the first time". + + Once you have downloaded Drupal successfully, you may install Composer + globally using the instructions at + https://getcomposer.org/doc/00-intro.md#globally + + With Composer installed, run the following command from the Drupal web root: + + composer install + 2. Create the Drupal database. Because Drupal stores all site information in a database, the Drupal diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 60aefd152..e884c10c5 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -23,9 +23,10 @@ Drupal 8 (Release Manager) - Alex Pott 'alexpott' https://www.drupal.org/u/alexpott (Framework Manager) - -Provisional membership: - Scott Reeves 'Cottser' https://www.drupal.org/u/cottser + (Framework Manager - Frontend) + +Provisional membership: None at this time. Drupal 7 - Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx @@ -472,11 +473,13 @@ Views module - Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett - Damian Lee 'damiankloip' https://www.drupal.org/u/damiankloip - Jess Myrbo 'xjm' https://www.drupal.org/u/xjm +- Len Swaneveld 'Lendude' https://www.drupal.org/u/lendude Views UI module - 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 Theme maintainers @@ -523,6 +526,9 @@ Multi-lingual Web services - Larry Garfield 'Crell' https://www.drupal.org/u/crell +Workflow Initiative +- Dick Olsson 'dixon_' https://www.drupal.org/u/dixon_ + Provisional membership: None at this time. diff --git a/core/core.api.php b/core/core.api.php index 4b2fe62fd..7b90ae21f 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -1894,21 +1894,32 @@ function hook_cron() { // Short-running operation example, not using a queue: // Delete all expired records since the last cron run. - $expires = \Drupal::state()->get('mymodule.cron_last_run', REQUEST_TIME); - db_delete('mymodule_table') + $expires = \Drupal::state()->get('mymodule.last_check', 0); + \Drupal::database()->delete('mymodule_table') ->condition('expires', $expires, '>=') ->execute(); - \Drupal::state()->set('mymodule.cron_last_run', REQUEST_TIME); + \Drupal::state()->set('mymodule.last_check', REQUEST_TIME); // Long-running operation example, leveraging a queue: - // Fetch feeds from other sites. - $result = db_query('SELECT * FROM {aggregator_feed} WHERE checked + refresh < :time AND refresh <> :never', array( - ':time' => REQUEST_TIME, - ':never' => AGGREGATOR_CLEAR_NEVER, - )); + // Queue news feeds for updates once their refresh interval has elapsed. $queue = \Drupal::queue('aggregator_feeds'); - foreach ($result as $feed) { - $queue->createItem($feed); + $ids = \Drupal::entityManager()->getStorage('aggregator_feed')->getFeedIdsToRefresh(); + foreach (Feed::loadMultiple($ids) as $feed) { + if ($queue->createItem($feed)) { + // Add timestamp to avoid queueing item more than once. + $feed->setQueuedTime(REQUEST_TIME); + $feed->save(); + } + } + $ids = \Drupal::entityQuery('aggregator_feed') + ->condition('queued', REQUEST_TIME - (3600 * 6), '<') + ->execute(); + if ($ids) { + $feeds = Feed::loadMultiple($ids); + foreach ($feeds as $feed) { + $feed->setQueuedTime(0); + $feed->save(); + } } } @@ -2036,7 +2047,7 @@ function hook_mail_alter(&$message) { * An array of parameters supplied by the caller of * MailManagerInterface->mail(). * - * @see \Drupal\Core\Mail\MailManagerInterface->mail() + * @see \Drupal\Core\Mail\MailManagerInterface::mail() */ function hook_mail($key, &$message, $params) { $account = $params['account']; diff --git a/core/includes/file.inc b/core/includes/file.inc index 017a3d650..a58e6dcd7 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -1220,7 +1220,9 @@ function file_directory_os_temp() { foreach ($directories as $directory) { if (is_dir($directory) && is_writable($directory)) { - return $directory; + // Both sys_get_temp_dir() and ini_get('upload_tmp_dir') can return paths + // with a trailing directory separator. + return rtrim($directory, DIRECTORY_SEPARATOR); } } return FALSE; diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index d75ed9f40..11bfbb3e6 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -369,7 +369,9 @@ function install_begin_request($class_loader, &$install_state) { $install_state['config_verified'] = FALSE; } $install_state['database_verified'] = install_verify_database_settings($site_path); - $install_state['settings_verified'] = $install_state['config_verified'] && $install_state['database_verified']; + // A valid settings.php has database settings and a hash_salt value. Other + // settings like config_directories will be checked by system_requirements(). + $install_state['settings_verified'] = $install_state['database_verified'] && (bool) Settings::get('hash_salt', FALSE); // Install factory tables only after checking the database. if ($install_state['database_verified'] && $install_state['database_ready']) { diff --git a/core/includes/update.inc b/core/includes/update.inc index 2c020db6b..ea126f864 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -596,7 +596,7 @@ function update_already_performed($module, $number) { * An array of return values obtained by merging the results of the * hook_update_dependencies() implementations in all installed modules. * - * @see \Drupal::moduleHandler()->invokeAll() + * @see \Drupal\Core\Extension\ModuleHandlerInterface::invokeAll() * @see hook_update_dependencies() */ function update_retrieve_dependencies() { diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 0e82afad8..6539eb6d9 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -81,7 +81,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '8.1.8'; + const VERSION = '8.1.9'; /** * Core API compatibility. diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php index 4cd660b4b..972a6e4cf 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php @@ -23,7 +23,7 @@ class Schema extends DatabaseSchema { * This is collected by DatabaseConnection_pgsql->queryTableInformation(), * by introspecting the database. * - * @see DatabaseConnection_pgsql->queryTableInformation() + * @see \Drupal\Core\Database\Driver\pgsql\Schema::queryTableInformation() * @var array */ protected $tableInformation = array(); diff --git a/core/lib/Drupal/Core/Datetime/FormattedDateDiff.php b/core/lib/Drupal/Core/Datetime/FormattedDateDiff.php index 07bb83638..266022be8 100644 --- a/core/lib/Drupal/Core/Datetime/FormattedDateDiff.php +++ b/core/lib/Drupal/Core/Datetime/FormattedDateDiff.php @@ -51,10 +51,23 @@ class FormattedDateDiff implements RenderableInterface, CacheableDependencyInter } /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return $this->maxAge; + } + + /** + * The maximum age for which this object may be cached. + * * @return int + * The maximum time in seconds that this object may be cached. + * + * @deprecated in Drupal 8.1.9 and will be removed before Drupal 9.0.0. Use + * \Drupal\Core\Datetime\FormattedDateDiff::getCacheMaxAge() instead. */ public function getMaxAge() { - return $this->maxAge; + return $this->getCacheMaxAge(); } /** diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 23fd0b50a..51adf91b7 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -297,7 +297,7 @@ class EntityAutocomplete extends Textfield { $multiples[] = $name . ' (' . $id . ')'; } $params['@id'] = $id; - $form_state->setError($element, t('Multiple entities match this reference; "%multiple". Specify the one you want by appending the id in parentheses, like "@value (@id)".', array('%multiple' => implode('", "', $multiples)))); + $form_state->setError($element, t('Multiple entities match this reference; "%multiple". Specify the one you want by appending the id in parentheses, like "@value (@id)".', array('%multiple' => implode('", "', $multiples)) + $params)); } else { // Take the one and only matching entity. diff --git a/core/lib/Drupal/Core/Entity/EntityAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityAccessCheck.php index 0de9b0141..bc8bd5973 100644 --- a/core/lib/Drupal/Core/Entity/EntityAccessCheck.php +++ b/core/lib/Drupal/Core/Entity/EntityAccessCheck.php @@ -17,13 +17,26 @@ class EntityAccessCheck implements AccessInterface { * Checks access to the entity operation on the given route. * * The value of the '_entity_access' key must be in the pattern - * 'entity_type.operation.' The entity type must match the {entity_type} - * parameter in the route pattern. This will check a node for 'update' access: + * 'entity_slug_name.operation.' For example, this will check a node for + * 'update' access: * @code * pattern: '/foo/{node}/bar' * requirements: * _entity_access: 'node.update' * @endcode + * And this will check a dynamic entity type: + * @code + * example.route: + * path: foo/{entity_type}/{example} + * requirements: + * _entity_access: example.update + * options: + * parameters: + * example: + * type: entity:{entity_type} + * @endcode + * @see \Drupal\Core\ParamConverter\EntityConverter + * * Available operations are 'view', 'update', 'create', and 'delete'. * * @param \Symfony\Component\Routing\Route $route diff --git a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php index 17b266ffc..630d17673 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php @@ -31,7 +31,6 @@ interface QueryInterface extends AlterableInterface { * ->condition('greetings', 'merhaba', '=', 'tr') * ->condition('greetings.value', 'siema', '=', 'pl') * ->execute(); - * $entity_ids = $query->execute(); * @endcode * * @param $field diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index e115bd211..b2be1c0c4 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -1543,6 +1543,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt $item_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC)) ->fields('t') ->condition('entity_id', $row['entity_id']) + ->condition('deleted', 1) ->orderBy('delta'); foreach ($item_query->execute() as $item_row) { @@ -1581,10 +1582,12 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id(); $this->database->delete($table_name) ->condition('revision_id', $revision_id) + ->condition('deleted', 1) ->execute(); if ($this->entityType->isRevisionable()) { $this->database->delete($revision_name) ->condition('revision_id', $revision_id) + ->condition('deleted', 1) ->execute(); } } @@ -1684,6 +1687,12 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt * Whether the field has been already deleted. */ protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) { + // Configurable fields are marked for deletion. + if ($storage_definition instanceOf FieldStorageConfigInterface) { + return $storage_definition->isDeleted(); + } + // For non configurable fields check whether they are still in the last + // installed schema repository. return !array_key_exists($storage_definition->getName(), $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityTypeId)); } diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index c61c2368e..36873845c 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -1252,21 +1252,6 @@ function hook_ENTITY_TYPE_revision_delete(Drupal\Core\Entity\EntityInterface $en } } -/** - * Alter or execute an Drupal\Core\Entity\Query\EntityQueryInterface. - * - * @param \Drupal\Core\Entity\Query\QueryInterface $query - * Note the $query->altered attribute which is TRUE in case the query has - * already been altered once. This happens with cloned queries. - * If there is a pager, then such a cloned query will be executed to count - * all elements. This query can be detected by checking for - * ($query->pager && $query->count), allowing the driver to return 0 from - * the count query and disable the pager. - */ -function hook_entity_query_alter(\Drupal\Core\Entity\Query\QueryInterface $query) { - // @todo: code example. -} - /** * Act on entities being assembled before rendering. * @@ -1860,7 +1845,8 @@ function hook_entity_operation_alter(array &$operations, \Drupal\Core\Entity\Ent * * @param string $operation * The operation to be performed. See - * \Drupal\Core\Access\AccessibleInterface::access() for possible values. + * \Drupal\Core\Entity\EntityAccessControlHandlerInterface::fieldAccess() + * for possible values. * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition * The field definition. * @param \Drupal\Core\Session\AccountInterface $account @@ -1871,6 +1857,8 @@ function hook_entity_operation_alter(array &$operations, \Drupal\Core\Entity\Ent * * @return \Drupal\Core\Access\AccessResultInterface * The access result. + * + * @see \Drupal\Core\Entity\EntityAccessControlHandlerInterface::fieldAccess() */ function hook_entity_field_access($operation, \Drupal\Core\Field\FieldDefinitionInterface $field_definition, \Drupal\Core\Session\AccountInterface $account, \Drupal\Core\Field\FieldItemListInterface $items = NULL) { if ($field_definition->getName() == 'field_of_interest' && $operation == 'edit') { diff --git a/core/lib/Drupal/Core/File/file.api.php b/core/lib/Drupal/Core/File/file.api.php index 43913fbd0..c26ac2be3 100644 --- a/core/lib/Drupal/Core/File/file.api.php +++ b/core/lib/Drupal/Core/File/file.api.php @@ -157,12 +157,6 @@ function hook_archiver_info_alter(&$info) { * will always be passed the full path to the root of the site that should * be used to restrict where file transfer operations can occur (the $jail) * and an array of settings values returned by the settings form. - * - 'file': Required. The include file containing the FileTransfer class. - * This should be a separate .inc file, not just the .module file, so that - * the minimum possible code is loaded when authorize.php is running. - * - 'file path': Optional. The directory (relative to the Drupal root) - * where the include file lives. If not defined, defaults to the base - * directory of the module implementing the hook. * - 'weight': Optional. Integer weight used for sorting connection types on * the authorize.php form. * diff --git a/core/lib/Drupal/Core/Form/ConfigFormBase.php b/core/lib/Drupal/Core/Form/ConfigFormBase.php index c2f83fadb..fb508f854 100644 --- a/core/lib/Drupal/Core/Form/ConfigFormBase.php +++ b/core/lib/Drupal/Core/Form/ConfigFormBase.php @@ -41,7 +41,7 @@ abstract class ConfigFormBase extends FormBase { '#button_type' => 'primary', ); - // By default, render the form using theme_system_config_form(). + // By default, render the form using system-config-form.html.twig. $form['#theme'] = 'system_config_form'; return $form; diff --git a/core/lib/Drupal/Core/Utility/ProjectInfo.php b/core/lib/Drupal/Core/Utility/ProjectInfo.php index 1eed1fe9a..24043d3b4 100644 --- a/core/lib/Drupal/Core/Utility/ProjectInfo.php +++ b/core/lib/Drupal/Core/Utility/ProjectInfo.php @@ -172,7 +172,7 @@ class ProjectInfo { * @return * Array of .info.yml file data we need for the update manager. * - * @see \Drupal\Core\Utility\ProjectInfo->processInfoList() + * @see \Drupal\Core\Utility\ProjectInfo::processInfoList() */ function filterProjectInfo($info, $additional_whitelist = array()) { $whitelist = array( diff --git a/core/misc/dialog/dialog.ajax.js b/core/misc/dialog/dialog.ajax.js index 16f72f00a..3f1b0c2ef 100644 --- a/core/misc/dialog/dialog.ajax.js +++ b/core/misc/dialog/dialog.ajax.js @@ -62,7 +62,7 @@ */ prepareDialogButtons: function ($dialog) { var buttons = []; - var $buttons = $dialog.find('.form-actions input[type=submit]'); + var $buttons = $dialog.find('.form-actions input[type=submit], .form-actions a.button'); $buttons.each(function () { // Hidden form buttons need special attention. For browser consistency, // the button needs to be "visible" in order to have the enter key fire @@ -74,14 +74,22 @@ width: 0, height: 0, padding: 0, - border: 0 + border: 0, + overflow: 'hidden' }); buttons.push({ text: $originalButton.html() || $originalButton.attr('value'), class: $originalButton.attr('class'), click: function (e) { - $originalButton.trigger('mousedown').trigger('mouseup').trigger('click'); - e.preventDefault(); + // If the original button is an anchor tag, triggering the "click" + // event will not simulate a click. Use the click method instead. + if ($originalButton.is('a')) { + $originalButton[0].click(); + } + else { + $originalButton.trigger('mousedown').trigger('mouseup').trigger('click'); + e.preventDefault(); + } } }); }); diff --git a/core/modules/action/src/ActionListBuilder.php b/core/modules/action/src/ActionListBuilder.php index 09024037b..0a54cd652 100644 --- a/core/modules/action/src/ActionListBuilder.php +++ b/core/modules/action/src/ActionListBuilder.php @@ -108,10 +108,10 @@ class ActionListBuilder extends ConfigEntityListBuilder { * {@inheritdoc} */ public function render() { - $build['action_header']['#markup'] = '<h3>' . t('Available actions:') . '</h3>'; + $build['action_header']['#markup'] = '<h3>' . $this->t('Available actions:') . '</h3>'; $build['action_table'] = parent::render(); if (!$this->hasConfigurableActions) { - unset($build['action_table']['#header']['operations']); + unset($build['action_table']['table']['#header']['operations']); } $build['action_admin_manage_form'] = \Drupal::formBuilder()->getForm('Drupal\action\Form\ActionAdminManageForm'); return $build; diff --git a/core/modules/action/src/Tests/ActionListTest.php b/core/modules/action/src/Tests/ActionListTest.php new file mode 100644 index 000000000..f533cdf1b --- /dev/null +++ b/core/modules/action/src/Tests/ActionListTest.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\action\Tests; + +use Drupal\simpletest\WebTestBase; + +/** + * Test behaviors when visiting the action listing page. + * + * @group action + */ +class ActionListTest extends WebTestBase { + + /** + * Modules to install. + * + * @var array + */ + public static $modules = array('action'); + + /** + * Tests the behavior when there are no actions to list in the admin page. + */ + public function testEmptyActionList() { + // Create a user with permission to view the actions administration pages. + $this->drupalLogin($this->drupalCreateUser(['administer actions'])); + + // Ensure the empty text appears on the action list page. + /** @var $storage \Drupal\Core\Entity\EntityStorageInterface */ + $storage = $this->container->get('entity.manager')->getStorage('action'); + $actions = $storage->loadMultiple(); + $storage->delete($actions); + $this->drupalGet('/admin/config/system/actions'); + $this->assertRaw('There is no Action yet.'); + } + +} diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php index 958fc7ac8..425117d83 100644 --- a/core/modules/block_content/src/Entity/BlockContent.php +++ b/core/modules/block_content/src/Entity/BlockContent.php @@ -104,9 +104,15 @@ class BlockContent extends ContentEntityBase implements BlockContentInterface { */ public function postSave(EntityStorageInterface $storage, $update = TRUE) { parent::postSave($storage, $update); + static::invalidateBlockPluginCache(); + } - // Invalidate the block cache to update custom block-based derivatives. - \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + static::invalidateBlockPluginCache(); } /** @@ -237,4 +243,12 @@ class BlockContent extends ContentEntityBase implements BlockContentInterface { return $this; } + /** + * Invalidates the block plugin cache after changes and deletions. + */ + protected static function invalidateBlockPluginCache() { + // Invalidate the block cache to update custom block-based derivatives. + \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); + } + } diff --git a/core/modules/block_content/src/Plugin/Derivative/BlockContent.php b/core/modules/block_content/src/Plugin/Derivative/BlockContent.php index 63b68c07d..ab564451c 100644 --- a/core/modules/block_content/src/Plugin/Derivative/BlockContent.php +++ b/core/modules/block_content/src/Plugin/Derivative/BlockContent.php @@ -44,6 +44,8 @@ class BlockContent extends DeriverBase implements ContainerDeriverInterface { */ public function getDerivativeDefinitions($base_plugin_definition) { $block_contents = $this->blockContentStorage->loadMultiple(); + // Reset the discovered definitions. + $this->derivatives = []; /** @var $block_content \Drupal\block_content\Entity\BlockContent */ foreach ($block_contents as $block_content) { $this->derivatives[$block_content->uuid()] = $base_plugin_definition; diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentDeletionTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentDeletionTest.php new file mode 100644 index 000000000..e54b2d3d4 --- /dev/null +++ b/core/modules/block_content/tests/src/Kernel/BlockContentDeletionTest.php @@ -0,0 +1,63 @@ +<?php + +namespace Drupal\Tests\block_content\Kernel; + +use Drupal\block_content\Entity\BlockContent; +use Drupal\block_content\Entity\BlockContentType; +use Drupal\Component\Plugin\PluginBase; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests that deleting a block clears the cached definitions. + * + * @group block_content + */ +class BlockContentDeletionTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['block', 'block_content', 'system', 'user']; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->installSchema('system', ['sequence']); + $this->installEntitySchema('user'); + $this->installEntitySchema('block_content'); + } + + /** + * Tests deleting a block_content updates the discovered block plugin. + */ + public function testDeletingBlockContentShouldClearPluginCache() { + // Create a block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'spiffy', + 'label' => 'Mucho spiffy', + 'description' => "Provides a block type that increases your site's spiffiness by upto 11%", + ]); + $block_content_type->save(); + // And a block content entity. + $block_content = BlockContent::create([ + 'info' => 'Spiffy prototype', + 'type' => 'spiffy', + ]); + $block_content->save(); + + // Make sure the block content provides a derivative block plugin in the + // block repository. + /** @var \Drupal\Core\Block\BlockManagerInterface $block_manager */ + $block_manager = $this->container->get('plugin.manager.block'); + $plugin_id = 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid(); + $this->assertTrue($block_manager->hasDefinition($plugin_id)); + + // Now delete the block content entity. + $block_content->delete(); + // The plugin should no longer exist. + $this->assertFalse($block_manager->hasDefinition($plugin_id)); + } + +} diff --git a/core/modules/comment/src/Plugin/migrate/source/d6/CommentVariablePerCommentType.php b/core/modules/comment/src/Plugin/migrate/source/d6/CommentVariablePerCommentType.php index ea987be92..5d16ed029 100644 --- a/core/modules/comment/src/Plugin/migrate/source/d6/CommentVariablePerCommentType.php +++ b/core/modules/comment/src/Plugin/migrate/source/d6/CommentVariablePerCommentType.php @@ -20,7 +20,7 @@ class CommentVariablePerCommentType extends CommentVariable { $return = array(); foreach ($node_types as $node_type => $data) { // Only 2 comment types depending on subject field visibility. - if (empty($data['comment_subject_field'])) { + if (!empty($data['comment_subject_field'])) { // Default label and description should be set in migration. $return['comment'] = array( 'comment_type' => 'comment', diff --git a/core/modules/comment/tests/src/Kernel/Migrate/d7/MigrateCommentTest.php b/core/modules/comment/tests/src/Kernel/Migrate/d7/MigrateCommentTest.php index ed1917838..4d4df032e 100644 --- a/core/modules/comment/tests/src/Kernel/Migrate/d7/MigrateCommentTest.php +++ b/core/modules/comment/tests/src/Kernel/Migrate/d7/MigrateCommentTest.php @@ -40,7 +40,7 @@ class MigrateCommentTest extends MigrateDrupal7TestBase { ), )); $this->executeMigrations([ - 'd7_node:test_content_type', + 'd7_node', 'd7_comment_type', 'd7_comment', ]); diff --git a/core/modules/comment/tests/src/Unit/Migrate/d6/CommentVariablePerCommentTypeTest.php b/core/modules/comment/tests/src/Unit/Migrate/d6/CommentVariablePerCommentTypeTest.php index 0055a2ca8..75d0932a9 100644 --- a/core/modules/comment/tests/src/Unit/Migrate/d6/CommentVariablePerCommentTypeTest.php +++ b/core/modules/comment/tests/src/Unit/Migrate/d6/CommentVariablePerCommentTypeTest.php @@ -24,10 +24,10 @@ class CommentVariablePerCommentTypeTest extends MigrateSqlSourceTestCase { // Each result will also include a label and description, but those are // static values set by the source plugin and don't need to be asserted. array( - 'comment_type' => 'comment_no_subject', + 'comment_type' => 'comment', ), array( - 'comment_type' => 'comment', + 'comment_type' => 'comment_no_subject', ), ); diff --git a/core/modules/config/src/StorageReplaceDataWrapper.php b/core/modules/config/src/StorageReplaceDataWrapper.php index 58b3b492b..91c3a7cfc 100644 --- a/core/modules/config/src/StorageReplaceDataWrapper.php +++ b/core/modules/config/src/StorageReplaceDataWrapper.php @@ -44,6 +44,7 @@ class StorageReplaceDataWrapper implements StorageInterface { public function __construct(StorageInterface $storage, $collection = StorageInterface::DEFAULT_COLLECTION) { $this->storage = $storage; $this->collection = $collection; + $this->replacementData[$collection] = []; } /** @@ -104,7 +105,7 @@ class StorageReplaceDataWrapper implements StorageInterface { $this->replacementData[$this->collection][$new_name] = $this->replacementData[$this->collection][$name]; unset($this->replacementData[$this->collection][$name]); } - return $this->rename($name, $new_name); + return $this->storage->rename($name, $new_name); } /** @@ -164,8 +165,10 @@ class StorageReplaceDataWrapper implements StorageInterface { * {@inheritdoc} */ public function createCollection($collection) { - $this->collection = $collection; - return $this->storage->createCollection($collection); + return new static( + $this->storage->createCollection($collection), + $collection + ); } /** diff --git a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php index 876cbfdd4..7c4bc84cc 100644 --- a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php +++ b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php @@ -20,7 +20,10 @@ class ConfigSingleImportExportTest extends WebTestBase { public static $modules = [ 'block', 'config', - 'config_test' + 'config_test', + // Adding language module makes it possible to involve non-default + // (language.xx) collections in import/export operations. + 'language', ]; protected function setUp() { diff --git a/core/modules/field/src/Plugin/migrate/process/FieldType.php b/core/modules/field/src/Plugin/migrate/process/FieldType.php index 129694006..7874f9c3b 100644 --- a/core/modules/field/src/Plugin/migrate/process/FieldType.php +++ b/core/modules/field/src/Plugin/migrate/process/FieldType.php @@ -3,12 +3,12 @@ namespace Drupal\field\Plugin\migrate\process; use Drupal\Component\Plugin\Exception\PluginNotFoundException; -use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\migrate\MigrateExecutableInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\migrate\process\StaticMap; use Drupal\migrate\Row; +use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -21,7 +21,7 @@ class FieldType extends StaticMap implements ContainerFactoryPluginInterface { /** * The cckfield plugin manager. * - * @var \Drupal\Component\Plugin\PluginManagerInterface + * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface */ protected $cckPluginManager; @@ -41,12 +41,12 @@ class FieldType extends StaticMap implements ContainerFactoryPluginInterface { * The plugin ID. * @param mixed $plugin_definition * The plugin definition. - * @param \Drupal\Component\Plugin\PluginManagerInterface $cck_plugin_manager + * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_plugin_manager * The cckfield plugin manager. * @param \Drupal\migrate\Plugin\MigrationInterface $migration * The migration being run. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, PluginManagerInterface $cck_plugin_manager, MigrationInterface $migration = NULL) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrateCckFieldPluginManagerInterface $cck_plugin_manager, MigrationInterface $migration = NULL) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->cckPluginManager = $cck_plugin_manager; $this->migration = $migration; @@ -72,7 +72,8 @@ class FieldType extends StaticMap implements ContainerFactoryPluginInterface { $field_type = is_array($value) ? $value[0] : $value; try { - return $this->cckPluginManager->createInstance($field_type, [], $this->migration)->getFieldType($row); + $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, [], $this->migration); + return $this->cckPluginManager->createInstance($plugin_id, [], $this->migration)->getFieldType($row); } catch (PluginNotFoundException $e) { return parent::transform($value, $migrate_executable, $row, $destination_property); diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php index d781084b1..4e8be4b34 100644 --- a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php +++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php @@ -208,6 +208,7 @@ class EntityReferenceAdminTest extends WebTestBase { 'id' => 'node_test_view', 'label' => 'Node Test View', 'show[wizard_key]' => 'node', + 'show[sort]' => 'none', 'page[create]' => 1, 'page[title]' => 'Test Node View', 'page[path]' => 'test/node/view', @@ -221,6 +222,14 @@ class EntityReferenceAdminTest extends WebTestBase { 'style_options[search_fields][title]' => 'title', ); $this->drupalPostForm(NULL, $edit, t('Apply')); + + // Set sort to NID ascending. + $edit = [ + 'name[node_field_data.nid]' => 1, + ]; + $this->drupalPostForm('admin/structure/views/nojs/add-handler/node_test_view/entity_reference_1/sort', $edit, t('Add and configure sort criteria')); + $this->drupalPostForm(NULL, NULL, t('Apply')); + $this->drupalPostForm('admin/structure/views/view/node_test_view/edit/entity_reference_1', array(), t('Save')); $this->clickLink(t('Settings')); @@ -301,6 +310,7 @@ class EntityReferenceAdminTest extends WebTestBase { $this->assertText(t('Multiple entities match this reference;')); $this->assertText(t("@node1", ['@node1' => $node1->getTitle() . ' (' . $node1->id() . ')'])); $this->assertText(t("@node2", ['@node2' => $node2->getTitle() . ' (' . $node2->id() . ')'])); + $this->assertText(t('Specify the one you want by appending the id in parentheses, like "@example".', ['@example' => $node2->getTitle() . ' (' . $node2->id() . ')'])); $edit = array( 'title[0][value]' => 'Test', diff --git a/core/modules/field/tests/src/Kernel/BulkDeleteTest.php b/core/modules/field/tests/src/Kernel/BulkDeleteTest.php index 41fedfc7f..ae3d4e1c3 100644 --- a/core/modules/field/tests/src/Kernel/BulkDeleteTest.php +++ b/core/modules/field/tests/src/Kernel/BulkDeleteTest.php @@ -210,6 +210,98 @@ class BulkDeleteTest extends FieldKernelTestBase { $this->assertFalse(array_diff($found, array_keys($this->entities))); } + /** + * Tests that recreating a field with the name as a deleted field works. + */ + public function testPurgeWithDeletedAndActiveField() { + $bundle = reset($this->bundles); + // Create another field storage. + $field_name = 'bf_3'; + $deleted_field_storage = FieldStorageConfig::create(array( + 'field_name' => $field_name, + 'entity_type' => $this->entityTypeId, + 'type' => 'test_field', + 'cardinality' => 1 + )); + $deleted_field_storage->save(); + // Create the field. + FieldConfig::create([ + 'field_storage' => $deleted_field_storage, + 'bundle' => $bundle, + ])->save(); + + for ($i = 0; $i < 20; $i++) { + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId) + ->create(array('type' => $bundle)); + $entity->{$field_name}->setValue($this->_generateTestFieldValues(1)); + $entity->save(); + } + + // Delete the field. + $deleted_field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name); + $deleted_field->delete(); + $deleted_field_uuid = $deleted_field->uuid(); + + // Reload the field storage. + $field_storages = entity_load_multiple_by_properties('field_storage_config', array('uuid' => $deleted_field_storage->uuid(), 'include_deleted' => TRUE)); + $deleted_field_storage = reset($field_storages); + + // Create the field again. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => $field_name, + 'entity_type' => $this->entityTypeId, + 'type' => 'test_field', + 'cardinality' => 1 + )); + $field_storage->save(); + FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => $bundle, + ])->save(); + + // The field still exists, deleted, with the same field name. + $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $deleted_field_uuid, 'include_deleted' => TRUE)); + $this->assertTrue(isset($fields[$deleted_field_uuid]) && $fields[$deleted_field_uuid]->isDeleted(), 'The field exists and is deleted'); + $this->assertTrue(isset($fields[$deleted_field_uuid]) && $fields[$deleted_field_uuid]->getName() == $field_name); + + for ($i = 0; $i < 10; $i++) { + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId) + ->create(array('type' => $bundle)); + $entity->{$field_name}->setValue($this->_generateTestFieldValues(1)); + $entity->save(); + } + + // Check that the two field storages have different tables. + $storage = \Drupal::entityManager()->getStorage($this->entityTypeId); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $deleted_table_name = $table_mapping->getDedicatedDataTableName($deleted_field_storage, TRUE); + $active_table_name = $table_mapping->getDedicatedDataTableName($field_storage); + + field_purge_batch(50); + + // Ensure the new field still has its table and the deleted one has been + // removed. + $this->assertTrue(\Drupal::database()->schema()->tableExists($active_table_name)); + $this->assertFalse(\Drupal::database()->schema()->tableExists($deleted_table_name)); + + // The field has been removed from the system. + $fields = entity_load_multiple_by_properties('field_config', array('field_storage_uuid' => $deleted_field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE)); + $this->assertEqual(count($fields), 0, 'The field is gone'); + + // Verify there are still 10 entries in the main table. + $count = \Drupal::database() + ->select('entity_test__' . $field_name, 'f') + ->fields('f', array('entity_id')) + ->condition('bundle', $bundle) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 10); + } + /** * Verify that field data items and fields are purged when a field storage is * deleted. diff --git a/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php b/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php index 47822d2d3..5dc096f65 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\field\Kernel\EntityReference\Views; +use Drupal\entity_test\Entity\EntityTestMulChanged; use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; use Drupal\entity_test\Entity\EntityTest; use Drupal\entity_test\Entity\EntityTestMul; @@ -27,6 +28,7 @@ class EntityReferenceRelationshipTest extends ViewsKernelTestBase { */ public static $testViews = array( 'test_entity_reference_entity_test_view', + 'test_entity_reference_entity_test_view_long', 'test_entity_reference_reverse_entity_test_view', 'test_entity_reference_entity_test_mul_view', 'test_entity_reference_reverse_entity_test_mul_view', @@ -55,6 +57,7 @@ class EntityReferenceRelationshipTest extends ViewsKernelTestBase { $this->installEntitySchema('user'); $this->installEntitySchema('entity_test'); $this->installEntitySchema('entity_test_mul'); + $this->installEntitySchema('entity_test_mul_changed'); // Create reference from entity_test to entity_test_mul. $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_data', 'field_test_data', 'entity_test_mul'); @@ -62,6 +65,12 @@ class EntityReferenceRelationshipTest extends ViewsKernelTestBase { // Create reference from entity_test_mul to entity_test. $this->createEntityReferenceField('entity_test_mul', 'entity_test_mul', 'field_data_test', 'field_data_test', 'entity_test'); + // Create another field for testing with a long name. So it's storage name + // will become hashed. Use entity_test_mul_changed, so the resulting field + // tables created will be greater than 48 chars long. + // @see \Drupal\Core\Entity\Sql\DefaultTableMapping::generateFieldTableName() + $this->createEntityReferenceField('entity_test_mul_changed', 'entity_test_mul_changed', 'field_test_data_with_a_long_name', 'field_test_data_with_a_long_name', 'entity_test'); + ViewTestData::createTestViews(get_class($this), array('entity_reference_test_views')); } @@ -124,7 +133,6 @@ class EntityReferenceRelationshipTest extends ViewsKernelTestBase { // Test that the correct relationship entity is on the row. $this->assertEqual($row->_relationship_entities['field_test_data']->id(), 1); $this->assertEqual($row->_relationship_entities['field_test_data']->bundle(), 'entity_test_mul'); - } // Check the backwards reference view. @@ -225,4 +233,47 @@ class EntityReferenceRelationshipTest extends ViewsKernelTestBase { } } + /** + * Tests views data generated for relationship. + * + * @see entity_reference_field_views_data() + */ + public function testDataTableRelationshipWithLongFieldName() { + // Create some test entities which link each other. + $referenced_entity = EntityTest::create(); + $referenced_entity->save(); + + $entity = EntityTestMulChanged::create(); + $entity->field_test_data_with_a_long_name->target_id = $referenced_entity->id(); + $entity->save(); + $this->entities[] = $entity; + + $entity = EntityTestMulChanged::create(); + $entity->field_test_data_with_a_long_name->target_id = $referenced_entity->id(); + $entity->save(); + $this->entities[] = $entity; + + Views::viewsData()->clear(); + + // Check an actual test view. + $view = Views::getView('test_entity_reference_entity_test_view_long'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + // Check that the actual ID of the entity is the expected one. + $this->assertEqual($row->id, $this->entities[$index]->id()); + + // Also check that we have the correct result entity. + $this->assertEqual($row->_entity->id(), $this->entities[$index]->id()); + + // Test the forward relationship. + //$this->assertEqual($row->entity_test_entity_test_mul__field_data_test_id, 1); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['field_test_data_with_a_long_name']->id(), 1); + $this->assertEqual($row->_relationship_entities['field_test_data_with_a_long_name']->bundle(), 'entity_test'); + + } + } + } diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackFieldInstanceTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackFieldInstanceTest.php new file mode 100644 index 000000000..e71ac3ce9 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackFieldInstanceTest.php @@ -0,0 +1,80 @@ +<?php + +namespace Drupal\Tests\field\Kernel\Migrate\d7; + +use Drupal\field\Entity\FieldConfig; +use Drupal\migrate\MigrateExecutable; + +/** + * Migrates and rolls back Drupal 7 fields. + * + * @group field + */ +class RollbackFieldInstanceTest extends MigrateFieldInstanceTest { + + /** + * Tests migrating D7 fields to field_storage_config entities, then rolling back. + */ + public function testFieldInstances() { + // Test that the field instances have migrated (prior to rollback). + parent::testFieldInstances(); + + $this->executeRollback('d7_field_instance'); + $this->executeRollback('d7_field'); + + // Check that field instances have been rolled back. + $field_instance_ids = [ + 'comment.comment_node_page.comment_body', + 'node.page.body', + 'comment.comment_node_article.comment_body', + 'node.article.body', + 'node.article.field_tags', + 'node.article.field_image', + 'comment.comment_node_blog.comment_body', + 'node.blog.body', + 'comment.comment_node_book.comment_body', + 'node.book.body', + 'node.forum.taxonomy_forums', + 'comment.comment_node_forum.comment_body', + 'node.forum.body', + 'comment.comment_node_test_content_type.comment_body', + 'node.test_content_type.field_boolean', + 'node.test_content_type.field_email', + 'node.test_content_type.field_phone', + 'node.test_content_type.field_date', + 'node.test_content_type.field_date_with_end_time', + 'node.test_content_type.field_file', + 'node.test_content_type.field_float', + 'node.test_content_type.field_images', + 'node.test_content_type.field_integer', + 'node.test_content_type.field_link', + 'node.test_content_type.field_text_list', + 'node.test_content_type.field_integer_list', + 'node.test_content_type.field_long_text', + 'node.test_content_type.field_term_reference', + 'node.test_content_type.field_text', + 'comment.comment_node_test_content_type.field_integer', + 'user.user.field_file', + ]; + foreach ($field_instance_ids as $field_instance_id) { + $this->assertNull(FieldConfig::load($field_instance_id)); + } + } + + /** + * Executes a single rollback. + * + * @param string|\Drupal\migrate\Plugin\MigrationInterface $migration + * The migration to rollback, or its ID. + */ + protected function executeRollback($migration) { + if (is_string($migration)) { + $this->migration = $this->getMigration($migration); + } + else { + $this->migration = $migration; + } + (new MigrateExecutable($this->migration, $this))->rollback(); + } + +} diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackFieldTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackFieldTest.php new file mode 100644 index 000000000..7c5c6fd41 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackFieldTest.php @@ -0,0 +1,77 @@ +<?php + +namespace Drupal\Tests\field\Kernel\Migrate\d7; + +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\migrate\MigrateExecutable; + +/** + * Migrates and rolls back Drupal 7 fields. + * + * @group field + */ +class RollbackFieldTest extends MigrateFieldTest { + + /** + * Tests migrating D7 fields to field_storage_config entities, then rolling back. + */ + public function testFields() { + // Test that the fields have migrated (prior to rollback). + parent::testFields(); + + $this->executeRollback('d7_field'); + + // Check that fields have been rolled back. + $rolled_back_field_ids = [ + 'comment.field_integer', + 'node.taxonomy_forums', + 'node.field_integer', + 'node.field_tags', + 'node.field_term_reference', + 'node.field_text_list', + 'node.field_text', + 'node.field_phone', + 'node.field_file', + 'node.field_images', + 'node.field_image', + 'node.field_long_text', + 'node.field_date_with_end_time', + 'node.field_integer_list', + 'node.field_date', + 'node.field_link', + 'node.field_float', + 'node.field_boolean', + 'node.field_email', + 'user.field_file', + ]; + foreach ($rolled_back_field_ids as $field_id) { + $this->assertNull(FieldStorageConfig::load($field_id)); + } + + // Check that fields that should persist have not been rolled back. + $non_rolled_back_field_ids = [ + 'node.body', + 'comment.comment_body', + ]; + foreach ($non_rolled_back_field_ids as $field_id) { + $this->assertNotNull(FieldStorageConfig::load($field_id)); + } + } + + /** + * Executes a single rollback. + * + * @param string|\Drupal\migrate\Plugin\MigrationInterface $migration + * The migration to rollback, or its ID. + */ + protected function executeRollback($migration) { + if (is_string($migration)) { + $this->migration = $this->getMigration($migration); + } + else { + $this->migration = $migration; + } + (new MigrateExecutable($this->migration, $this))->rollback(); + } + +} diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackViewModesTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackViewModesTest.php new file mode 100644 index 000000000..b069387cf --- /dev/null +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/RollbackViewModesTest.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\Tests\field\Kernel\Migrate\d7; + +use Drupal\Core\Entity\Entity\EntityViewMode; +use Drupal\migrate\MigrateExecutable; + +/** + * Migrates and rolls back Drupal 7 view modes. + * + * @group field + */ +class RollbackViewModesTest extends MigrateViewModesTest { + + /** + * Tests migrating D7 view modes, then rolling back. + */ + public function testMigration() { + // Test that the view modes have migrated (prior to rollback). + parent::testMigration(); + + $this->executeRollback('d7_view_modes'); + + // Check that view modes have been rolled back. + $view_mode_ids = [ + 'comment.full', + 'node.teaser', + 'node.full', + 'user.full', + ]; + foreach ($view_mode_ids as $view_mode_id) { + $this->assertNull(EntityViewMode::load($view_mode_id)); + } + } + + /** + * Executes a single rollback. + * + * @param string|\Drupal\migrate\Plugin\MigrationInterface $migration + * The migration to rollback, or its ID. + */ + protected function executeRollback($migration) { + if (is_string($migration)) { + $this->migration = $this->getMigration($migration); + } + else { + $this->migration = $migration; + } + (new MigrateExecutable($this->migration, $this))->rollback(); + } + +} diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php b/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php index 3d064afcf..20f479750 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php @@ -81,9 +81,11 @@ class FileFieldItemList extends EntityReferenceFieldItemList { parent::delete(); $entity = $this->getEntity(); - // Delete all file usages within this entity. + // If a translation is deleted only decrement the file usage by one. If the + // default translation is deleted remove all file usages within this entity. + $count = $entity->isDefaultTranslation() ? 0 : 1; foreach ($this->referencedEntities() as $file) { - \Drupal::service('file.usage')->delete($file, 'file', $entity->getEntityTypeId(), $entity->id(), 0); + \Drupal::service('file.usage')->delete($file, 'file', $entity->getEntityTypeId(), $entity->id(), $count); } } diff --git a/core/modules/file/tests/src/Kernel/UsageTest.php b/core/modules/file/tests/src/Kernel/UsageTest.php index fdf12311b..8e2601510 100644 --- a/core/modules/file/tests/src/Kernel/UsageTest.php +++ b/core/modules/file/tests/src/Kernel/UsageTest.php @@ -2,6 +2,13 @@ namespace Drupal\Tests\file\Kernel; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\language\Entity\ContentLanguageSettings; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; + /** * Tests file usage functions. * @@ -203,4 +210,57 @@ class UsageTest extends FileManagedUnitTestBase { $this->assertTrue(file_exists($perm_new->getFileUri()), 'New permanent file was correctly ignored.'); } + /** + * Tests file usage with translated entities. + */ + public function testFileUsageWithEntityTranslation() { + /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */ + $file_usage = $this->container->get('file.usage'); + + $this->enableModules(['node', 'language']); + $this->installEntitySchema('node'); + $this->installSchema('node', ['node_access']); + + // Activate English and Romanian languages. + ConfigurableLanguage::create(['id' => 'en'])->save(); + ConfigurableLanguage::create(['id' => 'ro'])->save(); + + NodeType::create(['type' => 'page'])->save(); + ContentLanguageSettings::loadByEntityTypeBundle('node', 'page') + ->setLanguageAlterable(FALSE) + ->setDefaultLangcode('en') + ->save(); + // Create a file field attached to 'page' node-type. + FieldStorageConfig::create([ + 'type' => 'file', + 'entity_type' => 'node', + 'field_name' => 'file', + ])->save(); + FieldConfig::create([ + 'entity_type' => 'node', + 'bundle' => 'page', + 'field_name' => 'file', + 'label' => 'File', + ])->save(); + + // Create a node, attach a file and add a Romanian translation. + $node = Node::create(['type' => 'page', 'title' => 'Page']); + $node + ->set('file', $file = $this->createFile()) + ->addTranslation('ro', $node->getTranslation('en')->toArray()) + ->save(); + + // Check that the file is used twice. + $usage = $file_usage->listUsage($file); + $this->assertEquals(2, $usage['file']['node'][$node->id()]); + + // Remove the Romanian translation. + $node->removeTranslation('ro'); + $node->save(); + + // Check that one usage has been removed and is used only once now. + $usage = $file_usage->listUsage($file); + $this->assertEquals(1, $usage['file']['node'][$node->id()]); + } + } diff --git a/core/modules/filter/migration_templates/d6_filter_format.yml b/core/modules/filter/migration_templates/d6_filter_format.yml index 6b767f208..a5f438862 100644 --- a/core/modules/filter/migration_templates/d6_filter_format.yml +++ b/core/modules/filter/migration_templates/d6_filter_format.yml @@ -16,11 +16,16 @@ process: key: '@id' process: id: - plugin: static_map - default_value: filter_null + # If the filter ID cannot be mapped, it will be passed through + # unchanged because the bypass flag is set. The filter_id plugin + # will flatten the input value and default it to filter_null (the + # fallback filter plugin ID) if the flattened input value is not + # a valid plugin ID. + plugin: filter_id source: - module - delta + bypass: true map: filter: - filter_html diff --git a/core/modules/filter/migration_templates/d7_filter_format.yml b/core/modules/filter/migration_templates/d7_filter_format.yml index 2b44a80c0..c0710b5f0 100644 --- a/core/modules/filter/migration_templates/d7_filter_format.yml +++ b/core/modules/filter/migration_templates/d7_filter_format.yml @@ -15,11 +15,16 @@ process: key: '@id' process: id: - plugin: static_map + # If the filter ID cannot be mapped, it will pass through unmodified + # because the bypass flag is set. When the user actually tries to + # view text through an invalid filter plugin, the filter system will + # fall back to filter_null and display a helpful error message. + plugin: filter_id bypass: true source: name - map: - php_code: filter_null + # No need to map anything -- filter plugin IDs haven't changed since + # Drupal 7. + map: { } settings: plugin: filter_settings source: settings diff --git a/core/modules/filter/src/Plugin/migrate/process/FilterID.php b/core/modules/filter/src/Plugin/migrate/process/FilterID.php new file mode 100644 index 000000000..26569860f --- /dev/null +++ b/core/modules/filter/src/Plugin/migrate/process/FilterID.php @@ -0,0 +1,90 @@ +<?php + +namespace Drupal\filter\Plugin\migrate\process; + +use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\migrate\process\StaticMap; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * @MigrateProcessPlugin( + * id = "filter_id" + * ) + */ +class FilterID extends StaticMap implements ContainerFactoryPluginInterface { + + /** + * The filter plugin manager. + * + * @var \Drupal\Component\Plugin\PluginManagerInterface|\Drupal\Component\Plugin\FallbackPluginManagerInterface + */ + protected $filterManager; + + /** + * FilterID constructor. + * + * @param array $configuration + * Plugin configuration. + * @param string $plugin_id + * The plugin ID. + * @param mixed $plugin_definition + * The plugin definition. + * @param \Drupal\Component\Plugin\PluginManagerInterface $filter_manager + * The filter plugin manager. + * @param TranslationInterface $translator + * (optional) The string translation service. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, PluginManagerInterface $filter_manager, TranslationInterface $translator = NULL) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->filterManager = $filter_manager; + $this->stringTranslation = $translator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('plugin.manager.filter'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + $plugin_id = parent::transform($value, $migrate_executable, $row, $destination_property); + + // If the static map is bypassed on failure, the returned plugin ID will be + // an array if $value was. Plugin IDs cannot be arrays, so flatten it before + // passing it into the filter manager. + if (is_array($plugin_id)) { + $plugin_id = implode(':', $plugin_id); + } + + if ($this->filterManager->hasDefinition($plugin_id)) { + return $plugin_id; + } + else { + $fallback = $this->filterManager->getFallbackPluginId($plugin_id); + + $message = $this->t('Filter @plugin_id could not be mapped to an existing filter plugin; defaulting to @fallback.', [ + '@plugin_id' => $plugin_id, + '@fallback' => $fallback, + ]); + $migrate_executable->saveMessage((string) $message, MigrationInterface::MESSAGE_WARNING); + + return $fallback; + } + } + +} diff --git a/core/modules/filter/tests/src/Kernel/Migrate/d6/MigrateFilterFormatTest.php b/core/modules/filter/tests/src/Kernel/Migrate/d6/MigrateFilterFormatTest.php index ed5f0fbfe..81faf3e51 100644 --- a/core/modules/filter/tests/src/Kernel/Migrate/d6/MigrateFilterFormatTest.php +++ b/core/modules/filter/tests/src/Kernel/Migrate/d6/MigrateFilterFormatTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\filter\Kernel\Migrate\d6; use Drupal\filter\Entity\FilterFormat; +use Drupal\filter\FilterFormatInterface; use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase; /** @@ -39,14 +40,18 @@ class MigrateFilterFormatTest extends MigrateDrupal6TestBase { $this->assertFalse(isset($filters['filter_html_image_secure'])); // Check variables migrated into filter. - $this->assertIdentical('<a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd>', $filters['filter_html']['settings']['allowed_html']); - $this->assertIdentical(TRUE, $filters['filter_html']['settings']['filter_html_help']); - $this->assertIdentical(FALSE, $filters['filter_html']['settings']['filter_html_nofollow']); - $this->assertIdentical(72, $filters['filter_url']['settings']['filter_url_length']); + $this->assertSame('<a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd>', $filters['filter_html']['settings']['allowed_html']); + $this->assertSame(TRUE, $filters['filter_html']['settings']['filter_html_help']); + $this->assertSame(FALSE, $filters['filter_html']['settings']['filter_html_nofollow']); + $this->assertSame(72, $filters['filter_url']['settings']['filter_url_length']); - // Check that the PHP code filter is converted to filter_null. - $filters = FilterFormat::load('php_code')->get('filters'); - $this->assertTrue(isset($filters['filter_null'])); + // Assert that the php_code format was migrated with filter_null in the + // php_code filter's place. + $filter_format = FilterFormat::load('php_code'); + $this->assertInstanceOf(FilterFormatInterface::class, $filter_format); + $filters = $filter_format->get('filters'); + $this->assertArrayHasKey('filter_null', $filters); + $this->assertArrayNotHasKey('php_code', $filters); } } diff --git a/core/modules/filter/tests/src/Kernel/Migrate/d7/MigrateFilterFormatTest.php b/core/modules/filter/tests/src/Kernel/Migrate/d7/MigrateFilterFormatTest.php index 8eb13b9d8..e4053ae18 100644 --- a/core/modules/filter/tests/src/Kernel/Migrate/d7/MigrateFilterFormatTest.php +++ b/core/modules/filter/tests/src/Kernel/Migrate/d7/MigrateFilterFormatTest.php @@ -42,13 +42,13 @@ class MigrateFilterFormatTest extends MigrateDrupal7TestBase { protected function assertEntity($id, $label, array $enabled_filters, $weight) { /** @var \Drupal\filter\FilterFormatInterface $entity */ $entity = FilterFormat::load($id); - $this->assertTrue($entity instanceof FilterFormatInterface); - $this->assertIdentical($label, $entity->label()); + $this->assertInstanceOf(FilterFormatInterface::class, $entity); + $this->assertSame($label, $entity->label()); // get('filters') will return enabled filters only, not all of them. - $this->assertIdentical(array_keys($enabled_filters), array_keys($entity->get('filters'))); - $this->assertIdentical($weight, $entity->get('weight')); + $this->assertSame(array_keys($enabled_filters), array_keys($entity->get('filters'))); + $this->assertSame($weight, $entity->get('weight')); foreach ($entity->get('filters') as $filter_id => $filter) { - $this->assertIdentical($filter['weight'], $enabled_filters[$filter_id]); + $this->assertSame($filter['weight'], $enabled_filters[$filter_id]); } } @@ -68,15 +68,17 @@ class MigrateFilterFormatTest extends MigrateDrupal7TestBase { // Ensure that filter-specific settings were migrated. /** @var \Drupal\filter\FilterFormatInterface $format */ $format = FilterFormat::load('filtered_html'); + $this->assertInstanceOf(FilterFormatInterface::class, $format); $config = $format->filters('filter_html')->getConfiguration(); - $this->assertIdentical('<div> <span> <ul type> <li> <ol start type> <a href hreflang> <img src alt height width>', $config['settings']['allowed_html']); + $this->assertSame('<div> <span> <ul type> <li> <ol start type> <a href hreflang> <img src alt height width>', $config['settings']['allowed_html']); $config = $format->filters('filter_url')->getConfiguration(); - $this->assertIdentical(128, $config['settings']['filter_url_length']); + $this->assertSame(128, $config['settings']['filter_url_length']); // The php_code format gets migrated, but the php_code filter is changed to // filter_null. - $filters = FilterFormat::load('php_code')->get('filters'); - $this->assertTrue(isset($filters['filter_null'])); + $format = FilterFormat::load('php_code'); + $this->assertInstanceOf(FilterFormatInterface::class, $format); + $this->assertArrayHasKey('filter_null', $format->get('filters')); } } diff --git a/core/modules/filter/tests/src/Kernel/Plugin/migrate/process/FilterIdTest.php b/core/modules/filter/tests/src/Kernel/Plugin/migrate/process/FilterIdTest.php new file mode 100644 index 000000000..39e4037c1 --- /dev/null +++ b/core/modules/filter/tests/src/Kernel/Plugin/migrate/process/FilterIdTest.php @@ -0,0 +1,119 @@ +<?php + +namespace Drupal\Tests\filter\Kernel\Plugin\migrate\process; + +use Drupal\filter\Plugin\migrate\process\FilterID; +use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Row; + +/** + * Unit tests of the filter_id plugin. + * + * @coversDefaultClass \Drupal\filter\Plugin\migrate\process\FilterID + * @group filter + */ +class FilterIdTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['filter']; + + /** + * The mocked MigrateExecutable. + * + * @var MigrateExecutableInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $executable; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->executable = $this->getMock(MigrateExecutableInterface::class); + } + + /** + * Tests the filter_id plugin. + * + * @param mixed $value + * The input value to the plugin. + * @param string $expected_value + * The output value expected from the plugin. + * @param string $invalid_id + * (optional) The invalid plugin ID which is expected to be logged by the + * MigrateExecutable object. + * + * @dataProvider testProvider + * + * @covers ::transform + */ + public function test($value, $expected_value, $invalid_id = NULL) { + $configuration = [ + 'bypass' => TRUE, + 'map' => [ + 'foo' => 'filter_html', + 'baz' => 'php_code', + ], + ]; + $plugin = FilterID::create($this->container, $configuration, 'filter_id', []); + + if (isset($invalid_id)) { + $this->executable + ->expects($this->exactly(1)) + ->method('saveMessage') + ->with( + 'Filter ' . $invalid_id . ' could not be mapped to an existing filter plugin; defaulting to filter_null.', + MigrationInterface::MESSAGE_WARNING + ); + } + + $row = new Row([], []); + $output_value = $plugin->transform($value, $this->executable, $row, 'foo'); + + $this->assertSame($expected_value, $output_value); + } + + /** + * The test data provider. + * + * @return array + */ + public function testProvider() { + return [ + // The filter ID is mapped, and the plugin exists. + [ + 'foo', + 'filter_html', + ], + // The filter ID isn't mapped, but it's unchanged from the source (i.e., + // it bypasses the static map) and the plugin exists. + [ + 'filter_html', + 'filter_html', + ], + // The filter ID is mapped, but the plugin does not exist. + [ + 'baz', + 'filter_null', + 'php_code', + ], + // The filter ID isn't mapped, but it's unchanged from the source (i.e., + // it bypasses the static map) but the plugin does not exist. + [ + 'php_code', + 'filter_null', + 'php_code', + ], + [ + ['filter', 1], + 'filter_null', + 'filter:1', + ], + ]; + } + +} diff --git a/core/modules/image/src/Controller/ImageStyleDownloadController.php b/core/modules/image/src/Controller/ImageStyleDownloadController.php index 885557c5b..5da6bc89d 100644 --- a/core/modules/image/src/Controller/ImageStyleDownloadController.php +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php @@ -113,14 +113,9 @@ class ImageStyleDownloadController extends FileDownloadController { // If using the private scheme, let other modules provide headers and // control access to the file. if ($scheme == 'private') { - if (file_exists($derivative_uri)) { - return parent::download($request, $scheme); - } - else { - $headers = $this->moduleHandler()->invokeAll('file_download', array($image_uri)); - if (in_array(-1, $headers) || empty($headers)) { - throw new AccessDeniedHttpException(); - } + $headers = $this->moduleHandler()->invokeAll('file_download', array($image_uri)); + if (in_array(-1, $headers) || empty($headers)) { + throw new AccessDeniedHttpException(); } } diff --git a/core/modules/image/src/ImageStyleListBuilder.php b/core/modules/image/src/ImageStyleListBuilder.php index e4e0be658..140ed46b8 100644 --- a/core/modules/image/src/ImageStyleListBuilder.php +++ b/core/modules/image/src/ImageStyleListBuilder.php @@ -4,10 +4,7 @@ namespace Drupal\image; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Routing\UrlGeneratorInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Url; /** * Defines a class to build a listing of image style entities. @@ -16,40 +13,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class ImageStyleListBuilder extends ConfigEntityListBuilder { - /** - * The URL generator. - * - * @var \Drupal\Core\Routing\UrlGeneratorInterface - */ - protected $urlGenerator; - - /** - * Constructs a new ImageStyleListBuilder object. - * - * @param EntityTypeInterface $entity_type - * The entity type definition. - * @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage - * The image style entity storage class. - * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator - * The URL generator. - */ - public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $image_style_storage, UrlGeneratorInterface $url_generator) { - parent::__construct($entity_type, $image_style_storage); - $this->urlGenerator = $url_generator; - } - - /** - * {@inheritdoc} - */ - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( - $entity_type, - $container->get('entity.manager')->getStorage($entity_type->id()), - $container->get('url_generator'), - $container->get('string_translation') - ); - } - /** * {@inheritdoc} */ @@ -87,7 +50,7 @@ class ImageStyleListBuilder extends ConfigEntityListBuilder { public function render() { $build = parent::render(); $build['table']['#empty'] = $this->t('There are currently no styles. <a href=":url">Add a new one</a>.', [ - ':url' => $this->urlGenerator->generateFromRoute('image.style_add'), + ':url' => Url::fromRoute('image.style_add')->toString(), ]); return $build; } diff --git a/core/modules/image/src/Tests/ImageStylesPathAndUrlTest.php b/core/modules/image/src/Tests/ImageStylesPathAndUrlTest.php index f146ccc5a..1fed32f3b 100644 --- a/core/modules/image/src/Tests/ImageStylesPathAndUrlTest.php +++ b/core/modules/image/src/Tests/ImageStylesPathAndUrlTest.php @@ -155,6 +155,11 @@ class ImageStylesPathAndUrlTest extends WebTestBase { $image = $this->container->get('image.factory')->get($generated_uri); $this->assertEqual($this->drupalGetHeader('Content-Type'), $image->getMimeType(), 'Expected Content-Type was reported.'); $this->assertEqual($this->drupalGetHeader('Content-Length'), $image->getFileSize(), 'Expected Content-Length was reported.'); + + // Check that we did not download the original file. + $original_image = $this->container->get('image.factory')->get($original_uri); + $this->assertNotEqual($this->drupalGetHeader('Content-Length'), $original_image->getFileSize()); + if ($scheme == 'private') { $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.'); $this->assertNotEqual(strpos($this->drupalGetHeader('Cache-Control'), 'no-cache'), FALSE, 'Cache-Control header contains \'no-cache\' to prevent caching.'); @@ -165,6 +170,12 @@ class ImageStylesPathAndUrlTest extends WebTestBase { $this->drupalGet($generate_url); $this->assertResponse(200, 'Image was generated at the URL.'); + // Check that the second request also returned the generated image. + $this->assertEqual($this->drupalGetHeader('Content-Length'), $image->getFileSize()); + + // Check that we did not download the original file. + $this->assertNotEqual($this->drupalGetHeader('Content-Length'), $original_image->getFileSize()); + // Make sure that access is denied for existing style files if we do not // have access. \Drupal::state()->delete('image.test_file_download'); diff --git a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php index 3c92345ba..6e98ae225 100644 --- a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php +++ b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php @@ -169,7 +169,7 @@ class LinkItem extends FieldItemBase implements LinkItemInterface { * {@inheritdoc} */ public function getUrl() { - return Url::fromUri($this->uri, $this->options); + return Url::fromUri($this->uri, (array) $this->options); } /** diff --git a/core/modules/link/tests/src/Kernel/LinkItemTest.php b/core/modules/link/tests/src/Kernel/LinkItemTest.php index 6473efbf1..2785164ba 100644 --- a/core/modules/link/tests/src/Kernel/LinkItemTest.php +++ b/core/modules/link/tests/src/Kernel/LinkItemTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\link\Kernel; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldItemInterface; +use Drupal\Core\Url; use Drupal\entity_test\Entity\EntityTest; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; @@ -154,6 +155,11 @@ class LinkItemTest extends FieldKernelTestBase { $this->assertNull($entity->field_test->title); $this->assertIdentical($entity->field_test->options, []); + // Check that setting options to NULL does not trigger an error when + // calling getUrl(); + $entity->field_test->options = NULL; + $this->assertInstanceOf(Url::class, $entity->field_test[0]->getUrl()); + // Check that setting LinkItem value NULL doesn't generate any error or // warning. $entity->field_test[0] = NULL; diff --git a/core/modules/migrate/src/Event/MigrateEvents.php b/core/modules/migrate/src/Event/MigrateEvents.php index edc2beccb..e2de42eb9 100644 --- a/core/modules/migrate/src/Event/MigrateEvents.php +++ b/core/modules/migrate/src/Event/MigrateEvents.php @@ -81,7 +81,7 @@ final class MigrateEvents { * * This event allows modules to perform an action whenever a specific item * is about to be saved by the destination plugin. The event listener method - * receives a \Drupal\migrate\Event\MigratePreSaveEvent instance. + * receives a \Drupal\migrate\Event\MigratePreRowSaveEvent instance. * * @Event * diff --git a/core/modules/migrate/src/MigrateExecutable.php b/core/modules/migrate/src/MigrateExecutable.php index c0b3537ac..ef5b86c4f 100644 --- a/core/modules/migrate/src/MigrateExecutable.php +++ b/core/modules/migrate/src/MigrateExecutable.php @@ -329,6 +329,12 @@ class MigrateExecutable implements MigrateExecutableInterface { // We're now done with this row, so remove it from the map. $id_map->deleteDestination($destination_key); } + else { + // If there is no destination key the import probably failed and we can + // remove the row without further action. + $source_key = $id_map->currentSource(); + $id_map->delete($source_key); + } // Check for memory exhaustion. if (($return = $this->checkStatus()) != MigrationInterface::RESULT_COMPLETED) { diff --git a/core/modules/migrate/src/Plugin/MigrateIdMapInterface.php b/core/modules/migrate/src/Plugin/MigrateIdMapInterface.php index d3bdb8923..eafac2966 100644 --- a/core/modules/migrate/src/Plugin/MigrateIdMapInterface.php +++ b/core/modules/migrate/src/Plugin/MigrateIdMapInterface.php @@ -243,6 +243,14 @@ interface MigrateIdMapInterface extends \Iterator, PluginInspectionInterface { */ public function currentDestination(); + /** + * Looks up the source identifier(s) currently being iterated. + * + * @return array + * The source identifier values of the record, or NULL on failure. + */ + public function currentSource(); + /** * Removes any persistent storage used by this map. * diff --git a/core/modules/migrate/src/Plugin/MigratePluginManager.php b/core/modules/migrate/src/Plugin/MigratePluginManager.php index a51efdae2..8fd1644e5 100644 --- a/core/modules/migrate/src/Plugin/MigratePluginManager.php +++ b/core/modules/migrate/src/Plugin/MigratePluginManager.php @@ -21,7 +21,7 @@ use Drupal\Core\Plugin\DefaultPluginManager; * * @ingroup migration */ -class MigratePluginManager extends DefaultPluginManager { +class MigratePluginManager extends DefaultPluginManager implements MigratePluginManagerInterface { /** * Constructs a MigratePluginManager object. @@ -49,8 +49,6 @@ class MigratePluginManager extends DefaultPluginManager { /** * {@inheritdoc} - * - * A specific createInstance method is necessary to pass the migration on. */ public function createInstance($plugin_id, array $configuration = array(), MigrationInterface $migration = NULL) { $plugin_definition = $this->getDefinition($plugin_id); diff --git a/core/modules/migrate/src/Plugin/MigratePluginManagerInterface.php b/core/modules/migrate/src/Plugin/MigratePluginManagerInterface.php new file mode 100644 index 000000000..d6ed39fdc --- /dev/null +++ b/core/modules/migrate/src/Plugin/MigratePluginManagerInterface.php @@ -0,0 +1,29 @@ +<?php + +namespace Drupal\migrate\Plugin; + +use Drupal\Component\Plugin\PluginManagerInterface; + +interface MigratePluginManagerInterface extends PluginManagerInterface { + + /** + * Creates a pre-configured instance of a migration plugin. + * + * A specific createInstance method is necessary to pass the migration on. + * + * @param string $plugin_id + * The ID of the plugin being instantiated. + * @param array $configuration + * An array of configuration relevant to the plugin instance. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration context in which the plugin will run. + * + * @return object + * A fully configured plugin instance. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * If the instance cannot be created, such as if the ID is invalid. + */ + public function createInstance($plugin_id, array $configuration = [], MigrationInterface $migration = NULL); + +} diff --git a/core/modules/migrate/src/Plugin/Migration.php b/core/modules/migrate/src/Plugin/Migration.php index abad54f4d..be3b65bd5 100644 --- a/core/modules/migrate/src/Plugin/Migration.php +++ b/core/modules/migrate/src/Plugin/Migration.php @@ -268,16 +268,16 @@ class Migration extends PluginBase implements MigrationInterface, RequirementsIn * The plugin definition. * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager * The migration plugin manager. - * @param \Drupal\migrate\Plugin\MigratePluginManager $source_plugin_manager + * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $source_plugin_manager * The source migration plugin manager. - * @param \Drupal\migrate\Plugin\MigratePluginManager $process_plugin_manager + * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $process_plugin_manager * The process migration plugin manager. * @param \Drupal\migrate\Plugin\MigrateDestinationPluginManager $destination_plugin_manager * The destination migration plugin manager. - * @param \Drupal\migrate\Plugin\MigratePluginManager $idmap_plugin_manager + * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $idmap_plugin_manager * The ID map migration plugin manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManager $source_plugin_manager, MigratePluginManager $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManager $idmap_plugin_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManagerInterface $source_plugin_manager, MigratePluginManagerInterface $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManagerInterface $idmap_plugin_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->migrationPluginManager = $migration_plugin_manager; $this->sourcePluginManager = $source_plugin_manager; diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldInstance.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldInstance.php index 54bc35843..966cb3708 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldInstance.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldInstance.php @@ -21,4 +21,12 @@ class EntityFieldInstance extends EntityConfigBase { return $ids; } + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifier) { + $destination_identifier = implode('.', $destination_identifier); + parent::rollback(array($destination_identifier)); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldStorageConfig.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldStorageConfig.php index 6851f2708..7ad01045a 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldStorageConfig.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldStorageConfig.php @@ -20,4 +20,12 @@ class EntityFieldStorageConfig extends EntityConfigBase { return $ids; } + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifier) { + $destination_identifier = implode('.', $destination_identifier); + parent::rollback(array($destination_identifier)); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityViewMode.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityViewMode.php index 810152aa1..fad171b59 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityViewMode.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityViewMode.php @@ -20,4 +20,12 @@ class EntityViewMode extends EntityConfigBase { return $ids; } + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifier) { + $destination_identifier = implode('.', $destination_identifier); + parent::rollback(array($destination_identifier)); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php index cf39275f8..cbf4a9f46 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -845,7 +845,25 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP if ($this->valid()) { $result = array(); foreach ($this->destinationIdFields() as $destination_field_name => $idmap_field_name) { - $result[$destination_field_name] = $this->currentRow[$idmap_field_name]; + if (!is_null($this->currentRow[$idmap_field_name])) { + $result[$destination_field_name] = $this->currentRow[$idmap_field_name]; + } + } + return $result; + } + else { + return NULL; + } + } + + /** + * @inheritdoc + */ + public function currentSource() { + if ($this->valid()) { + $result = array(); + foreach ($this->sourceIdFields() as $field_name => $source_id) { + $result[$field_name] = $this->currentKey[$source_id]; } return $result; } diff --git a/core/modules/migrate/src/Plugin/migrate/process/Callback.php b/core/modules/migrate/src/Plugin/migrate/process/Callback.php index 2565a31fb..618e05d4b 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Callback.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Callback.php @@ -15,6 +15,8 @@ use Drupal\migrate\Row; * arguments can be passed to the callback as this would make the migration YAML * file too complex. * + * @link https://www.drupal.org/node/2181783 Online handbook documentation for callback process plugin @endlink + * * @MigrateProcessPlugin( * id = "callback" * ) diff --git a/core/modules/migrate/src/Plugin/migrate/process/Concat.php b/core/modules/migrate/src/Plugin/migrate/process/Concat.php index bb1891e30..c013bdc78 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Concat.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Concat.php @@ -10,6 +10,8 @@ use Drupal\migrate\Row; /** * Concatenates the strings in the current value. * + * @link https://www.drupal.org/node/2345927 Online handbook documentation for concat process plugin @endlink + * * @MigrateProcessPlugin( * id = "concat", * handle_multiples = TRUE diff --git a/core/modules/migrate/src/Plugin/migrate/process/DedupeBase.php b/core/modules/migrate/src/Plugin/migrate/process/DedupeBase.php index a15d058c2..adaf72009 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/DedupeBase.php +++ b/core/modules/migrate/src/Plugin/migrate/process/DedupeBase.php @@ -15,6 +15,8 @@ use Drupal\Component\Utility\Unicode; * creating filter format names, the current value is checked against the * existing filter format names and if it exists, a numeric postfix is added * and incremented until a unique value is created. + * + * @link https://www.drupal.org/node/2345929 Online handbook documentation for dedupebase process plugin @endlink */ abstract class DedupeBase extends ProcessPluginBase { diff --git a/core/modules/migrate/src/Plugin/migrate/process/DedupeEntity.php b/core/modules/migrate/src/Plugin/migrate/process/DedupeEntity.php index b04dba719..d2afe8f58 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/DedupeEntity.php +++ b/core/modules/migrate/src/Plugin/migrate/process/DedupeEntity.php @@ -10,6 +10,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Ensures value is not duplicated against an entity field. * + * @link https://www.drupal.org/node/2135325 Online handbook documentation for dedupe_entity process plugin @endlink + * * @MigrateProcessPlugin( * id = "dedupe_entity" * ) diff --git a/core/modules/migrate/src/Plugin/migrate/process/DefaultValue.php b/core/modules/migrate/src/Plugin/migrate/process/DefaultValue.php index e9ff3d9f5..db216f6f0 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/DefaultValue.php +++ b/core/modules/migrate/src/Plugin/migrate/process/DefaultValue.php @@ -9,6 +9,8 @@ use Drupal\migrate\Row; /** * This plugin sets missing values on the destination. * + * @link https://www.drupal.org/node/2135313 Online handbook documentation for default_value process plugin @endlink + * * @MigrateProcessPlugin( * id = "default_value" * ) diff --git a/core/modules/migrate/src/Plugin/migrate/process/Explode.php b/core/modules/migrate/src/Plugin/migrate/process/Explode.php index 5a22a2170..fd3cc9cfb 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Explode.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Explode.php @@ -10,6 +10,8 @@ use Drupal\migrate\Row; /** * This plugin explodes a delimited string into an array of values. * + * @link https://www.drupal.org/node/2674504 Online handbook documentation for explode process plugin @endlink + * * @MigrateProcessPlugin( * id = "explode" * ) diff --git a/core/modules/migrate/src/Plugin/migrate/process/Extract.php b/core/modules/migrate/src/Plugin/migrate/process/Extract.php index 123388cef..81671d879 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Extract.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Extract.php @@ -11,7 +11,7 @@ use Drupal\migrate\Row; /** * This plugin extracts a value from an array. * - * @see https://www.drupal.org/node/2152731 + * @link https://www.drupal.org/node/2152731 Online handbook documentation for extract process plugin @endlink * * @MigrateProcessPlugin( * id = "extract" diff --git a/core/modules/migrate/src/Plugin/migrate/process/Flatten.php b/core/modules/migrate/src/Plugin/migrate/process/Flatten.php index e10bc63c9..f663e4d95 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Flatten.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Flatten.php @@ -12,7 +12,7 @@ use Drupal\migrate\Row; * once a single value gets transformed into multiple values. This plugin will * flatten them back down to single values again. * - * @see https://www.drupal.org/node/2154215 + * @link https://www.drupal.org/node/2154215 Online handbook documentation for flatten process plugin @endlink * * @MigrateProcessPlugin( * id = "flatten", diff --git a/core/modules/migrate/src/Plugin/migrate/process/Get.php b/core/modules/migrate/src/Plugin/migrate/process/Get.php index 20a9ba8c9..f6b3c34e8 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Get.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Get.php @@ -9,6 +9,8 @@ use Drupal\migrate\Row; /** * This plugin copies from the source to the destination. * + * @link https://www.drupal.org/node/2135307 Online handbook documentation for get process plugin @endlink + * * @MigrateProcessPlugin( * id = "get" * ) diff --git a/core/modules/migrate/src/Plugin/migrate/process/Iterator.php b/core/modules/migrate/src/Plugin/migrate/process/Iterator.php index 7affba94d..70df9baa4 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Iterator.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Iterator.php @@ -9,7 +9,7 @@ use Drupal\migrate\Row; /** * This plugin iterates and processes an array. * - * @see https://www.drupal.org/node/2135345 + * @link https://www.drupal.org/node/2135345 Online handbook documentation for iterator process plugin @endlink * * @MigrateProcessPlugin( * id = "iterator", diff --git a/core/modules/migrate/src/Plugin/migrate/process/MachineName.php b/core/modules/migrate/src/Plugin/migrate/process/MachineName.php index aef8c24c5..15ed35677 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/MachineName.php +++ b/core/modules/migrate/src/Plugin/migrate/process/MachineName.php @@ -17,6 +17,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * and replaced by an underscore and multiple underscores are collapsed into * one. * + * @link https://www.drupal.org/node/2135323 Online handbook documentation for machine_name process plugin @endlink + * * @MigrateProcessPlugin( * id = "machine_name" * ) diff --git a/core/modules/migrate/src/Plugin/migrate/process/Migration.php b/core/modules/migrate/src/Plugin/migrate/process/Migration.php index c67c75497..a8d5b601b 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Migration.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Migration.php @@ -4,7 +4,7 @@ namespace Drupal\migrate\Plugin\migrate\process; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\migrate\MigrateSkipProcessException; -use Drupal\migrate\Plugin\MigratePluginManager; +use Drupal\migrate\Plugin\MigratePluginManagerInterface; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate\ProcessPluginBase; @@ -16,6 +16,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Calculates the value of a property based on a previous migration. * + * @link https://www.drupal.org/node/2149801 Online handbook documentation for migration process plugin @endlink + * * @MigrateProcessPlugin( * id = "migration" * ) @@ -39,7 +41,7 @@ class Migration extends ProcessPluginBase implements ContainerFactoryPluginInter /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManager $process_plugin_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManagerInterface $process_plugin_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->migrationPluginManager = $migration_plugin_manager; $this->migration = $migration; diff --git a/core/modules/migrate/src/Plugin/migrate/process/Route.php b/core/modules/migrate/src/Plugin/migrate/process/Route.php index ff04a527f..2c0144bcb 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Route.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Route.php @@ -11,7 +11,10 @@ use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\Row; /** - * @MigrateProcessPlugin( + * + * @link https://www.drupal.org/node/2750777 Online handbook documentation for route process plugin @endlink + * + * * @MigrateProcessPlugin( * id = "route" * ) */ diff --git a/core/modules/migrate/src/Plugin/migrate/process/SkipOnEmpty.php b/core/modules/migrate/src/Plugin/migrate/process/SkipOnEmpty.php index e88946bb3..28b6df0d7 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/SkipOnEmpty.php +++ b/core/modules/migrate/src/Plugin/migrate/process/SkipOnEmpty.php @@ -11,6 +11,8 @@ use Drupal\migrate\MigrateSkipRowException; /** * If the source evaluates to empty, we skip processing or the whole row. * + * @link https://www.drupal.org/node/2228793 Online handbook documentation for skip_on_empty process plugin @endlink + * * @MigrateProcessPlugin( * id = "skip_on_empty" * ) diff --git a/core/modules/migrate/src/Plugin/migrate/process/SkipRowIfNotSet.php b/core/modules/migrate/src/Plugin/migrate/process/SkipRowIfNotSet.php index 4b5e3397b..a6cffceb4 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/SkipRowIfNotSet.php +++ b/core/modules/migrate/src/Plugin/migrate/process/SkipRowIfNotSet.php @@ -10,6 +10,8 @@ use Drupal\migrate\MigrateSkipRowException; /** * If the source evaluates to empty, we skip the current row. * + * @link https://www.drupal.org/node/2345935 Online handbook documentation for skip_row_if_not_set process plugin @endlink + * * @MigrateProcessPlugin( * id = "skip_row_if_not_set", * handle_multiples = TRUE diff --git a/core/modules/migrate/src/Plugin/migrate/process/StaticMap.php b/core/modules/migrate/src/Plugin/migrate/process/StaticMap.php index aa74736e0..769da3b7e 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/StaticMap.php +++ b/core/modules/migrate/src/Plugin/migrate/process/StaticMap.php @@ -12,7 +12,7 @@ use Drupal\migrate\MigrateSkipRowException; /** * This plugin changes the current value based on a static lookup map. * - * @see https://www.drupal.org/node/2143521 + * @link https://www.drupal.org/node/2143521 Online handbook documentation for static_map process plugin @endlink * * @MigrateProcessPlugin( * id = "static_map" diff --git a/core/modules/migrate/src/Plugin/migrate/process/Substr.php b/core/modules/migrate/src/Plugin/migrate/process/Substr.php index ab6171a4d..8ebc98c6d 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Substr.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Substr.php @@ -11,6 +11,8 @@ use Drupal\Component\Utility\Unicode; /** * This plugin returns a substring of the current value. * + * @link https://www.drupal.org/node/2771965 Online handbook documentation for substr process plugin @endlink + * * @MigrateProcessPlugin( * id = "substr" * ) diff --git a/core/modules/migrate/tests/src/Kernel/MigrateRollbackTest.php b/core/modules/migrate/tests/src/Kernel/MigrateRollbackTest.php index 9ec0c65e0..4b78e6526 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateRollbackTest.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateRollbackTest.php @@ -128,6 +128,9 @@ class MigrateRollbackTest extends MigrateTestBase { $this->assertNotNull($map_row['destid1']); } + // Add a failed row to test if this can be rolled back without errors. + $this->mockFailure($term_migration, ['id' => '4', 'vocab' => '2', 'name' => 'FAIL']); + // Rollback and verify the entities are gone. $term_executable->rollback(); foreach ($term_data_rows as $row) { diff --git a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php index a9d1313f5..e38e971be 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php @@ -640,6 +640,35 @@ class MigrateSqlIdMapTest extends MigrateTestCase { $this->assertSame(0, count($source_id)); } + /** + * Tests currentDestination() and currentSource(). + */ + public function testCurrentDestinationAndSource() { + // Simple map with one source and one destination ID. + $id_map = $this->setupRows(['nid'], ['nid'], [ + [1, 101], + [2, 102], + [3, 103], + // Mock a failed row by setting the destination ID to NULL. + [4, NULL], + ]); + + // The rows are ordered by destination ID so the failed row should be first. + $id_map->rewind(); + $this->assertEquals([], $id_map->currentDestination()); + $this->assertEquals(['nid' => 4], $id_map->currentSource()); + $id_map->next(); + $this->assertEquals(['nid' => 101], $id_map->currentDestination()); + $this->assertEquals(['nid' => 1], $id_map->currentSource()); + $id_map->next(); + $this->assertEquals(['nid' => 102], $id_map->currentDestination()); + $this->assertEquals(['nid' => 2], $id_map->currentSource()); + $id_map->next(); + $this->assertEquals(['nid' => 103], $id_map->currentDestination()); + $this->assertEquals(['nid' => 3], $id_map->currentSource()); + $id_map->next(); + } + /** * Tests the imported count method. * diff --git a/core/modules/migrate_drupal/src/Plugin/MigrateCckFieldPluginManager.php b/core/modules/migrate_drupal/src/Plugin/MigrateCckFieldPluginManager.php index 44151ccfa..56c7b5bad 100644 --- a/core/modules/migrate_drupal/src/Plugin/MigrateCckFieldPluginManager.php +++ b/core/modules/migrate_drupal/src/Plugin/MigrateCckFieldPluginManager.php @@ -15,7 +15,7 @@ use Drupal\migrate\Plugin\MigrationInterface; * * @ingroup migration */ -class MigrateCckFieldPluginManager extends MigratePluginManager { +class MigrateCckFieldPluginManager extends MigratePluginManager implements MigrateCckFieldPluginManagerInterface { /** * The default version of core to use for cck field plugins. @@ -29,7 +29,7 @@ class MigrateCckFieldPluginManager extends MigratePluginManager { /** * {@inheritdoc} */ - public function createInstance($field_type, array $configuration = array(), MigrationInterface $migration = NULL) { + public function getPluginIdFromFieldType($field_type, array $configuration = [], MigrationInterface $migration = NULL) { $core = static::DEFAULT_CORE_VERSION; if (!empty($configuration['core'])) { $core = $configuration['core']; @@ -45,7 +45,7 @@ class MigrateCckFieldPluginManager extends MigratePluginManager { foreach ($this->getDefinitions() as $plugin_id => $definition) { if (in_array($core, $definition['core'])) { if (array_key_exists($field_type, $definition['type_map']) || $field_type === $plugin_id) { - return parent::createInstance($plugin_id, $configuration, $migration); + return $plugin_id; } } } diff --git a/core/modules/migrate_drupal/src/Plugin/MigrateCckFieldPluginManagerInterface.php b/core/modules/migrate_drupal/src/Plugin/MigrateCckFieldPluginManagerInterface.php new file mode 100644 index 000000000..a5371b9dc --- /dev/null +++ b/core/modules/migrate_drupal/src/Plugin/MigrateCckFieldPluginManagerInterface.php @@ -0,0 +1,28 @@ +<?php + +namespace Drupal\migrate_drupal\Plugin; + +use Drupal\migrate\Plugin\MigratePluginManagerInterface; +use Drupal\migrate\Plugin\MigrationInterface; + +interface MigrateCckFieldPluginManagerInterface extends MigratePluginManagerInterface { + + /** + * Get the plugin ID from the field type. + * + * @param string $field_type + * The field type being migrated. + * @param array $configuration + * (optional) An array of configuration relevant to the plugin instance. + * @param \Drupal\migrate\Plugin\MigrationInterface|null $migration + * (optional) The current migration instance. + * + * @return string + * The ID of the plugin for the field_type if available. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * If the plugin cannot be determined, such as if the field type is invalid. + */ + public function getPluginIdFromFieldType($field_type, array $configuration = [], MigrationInterface $migration = NULL); + +} diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/CckMigration.php b/core/modules/migrate_drupal/src/Plugin/migrate/CckMigration.php index bffba2c54..017b7e778 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/CckMigration.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/CckMigration.php @@ -2,13 +2,15 @@ namespace Drupal\migrate_drupal\Plugin\migrate; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\migrate\Exception\RequirementsException; use Drupal\migrate\Plugin\MigrateDestinationPluginManager; -use Drupal\migrate\Plugin\MigratePluginManager; +use Drupal\migrate\Plugin\MigratePluginManagerInterface; use Drupal\migrate\Plugin\Migration; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate\Plugin\RequirementsInterface; +use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -40,7 +42,7 @@ class CckMigration extends Migration implements ContainerFactoryPluginInterface /** * The cckfield plugin manager. * - * @var \Drupal\migrate\Plugin\MigratePluginManager + * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface */ protected $cckPluginManager; @@ -53,20 +55,20 @@ class CckMigration extends Migration implements ContainerFactoryPluginInterface * The plugin ID. * @param mixed $plugin_definition * The plugin definition. - * @param \Drupal\migrate\Plugin\MigratePluginManager $cck_manager + * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager * The cckfield plugin manager. * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager * The migration plugin manager. - * @param \Drupal\migrate\Plugin\MigratePluginManager $source_plugin_manager + * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $source_plugin_manager * The source migration plugin manager. - * @param \Drupal\migrate\Plugin\MigratePluginManager $process_plugin_manager + * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $process_plugin_manager * The process migration plugin manager. * @param \Drupal\migrate\Plugin\MigrateDestinationPluginManager $destination_plugin_manager * The destination migration plugin manager. - * @param \Drupal\migrate\Plugin\MigratePluginManager $idmap_plugin_manager + * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $idmap_plugin_manager * The ID map migration plugin manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigratePluginManager $cck_manager, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManager $source_plugin_manager, MigratePluginManager $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManager $idmap_plugin_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrateCckFieldPluginManagerInterface $cck_manager, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManagerInterface $source_plugin_manager, MigratePluginManagerInterface $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManagerInterface $idmap_plugin_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition, $migration_plugin_manager, $source_plugin_manager, $process_plugin_manager, $destination_plugin_manager, $idmap_plugin_manager); $this->cckPluginManager = $cck_manager; } @@ -106,12 +108,19 @@ class CckMigration extends Migration implements ContainerFactoryPluginInterface } foreach ($source_plugin as $row) { $field_type = $row->getSourceProperty('type'); - if (!isset($this->processedFieldTypes[$field_type]) && $this->cckPluginManager->hasDefinition($field_type)) { + try { + $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, [], $this); + } + catch (PluginNotFoundException $ex) { + continue; + } + + if (!isset($this->processedFieldTypes[$field_type])) { $this->processedFieldTypes[$field_type] = TRUE; // Allow the cckfield plugin to alter the migration as necessary so // that it knows how to handle fields of this type. if (!isset($this->cckPluginCache[$field_type])) { - $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($field_type, [], $this); + $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, [], $this); } call_user_func([$this->cckPluginCache[$field_type], $this->pluginDefinition['cck_plugin_method']], $this); } diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal7.php b/core/modules/migrate_drupal/tests/fixtures/drupal7.php index bbc808827..a02d12f57 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal7.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal7.php @@ -3835,6 +3835,18 @@ $connection->insert('field_data_body') 'body_summary' => '', 'body_format' => 'filtered_html', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '3', + 'revision_id' => '3', + 'language' => 'und', + 'delta' => '0', + 'body_value' => "is - ...is that it's the absolute best show ever. Trust me, I would know.", + 'body_summary' => '', + 'body_format' => 'filtered_html', +)) ->execute(); $connection->schema()->createTable('field_data_comment_body', array( @@ -4930,6 +4942,18 @@ $connection->insert('field_data_field_link') 'field_link_title' => 'Home', 'field_link_attributes' => 'a:0:{}', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '3', + 'revision_id' => '3', + 'language' => 'und', + 'delta' => '0', + 'field_link_url' => '<front>', + 'field_link_title' => 'Home', + 'field_link_attributes' => 'a:1:{s:5:"title";s:0:"";}', +)) ->execute(); $connection->schema()->createTable('field_data_field_long_text', array( @@ -5167,6 +5191,16 @@ $connection->insert('field_data_field_tags') 'delta' => '0', 'field_tags_tid' => '9', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '3', + 'revision_id' => '3', + 'language' => 'und', + 'delta' => '0', + 'field_tags_tid' => '9', +)) ->values(array( 'entity_type' => 'node', 'bundle' => 'article', @@ -5177,6 +5211,16 @@ $connection->insert('field_data_field_tags') 'delta' => '1', 'field_tags_tid' => '14', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '3', + 'revision_id' => '3', + 'language' => 'und', + 'delta' => '1', + 'field_tags_tid' => '14', +)) ->values(array( 'entity_type' => 'node', 'bundle' => 'article', @@ -5187,6 +5231,16 @@ $connection->insert('field_data_field_tags') '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', +)) ->execute(); $connection->schema()->createTable('field_data_field_term_reference', array( @@ -5603,6 +5657,18 @@ $connection->insert('field_revision_body') 'body_summary' => '', 'body_format' => 'filtered_html', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '3', + 'revision_id' => '3', + 'language' => 'und', + 'delta' => '0', + 'body_value' => "is - ...is that it's the absolute best show ever. Trust me, I would know.", + 'body_summary' => '', + 'body_format' => 'filtered_html', +)) ->execute(); $connection->schema()->createTable('field_revision_comment_body', array( @@ -6710,6 +6776,18 @@ $connection->insert('field_revision_field_link') 'field_link_title' => 'Home', 'field_link_attributes' => 'a:0:{}', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '3', + 'revision_id' => '3', + 'language' => 'und', + 'delta' => '0', + 'field_link_url' => '<front>', + 'field_link_title' => 'Home', + 'field_link_attributes' => 'a:1:{s:5:"title";s:0:"";}', +)) ->execute(); $connection->schema()->createTable('field_revision_field_long_text', array( @@ -6950,6 +7028,16 @@ $connection->insert('field_revision_field_tags') 'delta' => '0', 'field_tags_tid' => '9', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '3', + 'revision_id' => '3', + 'language' => 'und', + 'delta' => '0', + 'field_tags_tid' => '9', +)) ->values(array( 'entity_type' => 'node', 'bundle' => 'article', @@ -6960,6 +7048,16 @@ $connection->insert('field_revision_field_tags') 'delta' => '1', 'field_tags_tid' => '14', )) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '3', + 'revision_id' => '3', + 'language' => 'und', + 'delta' => '1', + 'field_tags_tid' => '14', +)) ->values(array( 'entity_type' => 'node', 'bundle' => 'article', @@ -6970,6 +7068,16 @@ $connection->insert('field_revision_field_tags') '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', +)) ->execute(); $connection->schema()->createTable('field_revision_field_term_reference', array( @@ -29674,7 +29782,23 @@ $connection->insert('node') 'comment' => '2', 'promote' => '1', 'sticky' => '0', - 'tnid' => '0', + 'tnid' => '2', + 'translate' => '0', +)) +->values(array( + 'nid' => '3', + 'vid' => '3', + 'type' => 'article', + 'language' => 'is', + 'title' => 'is - The thing about Deep Space 9', + 'uid' => '1', + 'status' => '1', + 'created' => '1471428152', + 'changed' => '1471428152', + 'comment' => '2', + 'promote' => '1', + 'sticky' => '0', + 'tnid' => '2', 'translate' => '0', )) ->execute(); @@ -29813,6 +29937,14 @@ $connection->insert('node_comment_statistics') 'last_comment_uid' => '1', 'comment_count' => '1', )) +->values(array( + 'nid' => '3', + 'cid' => '0', + 'last_comment_timestamp' => '1471428152', + 'last_comment_name' => NULL, + 'last_comment_uid' => '1', + 'comment_count' => '0', +)) ->execute(); $connection->schema()->createTable('node_counter', array( @@ -29864,6 +29996,18 @@ $connection->insert('node_counter') 'daycount' => '0', 'timestamp' => '1421727536', )) +->values(array( + 'nid' => '2', + 'totalcount' => '1', + 'daycount' => '1', + 'timestamp' => '1471428059', +)) +->values(array( + 'nid' => '3', + 'totalcount' => '1', + 'daycount' => '1', + 'timestamp' => '1471428153', +)) ->execute(); $connection->schema()->createTable('node_revision', array( @@ -29972,6 +30116,18 @@ $connection->insert('node_revision') 'promote' => '1', 'sticky' => '0', )) +->values(array( + 'nid' => '3', + 'vid' => '3', + 'uid' => '1', + 'title' => 'is - The thing about Deep Space 9', + 'log' => '', + 'timestamp' => '1471428152', + 'status' => '1', + 'comment' => '2', + 'promote' => '1', + 'sticky' => '0', +)) ->execute(); $connection->schema()->createTable('node_type', array( @@ -40112,6 +40268,24 @@ $connection->insert('taxonomy_index') 'sticky' => '0', 'created' => '1441306772', )) +->values(array( + 'nid' => '3', + 'tid' => '9', + 'sticky' => '0', + 'created' => '1471428152', +)) +->values(array( + 'nid' => '3', + 'tid' => '14', + 'sticky' => '0', + 'created' => '1471428152', +)) +->values(array( + 'nid' => '3', + 'tid' => '17', + 'sticky' => '0', + 'created' => '1471428152', +)) ->execute(); $connection->schema()->createTable('taxonomy_term_data', array( @@ -41342,7 +41516,7 @@ $connection->insert('variable') )) ->values(array( 'name' => 'language_content_type_article', - 'value' => 's:1:"0";', + 'value' => 's:1:"2";', )) ->values(array( 'name' => 'language_content_type_blog', @@ -41442,7 +41616,7 @@ $connection->insert('variable') )) ->values(array( 'name' => 'menu_override_parent_selector', - 'value' => 'b:1;', + 'value' => 'b:0;', )) ->values(array( 'name' => 'menu_parent_article', diff --git a/core/modules/migrate_drupal/tests/src/Kernel/MigrateCckFieldPluginManagerTest.php b/core/modules/migrate_drupal/tests/src/Kernel/MigrateCckFieldPluginManagerTest.php index 8cea74c4d..1754cfef5 100644 --- a/core/modules/migrate_drupal/tests/src/Kernel/MigrateCckFieldPluginManagerTest.php +++ b/core/modules/migrate_drupal/tests/src/Kernel/MigrateCckFieldPluginManagerTest.php @@ -22,32 +22,33 @@ class MigrateCckFieldPluginManagerTest extends MigrateDrupalTestBase { public function testPluginSelection() { $plugin_manager = \Drupal::service('plugin.manager.migrate.cckfield'); - $this->assertIdentical('Drupal\\file\\Plugin\\migrate\\cckfield\\d6\\FileField', get_class($plugin_manager->createInstance('filefield', ['core' => 6]))); + $plugin_id = $plugin_manager->getPluginIdFromFieldType('filefield', ['core' => 6]); + $this->assertIdentical('Drupal\\file\\Plugin\\migrate\\cckfield\\d6\\FileField', get_class($plugin_manager->createInstance($plugin_id, ['core' => 6]))); try { - // If this test passes, createInstance will raise a + // If this test passes, getPluginIdFromFieldType will raise a // PluginNotFoundException and we'll never reach fail(). - $plugin_manager->createInstance('filefield', ['core' => 7]); + $plugin_manager->getPluginIdFromFieldType('filefield', ['core' => 7]); $this->fail('Expected Drupal\Component\Plugin\Exception\PluginNotFoundException.'); } catch (PluginNotFoundException $e) { $this->assertIdentical($e->getMessage(), "Plugin ID 'filefield' was not found."); } - $this->assertIdentical('Drupal\\file\\Plugin\\migrate\\cckfield\\d7\\ImageField', get_class($plugin_manager->createInstance('image', ['core' => 7]))); - $this->assertIdentical('Drupal\\file\\Plugin\\migrate\\cckfield\\d7\\FileField', get_class($plugin_manager->createInstance('file', ['core' => 7]))); - $this->assertIdentical('Drupal\\migrate_cckfield_plugin_manager_test\\Plugin\\migrate\\cckfield\\D6FileField', get_class($plugin_manager->createInstance('file', ['core' => 6]))); + $this->assertIdentical('image', $plugin_manager->getPluginIdFromFieldType('image', ['core' => 7])); + $this->assertIdentical('file', $plugin_manager->getPluginIdFromFieldType('file', ['core' => 7])); + $this->assertIdentical('d6_file', $plugin_manager->getPluginIdFromFieldType('file', ['core' => 6])); - $this->assertIdentical('Drupal\\text\\Plugin\\migrate\\cckfield\\TextField', get_class($plugin_manager->createInstance('text', ['core' => 6]))); - $this->assertIdentical('Drupal\\text\\Plugin\\migrate\\cckfield\\TextField', get_class($plugin_manager->createInstance('text', ['core' => 7]))); + $this->assertIdentical('text', $plugin_manager->getPluginIdFromFieldType('text', ['core' => 6])); + $this->assertIdentical('text', $plugin_manager->getPluginIdFromFieldType('text', ['core' => 7])); // Test fallback when no core version is specified. - $this->assertIdentical('Drupal\\migrate_cckfield_plugin_manager_test\\Plugin\\migrate\\cckfield\\D6NoCoreVersionSpecified', get_class($plugin_manager->createInstance('d6_no_core_version_specified', ['core' => 6]))); + $this->assertIdentical('d6_no_core_version_specified', $plugin_manager->getPluginIdFromFieldType('d6_no_core_version_specified', ['core' => 6])); try { - // If this test passes, createInstance will raise a + // If this test passes, getPluginIdFromFieldType will raise a // PluginNotFoundException and we'll never reach fail(). - $plugin_manager->createInstance('d6_no_core_version_specified', ['core' => 7]); + $plugin_manager->getPluginIdFromFieldType('d6_no_core_version_specified', ['core' => 7]); $this->fail('Expected Drupal\Component\Plugin\Exception\PluginNotFoundException.'); } catch (PluginNotFoundException $e) { 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 6b30adea5..2769b4431 100644 --- a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php +++ b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php @@ -48,9 +48,9 @@ class MigrateUpgrade7Test extends MigrateUpgradeTestBase { 'file' => 1, 'filter_format' => 7, 'image_style' => 6, - 'language_content_settings' => 1, + 'language_content_settings' => 2, 'migration' => 59, - 'node' => 2, + 'node' => 3, 'node_type' => 6, 'rdf_mapping' => 5, 'search_page' => 2, diff --git a/core/modules/node/node.module b/core/modules/node/node.module index aade3e833..53d43ab61 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1213,7 +1213,7 @@ function _node_access_rebuild_batch_operation(&$context) { // Initiate multistep processing. $context['sandbox']['progress'] = 0; $context['sandbox']['current_node'] = 0; - $context['sandbox']['max'] = \Drupal::entityQuery('node')->count()->execute(); + $context['sandbox']['max'] = \Drupal::entityQuery('node')->accessCheck(FALSE)->count()->execute(); } // Process the next 20 nodes. diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php index 7244bdfeb..7131083bb 100644 --- a/core/modules/node/src/Controller/NodeController.php +++ b/core/modules/node/src/Controller/NodeController.php @@ -127,7 +127,7 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa public function revisionShow($node_revision) { $node = $this->entityManager()->getStorage('node')->loadRevision($node_revision); $node = $this->entityManager()->getTranslationFromContext($node); - $node_view_controller = new NodeViewController($this->entityManager, $this->renderer); + $node_view_controller = new NodeViewController($this->entityManager, $this->renderer, $this->currentUser()); $page = $node_view_controller->view($node); unset($page['nodes'][$node->id()]['#cache']); return $page; diff --git a/core/modules/node/src/Controller/NodeViewController.php b/core/modules/node/src/Controller/NodeViewController.php index f80e65d27..fce50e144 100644 --- a/core/modules/node/src/Controller/NodeViewController.php +++ b/core/modules/node/src/Controller/NodeViewController.php @@ -4,12 +4,50 @@ namespace Drupal\node\Controller; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Controller\EntityViewController; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Session\AccountInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a controller to render a single node. */ class NodeViewController extends EntityViewController { + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * Creates an NodeViewController object. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. For backwards compatibility this is optional, however + * this will be removed before Drupal 9.0.0. + */ + public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer, AccountInterface $current_user = NULL) { + parent::__construct($entity_manager, $renderer); + $this->currentUser = $current_user ?: \Drupal::currentUser(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('renderer'), + $container->get('current_user') + ); + } + /** * {@inheritdoc} */ @@ -17,27 +55,44 @@ class NodeViewController extends EntityViewController { $build = parent::view($node, $view_mode, $langcode); foreach ($node->uriRelationships() as $rel) { - // Set the node path as the canonical URL to prevent duplicate content. - $build['#attached']['html_head_link'][] = array( - array( - 'rel' => $rel, - 'href' => $node->url($rel), - ), - TRUE, - ); + $url = $node->toUrl($rel); + // Add link relationships if the user is authenticated or if the anonymous + // user has access. Access checking must be done for anonymous users to + // avoid traffic to inaccessible pages from web crawlers. For + // authenticated users, showing the links in HTML head does not impact + // user experience or security, since the routes are access checked when + // visited and only visible via view source. This prevents doing + // potentially expensive and hard to cache access checks on every request. + // This means that the page will vary by user.permissions. We also rely on + // the access checking fallback to ensure the correct cacheability + // metadata if we have to check access. + if ($this->currentUser->isAuthenticated() || $url->access($this->currentUser)) { + // Set the node path as the canonical URL to prevent duplicate content. + $build['#attached']['html_head_link'][] = array( + array( + 'rel' => $rel, + 'href' => $url->toString(), + ), + TRUE, + ); + } if ($rel == 'canonical') { // Set the non-aliased canonical path as a default shortlink. $build['#attached']['html_head_link'][] = array( array( 'rel' => 'shortlink', - 'href' => $node->url($rel, array('alias' => TRUE)), + 'href' => $url->setOption('alias', TRUE)->toString(), ), TRUE, ); } } + // Given this varies by $this->currentUser->isAuthenticated(), add a cache + // context based on the anonymous role. + $build['#cache']['contexts'][] = 'user.roles:anonymous'; + return $build; } diff --git a/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php b/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php index f3e1a9210..3fba70ac4 100644 --- a/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php +++ b/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php @@ -3,11 +3,12 @@ namespace Drupal\node\Plugin\migrate; use Drupal\Component\Plugin\Derivative\DeriverBase; -use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; use Drupal\migrate\Exception\RequirementsException; use Drupal\migrate\Plugin\MigrationDeriverTrait; +use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -33,7 +34,7 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface { /** * The CCK plugin manager. * - * @var \Drupal\Component\Plugin\PluginManagerInterface + * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface */ protected $cckPluginManager; @@ -49,12 +50,12 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface { * * @param string $base_plugin_id * The base plugin ID for the plugin ID. - * @param \Drupal\Component\Plugin\PluginManagerInterface $cck_manager + * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager * The CCK plugin manager. * @param bool $translations * Whether or not to include translations. */ - public function __construct($base_plugin_id, PluginManagerInterface $cck_manager, $translations) { + public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $cck_manager, $translations) { $this->basePluginId = $base_plugin_id; $this->cckPluginManager = $cck_manager; $this->includeTranslations = $translations; @@ -128,14 +129,15 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface { if (isset($fields[$node_type])) { foreach ($fields[$node_type] as $field_name => $info) { $field_type = $info['type']; - if ($this->cckPluginManager->hasDefinition($info['type'])) { + try { + $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 6], $migration); if (!isset($this->cckPluginCache[$field_type])) { - $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($field_type, ['core' => 6], $migration); + $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 6], $migration); } $this->cckPluginCache[$field_type] ->processCckFieldValues($migration, $field_name, $info); } - else { + catch (PluginNotFoundException $ex) { $migration->setProcessOfProperty($field_name, $field_name); } } diff --git a/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php b/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php index 6efa1c746..bd3d8b9fc 100644 --- a/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php +++ b/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php @@ -3,11 +3,12 @@ namespace Drupal\node\Plugin\migrate; use Drupal\Component\Plugin\Derivative\DeriverBase; -use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; use Drupal\migrate\Exception\RequirementsException; use Drupal\migrate\Plugin\MigrationDeriverTrait; +use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -33,7 +34,7 @@ class D7NodeDeriver extends DeriverBase implements ContainerDeriverInterface { /** * The CCK plugin manager. * - * @var \Drupal\Component\Plugin\PluginManagerInterface + * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface */ protected $cckPluginManager; @@ -42,10 +43,10 @@ class D7NodeDeriver extends DeriverBase implements ContainerDeriverInterface { * * @param string $base_plugin_id * The base plugin ID for the plugin ID. - * @param \Drupal\Component\Plugin\PluginManagerInterface $cck_manager + * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager * The CCK plugin manager. */ - public function __construct($base_plugin_id, PluginManagerInterface $cck_manager) { + public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $cck_manager) { $this->basePluginId = $base_plugin_id; $this->cckPluginManager = $cck_manager; } @@ -98,14 +99,15 @@ class D7NodeDeriver extends DeriverBase implements ContainerDeriverInterface { if (isset($fields[$node_type])) { foreach ($fields[$node_type] as $field_name => $info) { $field_type = $info['type']; - if ($this->cckPluginManager->hasDefinition($field_type)) { + try { + $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration); if (!isset($this->cckPluginCache[$field_type])) { - $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($field_type, ['core' => 7], $migration); + $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 7], $migration); } $this->cckPluginCache[$field_type] ->processCckFieldValues($migration, $field_name, $info); } - else { + catch (PluginNotFoundException $ex) { $migration->setProcessOfProperty($field_name, $field_name); } } diff --git a/core/modules/node/src/Tests/NodeAccessRebuildNodeGrantsTest.php b/core/modules/node/src/Tests/NodeAccessRebuildNodeGrantsTest.php index d5cca9526..d151aadc3 100644 --- a/core/modules/node/src/Tests/NodeAccessRebuildNodeGrantsTest.php +++ b/core/modules/node/src/Tests/NodeAccessRebuildNodeGrantsTest.php @@ -2,6 +2,8 @@ namespace Drupal\node\Tests; +use Drupal\node\Entity\NodeType; + /** * Ensures that node access rebuild functions work correctly even * when other modules implements hook_node_grants(). @@ -11,20 +13,27 @@ namespace Drupal\node\Tests; class NodeAccessRebuildNodeGrantsTest extends NodeTestBase { /** - * A user to test the rebuild nodes feature. + * A user to create nodes that only it has access to. * * @var \Drupal\user\UserInterface */ protected $webUser; + /** + * A user to test the rebuild nodes feature which can't access the nodes. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'access administration pages', 'access site reports', 'bypass node access')); - $this->drupalLogin($admin_user); + $this->adminUser = $this->drupalCreateUser(array('administer site configuration', 'access administration pages', 'access site reports')); + $this->drupalLogin($this->adminUser); $this->webUser = $this->drupalCreateUser(); } @@ -34,25 +43,54 @@ class NodeAccessRebuildNodeGrantsTest extends NodeTestBase { */ public function testNodeAccessRebuildNodeGrants() { \Drupal::service('module_installer')->install(['node_access_test']); + \Drupal::state()->set('node_access_test.private', TRUE); + node_access_test_add_field(NodeType::load('page')); $this->resetAll(); - $node = $this->drupalCreateNode(array( - 'uid' => $this->webUser->id(), - )); + // Create 30 nodes so that _node_access_rebuild_batch_operation() has to run + // more than once. + for ($i = 0; $i < 30; $i++) { + $nodes[] = $this->drupalCreateNode(array( + 'uid' => $this->webUser->id(), + 'private' => [['value' => 1]] + )); + } + /** @var \Drupal\node\NodeGrantDatabaseStorageInterface $grant_storage */ + $grant_storage = \Drupal::service('node.grant_storage'); // Default realm access and node records are present. - $this->assertTrue(\Drupal::service('node.grant_storage')->access($node, 'view', $this->webUser), 'The expected node access records are present'); + foreach ($nodes as $node) { + $this->assertTrue($node->private->value); + $this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'Prior to rebuilding node access the grant storage returns allowed for the node author.'); + $this->assertTrue($grant_storage->access($node, 'view', $this->adminUser)->isAllowed(), 'Prior to rebuilding node access the grant storage returns allowed for the admin user.'); + } + $this->assertEqual(1, \Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is an all realm access record'); $this->assertTrue(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions need to be rebuilt'); // Rebuild permissions. - $this->drupalGet('admin/reports/status/rebuild'); + $this->drupalGet('admin/reports/status'); + $this->clickLink(t('Rebuild permissions')); $this->drupalPostForm(NULL, array(), t('Rebuild permissions')); $this->assertText(t('The content access permissions have been rebuilt.')); - // Test if the rebuild has been successful. + // Test if the rebuild by user that cannot bypass node access and does not + // have access to the nodes has been successful. + $this->assertFalse($this->adminUser->hasPermission('bypass node access')); $this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions have been rebuilt'); - $this->assertTrue(\Drupal::service('node.grant_storage')->access($node, 'view', $this->webUser), 'The expected node access records are present'); + foreach ($nodes as $node) { + $this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'After rebuilding node access the grant storage returns allowed for the node author.'); + $this->assertFalse($grant_storage->access($node, 'view', $this->adminUser)->isForbidden(), 'After rebuilding node access the grant storage returns forbidden for the admin user.'); + } + $this->assertFalse(\Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is no all realm access record'); + + // Test an anonymous node access rebuild from code. + $this->drupalLogout(); + node_access_rebuild(); + foreach ($nodes as $node) { + $this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'After rebuilding node access the grant storage returns allowed for the node author.'); + $this->assertFalse($grant_storage->access($node, 'view', $this->adminUser)->isForbidden(), 'After rebuilding node access the grant storage returns forbidden for the admin user.'); + } $this->assertFalse(\Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is no all realm access record'); } diff --git a/core/modules/node/src/Tests/NodeAccessRebuildTest.php b/core/modules/node/src/Tests/NodeAccessRebuildTest.php deleted file mode 100644 index 187f3a059..000000000 --- a/core/modules/node/src/Tests/NodeAccessRebuildTest.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -namespace Drupal\node\Tests; - -/** - * Ensures that node access rebuild functions work correctly. - * - * @group node - */ -class NodeAccessRebuildTest extends NodeTestBase { - /** - * A normal authenticated user. - * - * @var \Drupal\user\UserInterface - */ - protected $webUser; - - protected function setUp() { - parent::setUp(); - - $web_user = $this->drupalCreateUser(array('administer site configuration', 'access administration pages', 'access site reports')); - $this->drupalLogin($web_user); - $this->webUser = $web_user; - } - - /** - * Tests rebuilding the node access permissions table. - */ - function testNodeAccessRebuild() { - $this->drupalGet('admin/reports/status'); - $this->clickLink(t('Rebuild permissions')); - $this->drupalPostForm(NULL, array(), t('Rebuild permissions')); - $this->assertText(t('Content permissions have been rebuilt.')); - } - -} diff --git a/core/modules/node/src/Tests/NodeCacheTagsTest.php b/core/modules/node/src/Tests/NodeCacheTagsTest.php index 990f00b24..66932cce0 100644 --- a/core/modules/node/src/Tests/NodeCacheTagsTest.php +++ b/core/modules/node/src/Tests/NodeCacheTagsTest.php @@ -38,6 +38,16 @@ class NodeCacheTagsTest extends EntityWithUriCacheTagsTestBase { return $node; } + /** + * {@inheritdoc} + */ + protected function getDefaultCacheContexts() { + $defaults = parent::getDefaultCacheContexts(); + // @see \Drupal\node\Controller\NodeViewController::view() + $defaults[] = 'user.roles:anonymous'; + return $defaults; + } + /** * {@inheritdoc} */ diff --git a/core/modules/node/src/Tests/NodeViewTest.php b/core/modules/node/src/Tests/NodeViewTest.php index 5b2200c10..fae056c6f 100644 --- a/core/modules/node/src/Tests/NodeViewTest.php +++ b/core/modules/node/src/Tests/NodeViewTest.php @@ -18,14 +18,49 @@ class NodeViewTest extends NodeTestBase { $this->drupalGet($node->urlInfo()); + $result = $this->xpath('//link[@rel = "canonical"]'); + $this->assertEqual($result[0]['href'], $node->url()); + + // Link relations are checked for access for anonymous users. + $result = $this->xpath('//link[@rel = "version-history"]'); + $this->assertFalse($result, 'Version history not present for anonymous users without access.'); + + $result = $this->xpath('//link[@rel = "edit-form"]'); + $this->assertFalse($result, 'Edit form not present for anonymous users without access.'); + + $this->drupalLogin($this->createUser(['access content'])); + $this->drupalGet($node->urlInfo()); + + $result = $this->xpath('//link[@rel = "canonical"]'); + $this->assertEqual($result[0]['href'], $node->url()); + + // Link relations are present regardless of access for authenticated users. $result = $this->xpath('//link[@rel = "version-history"]'); $this->assertEqual($result[0]['href'], $node->url('version-history')); $result = $this->xpath('//link[@rel = "edit-form"]'); $this->assertEqual($result[0]['href'], $node->url('edit-form')); + // Give anonymous users access to edit the node. Do this through the UI to + // ensure caches are handled properly. + $this->drupalLogin($this->rootUser); + $edit = [ + 'anonymous[edit own ' . $node->bundle() . ' content]' => TRUE + ]; + $this->drupalPostForm('admin/people/permissions', $edit, 'Save permissions'); + $this->drupalLogout(); + + // Anonymous user's should now see the edit-form link but not the + // version-history link. + $this->drupalGet($node->urlInfo()); $result = $this->xpath('//link[@rel = "canonical"]'); $this->assertEqual($result[0]['href'], $node->url()); + + $result = $this->xpath('//link[@rel = "version-history"]'); + $this->assertFalse($result, 'Version history not present for anonymous users without access.'); + + $result = $this->xpath('//link[@rel = "edit-form"]'); + $this->assertEqual($result[0]['href'], $node->url('edit-form')); } /** diff --git a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php index cb752576d..404e35810 100644 --- a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php +++ b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php @@ -47,7 +47,7 @@ class MigrateNodeTest extends MigrateDrupal7TestBase { 'd7_taxonomy_vocabulary', 'd7_field', 'd7_field_instance', - 'd7_node:test_content_type', + 'd7_node', 'd7_node:article', ]); } diff --git a/core/modules/simpletest/simpletest.api.php b/core/modules/simpletest/simpletest.api.php index e5251b78b..d1b5f442c 100644 --- a/core/modules/simpletest/simpletest.api.php +++ b/core/modules/simpletest/simpletest.api.php @@ -50,7 +50,7 @@ function hook_test_group_finished() { * $results The results of the test as gathered by * \Drupal\simpletest\WebTestBase. * - * @see \Drupal\simpletest\WebTestBase->results() + * @see \Drupal\simpletest\WebTestBase::results() */ function hook_test_finished($results) { } diff --git a/core/modules/simpletest/tests/src/Functional/BrowserTestBaseTest.php b/core/modules/simpletest/tests/src/Functional/BrowserTestBaseTest.php index 4d1906e76..4cd379abc 100644 --- a/core/modules/simpletest/tests/src/Functional/BrowserTestBaseTest.php +++ b/core/modules/simpletest/tests/src/Functional/BrowserTestBaseTest.php @@ -33,6 +33,11 @@ class BrowserTestBaseTest extends BrowserTestBase { // Test page contains some text. $this->assertSession()->pageTextContains('Test page text.'); + // Check that returned plain text is correct. + $text = $this->getTextContent(); + $this->assertContains('Test page text.', $text); + $this->assertNotContains('</html>', $text); + // Response includes cache tags that we can assert. $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', 'rendered'); diff --git a/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php index 363fa9297..ad4f59b9f 100644 --- a/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php @@ -27,7 +27,7 @@ abstract class EntityWithUriCacheTagsTestBase extends EntityCacheTagsTestBase { $view_mode = $this->selectViewMode($entity_type); // The default cache contexts for rendered entities. - $entity_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; + $entity_cache_contexts = $this->getDefaultCacheContexts(); // Generate the standardized entity cache tags. $cache_tag = $this->entity->getCacheTags(); @@ -141,4 +141,14 @@ abstract class EntityWithUriCacheTagsTestBase extends EntityCacheTagsTestBase { $this->assertResponse(404); } + /** + * Gets the default cache contexts for rendered entities. + * + * @return array + * The default cache contexts for rendered entities. + */ + protected function getDefaultCacheContexts() { + return ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; + } + } diff --git a/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryErrorTest.php b/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryErrorTest.php new file mode 100644 index 000000000..dbb15be52 --- /dev/null +++ b/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryErrorTest.php @@ -0,0 +1,62 @@ +<?php + +namespace Drupal\system\Tests\Installer; + +use Drupal\Component\Utility\Crypt; +use Drupal\simpletest\InstallerTestBase; + +/** + * Tests the installer when a config_directory set up but does not exist. + * + * @group Installer + */ +class InstallerConfigDirectorySetNoDirectoryErrorTest extends InstallerTestBase { + + /** + * The directory where the sync directory should be created during install. + * + * @var string + */ + protected $configDirectory; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->configDirectory = $this->publicFilesDirectory . '/config_' . Crypt::randomBytesBase64(); + $this->settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) array( + 'value' => $this->configDirectory . '/sync', + 'required' => TRUE, + ); + // Create the files directory early so we can test the error case. + mkdir($this->publicFilesDirectory); + // Create a file so the directory can not be created. + file_put_contents($this->configDirectory, 'Test'); + parent::setUp(); + } + + /** + * Installer step: Configure settings. + */ + protected function setUpSettings() { + // This step should not appear as we had a failure prior to the settings + // screen. + } + + /** + * @{inheritdoc} + */ + protected function setUpSite() { + // This step should not appear as we had a failure prior to the settings + // screen. + } + + /** + * Verifies that installation failed. + */ + public function testError() { + $this->assertText("An automated attempt to create the directory {$this->configDirectory}/sync failed, possibly due to a permissions problem."); + $this->assertFalse(file_exists($this->configDirectory . '/sync') && is_dir($this->configDirectory . '/sync'), "The directory {$this->configDirectory}/sync does not exist."); + } + +} diff --git a/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryTest.php b/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryTest.php new file mode 100644 index 000000000..4daa45cc4 --- /dev/null +++ b/core/modules/system/src/Tests/Installer/InstallerConfigDirectorySetNoDirectoryTest.php @@ -0,0 +1,49 @@ +<?php + +namespace Drupal\system\Tests\Installer; + +use Drupal\Component\Utility\Crypt; +use Drupal\simpletest\InstallerTestBase; + +/** + * Tests the installer when a config_directory set up but does not exist. + * + * @group Installer + */ +class InstallerConfigDirectorySetNoDirectoryTest extends InstallerTestBase { + + /** + * The sync directory created during the install. + * + * @var string + */ + protected $syncDirectory; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->syncDirectory = $this->publicFilesDirectory . '/config_' . Crypt::randomBytesBase64() . '/sync'; + $this->settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) array( + 'value' => $this->syncDirectory, + 'required' => TRUE, + ); + // Other directories will be created too. + $this->settings['config_directories']['custom'] = (object) array( + 'value' => $this->publicFilesDirectory . '/config_custom', + 'required' => TRUE, + ); + parent::setUp(); + } + + /** + * Verifies that installation succeeded. + */ + public function testInstaller() { + $this->assertUrl('user/1'); + $this->assertResponse(200); + $this->assertTrue(file_exists($this->syncDirectory) && is_dir($this->syncDirectory), "The directory {$this->syncDirectory} exists."); + $this->assertTrue(file_exists($this->publicFilesDirectory . '/config_custom') && is_dir($this->publicFilesDirectory . '/config_custom'), "The directory {$this->publicFilesDirectory}/custom_config exists."); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index c1b92929c..99b4ec12a 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -551,10 +551,20 @@ function system_requirements($phase) { if (!empty($GLOBALS['config_directories'])) { foreach (array_keys(array_filter($GLOBALS['config_directories'])) as $type) { $directory = config_get_config_directory($type); + // If we're installing Drupal try and create the config sync directory. + if (!is_dir($directory) && $phase == 'install') { + file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + } if (!is_dir($directory)) { + if ($phase == 'install') { + $description = t('An automated attempt to create the directory %directory failed, possibly due to a permissions problem. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see INSTALL.txt or the <a href=":handbook_url">online handbook</a>.', array('%directory' => $directory, ':handbook_url' => 'https://www.drupal.org/server-permissions')); + } + else { + $description = t('The directory %directory does not exist.', array('%directory' => $directory)); + } $requirements['config directory ' . $type] = array( 'title' => t('Configuration directory: %type', ['%type' => $type]), - 'description' => t('The directory %directory does not exist.', array('%directory' => $directory)), + 'description' => $description, 'severity' => REQUIREMENT_ERROR, ); } diff --git a/core/modules/system/tests/modules/entity_reference_test_views/test_views/views.view.test_entity_reference_entity_test_view_long.yml b/core/modules/system/tests/modules/entity_reference_test_views/test_views/views.view.test_entity_reference_entity_test_view_long.yml new file mode 100644 index 000000000..997820574 --- /dev/null +++ b/core/modules/system/tests/modules/entity_reference_test_views/test_views/views.view.test_entity_reference_entity_test_view_long.yml @@ -0,0 +1,121 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_entity_reference_entity_test_view_long +label: test_entity_reference_entity_test_view_long +module: views +description: '' +tag: '' +base_table: entity_test_mul_changed_property +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + 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: full + 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: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test_mul_changed_property + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + relationship: field_test_data_with_a_long_name + filters: { } + sorts: + id: + id: id + table: entity_test_mul_changed_property + field: id + entity_type: entity_test + entity_field: id + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: + field_test_data_with_a_long_name: + id: field_test_data_with_a_long_name_data + table: entity_test_mul_changed__field_test_data_with_a_long_name + field: field_test_data_with_a_long_name + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - entity_test_view_grants + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/core/modules/tracker/tests/src/Kernel/Migrate/d7/MigrateTrackerNodeTest.php b/core/modules/tracker/tests/src/Kernel/Migrate/d7/MigrateTrackerNodeTest.php index 1b6a4edd6..d72f13dc0 100644 --- a/core/modules/tracker/tests/src/Kernel/Migrate/d7/MigrateTrackerNodeTest.php +++ b/core/modules/tracker/tests/src/Kernel/Migrate/d7/MigrateTrackerNodeTest.php @@ -36,7 +36,7 @@ class MigrateTrackerNodeTest extends MigrateDrupal7TestBase { 'd7_user_role', 'd7_user', 'd7_node_type', - 'd7_node:test_content_type', + 'd7_node', 'd7_tracker_node', ]); } diff --git a/core/modules/tracker/tests/src/Kernel/Migrate/d7/MigrateTrackerUserTest.php b/core/modules/tracker/tests/src/Kernel/Migrate/d7/MigrateTrackerUserTest.php index 8e679c4e3..e3ed88e9c 100644 --- a/core/modules/tracker/tests/src/Kernel/Migrate/d7/MigrateTrackerUserTest.php +++ b/core/modules/tracker/tests/src/Kernel/Migrate/d7/MigrateTrackerUserTest.php @@ -36,7 +36,7 @@ class MigrateTrackerUserTest extends MigrateDrupal7TestBase { 'd7_user_role', 'd7_user', 'd7_node_type', - 'd7_node:test_content_type', + 'd7_node', 'd7_tracker_node', ]); } diff --git a/core/modules/update/update.api.php b/core/modules/update/update.api.php index 362c1a228..37ef66a47 100644 --- a/core/modules/update/update.api.php +++ b/core/modules/update/update.api.php @@ -32,7 +32,7 @@ * examples of how to populate the array with real values. * * @see \Drupal\Update\UpdateManager::getProjects() - * @see \Drupal\Core\Utility\ProjectInfo->processInfoList() + * @see \Drupal\Core\Utility\ProjectInfo::processInfoList() */ function hook_update_projects_alter(&$projects) { // Hide a site-specific module from the list. diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php index 0eb2729c7..779cf90fb 100644 --- a/core/modules/user/src/Controller/UserController.php +++ b/core/modules/user/src/Controller/UserController.php @@ -6,10 +6,13 @@ use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Xss; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\user\Form\UserPasswordResetForm; use Drupal\user\UserDataInterface; use Drupal\user\UserInterface; use Drupal\user\UserStorageInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** @@ -38,6 +41,13 @@ class UserController extends ControllerBase { */ protected $userData; + /** + * A logger instance. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + /** * Constructs a UserController object. * @@ -47,11 +57,14 @@ class UserController extends ControllerBase { * The user storage. * @param \Drupal\user\UserDataInterface $user_data * The user data service. + * @param \Psr\Log\LoggerInterface $logger + * A logger instance. */ - public function __construct(DateFormatterInterface $date_formatter, UserStorageInterface $user_storage, UserDataInterface $user_data) { + public function __construct(DateFormatterInterface $date_formatter, UserStorageInterface $user_storage, UserDataInterface $user_data, LoggerInterface $logger) { $this->dateFormatter = $date_formatter; $this->userStorage = $user_storage; $this->userData = $user_data; + $this->logger = $logger; } /** @@ -61,38 +74,51 @@ class UserController extends ControllerBase { return new static( $container->get('date.formatter'), $container->get('entity.manager')->getStorage('user'), - $container->get('user.data') + $container->get('user.data'), + $container->get('logger.factory')->get('user') ); } /** - * Returns the user password reset page. + * Redirects to the user password reset form. * + * In order to never disclose a reset link via a referrer header this + * controller must always return a redirect response. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. * @param int $uid - * UID of user requesting reset. + * User ID of the user requesting reset. * @param int $timestamp * The current timestamp. * @param string $hash * Login link hash. * - * @return array|\Symfony\Component\HttpFoundation\RedirectResponse - * The form structure or a redirect response. - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - * If the login link is for a blocked user or invalid user ID. + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * The redirect response. */ - public function resetPass($uid, $timestamp, $hash) { + public function resetPass(Request $request, $uid, $timestamp, $hash) { $account = $this->currentUser(); - $config = $this->config('user.settings'); // When processing the one-time login link, we have to make sure that a user // isn't already logged in. if ($account->isAuthenticated()) { // The current user is already logged in. if ($account->id() == $uid) { user_logout(); + // We need to begin the redirect process again because logging out will + // destroy the session. + return $this->redirect( + 'user.reset', + [ + 'uid' => $uid, + 'timestamp' => $timestamp, + 'hash' => $hash, + ] + ); } // A different user is already logged in on the computer. else { + /** @var \Drupal\user\UserInterface $reset_link_user */ if ($reset_link_user = $this->userStorage->load($uid)) { drupal_set_message($this->t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please <a href=":logout">log out</a> and try using the link again.', array('%other_user' => $account->getUsername(), '%resetting_user' => $reset_link_user->getUsername(), ':logout' => $this->url('user.logout'))), 'warning'); @@ -104,33 +130,117 @@ class UserController extends ControllerBase { return $this->redirect('<front>'); } } - // The current user is not logged in, so check the parameters. - // Time out, in seconds, until login URL expires. - $timeout = $config->get('password_reset_timeout'); - $current = REQUEST_TIME; - /* @var \Drupal\user\UserInterface $user */ + $session = $request->getSession(); + $session->set('pass_reset_hash', $hash); + $session->set('pass_reset_timeout', $timestamp); + return $this->redirect( + 'user.reset.form', + ['uid' => $uid] + ); + } + + /** + * Returns the user password reset form. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * @param int $uid + * User ID of the user requesting reset. + * + * @return array|\Symfony\Component\HttpFoundation\RedirectResponse + * The form structure or a redirect response. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * If the pass_reset_timeout or pass_reset_hash are not available in the + * session. Or if $uid is for a blocked user or invalid user ID. + */ + public function getResetPassForm(Request $request, $uid) { + $session = $request->getSession(); + $timestamp = $session->get('pass_reset_timeout'); + $hash = $session->get('pass_reset_hash'); + // As soon as the session variables are used they are removed to prevent the + // hash and timestamp from being leaked unexpectedly. This could occur if + // the user does not click on the log in button on the form. + $session->remove('pass_reset_timeout'); + $session->remove('pass_reset_hash'); + if (!$hash || !$timestamp) { + throw new AccessDeniedHttpException(); + } + + /** @var \Drupal\user\UserInterface $user */ + $user = $this->userStorage->load($uid); + if ($user === NULL || !$user->isActive()) { + // Blocked or invalid user ID, so deny access. The parameters will be in + // the watchdog's URL for the administrator to check. + throw new AccessDeniedHttpException(); + } + + // Time out, in seconds, until login URL expires. + $timeout = $this->config('user.settings')->get('password_reset_timeout'); + + $expiration_date = $user->getLastLoginTime() ? $this->dateFormatter->format($timestamp + $timeout) : NULL; + return $this->formBuilder()->getForm(UserPasswordResetForm::class, $user, $expiration_date, $timestamp, $hash); + } + + /** + * Validates user, hash, and timestamp; logs the user in if correct. + * + * @param int $uid + * User ID of the user requesting reset. + * @param int $timestamp + * The current timestamp. + * @param string $hash + * Login link hash. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * Returns a redirect to the user edit form if the information is correct. + * If the information is incorrect redirects to 'user.pass' route with a + * message for the user. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * If $uid is for a blocked user or invalid user ID. + */ + public function resetPassLogin($uid, $timestamp, $hash) { + // The current user is not logged in, so check the parameters. + $current = REQUEST_TIME; + /** @var \Drupal\user\UserInterface $user */ $user = $this->userStorage->load($uid); // Verify that the user exists and is active. - if ($user && $user->isActive()) { - // No time out for first time login. - if ($user->getLastLoginTime() && $current - $timestamp > $timeout) { - drupal_set_message($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error'); - return $this->redirect('user.pass'); - } - elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && Crypt::hashEquals($hash, user_pass_rehash($user, $timestamp))) { - $expiration_date = $user->getLastLoginTime() ? $this->dateFormatter->format($timestamp + $timeout) : NULL; - return $this->formBuilder()->getForm('Drupal\user\Form\UserPasswordResetForm', $user, $expiration_date, $timestamp, $hash); - } - else { - drupal_set_message($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'error'); - return $this->redirect('user.pass'); - } + if ($user === NULL || !$user->isActive()) { + // Blocked or invalid user ID, so deny access. The parameters will be in + // the watchdog's URL for the administrator to check. + throw new AccessDeniedHttpException(); } - // Blocked or invalid user ID, so deny access. The parameters will be in the - // watchdog's URL for the administrator to check. - throw new AccessDeniedHttpException(); + + // Time out, in seconds, until login URL expires. + $timeout = $this->config('user.settings')->get('password_reset_timeout'); + // No time out for first time login. + if ($user->getLastLoginTime() && $current - $timestamp > $timeout) { + drupal_set_message($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error'); + return $this->redirect('user.pass'); + } + elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && Crypt::hashEquals($hash, user_pass_rehash($user, $timestamp))) { + user_login_finalize($user); + $this->logger->notice('User %name used one-time login link at time %timestamp.', ['%name' => $user->getDisplayName(), '%timestamp' => $timestamp]); + drupal_set_message($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); + // Let the user's password be changed without the current password + // check. + $token = Crypt::randomBytesBase64(55); + $_SESSION['pass_reset_' . $user->id()] = $token; + return $this->redirect( + 'entity.user.edit_form', + ['user' => $user->id()], + [ + 'query' => ['pass-reset-token' => $token], + 'absolute' => TRUE, + ] + ); + } + + drupal_set_message($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'error'); + return $this->redirect('user.pass'); } /** diff --git a/core/modules/user/src/Form/UserPasswordResetForm.php b/core/modules/user/src/Form/UserPasswordResetForm.php index 432941a70..1372415cf 100644 --- a/core/modules/user/src/Form/UserPasswordResetForm.php +++ b/core/modules/user/src/Form/UserPasswordResetForm.php @@ -4,42 +4,14 @@ namespace Drupal\user\Form; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\Component\Utility\Crypt; use Drupal\Core\Form\FormBase; -use Psr\Log\LoggerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Url; /** * Form controller for the user password forms. */ class UserPasswordResetForm extends FormBase { - /** - * A logger instance. - * - * @var \Psr\Log\LoggerInterface - */ - protected $logger; - - /** - * Constructs a new UserPasswordResetForm. - * - * @param \Psr\Log\LoggerInterface $logger - * A logger instance. - */ - public function __construct(LoggerInterface $logger) { - $this->logger = $logger; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('logger.factory')->get('user') - ); - } - /** * {@inheritdoc} */ @@ -75,20 +47,17 @@ class UserPasswordResetForm extends FormBase { $form['#title'] = $this->t('Set password'); } - $form['user'] = array( - '#type' => 'value', - '#value' => $user, - ); - $form['timestamp'] = array( - '#type' => 'value', - '#value' => $timestamp, - ); $form['help'] = array('#markup' => '<p>' . $this->t('This login can be used only once.') . '</p>'); $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array( '#type' => 'submit', '#value' => $this->t('Log in'), ); + $form['#action'] = Url::fromRoute('user.reset.login', [ + 'uid' => $user->id(), + 'timestamp' => $timestamp, + 'hash' => $hash, + ])->toString(); return $form; } @@ -96,22 +65,8 @@ class UserPasswordResetForm extends FormBase { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - /** @var $user \Drupal\user\UserInterface */ - $user = $form_state->getValue('user'); - user_login_finalize($user); - $this->logger->notice('User %name used one-time login link at time %timestamp.', array('%name' => $user->getUsername(), '%timestamp' => $form_state->getValue('timestamp'))); - drupal_set_message($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); - // Let the user's password be changed without the current password check. - $token = Crypt::randomBytesBase64(55); - $_SESSION['pass_reset_' . $user->id()] = $token; - $form_state->setRedirect( - 'entity.user.edit_form', - array('user' => $user->id()), - array( - 'query' => array('pass-reset-token' => $token), - 'absolute' => TRUE, - ) - ); + // This form works by submitting the hash and timestamp to the user.reset + // route with a 'login' action. } } diff --git a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php index 6ec88adb8..b11d8670b 100644 --- a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php @@ -43,8 +43,6 @@ class EntityUser extends EntityContentBase { * The storage for this entity type. * @param array $bundles * The list of bundles this entity type has. - * @param \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager - * The migrate plugin manager. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager service. * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager diff --git a/core/modules/user/src/Tests/UserMailNotifyTest.php b/core/modules/user/src/Tests/UserMailNotifyTest.php new file mode 100644 index 000000000..9943c5b73 --- /dev/null +++ b/core/modules/user/src/Tests/UserMailNotifyTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\user\Tests; + +use Drupal\Core\Test\AssertMailTrait; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; + +/** + * Tests _user_mail_notify() use of user.settings.notify.*. + * + * @group user + */ +class UserMailNotifyTest extends EntityKernelTestBase { + + use AssertMailTrait { + getMails as drupalGetMails; + } + + /** + * Data provider for user mail testing. + * + * @return array + */ + public function userMailsProvider() { + return [ + ['cancel_confirm', ['cancel_confirm']], + ['password_reset', ['password_reset']], + ['status_activated', ['status_activated']], + ['status_blocked', ['status_blocked']], + ['status_canceled', ['status_canceled']], + ['register_admin_created', ['register_admin_created']], + ['register_no_approval_required', ['register_no_approval_required']], + ['register_pending_approval', ['register_pending_approval', 'register_pending_approval_admin']] + ]; + } + + /** + * Tests mails are sent when notify.$op is TRUE. + * + * @param string $op + * The operation being performed on the account. + * @param array $mail_keys + * The mail keys to test for. + * + * @dataProvider userMailsProvider + */ + public function testUserMailsSent($op, array $mail_keys) { + $this->config('user.settings')->set('notify.' . $op, TRUE)->save(); + $return = _user_mail_notify($op, $this->createUser()); + $this->assertTrue($return, '_user_mail_notify() returns TRUE.'); + foreach ($mail_keys as $key) { + $filter = array('key' => $key); + $this->assertNotEmpty($this->getMails($filter), "Mails with $key exists."); + } + $this->assertCount(count($mail_keys), $this->getMails(), 'The expected number of emails sent.'); + } + + /** + * Tests mails are not sent when notify.$op is FALSE. + * + * @param string $op + * The operation being performed on the account. + * @param array $mail_keys + * The mail keys to test for. Ignored by this test because we assert that no + * mails at all are sent. + * + * @dataProvider userMailsProvider + */ + public function testUserMailsNotSent($op, array $mail_keys) { + $this->config('user.settings')->set('notify.' . $op, FALSE)->save(); + $return = _user_mail_notify($op, $this->createUser()); + $this->assertFalse($return, '_user_mail_notify() returns FALSE.'); + $this->assertEmpty($this->getMails(), 'No emails sent by _user_mail_notify().'); + } + +} diff --git a/core/modules/user/src/Tests/UserPasswordResetTest.php b/core/modules/user/src/Tests/UserPasswordResetTest.php index af8f56c06..8cf61db1d 100644 --- a/core/modules/user/src/Tests/UserPasswordResetTest.php +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php @@ -2,6 +2,8 @@ namespace Drupal\user\Tests; +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Url; use Drupal\system\Tests\Cache\PageCacheTagsTestBase; use Drupal\user\Entity\User; @@ -68,6 +70,11 @@ class UserPasswordResetTest extends PageCacheTagsTestBase { * Tests password reset functionality. */ function testUserPasswordReset() { + // Verify that accessing the password reset form without having the session + // variables set results in an access denied message. + $this->drupalGet(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()])); + $this->assertResponse(403); + // Try to reset the password for an invalid account. $this->drupalGet('user/password'); @@ -88,6 +95,9 @@ class UserPasswordResetTest extends PageCacheTagsTestBase { $resetURL = $this->getResetURL(); $this->drupalGet($resetURL); + // Ensure that the current url does not contain the hash and timestamp. + $this->assertUrl(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()])); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache')); // Ensure the password reset URL is not cached. @@ -125,6 +135,7 @@ class UserPasswordResetTest extends PageCacheTagsTestBase { // Log out, and try to log in again using the same one-time link. $this->drupalLogout(); $this->drupalGet($resetURL); + $this->drupalPostForm(NULL, NULL, t('Log in')); $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.'); // Request a new password again, this time using the email address. @@ -149,6 +160,7 @@ class UserPasswordResetTest extends PageCacheTagsTestBase { $bogus_timestamp = REQUEST_TIME - $timeout - 60; $_uid = $this->account->id(); $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp)); + $this->drupalPostForm(NULL, NULL, t('Log in')); $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.'); // Create a user, block the account, and verify that a login link is denied. @@ -175,7 +187,31 @@ class UserPasswordResetTest extends PageCacheTagsTestBase { $this->account->setEmail("1" . $this->account->getEmail()); $this->account->save(); $this->drupalGet($old_email_reset_link); + $this->drupalPostForm(NULL, NULL, t('Log in')); $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.'); + + // Verify a password reset link will automatically log a user when /login is + // appended. + $this->drupalGet('user/password'); + $edit = array('name' => $this->account->getUsername()); + $this->drupalPostForm(NULL, $edit, t('Submit')); + $reset_url = $this->getResetURL(); + $this->drupalGet($reset_url . '/login'); + $this->assertLink(t('Log out')); + $this->assertTitle(t('@name | @site', array('@name' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name'))), 'Logged in using password reset link.'); + + // Ensure blocked and deleted accounts can't access the user.reset.login + // route. + $this->drupalLogout(); + $timestamp = REQUEST_TIME - 1; + $blocked_account = $this->drupalCreateUser()->block(); + $blocked_account->save(); + $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login'); + $this->assertResponse(403); + + $blocked_account->delete(); + $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login'); + $this->assertResponse(403); } /** @@ -195,6 +231,25 @@ class UserPasswordResetTest extends PageCacheTagsTestBase { * Test user password reset while logged in. */ public function testUserPasswordResetLoggedIn() { + $another_account = $this->drupalCreateUser(); + $this->drupalLogin($another_account); + $this->drupalGet('user/password'); + $this->drupalPostForm(NULL, NULL, t('Submit')); + + // Click the reset URL while logged and change our password. + $resetURL = $this->getResetURL(); + // Log in as a different user. + $this->drupalLogin($this->account); + $this->drupalGet($resetURL); + $this->assertRaw(new FormattableMarkup( + 'Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please <a href=":logout">log out</a> and try using the link again.', + ['%other_user' => $this->account->getUsername(), '%resetting_user' => $another_account->getUsername(), ':logout' => Url::fromRoute('user.logout')->toString()] + )); + + $another_account->delete(); + $this->drupalGet($resetURL); + $this->assertText('The one-time login link you clicked is invalid.'); + // Log in. $this->drupalLogin($this->account); @@ -212,6 +267,14 @@ class UserPasswordResetTest extends PageCacheTagsTestBase { $edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password); $this->drupalPostForm(NULL, $edit, t('Save')); $this->assertText(t('The changes have been saved.'), 'Password changed.'); + + // Logged in users should not be able to access the user.reset.login or the + // user.reset.form routes. + $timestamp = REQUEST_TIME - 1; + $this->drupalGet("user/reset/" . $this->account->id() . "/$timestamp/" . user_pass_rehash($this->account, $timestamp) . '/login'); + $this->assertResponse(403); + $this->drupalGet("user/reset/" . $this->account->id()); + $this->assertResponse(403); } /** @@ -265,6 +328,7 @@ class UserPasswordResetTest extends PageCacheTagsTestBase { $reset_url = user_pass_reset_url($user1); $attack_reset_url = str_replace("user/reset/{$user1->id()}", "user/reset/{$user2->id()}", $reset_url); $this->drupalGet($attack_reset_url); + $this->drupalPostForm(NULL, NULL, t('Log in')); $this->assertNoText($user2->getUsername(), 'The invalid password reset page does not show the user name.'); $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); diff --git a/core/modules/user/src/UserServiceProvider.php b/core/modules/user/src/UserServiceProvider.php index 79715dfc8..49537e56a 100644 --- a/core/modules/user/src/UserServiceProvider.php +++ b/core/modules/user/src/UserServiceProvider.php @@ -1,5 +1,8 @@ <?php // @codingStandardsIgnoreFile -// This file is intentionally empty so that it overwrites when sites are +// This class is intentionally empty so that it overwrites when sites are // updated from a zip/tarball without deleting the /core folder first. // @todo: remove in 8.3.x +// +namespace Drupal\user; +class UserServiceProvider {} diff --git a/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php index f7613e2c5..d3f49ccfc 100644 --- a/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php @@ -28,22 +28,22 @@ class MigrateUserRoleTest extends MigrateDrupal6TestBase { $id_map = $this->getMigration('d6_user_role')->getIdMap(); $rid = 'anonymous'; $anonymous = Role::load($rid); - $this->assertIdentical($rid, $anonymous->id()); - $this->assertIdentical(array('migrate test anonymous permission', 'use text format filtered_html'), $anonymous->getPermissions()); - $this->assertIdentical(array($rid), $id_map->lookupDestinationId(array(1))); + $this->assertSame($rid, $anonymous->id()); + $this->assertSame(array('migrate test anonymous permission', 'use text format filtered_html'), $anonymous->getPermissions()); + $this->assertSame(array($rid), $id_map->lookupDestinationId(array(1))); $rid = 'authenticated'; $authenticated = Role::load($rid); - $this->assertIdentical($rid, $authenticated->id()); - $this->assertIdentical(array('migrate test authenticated permission', 'use text format filtered_html'), $authenticated->getPermissions()); - $this->assertIdentical(array($rid), $id_map->lookupDestinationId(array(2))); + $this->assertSame($rid, $authenticated->id()); + $this->assertSame(array('migrate test authenticated permission', 'use text format filtered_html'), $authenticated->getPermissions()); + $this->assertSame(array($rid), $id_map->lookupDestinationId(array(2))); $rid = 'migrate_test_role_1'; $migrate_test_role_1 = Role::load($rid); - $this->assertIdentical($rid, $migrate_test_role_1->id()); - $this->assertIdentical(array('migrate test role 1 test permission', 'use text format full_html', 'use text format php_code'), $migrate_test_role_1->getPermissions()); - $this->assertIdentical(array($rid), $id_map->lookupDestinationId(array(3))); + $this->assertSame($rid, $migrate_test_role_1->id()); + $this->assertSame(array('migrate test role 1 test permission', 'use text format full_html', 'use text format php_code'), $migrate_test_role_1->getPermissions()); + $this->assertSame(array($rid), $id_map->lookupDestinationId(array(3))); $rid = 'migrate_test_role_2'; $migrate_test_role_2 = Role::load($rid); - $this->assertIdentical(array( + $this->assertSame(array( 'migrate test role 2 test permission', 'use PHP for settings', 'administer contact forms', @@ -61,12 +61,12 @@ class MigrateUserRoleTest extends MigrateDrupal6TestBase { 'access content overview', 'use text format php_code', ), $migrate_test_role_2->getPermissions()); - $this->assertIdentical($rid, $migrate_test_role_2->id()); - $this->assertIdentical(array($rid), $id_map->lookupDestinationId(array(4))); + $this->assertSame($rid, $migrate_test_role_2->id()); + $this->assertSame(array($rid), $id_map->lookupDestinationId(array(4))); $rid = 'migrate_test_role_3_that_is_long'; $migrate_test_role_3 = Role::load($rid); - $this->assertIdentical($rid, $migrate_test_role_3->id()); - $this->assertIdentical(array($rid), $id_map->lookupDestinationId(array(5))); + $this->assertSame($rid, $migrate_test_role_3->id()); + $this->assertSame(array($rid), $id_map->lookupDestinationId(array(5))); } } diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php index 6700727ee..d4a093e36 100644 --- a/core/modules/user/user.api.php +++ b/core/modules/user/user.api.php @@ -115,7 +115,7 @@ function hook_user_cancel_methods_alter(&$methods) { * @param $account * The account object the name belongs to. * - * @see \Drupal\Core\Session\AccountInterface->getDisplayName() + * @see \Drupal\Core\Session\AccountInterface::getDisplayName() */ function hook_user_format_name_alter(&$name, $account) { // Display the user's uid instead of name. diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 12adb4a2b..04992a0b9 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1195,9 +1195,7 @@ function user_role_revoke_permissions($rid, array $permissions = array()) { * @see user_mail_tokens() */ function _user_mail_notify($op, $account, $langcode = NULL) { - // By default, we always notify except for canceled and blocked. - $notify = \Drupal::config('user.settings')->get('notify.' . $op); - if ($notify || ($op != 'status_canceled' && $op != 'status_blocked')) { + if (\Drupal::config('user.settings')->get('notify.' . $op)) { $params['account'] = $account; $langcode = $langcode ? $langcode : $account->getPreferredLangcode(); // Get the custom site notification email to use as the from email address diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 6eea7ecec..8864eecc8 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -140,6 +140,17 @@ user.cancel_confirm: _entity_access: 'user.delete' user: \d+ +user.reset.login: + path: '/user/reset/{uid}/{timestamp}/{hash}/login' + defaults: + _controller: '\Drupal\user\Controller\UserController::resetPassLogin' + _title: 'Reset password' + requirements: + _user_is_logged_in: 'FALSE' + options: + _maintenance_access: TRUE + no_cache: TRUE + user.reset: path: '/user/reset/{uid}/{timestamp}/{hash}' defaults: @@ -150,3 +161,14 @@ user.reset: options: _maintenance_access: TRUE no_cache: TRUE + +user.reset.form: + path: '/user/reset/{uid}' + defaults: + _controller: '\Drupal\user\Controller\UserController::getResetPassForm' + _title: 'Reset password' + requirements: + _user_is_logged_in: 'FALSE' + options: + _maintenance_access: TRUE + no_cache: TRUE diff --git a/core/modules/views/src/Tests/FieldApiDataTest.php b/core/modules/views/src/Tests/FieldApiDataTest.php index 086f1795b..3fd8a03fa 100644 --- a/core/modules/views/src/Tests/FieldApiDataTest.php +++ b/core/modules/views/src/Tests/FieldApiDataTest.php @@ -64,6 +64,7 @@ class FieldApiDataTest extends FieldTestBase { $this->assertTrue(isset($data[$revision_table]['table']['join']['node_field_revision'])); $expected_join = array( + 'table' => $current_table, 'left_field' => 'nid', 'field' => 'entity_id', 'extra' => array( @@ -73,6 +74,7 @@ class FieldApiDataTest extends FieldTestBase { ); $this->assertEqual($expected_join, $data[$current_table]['table']['join']['node_field_data']); $expected_join = array( + 'table' => $revision_table, 'left_field' => 'vid', 'field' => 'revision_id', 'extra' => array( diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php index db06b46f7..ed9f2dbff 100644 --- a/core/modules/views/views.api.php +++ b/core/modules/views/views.api.php @@ -701,7 +701,7 @@ function hook_views_pre_view(ViewExecutable $view, $display_id, array &$args) { // Modify contextual filters for my_special_view if user has 'my special permission'. $account = \Drupal::currentUser(); - if ($view->name == 'my_special_view' && $account->hasPermission('my special permission') && $display_id == 'public_display') { + if ($view->id() == 'my_special_view' && $account->hasPermission('my special permission') && $display_id == 'public_display') { $args[0] = 'custom value'; } } @@ -744,7 +744,7 @@ function hook_views_post_build(ViewExecutable $view) { // assumptions about both exposed filter settings and the fields in the view. // Also note that this alter could be done at any point before the view being // rendered.) - if ($view->name == 'my_view' && isset($view->exposed_raw_input['type']) && $view->exposed_raw_input['type'] != 'All') { + if ($view->id() == 'my_view' && isset($view->exposed_raw_input['type']) && $view->exposed_raw_input['type'] != 'All') { // 'Type' should be interpreted as content type. if (isset($view->field['type'])) { $view->field['type']->options['exclude'] = TRUE; @@ -771,7 +771,7 @@ function hook_views_pre_execute(ViewExecutable $view) { $account = \Drupal::currentUser(); if (count($view->query->tables) > 2 && $account->hasPermission('administer views')) { - drupal_set_message(t('The view %view may be heavy to execute.', array('%view' => $view->name)), 'warning'); + drupal_set_message(t('The view %view may be heavy to execute.', array('%view' => $view->id())), 'warning'); } } diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc index abbbf314e..466e30e31 100644 --- a/core/modules/views/views.views.inc +++ b/core/modules/views/views.views.inc @@ -357,6 +357,7 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora if ($data_table) { // Tell Views how to join to the base table, via the data table. $data[$table_alias]['table']['join'][$data_table] = array( + 'table' => $table_mapping->getDedicatedDataTableName($field_storage), 'left_field' => $entity_type->getKey('id'), 'field' => 'entity_id', 'extra' => array( @@ -368,6 +369,7 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora else { // If there is no data table, just join directly. $data[$table_alias]['table']['join'][$base_table] = array( + 'table' => $table_mapping->getDedicatedDataTableName($field_storage), 'left_field' => $entity_type->getKey('id'), 'field' => 'entity_id', 'extra' => array( @@ -381,6 +383,7 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora if ($entity_revision_data_table) { // Tell Views how to join to the revision table, via the data table. $data[$table_alias]['table']['join'][$entity_revision_data_table] = array( + 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), 'left_field' => $entity_type->getKey('revision'), 'field' => 'revision_id', 'extra' => array( @@ -392,6 +395,7 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora else { // If there is no data table, just join directly. $data[$table_alias]['table']['join'][$entity_revision_table] = array( + 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), 'left_field' => $entity_type->getKey('revision'), 'field' => 'revision_id', 'extra' => array( diff --git a/core/phpcs.xml.dist b/core/phpcs.xml.dist index 3fcb48efe..05fc8d39f 100644 --- a/core/phpcs.xml.dist +++ b/core/phpcs.xml.dist @@ -44,8 +44,10 @@ <rule ref="Drupal.Commenting.DocCommentStar"/> <rule ref="Drupal.Commenting.FileComment"/> <rule ref="Drupal.Commenting.FunctionComment"> + <exclude name="Drupal.Commenting.FunctionComment.IncorrectParamVarName"/> <exclude name="Drupal.Commenting.FunctionComment.IncorrectTypeHint"/> <exclude name="Drupal.Commenting.FunctionComment.InvalidNoReturn"/> + <exclude name="Drupal.Commenting.FunctionComment.InvalidReturn"/> <exclude name="Drupal.Commenting.FunctionComment.InvalidReturnNotVoid"/> <exclude name="Drupal.Commenting.FunctionComment.InvalidTypeHint"/> <exclude name="Drupal.Commenting.FunctionComment.Missing"/> diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index 951bd55d4..9658f2ce0 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -1,19 +1,23 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- TODO set checkForUnintentionallyCoveredCode="true" once https://www.drupal.org/node/2626832 is resolved. --> -<!-- TODO set printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter" once - https://youtrack.jetbrains.com/issue/WI-24808 is resolved. Drupal provides a - result printer that links to the html output results for functional tests. - Unfortunately, this breaks the output of PHPStorm's PHPUnit runner. However, if - using the command line you can add - - -printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" to use it (note there - should be no spaces between the hyphens). +<!-- PHPUnit expects functional tests to be run with either a privileged user + or your current system user. See core/tests/README.md and + https://www.drupal.org/node/2116263 for details. --> <phpunit bootstrap="tests/bootstrap.php" colors="true" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" beStrictAboutChangesToGlobalState="true" checkForUnintentionallyCoveredCode="false"> +<!-- TODO set printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter" once + https://youtrack.jetbrains.com/issue/WI-24808 is resolved. Drupal provides a + result printer that links to the html output results for functional tests. + Unfortunately, this breaks the output of PHPStorm's PHPUnit runner. However, if + using the command line you can add + - -printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter" to use it (note + there should be no spaces between the hyphens). +--> <php> <!-- Set error reporting to E_ALL. --> <ini name="error_reporting" value="32767"/> diff --git a/core/tests/Drupal/KernelTests/Core/Config/Storage/StorageReplaceDataWrapperTest.php b/core/tests/Drupal/KernelTests/Core/Config/Storage/StorageReplaceDataWrapperTest.php new file mode 100644 index 000000000..84b4f564c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Config/Storage/StorageReplaceDataWrapperTest.php @@ -0,0 +1,93 @@ +<?php + +namespace Drupal\KernelTests\Core\Config\Storage; + +use Drupal\config\StorageReplaceDataWrapper; +use Drupal\Core\Config\StorageInterface; + +/** + * Tests StorageReplaceDataWrapper operations. + * + * @group config + */ +class StorageReplaceDataWrapperTest extends ConfigStorageTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->storage = new StorageReplaceDataWrapper($this->container->get('config.storage')); + // ::listAll() verifications require other configuration data to exist. + $this->storage->write('system.performance', array()); + $this->storage->replaceData('system.performance', array('foo' => 'bar')); + } + + /** + * {@inheritdoc} + */ + protected function read($name) { + return $this->storage->read($name); + } + + /** + * {@inheritdoc} + */ + protected function insert($name, $data) { + $this->storage->write($name, $data); + } + + /** + * {@inheritdoc} + */ + protected function update($name, $data) { + $this->storage->write($name, $data); + } + + /** + * {@inheritdoc} + */ + protected function delete($name) { + $this->storage->delete($name); + } + + /** + * {@inheritdoc} + */ + public function testInvalidStorage() { + // No-op as this test does not make sense. + } + + /** + * Tests if new collections created correctly. + * + * @param string $collection + * The collection name. + * + * @dataProvider providerCollections + */ + public function testCreateCollection($collection) { + $initial_collection_name = $this->storage->getCollectionName(); + + // Create new storage with given collection and check it is set correctly. + $new_storage = $this->storage->createCollection($collection); + $this->assertSame($collection, $new_storage->getCollectionName()); + + // Check collection not changed in the current storage instance. + $this->assertSame($initial_collection_name, $this->storage->getCollectionName()); + } + + /** + * Data provider for testing different collections. + * + * @return array + * Returns an array of collection names. + */ + public function providerCollections() { + return [ + [StorageInterface::DEFAULT_COLLECTION], + ['foo.bar'], + ]; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php index 5e2265de2..edb66939c 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php @@ -62,9 +62,9 @@ class StableTemplateOverrideTest extends KernelTestBase { // Enable all core modules. $all_modules = system_rebuild_module_data(); $all_modules = array_filter($all_modules, function ($module) { - // Filter contrib, hidden, already enabled modules and modules in the - // Testing package. - if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') { + // Filter contrib, hidden, experimental, already enabled modules, and + // modules in the Testing package. + if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info['package'] == 'Core (Experimental)') { return FALSE; } return TRUE; diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index ded28f2f7..0e881dfd8 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -1575,7 +1575,7 @@ abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase { * Retrieves the plain-text content from the current page. */ protected function getTextContent() { - return $this->getSession()->getPage()->getContent(); + return $this->getSession()->getPage()->getText(); } /** diff --git a/core/tests/Drupal/Tests/Core/Datetime/DateTest.php b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php index 238b5c88e..edf6d714e 100644 --- a/core/tests/Drupal/Tests/Core/Datetime/DateTest.php +++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\Core\Datetime; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Datetime\DateFormatter; use Drupal\Core\Datetime\FormattedDateDiff; use Drupal\Core\DependencyInjection\ContainerBuilder; @@ -392,6 +393,38 @@ class DateTest extends UnitTestCase { return $data; } + /** + * Tests FormattedDateDiff. + * + * @covers \Drupal\Core\Datetime\FormattedDateDiff::toRenderable + * @covers \Drupal\Core\Datetime\FormattedDateDiff::getString + * @covers \Drupal\Core\Datetime\FormattedDateDiff::getCacheMaxAge + */ + public function testFormattedDateDiff() { + $string = '10 minutes'; + $max_age = 60; + $object = new FormattedDateDiff($string, $max_age); + + // Test conversion to a render array. + $expected = [ + '#markup' => $string, + '#cache' => [ + 'max-age' => $max_age, + ], + ]; + $this->assertArrayEquals($expected, $object->toRenderable()); + + // Test retrieving the formatted time difference string. + $this->assertEquals($string, $object->getString()); + + // Test applying cacheability data to an existing build. + $build = []; + CacheableMetadata::createFromObject($object)->applyTo($build); + $this->assertEquals($max_age, $build['#cache']['max-age']); + // Test the BC layer. + $this->assertSame($object->getCacheMaxAge(), $object->getMaxAge()); + } + /** * Creates a UNIX timestamp given a date and time string in the format * year-month-day hour:minute:seconds (e.g. 2013-12-11 10:09:08). diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php index 77a3d2253..ee5dfa9d5 100644 --- a/core/tests/Drupal/Tests/UnitTestCase.php +++ b/core/tests/Drupal/Tests/UnitTestCase.php @@ -198,8 +198,8 @@ abstract class UnitTestCase extends \PHPUnit_Framework_TestCase { /** * Returns a stub translation manager that just returns the passed string. * - * @return \PHPUnit_Framework_MockObject_MockBuilder - * A MockBuilder of \Drupal\Core\StringTranslation\TranslationInterface + * @return \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\StringTranslation\TranslationInterface + * A mock translation object. */ public function getStringTranslationStub() { $translation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface'); diff --git a/core/tests/README.md b/core/tests/README.md index dfd1154a7..b3ed5a07e 100644 --- a/core/tests/README.md +++ b/core/tests/README.md @@ -13,3 +13,36 @@ ./vendor/bin/phpunit -c core --testsuite functional ./vendor/bin/phpunit -c core --testsuite functional-javascript ``` + +Note: functional tests have to be invoked with a user in the same group as the +web server user. You can either configure Apache (or nginx) to run as your own +system user or run tests as a privileged user instead. + +To develop locally, a straigtforward - but also less secure - approach is to run +tests as your own system user. To achieve that, change the default Apache user +to run as your system user. Typically, you'd need to modify +`/etc/apache2/envvars` on Linux or `/etc/apache2/httpd.conf` on Mac. + +Example for Linux: + +``` +export APACHE_RUN_USER=<your-user> +export APACHE_RUN_GROUP=<your-group> +``` + +Example for Mac: + +``` +User <your-user> +Group <your-group> +``` + +If the default user is e.g. `www-data`, the above functional tests will have to +be invoked with sudo instead: + +``` +export SIMPLETEST_DB='mysql://root@localhost/dev_d8' +export SIMPLETEST_BASE_URL='http://d8.dev' +sudo -u www-data ./vendor/bin/phpunit -c core --testsuite functional +sudo -u www-data ./vendor/bin/phpunit -c core --testsuite functional-javascript +``` diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php index 804aedf0b..9aeb1d79f 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -51,6 +51,7 @@ function drupal_phpunit_contrib_extension_directory_roots($root = NULL) { $root . '/core/profiles', $root . '/modules', $root . '/profiles', + $root . '/themes', ); $sites_path = $root . '/sites'; // Note this also checks sites/../modules and sites/../profiles. @@ -61,6 +62,7 @@ function drupal_phpunit_contrib_extension_directory_roots($root = NULL) { $path = "$sites_path/$site"; $paths[] = is_dir("$path/modules") ? realpath("$path/modules") : NULL; $paths[] = is_dir("$path/profiles") ? realpath("$path/profiles") : NULL; + $paths[] = is_dir("$path/themes") ? realpath("$path/themes") : NULL; } return array_filter($paths); }