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 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Content Moderation module.
+ */
+
+/**
+ * Remove the 'content_revision_tracker' table.
+ */
+function content_moderation_update_8401() {
+  $database_schema = \Drupal::database()->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 @@
-<?php
-
-namespace Drupal\content_moderation;
-
-use Drupal\Core\Database\Connection;
-use Drupal\Core\Database\DatabaseExceptionWrapper;
-use Drupal\Core\Database\SchemaObjectExistsException;
-
-/**
- * Tracks metadata about revisions across entities.
- *
- * @internal
- */
-class RevisionTracker implements RevisionTrackerInterface {
-
-  /**
-   * The name of the SQL table we use for tracking.
-   *
-   * @var string
-   */
-  protected $tableName;
-
-  /**
-   * The database connection.
-   *
-   * @var \Drupal\Core\Database\Connection
-   */
-  protected $connection;
-
-  /**
-   * Constructs a new RevisionTracker.
-   *
-   * @param \Drupal\Core\Database\Connection $connection
-   *   The database connection.
-   * @param string $table
-   *   The table that should be used for tracking.
-   */
-  public function __construct(Connection $connection, $table = 'content_revision_tracker') {
-    $this->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 @@
-<?php
-
-namespace Drupal\content_moderation;
-
-/**
- * Tracks metadata about revisions across content entities.
- *
- * @internal
- */
-interface RevisionTrackerInterface {
-
-  /**
-   * Sets 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 static
-   */
-  public function setLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id);
-
-}
diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php
index 189562dd8c..57d6d76160 100644
--- a/core/modules/content_moderation/src/ViewsData.php
+++ b/core/modules/content_moderation/src/ViewsData.php
@@ -51,137 +51,13 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Mod
   public function getViewsData() {
     $data = [];
 
-    $data['content_revision_tracker']['table']['group'] = $this->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 @@
-<?php
-
-namespace Drupal\Tests\content_moderation\Functional;
-
-use Drupal\node\Entity\Node;
-use Drupal\node\Entity\NodeType;
-use Drupal\Tests\BrowserTestBase;
-use Drupal\workflows\Entity\Workflow;
-
-/**
- * Tests the "Latest Revision" views filter.
- *
- * @group content_moderation
- */
-class LatestRevisionViewsFilterTest extends BrowserTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static $modules = [
-    'content_moderation_test_views',
-    'content_moderation',
-  ];
-
-  /**
-   * Tests view shows the correct node IDs.
-   */
-  public function testViewShowsCorrectNids() {
-    $this->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 <em class="placeholder">Publish</em> transition from <em class="placeholder">Simple Workflow</em> workflow.',
+            'title' => '<em class="placeholder">Simple Workflow</em> workflow: Use <em class="placeholder">Publish</em> transition.',
           ],
           'use simple_workflow transition create_new_draft' => [
-            'title' => 'Use <em class="placeholder">Create New Draft</em> transition from <em class="placeholder">Simple Workflow</em> workflow.',
+            'title' => '<em class="placeholder">Simple Workflow</em> workflow: Use <em class="placeholder">Create New Draft</em> 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. <a href=":link">Add term</a>.', [':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. <a href=":link">Add term</a>.', [':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 @@
+<?php
+
+namespace Drupal\taxonomy;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access control handler for the taxonomy vocabulary entity type.
+ *
+ * @see \Drupal\taxonomy\Entity\Vocabulary
+ */
+class VocabularyAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    switch ($operation) {
+      case 'access taxonomy overview':
+        return AccessResult::allowedIfHasPermissions($account, ['access taxonomy overview', 'administer taxonomy'], 'OR');
+
+      default:
+        return parent::checkAccess($entity, $operation, $account);
+    }
+  }
+
+}
diff --git a/core/modules/taxonomy/src/VocabularyListBuilder.php b/core/modules/taxonomy/src/VocabularyListBuilder.php
index 8503c288a9..e7c021b4d7 100644
--- a/core/modules/taxonomy/src/VocabularyListBuilder.php
+++ b/core/modules/taxonomy/src/VocabularyListBuilder.php
@@ -4,8 +4,13 @@
 
 use Drupal\Core\Config\Entity\DraggableListBuilder;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a class to build a listing of taxonomy vocabulary entities.
@@ -20,6 +25,59 @@ class VocabularyListBuilder extends DraggableListBuilder {
   protected $entitiesKey = 'vocabularies';
 
   /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new VocabularyListBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(EntityTypeInterface $entity_type, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer = NULL) {
+    parent::__construct($entity_type, $entity_type_manager->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. <a href=":link">Add vocabulary</a>.', [':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. <a href=":link">Add vocabulary</a>.', [
+          ':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 '<p>' . 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()]) . '</p>';
-        case VocabularyInterface::HIERARCHY_SINGLE:
-          return '<p>' . 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()]) . '</p>';
-        case VocabularyInterface::HIERARCHY_MULTIPLE:
-          return '<p>' . 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())]) . '</p>';
+      if (\Drupal::currentUser()->hasPermission('administer taxonomy') || \Drupal::currentUser()->hasPermission('edit terms in ' . $vocabulary->id())) {
+        switch ($vocabulary->getHierarchy()) {
+          case VocabularyInterface::HIERARCHY_DISABLED:
+            return '<p>' . 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()]) . '</p>';
+          case VocabularyInterface::HIERARCHY_SINGLE:
+            return '<p>' . 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()]) . '</p>';
+          case VocabularyInterface::HIERARCHY_MULTIPLE:
+            return '<p>' . 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())]) . '</p>';
+        }
+      }
+      else {
+        switch ($vocabulary->getHierarchy()) {
+          case VocabularyInterface::HIERARCHY_DISABLED:
+            return '<p>' . t('%capital_name contains the following terms.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
+          case VocabularyInterface::HIERARCHY_SINGLE:
+            return '<p>' . t('%capital_name contains terms grouped under parent terms', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
+          case VocabularyInterface::HIERARCHY_MULTIPLE:
+            return '<p>' . t('%capital_name contains terms with multiple parents.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
+        }
       }
   }
 }
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 a <code>destination</code> parameter in the link to return the user to the original view upon completing the link action.'),
+      '#description' => $this->t('Enforce a <code>destination</code> 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 @@
 <?php
 
-namespace Drupal\content_moderation\Plugin\views\filter;
+namespace Drupal\views\Plugin\views\filter;
 
-use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\views\Plugin\views\filter\FilterPluginBase;
 use Drupal\views\Plugin\ViewsHandlerManager;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -34,13 +32,6 @@ class LatestRevision extends FilterPluginBase implements ContainerFactoryPluginI
   protected $joinHandler;
 
   /**
-   * Database Connection.
-   *
-   * @var \Drupal\Core\Database\Connection
-   */
-  protected $connection;
-
-  /**
    * Constructs a new LatestRevision.
    *
    * @param array $configuration
@@ -53,14 +44,12 @@ class LatestRevision extends FilterPluginBase implements ContainerFactoryPluginI
    *   Entity Type Manager Service.
    * @param \Drupal\views\Plugin\ViewsHandlerManager $join_handler
    *   Views Handler Plugin Manager.
-   * @param \Drupal\Core\Database\Connection $connection
-   *   Database Connection.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ViewsHandlerManager $join_handler, Connection $connection) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ViewsHandlerManager $join_handler) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
+
     $this->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 @@
+<?php
+
+namespace Drupal\Tests\views\Functional\Entity;
+
+use Drupal\node\Entity\Node;
+use Drupal\Tests\views\Functional\ViewTestBase;
+use Drupal\views\ViewExecutable;
+use Drupal\views\Views;
+
+/**
+ * Tests the 'Latest revision' filter.
+ *
+ * @group views
+ */
+class LatestRevisionFilterTest extends ViewTestBase {
+
+  /**
+   * An array of node revisions.
+   *
+   * @var \Drupal\node\NodeInterface[]
+   */
+  protected $allRevisions = [];
+
+  /**
+   * An array of node revisions.
+   *
+   * @var \Drupal\node\NodeInterface[]
+   */
+  protected $latestRevisions = [];
+
+  /**
+   * Views used by this test.
+   *
+   * @var array
+   */
+  public static $testViews = ['test_latest_revision_filter'];
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp($import_test_views = TRUE) {
+    parent::setUp();
+
+    $this->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 @@
+<?php
+
+namespace Drupal\Tests\Core\Cache;
+
+use Drupal\Core\Cache\CacheTagsChecksumInterface;
+use Drupal\Core\Cache\DatabaseBackend;
+use Drupal\Core\Cache\DatabaseBackendFactory;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Site\Settings;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Cache\DatabaseBackendFactory
+ * @group Cache
+ */
+class DatabaseBackendFactoryTest extends UnitTestCase {
+
+  /**
+   * @covers ::__construct
+   * @covers ::get
+   * @dataProvider getProvider
+   */
+  public function testGet(array $settings, $expected_max_rows_foo, $expected_max_rows_bar) {
+    $database_backend_factory = new DatabaseBackendFactory(
+      $this->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);
