From 88b6c938d5e944b790486fc28c6a31d5a3d562c5 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Mon, 2 Sep 2013 14:31:46 +0100
Subject: [PATCH] Issue #1951268 by larowlan, nick_schuch, jibran, dawehner,
 alexpott, tim.plunkett: Convert /forum and /forum/% to new routing system,
 remove forum_forum_load(), forum_get_topics(), create new Forum service.

---
 ...y.display.taxonomy_term.forums.default.yml |   9 +
 ...m_display.taxonomy_term.forums.default.yml |  12 +
 .../config/field.field.forum_container.yml    |  18 +
 ...e.taxonomy_term.forums.forum_container.yml |  16 +
 core/modules/forum/config/forum.settings.yml  |   1 -
 .../forum/config/schema/forum.schema.yml      |   6 -
 core/modules/forum/forum.admin.inc            |   2 +-
 core/modules/forum/forum.info.yml             |   1 +
 core/modules/forum/forum.install              | 146 ++++-
 core/modules/forum/forum.module               | 567 +++---------------
 core/modules/forum/forum.pages.inc            |  62 --
 core/modules/forum/forum.routing.yml          |  16 +
 core/modules/forum/forum.services.yml         |   5 +-
 .../forum/Controller/ForumController.php      | 152 ++++-
 .../forum/Form/ContainerFormController.php    |   8 +-
 .../Drupal/forum/ForumBreadcrumbBuilder.php   |  44 +-
 .../forum/lib/Drupal/forum/ForumManager.php   | 551 +++++++++++++++++
 .../Drupal/forum/ForumManagerInterface.php    | 104 ++++
 .../lib/Drupal/forum/Tests/ForumTest.php      |   9 +-
 .../Drupal/forum/Tests/ForumManagerTest.php   | 101 ++++
 .../Tests/Upgrade/ForumUpgradePathTest.php    |  56 ++
 .../tests/upgrade/drupal-7.forum.database.php |  48 ++
 22 files changed, 1315 insertions(+), 619 deletions(-)
 create mode 100644 core/modules/forum/config/entity.display.taxonomy_term.forums.default.yml
 create mode 100644 core/modules/forum/config/entity.form_display.taxonomy_term.forums.default.yml
 create mode 100644 core/modules/forum/config/field.field.forum_container.yml
 create mode 100644 core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml
 delete mode 100644 core/modules/forum/forum.pages.inc
 create mode 100644 core/modules/forum/lib/Drupal/forum/ForumManager.php
 create mode 100644 core/modules/forum/lib/Drupal/forum/ForumManagerInterface.php
 create mode 100644 core/modules/forum/tests/Drupal/forum/Tests/ForumManagerTest.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Upgrade/ForumUpgradePathTest.php
 create mode 100644 core/modules/system/tests/upgrade/drupal-7.forum.database.php

diff --git a/core/modules/forum/config/entity.display.taxonomy_term.forums.default.yml b/core/modules/forum/config/entity.display.taxonomy_term.forums.default.yml
new file mode 100644
index 000000000000..ef18047ad6ef
--- /dev/null
+++ b/core/modules/forum/config/entity.display.taxonomy_term.forums.default.yml
@@ -0,0 +1,9 @@
+id: taxonomy_term.forums.default
+uuid: adaef6a9-8dc0-4f2e-9858-88daac440aa9
+targetEntityType: taxonomy_term
+bundle: forums
+mode: default
+content:
+  description:
+    weight: '0'
+    visible: '1'
diff --git a/core/modules/forum/config/entity.form_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/entity.form_display.taxonomy_term.forums.default.yml
new file mode 100644
index 000000000000..203b69a4704f
--- /dev/null
+++ b/core/modules/forum/config/entity.form_display.taxonomy_term.forums.default.yml
@@ -0,0 +1,12 @@
+id: taxonomy_term.forums.default
+uuid: c8eab085-8fd3-4545-8600-e13b7d8bb9c4
+targetEntityType: taxonomy_term
+bundle: forums
+mode: default
+content:
+  name:
+    weight: '-5'
+    visible: '1'
+  description:
+    weight: '0'
+    visible: '1'
diff --git a/core/modules/forum/config/field.field.forum_container.yml b/core/modules/forum/config/field.field.forum_container.yml
new file mode 100644
index 000000000000..96eb824f8643
--- /dev/null
+++ b/core/modules/forum/config/field.field.forum_container.yml
@@ -0,0 +1,18 @@
+id: taxonomy_term.forum_container
+uuid: babf2ba1-505f-4c71-8a07-7be19f4fb9f3
+status: '1'
+langcode: en
+name: forum_container
+type: list_boolean
+settings:
+  allowed_values:
+    - ''
+    - ''
+  allowed_values_function: ''
+module: options
+active: '1'
+entity_type: taxonomy_term
+locked: '1'
+cardinality: '1'
+translatable: '0'
+indexes: {  }
diff --git a/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml b/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml
new file mode 100644
index 000000000000..c8c36afcc031
--- /dev/null
+++ b/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml
@@ -0,0 +1,16 @@
+id: taxonomy_term.forums.forum_container
+uuid: 8421d585-f6ef-4209-ad00-cfb30a1ab075
+status: '1'
+langcode: en
+field_uuid: babf2ba1-505f-4c71-8a07-7be19f4fb9f3
+entity_type: taxonomy_term
+bundle: forums
+label: Container
+description: ''
+required: '1'
+default_value:
+  -
+    value: '0'
+default_value_function: ''
+settings: {  }
+field_type: list_boolean
diff --git a/core/modules/forum/config/forum.settings.yml b/core/modules/forum/config/forum.settings.yml
index 3f0b30f72371..4f2516ef8793 100644
--- a/core/modules/forum/config/forum.settings.yml
+++ b/core/modules/forum/config/forum.settings.yml
@@ -3,7 +3,6 @@ block:
     limit: '5'
   new:
     limit: '5'
-containers: []
 topics:
   hot_threshold: '15'
   order: '1'
diff --git a/core/modules/forum/config/schema/forum.schema.yml b/core/modules/forum/config/schema/forum.schema.yml
index 60bd0f771832..9ca1ed80c787 100644
--- a/core/modules/forum/config/schema/forum.schema.yml
+++ b/core/modules/forum/config/schema/forum.schema.yml
@@ -22,12 +22,6 @@ forum.settings:
             limit:
               type: integer
               label: 'New forum Count'
-    containers:
-      type: sequence
-      label: 'Containers to group related forums'
-      sequence:
-        - type: integer
-          label: 'Taxonomy Term ID'
     topics:
       type: mapping
       label: 'Forum topics block'
diff --git a/core/modules/forum/forum.admin.inc b/core/modules/forum/forum.admin.inc
index 332f11ca5f41..962e84d46add 100644
--- a/core/modules/forum/forum.admin.inc
+++ b/core/modules/forum/forum.admin.inc
@@ -32,7 +32,7 @@ function forum_overview($form, &$form_state) {
       $term = $form['terms'][$key]['#term'];
       $form['terms'][$key]['term']['#href'] = 'forum/' . $term->id();
       unset($form['terms'][$key]['operations']['#links']['delete']);
-      if (in_array($form['terms'][$key]['#term']->id(), $config->get('containers'))) {
+      if (!empty($term->forum_container->value)) {
         $form['terms'][$key]['operations']['#links']['edit']['title'] = t('edit container');
         $form['terms'][$key]['operations']['#links']['edit']['href'] = 'admin/structure/forum/edit/container/' . $term->id();
         // We don't want the redirect from the link so we can redirect the
diff --git a/core/modules/forum/forum.info.yml b/core/modules/forum/forum.info.yml
index 86e562765026..c136ae423ec6 100644
--- a/core/modules/forum/forum.info.yml
+++ b/core/modules/forum/forum.info.yml
@@ -6,6 +6,7 @@ dependencies:
   - history
   - taxonomy
   - comment
+  - options
 package: Core
 version: VERSION
 core: 8.x
diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install
index adc8ddbb1d7e..e4b1e26352e0 100644
--- a/core/modules/forum/forum.install
+++ b/core/modules/forum/forum.install
@@ -5,6 +5,8 @@
  * Install, update, and uninstall functions for the Forum module.
  */
 
+use Drupal\Core\Language\Language;
+
 /**
  * Implements hook_install().
  */
@@ -73,6 +75,7 @@ function forum_enable() {
       'description' => '',
       'parent' => array(0),
       'vid' => $vocabulary->id(),
+      'forum_container' => 0,
     ));
     $term->save();
 
@@ -108,6 +111,18 @@ function forum_enable() {
   }
 }
 
+/**
+ * Implements hook_modules_preinstall().
+ */
+function forum_modules_preinstall($modules) {
+  $list_boolean = Drupal::service('plugin.manager.entity.field.field_type')->getDefinition('list_boolean');
+  if (empty($list_boolean) && in_array('forum', $modules)) {
+    // Make sure that the list_boolean field type is available before our
+    // default config is installed.
+    field_info_cache_clear();
+  }
+}
+
 /**
  * Implements hook_uninstall().
  */
@@ -119,8 +134,15 @@ function forum_uninstall() {
     $field->delete();
   }
 
-  // Purge field data now to allow taxonomy module to be uninstalled
-  // if this is the only field remaining.
+  if ($field = field_info_field('taxonomy_term', 'forum_container')) {
+    $field->delete();
+  }
+
+  // Purge field data now to allow taxonomy and options module to be uninstalled
+  // if this is the only field remaining. We need to run it twice because
+  // field_purge_batch() will not remove the instance and the field in the same
+  // pass.
+  field_purge_batch(10);
   field_purge_batch(10);
   // Allow to delete a forum's node type.
   $locked = Drupal::state()->get('node.type.locked');
@@ -261,13 +283,131 @@ function forum_update_last_removed() {
  * @ingroup config_upgrade
  */
 function forum_update_8000() {
+  $map = db_query('SELECT vid, machine_name FROM {taxonomy_vocabulary}')->fetchAllKeyed();
+  $forum_vid = update_variable_get('forum_nav_vocabulary', FALSE);
+  if (!empty($map[$forum_vid])) {
+    // Update the variable to reference the machine name instead of the vid.
+    update_variable_set('forum_nav_vocabulary', $map[$forum_vid]);
+  }
   update_variables_to_config('forum.settings', array(
     'forum_hot_topic' => 'topics.hot_threshold',
     'forum_per_page' => 'topics.page_limit',
     'forum_order' => 'topics.order',
     'forum_nav_vocabulary' => 'vocabulary',
-    'forum_containers' => 'containers',
     'forum_block_num_active' => 'block.active.limit',
     'forum_block_num_new' => 'block.new.limit',
   ));
 }
+
+/**
+ * Implements hook_update_dependencies().
+ */
+function forum_update_dependencies() {
+  // Convert containers to field after the fields and instances are converted to
+  // ConfigEntities.
+  $dependencies['forum'][8001] = array(
+    'field' => 8003,
+    'taxonomy' => 8007,
+  );
+  return $dependencies;
+}
+
+/**
+ * Adds the forum_container field and copies the values over.
+ */
+function forum_update_8001() {
+  $vocabulary = config('forum.settings')->get('vocabulary');
+  // Create the field and instance.
+  $field = array(
+    'id' => 'taxonomy_term.forum_container',
+    'name' => 'forum_container',
+    'entity_type' => 'taxonomy_term',
+    'module' => 'options',
+    'type' => 'list_boolean',
+    'cardinality' => 1,
+    'locked' => TRUE,
+    'indexes' => array(),
+    'settings' => array(
+      'allowed_values' => array('', ''),
+      'allowed_values_function' => '',
+    ),
+    'schema' => array(
+      'columns' => array(
+        'value' => array(
+          'type' => 'int',
+          'not null' => FALSE,
+        ),
+      ),
+      'indexes' => array(),
+      'foreign keys' => array(),
+    ),
+  );
+  _update_8003_field_create_field($field);
+
+  $instance = array(
+    'id' => 'taxonomy_term.' . $vocabulary . '.forum_container',
+    'entity_type' => 'taxonomy_term',
+    'label' => 'Container',
+    'bundle' => $vocabulary,
+    'description' => '',
+    'required' => TRUE,
+    'settings' => array(),
+    'default_value' => array('value' => 0),
+  );
+  _update_8003_field_create_instance($field, $instance);
+}
+
+/**
+ * Migrate forum containers from variable to field values.
+ */
+function forum_update_8002(&$sandbox) {
+  // Initialize total values to process.
+  if (!isset($sandbox['total'])) {
+    $containers = update_variable_get('forum_containers', array());
+    $vocabulary = config('forum.settings')->get('vocabulary');
+    $sandbox['containers'] = $containers;
+    $sandbox['vocabulary'] = $vocabulary;
+    $sandbox['total'] = count($containers);
+    $sandbox['processed'] = 0;
+  }
+
+  if ($sandbox['total']) {
+    // Retrieve next 20 containers to migrate.
+    $containers = array_splice($containers, $sandbox['processed'], 20);
+    foreach ($containers as $tid) {
+      // Add a row to the field data and revision tables.
+      db_insert('taxonomy_term__forum_container')
+        ->fields(array(
+          'bundle' => $sandbox['vocabulary'],
+          'entity_id' => $tid,
+          'revision_id' => $tid,
+          'langcode' => Language::LANGCODE_NOT_SPECIFIED,
+          'delta' => 0,
+          'forum_container_value' => 1,
+        ))
+        ->execute();
+      db_insert('taxonomy_term_revision__forum_container')
+        ->fields(array(
+          'bundle' => $vocabulary,
+          'entity_id' => $tid,
+          'revision_id' => $tid,
+          'langcode' => Language::LANGCODE_NOT_SPECIFIED,
+          'delta' => 0,
+          'forum_container_value' => 1,
+        ))
+        ->execute();
+    }
+
+    // Report status.
+    $sandbox['processed'] += count($containers);
+  }
+  $sandbox['#finished'] = $sandbox['total'] ? $sandbox['processed'] / $sandbox['total'] : 1;
+
+}
+
+/**
+ * Remove the forum_containers variable.
+ */
+function forum_update_8003() {
+  update_variable_del('forum_containers');
+}
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index 1ed6ac3d2330..0f076c49de79 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -6,8 +6,8 @@
  */
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\field\Field;
 use Drupal\node\NodeInterface;
-use Drupal\taxonomy\Entity\Term;
 
 /**
  * Implements hook_help().
@@ -74,7 +74,7 @@ function forum_theme() {
   return array(
     'forums' => array(
       'template' => 'forums',
-      'variables' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'tid' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
+      'variables' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'term' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
     ),
     'forum_list' => array(
       'template' => 'forum-list',
@@ -105,18 +105,13 @@ function forum_theme() {
 function forum_menu() {
   $items['forum'] = array(
     'title' => 'Forums',
-    'page callback' => 'forum_page',
-    'access arguments' => array('access content'),
-    'file' => 'forum.pages.inc',
+    'route_name' => 'forum_index',
   );
-  $items['forum/%forum_forum'] = array(
+  $items['forum/%forum'] = array(
     'title' => 'Forums',
     'title callback' => 'entity_page_label',
     'title arguments' => array(1),
-    'page callback' => 'forum_page',
-    'page arguments' => array(1),
-    'access arguments' => array('access content'),
-    'file' => 'forum.pages.inc',
+    'route_name' => 'forum_page',
   );
   $items['admin/structure/forum'] = array(
     'title' => 'Forums',
@@ -168,48 +163,53 @@ function forum_menu_local_tasks(&$data, $router_item, $root_path) {
 
   // Add action link to 'node/add/forum' on 'forum' sub-pages.
   if ($root_path == 'forum' || $root_path == 'forum/%') {
-    $tid = (isset($router_item['page_arguments'][0]) ? $router_item['page_arguments'][0]->id() : 0);
-    $forum_term = forum_forum_load($tid);
-    if ($forum_term) {
-      $links = array();
-      // Loop through all bundles for forum taxonomy vocabulary field.
-      $field = field_info_field('node', 'taxonomy_forums');
-      foreach ($field['bundles'] as $type_name) {
-        if (($type = entity_load('node_type', $type_name)) && node_access('create', $type_name)) {
-          $links[$type_name] = array(
-            '#theme' => 'menu_local_action',
-            '#link' => array(
-              'title' => t('Add new @node_type', array('@node_type' => $type->label())),
-              'href' => 'node/add/' . $type_name . '/' . $forum_term->id(),
-            ),
-          );
+    $request = Drupal::request();
+    $forum_term = $request->attributes->get('taxonomy_term');
+    $vid = Drupal::config('forum.settings')->get('vocabulary');
+    $links = array();
+    // Loop through all bundles for forum taxonomy vocabulary field.
+    $field = Field::fieldInfo()->getField('node', 'taxonomy_forums');
+    foreach ($field['bundles'] as $type) {
+      if (node_access('create', $type)) {
+        $links[$type] = array(
+          '#theme' => 'menu_local_action',
+          '#link' => array(
+            'title' => t('Add new @node_type', array('@node_type' => entity_load('node_type', $type)->label())),
+            'href' => 'node/add/' . $type,
+          ),
+        );
+        if ($forum_term && $forum_term->bundle() == $vid) {
+          // We are viewing a forum term (specific forum), append the tid to the
+          // url.
+          $links[$type]['#link']['href'] .= '/' . $forum_term->id();
+
         }
       }
-      if (empty($links)) {
-        // Authenticated user does not have access to create new topics.
-        if ($user->isAuthenticated()) {
-          $links['disallowed'] = array(
-            '#theme' => 'menu_local_action',
-            '#link' => array(
-              'title' => t('You are not allowed to post new content in the forum.'),
-            ),
-          );
-        }
-        // Anonymous user does not have access to create new topics.
-        else {
-          $links['login'] = array(
-            '#theme' => 'menu_local_action',
-            '#link' => array(
-              'title' => t('<a href="@login">Log in</a> to post new content in the forum.', array(
-                '@login' => url('user/login', array('query' => drupal_get_destination())),
-              )),
-              'localized_options' => array('html' => TRUE),
-            ),
-          );
-        }
+    }
+    if (empty($links)) {
+      // Authenticated user does not have access to create new topics.
+      if ($user->isAuthenticated()) {
+        $links['disallowed'] = array(
+          '#theme' => 'menu_local_action',
+          '#link' => array(
+            'title' => t('You are not allowed to post new content in the forum.'),
+          ),
+        );
+      }
+      // Anonymous user does not have access to create new topics.
+      else {
+        $links['login'] = array(
+          '#theme' => 'menu_local_action',
+          '#link' => array(
+            'title' => t('<a href="@login">Log in</a> to post new content in the forum.', array(
+              '@login' => url('user/login', array('query' => drupal_get_destination())),
+            )),
+            'localized_options' => array('html' => TRUE),
+          ),
+        );
       }
-      $data['actions'] += $links;
     }
+    $data['actions'] += $links;
   }
 }
 
@@ -255,21 +255,6 @@ function forum_uri($forum) {
   );
 }
 
-/**
- * Checks whether a node can be used in a forum, based on its content type.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity.
- *
- * @return
- *   Boolean indicating if the node can be assigned to a forum.
- */
-function _forum_node_check_node_type(EntityInterface $node) {
-  // Fetch information about the forum field.
-  $instance = field_info_instance('node', 'taxonomy_forums', $node->getType());
-  return !empty($instance);
-}
-
 /**
  * Implements hook_node_validate().
  *
@@ -277,12 +262,11 @@ function _forum_node_check_node_type(EntityInterface $node) {
  * forum taxonomy.
  */
 function forum_node_validate(EntityInterface $node, $form) {
-  if (_forum_node_check_node_type($node)) {
+  if (Drupal::service('forum_manager')->checkNodeType($node)) {
     $langcode = $form['taxonomy_forums']['#language'];
     // vocabulary is selected, not a "container" term.
     if (!$node->taxonomy_forums->isEmpty()) {
       // Extract the node's proper topic ID.
-      $containers = Drupal::config('forum.settings')->get('containers');
       foreach ($node->taxonomy_forums as $delta => $item) {
         // If no term was selected (e.g. when no terms exist yet), remove the
         // item.
@@ -299,7 +283,7 @@ function forum_node_validate(EntityInterface $node, $form) {
           ':tid' => $term->id(),
           ':vid' => $term->bundle(),
         ))->fetchField();
-        if ($used && in_array($term->id(), $containers)) {
+        if ($used && !empty($term->forum_container->value)) {
           form_set_error('taxonomy_forums', t('The item %forum is a forum container, not a forum. Select one of the forums below instead.', array('%forum' => $term->label())));
         }
       }
@@ -313,8 +297,7 @@ function forum_node_validate(EntityInterface $node, $form) {
  * Assigns the forum taxonomy when adding a topic from within a forum.
  */
 function forum_node_presave(EntityInterface $node) {
-
-  if (_forum_node_check_node_type($node)) {
+  if (Drupal::service('forum_manager')->checkNodeType($node)) {
     // Make sure all fields are set properly:
     $node->icon = !empty($node->icon) ? $node->icon : '';
     if (!$node->taxonomy_forums->isEmpty()) {
@@ -335,7 +318,7 @@ function forum_node_presave(EntityInterface $node) {
  * Implements hook_node_update().
  */
 function forum_node_update(EntityInterface $node) {
-  if (_forum_node_check_node_type($node)) {
+  if (Drupal::service('forum_manager')->checkNodeType($node)) {
     // If this is not a new revision and does exist, update the forum record,
     // otherwise insert a new one.
     if ($node->getRevisionId() == $node->original->getRevisionId() && db_query('SELECT tid FROM {forum} WHERE nid=:nid', array(':nid' => $node->id()))->fetchField()) {
@@ -399,8 +382,8 @@ function forum_node_update(EntityInterface $node) {
       }
       $query->execute();
       // The logic for determining last_comment_count is fairly complex, so
-      // call _forum_update_forum_index() too.
-      _forum_update_forum_index($node->id());
+      // update the index too.
+      Drupal::service('forum_manager')->updateIndex($node->id());
     }
     // When a forum node is unpublished, remove it from the forum_index table.
     else {
@@ -413,7 +396,7 @@ function forum_node_update(EntityInterface $node) {
  * Implements hook_node_insert().
  */
 function forum_node_insert(EntityInterface $node) {
-  if (_forum_node_check_node_type($node)) {
+  if (Drupal::service('forum_manager')->checkNodeType($node)) {
     if (!empty($node->forum_tid)) {
       $nid = db_insert('forum')
         ->fields(array(
@@ -448,7 +431,7 @@ function forum_node_insert(EntityInterface $node) {
  * Implements hook_node_predelete().
  */
 function forum_node_predelete(EntityInterface $node) {
-  if (_forum_node_check_node_type($node)) {
+  if (Drupal::service('forum_manager')->checkNodeType($node)) {
     db_delete('forum')
       ->condition('nid', $node->id())
       ->execute();
@@ -464,7 +447,7 @@ function forum_node_predelete(EntityInterface $node) {
 function forum_node_load($nodes) {
   $node_vids = array();
   foreach ($nodes as $node) {
-    if (_forum_node_check_node_type($node)) {
+    if (Drupal::service('forum_manager')->checkNodeType($node)) {
       $node_vids[] = $node->getRevisionId();
     }
   }
@@ -492,20 +475,6 @@ function forum_permission() {
   return $perms;
 }
 
-/**
- * Implements hook_taxonomy_term_delete().
- */
-function forum_taxonomy_term_delete(Term $term) {
-  // For containers, remove the tid from the forum_containers variable.
-  $config = Drupal::config('forum.settings');
-  $containers = $config->get('containers');
-  $key = array_search($term->id(), $containers);
-  if ($key !== FALSE) {
-    unset($containers[$key]);
-  }
-  $config->set('containers', $containers)->save();
-}
-
 /**
  * Implements hook_comment_publish().
  *
@@ -513,7 +482,7 @@ function forum_taxonomy_term_delete(Term $term) {
  * $comment->save() calls hook_comment_publish() for all published comments.
  */
 function forum_comment_publish($comment) {
-  _forum_update_forum_index($comment->nid->target_id);
+  Drupal::service('forum_manager')->updateIndex($comment->nid->target_id);
 }
 
 /**
@@ -526,7 +495,7 @@ function forum_comment_update($comment) {
   // $comment->save() calls hook_comment_publish() for all published comments,
   // so we need to handle all other values here.
   if (!$comment->status->value) {
-    _forum_update_forum_index($comment->nid->target_id);
+    Drupal::service('forum_manager')->updateIndex($comment->nid->target_id);
   }
 }
 
@@ -534,14 +503,14 @@ function forum_comment_update($comment) {
  * Implements hook_comment_unpublish().
  */
 function forum_comment_unpublish($comment) {
-  _forum_update_forum_index($comment->nid->target_id);
+  Drupal::service('forum_manager')->updateIndex($comment->nid->target_id);
 }
 
 /**
  * Implements hook_comment_delete().
  */
 function forum_comment_delete($comment) {
-  _forum_update_forum_index($comment->nid->target_id);
+  Drupal::service('forum_manager')->updateIndex($comment->nid->target_id);
 }
 
 /**
@@ -627,303 +596,6 @@ function forum_block_view_pre_render($elements) {
   return $elements;
 }
 
-/**
- * Returns a tree of all forums for a given taxonomy term ID.
- *
- * @param $tid
- *   (optional) Taxonomy term ID of the forum. If not given all forums will be
- *   returned.
- *
- * @return
- *   A tree of taxonomy objects, with the following additional properties:
- *   - num_topics: Number of topics in the forum.
- *   - num_posts: Total number of posts in all topics.
- *   - last_post: Most recent post for the forum.
- *   - forums: An array of child forums.
- */
-function forum_forum_load($tid = NULL) {
-  $cache = &drupal_static(__FUNCTION__, array());
-
-  // Return a cached forum tree if available.
-  if (!isset($tid)) {
-    $tid = 0;
-  }
-  if (isset($cache[$tid])) {
-    return $cache[$tid];
-  }
-
-  $config = Drupal::config('forum.settings');
-  $vid = $config->get('vocabulary');
-
-  // Load and validate the parent term.
-  if ($tid) {
-    $forum_term = entity_load('taxonomy_term', $tid);
-    if (!$forum_term || ($forum_term->bundle() != $vid)) {
-      return $cache[$tid] = FALSE;
-    }
-  }
-  // If $tid is 0, create an empty entity to hold the child terms.
-  elseif ($tid === 0) {
-    $forum_term = entity_create('taxonomy_term', array(
-      'tid' => 0,
-      'vid' => $vid,
-    ));
-  }
-
-  // Determine if the requested term is a container.
-  if (!$forum_term->id() || in_array($forum_term->id(), $config->get('containers'))) {
-    $forum_term->container = 1;
-  }
-
-  // Load parent terms.
-  $forum_term->parents = taxonomy_term_load_parents_all($forum_term->id());
-
-  // Load the tree below.
-  $forums = array();
-  $_forums = taxonomy_get_tree($vid, $tid, NULL, TRUE);
-
-  if (count($_forums)) {
-    $query = db_select('node_field_data', 'n');
-    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
-    $query->join('forum', 'f', 'n.vid = f.vid');
-    $query->addExpression('COUNT(n.nid)', 'topic_count');
-    $query->addExpression('SUM(ncs.comment_count)', 'comment_count');
-    $counts = $query
-      ->fields('f', array('tid'))
-      ->condition('n.status', 1)
-      // @todo This should be actually filtering on the desired node status
-      //   field language and just fall back to the default language.
-      ->condition('n.default_langcode', 1)
-      ->groupBy('tid')
-      ->addTag('node_access')
-      ->execute()
-      ->fetchAllAssoc('tid');
-  }
-
-  foreach ($_forums as $forum) {
-    // Determine if the child term is a container.
-    if (in_array($forum->id(), $config->get('containers'))) {
-      $forum->container = 1;
-    }
-
-    // Merge in the topic and post counters.
-    if (!empty($counts[$forum->id()])) {
-      $forum->num_topics = $counts[$forum->id()]->topic_count;
-      $forum->num_posts = $counts[$forum->id()]->topic_count + $counts[$forum->id()]->comment_count;
-    }
-    else {
-      $forum->num_topics = 0;
-      $forum->num_posts = 0;
-    }
-
-    // Query "Last Post" information for this forum.
-    $query = db_select('node_field_data', 'n');
-    $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $forum->id()));
-    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
-    $query->join('users', 'u', 'ncs.last_comment_uid = u.uid');
-    $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u.name END', 'last_comment_name');
-
-    $topic = $query
-      ->fields('ncs', array('last_comment_timestamp', 'last_comment_uid'))
-      ->condition('n.status', 1)
-      // @todo This should be actually filtering on the desired node status
-      //   field language and just fall back to the default language.
-      ->condition('n.default_langcode', 1)
-      ->orderBy('last_comment_timestamp', 'DESC')
-      ->range(0, 1)
-      ->addTag('node_access')
-      ->execute()
-      ->fetchObject();
-
-    // Merge in the "Last Post" information.
-    $last_post = new stdClass();
-    if (!empty($topic->last_comment_timestamp)) {
-      $last_post->created = $topic->last_comment_timestamp;
-      $last_post->name = $topic->last_comment_name;
-      $last_post->uid = $topic->last_comment_uid;
-    }
-    $forum->last_post = $last_post;
-
-    $forums[$forum->id()] = $forum;
-  }
-
-  // Cache the result, and return the tree.
-  $forum_term->forums = $forums;
-  $cache[$tid] = $forum_term;
-  return $forum_term;
-}
-
-/**
- * Calculates the number of new posts in a forum that the user has not yet read.
- *
- * Nodes are new if they are newer than HISTORY_READ_LIMIT.
- *
- * @param $term
- *   The term ID of the forum.
- * @param $uid
- *   The user ID.
- *
- * @return
- *   The number of new posts in the forum that have not been read by the user.
- */
-function _forum_topics_unread($term, $uid) {
-  $query = db_select('node_field_data', 'n');
-  $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $term));
-  $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', array(':uid' => $uid));
-  $query->addExpression('COUNT(n.nid)', 'count');
-  return $query
-    ->condition('status', 1)
-    // @todo This should be actually filtering on the desired node status field
-    //   language and just fall back to the default language.
-    ->condition('n.default_langcode', 1)
-    ->condition('n.created', HISTORY_READ_LIMIT, '>')
-    ->isNull('h.nid')
-    ->addTag('node_access')
-    ->execute()
-    ->fetchField();
-}
-
-/**
- * Gets all the topics in a forum.
- *
- * @param $tid
- *   The term ID of the forum.
- * @param $sortby
- *   One of the following integers indicating the sort criteria:
- *   - 1: Date - newest first.
- *   - 2: Date - oldest first.
- *   - 3: Posts with the most comments first.
- *   - 4: Posts with the least comments first.
- * @param $forum_per_page
- *   The maximum number of topics to display per page.
- *
- * @return
- *   A list of all the topics in a forum.
- */
-function forum_get_topics($tid, $sortby, $forum_per_page) {
-  global $user, $forum_topic_list_header;
-
-  $forum_topic_list_header = array(
-    array('data' => t('Topic'), 'field' => 'f.title'),
-    array('data' => t('Replies'), 'field' => 'f.comment_count'),
-    array('data' => t('Last reply'), 'field' => 'f.last_comment_timestamp'),
-  );
-
-  $order = _forum_get_topic_order($sortby);
-  for ($i = 0; $i < count($forum_topic_list_header); $i++) {
-    if ($forum_topic_list_header[$i]['field'] == $order['field']) {
-      $forum_topic_list_header[$i]['sort'] = $order['sort'];
-    }
-  }
-
-  $query = db_select('forum_index', 'f')
-    ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
-    ->extend('Drupal\Core\Database\Query\TableSortExtender');
-  $query->fields('f');
-  $query
-    ->condition('f.tid', $tid)
-    ->addTag('node_access')
-    ->addMetaData('base_table', 'forum_index')
-    ->orderBy('f.sticky', 'DESC')
-    ->orderByHeader($forum_topic_list_header)
-    ->limit($forum_per_page);
-
-  $count_query = db_select('forum_index', 'f');
-  $count_query->condition('f.tid', $tid);
-  $count_query->addExpression('COUNT(*)');
-  $count_query->addTag('node_access');
-  $count_query->addMetaData('base_table', 'forum_index');
-
-  $query->setCountQuery($count_query);
-  $result = $query->execute();
-  $nids = array();
-  foreach ($result as $record) {
-    $nids[] = $record->nid;
-  }
-  if ($nids) {
-    $nodes = node_load_multiple($nids);
-
-    $query = db_select('node_field_data', 'n')
-      ->extend('Drupal\Core\Database\Query\TableSortExtender');
-    $query->fields('n', array('nid'));
-
-    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
-    $query->fields('ncs', array('cid', 'last_comment_uid', 'last_comment_timestamp', 'comment_count'));
-
-    $query->join('forum_index', 'f', 'f.nid = ncs.nid');
-    $query->addField('f', 'tid', 'forum_tid');
-
-    $query->join('users', 'u', 'n.uid = u.uid');
-    $query->addField('u', 'name');
-
-    $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid');
-
-    $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name');
-
-    $query
-      ->orderBy('f.sticky', 'DESC')
-      ->orderByHeader($forum_topic_list_header)
-      ->condition('n.nid', $nids)
-      // @todo This should be actually filtering on the desired node language
-      //   and just fall back to the default language.
-      ->condition('n.default_langcode', 1);
-
-    $result = array();
-    foreach ($query->execute() as $row) {
-      $topic = $nodes[$row->nid];
-      $topic->comment_mode = $topic->comment;
-
-      foreach ($row as $key => $value) {
-        $topic->{$key} = $value;
-      }
-      $result[] = $topic;
-    }
-  }
-  else {
-    $result = array();
-  }
-
-  $topics = array();
-  $first_new_found = FALSE;
-  foreach ($result as $topic) {
-    if ($user->isAuthenticated()) {
-      // A forum is new if the topic is new, or if there are new comments since
-      // the user's last visit.
-      if ($topic->forum_tid != $tid) {
-        $topic->new = 0;
-      }
-      else {
-        $history = _forum_user_last_visit($topic->id());
-        $topic->new_replies = comment_num_new($topic->id(), $history);
-        $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history);
-      }
-    }
-    else {
-      // Do not track "new replies" status for topics if the user is anonymous.
-      $topic->new_replies = 0;
-      $topic->new = 0;
-    }
-
-    // Make sure only one topic is indicated as the first new topic.
-    $topic->first_new = FALSE;
-    if ($topic->new != 0 && !$first_new_found) {
-      $topic->first_new = TRUE;
-      $first_new_found = TRUE;
-    }
-
-    if ($topic->comment_count > 0) {
-      $last_reply = new stdClass();
-      $last_reply->created = $topic->last_comment_timestamp;
-      $last_reply->name = $topic->last_comment_name;
-      $last_reply->uid = $topic->last_comment_uid;
-      $topic->last_reply = $last_reply;
-    }
-    $topics[$topic->id()] = $topic;
-  }
-
-  return $topics;
-}
-
 /**
  * Implements hook_preprocess_HOOK() for block.html.twig.
  */
@@ -945,7 +617,7 @@ function forum_preprocess_block(&$variables) {
  *   - topics: An array of all the topics in the current forum.
  *   - parents: An array of taxonomy term objects that are ancestors of the
  *     current term ID.
- *   - tid: Taxonomy term ID of the current forum.
+ *   - term: Taxonomy term of the current forum.
  *   - sortby: One of the following integers indicating the sort criteria:
  *     - 1: Date - newest first.
  *     - 2: Date - oldest first.
@@ -954,6 +626,7 @@ function forum_preprocess_block(&$variables) {
  *   - forum_per_page: The maximum number of topics to display per page.
  */
 function template_preprocess_forums(&$variables) {
+  $variables['tid'] = $variables['term']->id();
   if ($variables['forums_defined'] = count($variables['forums']) || count($variables['parents'])) {
     if (!empty($variables['forums'])) {
       $variables['forums'] = array(
@@ -967,7 +640,7 @@ function template_preprocess_forums(&$variables) {
       $variables['forums'] = array();
     }
 
-    if ($variables['tid'] && array_search($variables['tid'], Drupal::config('forum.settings')->get('containers')) === FALSE) {
+    if ($variables['term'] && empty($variables['term']->forum_container->value)) {
       $variables['topics'] = array(
         '#theme' => 'forum_topic_list',
         '#tid' => $variables['tid'],
@@ -1024,7 +697,7 @@ function template_preprocess_forum_list(&$variables) {
     $variables['forums'][$id]->description = filter_xss_admin($forum->description->value);
     $variables['forums'][$id]->link = url("forum/" . $forum->id());
     $variables['forums'][$id]->name = check_plain($forum->label());
-    $variables['forums'][$id]->is_container = !empty($forum->container);
+    $variables['forums'][$id]->is_container = !empty($forum->forum_container->value);
     $variables['forums'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even';
     $row++;
 
@@ -1035,7 +708,7 @@ function template_preprocess_forum_list(&$variables) {
     $variables['forums'][$id]->icon_class = 'default';
     $variables['forums'][$id]->icon_title = t('No new posts');
     if ($user->isAuthenticated()) {
-      $variables['forums'][$id]->new_topics = _forum_topics_unread($forum->id(), $user->id());
+      $variables['forums'][$id]->new_topics = Drupal::service('forum_manager')->unreadTopics($forum->id(), $user->id());
       if ($variables['forums'][$id]->new_topics) {
         $variables['forums'][$id]->new_text = format_plural($variables['forums'][$id]->new_topics, '1 new post<span class="visually-hidden"> in forum %title</span>', '@count new posts<span class="visually-hidden"> in forum %title</span>', array('%title' => $variables['forums'][$id]->label()));
         $variables['forums'][$id]->new_url = url('forum/' . $forum->id(), array('fragment' => 'new'));
@@ -1071,12 +744,14 @@ function template_preprocess_forum_list(&$variables) {
 function template_preprocess_forum_topic_list(&$variables) {
   global $forum_topic_list_header;
 
-  // Create the tablesorting header.
-  $ts = tablesort_init($forum_topic_list_header);
   $header = '';
-  foreach ($forum_topic_list_header as $cell) {
-    $cell = tablesort_header($cell, $forum_topic_list_header, $ts);
-    $header .= _theme_table_cell($cell, TRUE);
+  if (!empty($forum_topic_list_header)) {
+    // Create the tablesorting header.
+    $ts = tablesort_init($forum_topic_list_header);
+    foreach ($forum_topic_list_header as $cell) {
+      $cell = tablesort_header($cell, $forum_topic_list_header, $ts);
+      $header .= _theme_table_cell($cell, TRUE);
+    }
   }
   $variables['header'] = $header;
 
@@ -1203,102 +878,6 @@ function template_preprocess_forum_submitted(&$variables) {
   $variables['time'] = isset($variables['topic']->created) ? format_interval(REQUEST_TIME - $variables['topic']->created) : '';
 }
 
-/**
- * Gets the last time the user viewed a node.
- *
- * @param $nid
- *   The node ID.
- *
- * @return
- *   The timestamp when the user last viewed this node, if the user has
- *   previously viewed the node; otherwise HISTORY_READ_LIMIT.
- */
-function _forum_user_last_visit($nid) {
-  global $user;
-  $history = &drupal_static(__FUNCTION__, array());
-
-  if (empty($history)) {
-    $result = db_query('SELECT nid, timestamp FROM {history} WHERE uid = :uid', array(':uid' => $user->id()));
-    foreach ($result as $t) {
-      $history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT;
-    }
-  }
-  return isset($history[$nid]) ? $history[$nid] : HISTORY_READ_LIMIT;
-}
-
-/**
- * Gets topic sorting information based on an integer code.
- *
- * @param $sortby
- *   One of the following integers indicating the sort criteria:
- *   - 1: Date - newest first.
- *   - 2: Date - oldest first.
- *   - 3: Posts with the most comments first.
- *   - 4: Posts with the least comments first.
- *
- * @return
- *   An array with the following values:
- *   - field: A field for an SQL query.
- *   - sort: 'asc' or 'desc'.
- */
-function _forum_get_topic_order($sortby) {
-  switch ($sortby) {
-    case 1:
-      return array('field' => 'f.last_comment_timestamp', 'sort' => 'desc');
-      break;
-    case 2:
-      return array('field' => 'f.last_comment_timestamp', 'sort' => 'asc');
-      break;
-    case 3:
-      return array('field' => 'f.comment_count', 'sort' => 'desc');
-      break;
-    case 4:
-      return array('field' => 'f.comment_count', 'sort' => 'asc');
-      break;
-  }
-}
-
-/**
- * Updates the taxonomy index for a given node.
- *
- * @param $nid
- *   The ID of the node to update.
- */
-function _forum_update_forum_index($nid) {
-  $count = db_query('SELECT COUNT(cid) FROM {comment} c INNER JOIN {forum_index} i ON c.nid = i.nid WHERE c.nid = :nid AND c.status = :status', array(
-    ':nid' => $nid,
-    ':status' => COMMENT_PUBLISHED,
-  ))->fetchField();
-
-  if ($count > 0) {
-    // Comments exist.
-    $last_reply = db_query_range('SELECT cid, name, created, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array(
-      ':nid' => $nid,
-      ':status' => COMMENT_PUBLISHED,
-    ))->fetchObject();
-    db_update('forum_index')
-      ->fields( array(
-        'comment_count' => $count,
-        'last_comment_timestamp' => $last_reply->created,
-      ))
-      ->condition('nid', $nid)
-      ->execute();
-  }
-  else {
-    // Comments do not exist.
-    // @todo This should be actually filtering on the desired node language and
-    //   just fall back to the default language.
-    $node = db_query('SELECT uid, created FROM {node_field_data} WHERE nid = :nid AND default_langcode = 1', array(':nid' => $nid))->fetchObject();
-    db_update('forum_index')
-      ->fields( array(
-        'comment_count' => 0,
-        'last_comment_timestamp' => $node->created,
-      ))
-      ->condition('nid', $nid)
-      ->execute();
-  }
-}
-
 /**
  * Returns HTML for a forum form.
  *
diff --git a/core/modules/forum/forum.pages.inc b/core/modules/forum/forum.pages.inc
deleted file mode 100644
index 3908f31799fe..000000000000
--- a/core/modules/forum/forum.pages.inc
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-/**
- * @file
- * User page callbacks for the Forum module.
- */
-
-/**
- * Page callback: Prints a forum listing.
- *
- * @param $forum_term
- *   A tree of all forums for a given taxonomy term ID. Defaults to NULL. See
- *   the return object of forum_forum_load() for a complete definition.
- *
- * @return
- *   A string containing HTML representing the themed forum listing.
- *
- * @see forum_menu()
- */
-function forum_page($forum_term = NULL) {
-  $config = Drupal::config('forum.settings');
-  $vocabulary = entity_load('taxonomy_vocabulary', $config->get('vocabulary'));
-
-  if (!isset($forum_term)) {
-    // On the main page, display all the top-level forums.
-    $forum_term = forum_forum_load(0);
-    // Set the page title to forum's vocabulary name.
-    drupal_set_title($vocabulary->label());
-  }
-
-  if ($forum_term->id() && array_search($forum_term->id(), $config->get('containers')) === FALSE) {
-    // Add RSS feed for forums.
-    drupal_add_feed('taxonomy/term/' . $forum_term->id() . '/feed', 'RSS - ' . $forum_term->label());
-  }
-
-  if (empty($forum_term->forums) && empty($forum_term->parents)) {
-    // Root of empty forum.
-    drupal_set_title(t('No forums defined'));
-  }
-
-  $forum_per_page = $config->get('topics.page_limit');
-  $sort_by = $config->get('topics.order');
-
-  if (empty($forum_term->container)) {
-    $topics = forum_get_topics($forum_term->id(), $sort_by, $forum_per_page);
-  }
-  else {
-    $topics = '';
-  }
-
-  $build = array(
-    '#theme' => 'forums',
-    '#forums' => $forum_term->forums,
-    '#topics' => $topics,
-    '#parents' => $forum_term->parents,
-    '#tid' => $forum_term->id(),
-    '#sortby' => $sort_by,
-    '#forums_per_page' => $forum_per_page,
-  );
-  $build['#attached']['css'][] = drupal_get_path('module', 'forum') . '/css/forum.module.css';
-  return $build;
-}
diff --git a/core/modules/forum/forum.routing.yml b/core/modules/forum/forum.routing.yml
index fe816f064fef..954e04af381a 100644
--- a/core/modules/forum/forum.routing.yml
+++ b/core/modules/forum/forum.routing.yml
@@ -4,12 +4,28 @@ forum_delete:
     _form: 'Drupal\forum\Form\DeleteForm'
   requirements:
     _permission: 'administer forums'
+
 forum_settings:
   pattern: '/admin/structure/forum/settings'
   defaults:
     _form: '\Drupal\forum\ForumSettingsForm'
   requirements:
     _permission: 'administer forums'
+
+forum_index:
+  pattern: '/forum'
+  defaults:
+    _content: 'Drupal\forum\Controller\ForumController::forumIndex'
+  requirements:
+    _permission: 'access content'
+
+forum_page:
+  pattern: '/forum/{taxonomy_term}'
+  defaults:
+    _content: 'Drupal\forum\Controller\ForumController::forumPage'
+  requirements:
+    _permission: 'access content'
+
 forum_add_container:
   pattern: 'admin/structure/forum/add/container'
   defaults:
diff --git a/core/modules/forum/forum.services.yml b/core/modules/forum/forum.services.yml
index e2467110ce79..8ea4dac2c79a 100644
--- a/core/modules/forum/forum.services.yml
+++ b/core/modules/forum/forum.services.yml
@@ -1,6 +1,9 @@
 services:
+  forum_manager:
+    class: Drupal\forum\ForumManager
+    arguments: ['@config.factory', '@plugin.manager.entity', '@database', '@field.info', '@string_translation']
   forum.breadcrumb:
     class: Drupal\forum\ForumBreadcrumbBuilder
-    arguments: ['@entity.manager', '@config.factory']
+    arguments: ['@entity.manager', '@config.factory', '@forum_manager']
     tags:
       - { name: breadcrumb_builder, priority: 1001 }
diff --git a/core/modules/forum/lib/Drupal/forum/Controller/ForumController.php b/core/modules/forum/lib/Drupal/forum/Controller/ForumController.php
index e9fec4b30dea..a0db5d2488eb 100644
--- a/core/modules/forum/lib/Drupal/forum/Controller/ForumController.php
+++ b/core/modules/forum/lib/Drupal/forum/Controller/ForumController.php
@@ -7,10 +7,15 @@
 
 namespace Drupal\forum\Controller;
 
-use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Config\Config;
+use Drupal\Core\Controller\ControllerInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\StringTranslation\TranslationManager;
+use Drupal\forum\ForumManagerInterface;
+use Drupal\taxonomy\TermInterface;
 use Drupal\taxonomy\TermStorageControllerInterface;
+use Drupal\taxonomy\VocabularyStorageControllerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -18,6 +23,13 @@
  */
 class ForumController implements ContainerInjectionInterface {
 
+  /**
+   * Forum manager service.
+   *
+   * @var \Drupal\forum\ForumManagerInterface
+   */
+  protected $forumManager;
+
   /**
    * Entity Manager Service.
    *
@@ -32,38 +44,142 @@ class ForumController implements ContainerInjectionInterface {
    */
   protected $config;
 
+  /**
+   * Vocabulary storage controller.
+   *
+   * @var \Drupal\taxonomy\VocabularyStorageControllerInterface
+   */
+  protected $vocabularyStorageController;
+
   /**
    * Term storage controller.
    *
    * @var \Drupal\taxonomy\TermStorageControllerInterface
    */
-  protected $storageController;
+  protected $termStorageController;
+
+  /**
+   * Translation manager service.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationManager
+   */
+  protected $translationManager;
+
+  /**
+   * Constructs a ForumController object.
+   *
+   * @param \Drupal\Core\Config\Config $config
+   *   Config object for forum.settings.
+   * @param \Drupal\forum\ForumManagerInterface $forum_manager
+   *   The forum manager service.
+   * @param \Drupal\taxonomy\VocabularyStorageControllerInterface $vocabulary_storage_controller
+   *   Vocabulary storage controller.
+   * @param \Drupal\taxonomy\TermStorageControllerInterface $term_storage_controller
+   *   Term storage controller.
+   * @param \Drupal\Core\Entity\EntityManager $entity_manager
+   *   The entity manager service.
+   * @param \Drupal\Core\StringTranslation\TranslationManager $translation_manager
+   *   The translation manager service.
+   */
+  public function __construct(Config $config, ForumManagerInterface $forum_manager, VocabularyStorageControllerInterface $vocabulary_storage_controller, TermStorageControllerInterface $term_storage_controller, EntityManager $entity_manager, TranslationManager $translation_manager) {
+    $this->config = $config;
+    $this->forumManager = $forum_manager;
+    $this->vocabularyStorageController = $vocabulary_storage_controller;
+    $this->termStorageController = $term_storage_controller;
+    $this->entityManager = $entity_manager;
+    $this->translationManager = $translation_manager;
+  }
 
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container) {
     return new static(
+      $container->get('config.factory')->get('forum.settings'),
+      $container->get('forum_manager'),
+      $container->get('entity.manager')->getStorageController('taxonomy_vocabulary'),
+      $container->get('entity.manager')->getStorageController('taxonomy_term'),
       $container->get('entity.manager'),
-      $container->get('config.factory'),
-      $container->get('entity.manager')->getStorageController('taxonomy_term')
+      $container->get('string_translation')
     );
   }
 
   /**
-   * Constructs a ForumController object.
+   * Returns forum page for a given forum.
    *
-   * @param \Drupal\Core\Entity\EntityManager $entity_manager
-   *   The entity manager service.
-   * @param \Drupal\Core\Config\ConfigFactory $config_factory
-   *   The factory for configuration objects.
-   * @param \Drupal\taxonomy\TermStorageControllerInterface $storage_controller
-   *   The term storage controller.
+   * @param \Drupal\taxonomy\TermInterface $taxonomy_term
+   *   The forum to render the page for.
+   *
+   * @return array
+   *   A render array.
    */
-  public function __construct(EntityManager $entity_manager, ConfigFactory $config_factory, TermStorageControllerInterface $storage_controller) {
-    $this->entityManager = $entity_manager;
-    $this->config = $config_factory->get('forum.settings');
-    $this->storageController = $storage_controller;
+  public function forumPage(TermInterface $taxonomy_term) {
+    // Get forum details.
+    $taxonomy_term->forums = $this->forumManager->getChildren($this->config->get('vocabulary'), $taxonomy_term->id());
+    $taxonomy_term->parents = $this->forumManager->getParents($taxonomy_term->id());
+    if (empty($taxonomy_term->forum_container->value)) {
+      // Add RSS feed for forums.
+      drupal_add_feed('taxonomy/term/' . $taxonomy_term->id() . '/feed', 'RSS - ' . $taxonomy_term->label());
+    }
+
+    if (empty($taxonomy_term->forum_container->value)) {
+      $topics = $this->forumManager->getTopics($taxonomy_term->id());
+    }
+    else {
+      $topics = '';
+    }
+    return $this->build($taxonomy_term->forums, $taxonomy_term, $topics, $taxonomy_term->parents);
+  }
+
+  /**
+   * Returns forum index page.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function forumIndex() {
+    $vocabulary = $this->vocabularyStorageController->load($this->config->get('vocabulary'));
+    $index = $this->forumManager->getIndex();
+    $build = $this->build($index->forums, $index);
+    if (empty($index->forums)) {
+      // Root of empty forum.
+      $build['#title'] = $this->translationManager->translate('No forums defined');
+    }
+    else {
+      // Set the page title to forum's vocabulary name.
+      $build['#title'] = $vocabulary->label();
+    }
+    return $build;
+  }
+
+  /**
+   * Returns a renderable forum index page array.
+   *
+   * @param array $forums
+   *   A list of forums.
+   * @param \Drupal\taxonomy\TermInterface $term
+   *   The taxonomy term of the forum.
+   * @param array $topics
+   *   The topics of this forum.
+   * @param array $parents
+   *   The parent forums in relation this forum.
+   *
+   * @return array
+   *   A render array.
+   */
+  protected function build($forums, TermInterface $term, $topics = array(), $parents = array()) {
+    $build = array(
+      '#theme' => 'forums',
+      '#forums' => $forums,
+      '#topics' => $topics,
+      '#parents' => $parents,
+      '#term' => $term,
+      '#sortby' => $this->config->get('topics.order'),
+      '#forums_per_page' => $this->config->get('topics.page_limit'),
+    );
+    // @todo Make this a library - see https://drupal.org/node/2028113.
+    $build['#attached']['css'][] = drupal_get_path('module', 'forum') . '/css/forum.module.css';
+    return $build;
   }
 
   /**
@@ -74,8 +190,9 @@ public function __construct(EntityManager $entity_manager, ConfigFactory $config
    */
   public function addForum() {
     $vid = $this->config->get('vocabulary');
-    $taxonomy_term = $this->storageController->create(array(
+    $taxonomy_term = $this->termStorageController->create(array(
       'vid' => $vid,
+      'forum_controller' => 0,
     ));
     return $this->entityManager->getForm($taxonomy_term, 'forum');
   }
@@ -88,8 +205,9 @@ public function addForum() {
    */
   public function addContainer() {
     $vid = $this->config->get('vocabulary');
-    $taxonomy_term = $this->storageController->create(array(
+    $taxonomy_term = $this->termStorageController->create(array(
       'vid' => $vid,
+      'forum_container' => 1,
     ));
     return $this->entityManager->getForm($taxonomy_term, 'container');
   }
diff --git a/core/modules/forum/lib/Drupal/forum/Form/ContainerFormController.php b/core/modules/forum/lib/Drupal/forum/Form/ContainerFormController.php
index 208d39c2f122..40f81998843a 100644
--- a/core/modules/forum/lib/Drupal/forum/Form/ContainerFormController.php
+++ b/core/modules/forum/lib/Drupal/forum/Form/ContainerFormController.php
@@ -42,14 +42,8 @@ public function form(array $form, array &$form_state) {
    */
   public function save(array $form, array &$form_state) {
     $is_new = $this->entity->isNew();
+    $this->entity->forum_container = TRUE;
     $term = parent::save($form, $form_state);
-    if ($is_new) {
-      // Update config item to track the container terms.
-      $config = $this->configFactory->get('forum.settings');
-      $containers = $config->get('containers');
-      $containers[] = $term->id();
-      $config->set('containers', $containers)->save();
-    }
   }
 
 }
diff --git a/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php b/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php
index c8d523c7325c..724baaf22168 100644
--- a/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php
+++ b/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php
@@ -10,6 +10,8 @@
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
 use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Entity\EntityManager;
+use Drupal\forum\ForumManagerInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 
 /**
  * Class to define the forum breadcrumb builder.
@@ -30,6 +32,13 @@ class ForumBreadcrumbBuilder implements BreadcrumbBuilderInterface {
    */
   protected $entityManager;
 
+  /**
+   * The forum manager service.
+   *
+   * @var \Drupal\forum\ForumManagerInterface
+   */
+  protected $forumManager;
+
   /**
    * Constructs a new ForumBreadcrumbBuilder.
    *
@@ -37,41 +46,34 @@ class ForumBreadcrumbBuilder implements BreadcrumbBuilderInterface {
    *   The entity manager.
    * @param \Drupal\Core\Config\ConfigFactory $configFactory
    *   The configuration factory.
+   * @param \Drupal\forum\ForumManagerInterface $forum_manager
+   *   The forum manager service.
    */
-  public function __construct(EntityManager $entity_manager, ConfigFactory $configFactory) {
+  public function __construct(EntityManager $entity_manager, ConfigFactory $configFactory, ForumManagerInterface $forum_manager) {
     $this->entityManager = $entity_manager;
     $this->config = $configFactory->get('forum.settings');
+    $this->forumManager = $forum_manager;
   }
 
   /**
    * {@inheritdoc}
    */
   public function build(array $attributes) {
-
     // @todo This only works for legacy routes. Once node/% and forum/% are
     //   converted to the new router this code will need to be updated.
-    if (isset($attributes['_drupal_menu_item'])) {
-      $item = $attributes['_drupal_menu_item'];
-      switch ($item['path']) {
-
-        case 'node/%':
-          $node = $item['map'][1];
-          // Load the object in case of missing wildcard loaders.
-          $node = is_object($node) ? $node : node_load($node);
-          if (_forum_node_check_node_type($node)) {
-            $breadcrumb = $this->forumPostBreadcrumb($node);
-          }
-          break;
-
-        case 'forum/%':
-          $term = $item['map'][1];
-          // Load the object in case of missing wildcard loaders.
-          $term = is_object($term) ? $term : forum_forum_load($term);
-          $breadcrumb = $this->forumTermBreadcrumb($term);
-          break;
+    if (isset($attributes['_drupal_menu_item']) && ($item = $attributes['_drupal_menu_item']) && $item['path'] == 'node/%') {
+      $node = $item['map'][1];
+      // Load the object in case of missing wildcard loaders.
+      $node = is_object($node) ? $node : node_load($node);
+      if ($this->forumManager->checkNodeType($node)) {
+        $breadcrumb = $this->forumPostBreadcrumb($node);
       }
     }
 
+    if (!empty($attributes[RouteObjectInterface::ROUTE_NAME]) && $attributes[RouteObjectInterface::ROUTE_NAME] == 'forum_page' && isset($attributes['taxonomy_term'])) {
+      $breadcrumb = $this->forumTermBreadcrumb($attributes['taxonomy_term']);
+    }
+
     if (!empty($breadcrumb)) {
       return $breadcrumb;
     }
diff --git a/core/modules/forum/lib/Drupal/forum/ForumManager.php b/core/modules/forum/lib/Drupal/forum/ForumManager.php
new file mode 100644
index 000000000000..0f03e635d409
--- /dev/null
+++ b/core/modules/forum/lib/Drupal/forum/ForumManager.php
@@ -0,0 +1,551 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\forum\ForumManager.
+ */
+
+namespace Drupal\forum;
+
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\field\FieldInfo;
+use Drupal\node\NodeInterface;
+
+/**
+ * Provides forum manager service.
+ */
+class ForumManager implements ForumManagerInterface {
+
+  /**
+   * Forum sort order, newest first.
+   */
+  const NEWEST_FIRST = 1;
+
+  /**
+   * Forum sort order, oldest first.
+   */
+  const OLDEST_FIRST = 2;
+
+  /**
+   * Forum sort order, posts with most comments first.
+   */
+  const MOST_POPULAR_FIRST = 3;
+
+  /**
+   * Forum sort order, posts with the least comments first.
+   */
+  const LEAST_POPULAR_FIRST = 4;
+
+  /**
+   * Forum settings config object.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $configFactory;
+
+  /**
+   * Entity manager service
+   *
+   * @var \Drupal\Core\Entity\EntityManager
+   */
+  protected $entityManager;
+
+  /**
+   * Database connection
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Array of last post information keyed by forum (term) id.
+   *
+   * @var array
+   */
+  protected $lastPostData = array();
+
+  /**
+   * Array of forum statistics keyed by forum (term) id.
+   *
+   * @var array
+   */
+  protected $forumStatistics = array();
+
+  /**
+   * Array of forum children keyed by parent forum (term) id.
+   *
+   * @var array
+   */
+  protected $forumChildren = array();
+
+  /**
+   * Array of history keyed by nid.
+   *
+   * @var array
+   */
+  protected $history = array();
+
+  /**
+   * Cached forum index.
+   *
+   * @var \Drupal\taxonomy\Plugin\Core\Entity\Term;
+   */
+  protected $index;
+
+  /**
+   * Field info service.
+   *
+   * @var \Drupal\field\FieldInfo
+   */
+  protected $fieldInfo;
+
+  /**
+   * Translation manager service.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface
+   */
+  protected $translationManager;
+
+  /**
+   * Constructs the forum manager service.
+   *
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The config factory service.
+   * @param \Drupal\Core\Entity\EntityManager $entity_manager
+   *   The entity manager service.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The current database connection.
+   * @param \Drupal\field\FieldInfo $field_info
+   *   The field info service.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
+   *   The translation manager service.
+   */
+  public function __construct(ConfigFactory $config_factory, EntityManager $entity_manager, Connection $connection, FieldInfo $field_info, TranslationInterface $translation_manager) {
+    $this->configFactory = $config_factory;
+    $this->entityManager = $entity_manager;
+    $this->connection = $connection;
+    $this->fieldInfo = $field_info;
+    $this->translationManager = $translation_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTopics($tid) {
+    $config = $this->configFactory->get('forum.settings');
+    $forum_per_page = $config->get('topics.page_limit');
+    $sortby = $config->get('topics.order');
+
+    global $user, $forum_topic_list_header;
+
+    $forum_topic_list_header = array(
+      array('data' => $this->translationManager->translate('Topic'), 'field' => 'f.title'),
+      array('data' => $this->translationManager->translate('Replies'), 'field' => 'f.comment_count'),
+      array('data' => $this->translationManager->translate('Last reply'), 'field' => 'f.last_comment_timestamp'),
+    );
+
+    $order = $this->getTopicOrder($sortby);
+    for ($i = 0; $i < count($forum_topic_list_header); $i++) {
+      if ($forum_topic_list_header[$i]['field'] == $order['field']) {
+        $forum_topic_list_header[$i]['sort'] = $order['sort'];
+      }
+    }
+
+    $query = $this->connection->select('forum_index', 'f')
+      ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
+      ->extend('Drupal\Core\Database\Query\TableSortExtender');
+    $query->fields('f');
+    $query
+      ->condition('f.tid', $tid)
+      ->addTag('node_access')
+      ->addMetaData('base_table', 'forum_index')
+      ->orderBy('f.sticky', 'DESC')
+      ->orderByHeader($forum_topic_list_header)
+      ->limit($forum_per_page);
+
+    $count_query = $this->connection->select('forum_index', 'f');
+    $count_query->condition('f.tid', $tid);
+    $count_query->addExpression('COUNT(*)');
+    $count_query->addTag('node_access');
+    $count_query->addMetaData('base_table', 'forum_index');
+
+    $query->setCountQuery($count_query);
+    $result = $query->execute();
+    $nids = array();
+    foreach ($result as $record) {
+      $nids[] = $record->nid;
+    }
+    if ($nids) {
+      $nodes = $this->entityManager->getStorageController('node')->loadMultiple($nids);
+
+      $query = $this->connection->select('node_field_data', 'n')
+        ->extend('Drupal\Core\Database\Query\TableSortExtender');
+      $query->fields('n', array('nid'));
+
+      $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
+      $query->fields('ncs', array(
+        'cid',
+        'last_comment_uid',
+        'last_comment_timestamp',
+        'comment_count'
+      ));
+
+      $query->join('forum_index', 'f', 'f.nid = ncs.nid');
+      $query->addField('f', 'tid', 'forum_tid');
+
+      $query->join('users', 'u', 'n.uid = u.uid');
+      $query->addField('u', 'name');
+
+      $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid');
+
+      $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name');
+
+      $query
+        ->orderBy('f.sticky', 'DESC')
+        ->orderByHeader($forum_topic_list_header)
+        ->condition('n.nid', $nids)
+        // @todo This should be actually filtering on the desired node language
+        //   and just fall back to the default language.
+        ->condition('n.default_langcode', 1);
+
+      $result = array();
+      foreach ($query->execute() as $row) {
+        $topic = $nodes[$row->nid];
+        $topic->comment_mode = $topic->comment;
+
+        foreach ($row as $key => $value) {
+          $topic->{$key} = $value;
+        }
+        $result[] = $topic;
+      }
+    }
+    else {
+      $result = array();
+    }
+
+    $topics = array();
+    $first_new_found = FALSE;
+    foreach ($result as $topic) {
+      if ($user->isAuthenticated()) {
+        // A forum is new if the topic is new, or if there are new comments since
+        // the user's last visit.
+        if ($topic->forum_tid != $tid) {
+          $topic->new = 0;
+        }
+        else {
+          $history = $this->lastVisit($topic->id());
+          $topic->new_replies = $this->numberNew($topic->id(), $history);
+          $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history);
+        }
+      }
+      else {
+        // Do not track "new replies" status for topics if the user is anonymous.
+        $topic->new_replies = 0;
+        $topic->new = 0;
+      }
+
+      // Make sure only one topic is indicated as the first new topic.
+      $topic->first_new = FALSE;
+      if ($topic->new != 0 && !$first_new_found) {
+        $topic->first_new = TRUE;
+        $first_new_found = TRUE;
+      }
+
+      if ($topic->comment_count > 0) {
+        $last_reply = new \stdClass();
+        $last_reply->created = $topic->last_comment_timestamp;
+        $last_reply->name = $topic->last_comment_name;
+        $last_reply->uid = $topic->last_comment_uid;
+        $topic->last_reply = $last_reply;
+      }
+      $topics[$topic->id()] = $topic;
+    }
+
+    return $topics;
+
+  }
+
+  /**
+   * Gets topic sorting information based on an integer code.
+   *
+   * @param int $sortby
+   *   One of the following integers indicating the sort criteria:
+   *   - ForumManager::NEWEST_FIRST: Date - newest first.
+   *   - ForumManager::OLDEST_FIRST: Date - oldest first.
+   *   - ForumManager::MOST_POPULAR_FIRST: Posts with the most comments first.
+   *   - ForumManager::LEAST_POPULAR_FIRST: Posts with the least comments first.
+   *
+   * @return array
+   *   An array with the following values:
+   *   - field: A field for an SQL query.
+   *   - sort: 'asc' or 'desc'.
+   */
+  protected function getTopicOrder($sortby) {
+    switch ($sortby) {
+      case static::NEWEST_FIRST:
+        return array('field' => 'f.last_comment_timestamp', 'sort' => 'desc');
+
+      case static::OLDEST_FIRST:
+        return array('field' => 'f.last_comment_timestamp', 'sort' => 'asc');
+
+      case static::MOST_POPULAR_FIRST:
+        return array('field' => 'f.comment_count', 'sort' => 'desc');
+
+      case static::LEAST_POPULAR_FIRST:
+        return array('field' => 'f.comment_count', 'sort' => 'asc');
+
+    }
+  }
+
+  /**
+   * Wraps comment_num_new() in a method.
+   *
+   * @param int $nid
+   *   Node ID.
+   * @param int $timestamp
+   *   Timestamp of last read.
+   *
+   * @return int
+   *   Number of new comments.
+   */
+  protected function numberNew($nid, $timestamp) {
+    return comment_num_new($nid, $timestamp);
+  }
+
+  /**
+   * Gets the last time the user viewed a node.
+   *
+   * @param int $nid
+   *   The node ID.
+   *
+   * @return int
+   *   The timestamp when the user last viewed this node, if the user has
+   *   previously viewed the node; otherwise HISTORY_READ_LIMIT.
+   */
+  protected function lastVisit($nid) {
+    global $user;
+
+    if (empty($this->history[$nid])) {
+      $result = $this->connection->select('history', 'h')
+        ->fields('h', array('nid', 'timestamp'))
+        ->condition('uid', $user->id())
+        ->execute();
+      foreach ($result as $t) {
+        $this->history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT;
+      }
+    }
+    return isset($this->history[$nid]) ? $this->history[$nid] : HISTORY_READ_LIMIT;
+  }
+
+  /**
+   * Provides the last post information for the given forum tid.
+   *
+   * @param int $tid
+   *   The forum tid.
+   *
+   * @return \stdClass
+   *   The last post for the given forum.
+   */
+  protected function getLastPost($tid) {
+    if (!empty($this->lastPostData[$tid])) {
+      return $this->lastPostData[$tid];
+    }
+    // Query "Last Post" information for this forum.
+    $query = $this->connection->select('node_field_data', 'n');
+    $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $tid));
+    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
+    $query->join('users', 'u', 'ncs.last_comment_uid = u.uid');
+    $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u.name END', 'last_comment_name');
+
+    $topic = $query
+      ->fields('ncs', array('last_comment_timestamp', 'last_comment_uid'))
+      ->condition('n.status', 1)
+      ->orderBy('last_comment_timestamp', 'DESC')
+      ->range(0, 1)
+      ->addTag('node_access')
+      ->execute()
+      ->fetchObject();
+
+    // Build the last post information.
+    $last_post = new \stdClass();
+    if (!empty($topic->last_comment_timestamp)) {
+      $last_post->created = $topic->last_comment_timestamp;
+      $last_post->name = $topic->last_comment_name;
+      $last_post->uid = $topic->last_comment_uid;
+    }
+
+    $this->lastPostData[$tid] = $last_post;
+    return $last_post;
+  }
+
+  /**
+   * Provides statistics for a forum.
+   *
+   * @param int $tid
+   *   The forum tid.
+   *
+   * @return \stdClass|null
+   *   Statistics for the given forum if statistics exist, else NULL.
+   */
+  protected function getForumStatistics($tid) {
+    if (empty($this->forumStatistics)) {
+      // Prime the statistics.
+      $query = $this->connection->select('node_field_data', 'n');
+      $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
+      $query->join('forum', 'f', 'n.vid = f.vid');
+      $query->addExpression('COUNT(n.nid)', 'topic_count');
+      $query->addExpression('SUM(ncs.comment_count)', 'comment_count');
+      $this->forumStatistics = $query
+        ->fields('f', array('tid'))
+        ->condition('n.status', 1)
+        ->condition('n.default_langcode', 1)
+        ->groupBy('tid')
+        ->addTag('node_access')
+        ->execute()
+        ->fetchAllAssoc('tid');
+    }
+
+    if (!empty($this->forumStatistics[$tid])) {
+      return $this->forumStatistics[$tid];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChildren($vid, $tid) {
+    if (!empty($this->forumChildren[$tid])) {
+      return $this->forumChildren[$tid];
+    }
+    $forums = array();
+    $_forums = taxonomy_get_tree($vid, $tid, NULL, TRUE);
+    foreach ($_forums as $forum) {
+      // Merge in the topic and post counters.
+      if (($count = $this->getForumStatistics($forum->id()))) {
+        $forum->num_topics = $count->topic_count;
+        $forum->num_posts = $count->topic_count + $count->comment_count;
+      }
+      else {
+        $forum->num_topics = 0;
+        $forum->num_posts = 0;
+      }
+
+      // Merge in last post details.
+      $forum->last_post = $this->getLastPost($forum->id());
+      $forums[$forum->id()] = $forum;
+    }
+
+    $this->forumChildren[$tid] = $forums;
+    return $forums;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIndex() {
+    if ($this->index) {
+      return $this->index;
+    }
+
+    $vid = $this->configFactory->get('forum.settings')->get('vocabulary');
+    $index = $this->entityManager->getStorageController('taxonomy_term')->create(array(
+      'tid' => 0,
+      'container' => 1,
+      'parents' => array(),
+      'isIndex' => TRUE,
+      'vid' => $vid
+    ));
+
+    // Load the tree below.
+    $index->forums = $this->getChildren($vid, 0);
+    $this->index = $index;
+    return $index;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetCache() {
+    // Reset the index.
+    $this->index = NULL;
+    // Reset history.
+    $this->history = NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParents($tid) {
+    return taxonomy_term_load_parents_all($tid);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkNodeType(NodeInterface $node) {
+    // Fetch information about the forum field.
+    $instances = $this->fieldInfo->getBundleInstances('node', $node->bundle());
+    return !empty($instances['taxonomy_forums']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function unreadTopics($term, $uid) {
+    $query = $this->connection->select('node_field_data', 'n');
+    $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $term));
+    $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', array(':uid' => $uid));
+    $query->addExpression('COUNT(n.nid)', 'count');
+    return $query
+      ->condition('status', 1)
+      // @todo This should be actually filtering on the desired node status
+      //   field language and just fall back to the default language.
+      ->condition('n.default_langcode', 1)
+      ->condition('n.created', HISTORY_READ_LIMIT, '>')
+      ->isNull('h.nid')
+      ->addTag('node_access')
+      ->execute()
+      ->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateIndex($nid) {
+    $count = $this->connection->query('SELECT COUNT(cid) FROM {comment} c INNER JOIN {forum_index} i ON c.nid = i.nid WHERE c.nid = :nid AND c.status = :status', array(
+      ':nid' => $nid,
+      ':status' => COMMENT_PUBLISHED,
+    ))->fetchField();
+
+    if ($count > 0) {
+      // Comments exist.
+      $last_reply = $this->connection->queryRange('SELECT cid, name, created, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array(
+        ':nid' => $nid,
+        ':status' => COMMENT_PUBLISHED,
+      ))->fetchObject();
+      $this->connection->update('forum_index')
+        ->fields( array(
+          'comment_count' => $count,
+          'last_comment_timestamp' => $last_reply->created,
+        ))
+        ->condition('nid', $nid)
+        ->execute();
+    }
+    else {
+      // Comments do not exist.
+      // @todo This should be actually filtering on the desired node language and
+      //   just fall back to the default language.
+      $node = $this->connection->query('SELECT uid, created FROM {node_field_data} WHERE nid = :nid AND default_langcode = 1', array(':nid' => $nid))->fetchObject();
+      $this->connection->update('forum_index')
+        ->fields( array(
+          'comment_count' => 0,
+          'last_comment_timestamp' => $node->created,
+        ))
+        ->condition('nid', $nid)
+        ->execute();
+    }
+  }
+}
diff --git a/core/modules/forum/lib/Drupal/forum/ForumManagerInterface.php b/core/modules/forum/lib/Drupal/forum/ForumManagerInterface.php
new file mode 100644
index 000000000000..b8896ce90147
--- /dev/null
+++ b/core/modules/forum/lib/Drupal/forum/ForumManagerInterface.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\forum\ForumManagerInterface.
+ */
+
+namespace Drupal\forum;
+
+use Drupal\node\NodeInterface;
+
+/**
+ * Provides forum manager interface.
+ */
+interface ForumManagerInterface {
+
+  /**
+   * Gets list of forum topics.
+   *
+   * @param int $tid
+   *   Term ID.
+   *
+   * @return array
+   *   Array of topics.
+   */
+  public function getTopics($tid);
+
+  /**
+   * Utility method to fetch the child forums for a given forum.
+   *
+   * @param int $vid
+   *   The forum vocabulary ID.
+   * @param int $tid
+   *   The forum ID to fetch the children for.
+   *
+   * @return array
+   *   Array of children.
+   */
+  public function getChildren($vid, $tid);
+
+  /**
+   * Generates and returns the forum index.
+   *
+   * The forum index is a pseudo term that provides an overview of all forums.
+   *
+   * @return \Drupal\taxonomy\TermInterface
+   *   A pseudo term representing the overview of all forums.
+   */
+  public function getIndex();
+
+  /**
+   * Resets the ForumManager index and history.
+   */
+  public function resetCache();
+
+  /**
+   * Protected function to wrap call to taxonomy_term_load_parents_all.
+   *
+   * @param int $tid
+   *   Term ID.
+   *
+   * @return array
+   *   Array of parent terms.
+   *
+   * @todo remove and inject a service when taxonomy_term_get_parents_all has an
+   *   object-oriented equivalent.
+   */
+  public function getParents($tid);
+
+  /**
+   * Checks whether a node can be used in a forum, based on its content type.
+   *
+   * @param \Drupal\node\NodeInterface $node
+   *   A node entity.
+   *
+   * @return bool
+   *   Boolean indicating if the node can be assigned to a forum.
+   */
+  public function checkNodeType(NodeInterface $node);
+
+  /**
+   * Calculates the number of new posts in a forum that the user has not yet read.
+   *
+   * Nodes are new if they are newer than HISTORY_READ_LIMIT.
+   *
+   * @param int $term
+   *   The term ID of the forum.
+   * @param int $uid
+   *   The user ID.
+   *
+   * @return
+   *   The number of new posts in the forum that have not been read by the user.
+   */
+  public function unreadTopics($term, $uid);
+
+  /**
+   * Updates the taxonomy index for a given node.
+   *
+   * @param int $nid
+   *   The ID of the node to update.
+   */
+  public function updateIndex($nid);
+
+}
diff --git a/core/modules/forum/lib/Drupal/forum/Tests/ForumTest.php b/core/modules/forum/lib/Drupal/forum/Tests/ForumTest.php
index 466ab8b5c285..ff4d712afcdd 100644
--- a/core/modules/forum/lib/Drupal/forum/Tests/ForumTest.php
+++ b/core/modules/forum/lib/Drupal/forum/Tests/ForumTest.php
@@ -190,7 +190,7 @@ function testForum() {
     $this->assertEqual($topics, '6', 'Number of topics found.');
 
     // Verify the number of unread topics.
-    $unread_topics = _forum_topics_unread($this->forum['tid'], $this->edit_any_topics_user->id());
+    $unread_topics = $this->container->get('forum_manager')->unreadTopics($this->forum['tid'], $this->edit_any_topics_user->id());
     $unread_topics = format_plural($unread_topics, '1 new post', '@count new posts');
     $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="topics"]//a', $forum_arg);
     $this->assertFieldByXPath($xpath, $unread_topics, 'Number of unread topics found.');
@@ -417,6 +417,8 @@ function createForum($type, $parent = 0) {
     $parent_tid = db_query("SELECT t.parent FROM {taxonomy_term_hierarchy} t WHERE t.tid = :tid", array(':tid' => $tid))->fetchField();
     $this->assertTrue($parent == $parent_tid, 'The ' . $type . ' is linked to its container');
 
+    $forum = $this->container->get('plugin.manager.entity')->getStorageController('taxonomy_term')->load($tid);
+    $this->assertEqual(($type == 'forum container'), (bool) $forum->forum_container->value);
     return $term;
   }
 
@@ -437,11 +439,6 @@ function deleteForum($tid) {
     // Assert that the forum no longer exists.
     $this->drupalGet('forum/' . $tid);
     $this->assertResponse(404, 'The forum was not found');
-
-    // Assert that the associated term has been removed from the
-    // forum_containers variable.
-    $containers = \Drupal::config('forum.settings')->get('containers');
-    $this->assertFalse(in_array($tid, $containers), 'The forum_containers variable has been updated.');
   }
 
   /**
diff --git a/core/modules/forum/tests/Drupal/forum/Tests/ForumManagerTest.php b/core/modules/forum/tests/Drupal/forum/Tests/ForumManagerTest.php
new file mode 100644
index 000000000000..96675fb4ae65
--- /dev/null
+++ b/core/modules/forum/tests/Drupal/forum/Tests/ForumManagerTest.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\forum\Tests\ForumManagerTest.
+ */
+
+namespace Drupal\forum\Tests;
+
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the ForumManager.
+ *
+ * @see \Drupal\forum\ForumManager
+ */
+class ForumManagerTest extends UnitTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Forum Manager',
+      'description' => 'Tests the forum manager functionality.',
+      'group' => 'Forum',
+    );
+  }
+
+  /**
+   * Tests ForumManager::getIndex().
+   */
+  public function testGetIndex() {
+    $entity_manager = $this->getMockBuilder('\Drupal\Core\Entity\EntityManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $storage_controller = $this->getMockBuilder('\Drupal\taxonomy\VocabularyStorageController')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $config_factory = $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $config = $this->getMockBuilder('\Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $config_factory->expects($this->once())
+      ->method('get')
+      ->will($this->returnValue($config));
+
+    $config->expects($this->once())
+      ->method('get')
+      ->will($this->returnValue('forums'));
+
+    $entity_manager->expects($this->once())
+      ->method('getStorageController')
+      ->will($this->returnValue($storage_controller));
+
+    // This is sufficient for testing purposes.
+    $term = new \stdClass();
+
+    $storage_controller->expects($this->once())
+      ->method('create')
+      ->will($this->returnValue($term));
+
+    $connection = $this->getMockBuilder('\Drupal\Core\Database\Connection')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $translation_manager = $this->getMockBuilder('\Drupal\Core\StringTranslation\TranslationManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $field_info = $this->getMockBuilder('\Drupal\field\FieldInfo')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $manager = $this->getMock('\Drupal\forum\ForumManager', array('getChildren'), array(
+      $config_factory,
+      $entity_manager,
+      $connection,
+      $field_info,
+      $translation_manager,
+    ));
+
+    $manager->expects($this->once())
+      ->method('getChildren')
+      ->will($this->returnValue(array()));
+
+    // Get the index once.
+    $index1 = $manager->getIndex();
+
+    // Get it again. This should not return the previously generated index. If
+    // it does not, then the test will fail as the mocked methods will be called
+    // more than once.
+    $index2 = $manager->getIndex();
+
+    $this->assertEquals($index1, $index2);
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/ForumUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/ForumUpgradePathTest.php
new file mode 100644
index 000000000000..5edc67c50974
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/ForumUpgradePathTest.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Upgrade\ForumUpgradePathTest.
+ */
+
+namespace Drupal\system\Tests\Upgrade;
+
+/**
+ * Tests upgrading a filled database with forum data.
+ *
+ * Loads a filled installation of Drupal 7 with forums and containers and runs
+ * the upgrade process on it.
+ */
+class ForumUpgradePathTest extends UpgradePathTestBase {
+
+  public static function getInfo() {
+    return array(
+      'name'  => 'Forum upgrade test',
+      'description'  => 'Upgrade tests with forum data.',
+      'group' => 'Upgrade path',
+    );
+  }
+
+  public function setUp() {
+    $path = drupal_get_path('module', 'system') . '/tests/upgrade';
+    $this->databaseDumpFiles = array(
+      $path . '/drupal-7.bare.standard_all.database.php.gz',
+      $path . '/drupal-7.forum.database.php',
+    );
+    parent::setUp();
+  }
+
+  /**
+   * Tests expected forum and container conversions after a successful upgrade.
+   */
+  public function testForumUpgrade() {
+    $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.');
+
+    // Make sure the field is created.
+    $vocabulary = $this->container->get('config.factory')->get('forum.settings')->get('vocabulary');
+    $field = field_info_instance('taxonomy_term', 'forum_container', $vocabulary);
+    $this->assertTrue((bool) $field, 'Field was found');
+
+    // Check that the values of forum_container are correct.
+    $containers = entity_load_multiple_by_properties('taxonomy_term', array('name' => 'Container'));
+    $container = reset($containers);
+    $this->assertTrue((bool) $container->forum_container->value);
+
+    $forums = entity_load_multiple_by_properties('taxonomy_term', array('name' => 'Forum'));
+    $forum = reset($forums);
+    $this->assertFalse((bool) $forum->forum_container->value);
+  }
+
+}
diff --git a/core/modules/system/tests/upgrade/drupal-7.forum.database.php b/core/modules/system/tests/upgrade/drupal-7.forum.database.php
new file mode 100644
index 000000000000..b6e6cf8db4cf
--- /dev/null
+++ b/core/modules/system/tests/upgrade/drupal-7.forum.database.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Database additions for user picture tests. Used in UserPictureUpgradePathTest.
+ *
+ * This dump only contains data and schema components relevant for user picture
+ * functionality. The drupal-7.bare.database.php file is imported before
+ * this dump, so the two form the database structure expected in tests
+ * altogether.
+ */
+
+// Create two terms.
+$vocabulary = db_select('taxonomy_vocabulary', 'tv')
+  ->fields('tv', array('vid'))
+  ->condition('name', 'forums')
+  ->execute()
+  ->fetchField();
+
+$container = db_insert('taxonomy_term_data')
+  ->fields(array(
+    'vid' => $vocabulary,
+    'name' => 'Container',
+    'description' => 'Container',
+    'format' => 'full_html',
+    'weight' => 0,
+  ))
+  ->execute();
+
+$forum = db_insert('taxonomy_term_data')
+  ->fields(array(
+    'vid' => $vocabulary,
+    'name' => 'Forum',
+    'description' => 'Forum',
+    'format' => 'full_html',
+    'weight' => 0,
+  ))
+  ->execute();
+
+db_delete('variable')
+  ->condition('name', 'forum_containers')
+  ->execute();
+
+db_insert('variable')->fields(array(
+  'name' => 'forum_containers',
+  'value' => serialize(array($container)),
+))
+->execute();
-- 
GitLab