diff --git a/core/core.api.php b/core/core.api.php index 161ddb9146..f537882a5a 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -606,6 +606,30 @@ * $settings['cache']['default'] = 'cache.custom'; * @endcode * + * For cache bins that are stored in the database, the number of rows is limited + * to 5000 by default. This can be changed for all database cache bins. For + * example, to instead limit the number of rows to 50000: + * @code + * $settings['database_cache_max_rows']['default'] = 50000; + * @endcode + * + * Or per bin (in this example we allow infinite entries): + * @code + * $settings['database_cache_max_rows']['bins']['dynamic_page_cache'] = -1; + * @endcode + * + * For monitoring reasons it might be useful to figure out the amount of data + * stored in tables. The following SQL snippet can be used for that: + * @code + * SELECT table_name AS `Table`, table_rows AS 'Num. of Rows', + * ROUND(((data_length + index_length) / 1024 / 1024), 2) `Size in MB` FROM + * information_schema.TABLES WHERE table_schema = '***DATABASE_NAME***' AND + * table_name LIKE 'cache_%' ORDER BY (data_length + index_length) DESC + * LIMIT 10; + * @encode + * + * @see \Drupal\Core\Cache\DatabaseBackend + * * Finally, you can chain multiple cache backends together, see * \Drupal\Core\Cache\ChainedFastBackend and \Drupal\Core\Cache\BackendChain. * diff --git a/core/core.services.yml b/core/core.services.yml index 76088786cd..598754e10b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -192,7 +192,7 @@ services: - [setContainer, ['@service_container']] cache.backend.database: class: Drupal\Core\Cache\DatabaseBackendFactory - arguments: ['@database', '@cache_tags.invalidator.checksum'] + arguments: ['@database', '@cache_tags.invalidator.checksum', '@settings'] cache.backend.apcu: class: Drupal\Core\Cache\ApcuBackendFactory arguments: ['@app.root', '@site.path', '@cache_tags.invalidator.checksum'] diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index d53c51c2fc..fafff0ebb8 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -17,6 +17,30 @@ class DatabaseBackend implements CacheBackendInterface { /** + * The default maximum number of rows that this cache bin table can store. + * + * This maximum is introduced to ensure that the database is not filled with + * hundred of thousand of cache entries with gigabytes in size. + * + * Read about how to change it in the @link cache Cache API topic. @endlink + */ + const DEFAULT_MAX_ROWS = 5000; + + /** + * -1 means infinite allows numbers of rows for the cache backend. + */ + const MAXIMUM_NONE = -1; + + /** + * The maximum number of rows that this cache bin table is allowed to store. + * + * * @see ::MAXIMUM_NONE + * + * @var int + */ + protected $maxRows; + + /** * @var string */ protected $bin; @@ -45,14 +69,18 @@ class DatabaseBackend implements CacheBackendInterface { * The cache tags checksum provider. * @param string $bin * The cache bin for which the object is created. + * @param int $max_rows + * (optional) The maximum number of rows that are allowed in this cache bin + * table. */ - public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin) { + public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin, $max_rows = NULL) { // All cache tables should be prefixed with 'cache_'. $bin = 'cache_' . $bin; $this->bin = $bin; $this->connection = $connection; $this->checksumProvider = $checksum_provider; + $this->maxRows = $max_rows === NULL ? static::DEFAULT_MAX_ROWS : $max_rows; } /** @@ -326,6 +354,22 @@ public function invalidateAll() { */ public function garbageCollection() { try { + // Bounded size cache bin, using FIFO. + if ($this->maxRows !== static::MAXIMUM_NONE) { + $first_invalid_create_time = $this->connection->select($this->bin) + ->fields($this->bin, ['created']) + ->orderBy("{$this->bin}.created", 'DESC') + ->range($this->maxRows, $this->maxRows + 1) + ->execute() + ->fetchField(); + + if ($first_invalid_create_time) { + $this->connection->delete($this->bin) + ->condition('created', $first_invalid_create_time, '<=') + ->execute(); + } + } + $this->connection->delete($this->bin) ->condition('expire', Cache::PERMANENT, '<>') ->condition('expire', REQUEST_TIME, '<') @@ -472,10 +516,20 @@ public function schemaDefinition() { ], 'indexes' => [ 'expire' => ['expire'], + 'created' => ['created'], ], 'primary key' => ['cid'], ]; return $schema; } + /** + * The maximum number of rows that this cache bin table is allowed to store. + * + * @return int + */ + public function getMaxRows() { + return $this->maxRows; + } + } diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php b/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php index 8aa018ec45..d61ecbca04 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Cache; use Drupal\Core\Database\Connection; +use Drupal\Core\Site\Settings; class DatabaseBackendFactory implements CacheFactoryInterface { @@ -21,16 +22,26 @@ class DatabaseBackendFactory implements CacheFactoryInterface { protected $checksumProvider; /** + * The settings array. + * + * @var \Drupal\Core\Site\Settings + */ + protected $settings; + + /** * Constructs the DatabaseBackendFactory object. * * @param \Drupal\Core\Database\Connection $connection * Database connection * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider * The cache tags checksum provider. + * @param \Drupal\Core\Site\Settings $settings + * (optional) The settings array. */ - public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider) { + public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, Settings $settings = NULL) { $this->connection = $connection; $this->checksumProvider = $checksum_provider; + $this->settings = $settings ?: Settings::getInstance(); } /** @@ -43,7 +54,35 @@ public function __construct(Connection $connection, CacheTagsChecksumInterface $ * The cache backend object for the specified cache bin. */ public function get($bin) { - return new DatabaseBackend($this->connection, $this->checksumProvider, $bin); + $max_rows = $this->getMaxRowsForBin($bin); + return new DatabaseBackend($this->connection, $this->checksumProvider, $bin, $max_rows); + } + + /** + * Gets the max rows for the specified cache bin. + * + * @param string $bin + * The cache bin for which the object is created. + * + * @return int + * The maximum number of rows for the given bin. Defaults to + * DatabaseBackend::DEFAULT_MAX_ROWS. + */ + protected function getMaxRowsForBin($bin) { + $max_rows_settings = $this->settings->get('database_cache_max_rows'); + // First, look for a cache bin specific setting. + if (isset($max_rows_settings['bins'][$bin])) { + $max_rows = $max_rows_settings['bins'][$bin]; + } + // Third, use configured default backend. + elseif (isset($max_rows_settings['default'])) { + $max_rows = $max_rows_settings['default']; + } + else { + // Fall back to the default max rows if nothing else is configured. + $max_rows = DatabaseBackend::DEFAULT_MAX_ROWS; + } + return $max_rows; } } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityListBuilder.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityListBuilder.php index 38b7fa87a1..e26a03ca85 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityListBuilder.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityListBuilder.php @@ -37,14 +37,14 @@ public function getDefaultOperations(EntityInterface $entity) { $operations['enable'] = [ 'title' => t('Enable'), 'weight' => -10, - 'url' => $entity->urlInfo('enable'), + 'url' => $this->ensureDestination($entity->toUrl('enable')), ]; } elseif ($entity->hasLinkTemplate('disable')) { $operations['disable'] = [ 'title' => t('Disable'), 'weight' => 40, - 'url' => $entity->urlInfo('disable'), + 'url' => $this->ensureDestination($entity->toUrl('disable')), ]; } } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 260e4772f3..217d4dc781 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -7,6 +7,7 @@ use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Cache\DatabaseBackend; use Drupal\Core\Config\BootstrapConfigStorageFactory; use Drupal\Core\Config\NullStorage; use Drupal\Core\Database\Database; @@ -77,7 +78,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { ], 'cache.container' => [ 'class' => 'Drupal\Core\Cache\DatabaseBackend', - 'arguments' => ['@database', '@cache_tags_provider.container', 'container'], + 'arguments' => ['@database', '@cache_tags_provider.container', 'container', DatabaseBackend::MAXIMUM_NONE], ], 'cache_tags_provider.container' => [ 'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum', diff --git a/core/lib/Drupal/Core/Entity/EntityListBuilder.php b/core/lib/Drupal/Core/Entity/EntityListBuilder.php index 7f46915ae9..c1654362d0 100644 --- a/core/lib/Drupal/Core/Entity/EntityListBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityListBuilder.php @@ -2,6 +2,8 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Routing\RedirectDestinationTrait; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -11,6 +13,8 @@ */ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderInterface, EntityHandlerInterface { + use RedirectDestinationTrait; + /** * The entity storage class. * @@ -143,14 +147,14 @@ protected function getDefaultOperations(EntityInterface $entity) { $operations['edit'] = [ 'title' => $this->t('Edit'), 'weight' => 10, - 'url' => $entity->urlInfo('edit-form'), + 'url' => $this->ensureDestination($entity->toUrl('edit-form')), ]; } if ($entity->access('delete') && $entity->hasLinkTemplate('delete-form')) { $operations['delete'] = [ 'title' => $this->t('Delete'), 'weight' => 100, - 'url' => $entity->urlInfo('delete-form'), + 'url' => $this->ensureDestination($entity->toUrl('delete-form')), ]; } @@ -250,4 +254,17 @@ protected function getTitle() { return; } + /** + * Ensures that a destination is present on the given URL. + * + * @param \Drupal\Core\Url $url + * The URL object to which the destination should be added. + * + * @return \Drupal\Core\Url + * The updated URL object. + */ + protected function ensureDestination(Url $url) { + return $url->mergeOptions(['query' => $this->getRedirectDestination()->getAsArray()]); + } + } diff --git a/core/modules/action/tests/src/Functional/ConfigurationTest.php b/core/modules/action/tests/src/Functional/ConfigurationTest.php index 472ba17125..a260c079cf 100644 --- a/core/modules/action/tests/src/Functional/ConfigurationTest.php +++ b/core/modules/action/tests/src/Functional/ConfigurationTest.php @@ -44,14 +44,15 @@ public function testActionConfiguration() { $this->drupalPostForm('admin/config/system/actions/add/' . Crypt::hashBase64('action_goto_action'), $edit, t('Save')); $this->assertResponse(200); + $action_id = $edit['id']; + // Make sure that the new complex action was saved properly. $this->assertText(t('The action has been successfully saved.'), "Make sure we get a confirmation that we've successfully saved the complex action."); $this->assertText($action_label, "Make sure the action label appears on the configuration page after we've saved the complex action."); // Make another POST request to the action edit page. $this->clickLink(t('Configure')); - preg_match('|admin/config/system/actions/configure/(.+)|', $this->getUrl(), $matches); - $aid = $matches[1]; + $edit = []; $new_action_label = $this->randomMachineName(); $edit['label'] = $new_action_label; @@ -73,7 +74,7 @@ public function testActionConfiguration() { $this->clickLink(t('Delete')); $this->assertResponse(200); $edit = []; - $this->drupalPostForm("admin/config/system/actions/configure/$aid/delete", $edit, t('Delete')); + $this->drupalPostForm(NULL, $edit, t('Delete')); $this->assertResponse(200); // Make sure that the action was actually deleted. @@ -82,7 +83,7 @@ public function testActionConfiguration() { $this->assertResponse(200); $this->assertNoText($new_action_label, "Make sure the action label does not appear on the overview page after we've deleted the action."); - $action = Action::load($aid); + $action = Action::load($action_id); $this->assertFalse($action, 'Make sure the action is gone after being deleted.'); } diff --git a/core/modules/block_content/src/BlockContentListBuilder.php b/core/modules/block_content/src/BlockContentListBuilder.php index 96259173cd..7a4bdfc4c8 100644 --- a/core/modules/block_content/src/BlockContentListBuilder.php +++ b/core/modules/block_content/src/BlockContentListBuilder.php @@ -4,7 +4,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityListBuilder; -use Drupal\Core\Routing\RedirectDestinationTrait; /** * Defines a class to build a listing of custom block entities. @@ -13,8 +12,6 @@ */ class BlockContentListBuilder extends EntityListBuilder { - use RedirectDestinationTrait; - /** * {@inheritdoc} */ @@ -31,15 +28,4 @@ public function buildRow(EntityInterface $entity) { return $row + parent::buildRow($entity); } - /** - * {@inheritdoc} - */ - public function getDefaultOperations(EntityInterface $entity) { - $operations = parent::getDefaultOperations($entity); - if (isset($operations['edit'])) { - $operations['edit']['query']['destination'] = $this->getRedirectDestination()->get(); - } - return $operations; - } - } diff --git a/core/modules/config/src/Tests/ConfigEntityListTest.php b/core/modules/config/src/Tests/ConfigEntityListTest.php index e9950ea424..46c0c064b6 100644 --- a/core/modules/config/src/Tests/ConfigEntityListTest.php +++ b/core/modules/config/src/Tests/ConfigEntityListTest.php @@ -2,6 +2,7 @@ namespace Drupal\config\Tests; +use Drupal\Core\Routing\RedirectDestinationTrait; use Drupal\simpletest\WebTestBase; use Drupal\config_test\Entity\ConfigTest; use Drupal\Core\Entity\EntityStorageInterface; @@ -13,6 +14,8 @@ */ class ConfigEntityListTest extends WebTestBase { + use RedirectDestinationTrait; + /** * Modules to enable. * @@ -54,17 +57,17 @@ public function testList() { 'edit' => [ 'title' => t('Edit'), 'weight' => 10, - 'url' => $entity->urlInfo(), + 'url' => $entity->toUrl()->setOption('query', $this->getRedirectDestination()->getAsArray()), ], 'disable' => [ 'title' => t('Disable'), 'weight' => 40, - 'url' => $entity->urlInfo('disable'), + 'url' => $entity->toUrl('disable')->setOption('query', $this->getRedirectDestination()->getAsArray()), ], 'delete' => [ 'title' => t('Delete'), 'weight' => 100, - 'url' => $entity->urlInfo('delete-form'), + 'url' => $entity->toUrl('delete-form')->setOption('query', $this->getRedirectDestination()->getAsArray()), ], ]; @@ -129,12 +132,12 @@ public function testList() { 'edit' => [ 'title' => t('Edit'), 'weight' => 10, - 'url' => $entity->urlInfo(), + 'url' => $entity->toUrl()->setOption('query', $this->getRedirectDestination()->getAsArray()), ], 'delete' => [ 'title' => t('Delete'), 'weight' => 100, - 'url' => $entity->urlInfo('delete-form'), + 'url' => $entity->toUrl('delete-form')->setOption('query', $this->getRedirectDestination()->getAsArray()), ], ]; diff --git a/core/modules/content_moderation/config/schema/content_moderation.schema.yml b/core/modules/content_moderation/config/schema/content_moderation.schema.yml index 5a10d85e9e..d48ffe439f 100644 --- a/core/modules/content_moderation/config/schema/content_moderation.schema.yml +++ b/core/modules/content_moderation/config/schema/content_moderation.schema.yml @@ -1,11 +1,3 @@ -views.filter.latest_revision: - type: views_filter - label: 'Latest revision' - mapping: - value: - type: string - label: 'Value' - content_moderation.state: type: workflows.state mapping: diff --git a/core/modules/content_moderation/content_moderation.install b/core/modules/content_moderation/content_moderation.install new file mode 100644 index 0000000000..e33a35973c --- /dev/null +++ b/core/modules/content_moderation/content_moderation.install @@ -0,0 +1,16 @@ +schema(); + if ($database_schema->tableExists('content_revision_tracker')) { + $database_schema->dropTable('content_revision_tracker'); + } +} diff --git a/core/modules/content_moderation/content_moderation.services.yml b/core/modules/content_moderation/content_moderation.services.yml index 256095a03a..5035b673c5 100644 --- a/core/modules/content_moderation/content_moderation.services.yml +++ b/core/modules/content_moderation/content_moderation.services.yml @@ -15,11 +15,6 @@ services: arguments: ['@content_moderation.moderation_information'] tags: - { name: access_check, applies_to: _content_moderation_latest_version } - content_moderation.revision_tracker: - class: Drupal\content_moderation\RevisionTracker - arguments: ['@database'] - tags: - - { name: backend_overridable } content_moderation.config_import_subscriber: class: Drupal\content_moderation\EventSubscriber\ConfigImportSubscriber arguments: ['@config.manager', '@entity_type.manager'] diff --git a/core/modules/content_moderation/content_moderation.views.inc b/core/modules/content_moderation/content_moderation.views.inc index faabc6aaec..799af94241 100644 --- a/core/modules/content_moderation/content_moderation.views.inc +++ b/core/modules/content_moderation/content_moderation.views.inc @@ -17,13 +17,6 @@ function content_moderation_views_data() { } /** - * Implements hook_views_data_alter(). - */ -function content_moderation_views_data_alter(array &$data) { - _content_moderation_views_data_object()->alterViewsData($data); -} - -/** * Creates a ViewsData object to respond to views hooks. * * @return \Drupal\content_moderation\ViewsData diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php index 580c21e81a..a7fcf7e8d9 100644 --- a/core/modules/content_moderation/src/EntityOperations.php +++ b/core/modules/content_moderation/src/EntityOperations.php @@ -42,13 +42,6 @@ class EntityOperations implements ContainerInjectionInterface { protected $formBuilder; /** - * The Revision Tracker service. - * - * @var \Drupal\content_moderation\RevisionTrackerInterface - */ - protected $tracker; - - /** * The entity bundle information service. * * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface @@ -64,16 +57,13 @@ class EntityOperations implements ContainerInjectionInterface { * Entity type manager service. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder. - * @param \Drupal\content_moderation\RevisionTrackerInterface $tracker - * The revision tracker. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info * The entity bundle information service. */ - public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_info) { + public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityTypeBundleInfoInterface $bundle_info) { $this->moderationInfo = $moderation_info; $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; - $this->tracker = $tracker; $this->bundleInfo = $bundle_info; } @@ -85,7 +75,6 @@ public static function create(ContainerInterface $container) { $container->get('content_moderation.moderation_information'), $container->get('entity_type.manager'), $container->get('form_builder'), - $container->get('content_moderation.revision_tracker'), $container->get('entity_type.bundle.info') ); } @@ -132,7 +121,6 @@ public function entityPresave(EntityInterface $entity) { public function entityInsert(EntityInterface $entity) { if ($this->moderationInfo->isModeratedEntity($entity)) { $this->updateOrCreateFromEntity($entity); - $this->setLatestRevision($entity); } } @@ -145,7 +133,6 @@ public function entityInsert(EntityInterface $entity) { public function entityUpdate(EntityInterface $entity) { if ($this->moderationInfo->isModeratedEntity($entity)) { $this->updateOrCreateFromEntity($entity); - $this->setLatestRevision($entity); } } @@ -203,22 +190,6 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { } /** - * Set the latest revision. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The content entity to create content_moderation_state entity for. - */ - protected function setLatestRevision(EntityInterface $entity) { - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ - $this->tracker->setLatestRevision( - $entity->getEntityTypeId(), - $entity->id(), - $entity->language()->getId(), - $entity->getRevisionId() - ); - } - - /** * @param \Drupal\Core\Entity\EntityInterface $entity * The entity being deleted. * diff --git a/core/modules/content_moderation/src/Permissions.php b/core/modules/content_moderation/src/Permissions.php index 22cebf586d..a6d6ce505d 100644 --- a/core/modules/content_moderation/src/Permissions.php +++ b/core/modules/content_moderation/src/Permissions.php @@ -26,9 +26,9 @@ public function transitionPermissions() { foreach (Workflow::loadMultipleByType('content_moderation') as $id => $workflow) { foreach ($workflow->getTypePlugin()->getTransitions() as $transition) { $permissions['use ' . $workflow->id() . ' transition ' . $transition->id()] = [ - 'title' => $this->t('Use %transition transition from %workflow workflow.', [ - '%transition' => $transition->label(), + 'title' => $this->t('%workflow workflow: Use %transition transition.', [ '%workflow' => $workflow->label(), + '%transition' => $transition->label(), ]), ]; } diff --git a/core/modules/content_moderation/src/RevisionTracker.php b/core/modules/content_moderation/src/RevisionTracker.php deleted file mode 100644 index 3f82fdd362..0000000000 --- a/core/modules/content_moderation/src/RevisionTracker.php +++ /dev/null @@ -1,154 +0,0 @@ -connection = $connection; - $this->tableName = $table; - } - - /** - * {@inheritdoc} - */ - public function setLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) { - try { - $this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id); - } - catch (DatabaseExceptionWrapper $e) { - $this->ensureTableExists(); - $this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id); - } - - return $this; - } - - /** - * Records the latest revision of a given entity. - * - * @param string $entity_type_id - * The machine name of the type of entity. - * @param string $entity_id - * The Entity ID in question. - * @param string $langcode - * The langcode of the revision we're saving. Each language has its own - * effective tree of entity revisions, so in different languages - * different revisions will be "latest". - * @param int $revision_id - * The revision ID that is now the latest revision. - * - * @return int - * One of the valid returns from a merge query's execute method. - */ - protected function recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) { - return $this->connection->merge($this->tableName) - ->keys([ - 'entity_type' => $entity_type_id, - 'entity_id' => $entity_id, - 'langcode' => $langcode, - ]) - ->fields([ - 'revision_id' => $revision_id, - ]) - ->execute(); - } - - /** - * Checks if the table exists and create it if not. - * - * @return bool - * TRUE if the table was created, FALSE otherwise. - */ - protected function ensureTableExists() { - try { - if (!$this->connection->schema()->tableExists($this->tableName)) { - $this->connection->schema()->createTable($this->tableName, $this->schemaDefinition()); - return TRUE; - } - } - catch (SchemaObjectExistsException $e) { - // If another process has already created the table, attempting to - // recreate it will throw an exception. In this case just catch the - // exception and do nothing. - return TRUE; - } - return FALSE; - } - - /** - * Defines the schema for the tracker table. - * - * @return array - * The schema API definition for the SQL storage table. - */ - protected function schemaDefinition() { - $schema = [ - 'description' => 'Tracks the latest revision for any entity', - 'fields' => [ - 'entity_type' => [ - 'description' => 'The entity type', - 'type' => 'varchar_ascii', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'entity_id' => [ - 'description' => 'The entity ID', - 'type' => 'int', - 'length' => 255, - 'not null' => TRUE, - 'default' => 0, - ], - 'langcode' => [ - 'description' => 'The language of the entity revision', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ], - 'revision_id' => [ - 'description' => 'The latest revision ID for this entity', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ], - ], - 'primary key' => ['entity_type', 'entity_id', 'langcode'], - ]; - - return $schema; - } - -} diff --git a/core/modules/content_moderation/src/RevisionTrackerInterface.php b/core/modules/content_moderation/src/RevisionTrackerInterface.php deleted file mode 100644 index 5079151ae8..0000000000 --- a/core/modules/content_moderation/src/RevisionTrackerInterface.php +++ /dev/null @@ -1,30 +0,0 @@ -t('Content moderation (tracker)'); - - $data['content_revision_tracker']['entity_type'] = [ - 'title' => $this->t('Entity type'), - 'field' => [ - 'id' => 'standard', - ], - 'filter' => [ - 'id' => 'string', - ], - 'argument' => [ - 'id' => 'string', - ], - 'sort' => [ - 'id' => 'standard', - ], - ]; - - $data['content_revision_tracker']['entity_id'] = [ - 'title' => $this->t('Entity ID'), - 'field' => [ - 'id' => 'standard', - ], - 'filter' => [ - 'id' => 'numeric', - ], - 'argument' => [ - 'id' => 'numeric', - ], - 'sort' => [ - 'id' => 'standard', - ], - ]; - - $data['content_revision_tracker']['langcode'] = [ - 'title' => $this->t('Entity language'), - 'field' => [ - 'id' => 'standard', - ], - 'filter' => [ - 'id' => 'language', - ], - 'argument' => [ - 'id' => 'language', - ], - 'sort' => [ - 'id' => 'standard', - ], - ]; - - $data['content_revision_tracker']['revision_id'] = [ - 'title' => $this->t('Latest revision ID'), - 'field' => [ - 'id' => 'standard', - ], - 'filter' => [ - 'id' => 'numeric', - ], - 'argument' => [ - 'id' => 'numeric', - ], - 'sort' => [ - 'id' => 'standard', - ], - ]; - $entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) { return $this->moderationInformation->canModerateEntitiesOfEntityType($type); }); - // Add a join for each entity type to the content_revision_tracker table. - foreach ($entity_types_with_moderation as $entity_type_id => $entity_type) { - /** @var \Drupal\views\EntityViewsDataInterface $views_data */ - // We need the views_data handler in order to get the table name later. - if ($this->entityTypeManager->hasHandler($entity_type_id, 'views_data') && $views_data = $this->entityTypeManager->getHandler($entity_type_id, 'views_data')) { - // Add a join from the entity base table to the revision tracker table. - $base_table = $views_data->getViewsTableForEntityType($entity_type); - $data['content_revision_tracker']['table']['join'][$base_table] = [ - 'left_field' => $entity_type->getKey('id'), - 'field' => 'entity_id', - 'extra' => [ - [ - 'field' => 'entity_type', - 'value' => $entity_type_id, - ], - ], - ]; - - // Some entity types might not be translatable. - if ($entity_type->hasKey('langcode')) { - $data['content_revision_tracker']['table']['join'][$base_table]['extra'][] = [ - 'field' => 'langcode', - 'left_field' => $entity_type->getKey('langcode'), - 'operation' => '=', - ]; - } - - // Add a relationship between the revision tracker table to the latest - // revision on the entity revision table. - $data['content_revision_tracker']['latest_revision__' . $entity_type_id] = [ - 'title' => $this->t('@label latest revision', ['@label' => $entity_type->getLabel()]), - 'group' => $this->t('@label revision', ['@label' => $entity_type->getLabel()]), - 'relationship' => [ - 'id' => 'standard', - 'label' => $this->t('@label latest revision', ['@label' => $entity_type->getLabel()]), - 'base' => $this->getRevisionViewsTableForEntityType($entity_type), - 'base field' => $entity_type->getKey('revision'), - 'relationship field' => 'revision_id', - 'extra' => [ - [ - 'left_field' => 'entity_type', - 'value' => $entity_type_id, - ], - ], - ], - ]; - - // Some entity types might not be translatable. - if ($entity_type->hasKey('langcode')) { - $data['content_revision_tracker']['latest_revision__' . $entity_type_id]['relationship']['extra'][] = [ - 'left_field' => 'langcode', - 'field' => $entity_type->getKey('langcode'), - 'operation' => '=', - ]; - } - } - } - // Provides a relationship from moderated entity to its moderation state // entity. - $content_moderation_state_entity_type = \Drupal::entityTypeManager()->getDefinition('content_moderation_state'); + $content_moderation_state_entity_type = $this->entityTypeManager->getDefinition('content_moderation_state'); $content_moderation_state_entity_base_table = $content_moderation_state_entity_type->getDataTable() ?: $content_moderation_state_entity_type->getBaseTable(); $content_moderation_state_entity_revision_base_table = $content_moderation_state_entity_type->getRevisionDataTable() ?: $content_moderation_state_entity_type->getRevisionTable(); foreach ($entity_types_with_moderation as $entity_type_id => $entity_type) { @@ -228,39 +104,4 @@ public function getViewsData() { return $data; } - /** - * Alters the table and field information from hook_views_data(). - * - * @param array $data - * An array of all information about Views tables and fields, collected from - * hook_views_data(), passed by reference. - * - * @see hook_views_data() - */ - public function alterViewsData(array &$data) { - $entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) { - return $this->moderationInformation->canModerateEntitiesOfEntityType($type); - }); - foreach ($entity_types_with_moderation as $type) { - $data[$type->getRevisionTable()]['latest_revision'] = [ - 'title' => t('Is Latest Revision'), - 'help' => t('Restrict the view to only revisions that are the latest revision of their entity.'), - 'filter' => ['id' => 'latest_revision'], - ]; - } - } - - /** - * Gets the table of an entity type to be used as revision table in views. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return string - * The revision base table. - */ - protected function getRevisionViewsTableForEntityType(EntityTypeInterface $entity_type) { - return $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); - } - } diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml deleted file mode 100644 index 4727efa28d..0000000000 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml +++ /dev/null @@ -1,447 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - node - - user -id: test_content_moderation_latest_revision -label: test_content_moderation_latest_revision -module: views -description: '' -tag: '' -base_table: node_field_data -base_field: nid -core: 8.x -display: - default: - display_plugin: default - id: default - display_title: Master - position: 0 - display_options: - access: - type: perm - options: - perm: 'access content' - cache: - type: tag - options: { } - query: - type: views_query - options: - disable_sql_rewrite: false - distinct: false - replica: false - query_comment: '' - query_tags: { } - exposed_form: - type: basic - options: - submit_button: Apply - reset_button: false - reset_button_label: Reset - exposed_sorts_label: 'Sort by' - expose_sort_order: true - sort_asc_label: Asc - sort_desc_label: Desc - pager: - type: mini - options: - items_per_page: 10 - offset: 0 - id: 0 - total_pages: null - expose: - items_per_page: false - items_per_page_label: 'Items per page' - items_per_page_options: '5, 10, 25, 50' - items_per_page_options_all: false - items_per_page_options_all_label: '- All -' - offset: false - offset_label: Offset - tags: - previous: ‹‹ - next: ›› - style: - type: 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: - nid: - id: nid - table: node_field_data - field: nid - relationship: none - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: value - type: number_integer - settings: - thousand_separator: '' - prefix_suffix: true - group_column: value - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - entity_type: node - entity_field: nid - plugin_id: field - revision_id: - id: revision_id - table: content_revision_tracker - field: revision_id - relationship: none - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - plugin_id: standard - title: - id: title - table: node_field_revision - field: title - relationship: latest_revision__node - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: value - type: string - settings: - link_to_entity: false - group_column: value - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - entity_type: node - entity_field: title - plugin_id: field - moderation_state: - id: moderation_state - table: content_moderation_state_field_revision - field: moderation_state - relationship: moderation_state - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: target_id - type: string - settings: { } - group_column: target_id - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - entity_type: content_moderation_state - entity_field: moderation_state - plugin_id: field - moderation_state_1: - id: moderation_state_1 - table: content_moderation_state_field_revision - field: moderation_state - relationship: moderation_state_1 - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: target_id - type: string - settings: { } - group_column: target_id - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - entity_type: content_moderation_state - entity_field: moderation_state - plugin_id: field - filters: { } - sorts: - nid: - id: nid - table: node_field_data - field: nid - relationship: none - group_type: group - admin_label: '' - order: ASC - exposed: false - expose: - label: '' - entity_type: node - entity_field: nid - plugin_id: standard - header: { } - footer: { } - empty: { } - relationships: - latest_revision__node: - id: latest_revision__node - table: content_revision_tracker - field: latest_revision__node - relationship: none - group_type: group - admin_label: 'Content latest revision' - required: false - plugin_id: standard - moderation_state_1: - id: moderation_state_1 - table: node_field_revision - field: moderation_state - relationship: latest_revision__node - group_type: group - admin_label: 'Content moderation state (latest revision)' - required: false - entity_type: node - plugin_id: standard - moderation_state: - id: moderation_state - table: node_field_revision - field: moderation_state - relationship: none - group_type: group - admin_label: 'Content moderation state' - required: false - entity_type: node - plugin_id: standard - arguments: { } - display_extenders: { } - rendering_language: '***LANGUAGE_entity_default***' - cache_metadata: - max-age: -1 - contexts: - - 'languages:language_interface' - - url.query_args - - 'user.node_grants:view' - - user.permissions - tags: { } diff --git a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php deleted file mode 100644 index cf14b23297..0000000000 --- a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php +++ /dev/null @@ -1,130 +0,0 @@ -createNodeType('Test', 'test'); - - $permissions = [ - 'access content', - 'view all revisions', - ]; - $editor1 = $this->drupalCreateUser($permissions); - - $this->drupalLogin($editor1); - - // Make a pre-moderation node. - /** @var Node $node_0 */ - $node_0 = Node::create([ - 'type' => 'test', - 'title' => 'Node 0 - Rev 1', - 'uid' => $editor1->id(), - ]); - $node_0->save(); - - // Now enable moderation for subsequent nodes. - $workflow = Workflow::load('editorial'); - $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test'); - $workflow->save(); - - // Make a node that is only ever in Draft. - /** @var Node $node_1 */ - $node_1 = Node::create([ - 'type' => 'test', - 'title' => 'Node 1 - Rev 1', - 'uid' => $editor1->id(), - ]); - $node_1->moderation_state->value = 'draft'; - $node_1->save(); - - // Make a node that is in Draft, then Published. - /** @var Node $node_2 */ - $node_2 = Node::create([ - 'type' => 'test', - 'title' => 'Node 2 - Rev 1', - 'uid' => $editor1->id(), - ]); - $node_2->moderation_state->value = 'draft'; - $node_2->save(); - - $node_2->setTitle('Node 2 - Rev 2'); - $node_2->moderation_state->value = 'published'; - $node_2->save(); - - // Make a node that is in Draft, then Published, then Draft. - /** @var Node $node_3 */ - $node_3 = Node::create([ - 'type' => 'test', - 'title' => 'Node 3 - Rev 1', - 'uid' => $editor1->id(), - ]); - $node_3->moderation_state->value = 'draft'; - $node_3->save(); - - $node_3->setTitle('Node 3 - Rev 2'); - $node_3->moderation_state->value = 'published'; - $node_3->save(); - - $node_3->setTitle('Node 3 - Rev 3'); - $node_3->moderation_state->value = 'draft'; - $node_3->save(); - - // Now show the View, and confirm that only the correct titles are showing. - $this->drupalGet('/latest'); - $page = $this->getSession()->getPage(); - $this->assertEquals(200, $this->getSession()->getStatusCode()); - $this->assertTrue($page->hasContent('Node 1 - Rev 1')); - $this->assertTrue($page->hasContent('Node 2 - Rev 2')); - $this->assertTrue($page->hasContent('Node 3 - Rev 3')); - $this->assertFalse($page->hasContent('Node 2 - Rev 1')); - $this->assertFalse($page->hasContent('Node 3 - Rev 1')); - $this->assertFalse($page->hasContent('Node 3 - Rev 2')); - $this->assertFalse($page->hasContent('Node 0 - Rev 1')); - } - - /** - * Creates a new node type. - * - * @param string $label - * The human-readable label of the type to create. - * @param string $machine_name - * The machine name of the type to create. - * - * @return NodeType - * The node type just created. - */ - protected function createNodeType($label, $machine_name) { - /** @var NodeType $node_type */ - $node_type = NodeType::create([ - 'type' => $machine_name, - 'label' => $label, - ]); - $node_type->save(); - - return $node_type; - } - -} diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php index 5acf93c7a8..f17e4ac83c 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php @@ -58,10 +58,10 @@ public function permissionsTestCases() { ], [ 'use simple_workflow transition publish' => [ - 'title' => 'Use Publish transition from Simple Workflow workflow.', + 'title' => 'Simple Workflow workflow: Use Publish transition.', ], 'use simple_workflow transition create_new_draft' => [ - 'title' => 'Use Create New Draft transition from Simple Workflow workflow.', + 'title' => 'Simple Workflow workflow: Use Create New Draft transition.', ], ], ], diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php index d4c4aa37cd..125d68fbd2 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php @@ -2,7 +2,6 @@ namespace Drupal\Tests\content_moderation\Kernel; -use Drupal\entity_test\Entity\EntityTestMulRevPub; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\views\Kernel\ViewsKernelTestBase; @@ -52,54 +51,6 @@ protected function setUp($import_test_views = TRUE) { } /** - * Tests content_moderation_views_data(). - * - * @see content_moderation_views_data() - */ - public function testViewsData() { - $node = Node::create([ - 'type' => 'page', - 'title' => 'Test title first revision', - ]); - $node->moderation_state->value = 'published'; - $node->save(); - - // Create a totally unrelated entity to ensure the extra join information - // joins by the correct entity type. - $unrelated_entity = EntityTestMulRevPub::create([ - 'id' => $node->id(), - ]); - $unrelated_entity->save(); - - $this->assertEquals($unrelated_entity->id(), $node->id()); - - $revision = clone $node; - $revision->setNewRevision(TRUE); - $revision->isDefaultRevision(FALSE); - $revision->title->value = 'Test title second revision'; - $revision->moderation_state->value = 'draft'; - $revision->save(); - - $view = Views::getView('test_content_moderation_latest_revision'); - $view->execute(); - - // Ensure that the content_revision_tracker contains the right latest - // revision ID. - // Also ensure that the relationship back to the revision table contains the - // right latest revision. - $expected_result = [ - [ - 'nid' => $node->id(), - 'revision_id' => $revision->getRevisionId(), - 'title' => $revision->label(), - 'moderation_state_1' => 'draft', - 'moderation_state' => 'published', - ], - ]; - $this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'content_revision_tracker_revision_id' => 'revision_id', 'moderation_state' => 'moderation_state', 'moderation_state_1' => 'moderation_state_1']); - } - - /** * Tests the join from the revision data table to the moderation state table. */ public function testContentModerationStateRevisionJoin() { diff --git a/core/modules/filter/tests/src/Functional/FilterAdminTest.php b/core/modules/filter/tests/src/Functional/FilterAdminTest.php index 21431e7969..a6e429304e 100644 --- a/core/modules/filter/tests/src/Functional/FilterAdminTest.php +++ b/core/modules/filter/tests/src/Functional/FilterAdminTest.php @@ -4,6 +4,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Url; use Drupal\filter\Entity\FilterFormat; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; @@ -135,16 +136,9 @@ public function testFormatAdmin() { // Edit text format. $this->drupalGet('admin/config/content/formats'); - // Cannot use the assertNoLinkByHref method as it does partial url matching - // and 'admin/config/content/formats/manage/' . $format_id . '/disable' - // exists. - // @todo: See https://www.drupal.org/node/2031223 for the above. - $edit_link = $this->xpath('//a[@href=:href]', [ - ':href' => \Drupal::url('entity.filter_format.edit_form', ['filter_format' => $format_id]) - ]); - $this->assertNotEmpty($edit_link, format_string('Link href %href found.', - ['%href' => 'admin/config/content/formats/manage/' . $format_id] - )); + $destination = Url::fromRoute('filter.admin_overview')->toString(); + $edit_href = Url::fromRoute('entity.filter_format.edit_form', ['filter_format' => $format_id], ['query' => ['destination' => $destination]])->toString(); + $this->assertSession()->linkByHrefExists($edit_href); $this->drupalGet('admin/config/content/formats/manage/' . $format_id); $this->drupalPostForm(NULL, [], t('Save configuration')); diff --git a/core/modules/forum/src/Form/Overview.php b/core/modules/forum/src/Form/Overview.php index df6f94725a..801888aabf 100644 --- a/core/modules/forum/src/Form/Overview.php +++ b/core/modules/forum/src/Form/Overview.php @@ -58,21 +58,30 @@ public function buildForm(array $form, FormStateInterface $form_state) { foreach (Element::children($form['terms']) as $key) { if (isset($form['terms'][$key]['#term'])) { + /** @var \Drupal\taxonomy\TermInterface $term */ $term = $form['terms'][$key]['#term']; $form['terms'][$key]['term']['#url'] = Url::fromRoute('forum.page', ['taxonomy_term' => $term->id()]); - unset($form['terms'][$key]['operations']['#links']['delete']); - $route_parameters = $form['terms'][$key]['operations']['#links']['edit']['url']->getRouteParameters(); + if (!empty($term->forum_container->value)) { - $form['terms'][$key]['operations']['#links']['edit']['title'] = $this->t('edit container'); - $form['terms'][$key]['operations']['#links']['edit']['url'] = Url::fromRoute('entity.taxonomy_term.forum_edit_container_form', $route_parameters); + $title = $this->t('edit container'); + $url = Url::fromRoute('entity.taxonomy_term.forum_edit_container_form', ['taxonomy_term' => $term->id()]); } else { - $form['terms'][$key]['operations']['#links']['edit']['title'] = $this->t('edit forum'); - $form['terms'][$key]['operations']['#links']['edit']['url'] = Url::fromRoute('entity.taxonomy_term.forum_edit_form', $route_parameters); + $title = $this->t('edit forum'); + $url = Url::fromRoute('entity.taxonomy_term.forum_edit_form', ['taxonomy_term' => $term->id()]); } - // We don't want the redirect from the link so we can redirect the - // delete action. - unset($form['terms'][$key]['operations']['#links']['edit']['query']['destination']); + + // Re-create the operations column and add only the edit link. + $form['terms'][$key]['operations'] = [ + '#type' => 'operations', + '#links' => [ + 'edit' => [ + 'title' => $title, + 'url' => $url, + ], + ], + ]; + } } diff --git a/core/modules/forum/tests/src/Functional/ForumIndexTest.php b/core/modules/forum/tests/src/Functional/ForumIndexTest.php index 38adb72ea8..e3d904d368 100644 --- a/core/modules/forum/tests/src/Functional/ForumIndexTest.php +++ b/core/modules/forum/tests/src/Functional/ForumIndexTest.php @@ -57,6 +57,8 @@ public function testForumIndexStatus() { 'parent[0]' => $tid, ]; $this->drupalPostForm('admin/structure/forum/add/forum', $edit, t('Save')); + $this->assertSession()->linkExists(t('edit forum')); + $tid_child = $tid + 1; // Verify that the node appears on the index. diff --git a/core/modules/node/src/NodeListBuilder.php b/core/modules/node/src/NodeListBuilder.php index 4dbdf27bf2..eac48fda31 100644 --- a/core/modules/node/src/NodeListBuilder.php +++ b/core/modules/node/src/NodeListBuilder.php @@ -26,13 +26,6 @@ class NodeListBuilder extends EntityListBuilder { protected $dateFormatter; /** - * The redirect destination service. - * - * @var \Drupal\Core\Routing\RedirectDestinationInterface - */ - protected $redirectDestination; - - /** * Constructs a new NodeListBuilder object. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type @@ -128,17 +121,4 @@ public function buildRow(EntityInterface $entity) { return $row + parent::buildRow($entity); } - /** - * {@inheritdoc} - */ - protected function getDefaultOperations(EntityInterface $entity) { - $operations = parent::getDefaultOperations($entity); - - $destination = $this->redirectDestination->getAsArray(); - foreach ($operations as $key => $operation) { - $operations[$key]['query'] = $destination; - } - return $operations; - } - } diff --git a/core/modules/node/tests/src/Functional/NodeActionsConfigurationTest.php b/core/modules/node/tests/src/Functional/NodeActionsConfigurationTest.php index fbfda8f169..c2c7617bf6 100644 --- a/core/modules/node/tests/src/Functional/NodeActionsConfigurationTest.php +++ b/core/modules/node/tests/src/Functional/NodeActionsConfigurationTest.php @@ -43,14 +43,14 @@ public function testAssignOwnerNodeActionConfiguration() { $this->drupalPostForm('admin/config/system/actions/add/' . Crypt::hashBase64('node_assign_owner_action'), $edit, t('Save')); $this->assertResponse(200); + $action_id = $edit['id']; + // Make sure that the new action was saved properly. $this->assertText(t('The action has been successfully saved.'), 'The node_assign_owner_action action has been successfully saved.'); $this->assertText($action_label, 'The label of the node_assign_owner_action action appears on the actions administration page after saving.'); // Make another POST request to the action edit page. $this->clickLink(t('Configure')); - preg_match('|admin/config/system/actions/configure/(.+)|', $this->getUrl(), $matches); - $aid = $matches[1]; $edit = []; $new_action_label = $this->randomMachineName(); $edit['label'] = $new_action_label; @@ -68,7 +68,7 @@ public function testAssignOwnerNodeActionConfiguration() { $this->clickLink(t('Delete')); $this->assertResponse(200); $edit = []; - $this->drupalPostForm("admin/config/system/actions/configure/$aid/delete", $edit, t('Delete')); + $this->drupalPostForm(NULL, $edit, t('Delete')); $this->assertResponse(200); // Make sure that the action was actually deleted. @@ -77,7 +77,7 @@ public function testAssignOwnerNodeActionConfiguration() { $this->assertResponse(200); $this->assertNoText($new_action_label, 'The label for the node_assign_owner_action action does not appear on the actions administration page after deleting.'); - $action = Action::load($aid); + $action = Action::load($action_id); $this->assertFalse($action, 'The node_assign_owner_action action is not available after being deleted.'); } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php index 44a4e83033..97599774b3 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -41,16 +41,23 @@ protected function setUpAuthorization($method) { case 'GET': $this->grantPermissionsToTestedRole(['access content']); break; + case 'POST': + $this->grantPermissionsToTestedRole(['create terms in camelids']); + break; + case 'PATCH': - case 'DELETE': // Grant the 'create url aliases' permission to test the case when // the path field is accessible, see // \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase // for a negative test. - // @todo Update once https://www.drupal.org/node/2824408 lands. - $this->grantPermissionsToTestedRole(['administer taxonomy', 'create url aliases']); + $this->grantPermissionsToTestedRole(['edit terms in camelids', 'create url aliases']); break; + + case 'DELETE': + $this->grantPermissionsToTestedRole(['delete terms in camelids']); + break; + } } @@ -168,7 +175,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) { case 'GET': return "The 'access content' permission is required."; case 'POST': - return "The 'administer taxonomy' permission is required."; + return "The following permissions are required: 'create terms in camelids' OR 'administer taxonomy'."; case 'PATCH': return "The following permissions are required: 'edit terms in camelids' OR 'administer taxonomy'."; case 'DELETE': diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 3c8332e367..33fa077e2c 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -10,6 +10,7 @@ use Drupal\Component\FileSystem\FileSystem; use Drupal\Component\Utility\OpCodeCache; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Cache\Cache; use Drupal\Core\Path\AliasStorage; use Drupal\Core\Url; use Drupal\Core\Database\Database; @@ -2025,3 +2026,19 @@ function system_update_8402() { } } } + +/** + * Delete all cache_* tables. They are recreated on demand with the new schema. + */ +function system_update_8403() { + foreach (Cache::getBins() as $bin => $cache_backend) { + // Try to delete the table regardless of which cache backend is handling it. + // This is to ensure the new schema is used if the configuration for the + // backend class is changed after the update hook runs. + $table_name = "cache_$bin"; + $schema = Database::getConnection()->schema(); + if ($schema->tableExists($table_name)) { + $schema->dropTable($table_name); + } + } +} diff --git a/core/modules/taxonomy/src/Entity/Vocabulary.php b/core/modules/taxonomy/src/Entity/Vocabulary.php index b0d1ac13d2..d61294b4e3 100644 --- a/core/modules/taxonomy/src/Entity/Vocabulary.php +++ b/core/modules/taxonomy/src/Entity/Vocabulary.php @@ -15,6 +15,7 @@ * handlers = { * "storage" = "Drupal\taxonomy\VocabularyStorage", * "list_builder" = "Drupal\taxonomy\VocabularyListBuilder", + * "access" = "Drupal\taxonomy\VocabularyAccessControlHandler", * "form" = { * "default" = "Drupal\taxonomy\VocabularyForm", * "reset" = "Drupal\taxonomy\Form\VocabularyResetForm", diff --git a/core/modules/taxonomy/src/Form/OverviewTerms.php b/core/modules/taxonomy/src/Form/OverviewTerms.php index 3811ee5fd8..d73598d672 100644 --- a/core/modules/taxonomy/src/Form/OverviewTerms.php +++ b/core/modules/taxonomy/src/Form/OverviewTerms.php @@ -2,10 +2,13 @@ namespace Drupal\taxonomy\Form; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Url; use Drupal\taxonomy\VocabularyInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -36,17 +39,35 @@ class OverviewTerms extends FormBase { protected $storageController; /** + * The term list builder. + * + * @var \Drupal\Core\Entity\EntityListBuilderInterface + */ + protected $termListBuilder; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** * Constructs an OverviewTerms object. * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler service. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. */ - public function __construct(ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager) { + public function __construct(ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager, RendererInterface $renderer = NULL) { $this->moduleHandler = $module_handler; $this->entityManager = $entity_manager; $this->storageController = $entity_manager->getStorage('taxonomy_term'); + $this->termListBuilder = $entity_manager->getListBuilder('taxonomy_term'); + $this->renderer = $renderer ?: \Drupal::service('renderer'); } /** @@ -55,7 +76,8 @@ public function __construct(ModuleHandlerInterface $module_handler, EntityManage public static function create(ContainerInterface $container) { return new static( $container->get('module_handler'), - $container->get('entity.manager') + $container->get('entity.manager'), + $container->get('renderer') ); } @@ -204,17 +226,28 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular } $errors = $form_state->getErrors(); - $destination = $this->getDestinationArray(); $row_position = 0; // Build the actual form. + $access_control_handler = $this->entityManager->getAccessControlHandler('taxonomy_term'); + $create_access = $access_control_handler->createAccess($taxonomy_vocabulary->id(), NULL, [], TRUE); + if ($create_access->isAllowed()) { + $empty = $this->t('No terms available. Add term.', [':link' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $taxonomy_vocabulary->id()])->toString()]); + } + else { + $empty = $this->t('No terms available.'); + } $form['terms'] = [ '#type' => 'table', - '#header' => [$this->t('Name'), $this->t('Weight'), $this->t('Operations')], - '#empty' => $this->t('No terms available. Add term.', [':link' => $this->url('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $taxonomy_vocabulary->id()])]), + '#empty' => $empty, '#attributes' => [ 'id' => 'taxonomy', ], ]; + $this->renderer->addCacheableDependency($form['terms'], $create_access); + + // Only allow access to changing weights if the user has update access for + // all terms. + $change_weight_access = AccessResult::allowed(); foreach ($current_page as $key => $term) { /** @var $term \Drupal\Core\Entity\EntityInterface */ $term = $this->entityManager->getTranslationFromContext($term); @@ -260,39 +293,26 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular ], ]; } - $form['terms'][$key]['weight'] = [ - '#type' => 'weight', - '#delta' => $delta, - '#title' => $this->t('Weight for added term'), - '#title_display' => 'invisible', - '#default_value' => $term->getWeight(), - '#attributes' => [ - 'class' => ['term-weight'], - ], - ]; - $operations = [ - 'edit' => [ - 'title' => $this->t('Edit'), - 'query' => $destination, - 'url' => $term->urlInfo('edit-form'), - ], - 'delete' => [ - 'title' => $this->t('Delete'), - 'query' => $destination, - 'url' => $term->urlInfo('delete-form'), - ], - ]; - if ($this->moduleHandler->moduleExists('content_translation') && content_translation_translate_access($term)->isAllowed()) { - $operations['translate'] = [ - 'title' => $this->t('Translate'), - 'query' => $destination, - 'url' => $term->urlInfo('drupal:content-translation-overview'), + $update_access = $term->access('update', NULL, TRUE); + $change_weight_access = $change_weight_access->andIf($update_access); + + if ($update_access->isAllowed()) { + $form['terms'][$key]['weight'] = [ + '#type' => 'weight', + '#delta' => $delta, + '#title' => $this->t('Weight for added term'), + '#title_display' => 'invisible', + '#default_value' => $term->getWeight(), + '#attributes' => ['class' => ['term-weight']], + ]; + } + + if ($operations = $this->termListBuilder->getOperations($term)) { + $form['terms'][$key]['operations'] = [ + '#type' => 'operations', + '#links' => $operations, ]; } - $form['terms'][$key]['operations'] = [ - '#type' => 'operations', - '#links' => $operations, - ]; $form['terms'][$key]['#attributes']['class'] = []; if ($parent_fields) { @@ -322,34 +342,42 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular $row_position++; } - if ($parent_fields) { + $form['terms']['#header'] = [$this->t('Name')]; + + $this->renderer->addCacheableDependency($form['terms'], $change_weight_access); + if ($change_weight_access->isAllowed()) { + $form['terms']['#header'][] = $this->t('Weight'); + if ($parent_fields) { + $form['terms']['#tabledrag'][] = [ + 'action' => 'match', + 'relationship' => 'parent', + 'group' => 'term-parent', + 'subgroup' => 'term-parent', + 'source' => 'term-id', + 'hidden' => FALSE, + ]; + $form['terms']['#tabledrag'][] = [ + 'action' => 'depth', + 'relationship' => 'group', + 'group' => 'term-depth', + 'hidden' => FALSE, + ]; + $form['terms']['#attached']['library'][] = 'taxonomy/drupal.taxonomy'; + $form['terms']['#attached']['drupalSettings']['taxonomy'] = [ + 'backStep' => $back_step, + 'forwardStep' => $forward_step, + ]; + } $form['terms']['#tabledrag'][] = [ - 'action' => 'match', - 'relationship' => 'parent', - 'group' => 'term-parent', - 'subgroup' => 'term-parent', - 'source' => 'term-id', - 'hidden' => FALSE, - ]; - $form['terms']['#tabledrag'][] = [ - 'action' => 'depth', - 'relationship' => 'group', - 'group' => 'term-depth', - 'hidden' => FALSE, - ]; - $form['terms']['#attached']['library'][] = 'taxonomy/drupal.taxonomy'; - $form['terms']['#attached']['drupalSettings']['taxonomy'] = [ - 'backStep' => $back_step, - 'forwardStep' => $forward_step, + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'term-weight', ]; } - $form['terms']['#tabledrag'][] = [ - 'action' => 'order', - 'relationship' => 'sibling', - 'group' => 'term-weight', - ]; - if ($taxonomy_vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) { + $form['terms']['#header'][] = $this->t('Operations'); + + if (($taxonomy_vocabulary->getHierarchy() !== VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access->isAllowed()) { $form['actions'] = ['#type' => 'actions', '#tree' => FALSE]; $form['actions']['submit'] = [ '#type' => 'submit', diff --git a/core/modules/taxonomy/src/TaxonomyPermissions.php b/core/modules/taxonomy/src/TaxonomyPermissions.php index 196c5a5258..1772a34f97 100644 --- a/core/modules/taxonomy/src/TaxonomyPermissions.php +++ b/core/modules/taxonomy/src/TaxonomyPermissions.php @@ -5,6 +5,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\taxonomy\Entity\Vocabulary; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -48,19 +49,30 @@ public static function create(ContainerInterface $container) { */ public function permissions() { $permissions = []; - foreach ($this->entityManager->getStorage('taxonomy_vocabulary')->loadMultiple() as $vocabulary) { - $permissions += [ - 'edit terms in ' . $vocabulary->id() => [ - 'title' => $this->t('Edit terms in %vocabulary', ['%vocabulary' => $vocabulary->label()]), - ], - ]; - $permissions += [ - 'delete terms in ' . $vocabulary->id() => [ - 'title' => $this->t('Delete terms from %vocabulary', ['%vocabulary' => $vocabulary->label()]), - ], - ]; + foreach (Vocabulary::loadMultiple() as $vocabulary) { + $permissions += $this->buildPermissions($vocabulary); } return $permissions; } + /** + * Builds a standard list of taxonomy term permissions for a given vocabulary. + * + * @param \Drupal\taxonomy\VocabularyInterface $vocabulary + * The vocabulary. + * + * @return array + * An array of permission names and descriptions. + */ + protected function buildPermissions(VocabularyInterface $vocabulary) { + $id = $vocabulary->id(); + $args = ['%vocabulary' => $vocabulary->label()]; + + return [ + "create terms in $id" => ['title' => $this->t('%vocabulary: Create terms', $args)], + "delete terms in $id" => ['title' => $this->t('%vocabulary: Delete terms', $args)], + "edit terms in $id" => ['title' => $this->t('%vocabulary: Edit terms', $args)], + ]; + } + } diff --git a/core/modules/taxonomy/src/TermAccessControlHandler.php b/core/modules/taxonomy/src/TermAccessControlHandler.php index 04c2c4f3fb..1d48463666 100644 --- a/core/modules/taxonomy/src/TermAccessControlHandler.php +++ b/core/modules/taxonomy/src/TermAccessControlHandler.php @@ -38,7 +38,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter * {@inheritdoc} */ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - return AccessResult::allowedIfHasPermission($account, 'administer taxonomy'); + return AccessResult::allowedIfHasPermissions($account, ["create terms in $entity_bundle", 'administer taxonomy'], 'OR'); } } diff --git a/core/modules/taxonomy/src/VocabularyAccessControlHandler.php b/core/modules/taxonomy/src/VocabularyAccessControlHandler.php new file mode 100644 index 0000000000..befc8a5fdf --- /dev/null +++ b/core/modules/taxonomy/src/VocabularyAccessControlHandler.php @@ -0,0 +1,30 @@ +getStorage($entity_type->id())); + + $this->currentUser = $current_user; + $this->entityTypeManager = $entity_type_manager; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('renderer') + ); + } + + /** * {@inheritdoc} */ public function getFormId() { @@ -36,16 +94,23 @@ public function getDefaultOperations(EntityInterface $entity) { $operations['edit']['title'] = t('Edit vocabulary'); } - $operations['list'] = [ - 'title' => t('List terms'), - 'weight' => 0, - 'url' => $entity->urlInfo('overview-form'), - ]; - $operations['add'] = [ - 'title' => t('Add terms'), - 'weight' => 10, - 'url' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $entity->id()]), - ]; + if ($entity->access('access taxonomy overview')) { + $operations['list'] = [ + 'title' => t('List terms'), + 'weight' => 0, + 'url' => $entity->toUrl('overview-form'), + ]; + } + + $taxonomy_term_access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term'); + if ($taxonomy_term_access_control_handler->createAccess($entity->id())) { + $operations['add'] = [ + 'title' => t('Add terms'), + 'weight' => 10, + 'url' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $entity->id()]), + ]; + } + unset($operations['delete']); return $operations; @@ -57,6 +122,11 @@ public function getDefaultOperations(EntityInterface $entity) { public function buildHeader() { $header['label'] = t('Vocabulary name'); $header['description'] = t('Description'); + + if ($this->currentUser->hasPermission('administer vocabularies')) { + $header['weight'] = t('Weight'); + } + return $header + parent::buildHeader(); } @@ -80,7 +150,25 @@ public function render() { unset($this->weightKey); } $build = parent::render(); - $build['table']['#empty'] = t('No vocabularies available. Add vocabulary.', [':link' => \Drupal::url('entity.taxonomy_vocabulary.add_form')]); + + // If the weight key was unset then the table is in the 'table' key, + // otherwise in vocabularies. The empty message is only needed if the table + // is possibly empty, so there is no need to support the vocabularies key + // here. + if (isset($build['table'])) { + $access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_vocabulary'); + $create_access = $access_control_handler->createAccess(NULL, NULL, [], TRUE); + $this->renderer->addCacheableDependency($build['table'], $create_access); + if ($create_access->isAllowed()) { + $build['table']['#empty'] = t('No vocabularies available. Add vocabulary.', [ + ':link' => Url::fromRoute('entity.taxonomy_vocabulary.add_form')->toString() + ]); + } + else { + $build['table']['#empty'] = t('No vocabularies available.'); + } + } + return $build; } diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 4a93989ad5..c165855c28 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -75,13 +75,25 @@ function taxonomy_help($route_name, RouteMatchInterface $route_match) { case 'entity.taxonomy_vocabulary.overview_form': $vocabulary = $route_match->getParameter('taxonomy_vocabulary'); - switch ($vocabulary->getHierarchy()) { - case VocabularyInterface::HIERARCHY_DISABLED: - return '
' . t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '
'; - case VocabularyInterface::HIERARCHY_SINGLE: - return '' . t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '
'; - case VocabularyInterface::HIERARCHY_MULTIPLE: - return '' . t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '
'; + if (\Drupal::currentUser()->hasPermission('administer taxonomy') || \Drupal::currentUser()->hasPermission('edit terms in ' . $vocabulary->id())) { + switch ($vocabulary->getHierarchy()) { + case VocabularyInterface::HIERARCHY_DISABLED: + return '' . t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '
'; + case VocabularyInterface::HIERARCHY_SINGLE: + return '' . t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '
'; + case VocabularyInterface::HIERARCHY_MULTIPLE: + return '' . t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '
'; + } + } + else { + switch ($vocabulary->getHierarchy()) { + case VocabularyInterface::HIERARCHY_DISABLED: + return '' . t('%capital_name contains the following terms.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '
'; + case VocabularyInterface::HIERARCHY_SINGLE: + return '' . t('%capital_name contains terms grouped under parent terms', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '
'; + case VocabularyInterface::HIERARCHY_MULTIPLE: + return '' . t('%capital_name contains terms with multiple parents.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '
'; + } } } } diff --git a/core/modules/taxonomy/taxonomy.permissions.yml b/core/modules/taxonomy/taxonomy.permissions.yml index d4859492af..bb71e93c12 100644 --- a/core/modules/taxonomy/taxonomy.permissions.yml +++ b/core/modules/taxonomy/taxonomy.permissions.yml @@ -1,5 +1,9 @@ administer taxonomy: title: 'Administer vocabularies and terms' +access taxonomy overview: + title: 'Access the taxonomy vocabulary overview page' + description: 'Get an overview of all taxonomy vocabularies.' + permission_callbacks: - Drupal\taxonomy\TaxonomyPermissions::permissions diff --git a/core/modules/taxonomy/taxonomy.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml index 8a3bd1a58d..19989241e4 100644 --- a/core/modules/taxonomy/taxonomy.routing.yml +++ b/core/modules/taxonomy/taxonomy.routing.yml @@ -4,7 +4,7 @@ entity.taxonomy_vocabulary.collection: _entity_list: 'taxonomy_vocabulary' _title: 'Taxonomy' requirements: - _permission: 'administer taxonomy' + _permission: 'access taxonomy overview+administer taxonomy' entity.taxonomy_term.add_form: path: '/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/add' @@ -74,7 +74,7 @@ entity.taxonomy_vocabulary.overview_form: _form: 'Drupal\taxonomy\Form\OverviewTerms' _title_callback: 'Drupal\taxonomy\Controller\TaxonomyController::vocabularyTitle' requirements: - _entity_access: 'taxonomy_vocabulary.view' + _entity_access: 'taxonomy_vocabulary.access taxonomy overview' entity.taxonomy_term.canonical: path: '/taxonomy/term/{taxonomy_term}' diff --git a/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php b/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php index 3ba8868f54..989398e62f 100644 --- a/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php +++ b/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\taxonomy\Functional; +use Drupal\Component\Utility\Unicode; + /** * Tests the taxonomy vocabulary permissions. * @@ -9,10 +11,204 @@ */ class VocabularyPermissionsTest extends TaxonomyTestBase { + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['help']; + protected function setUp() { parent::setUp(); $this->drupalPlaceBlock('page_title_block'); + $this->drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('help_block'); + } + + /** + * Create, edit and delete a vocabulary via the user interface. + */ + public function testVocabularyPermissionsVocabulary() { + // VocabularyTest.php already tests for user with "administer taxonomy" + // permission. + + // Test as user without proper permissions. + $authenticated_user = $this->drupalCreateUser([]); + $this->drupalLogin($authenticated_user); + + $assert_session = $this->assertSession(); + + // Visit the main taxonomy administration page. + $this->drupalGet('admin/structure/taxonomy'); + $assert_session->statusCodeEquals(403); + + // Test as user with "access taxonomy overview" permissions. + $proper_user = $this->drupalCreateUser(['access taxonomy overview']); + $this->drupalLogin($proper_user); + + // Visit the main taxonomy administration page. + $this->drupalGet('admin/structure/taxonomy'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('Vocabulary name'); + $assert_session->linkNotExists('Add vocabulary'); + } + + /** + * Test the vocabulary overview permission. + */ + public function testTaxonomyVocabularyOverviewPermissions() { + // Create two vocabularies, one with two terms, the other without any term. + /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary1 , $vocabulary2 */ + $vocabulary1 = $this->createVocabulary(); + $vocabulary2 = $this->createVocabulary(); + $vocabulary1_id = $vocabulary1->id(); + $vocabulary2_id = $vocabulary2->id(); + $this->createTerm($vocabulary1); + $this->createTerm($vocabulary1); + + // Assert expected help texts on first vocabulary. + $edit_help_text = t('You can reorganize the terms in @capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', ['@capital_name' => Unicode::ucfirst($vocabulary1->label())]); + $no_edit_help_text = t('@capital_name contains the following terms.', ['@capital_name' => Unicode::ucfirst($vocabulary1->label())]); + + $assert_session = $this->assertSession(); + + // Logged in as admin user with 'administer taxonomy' permission. + $admin_user = $this->drupalCreateUser(['administer taxonomy']); + $this->drupalLogin($admin_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkExists('Edit'); + $assert_session->linkExists('Delete'); + $assert_session->linkExists('Add term'); + $assert_session->buttonExists('Save'); + $assert_session->pageTextContains('Weight'); + $assert_session->pageTextContains($edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkExists('Add term'); + + // Login as a user without any of the required permissions. + $no_permission_user = $this->drupalCreateUser(); + $this->drupalLogin($no_permission_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(403); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(403); + + // Log in as a user with only the overview permission, neither edit nor + // delete operations must be available and no Save button. + $overview_only_user = $this->drupalCreateUser(['access taxonomy overview']); + $this->drupalLogin($overview_only_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkNotExists('Edit'); + $assert_session->linkNotExists('Delete'); + $assert_session->buttonNotExists('Save'); + $assert_session->pageTextNotContains('Weight'); + $assert_session->linkNotExists('Add term'); + $assert_session->pageTextContains($no_edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkNotExists('Add term'); + + // Login as a user with permission to edit terms, only edit link should be + // visible. + $edit_user = $this->createUser([ + 'access taxonomy overview', + 'edit terms in ' . $vocabulary1_id, + 'edit terms in ' . $vocabulary2_id, + ]); + $this->drupalLogin($edit_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkExists('Edit'); + $assert_session->linkNotExists('Delete'); + $assert_session->buttonExists('Save'); + $assert_session->pageTextContains('Weight'); + $assert_session->linkNotExists('Add term'); + $assert_session->pageTextContains($edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkNotExists('Add term'); + + // Login as a user with permission only to delete terms. + $edit_delete_user = $this->createUser([ + 'access taxonomy overview', + 'delete terms in ' . $vocabulary1_id, + 'delete terms in ' . $vocabulary2_id, + ]); + $this->drupalLogin($edit_delete_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkNotExists('Edit'); + $assert_session->linkExists('Delete'); + $assert_session->linkNotExists('Add term'); + $assert_session->buttonNotExists('Save'); + $assert_session->pageTextNotContains('Weight'); + $assert_session->pageTextContains($no_edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkNotExists('Add term'); + + // Login as a user with permission to edit and delete terms. + $edit_delete_user = $this->createUser([ + 'access taxonomy overview', + 'edit terms in ' . $vocabulary1_id, + 'delete terms in ' . $vocabulary1_id, + 'edit terms in ' . $vocabulary2_id, + 'delete terms in ' . $vocabulary2_id, + ]); + $this->drupalLogin($edit_delete_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkExists('Edit'); + $assert_session->linkExists('Delete'); + $assert_session->linkNotExists('Add term'); + $assert_session->buttonExists('Save'); + $assert_session->pageTextContains('Weight'); + $assert_session->pageTextContains($edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkNotExists('Add term'); + + // Login as a user with permission to create new terms, only add new term + // link should be visible. + $edit_user = $this->createUser([ + 'access taxonomy overview', + 'create terms in ' . $vocabulary1_id, + 'create terms in ' . $vocabulary2_id, + ]); + $this->drupalLogin($edit_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkNotExists('Edit'); + $assert_session->linkNotExists('Delete'); + $assert_session->linkExists('Add term'); + $assert_session->buttonNotExists('Save'); + $assert_session->pageTextNotContains('Weight'); + $assert_session->pageTextContains($no_edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkExists('Add term'); } /** @@ -42,7 +238,9 @@ public function testVocabularyPermissionsTaxonomyTerm() { $view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'term/']); $this->assert(isset($view_link), 'The message area contains a link to a term'); - $terms = taxonomy_term_load_multiple_by_name($edit['name[0][value]']); + $terms = \Drupal::entityTypeManager() + ->getStorage('taxonomy_term') + ->loadByProperties(['name' => $edit['name[0][value]']]); $term = reset($terms); // Edit the term. @@ -62,6 +260,35 @@ public function testVocabularyPermissionsTaxonomyTerm() { $this->drupalPostForm(NULL, NULL, t('Delete')); $this->assertRaw(t('Deleted term %name.', ['%name' => $edit['name[0][value]']]), 'Term deleted.'); + // Test as user with "create" permissions. + $user = $this->drupalCreateUser(["create terms in {$vocabulary->id()}"]); + $this->drupalLogin($user); + + $assert_session = $this->assertSession(); + + // Create a new term. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary->id() . '/add'); + $assert_session->statusCodeEquals(200); + $assert_session->fieldExists('name[0][value]'); + + // Submit the term. + $edit = []; + $edit['name[0][value]'] = $this->randomMachineName(); + + $this->drupalPostForm(NULL, $edit, t('Save')); + $assert_session->pageTextContains(t('Created new term @name.', ['@name' => $edit['name[0][value]']])); + + $terms = \Drupal::entityTypeManager() + ->getStorage('taxonomy_term') + ->loadByProperties(['name' => $edit['name[0][value]']]); + $term = reset($terms); + + // Ensure that edit and delete access is denied. + $this->drupalGet('taxonomy/term/' . $term->id() . '/edit'); + $assert_session->statusCodeEquals(403); + $this->drupalGet('taxonomy/term/' . $term->id() . '/delete'); + $assert_session->statusCodeEquals(403); + // Test as user with "edit" permissions. $user = $this->drupalCreateUser(["edit terms in {$vocabulary->id()}"]); $this->drupalLogin($user); diff --git a/core/modules/views/config/schema/views.filter.schema.yml b/core/modules/views/config/schema/views.filter.schema.yml index 00eb11a976..18c13b687d 100644 --- a/core/modules/views/config/schema/views.filter.schema.yml +++ b/core/modules/views/config/schema/views.filter.schema.yml @@ -142,6 +142,10 @@ views.filter.language: type: views.filter.in_operator label: 'Language' +views.filter.latest_revision: + type: views_filter + label: 'Latest revision' + views.filter_value.date: type: views.filter_value.numeric label: 'Date' diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php index 2ba2086ac1..49b0af80f8 100644 --- a/core/modules/views/src/EntityViewsData.php +++ b/core/modules/views/src/EntityViewsData.php @@ -236,6 +236,13 @@ public function getViewsData() { 'type' => 'INNER', ]; } + + // Add a filter for showing only the latest revisions of an entity. + $data[$revision_table]['latest_revision'] = [ + 'title' => $this->t('Is Latest Revision'), + 'help' => $this->t('Restrict the view to only revisions that are the latest revision of their entity.'), + 'filter' => ['id' => 'latest_revision'], + ]; } $this->addEntityLinks($data[$base_table]); diff --git a/core/modules/views/src/Plugin/views/field/EntityOperations.php b/core/modules/views/src/Plugin/views/field/EntityOperations.php index c8a307bd1d..4ca28f6d92 100644 --- a/core/modules/views/src/Plugin/views/field/EntityOperations.php +++ b/core/modules/views/src/Plugin/views/field/EntityOperations.php @@ -84,7 +84,7 @@ public function defineOptions() { $options = parent::defineOptions(); $options['destination'] = [ - 'default' => TRUE, + 'default' => FALSE, ]; return $options; @@ -99,7 +99,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $form['destination'] = [ '#type' => 'checkbox', '#title' => $this->t('Include destination'), - '#description' => $this->t('Include adestination
parameter in the link to return the user to the original view upon completing the link action.'),
+ '#description' => $this->t('Enforce a destination
parameter in the link to return the user to the original view upon completing the link action. Most operations include a destination by default and this setting is no longer needed.'),
'#default_value' => $this->options['destination'],
];
}
diff --git a/core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php b/core/modules/views/src/Plugin/views/filter/LatestRevision.php
similarity index 61%
rename from core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php
rename to core/modules/views/src/Plugin/views/filter/LatestRevision.php
index 64400197ec..7930a7fb1c 100644
--- a/core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php
+++ b/core/modules/views/src/Plugin/views/filter/LatestRevision.php
@@ -1,12 +1,10 @@
entityTypeManager = $entity_type_manager;
$this->joinHandler = $join_handler;
- $this->connection = $connection;
}
/**
@@ -70,8 +59,7 @@ public static function create(ContainerInterface $container, array $configuratio
return new static(
$configuration, $plugin_id, $plugin_definition,
$container->get('entity_type.manager'),
- $container->get('plugin.manager.views.join'),
- $container->get('database')
+ $container->get('plugin.manager.views.join')
);
}
@@ -98,39 +86,28 @@ public function canExpose() {
* {@inheritdoc}
*/
public function query() {
- // The table doesn't exist until a moderated node has been saved at least
- // once. Just in case, disable this filter until then. Note that this means
- // the view will still show all revisions, not just latest, but this is
- // sufficiently edge-case-y that it's probably not worth the time to
- // handle more robustly.
- if (!$this->connection->schema()->tableExists('content_revision_tracker')) {
- return;
- }
-
- $table = $this->ensureMyTable();
-
/** @var \Drupal\views\Plugin\views\query\Sql $query */
$query = $this->query;
+ $query_base_table = $this->relationship ?: $this->view->storage->get('base_table');
- $definition = $this->entityTypeManager->getDefinition($this->getEntityType());
- $keys = $definition->getKeys();
+ $entity_type = $this->entityTypeManager->getDefinition($this->getEntityType());
+ $keys = $entity_type->getKeys();
$definition = [
- 'table' => 'content_revision_tracker',
- 'type' => 'INNER',
- 'field' => 'entity_id',
- 'left_table' => $table,
+ 'table' => $query_base_table,
+ 'type' => 'LEFT',
+ 'field' => $keys['id'],
+ 'left_table' => $query_base_table,
'left_field' => $keys['id'],
'extra' => [
- ['left_field' => $keys['langcode'], 'field' => 'langcode'],
- ['left_field' => $keys['revision'], 'field' => 'revision_id'],
- ['field' => 'entity_type', 'value' => $this->getEntityType()],
+ ['left_field' => $keys['revision'], 'field' => $keys['revision'], 'operator' => '>'],
],
];
$join = $this->joinHandler->createInstance('standard', $definition);
- $query->ensureTable('content_revision_tracker', $this->relationship, $join);
+ $join_table_alias = $query->addTable($query_base_table, $this->relationship, $join);
+ $query->addWhere($this->options['group'], "$join_table_alias.{$keys['id']}", NULL, 'IS NULL');
}
}
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_latest_revision_filter.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_latest_revision_filter.yml
new file mode 100644
index 0000000000..53f0f72a66
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_latest_revision_filter.yml
@@ -0,0 +1,163 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - node
+id: test_latest_revision_filter
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: node_field_revision
+base_field: vid
+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: none
+ options:
+ offset: 0
+ 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:
+ title:
+ id: title
+ table: node_field_revision
+ field: title
+ entity_type: node
+ entity_field: title
+ label: ''
+ alter:
+ alter_text: false
+ make_link: false
+ absolute: false
+ trim: false
+ word_boundary: false
+ ellipsis: false
+ strip_tags: false
+ html: false
+ hide_empty: false
+ empty_zero: false
+ settings:
+ link_to_entity: false
+ plugin_id: field
+ relationship: none
+ group_type: group
+ admin_label: ''
+ exclude: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_alter_empty: true
+ click_sort_column: value
+ type: string
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ filters:
+ latest_revision:
+ id: latest_revision
+ table: node_revision
+ field: latest_revision
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value: ''
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: node
+ plugin_id: latest_revision
+ sorts: { }
+ header: { }
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ show_admin_links: false
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - 'user.node_grants:view'
+ tags: { }
diff --git a/core/modules/views/tests/src/Functional/Entity/LatestRevisionFilterTest.php b/core/modules/views/tests/src/Functional/Entity/LatestRevisionFilterTest.php
new file mode 100644
index 0000000000..d73a11793b
--- /dev/null
+++ b/core/modules/views/tests/src/Functional/Entity/LatestRevisionFilterTest.php
@@ -0,0 +1,160 @@
+drupalCreateContentType(['type' => 'article']);
+
+ // Create a node that goes through various default/pending revision stages.
+ $node = Node::create([
+ 'title' => 'First node - v1 - default',
+ 'type' => 'article',
+ ]);
+ $node->save();
+ $this->allRevisions[$node->getRevisionId()] = $node;
+
+ $node->setTitle('First node - v2 - pending');
+ $node->setNewRevision(TRUE);
+ $node->isDefaultRevision(FALSE);
+ $node->save();
+ $this->allRevisions[$node->getRevisionId()] = $node;
+
+ $node->setTitle('First node - v3 - default');
+ $node->setNewRevision(TRUE);
+ $node->isDefaultRevision(TRUE);
+ $node->save();
+ $this->allRevisions[$node->getRevisionId()] = $node;
+
+ $node->setTitle('First node - v4 - pending');
+ $node->setNewRevision(TRUE);
+ $node->isDefaultRevision(TRUE);
+ $node->save();
+ $this->allRevisions[$node->getRevisionId()] = $node;
+ $this->latestRevisions[$node->getRevisionId()] = $node;
+
+ // Create a node that has a default and a pending revision.
+ $node = Node::create([
+ 'title' => 'Second node - v1 - default',
+ 'type' => 'article',
+ ]);
+ $node->save();
+ $this->allRevisions[$node->getRevisionId()] = $node;
+
+ $node->setTitle('Second node - v2 - pending');
+ $node->setNewRevision(TRUE);
+ $node->isDefaultRevision(FALSE);
+ $node->save();
+ $this->allRevisions[$node->getRevisionId()] = $node;
+ $this->latestRevisions[$node->getRevisionId()] = $node;
+
+ // Create a node that only has a default revision.
+ $node = Node::create([
+ 'title' => 'Third node - v1 - default',
+ 'type' => 'article',
+ ]);
+ $node->save();
+ $this->allRevisions[$node->getRevisionId()] = $node;
+ $this->latestRevisions[$node->getRevisionId()] = $node;
+
+ // Create a node that only has a pending revision.
+ $node = Node::create([
+ 'title' => 'Fourth node - v1 - pending',
+ 'type' => 'article',
+ ]);
+ $node->isDefaultRevision(FALSE);
+ $node->save();
+ $this->allRevisions[$node->getRevisionId()] = $node;
+ $this->latestRevisions[$node->getRevisionId()] = $node;
+ }
+
+ /**
+ * Tests the 'Latest revision' filter.
+ */
+ public function testLatestRevisionFilter() {
+ $view = Views::getView('test_latest_revision_filter');
+
+ $this->executeView($view);
+
+ // Check that we have all the results.
+ $this->assertCount(count($this->latestRevisions), $view->result);
+
+ $expected = $not_expected = [];
+ foreach ($this->allRevisions as $revision_id => $revision) {
+ if (isset($this->latestRevisions[$revision_id])) {
+ $expected[] = [
+ 'vid' => $revision_id,
+ 'title' => $revision->label(),
+ ];
+ }
+ else {
+ $not_expected[] = $revision_id;
+ }
+ }
+ $this->assertIdenticalResultset($view, $expected, ['vid' => 'vid', 'title' => 'title'], 'The test view only shows the latest revisions.');
+ $this->assertNotInResultSet($view, $not_expected, 'Non-latest revisions are not shown by the view.');
+ $view->destroy();
+ }
+
+ /**
+ * Verifies that a list of revision IDs are not in the result.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * An executed View.
+ * @param array $not_expected_revision_ids
+ * An array of revision IDs which should not be part of the result set.
+ * @param string $message
+ * (optional) A custom message to display with the assertion.
+ */
+ protected function assertNotInResultSet(ViewExecutable $view, array $not_expected_revision_ids, $message = '') {
+ $found_revision_ids = array_filter($view->result, function ($row) use ($not_expected_revision_ids) {
+ return in_array($row->vid, $not_expected_revision_ids);
+ });
+ $this->assertFalse($found_revision_ids, $message);
+ }
+
+}
diff --git a/core/modules/views/tests/src/Functional/Handler/FieldEntityOperationsTest.php b/core/modules/views/tests/src/Functional/Handler/FieldEntityOperationsTest.php
index 653ce11527..7ced4f7071 100644
--- a/core/modules/views/tests/src/Functional/Handler/FieldEntityOperationsTest.php
+++ b/core/modules/views/tests/src/Functional/Handler/FieldEntityOperationsTest.php
@@ -73,7 +73,10 @@ public function testEntityOperations() {
$this->assertTrue(count($operations) > 0, 'There are operations.');
foreach ($operations as $operation) {
$expected_destination = Url::fromUri('internal:/test-entity-operations')->toString();
- $result = $this->xpath('//ul[contains(@class, dropbutton)]/li/a[@href=:path and text()=:title]', [':path' => $operation['url']->toString() . '?destination=' . $expected_destination, ':title' => (string) $operation['title']]);
+ // Update destination property of the URL as generating it in the
+ // test would by default point to the frontpage.
+ $operation['url']->setOption('query', ['destination' => $expected_destination]);
+ $result = $this->xpath('//ul[contains(@class, dropbutton)]/li/a[@href=:path and text()=:title]', [':path' => $operation['url']->toString(), ':title' => (string) $operation['title']]);
$this->assertEqual(count($result), 1, t('Found entity @operation link with destination parameter.', ['@operation' => $operation['title']]));
// Entities which were created in Hungarian should link to the Hungarian
// edit form, others to the English one (which has no path prefix here).
diff --git a/core/tests/Drupal/KernelTests/Core/Cache/ChainedFastBackendTest.php b/core/tests/Drupal/KernelTests/Core/Cache/ChainedFastBackendTest.php
index 3021b041fe..a93b674d5c 100644
--- a/core/tests/Drupal/KernelTests/Core/Cache/ChainedFastBackendTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Cache/ChainedFastBackendTest.php
@@ -20,7 +20,7 @@ class ChainedFastBackendTest extends GenericCacheBackendUnitTestBase {
* A new ChainedFastBackend object.
*/
protected function createCacheBackend($bin) {
- $consistent_backend = new DatabaseBackend(\Drupal::service('database'), \Drupal::service('cache_tags.invalidator.checksum'), $bin);
+ $consistent_backend = new DatabaseBackend(\Drupal::service('database'), \Drupal::service('cache_tags.invalidator.checksum'), $bin, 100);
$fast_backend = new PhpBackend($bin, \Drupal::service('cache_tags.invalidator.checksum'));
$backend = new ChainedFastBackend($consistent_backend, $fast_backend, $bin);
// Explicitly register the cache bin as it can not work through the
diff --git a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php
index de8bbda553..4f10c71e6c 100644
--- a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php
@@ -12,6 +12,13 @@
class DatabaseBackendTest extends GenericCacheBackendUnitTestBase {
/**
+ * The max rows to use for test bins.
+ *
+ * @var int
+ */
+ protected static $maxRows = 100;
+
+ /**
* Modules to enable.
*
* @var array
@@ -25,7 +32,7 @@ class DatabaseBackendTest extends GenericCacheBackendUnitTestBase {
* A new DatabaseBackend object.
*/
protected function createCacheBackend($bin) {
- return new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), $bin);
+ return new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), $bin, static::$maxRows);
}
/**
@@ -48,4 +55,47 @@ public function testSetGet() {
$this->assertIdentical($cached_value_short, $backend->get($cid_short)->data, "Backend contains the correct value for short, non-ASCII cache id.");
}
+ /**
+ * Tests the row count limiting of cache bin database tables.
+ */
+ public function testGarbageCollection() {
+ $backend = $this->getCacheBackend();
+ $max_rows = static::$maxRows;
+
+ $this->assertSame(0, (int) $this->getNumRows());
+
+ // Fill to just the limit.
+ for ($i = 0; $i < $max_rows; $i++) {
+ $backend->set("test$i", $i);
+ }
+ $this->assertSame($max_rows, $this->getNumRows());
+
+ // Garbage collection has no effect.
+ $backend->garbageCollection();
+ $this->assertSame($max_rows, $this->getNumRows());
+
+ // Go one row beyond the limit.
+ $backend->set('test' . ($max_rows + 1), $max_rows + 1);
+ $this->assertSame($max_rows + 1, $this->getNumRows());
+
+ // Garbage collection removes one row: the oldest.
+ $backend->garbageCollection();
+ $this->assertSame($max_rows, $this->getNumRows());
+ $this->assertFalse($backend->get('test0'));
+ }
+
+ /**
+ * Gets the number of rows in the test cache bin database table.
+ *
+ * @return int
+ * The number of rows in the test cache bin database table.
+ */
+ protected function getNumRows() {
+ $table = 'cache_' . $this->testBin;
+ $connection = $this->container->get('database');
+ $query = $connection->select($table);
+ $query->addExpression('COUNT(cid)', 'cid');
+ return (int) $query->execute()->fetchField();
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php b/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php
index 8129410e60..13b29c33f3 100644
--- a/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php
@@ -70,7 +70,8 @@ public function register(ContainerBuilder $container) {
parent::register($container);
$container->register('cache_factory', 'Drupal\Core\Cache\DatabaseBackendFactory')
->addArgument(new Reference('database'))
- ->addArgument(new Reference('cache_tags.invalidator.checksum'));
+ ->addArgument(new Reference('cache_tags.invalidator.checksum'))
+ ->addArgument(new Reference('settings'));
}
/**
diff --git a/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendFactoryTest.php b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendFactoryTest.php
new file mode 100644
index 0000000000..9d5ac4bdf9
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendFactoryTest.php
@@ -0,0 +1,112 @@
+prophesize(Connection::class)->reveal(),
+ $this->prophesize(CacheTagsChecksumInterface::class)->reveal(),
+ new Settings($settings)
+ );
+
+ $this->assertSame($expected_max_rows_foo, $database_backend_factory->get('foo')->getMaxRows());
+ $this->assertSame($expected_max_rows_bar, $database_backend_factory->get('bar')->getMaxRows());
+ }
+
+ public function getProvider() {
+ return [
+ 'default' => [
+ [],
+ DatabaseBackend::DEFAULT_MAX_ROWS,
+ DatabaseBackend::DEFAULT_MAX_ROWS,
+ ],
+ 'default overridden' => [
+ [
+ 'database_cache_max_rows' => [
+ 'default' => 99,
+ ],
+ ],
+ 99,
+ 99,
+ ],
+ 'default + foo bin overridden' => [
+ [
+ 'database_cache_max_rows' => [
+ 'bins' => [
+ 'foo' => 13,
+ ],
+ ],
+ ],
+ 13,
+ DatabaseBackend::DEFAULT_MAX_ROWS,
+ ],
+ 'default + bar bin overridden' => [
+ [
+ 'database_cache_max_rows' => [
+ 'bins' => [
+ 'bar' => 13,
+ ],
+ ],
+ ],
+ DatabaseBackend::DEFAULT_MAX_ROWS,
+ 13,
+ ],
+ 'default overridden + bar bin overridden' => [
+ [
+ 'database_cache_max_rows' => [
+ 'default' => 99,
+ 'bins' => [
+ 'bar' => 13,
+ ],
+ ],
+ ],
+ 99,
+ 13,
+ ],
+ 'default + both bins overridden' => [
+ [
+ 'database_cache_max_rows' => [
+ 'bins' => [
+ 'foo' => 13,
+ 'bar' => 31,
+ ],
+ ],
+ ],
+ 13,
+ 31,
+ ],
+ 'default overridden + both bins overridden' => [
+ [
+ 'database_cache_max_rows' => [
+ 'default' => 99,
+ 'bins' => [
+ 'foo' => 13,
+ 'bar' => 31,
+ ],
+ ],
+ ],
+ 13,
+ 31,
+ ],
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php
index 44e771ceaa..ad68e2df68 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php
@@ -11,6 +11,7 @@
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\entity_test\EntityTestListBuilder;
use Drupal\Tests\UnitTestCase;
@@ -63,6 +64,13 @@ class EntityListBuilderTest extends UnitTestCase {
protected $role;
/**
+ * The redirect destination service.
+ *
+ * @var \Drupal\Core\Routing\RedirectDestinationInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $redirectDestination;
+
+ /**
* The EntityListBuilder object to test.
*
* @var \Drupal\Core\Entity\EntityListBuilder
@@ -80,7 +88,8 @@ protected function setUp() {
$this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
$this->entityType = $this->getMock('\Drupal\Core\Entity\EntityTypeInterface');
$this->translationManager = $this->getMock('\Drupal\Core\StringTranslation\TranslationInterface');
- $this->entityListBuilder = new TestEntityListBuilder($this->entityType, $this->roleStorage, $this->moduleHandler);
+ $this->entityListBuilder = new TestEntityListBuilder($this->entityType, $this->roleStorage);
+ $this->redirectDestination = $this->getMock(RedirectDestinationInterface::class);
$this->container = new ContainerBuilder();
\Drupal::setContainer($this->container);
}
@@ -117,12 +126,20 @@ public function testGetOperations() {
$url->expects($this->any())
->method('toArray')
->will($this->returnValue([]));
+ $url->expects($this->atLeastOnce())
+ ->method('mergeOptions')
+ ->with(['query' => ['destination' => '/foo/bar']]);
$this->role->expects($this->any())
- ->method('urlInfo')
+ ->method('toUrl')
->will($this->returnValue($url));
- $list = new EntityListBuilder($this->entityType, $this->roleStorage, $this->moduleHandler);
+ $this->redirectDestination->expects($this->atLeastOnce())
+ ->method('getAsArray')
+ ->willReturn(['destination' => '/foo/bar']);
+
+ $list = new EntityListBuilder($this->entityType, $this->roleStorage);
$list->setStringTranslation($this->translationManager);
+ $list->setRedirectDestination($this->redirectDestination);
$operations = $list->getOperations($this->role);
$this->assertInternalType('array', $operations);