diff --git a/composer.json b/composer.json
index d94ede2..f58b5af 100644
--- a/composer.json
+++ b/composer.json
@@ -26,10 +26,10 @@
     "zendframework/zend-feed": "2.2.*"
   },
   "autoload": {
-    "psr-0": {
-      "Drupal\\Core": "core/lib/",
-      "Drupal\\Component": "core/lib/",
-      "Drupal\\Driver": "drivers/lib/"
+    "psr-4": {
+      "Drupal\\Core\\": "core/lib/Drupal/Core",
+      "Drupal\\Component\\": "core/lib/Drupal/Component",
+      "Drupal\\Driver\\": "drivers/lib/Drupal/Driver"
     },
     "files": [
       "core/lib/Drupal.php"
diff --git a/core/config/install/core.extension.yml b/core/config/install/core.extension.yml
index eae39ef..1514a9e 100644
--- a/core/config/install/core.extension.yml
+++ b/core/config/install/core.extension.yml
@@ -1,5 +1,4 @@
 module: {}
-theme:
-  stark: 0
+theme: {}
 disabled:
   theme: {}
diff --git a/core/core.services.yml b/core/core.services.yml
index 3f7c394..9b9c80b 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -203,7 +203,7 @@ services:
     arguments: ['%container.modules%', '@cache.bootstrap']
   theme_handler:
     class: Drupal\Core\Extension\ThemeHandler
-    arguments: ['@config.factory', '@module_handler', '@cache.default', '@info_parser', '@config.installer', '@router.builder']
+    arguments: ['@config.factory', '@module_handler', '@state', '@info_parser', '@config.installer', '@router.builder']
   entity.manager:
     class: Drupal\Core\Entity\EntityManager
     arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation']
@@ -499,8 +499,6 @@ services:
   csrf_token:
     class: Drupal\Core\Access\CsrfTokenGenerator
     arguments: ['@private_key']
-    calls:
-      - [setCurrentUser, ['@?current_user']]
   access_manager:
     class: Drupal\Core\Access\AccessManager
     arguments: ['@router.route_provider', '@url_generator', '@paramconverter_manager']
@@ -652,7 +650,7 @@ services:
     arguments: ['@module_handler']
   token:
     class: Drupal\Core\Utility\Token
-    arguments: ['@module_handler']
+    arguments: ['@module_handler', '@cache.discovery', '@language_manager']
   batch.storage:
     class: Drupal\Core\Batch\BatchStorage
     arguments: ['@database']
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index d6e6aee..83aa307 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -2124,7 +2124,10 @@ function drupal_classloader($class_loader = NULL) {
  */
 function drupal_classloader_register($name, $path) {
   $loader = drupal_classloader();
-  $loader->add('Drupal\\' . $name, DRUPAL_ROOT . '/' . $path . '/lib');
+  $loader->addPsr4('Drupal\\' . $name . '\\', array(
+    DRUPAL_ROOT . '/' . $path . '/lib/Drupal/' . $name,
+    DRUPAL_ROOT . '/' . $path . '/src',
+  ));
 }
 
 /**
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 46a9356..107e515 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2893,20 +2893,17 @@ function drupal_get_token($value = '') {
  *   The token to be validated.
  * @param string $value
  *   An additional value to base the token on.
- * @param bool $skip_anonymous
- *   Set to true to skip token validation for anonymous users.
  *
  * @return bool
- *   True for a valid token, false for an invalid token. When $skip_anonymous
- *   is true, the return value will always be true for anonymous users.
+ *   True for a valid token, false for an invalid token.
  *
  * @see \Drupal\Core\Access\CsrfTokenGenerator
  *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
  *   Use return \Drupal::csrfToken()->validate().
  */
-function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) {
-  return \Drupal::csrfToken()->validate($token, $value, $skip_anonymous);
+function drupal_valid_token($token, $value = '') {
+  return \Drupal::csrfToken()->validate($token, $value);
 }
 
 /**
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 9f8088a..d0795b8 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1,11 +1,7 @@
 <?php
 
 use Drupal\Component\Utility\UrlHelper;
-use Drupal\Component\Utility\UserAgent;
-use Drupal\Component\Utility\Crypt;
-
 use Drupal\Component\Utility\Settings;
-use Drupal\Core\Config\FileStorage;
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
@@ -674,6 +670,7 @@ function install_tasks($install_state) {
       // we still need to display it if settings.php is invalid in any way,
       // since the form submit handler is where settings.php is rewritten.
       'run' => $install_state['settings_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED,
+      'function' => 'Drupal\Core\Installer\Form\SiteSettingsForm',
     ),
     'install_base_system' => array(
       'run' => $install_state['base_system_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED,
@@ -686,6 +683,8 @@ function install_tasks($install_state) {
       'display_name' => t('Install site'),
       'type' => 'batch',
     ),
+    'install_profile_themes' => array(
+    ),
     'install_import_translations' => array(
       'display_name' => t('Set up translations'),
       'display' => $needs_translations,
@@ -695,6 +694,7 @@ function install_tasks($install_state) {
     'install_configure_form' => array(
       'display_name' => t('Configure site'),
       'type' => 'form',
+      'function' => 'Drupal\Core\Installer\Form\SiteConfigureForm',
     ),
   );
 
@@ -809,8 +809,9 @@ function install_get_form($form_id, array &$install_state) {
     ),
     'no_redirect' => TRUE,
   );
+  $form_builder = \Drupal::formBuilder();
   if ($install_state['interactive']) {
-    $form = drupal_build_form($form_id, $form_state);
+    $form = $form_builder->buildForm($form_id, $form_state);
     // If the form submission was not successful, the form needs to be rendered,
     // which means the task is not complete yet.
     if (empty($form_state['executed'])) {
@@ -822,13 +823,14 @@ function install_get_form($form_id, array &$install_state) {
     // For non-interactive installs, submit the form programmatically with the
     // values taken from the installation state.
     $form_state['values'] = array();
-    if (!empty($install_state['forms'][$form_id])) {
-      $form_state['values'] = $install_state['forms'][$form_id];
+    $install_form_id = $form_builder->getFormId($form_id, $form_state);
+    if (!empty($install_state['forms'][$install_form_id])) {
+      $form_state['values'] = $install_state['forms'][$install_form_id];
     }
-    drupal_form_submit($form_id, $form_state);
+    $form_builder->submitForm($form_id, $form_state);
 
     // Throw an exception in case of any form validation error.
-    if ($errors = form_get_errors($form_state)) {
+    if ($errors = $form_builder->getErrors($form_state)) {
       throw new InstallerException(implode("\n", $errors));
     }
   }
@@ -1015,117 +1017,6 @@ function install_verify_database_settings() {
 }
 
 /**
- * Form constructor for a form to configure and rewrite settings.php.
- *
- * @param $install_state
- *   An array of information about the current installation state.
- *
- * @see install_settings_form_validate()
- * @see install_settings_form_submit()
- * @ingroup forms
- */
-function install_settings_form($form, &$form_state, &$install_state) {
-  global $databases;
-
-  $conf_path = './' . conf_path(FALSE);
-  $settings_file = $conf_path . '/settings.php';
-
-  $form['#title'] = t('Database configuration');
-
-  $drivers = drupal_get_database_types();
-  $drivers_keys = array_keys($drivers);
-
-  // If database connection settings have been prepared in settings.php already,
-  // then the existing values need to be taken over.
-  // Note: The installer even executes this form if there is a valid database
-  // connection already, since the submit handler of this form is responsible
-  // for writing all $settings to settings.php (not limited to $databases).
-  if (isset($databases['default']['default'])) {
-    $default_driver = $databases['default']['default']['driver'];
-    $default_options = $databases['default']['default'];
-  }
-  // Otherwise, use the database connection settings from the form input.
-  // For a non-interactive installation, this is derived from the original
-  // $settings array passed into install_drupal().
-  elseif (isset($form_state['input']['driver'])) {
-    $default_driver = $form_state['input']['driver'];
-    $default_options = $form_state['input'][$default_driver];
-  }
-  // If there is no database information at all yet, just suggest the first
-  // available driver as default value, so that its settings form is made
-  // visible via #states when JavaScript is enabled (see below).
-  else {
-    $default_driver = current($drivers_keys);
-    $default_options = array();
-  }
-
-  $form['driver'] = array(
-    '#type' => 'radios',
-    '#title' => t('Database type'),
-    '#required' => TRUE,
-    '#default_value' => $default_driver,
-  );
-  if (count($drivers) == 1) {
-    $form['driver']['#disabled'] = TRUE;
-  }
-
-  // Add driver specific configuration options.
-  foreach ($drivers as $key => $driver) {
-    $form['driver']['#options'][$key] = $driver->name();
-
-    $form['settings'][$key] = $driver->getFormOptions($default_options);
-    $form['settings'][$key]['#prefix'] = '<h2 class="js-hide">' . t('@driver_name settings', array('@driver_name' => $driver->name())) . '</h2>';
-    $form['settings'][$key]['#type'] = 'container';
-    $form['settings'][$key]['#tree'] = TRUE;
-    $form['settings'][$key]['advanced_options']['#parents'] = array($key);
-    $form['settings'][$key]['#states'] = array(
-      'visible' => array(
-        ':input[name=driver]' => array('value' => $key),
-      )
-    );
-  }
-
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['save'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save and continue'),
-    '#button_type' => 'primary',
-    '#limit_validation_errors' => array(
-      array('driver'),
-      array($default_driver),
-    ),
-    '#submit' => array('install_settings_form_submit'),
-  );
-
-  $form['errors'] = array();
-  $form['settings_file'] = array('#type' => 'value', '#value' => $settings_file);
-
-  return $form;
-}
-
-/**
- * Form validation handler for install_settings_form().
- *
- * @see install_settings_form_submit()
- */
-function install_settings_form_validate($form, &$form_state) {
-  $driver = $form_state['values']['driver'];
-  $database = $form_state['values'][$driver];
-  $drivers = drupal_get_database_types();
-  $reflection = new \ReflectionClass($drivers[$driver]);
-  $install_namespace = $reflection->getNamespaceName();
-  // Cut the trailing \Install from namespace.
-  $database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\'));
-  $database['driver'] = $driver;
-
-  $form_state['storage']['database'] = $database;
-  $errors = install_database_errors($database, $form_state['values']['settings_file']);
-  foreach ($errors as $name => $message) {
-    form_set_error($name, $form_state, $message);
-  }
-}
-
-/**
  * Checks a database connection and returns any errors.
  */
 function install_database_errors($database, $settings_file) {
@@ -1167,46 +1058,6 @@ function install_database_errors($database, $settings_file) {
 }
 
 /**
- * Form submission handler for install_settings_form().
- *
- * @see install_settings_form_validate()
- */
-function install_settings_form_submit($form, &$form_state) {
-  global $install_state;
-
-  // Update global settings array and save.
-  $settings = array();
-  $database = $form_state['storage']['database'];
-  $settings['databases']['default']['default'] = (object) array(
-    'value'    => $database,
-    'required' => TRUE,
-  );
-  $settings['settings']['hash_salt'] = (object) array(
-    'value'    => Crypt::randomBytesBase64(55),
-    'required' => TRUE,
-  );
-  // Remember the profile which was used.
-  $settings['settings']['install_profile'] = (object) array(
-    'value' => $install_state['parameters']['profile'],
-    'required' => TRUE,
-  );
-
-  drupal_rewrite_settings($settings);
-
-  // Add the config directories to settings.php.
-  drupal_install_config_directories();
-
-  // Indicate that the settings file has been verified, and check the database
-  // for the last completed task, now that we have a valid connection. This
-  // last step is important since we want to trigger an error if the new
-  // database already has Drupal installed.
-  $install_state['settings_verified'] = TRUE;
-  $install_state['config_verified'] = TRUE;
-  $install_state['database_verified'] = TRUE;
-  $install_state['completed_task'] = install_verify_completed_task();
-}
-
-/**
  * Selects which profile to install.
  *
  * @param $install_state
@@ -1235,7 +1086,7 @@ function install_select_profile(&$install_state) {
         throw new InstallerException(t('Missing profile parameter.'));
       }
       // Otherwise, display a form to select a profile.
-      return install_get_form('install_select_profile_form', $install_state);
+      return install_get_form('Drupal\Core\Installer\Form\SelectProfileForm', $install_state);
     }
   }
 }
@@ -1282,85 +1133,6 @@ function _install_select_profile(&$install_state) {
 }
 
 /**
- * Form constructor for the profile selection form.
- *
- * @param array $install_state
- *   An array of information about the current installation state.
- *
- * @ingroup forms
- */
-function install_select_profile_form($form, &$form_state, $install_state) {
-  $form['#title'] = t('Select an installation profile');
-
-  $profiles = array();
-  $names = array();
-  foreach ($install_state['profiles'] as $profile) {
-    $details = install_profile_info($profile->getName());
-    // Skip this extension if its type is not profile.
-    if (!isset($details['type']) || $details['type'] != 'profile') {
-      continue;
-    }
-    // Don't show hidden profiles. This is used by to hide the testing profile,
-    // which only exists to speed up test runs.
-    if ($details['hidden'] === TRUE) {
-      continue;
-    }
-    $profiles[$profile->getName()] = $details;
-
-    // Determine the name of the profile; default to file name if defined name
-    // is unspecified.
-    $name = isset($details['name']) ? $details['name'] : $profile->getName();
-    $names[$profile->getName()] = $name;
-  }
-
-  // Display radio buttons alphabetically by human-readable name, but always
-  // put the core profiles first (if they are present in the filesystem).
-  natcasesort($names);
-  if (isset($names['minimal'])) {
-    // If the expert ("Minimal") core profile is present, put it in front of
-    // any non-core profiles rather than including it with them alphabetically,
-    // since the other profiles might be intended to group together in a
-    // particular way.
-    $names = array('minimal' => $names['minimal']) + $names;
-  }
-  if (isset($names['standard'])) {
-    // If the default ("Standard") core profile is present, put it at the very
-    // top of the list. This profile will have its radio button pre-selected,
-    // so we want it to always appear at the top.
-    $names = array('standard' => $names['standard']) + $names;
-  }
-
-  // The profile name and description are extracted for translation from the
-  // .info file, so we can use t() on them even though they are dynamic data
-  // at this point.
-  $form['profile'] = array(
-    '#type' => 'radios',
-    '#title' => t('Select an installation profile'),
-    '#title_display' => 'invisible',
-    '#options' => array_map('t', $names),
-    '#default_value' => 'standard',
-  );
-  foreach (array_keys($names) as $profile) {
-    $form['profile'][$profile]['#description'] = isset($profiles[$profile]['description']) ? t($profiles[$profile]['description']) : '';
-  }
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] =  array(
-    '#type' => 'submit',
-    '#value' => t('Save and continue'),
-    '#button_type' => 'primary',
-  );
-  return $form;
-}
-
-/**
- * Form submission handler for install_select_profile_form().
- */
-function install_select_profile_form_submit($form, &$form_state) {
-  global $install_state;
-  $install_state['parameters']['profile'] = $form_state['values']['profile'];
-}
-
-/**
  * Finds all .po files that are useful to the installer.
  *
  * @return
@@ -1427,7 +1199,7 @@ function install_select_language(&$install_state) {
     // translation files were found the form shows a select list of the
     // corresponding languages to choose from.
     if ($install_state['interactive']) {
-      return install_get_form('install_select_language_form', $install_state);
+      return install_get_form('Drupal\Core\Installer\Form\SelectLanguageForm', $install_state);
     }
     // If we are performing a non-interactive installation. If only one language
     // (English) is available, assume the user knows what he is doing. Otherwise
@@ -1445,88 +1217,6 @@ function install_select_language(&$install_state) {
 }
 
 /**
- * Form constructor for the language selection form.
- *
- * @param $install_state
- *   An array of information about the current installation state.
- *
- * @see file_scan_directory()
- * @ingroup forms
- */
-function install_select_language_form($form, &$form_state, &$install_state) {
-  if (count($install_state['translations']) > 1) {
-    $files = $install_state['translations'];
-  }
-  else {
-    $files = array();
-  }
-  $standard_languages = LanguageManager::getStandardLanguageList();
-  $select_options = array();
-  $browser_options = array();
-
-  $form['#title'] = t('Choose language');
-
-  // Build a select list with language names in native language for the user
-  // to choose from. And build a list of available languages for the browser
-  // to select the language default from.
-  if (count($files)) {
-    // Select lists based on available language files.
-    foreach ($files as $langcode => $uri) {
-      $select_options[$langcode] = isset($standard_languages[$langcode]) ? $standard_languages[$langcode][1] : $langcode;
-      $browser_options[] = $langcode;
-    }
-  }
-  else {
-    // Select lists based on all standard languages.
-    foreach ($standard_languages as $langcode => $language_names) {
-      $select_options[$langcode] = $language_names[1];
-      $browser_options[] = $langcode;
-    }
-  }
-
-  $request = Request::createFromGlobals();
-  $browser_langcode = UserAgent::getBestMatchingLangcode($request->server->get('HTTP_ACCEPT_LANGUAGE'), $browser_options);
-  $form['langcode'] = array(
-    '#type' => 'select',
-    '#title' => t('Choose language'),
-    '#title_display' => 'invisible',
-    '#options' => $select_options,
-    // Use the browser detected language as default or English if nothing found.
-    '#default_value' => !empty($browser_langcode) ? $browser_langcode : 'en',
-  );
-
-  if (empty($files)) {
-    $form['help'] = array(
-      '#type' => 'item',
-      '#markup' => \Drupal\Component\Utility\String::format('<p>Translations will be downloaded from the <a href="http://localize.drupal.org">Drupal Translation website</a>.
-        If you do not want this, select <a href="!english">English</a>.</p>', array(
-          '!english' => install_full_redirect_url(array('parameters' => array('langcode' => 'en'))),
-      )),
-      '#states' => array(
-        'invisible' => array(
-          'select[name="langcode"]' => array('value' => 'en'),
-        ),
-      ),
-    );
-  }
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] =  array(
-    '#type' => 'submit',
-    '#value' => t('Save and continue'),
-    '#button_type' => 'primary',
-  );
-  return $form;
-}
-
-/**
- * Form submission handler for the language selection form.
- */
-function install_select_language_form_submit($form, &$form_state) {
-  $install_state = &$form_state['build_info']['args'][0];
-  $install_state['parameters']['langcode'] = $form_state['values']['langcode'];
-}
-
-/**
  * Download a translation file for the selected langaguage.
  *
  * @param array $install_state
@@ -1828,6 +1518,34 @@ function install_profile_modules(&$install_state) {
 }
 
 /**
+ * Installs themes.
+ *
+ * This does not use a batch, since installing themes is faster than modules and
+ * because an installation profile typically enables 1-3 themes only (default
+ * theme, base theme, admin theme).
+ *
+ * @param $install_state
+ *   An array of information about the current installation state.
+ */
+function install_profile_themes(&$install_state) {
+  $theme_handler = \Drupal::service('theme_handler');
+
+  // ThemeHandler::enable() resets the current list of themes. The theme used in
+  // the installer is not necessarily in the list of themes to install, so
+  // retain the current list.
+  // @see _drupal_maintenance_theme()
+  $current_themes = $theme_handler->listInfo();
+
+  // Install the themes specified by the installation profile.
+  $themes = $install_state['profile_info']['themes'];
+  $theme_handler->enable($themes);
+
+  foreach ($current_themes as $theme) {
+    $theme_handler->addTheme($theme);
+  }
+}
+
+/**
  * Imports languages via a batch process during installation.
  *
  * @param $install_state
@@ -1914,55 +1632,6 @@ function _install_prepare_import($langcode) {
 }
 
 /**
- * Form constructor for a form to configure the new site.
- *
- * @param $install_state
- *   An array of information about the current installation state.
- *
- * @see install_configure_form_validate()
- * @see install_configure_form_submit()
- * @ingroup forms
- */
-function install_configure_form($form, &$form_state, &$install_state) {
-  $form['#title'] = t('Configure site');
-
-  // Warn about settings.php permissions risk
-  $settings_dir = conf_path();
-  $settings_file = $settings_dir . '/settings.php';
-  // Check that $_POST is empty so we only show this message when the form is
-  // first displayed, not on the next page after it is submitted. (We do not
-  // want to repeat it multiple times because it is a general warning that is
-  // not related to the rest of the installation process; it would also be
-  // especially out of place on the last page of the installer, where it would
-  // distract from the message that the Drupal installation has completed
-  // successfully.)
-  $post_params = \Drupal::request()->request->all();
-  if (empty($post_params) && (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_file, FILE_EXIST|FILE_READABLE|FILE_NOT_WRITABLE) || !drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_dir, FILE_NOT_WRITABLE, 'dir'))) {
-    drupal_set_message(t('All necessary changes to %dir and %file have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the <a href="@handbook_url">online handbook</a>.', array('%dir' => $settings_dir, '%file' => $settings_file, '@handbook_url' => 'http://drupal.org/server-permissions')), 'warning');
-  }
-
-  $form['#attached']['library'][] = 'system/drupal.system';
-  // Add JavaScript time zone detection.
-  $form['#attached']['library'][] = 'core/drupal.timezone';
-  // We add these strings as settings because JavaScript translation does not
-  // work during installation.
-  $js = array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail')));
-  $form['#attached']['js'][] = array('data' => $js, 'type' => 'setting');
-
-  // Cache a fully-built schema. This is necessary for any invocation of
-  // index.php because: (1) setting cache table entries requires schema
-  // information, (2) that occurs during bootstrap before any module are
-  // loaded, so (3) if there is no cached schema, drupal_get_schema() will
-  // try to generate one but with no loaded modules will return nothing.
-  //
-  // @todo Move this to the 'install_finished' task?
-  drupal_get_schema(NULL, TRUE);
-
-  // Return the form.
-  return _install_configure_form($form, $form_state, $install_state);
-}
-
-/**
  * Finishes importing files at end of installation.
  *
  * If other projects besides Drupal core have been installed, their translation
@@ -2412,170 +2081,3 @@ function install_display_requirements($install_state, $requirements) {
     }
   }
 }
-
-/**
- * Form constructor for a site configuration form.
- *
- * @param $install_state
- *   An array of information about the current installation state.
- *
- * @see install_configure_form()
- * @see install_configure_form_validate()
- * @see install_configure_form_submit()
- * @ingroup forms
- */
-function _install_configure_form($form, &$form_state, &$install_state) {
-  $form['site_information'] = array(
-    '#type' => 'fieldgroup',
-    '#title' => t('Site information'),
-  );
-  $form['site_information']['site_name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Site name'),
-    '#required' => TRUE,
-    '#weight' => -20,
-  );
-  $form['site_information']['site_mail'] = array(
-    '#type' => 'email',
-    '#title' => t('Site e-mail address'),
-    '#default_value' => ini_get('sendmail_from'),
-    '#description' => t("Automated e-mails, such as registration information, will be sent from this address. Use an address ending in your site's domain to help prevent these e-mails from being flagged as spam."),
-    '#required' => TRUE,
-    '#weight' => -15,
-  );
-
-  $form['admin_account'] = array(
-    '#type' => 'fieldgroup',
-    '#title' => t('Site maintenance account'),
-  );
-  $form['admin_account']['account']['#tree'] = TRUE;
-  $form['admin_account']['account']['name'] = array('#type' => 'textfield',
-    '#title' => t('Username'),
-    '#maxlength' => USERNAME_MAX_LENGTH,
-    '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, and underscores.'),
-    '#required' => TRUE,
-    '#weight' => -10,
-    '#attributes' => array('class' => array('username')),
-  );
-  $form['admin_account']['account']['mail'] = array(
-    '#type' => 'email',
-    '#title' => t('E-mail address'),
-    '#required' => TRUE,
-    '#weight' => -5,
-  );
-  $form['admin_account']['account']['pass'] = array(
-    '#type' => 'password_confirm',
-    '#required' => TRUE,
-    '#size' => 25,
-    '#weight' => 0,
-  );
-
-  $form['regional_settings'] = array(
-    '#type' => 'fieldgroup',
-    '#title' => t('Regional settings'),
-  );
-  $countries = \Drupal::service('country_manager')->getList();
-  $form['regional_settings']['site_default_country'] = array(
-    '#type' => 'select',
-    '#title' => t('Default country'),
-    '#empty_value' => '',
-    '#default_value' => \Drupal::config('system.date')->get('country.default'),
-    '#options' => $countries,
-    '#description' => t('Select the default country for the site.'),
-    '#weight' => 0,
-  );
-  $form['regional_settings']['date_default_timezone'] = array(
-    '#type' => 'select',
-    '#title' => t('Default time zone'),
-    '#default_value' => date_default_timezone_get(),
-    '#options' => system_time_zones(),
-    '#description' => t('By default, dates in this site will be displayed in the chosen time zone.'),
-    '#weight' => 5,
-    '#attributes' => array('class' => array('timezone-detect')),
-  );
-
-  $form['update_notifications'] = array(
-    '#type' => 'fieldgroup',
-    '#title' => t('Update notifications'),
-  );
-  $form['update_notifications']['update_status_module'] = array(
-    '#type' => 'checkboxes',
-    '#title' => t('Update notifications'),
-    '#options' => array(
-      1 => t('Check for updates automatically'),
-      2 => t('Receive e-mail notifications'),
-    ),
-    '#default_value' => array(1, 2),
-    '#description' => t('The system will notify you when updates and important security releases are available for installed components. Anonymous information about your site is sent to <a href="@drupal">Drupal.org</a>.', array('@drupal' => 'http://drupal.org')),
-    '#weight' => 15,
-  );
-  $form['update_notifications']['update_status_module'][2] = array(
-    '#states' => array(
-      'visible' => array(
-        'input[name="update_status_module[1]"]' => array('checked' => TRUE),
-      ),
-    ),
-  );
-
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save and continue'),
-    '#weight' => 15,
-    '#button_type' => 'primary',
-  );
-
-  return $form;
-}
-
-/**
- * Form validation handler for install_configure_form().
- *
- * @see install_configure_form_submit()
- */
-function install_configure_form_validate($form, &$form_state) {
-  if ($error = user_validate_name($form_state['values']['account']['name'])) {
-    form_error($form['admin_account']['account']['name'], $form_state, $error);
-  }
-}
-
-/**
- * Form submission handler for install_configure_form().
- *
- * @see install_configure_form_validate()
- */
-function install_configure_form_submit($form, &$form_state) {
-  \Drupal::config('system.site')
-    ->set('name', $form_state['values']['site_name'])
-    ->set('mail', $form_state['values']['site_mail'])
-    ->save();
-
-  \Drupal::config('system.date')
-    ->set('timezone.default', $form_state['values']['date_default_timezone'])
-    ->set('country.default', $form_state['values']['site_default_country'])
-    ->save();
-
-  // Enable update.module if this option was selected.
-  if ($form_state['values']['update_status_module'][1]) {
-    \Drupal::moduleHandler()->install(array('file', 'update'), FALSE);
-
-    // Add the site maintenance account's email address to the list of
-    // addresses to be notified when updates are available, if selected.
-    if ($form_state['values']['update_status_module'][2]) {
-      \Drupal::config('update.settings')->set('notification.emails', array($form_state['values']['account']['mail']))->save();
-    }
-  }
-
-  // We precreated user 1 with placeholder values. Let's save the real values.
-  $account = user_load(1);
-  $account->init = $account->mail = $form_state['values']['account']['mail'];
-  $account->roles = $account->getRoles();
-  $account->activate();
-  $account->timezone = $form_state['values']['date_default_timezone'];
-  $account->pass = $form_state['values']['account']['pass'];
-  $account->name = $form_state['values']['account']['name'];
-  $account->save();
-
-  // Record when this install ran.
-  \Drupal::state()->set('install_time', $_SERVER['REQUEST_TIME']);
-}
diff --git a/core/includes/install.inc b/core/includes/install.inc
index 3789ff3..ac6e00e 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -1077,6 +1077,7 @@ function install_profile_info($profile, $langcode = 'en') {
     // Set defaults for module info.
     $defaults = array(
       'dependencies' => array(),
+      'themes' => array('stark'),
       'description' => '',
       'version' => NULL,
       'hidden' => FALSE,
diff --git a/core/includes/module.inc b/core/includes/module.inc
index 18de4fc..cbbe811 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -9,26 +9,18 @@
 use Drupal\Core\Extension\ExtensionDiscovery;
 
 /**
- * Builds a list of bootstrap modules and enabled modules and themes.
+ * Builds a list of enabled themes.
  *
  * @param $type
  *   The type of list to return:
- *   - module_enabled: All enabled modules.
- *   - bootstrap: All enabled modules required for bootstrap.
- *   - theme: All themes.
+ *   - theme: All enabled themes.
  *
  * @return
- *   An associative array of modules or themes, keyed by name. For $type
- *   'bootstrap' and 'module_enabled', the array values equal the keys.
+ *   An associative array of themes, keyed by name.
  *   For $type 'theme', the array values are objects representing the
  *   respective database row, with the 'info' property already unserialized.
  *
  * @see list_themes()
- *
- * @todo There are too many layers/levels of caching involved for system_list()
- *   data. Consider to add a \Drupal::config($name, $cache = TRUE) argument to allow
- *   callers like system_list() to force-disable a possible configuration
- *   storage cache or some other way to circumvent it/take it over.
  */
 function system_list($type) {
   $lists = &drupal_static(__FUNCTION__);
@@ -40,32 +32,15 @@ function system_list($type) {
       'theme' => array(),
       'filepaths' => array(),
     );
-    // Build a list of themes.
-    $enabled_themes = \Drupal::config('core.extension')->get('theme') ?: array();
-    // @todo Themes include all themes, including disabled/uninstalled. This
-    //   system.theme.data state will go away entirely as soon as themes have
-    //   a proper installation status.
-    // @see http://drupal.org/node/1067408
-    $theme_data = \Drupal::state()->get('system.theme.data');
-    if (empty($theme_data)) {
-      // @todo: system_list() may be called from _drupal_bootstrap_code(), in
-      // which case system.module is not loaded yet.
-      // Prevent a filesystem scan in drupal_load() and include it directly.
-      // @see http://drupal.org/node/1067408
-      require_once DRUPAL_ROOT . '/core/modules/system/system.module';
-      $theme_data = system_rebuild_theme_data();
-    }
+    // ThemeHandler maintains the 'system.theme.data' state record.
+    $theme_data = \Drupal::state()->get('system.theme.data', array());
     foreach ($theme_data as $name => $theme) {
-      $theme->status = (int) isset($enabled_themes[$name]);
       $lists['theme'][$name] = $theme;
-      // Build a list of filenames so drupal_get_filename can use it.
-      if (isset($enabled_themes[$name])) {
-        $lists['filepaths'][] = array(
-          'type' => 'theme',
-          'name' => $name,
-          'filepath' => $theme->getPathname(),
-        );
-      }
+      $lists['filepaths'][] = array(
+        'type' => 'theme',
+        'name' => $name,
+        'filepath' => $theme->getPathname(),
+      );
     }
     \Drupal::cache('bootstrap')->set('system_list', $lists);
   }
@@ -84,25 +59,17 @@ function system_list($type) {
 function system_list_reset() {
   drupal_static_reset('system_list');
   drupal_static_reset('system_rebuild_module_data');
-  drupal_static_reset('list_themes');
   \Drupal::cache('bootstrap')->delete('system_list');
-  \Drupal::cache()->delete('system_info');
 
   // Clear the library info cache.
   // Libraries may be provided by all extension types, and may be altered by any
   // other extensions (types) due to the nature of
   // \Drupal\Core\Extension\ModuleHandler::alter() and the fact that profiles
   // are recorded and handled as modules.
+  // @todo Trigger an event upon module install/uninstall and theme
+  //   enable/disable, and move this into an event subscriber.
+  // @see https://drupal.org/node/2206347
   Cache::invalidateTags(array('extension' => TRUE));
-
-  // Remove last known theme data state.
-  // This causes system_list() to call system_rebuild_theme_data() on its next
-  // invocation. When enabling a module that implements hook_system_info_alter()
-  // to inject a new (testing) theme or manipulate an existing theme, then that
-  // will cause system_list_reset() to be called, but theme data is not
-  // necessarily rebuilt afterwards.
-  // @todo Obsolete with proper installation status for themes.
-  \Drupal::state()->delete('system.theme.data');
 }
 
 /**
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index d45c2e5..dc3570b 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -103,7 +103,21 @@ function drupal_theme_initialize() {
   // Determine the active theme for the theme negotiator service. This includes
   // the default theme as well as really specific ones like the ajax base theme.
   $request = \Drupal::request();
-  $theme = \Drupal::service('theme.negotiator')->determineActiveTheme($request) ?: 'stark';
+  $theme = \Drupal::service('theme.negotiator')->determineActiveTheme($request);
+
+  // If no theme could be negotiated, or if the negotiated theme is not within
+  // the list of enabled themes, fall back to the default theme output of core
+  // and modules (similar to Stark, but without a theme extension at all). This
+  // is possible, because _drupal_theme_initialize() always loads the Twig theme
+  // engine.
+  if (!$theme || !isset($themes[$theme])) {
+    $theme = 'core';
+    $theme_key = $theme;
+    // /core/core.info.yml does not actually exist, but is required because
+    // Extension expects a pathname.
+    _drupal_theme_initialize(new Extension('theme', 'core/core.info.yml'));
+    return;
+  }
 
   // Store the identifier for retrieving theme settings with.
   $theme_key = $theme;
@@ -401,6 +415,8 @@ function _theme($hook, $variables = array()) {
   if (!$module_handler->isLoaded() && !defined('MAINTENANCE_MODE')) {
     throw new Exception(t('_theme() may not be called until all modules are loaded.'));
   }
+  // Ensure the theme is initialized.
+  drupal_theme_initialize();
 
   /** @var \Drupal\Core\Utility\ThemeRegistry $theme_registry */
   $theme_registry = \Drupal::service('theme.registry')->getRuntime();
@@ -851,8 +867,8 @@ function theme_get_setting($setting_name, $theme = NULL) {
 
     // Get the values for the theme-specific settings from the .info.yml files
     // of the theme and all its base themes.
-    if ($theme) {
-      $themes = list_themes();
+    $themes = list_themes();
+    if ($theme && isset($themes[$theme])) {
       $theme_object = $themes[$theme];
 
       // Create a list which includes the current theme and all its base themes.
@@ -874,7 +890,7 @@ function theme_get_setting($setting_name, $theme = NULL) {
     // Get the global settings from configuration.
     $cache[$theme]->merge(\Drupal::config('system.theme.global')->get());
 
-    if ($theme) {
+    if ($theme && isset($themes[$theme])) {
       // Retrieve configured theme-specific settings, if any.
       try {
         if ($theme_settings = \Drupal::config($theme . '.settings')->get()) {
@@ -975,13 +991,16 @@ function theme_settings_convert_to_config(array $theme_settings, Config $config)
  * @param $theme_list
  *   An array of theme names.
  *
+ * @return bool
+ *   Whether any of the given themes have been enabled.
+ *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
  *   Use \Drupal::service('theme_handler')->enable().
  *
  * @see \Drupal\Core\Extension\ThemeHandler::enable().
  */
 function theme_enable($theme_list) {
-  \Drupal::service('theme_handler')->enable($theme_list);
+  return \Drupal::service('theme_handler')->enable($theme_list);
 }
 
 /**
@@ -990,13 +1009,16 @@ function theme_enable($theme_list) {
  * @param $theme_list
  *   An array of theme names.
  *
+ * @return bool
+ *   Whether any of the given themes have been disabled.
+ *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
  *   Use \Drupal::service('theme_handler')->disable().
  *
  * @see \Drupal\Core\Extension\ThemeHandler::disable().
  */
 function theme_disable($theme_list) {
-  \Drupal::service('theme_handler')->disable($theme_list);
+  return \Drupal::service('theme_handler')->disable($theme_list);
 }
 
 /**
diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc
index 1cd436c..a79adca 100644
--- a/core/includes/theme.maintenance.inc
+++ b/core/includes/theme.maintenance.inc
@@ -66,7 +66,7 @@ function _drupal_maintenance_theme() {
   }
 
   // Ensure that system.module is loaded.
-  if (!function_exists('_system_rebuild_theme_data')) {
+  if (!function_exists('system_rebuild_theme_data')) {
     $module_handler = \Drupal::moduleHandler();
     $module_handler->addModule('system', 'core/modules/system');
     $module_handler->load('system');
@@ -74,6 +74,14 @@ function _drupal_maintenance_theme() {
 
   $themes = list_themes();
 
+  // If no themes are installed yet, or if the requested custom theme is not
+  // installed, retrieve all available themes.
+  if (empty($themes) || !isset($themes[$custom_theme])) {
+    $theme_handler = \Drupal::service('theme_handler');
+    $themes = $theme_handler->rebuildThemeData();
+    $theme_handler->addTheme($themes[$custom_theme]);
+  }
+
   // list_themes() triggers a \Drupal\Core\Extension\ModuleHandler::alter() in
   // maintenance mode, but we can't let themes alter the .info.yml data until
   // we know a theme's base themes. So don't set global $theme until after
diff --git a/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php b/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php
index b04e39d..b1c0dbf 100644
--- a/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php
+++ b/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php
@@ -99,7 +99,6 @@ public function getDefinitions() {
     // Search for classes within all PSR-0 namespace locations.
     foreach ($this->getPluginNamespaces() as $namespace => $dirs) {
       foreach ($dirs as $dir) {
-        $dir .= DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $namespace);
         if (file_exists($dir)) {
           $iterator = new \RecursiveIteratorIterator(
             new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
diff --git a/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php b/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php
index 2e4e44d..9918610 100644
--- a/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php
+++ b/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php
@@ -26,13 +26,6 @@ class CsrfTokenGenerator {
   protected $privateKey;
 
   /**
-   * The current user.
-   *
-   * @var \Drupal\Core\Session\AccountInterface
-   */
-  protected $currentUser;
-
-  /**
    * Constructs the token generator.
    *
    * @param \Drupal\Core\PrivateKey $private_key
@@ -43,16 +36,6 @@ public function __construct(PrivateKey $private_key) {
   }
 
   /**
-   * Sets the current user.
-   *
-   * @param \Drupal\Core\Session\AccountInterface|null $current_user
-   *  The current user service.
-   */
-  public function setCurrentUser(AccountInterface $current_user = NULL) {
-    $this->currentUser = $current_user;
-  }
-
-  /**
    * Generates a token based on $value, the user session, and the private key.
    *
    * The generated token is based on the session ID of the current user. Normally,
@@ -82,15 +65,12 @@ public function get($value = '') {
    *   The token to be validated.
    * @param string $value
    *   (optional) An additional value to base the token on.
-   * @param bool $skip_anonymous
-   *   (optional) Set to TRUE to skip token validation for anonymous users.
    *
    * @return bool
-   *   TRUE for a valid token, FALSE for an invalid token. When $skip_anonymous
-   *   is TRUE, the return value will always be TRUE for anonymous users.
+   *   TRUE for a valid token, FALSE for an invalid token.
    */
-  public function validate($token, $value = '', $skip_anonymous = FALSE) {
-    return ($skip_anonymous && $this->currentUser->isAnonymous()) || ($token === $this->get($value));
+  public function validate($token, $value = '') {
+    return $token === $this->get($value);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php
index e81b29b..bdedf90 100644
--- a/core/lib/Drupal/Core/Config/ConfigImporter.php
+++ b/core/lib/Drupal/Core/Config/ConfigImporter.php
@@ -499,11 +499,11 @@ public function import() {
    *   Exception thrown if the $sync_step can not be called.
    */
   public function doSyncStep($sync_step, &$context) {
-    if (method_exists($this, $sync_step)) {
+    if (!is_array($sync_step) && method_exists($this, $sync_step)) {
       $this->$sync_step($context);
     }
     elseif (is_callable($sync_step)) {
-      call_user_func_array($sync_step, array(&$context));
+      call_user_func_array($sync_step, array(&$context, $this));
     }
     else {
       throw new \InvalidArgumentException('Invalid configuration synchronization step');
@@ -549,7 +549,7 @@ public function initialize() {
     $sync_steps[] = 'processConfigurations';
 
     // Allow modules to add new steps to configuration synchronization.
-    $this->moduleHandler->alter('config_import_steps', $sync_steps);
+    $this->moduleHandler->alter('config_import_steps', $sync_steps, $this);
     $sync_steps[] = 'finish';
     return $sync_steps;
   }
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
index 1fd73e7..a9b6d97 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -17,8 +17,6 @@
 use Drupal\Core\Config\StorageInterface;
 use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\EntityStorageException;
-use Drupal\Core\Entity\Query\QueryFactory;
 use Drupal\Component\Uuid\UuidInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -56,11 +54,9 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
   const MAX_ID_LENGTH = 166;
 
   /**
-   * The UUID service.
-   *
-   * @var \Drupal\Component\Uuid\UuidInterface
+   * {@inheritdoc}
    */
-  protected $uuidService;
+  protected $uuidKey = 'uuid';
 
   /**
    * Name of the entity's status key or FALSE if a status is not supported.
@@ -107,7 +103,6 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
   public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service, LanguageManagerInterface $language_manager) {
     parent::__construct($entity_type);
 
-    $this->idKey = $this->entityType->getKey('id');
     $this->statusKey = $this->entityType->getKey('status');
 
     $this->configFactory = $config_factory;
@@ -130,53 +125,6 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function loadMultiple(array $ids = NULL) {
-    $entities = array();
-
-    // Create a new variable which is either a prepared version of the $ids
-    // array for later comparison with the entity cache, or FALSE if no $ids
-    // were passed.
-    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
-
-    // Load any remaining entities. This is the case if $ids is set to NULL (so
-    // we load all entities).
-    if ($ids === NULL || $ids) {
-      $queried_entities = $this->buildQuery($ids);
-    }
-
-    // Pass all entities loaded from the database through $this->postLoad(),
-    // which calls the
-    // entity type specific load callback, for example hook_node_type_load().
-    if (!empty($queried_entities)) {
-      $this->postLoad($queried_entities);
-      $entities += $queried_entities;
-    }
-
-    // Ensure that the returned array is ordered the same as the original
-    // $ids array if this was passed in and remove any invalid ids.
-    if ($passed_ids) {
-      // Remove any invalid ids from the array.
-      $passed_ids = array_intersect_key($passed_ids, $entities);
-      foreach ($entities as $entity) {
-        $passed_ids[$entity->id()] = $entity;
-      }
-      $entities = $passed_ids;
-    }
-
-    return $entities;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function load($id) {
-    $entities = $this->loadMultiple(array($id));
-    return isset($entities[$id]) ? $entities[$id] : NULL;
-  }
-
-  /**
    * Implements Drupal\Core\Entity\EntityStorageInterface::loadRevision().
    */
   public function loadRevision($revision_id) {
@@ -205,28 +153,9 @@ public static function getIDFromConfigName($config_name, $config_prefix) {
   }
 
   /**
-   * Builds the query to load the entity.
-   *
-   * This has full revision support. For entities requiring special queries,
-   * the class can be extended, and the default query can be constructed by
-   * calling parent::buildQuery(). This is usually necessary when the object
-   * being loaded needs to be augmented with additional data from another
-   * table, such as loading node type into comments or vocabulary machine name
-   * into terms, however it can also support $conditions on different tables.
-   * See Drupal\comment\CommentStorage::buildQuery() or
-   * Drupal\taxonomy\TermStorage::buildQuery() for examples.
-   *
-   * @param $ids
-   *   An array of entity IDs, or NULL to load all entities.
-   * @param $revision_id
-   *   The ID of the revision to load, or FALSE if this query is asking for the
-   *   most current revision(s).
-   *
-   * @return SelectQuery
-   *   A SelectQuery object for loading the entity.
+   * {@inheritdoc}
    */
-  protected function buildQuery($ids, $revision_id = FALSE) {
-    $config_class = $this->entityType->getClass();
+  protected function doLoadMultiple(array $ids = NULL) {
     $prefix = $this->getConfigPrefix();
 
     // Get the names of the configuration entities we are going to load.
@@ -242,70 +171,36 @@ protected function buildQuery($ids, $revision_id = FALSE) {
     }
 
     // Load all of the configuration entities.
-    $result = array();
+    $records = array();
     foreach ($this->configFactory->loadMultiple($names) as $config) {
-      $result[$config->get($this->idKey)] = new $config_class($config->get(), $this->entityTypeId);
+      $records[$config->get($this->idKey)] = $config->get();
     }
-    return $result;
+    return $this->mapFromStorageRecords($records);
   }
 
   /**
-   * Implements Drupal\Core\Entity\EntityStorageInterface::create().
+   * {@inheritdoc}
    */
-  public function create(array $values = array()) {
-    $class = $this->entityType->getClass();
-    $class::preCreate($this, $values);
-
+  protected function doCreate(array $values) {
     // Set default language to site default if not provided.
     $values += array('langcode' => $this->languageManager->getDefaultLanguage()->id);
-
-    $entity = new $class($values, $this->entityTypeId);
-    // Mark this entity as new, so isNew() returns TRUE. This does not check
-    // whether a configuration entity with the same ID (if any) already exists.
-    $entity->enforceIsNew();
-
-    // Assign a new UUID if there is none yet.
-    if (!$entity->uuid()) {
-      $entity->set('uuid', $this->uuidService->generate());
-    }
-    $entity->postCreate($this);
-
-    // Modules might need to add or change the data initially held by the new
-    // entity object, for instance to fill-in default values.
-    $this->invokeHook('create', $entity);
+    $entity = new $this->entityClass($values, $this->entityTypeId);
 
     // Default status to enabled.
     if (!empty($this->statusKey) && !isset($entity->{$this->statusKey})) {
       $entity->{$this->statusKey} = TRUE;
     }
-
     return $entity;
   }
 
   /**
-   * Implements Drupal\Core\Entity\EntityStorageInterface::delete().
+   * {@inheritdoc}
    */
-  public function delete(array $entities) {
-    if (!$entities) {
-      // If no IDs or invalid IDs were passed, do nothing.
-      return;
-    }
-
-    $entity_class = $this->entityType->getClass();
-    $entity_class::preDelete($this, $entities);
-    foreach ($entities as $entity) {
-      $this->invokeHook('predelete', $entity);
-    }
-
+  protected function doDelete($entities) {
     foreach ($entities as $entity) {
       $config = $this->configFactory->get($this->getConfigPrefix() . $entity->id());
       $config->delete();
     }
-
-    $entity_class::postDelete($this, $entities);
-    foreach ($entities as $entity) {
-      $this->invokeHook('delete', $entity);
-    }
   }
 
   /**
@@ -315,32 +210,16 @@ public function delete(array $entities) {
    *   When attempting to save a configuration entity that has no ID.
    */
   public function save(EntityInterface $entity) {
-    $prefix = $this->getConfigPrefix();
-
     // Configuration entity IDs are strings, and '0' is a valid ID.
     $id = $entity->id();
     if ($id === NULL || $id === '') {
       throw new EntityMalformedException('The entity does not have an ID.');
     }
 
-    // Load the stored entity, if any.
-    // At this point, the original ID can only be NULL or a valid ID.
-    if ($entity->getOriginalId() !== NULL) {
-      $id = $entity->getOriginalId();
-    }
-    $config = $this->configFactory->get($prefix . $id);
-
-    // Prevent overwriting an existing configuration file if the entity is new.
-    if ($entity->isNew() && !$config->isNew()) {
-      throw new EntityStorageException(String::format('@type entity with ID @id already exists.', array('@type' => $this->entityTypeId, '@id' => $id)));
-    }
-
-    if (!$config->isNew() && !isset($entity->original)) {
-      $entity->original = $this->loadUnchanged($id);
-    }
-
     // Check the configuration entity ID length.
     // @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH
+    // @todo Consider moving this to a protected method on the parent class, and
+    //   abstracting it for all entity types.
     if (strlen($entity->{$this->idKey}) > self::MAX_ID_LENGTH) {
       throw new ConfigEntityIdLengthException(String::format('Configuration entity ID @id exceeds maximum allowed length of @length characters.', array(
         '@id' => $entity->{$this->idKey},
@@ -348,9 +227,15 @@ public function save(EntityInterface $entity) {
       )));
     }
 
-    $entity->preSave($this);
-    $this->invokeHook('presave', $entity);
+    return parent::save($entity);
+  }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSave($id, EntityInterface $entity) {
+    $is_new = $entity->isNew();
+    $prefix = $this->getConfigPrefix();
     if ($id !== $entity->id()) {
       // Renaming a config object needs to cater for:
       // - Storage needs to access the original object.
@@ -358,34 +243,26 @@ public function save(EntityInterface $entity) {
       // - All instances of the object need to be renamed.
       $config = $this->configFactory->rename($prefix . $id, $prefix . $entity->id());
     }
+    else {
+      $config = $this->configFactory->get($prefix . $id);
+    }
 
     // Retrieve the desired properties and set them in config.
     foreach ($entity->toArray() as $key => $value) {
       $config->set($key, $value);
     }
+    $config->save();
 
-    if (!$config->isNew()) {
-      $return = SAVED_UPDATED;
-      $config->save();
-      $entity->postSave($this, TRUE);
-      $this->invokeHook('update', $entity);
-    }
-    else {
-      $return = SAVED_NEW;
-      $config->save();
-      $entity->enforceIsNew(FALSE);
-      $entity->postSave($this, FALSE);
-      $this->invokeHook('insert', $entity);
-    }
-
-    // After saving, this is now the "original entity", and subsequent saves
-    // will be updates instead of inserts, and updates must always be able to
-    // correctly identify the original entity.
-    $entity->setOriginalId($entity->id());
-
-    unset($entity->original);
+    return $is_new ? SAVED_NEW : SAVED_UPDATED;
+  }
 
-    return $return;
+  /**
+   * {@inheritdoc}
+   */
+  protected function has($id, EntityInterface $entity) {
+    $prefix = $this->getConfigPrefix();
+    $config = $this->configFactory->get($prefix . $id);
+    return !$config->isNew();
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
index 1731ac4..7960edc 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
@@ -58,6 +58,11 @@ class ConfigEntityType extends EntityType {
   /**
    * {@inheritdoc}
    */
+  protected $static_cache = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
   public function getControllerClasses() {
     return parent::getControllerClasses() + array(
       'storage' => 'Drupal\Core\Config\Entity\ConfigEntityStorage',
diff --git a/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php b/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php
index 60dfb53..32ad9b7 100644
--- a/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php
+++ b/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php
@@ -41,7 +41,7 @@ public function register(ContainerBuilder $container) {
       $container->register('theme_handler', 'Drupal\Core\Extension\ThemeHandler')
         ->addArgument(new Reference('config.factory'))
         ->addArgument(new Reference('module_handler'))
-        ->addArgument(new Reference('cache.default'))
+        ->addArgument(new Reference('state'))
         ->addArgument(new Reference('info_parser'));
     }
   }
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 8812cab..e6f9385 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -218,7 +218,7 @@ public function discoverServiceProviders() {
       $this->moduleList = isset($extensions['module']) ? $extensions['module'] : array();
     }
     $module_filenames = $this->getModuleFileNames();
-    $this->registerNamespaces($this->getModuleNamespaces($module_filenames));
+    $this->registerNamespacesPsr4($this->getModuleNamespacesPsr4($module_filenames));
 
     // Load each module's serviceProvider class.
     foreach ($this->moduleList as $module => $weight) {
@@ -249,7 +249,6 @@ public function discoverServiceProviders() {
     return $serviceProviders;
   }
 
-
   /**
    * {@inheritdoc}
    */
@@ -409,8 +408,8 @@ protected function initializeContainer() {
       // All namespaces must be registered before we attempt to use any service
       // from the container.
       $container_modules = $this->container->getParameter('container.modules');
-      $namespaces_before = $this->classLoader->getPrefixes();
-      $this->registerNamespaces($this->container->getParameter('container.namespaces'));
+      $namespaces_before = $this->classLoader->getPrefixesPsr4();
+      $this->registerNamespacesPsr4($this->container->getParameter('container.namespaces'));
 
       // If 'container.modules' is wrong, the container must be rebuilt.
       if (!isset($this->moduleList)) {
@@ -423,9 +422,9 @@ protected function initializeContainer() {
         // registerNamespaces() performs a merge rather than replace, so to
         // effectively remove erroneous registrations, we must replace them with
         // empty arrays.
-        $namespaces_after = $this->classLoader->getPrefixes();
+        $namespaces_after = $this->classLoader->getPrefixesPsr4();
         $namespaces_before += array_fill_keys(array_diff(array_keys($namespaces_after), array_keys($namespaces_before)), array());
-        $this->registerNamespaces($namespaces_before);
+        $this->registerNamespacesPsr4($namespaces_before);
       }
     }
 
@@ -501,14 +500,15 @@ protected function buildContainer() {
     $container->setParameter('container.modules', $this->getModulesParameter());
 
     // Get a list of namespaces and put it onto the container.
-    $namespaces = $this->getModuleNamespaces($this->getModuleFileNames());
+    $namespaces = $this->getModuleNamespacesPsr4($this->getModuleFileNames());
     // Add all components in \Drupal\Core and \Drupal\Component that have a
     // Plugin directory.
     foreach (array('Core', 'Component') as $parent_directory) {
       $path = DRUPAL_ROOT . '/core/lib/Drupal/' . $parent_directory;
+      $parent_namespace = 'Drupal\\' . $parent_directory;
       foreach (new \DirectoryIterator($path) as $component) {
         if (!$component->isDot() && $component->isDir() && is_dir($component->getPathname() . '/Plugin')) {
-          $namespaces['Drupal\\' . $parent_directory . '\\' . $component->getFilename()] = DRUPAL_ROOT . '/core/lib';
+          $namespaces[$parent_namespace . '\\' . $component->getFilename()] = $path . '/' . $component->getFilename();
         }
       }
     }
@@ -681,7 +681,11 @@ protected function getModulesParameter() {
   }
 
   /**
-   * Returns the file name for each enabled module.
+   * Gets the file name for each enabled module.
+   *
+   * @return array
+   *   Array where each key is a module name, and each value is a path to the
+   *   respective *.module or *.profile file.
    */
   protected function getModuleFileNames() {
     $filenames = array();
@@ -694,18 +698,67 @@ protected function getModuleFileNames() {
   }
 
   /**
-   * Gets the namespaces of each enabled module.
+   * Gets the PSR-4 base directories for module namespaces.
+   *
+   * @param array $module_file_names
+   *   Array where each key is a module name, and each value is a path to the
+   *   respective *.module or *.profile file.
+   *
+   * @return array
+   *   Array where each key is a module namespace like 'Drupal\system', and each
+   *   value is an array of PSR-4 base directories associated with the module
+   *   namespace.
    */
-  protected function getModuleNamespaces($moduleFileNames) {
+  protected function getModuleNamespacesPsr4($module_file_names) {
     $namespaces = array();
-    foreach ($moduleFileNames as $module => $filename) {
+    foreach ($module_file_names as $module => $filename) {
+      // @todo Remove lib/Drupal/$module, once the switch to PSR-4 is complete.
+      $namespaces["Drupal\\$module"][] = DRUPAL_ROOT . '/' . dirname($filename) . '/lib/Drupal/' . $module;
+      $namespaces["Drupal\\$module"][] = DRUPAL_ROOT . '/' . dirname($filename) . '/src';
+    }
+    return $namespaces;
+  }
+
+  /**
+   * Gets the PSR-0 base directories for module namespaces.
+   *
+   * @param array $module_file_names
+   *   Array where each key is a module name, and each value is a path to the
+   *   respective *.module or *.profile file.
+   *
+   * @return array
+   *   Array where each key is a module namespace like 'Drupal\system', and each
+   *   value is a PSR-0 base directory associated with the module namespace.
+   */
+  protected function getModuleNamespaces($module_file_names) {
+    $namespaces = array();
+    foreach ($module_file_names as $module => $filename) {
       $namespaces["Drupal\\$module"] = DRUPAL_ROOT . '/' . dirname($filename) . '/lib';
     }
     return $namespaces;
   }
 
   /**
-   * Registers a list of namespaces.
+   * Registers a list of namespaces with PSR-4 directories for class loading.
+   *
+   * @param array $namespaces
+   *   Array where each key is a namespace like 'Drupal\system', and each value
+   *   is either a PSR-4 base directory, or an array of PSR-4 base directories
+   *   associated with this namespace.
+   */
+  protected function registerNamespacesPsr4(array $namespaces = array()) {
+    foreach ($namespaces as $prefix => $paths) {
+      $this->classLoader->addPsr4($prefix . '\\', $paths);
+    }
+  }
+
+  /**
+   * Registers a list of namespaces with PSR-0 directories for class loading.
+   *
+   * @param array $namespaces
+   *   Array where each key is a namespace like 'Drupal\system', and each value
+   *   is either a PSR-0 base directory, or an array of PSR-0 base directories
+   *   associated with this namespace.
    */
   protected function registerNamespaces(array $namespaces = array()) {
     foreach ($namespaces as $prefix => $path) {
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
index 22cf527..531d101 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -59,13 +59,6 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase {
   protected $revisionDataTable;
 
   /**
-   * Whether this entity type should use the static cache.
-   *
-   * @var boolean
-   */
-  protected $cache;
-
-  /**
    * Active database connection.
    *
    * @var \Drupal\Core\Database\Connection
@@ -106,11 +99,6 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
     $this->database = $database;
     $this->fieldInfo = $field_info;
 
-    // Check if the entity type supports IDs.
-    if ($this->entityType->hasKey('id')) {
-      $this->idKey = $this->entityType->getKey('id');
-    }
-
     // Check if the entity type supports UUIDs.
     $this->uuidKey = $this->entityType->getKey('uuid');
 
@@ -134,74 +122,27 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
   /**
    * {@inheritdoc}
    */
-  public function loadMultiple(array $ids = NULL) {
-    $entities = array();
-
-    // Create a new variable which is either a prepared version of the $ids
-    // array for later comparison with the entity cache, or FALSE if no $ids
-    // were passed. The $ids array is reduced as items are loaded from cache,
-    // and we need to know if it's empty for this reason to avoid querying the
-    // database when all requested entities are loaded from cache.
-    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
-    // Try to load entities from the static cache, if the entity type supports
-    // static caching.
-    if ($this->cache && $ids) {
-      $entities += $this->cacheGet($ids);
-      // If any entities were loaded, remove them from the ids still to load.
-      if ($passed_ids) {
-        $ids = array_keys(array_diff_key($passed_ids, $entities));
-      }
-    }
-
-    // Load any remaining entities from the database. This is the case if $ids
-    // is set to NULL (so we load all entities) or if there are any ids left to
-    // load.
-    if ($ids === NULL || $ids) {
-      // Build and execute the query.
-      $query_result = $this->buildQuery($ids)->execute();
-      $queried_entities = $query_result->fetchAllAssoc($this->idKey);
-    }
-
-    // Pass all entities loaded from the database through $this->postLoad(),
-    // which attaches fields (if supported by the entity type) and calls the
-    // entity type specific load callback, for example hook_node_load().
-    if (!empty($queried_entities)) {
-      $this->postLoad($queried_entities);
-      $entities += $queried_entities;
-    }
-
-    if ($this->cache) {
-      // Add entities to the cache.
-      if (!empty($queried_entities)) {
-        $this->cacheSet($queried_entities);
-      }
-    }
-
-    // Ensure that the returned array is ordered the same as the original
-    // $ids array if this was passed in and remove any invalid ids.
-    if ($passed_ids) {
-      // Remove any invalid ids from the array.
-      $passed_ids = array_intersect_key($passed_ids, $entities);
-      foreach ($entities as $entity) {
-        $passed_ids[$entity->id()] = $entity;
-      }
-      $entities = $passed_ids;
-    }
-
-    return $entities;
-  }
+  protected function doLoadMultiple(array $ids = NULL) {
+    // Build and execute the query.
+    $records = $this
+      ->buildQuery($ids)
+      ->execute()
+      ->fetchAllAssoc($this->idKey);
 
-  /**
-   * {@inheritdoc}
-   */
-  public function load($id) {
-    $entities = $this->loadMultiple(array($id));
-    return isset($entities[$id]) ? $entities[$id] : NULL;
+    return $this->mapFromStorageRecords($records);
   }
 
   /**
    * Maps from storage records to entity objects.
    *
+   * This will attach fields, if the entity is fieldable. It calls
+   * hook_entity_load() for modules which need to add data to all entities.
+   * It also calls hook_TYPE_load() on the loaded entities. For example
+   * hook_node_load() or hook_user_load(). If your hook_TYPE_load()
+   * expects special parameters apart from the queried entities, you can set
+   * $this->hookLoadArguments prior to calling the method.
+   * See Drupal\node\NodeStorage::attachLoad() for an example.
+   *
    * @param array $records
    *   Associative array of query results, keyed on the entity ID.
    *
@@ -209,6 +150,10 @@ public function load($id) {
    *   An array of entity objects implementing the EntityInterface.
    */
   protected function mapFromStorageRecords(array $records) {
+    if (!$records) {
+      return array();
+    }
+
     $entities = array();
     foreach ($records as $id => $record) {
       $entities[$id] = array();
@@ -237,6 +182,12 @@ protected function mapFromStorageRecords(array $records) {
       }
     }
     $this->attachPropertyData($entities);
+
+    // Attach field values.
+    if ($this->entityType->isFieldable()) {
+      $this->loadFieldItems($entities);
+    }
+
     return $entities;
   }
 
@@ -319,15 +270,14 @@ protected function attachPropertyData(array &$entities) {
   public function loadRevision($revision_id) {
     // Build and execute the query.
     $query_result = $this->buildQuery(array(), $revision_id)->execute();
-    $queried_entities = $query_result->fetchAllAssoc($this->idKey);
+    $records = $query_result->fetchAllAssoc($this->idKey);
 
-    // Pass the loaded entities from the database through $this->postLoad(),
-    // which attaches fields (if supported by the entity type) and calls the
-    // entity type specific load callback, for example hook_node_load().
-    if (!empty($queried_entities)) {
-      $this->postLoad($queried_entities);
+    if (!empty($records)) {
+      // Convert the raw records to entity objects.
+      $entities = $this->mapFromStorageRecords($records);
+      $this->postLoad($entities);
+      return reset($entities);
     }
-    return reset($queried_entities);
   }
 
   /**
@@ -388,7 +338,7 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
    *   The ID of the revision to load, or FALSE if this query is asking for the
    *   most current revision(s).
    *
-   * @return SelectQuery
+   * @return \Drupal\Core\Database\Query\Select
    *   A SelectQuery object for loading the entity.
    */
   protected function buildQuery($ids, $revision_id = FALSE) {
@@ -438,32 +388,6 @@ protected function buildQuery($ids, $revision_id = FALSE) {
   }
 
   /**
-   * Attaches data to entities upon loading.
-   *
-   * This will attach fields, if the entity is fieldable. It calls
-   * hook_entity_load() for modules which need to add data to all entities.
-   * It also calls hook_TYPE_load() on the loaded entities. For example
-   * hook_node_load() or hook_user_load(). If your hook_TYPE_load()
-   * expects special parameters apart from the queried entities, you can set
-   * $this->hookLoadArguments prior to calling the method.
-   * See Drupal\node\NodeStorage::attachLoad() for an example.
-   *
-   * @param $queried_entities
-   *   Associative array of query results, keyed on the entity ID.
-   */
-  protected function postLoad(array &$queried_entities) {
-    // Map the loaded records into entity objects and according fields.
-    $queried_entities = $this->mapFromStorageRecords($queried_entities);
-
-    // Attach field values.
-    if ($this->entityType->isFieldable()) {
-      $this->loadFieldItems($queried_entities);
-    }
-
-    parent::postLoad($queried_entities);
-  }
-
-  /**
    * Implements \Drupal\Core\Entity\EntityStorageInterface::delete().
    */
   public function delete(array $entities) {
@@ -474,48 +398,8 @@ public function delete(array $entities) {
 
     $transaction = $this->database->startTransaction();
     try {
-      $entity_class = $this->entityClass;
-      $entity_class::preDelete($this, $entities);
-
-      foreach ($entities as $entity) {
-        $this->invokeHook('predelete', $entity);
-      }
-      $ids = array_keys($entities);
-
-      $this->database->delete($this->entityType->getBaseTable())
-        ->condition($this->idKey, $ids)
-        ->execute();
+      parent::delete($entities);
 
-      if ($this->revisionTable) {
-        $this->database->delete($this->revisionTable)
-          ->condition($this->idKey, $ids)
-          ->execute();
-      }
-
-      if ($this->dataTable) {
-        $this->database->delete($this->dataTable)
-          ->condition($this->idKey, $ids)
-          ->execute();
-      }
-
-      if ($this->revisionDataTable) {
-        $this->database->delete($this->revisionDataTable)
-          ->condition($this->idKey, $ids)
-          ->execute();
-      }
-
-      foreach ($entities as $entity) {
-        $this->invokeFieldMethod('delete', $entity);
-        $this->deleteFieldItems($entity);
-      }
-
-      // Reset the cache as soon as the changes have been applied.
-      $this->resetCache($ids);
-
-      $entity_class::postDelete($this, $entities);
-      foreach ($entities as $entity) {
-        $this->invokeHook('delete', $entity);
-      }
       // Ignore slave server temporarily.
       db_ignore_slave();
     }
@@ -529,92 +413,53 @@ public function delete(array $entities) {
   /**
    * {@inheritdoc}
    */
-  public function save(EntityInterface $entity) {
-    $transaction = $this->database->startTransaction();
-    try {
-      // Sync the changes made in the fields array to the internal values array.
-      $entity->updateOriginalValues();
+  protected function doDelete($entities) {
+    $ids = array_keys($entities);
 
-      // Load the stored entity, if any.
-      if (!$entity->isNew() && !isset($entity->original)) {
-        $id = $entity->id();
-        if ($entity->getOriginalId() !== NULL) {
-          $id = $entity->getOriginalId();
-        }
-        $entity->original = $this->loadUnchanged($id);
-      }
+    $this->database->delete($this->entityType->getBaseTable())
+      ->condition($this->idKey, $ids)
+      ->execute();
 
-      $entity->preSave($this);
-      $this->invokeFieldMethod('preSave', $entity);
-      $this->invokeHook('presave', $entity);
+    if ($this->revisionTable) {
+      $this->database->delete($this->revisionTable)
+        ->condition($this->idKey, $ids)
+        ->execute();
+    }
 
-      // Create the storage record to be saved.
-      $record = $this->mapToStorageRecord($entity);
+    if ($this->dataTable) {
+      $this->database->delete($this->dataTable)
+        ->condition($this->idKey, $ids)
+        ->execute();
+    }
 
-      if (!$entity->isNew()) {
-        if ($entity->isDefaultRevision()) {
-          $return = drupal_write_record($this->entityType->getBaseTable(), $record, $this->idKey);
-        }
-        else {
-          // @todo, should a different value be returned when saving an entity
-          // with $isDefaultRevision = FALSE?
-          $return = FALSE;
-        }
-        if ($this->revisionTable) {
-          $record->{$this->revisionKey} = $this->saveRevision($entity);
-        }
-        if ($this->dataTable) {
-          $this->savePropertyData($entity);
-        }
-        if ($this->revisionDataTable) {
-          $this->savePropertyData($entity, 'revision_data_table');
-        }
-        if ($this->revisionTable) {
-          $entity->setNewRevision(FALSE);
-        }
-        $this->invokeFieldMethod('update', $entity);
-        $this->saveFieldItems($entity, TRUE);
-        $this->resetCache(array($entity->id()));
-        $entity->postSave($this, TRUE);
-        $this->invokeHook('update', $entity);
-        if ($this->dataTable) {
-          $this->invokeTranslationHooks($entity);
-        }
-      }
-      else {
-        // Ensure the entity is still seen as new after assigning it an id,
-        // while storing its data.
-        $entity->enforceIsNew();
-        $return = drupal_write_record($this->entityType->getBaseTable(), $record);
-        $entity->{$this->idKey}->value = (string) $record->{$this->idKey};
-        if ($this->revisionTable) {
-          $entity->setNewRevision();
-          $record->{$this->revisionKey} = $this->saveRevision($entity);
-        }
-        if ($this->dataTable) {
-          $this->savePropertyData($entity);
-        }
-        if ($this->revisionDataTable) {
-          $this->savePropertyData($entity, 'revision_data_table');
-        }
+    if ($this->revisionDataTable) {
+      $this->database->delete($this->revisionDataTable)
+        ->condition($this->idKey, $ids)
+        ->execute();
+    }
 
-        $entity->enforceIsNew(FALSE);
-        if ($this->revisionTable) {
-          $entity->setNewRevision(FALSE);
-        }
+    foreach ($entities as $entity) {
+      $this->invokeFieldMethod('delete', $entity);
+      $this->deleteFieldItems($entity);
+    }
 
-        $this->invokeFieldMethod('insert', $entity);
-        $this->saveFieldItems($entity, FALSE);
-        // Reset general caches, but keep caches specific to certain entities.
-        $this->resetCache(array());
-        $entity->postSave($this, FALSE);
-        $this->invokeHook('insert', $entity);
-      }
+    // Reset the cache as soon as the changes have been applied.
+    $this->resetCache($ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(EntityInterface $entity) {
+    $transaction = $this->database->startTransaction();
+    try {
+      // Sync the changes made in the fields array to the internal values array.
+      $entity->updateOriginalValues();
+
+      $return = parent::save($entity);
 
       // Ignore slave server temporarily.
       db_ignore_slave();
-      unset($entity->original);
-
       return $return;
     }
     catch (\Exception $e) {
@@ -625,6 +470,77 @@ public function save(EntityInterface $entity) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  protected function doSave($id, EntityInterface $entity) {
+    // Create the storage record to be saved.
+    $record = $this->mapToStorageRecord($entity);
+
+    $is_new = $entity->isNew();
+    if (!$is_new) {
+      if ($entity->isDefaultRevision()) {
+        $return = drupal_write_record($this->entityType->getBaseTable(), $record, $this->idKey);
+      }
+      else {
+        // @todo, should a different value be returned when saving an entity
+        // with $isDefaultRevision = FALSE?
+        $return = FALSE;
+      }
+      if ($this->revisionTable) {
+        $record->{$this->revisionKey} = $this->saveRevision($entity);
+      }
+      if ($this->dataTable) {
+        $this->savePropertyData($entity);
+      }
+      if ($this->revisionDataTable) {
+        $this->savePropertyData($entity, 'revision_data_table');
+      }
+      if ($this->revisionTable) {
+        $entity->setNewRevision(FALSE);
+      }
+      $cache_ids = array($entity->id());
+    }
+    else {
+      // Ensure the entity is still seen as new after assigning it an id,
+      // while storing its data.
+      $entity->enforceIsNew();
+      $return = drupal_write_record($this->entityType->getBaseTable(), $record);
+      $entity->{$this->idKey}->value = (string) $record->{$this->idKey};
+      if ($this->revisionTable) {
+        $entity->setNewRevision();
+        $record->{$this->revisionKey} = $this->saveRevision($entity);
+      }
+      if ($this->dataTable) {
+        $this->savePropertyData($entity);
+      }
+      if ($this->revisionDataTable) {
+        $this->savePropertyData($entity, 'revision_data_table');
+      }
+      $entity->enforceIsNew(FALSE);
+      if ($this->revisionTable) {
+        $entity->setNewRevision(FALSE);
+      }
+      // Reset general caches, but keep caches specific to certain entities.
+      $cache_ids = array();
+    }
+    $this->invokeFieldMethod($is_new ? 'insert' : 'update', $entity);
+    $this->saveFieldItems($entity, !$is_new);
+    $this->resetCache($cache_ids);
+
+    if (!$is_new && $this->dataTable) {
+      $this->invokeTranslationHooks($entity);
+    }
+    return $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function has($id, EntityInterface $entity) {
+    return !$entity->isNew();
+  }
+
+  /**
    * Stores the entity property language-aware data.
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
@@ -661,6 +577,16 @@ protected function savePropertyData(EntityInterface $entity, $table_key = 'data_
   }
 
   /**
+   * {@inheritdoc}
+   */
+  protected function invokeHook($hook, EntityInterface $entity) {
+    if ($hook == 'presave') {
+      $this->invokeFieldMethod('preSave', $entity);
+    }
+    parent::invokeHook($hook, $entity);
+  }
+
+  /**
    * Maps from an entity object to the storage record.
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
index 3a7e7e5..98dbd2f 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
@@ -27,6 +27,12 @@ public function loadMultiple(array $ids = NULL) {
   /**
    * {@inheritdoc}
    */
+  protected function doLoadMultiple(array $ids = NULL) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function load($id) {
     return NULL;
   }
@@ -60,6 +66,12 @@ public function delete(array $entities) {
   /**
    * {@inheritdoc}
    */
+  protected function doDelete($entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function save(EntityInterface $entity) {
   }
 
@@ -106,4 +118,16 @@ protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceC
   protected function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance) {
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSave($id, EntityInterface $entity) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function has($id, EntityInterface $entity) {
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index cf43ee6..b40f73a 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -23,13 +23,6 @@
   protected $bundleKey = FALSE;
 
   /**
-   * Name of the entity class.
-   *
-   * @var string
-   */
-  protected $entityClass;
-
-  /**
    * Constructs a ContentEntityStorageBase object.
    *
    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
@@ -39,7 +32,6 @@ public function __construct(EntityTypeInterface $entity_type) {
     parent::__construct($entity_type);
 
     $this->bundleKey = $this->entityType->getKey('bundle');
-    $this->entityClass = $this->entityType->getClass();
   }
 
   /**
@@ -54,10 +46,7 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
   /**
    * {@inheritdoc}
    */
-  public function create(array $values = array()) {
-    $entity_class = $this->entityType->getClass();
-    $entity_class::preCreate($this, $values);
-
+  protected function doCreate(array $values) {
     // We have to determine the bundle first.
     $bundle = FALSE;
     if ($this->bundleKey) {
@@ -66,8 +55,7 @@ public function create(array $values = array()) {
       }
       $bundle = $values[$this->bundleKey];
     }
-    $entity = new $entity_class(array(), $this->entityTypeId, $bundle);
-    $entity->enforceIsNew();
+    $entity = new $this->entityClass(array(), $this->entityTypeId, $bundle);
 
     foreach ($entity as $name => $field) {
       if (isset($values[$name])) {
@@ -83,12 +71,6 @@ public function create(array $values = array()) {
     foreach ($values as $name => $value) {
       $entity->$name = $value;
     }
-    $entity->postCreate($this);
-
-    // Modules might need to add or change the data initially held by the new
-    // entity object, for instance to fill-in default values.
-    $this->invokeHook('create', $entity);
-
     return $entity;
   }
 
diff --git a/core/lib/Drupal/Core/Entity/EntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/EntityDatabaseStorage.php
index 180dfef..33eb34c 100644
--- a/core/lib/Drupal/Core/Entity/EntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/EntityDatabaseStorage.php
@@ -9,16 +9,6 @@
 
 use Drupal\Component\Uuid\UuidInterface;
 use Drupal\Core\Database\Connection;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\Query\QueryInterface;
-use Drupal\Core\Language\Language;
-use Drupal\Component\Utility\NestedArray;
-use Drupal\Component\Uuid\Uuid;
-use Drupal\field\FieldInfo;
-use Drupal\field\FieldConfigUpdateForbiddenException;
-use Drupal\field\FieldConfigInterface;
-use Drupal\field\FieldInstanceConfigInterface;
-use Drupal\field\Entity\FieldConfig;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -83,9 +73,6 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
     $this->database = $database;
     $this->uuidService = $uuid_service;
 
-    // Check if the entity type supports IDs.
-    $this->idKey = $this->entityType->getKey('id');
-
     // Check if the entity type supports UUIDs.
     $this->uuidKey = $this->entityType->getKey('uuid');
   }
@@ -93,76 +80,14 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
   /**
    * {@inheritdoc}
    */
-  public function loadMultiple(array $ids = NULL) {
-    $entities = array();
-
-    // Create a new variable which is either a prepared version of the $ids
-    // array for later comparison with the entity cache, or FALSE if no $ids
-    // were passed. The $ids array is reduced as items are loaded from cache,
-    // and we need to know if it's empty for this reason to avoid querying the
-    // database when all requested entities are loaded from cache.
-    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
-    // Try to load entities from the static cache, if the entity type supports
-    // static caching.
-    if ($this->cache && $ids) {
-      $entities += $this->cacheGet($ids);
-      // If any entities were loaded, remove them from the ids still to load.
-      if ($passed_ids) {
-        $ids = array_keys(array_diff_key($passed_ids, $entities));
-      }
-    }
-
-    // Load any remaining entities from the database. This is the case if $ids
-    // is set to NULL (so we load all entities) or if there are any ids left to
-    // load.
-    if ($ids === NULL || $ids) {
-      // Build and execute the query.
-      $query_result = $this->buildQuery($ids)->execute();
-
-      if ($class = $this->entityType->getClass()) {
-        // We provide the necessary arguments for PDO to create objects of the
-        // specified entity class.
-        // @see \Drupal\Core\Entity\EntityInterface::__construct()
-        $query_result->setFetchMode(\PDO::FETCH_CLASS, $class, array(array(), $this->entityTypeId));
-      }
-      $queried_entities = $query_result->fetchAllAssoc($this->idKey);
-    }
-
-    // Pass all entities loaded from the database through $this->postLoad(),
-    // which attaches fields (if supported by the entity type) and calls the
-    // entity type specific load callback, for example hook_node_load().
-    if (!empty($queried_entities)) {
-      $this->postLoad($queried_entities);
-      $entities += $queried_entities;
-    }
-
-    if ($this->cache) {
-      // Add entities to the cache.
-      if (!empty($queried_entities)) {
-        $this->cacheSet($queried_entities);
-      }
-    }
+  protected function doLoadMultiple(array $ids = NULL) {
+    // Build and execute the query.
+    $records = $this
+      ->buildQuery($ids)
+      ->execute()
+      ->fetchAllAssoc($this->idKey, \PDO::FETCH_ASSOC);
 
-    // Ensure that the returned array is ordered the same as the original
-    // $ids array if this was passed in and remove any invalid ids.
-    if ($passed_ids) {
-      // Remove any invalid ids from the array.
-      $passed_ids = array_intersect_key($passed_ids, $entities);
-      foreach ($entities as $entity) {
-        $passed_ids[$entity->id()] = $entity;
-      }
-      $entities = $passed_ids;
-    }
-
-    return $entities;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function load($id) {
-    $entities = $this->loadMultiple(array($id));
-    return isset($entities[$id]) ? $entities[$id] : NULL;
+    return $this->mapFromStorageRecords($records);
   }
 
   /**
@@ -180,41 +105,15 @@ public function deleteRevision($revision_id) {
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function loadByProperties(array $values = array()) {
-    // Build a query to fetch the entity IDs.
-    $entity_query = \Drupal::entityQuery($this->entityTypeId);
-    $this->buildPropertyQuery($entity_query, $values);
-    $result = $entity_query->execute();
-    return $result ? $this->loadMultiple($result) : array();
-  }
-
-  /**
-   * Builds an entity query.
-   *
-   * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
-   *   EntityQuery instance.
-   * @param array $values
-   *   An associative array of properties of the entity, where the keys are the
-   *   property names and the values are the values those properties must have.
-   */
-  protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
-    foreach ($values as $name => $value) {
-      $entity_query->condition($name, $value);
-    }
-  }
-
-  /**
    * Builds the query to load the entity.
    *
    * @param array|null $ids
    *   An array of entity IDs, or NULL to load all entities.
    *
-   * @return SelectQuery
+   * @return \Drupal\Core\Database\Query\Select
    *   A SelectQuery object for loading the entity.
    */
-  protected function buildQuery($ids, $revision_id = FALSE) {
+  protected function buildQuery($ids) {
     $query = $this->database->select($this->entityType->getBaseTable(), 'base');
 
     $query->addTag($this->entityTypeId . '_load_multiple');
@@ -233,29 +132,6 @@ protected function buildQuery($ids, $revision_id = FALSE) {
   /**
    * {@inheritdoc}
    */
-  public function create(array $values = array()) {
-    $entity_class = $this->entityType->getClass();
-    $entity_class::preCreate($this, $values);
-
-    $entity = new $entity_class($values, $this->entityTypeId);
-    $entity->enforceIsNew();
-
-    // Assign a new UUID if there is none yet.
-    if ($this->uuidKey && !isset($entity->{$this->uuidKey})) {
-      $entity->{$this->uuidKey} = $this->uuidService->generate();
-    }
-    $entity->postCreate($this);
-
-    // Modules might need to add or change the data initially held by the new
-    // entity object, for instance to fill-in default values.
-    $this->invokeHook('create', $entity);
-
-    return $entity;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function delete(array $entities) {
     if (!$entities) {
       // If no IDs or invalid IDs were passed, do nothing.
@@ -264,24 +140,8 @@ public function delete(array $entities) {
     $transaction = $this->database->startTransaction();
 
     try {
-      $entity_class = $this->entityType->getClass();
-      $entity_class::preDelete($this, $entities);
-      foreach ($entities as $entity) {
-        $this->invokeHook('predelete', $entity);
-      }
-      $ids = array_keys($entities);
-
-      $this->database->delete($this->entityType->getBaseTable())
-        ->condition($this->idKey, $ids, 'IN')
-        ->execute();
-
-      // Reset the cache as soon as the changes have been applied.
-      $this->resetCache($ids);
-
-      $entity_class::postDelete($this, $entities);
-      foreach ($entities as $entity) {
-        $this->invokeHook('delete', $entity);
-      }
+      parent::delete($entities);
+
       // Ignore slave server temporarily.
       db_ignore_slave();
     }
@@ -295,41 +155,27 @@ public function delete(array $entities) {
   /**
    * {@inheritdoc}
    */
+  protected function doDelete($entities) {
+    $ids = array_keys($entities);
+
+    $this->database->delete($this->entityType->getBaseTable())
+      ->condition($this->idKey, $ids, 'IN')
+      ->execute();
+
+    // Reset the cache as soon as the changes have been applied.
+    $this->resetCache($ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function save(EntityInterface $entity) {
     $transaction = $this->database->startTransaction();
     try {
-      // Load the stored entity, if any.
-      if (!$entity->isNew() && !isset($entity->original)) {
-        $id = $entity->id();
-        if ($entity->getOriginalId() !== NULL) {
-          $id = $entity->getOriginalId();
-        }
-        $entity->original = $this->loadUnchanged($id);
-      }
-
-      $entity->preSave($this);
-      $this->invokeHook('presave', $entity);
-
-      if (!$entity->isNew()) {
-        $return = drupal_write_record($this->entityType->getBaseTable(), $entity, $this->idKey);
-        $this->resetCache(array($entity->id()));
-        $entity->postSave($this, TRUE);
-        $this->invokeHook('update', $entity);
-      }
-      else {
-        $return = drupal_write_record($this->entityType->getBaseTable(), $entity);
-        // Reset general caches, but keep caches specific to certain entities.
-        $this->resetCache(array());
-
-        $entity->enforceIsNew(FALSE);
-        $entity->postSave($this, FALSE);
-        $this->invokeHook('insert', $entity);
-      }
+      $return = parent::save($entity);
 
       // Ignore slave server temporarily.
       db_ignore_slave();
-      unset($entity->original);
-
       return $return;
     }
     catch (\Exception $e) {
@@ -342,6 +188,30 @@ public function save(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
+  protected function doSave($id, EntityInterface $entity) {
+    if (!$entity->isNew()) {
+      $return = drupal_write_record($this->entityType->getBaseTable(), $entity, $this->idKey);
+      $this->resetCache(array($entity->id()));
+    }
+    else {
+      $return = drupal_write_record($this->entityType->getBaseTable(), $entity);
+      // Reset general caches, but keep caches specific to certain entities.
+      $this->resetCache(array());
+    }
+
+    return $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function has($id, EntityInterface $entity) {
+    return !$entity->isNew();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getQueryServiceName() {
     return 'entity.query.sql';
   }
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index 679f849..00bb0b0 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -341,12 +341,18 @@ public function getBaseFieldDefinitions($entity_type_id) {
    *   An array of field definitions, keyed by field name.
    *
    * @throws \LogicException
-   *   Thrown if one of the entity keys is flagged as translatable.
+   *   Thrown if a config entity type is given or if one of the entity keys is
+   *   flagged as translatable.
    */
   protected function buildBaseFieldDefinitions($entity_type_id) {
     $entity_type = $this->getDefinition($entity_type_id);
     $class = $entity_type->getClass();
 
+    // Fail with an exception for config entity types.
+    if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) {
+      throw new \LogicException(String::format('Getting the base fields is not supported for entity type @type.', array('@type' => $entity_type->getLabel())));
+    }
+
     // Retrieve base field definitions and assign them the entity type provider.
     $base_field_definitions = $class::baseFieldDefinitions($entity_type);
     $provider = $entity_type->getProvider();
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index 1befefc..5d24441 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -6,9 +6,9 @@
  */
 
 namespace Drupal\Core\Entity;
+
+use Drupal\Component\Utility\String;
 use Drupal\Core\Entity\Query\QueryInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * A base entity storage class.
@@ -20,14 +20,7 @@
    *
    * @var array
    */
-  protected $entityCache = array();
-
-  /**
-   * Whether this entity type should use the static cache.
-   *
-   * @var boolean
-   */
-  protected $cache;
+  protected $entities = array();
 
   /**
    * Entity type ID for this controller instance.
@@ -60,6 +53,20 @@
   protected $uuidKey;
 
   /**
+   * The UUID service.
+   *
+   * @var \Drupal\Component\Uuid\UuidInterface
+   */
+  protected $uuidService;
+
+  /**
+   * Name of the entity class.
+   *
+   * @var string
+   */
+  protected $entityClass;
+
+  /**
    * Constructs an EntityStorageBase instance.
    *
    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
@@ -68,8 +75,8 @@
   public function __construct(EntityTypeInterface $entity_type) {
     $this->entityTypeId = $entity_type->id();
     $this->entityType = $entity_type;
-    // Check if the entity type supports static caching of loaded entities.
-    $this->cache = $this->entityType->isStaticallyCacheable();
+    $this->idKey = $this->entityType->getKey('id');
+    $this->entityClass = $this->entityType->getClass();
   }
 
   /**
@@ -98,30 +105,30 @@ public function loadUnchanged($id) {
    * {@inheritdoc}
    */
   public function resetCache(array $ids = NULL) {
-    if ($this->cache && isset($ids)) {
+    if ($this->entityType->isStaticallyCacheable() && isset($ids)) {
       foreach ($ids as $id) {
-        unset($this->entityCache[$id]);
+        unset($this->entities[$id]);
       }
     }
     else {
-      $this->entityCache = array();
+      $this->entities = array();
     }
   }
 
   /**
    * Gets entities from the static cache.
    *
-   * @param $ids
+   * @param array $ids
    *   If not empty, return entities that match these IDs.
    *
-   * @return
+   * @return \Drupal\Core\Entity\EntityInterface[]
    *   Array of entities from the entity cache.
    */
-  protected function cacheGet($ids) {
+  protected function getFromStaticCache(array $ids) {
     $entities = array();
     // Load any available entities from the internal cache.
-    if ($this->cache && !empty($this->entityCache)) {
-      $entities += array_intersect_key($this->entityCache, array_flip($ids));
+    if ($this->entityType->isStaticallyCacheable() && !empty($this->entities)) {
+      $entities += array_intersect_key($this->entities, array_flip($ids));
     }
     return $entities;
   }
@@ -129,12 +136,12 @@ protected function cacheGet($ids) {
   /**
    * Stores entities in the static entity cache.
    *
-   * @param $entities
+   * @param \Drupal\Core\Entity\EntityInterface[] $entities
    *   Entities to store in the cache.
    */
-  protected function cacheSet($entities) {
-    if ($this->cache) {
-      $this->entityCache += $entities;
+  protected function setStaticCache(array $entities) {
+    if ($this->entityType->isStaticallyCacheable()) {
+      $this->entities += $entities;
     }
   }
 
@@ -155,27 +162,269 @@ protected function invokeHook($hook, EntityInterface $entity) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function create(array $values = array()) {
+    $entity_class = $this->entityClass;
+    $entity_class::preCreate($this, $values);
+
+    // Assign a new UUID if there is none yet.
+    if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
+      $values[$this->uuidKey] = $this->uuidService->generate();
+    }
+
+    $entity = $this->doCreate($values);
+    $entity->enforceIsNew();
+
+    $entity->postCreate($this);
+
+    // Modules might need to add or change the data initially held by the new
+    // entity object, for instance to fill-in default values.
+    $this->invokeHook('create', $entity);
+
+    return $entity;
+  }
+
+  /**
+   * Performs storage-specific creation of entities.
+   *
+   * @param array $values
+   *   An array of values to set, keyed by property name.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   */
+  protected function doCreate(array $values) {
+    return new $this->entityClass($values, $this->entityTypeId);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load($id) {
+    $entities = $this->loadMultiple(array($id));
+    return isset($entities[$id]) ? $entities[$id] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultiple(array $ids = NULL) {
+    $entities = array();
+
+    // Create a new variable which is either a prepared version of the $ids
+    // array for later comparison with the entity cache, or FALSE if no $ids
+    // were passed. The $ids array is reduced as items are loaded from cache,
+    // and we need to know if it's empty for this reason to avoid querying the
+    // database when all requested entities are loaded from cache.
+    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
+    // Try to load entities from the static cache, if the entity type supports
+    // static caching.
+    if ($this->entityType->isStaticallyCacheable() && $ids) {
+      $entities += $this->getFromStaticCache($ids);
+      // If any entities were loaded, remove them from the ids still to load.
+      if ($passed_ids) {
+        $ids = array_keys(array_diff_key($passed_ids, $entities));
+      }
+    }
+
+    // Load any remaining entities from the database. This is the case if $ids
+    // is set to NULL (so we load all entities) or if there are any ids left to
+    // load.
+    if ($ids === NULL || $ids) {
+      $queried_entities = $this->doLoadMultiple($ids);
+    }
+
+    // Pass all entities loaded from the database through $this->postLoad(),
+    // which attaches fields (if supported by the entity type) and calls the
+    // entity type specific load callback, for example hook_node_load().
+    if (!empty($queried_entities)) {
+      $this->postLoad($queried_entities);
+      $entities += $queried_entities;
+    }
+
+    if ($this->entityType->isStaticallyCacheable()) {
+      // Add entities to the cache.
+      if (!empty($queried_entities)) {
+        $this->setStaticCache($queried_entities);
+      }
+    }
+
+    // Ensure that the returned array is ordered the same as the original
+    // $ids array if this was passed in and remove any invalid ids.
+    if ($passed_ids) {
+      // Remove any invalid ids from the array.
+      $passed_ids = array_intersect_key($passed_ids, $entities);
+      foreach ($entities as $entity) {
+        $passed_ids[$entity->id()] = $entity;
+      }
+      $entities = $passed_ids;
+    }
+
+    return $entities;
+  }
+
+  /**
+   * Performs storage-specific loading of entities.
+   *
+   * Override this method to add custom functionality directly after loading.
+   * This is always called, while self::postLoad() is only called when there are
+   * actual results.
+   *
+   * @param array|null $ids
+   *   (optional) An array of entity IDs, or NULL to load all entities.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   Associative array of entities, keyed on the entity ID.
+   */
+  abstract protected function doLoadMultiple(array $ids = NULL);
+
+  /**
    * Attaches data to entities upon loading.
    *
-   * @param array $queried_entities
+   * @param array $entities
    *   Associative array of query results, keyed on the entity ID.
    */
-  protected function postLoad(array &$queried_entities) {
-    $entity_class = $this->entityType->getClass();
-    $entity_class::postLoad($this, $queried_entities);
+  protected function postLoad(array &$entities) {
+    $entity_class = $this->entityClass;
+    $entity_class::postLoad($this, $entities);
     // Call hook_entity_load().
     foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) {
       $function = $module . '_entity_load';
-      $function($queried_entities, $this->entityTypeId);
+      $function($entities, $this->entityTypeId);
     }
     // Call hook_TYPE_load().
     foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
       $function = $module . '_' . $this->entityTypeId . '_load';
-      $function($queried_entities);
+      $function($entities);
     }
   }
 
   /**
+   * Maps from storage records to entity objects.
+   *
+   * @param array $records
+   *   Associative array of query results, keyed on the entity ID.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   An array of entity objects implementing the EntityInterface.
+   */
+  protected function mapFromStorageRecords(array $records) {
+    $entities = array();
+    foreach ($records as $record) {
+      $entity = new $this->entityClass($record, $this->entityTypeId);
+      $entities[$entity->id()] = $entity;
+    }
+    return $entities;
+  }
+
+  /**
+   * Determines if this entity already exists in storage.
+   *
+   * @param int|string $id
+   *   The original entity ID.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being saved.
+   *
+   * @return bool
+   */
+  abstract protected function has($id, EntityInterface $entity);
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete(array $entities) {
+    if (!$entities) {
+      // If no IDs or invalid IDs were passed, do nothing.
+      return;
+    }
+
+    $entity_class = $this->entityClass;
+    $entity_class::preDelete($this, $entities);
+    foreach ($entities as $entity) {
+      $this->invokeHook('predelete', $entity);
+    }
+
+    $this->doDelete($entities);
+
+    $entity_class::postDelete($this, $entities);
+    foreach ($entities as $entity) {
+      $this->invokeHook('delete', $entity);
+    }
+  }
+
+  /**
+   * Performs storage-specific entity deletion.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface[] $entities
+   *   An array of entity objects to delete.
+   */
+  abstract protected function doDelete($entities);
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(EntityInterface $entity) {
+    $id = $entity->id();
+
+    // Track the original ID.
+    if ($entity->getOriginalId() !== NULL) {
+      $id = $entity->getOriginalId();
+    }
+
+    // Track if this entity is new.
+    $is_new = $entity->isNew();
+    // Track if this entity exists already.
+    $id_exists = $this->has($id, $entity);
+
+    // A new entity should not already exist.
+    if ($id_exists && $is_new) {
+      throw new EntityStorageException(String::format('@type entity with ID @id already exists.', array('@type' => $this->entityTypeId, '@id' => $id)));
+    }
+
+    // Load the original entity, if any.
+    if ($id_exists && !isset($entity->original)) {
+      $entity->original = $this->loadUnchanged($id);
+    }
+
+    // Allow code to run before saving.
+    $entity->preSave($this);
+    $this->invokeHook('presave', $entity);
+
+    // Perform the save.
+    $return = $this->doSave($id, $entity);
+
+    // The entity is no longer new.
+    $entity->enforceIsNew(FALSE);
+
+    // Allow code to run after saving.
+    $entity->postSave($this, !$is_new);
+    $this->invokeHook($is_new ? 'insert' : 'update', $entity);
+
+    // After saving, this is now the "original entity", and subsequent saves
+    // will be updates instead of inserts, and updates must always be able to
+    // correctly identify the original entity.
+    $entity->setOriginalId($entity->id());
+
+    unset($entity->original);
+
+    return $return;
+  }
+
+  /**
+   * Performs storage-specific saving of the entity.
+   *
+   * @param int|string $id
+   *   The original entity ID.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to save.
+   *
+   * @return bool|int
+   *   If the record insert or update failed, returns FALSE. If it succeeded,
+   *   returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
+   */
+  abstract protected function doSave($id, EntityInterface $entity);
+
+  /**
    * Builds an entity query.
    *
    * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
diff --git a/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
index e3302a5..07d844e 100644
--- a/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
+++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
@@ -57,14 +57,21 @@ public static function createFromDataType($data_type) {
   public function getPropertyDefinitions() {
     if (!isset($this->propertyDefinitions)) {
       if ($entity_type_id = $this->getEntityTypeId()) {
-        // @todo: Add support for handling multiple bundles.
-        // See https://drupal.org/node/2169813.
-        $bundles = $this->getBundles();
-        if (is_array($bundles) && count($bundles) == 1) {
-          $this->propertyDefinitions = \Drupal::entityManager()->getFieldDefinitions($entity_type_id, reset($bundles));
+        // Return an empty array for entity types that don't support typed data.
+        $entity_type_class = \Drupal::entityManager()->getDefinition($entity_type_id)->getClass();
+        if (!in_array('Drupal\Core\TypedData\TypedDataInterface', class_implements($entity_type_class))) {
+          $this->propertyDefinitions = array();
         }
         else {
-          $this->propertyDefinitions = \Drupal::entityManager()->getBaseFieldDefinitions($entity_type_id);
+          // @todo: Add support for handling multiple bundles.
+          // See https://drupal.org/node/2169813.
+          $bundles = $this->getBundles();
+          if (is_array($bundles) && count($bundles) == 1) {
+            $this->propertyDefinitions = \Drupal::entityManager()->getFieldDefinitions($entity_type_id, reset($bundles));
+          }
+          else {
+            $this->propertyDefinitions = \Drupal::entityManager()->getBaseFieldDefinitions($entity_type_id);
+          }
         }
       }
       else {
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index 537a844..08a3ec5 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -758,8 +758,6 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
 
         // Refresh the schema to include it.
         drupal_get_schema(NULL, TRUE);
-        // Update the theme registry to include it.
-        drupal_theme_rebuild();
 
         // Allow modules to react prior to the installation of a module.
         $this->invokeAll('module_preinstall', array($module));
@@ -804,8 +802,18 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         // Record the fact that it was installed.
         $modules_installed[] = $module;
 
+        // Update the theme registry to include it.
+        drupal_theme_rebuild();
+
+        // Modules can alter theme info, so refresh theme data.
+        // @todo ThemeHandler cannot be injected into ModuleHandler, since that
+        //   causes a circular service dependency.
+        // @see https://drupal.org/node/2208429
+        \Drupal::service('theme_handler')->refreshInfo();
+
         // Allow the module to perform install tasks.
         $this->invoke($module, 'install');
+
         // Record the fact that it was installed.
         watchdog('system', '%module module installed.', array('%module' => $module), WATCHDOG_INFO);
       }
@@ -912,6 +920,12 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
       // Update the theme registry to remove the newly uninstalled module.
       drupal_theme_rebuild();
 
+      // Modules can alter theme info, so refresh theme data.
+      // @todo ThemeHandler cannot be injected into ModuleHandler, since that
+      //   causes a circular service dependency.
+      // @see https://drupal.org/node/2208429
+      \Drupal::service('theme_handler')->refreshInfo();
+
       watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO);
 
       $schema_store->delete($module);
diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php
index b43d3e4..25784ca 100644
--- a/core/lib/Drupal/Core/Extension/ThemeHandler.php
+++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php
@@ -9,9 +9,9 @@
 
 use Drupal\Component\Utility\String;
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Config\ConfigInstallerInterface;
+use Drupal\Core\State\StateInterface;
 use Drupal\Core\Routing\RouteBuilder;
 
 /**
@@ -41,7 +41,7 @@ class ThemeHandler implements ThemeHandlerInterface {
    *
    * @var array
    */
-  protected $list = array();
+  protected $list;
 
   /**
    * The config factory to get the enabled themes.
@@ -58,11 +58,11 @@ class ThemeHandler implements ThemeHandlerInterface {
   protected $moduleHandler;
 
   /**
-   * The cache backend to clear the local tasks cache.
+   * The state backend.
    *
-   * @var \Drupal\Core\Cache\CacheBackendInterface
+   * @var \Drupal\Core\State\StateInterface
    */
-  protected $cacheBackend;
+  protected $state;
 
   /**
    *  The config installer to install configuration.
@@ -99,8 +99,8 @@ class ThemeHandler implements ThemeHandlerInterface {
    *   The config factory to get the enabled themes.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler to fire themes_enabled/themes_disabled hooks.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
-   *   The cache backend to clear the local tasks cache.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state store.
    * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
    *   The info parser to parse the theme.info.yml files.
    * @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer
@@ -112,10 +112,10 @@ class ThemeHandler implements ThemeHandlerInterface {
    * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
    *   (optional) A extension discovery instance (for unit tests).
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $extension_discovery = NULL) {
+  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $extension_discovery = NULL) {
     $this->configFactory = $config_factory;
     $this->moduleHandler = $module_handler;
-    $this->cacheBackend = $cache_backend;
+    $this->state = $state;
     $this->infoParser = $info_parser;
     $this->configInstaller = $config_installer;
     $this->routeBuilder = $route_builder;
@@ -125,10 +125,90 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle
   /**
    * {@inheritdoc}
    */
-  public function enable(array $theme_list) {
-    $this->clearCssCache();
+  public function getDefault() {
+    return $this->configFactory->get('system.theme')->get('default');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDefault($name) {
+    if (!isset($this->list)) {
+      $this->listInfo();
+    }
+    if (!isset($this->list[$name])) {
+      throw new \InvalidArgumentException("$name theme is not enabled.");
+    }
+    $this->configFactory->get('system.theme')
+      ->set('default', $name)
+      ->save();
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function enable(array $theme_list, $enable_dependencies = TRUE) {
     $extension_config = $this->configFactory->get('core.extension');
+
+    $theme_data = $this->rebuildThemeData();
+
+    if ($enable_dependencies) {
+      $theme_list = array_combine($theme_list, $theme_list);
+
+      if ($missing = array_diff_key($theme_list, $theme_data)) {
+        // One or more of the given themes doesn't exist.
+        throw new \InvalidArgumentException(String::format('Unknown themes: !themes.', array(
+          '!themes' => implode(', ', $missing),
+        )));
+      }
+
+      // Only process themes that are not enabled currently.
+      $installed_themes = $extension_config->get('theme') ?: array();
+      if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
+        // Nothing to do. All themes already enabled.
+        return TRUE;
+      }
+      $installed_themes += $extension_config->get('disabled.theme') ?: array();
+
+      while (list($theme) = each($theme_list)) {
+        // Add dependencies to the list. The new themes will be processed as
+        // the while loop continues.
+        foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
+          if (!isset($theme_data[$dependency])) {
+            // The dependency does not exist.
+            return FALSE;
+          }
+
+          // Skip already installed themes.
+          if (!isset($theme_list[$dependency]) && !isset($installed_themes[$dependency])) {
+            $theme_list[$dependency] = $dependency;
+          }
+        }
+      }
+
+      // Set the actual theme weights.
+      $theme_list = array_map(function ($theme) use ($theme_data) {
+        return $theme_data[$theme]->sort;
+      }, $theme_list);
+
+      // Sort the theme list by their weights (reverse).
+      arsort($theme_list);
+      $theme_list = array_keys($theme_list);
+    }
+    else {
+      $installed_themes = $extension_config->get('theme') ?: array();
+      $installed_themes += $extension_config->get('disabled.theme') ?: array();
+    }
+
+    $themes_enabled = array();
     foreach ($theme_list as $key) {
+      // Only process themes that are not already enabled.
+      $enabled = $extension_config->get("theme.$key") !== NULL;
+      if ($enabled) {
+        continue;
+      }
+
       // Throw an exception if the theme name is too long.
       if (strlen($key) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
         throw new ExtensionNameLengthException(String::format('Theme name %name is over the maximum allowed length of @max characters.', array(
@@ -143,53 +223,107 @@ public function enable(array $theme_list) {
         ->clear("disabled.theme.$key")
         ->save();
 
-      // Refresh the theme list as installation of default configuration needs
-      // an updated list to work.
-      $this->reset();
-
-      // The default config installation storage only knows about the currently
-      // enabled list of themes, so it has to be reset in order to pick up the
-      // default config of the newly installed theme. However, do not reset the
-      // source storage when synchronizing configuration, since that would
-      // needlessly trigger a reload of the whole configuration to be imported.
-      if (!$this->configInstaller->isSyncing()) {
-        $this->configInstaller->resetSourceStorage();
+      // Add the theme to the current list.
+      // @todo Remove all code that relies on $status property.
+      $theme_data[$key]->status = 1;
+      $this->addTheme($theme_data[$key]);
+
+      // Update the current theme data accordingly.
+      $current_theme_data = $this->state->get('system.theme.data', array());
+      $current_theme_data[$key] = $theme_data[$key];
+      $this->state->set('system.theme.data', $current_theme_data);
+
+      // Reset theme settings.
+      $theme_settings = &drupal_static('theme_get_setting');
+      unset($theme_settings[$key]);
+
+      // @todo Remove system_list().
+      $this->systemListReset();
+
+      // Only install default configuration if this theme has not been installed
+      // already.
+      if (!isset($installed_themes[$key])) {
+        // The default config installation storage only knows about the currently
+        // enabled list of themes, so it has to be reset in order to pick up the
+        // default config of the newly installed theme. However, do not reset the
+        // source storage when synchronizing configuration, since that would
+        // needlessly trigger a reload of the whole configuration to be imported.
+        if (!$this->configInstaller->isSyncing()) {
+          $this->configInstaller->resetSourceStorage();
+        }
+
+        // Install default configuration of the theme.
+        $this->configInstaller->installDefaultConfig('theme', $key);
       }
-      // Install default configuration of the theme.
-      $this->configInstaller->installDefaultConfig('theme', $key);
+
+      $themes_enabled[] = $key;
+
+      // Record the fact that it was enabled.
+      watchdog('system', '%theme theme enabled.', array('%theme' => $key), WATCHDOG_INFO);
     }
 
+    $this->clearCssCache();
     $this->resetSystem();
 
     // Invoke hook_themes_enabled() after the themes have been enabled.
-    $this->moduleHandler->invokeAll('themes_enabled', array($theme_list));
+    $this->moduleHandler->invokeAll('themes_enabled', array($themes_enabled));
+
+    return !empty($themes_enabled);
   }
 
   /**
    * {@inheritdoc}
    */
   public function disable(array $theme_list) {
-    // Don't disable the default or admin themes.
     $theme_config = $this->configFactory->get('system.theme');
-    $default_theme = $theme_config->get('default');
-    $admin_theme = $theme_config->get('admin');
-    $theme_list = array_diff($theme_list, array($default_theme, $admin_theme));
-    if (empty($theme_list)) {
-      return;
+
+    foreach ($theme_list as $key) {
+      if (!isset($this->list[$key])) {
+        throw new \InvalidArgumentException("Unknown theme: $key.");
+      }
+      if ($key === $theme_config->get('default')) {
+        throw new \InvalidArgumentException("The current default theme $key cannot be disabled.");
+      }
+      if ($key === $theme_config->get('admin')) {
+        throw new \InvalidArgumentException("The current admin theme $key cannot be disabled.");
+      }
+      // Base themes cannot be disabled if sub themes are enabled, and if they
+      // are not disabled at the same time.
+      if (!empty($this->list[$key]->sub_themes)) {
+        foreach ($this->list[$key]->sub_themes as $sub_key => $sub_label) {
+          if (isset($this->list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) {
+            throw new \InvalidArgumentException("The base theme $key cannot be disabled, because theme $sub_key depends on it.");
+          }
+        }
+      }
     }
 
     $this->clearCssCache();
 
     $extension_config = $this->configFactory->get('core.extension');
+    $current_theme_data = $this->state->get('system.theme.data', array());
     foreach ($theme_list as $key) {
       // The value is not used; the weight is ignored for themes currently.
       $extension_config
         ->clear("theme.$key")
         ->set("disabled.theme.$key", 0);
+
+      // Remove the theme from the current list.
+      unset($this->list[$key]);
+
+      // Update the current theme data accordingly.
+      unset($current_theme_data[$key]);
+
+      // Reset theme settings.
+      $theme_settings = &drupal_static('theme_get_setting');
+      unset($theme_settings[$key]);
+
+      // @todo Remove system_list().
+      $this->systemListReset();
     }
     $extension_config->save();
+    $this->state->set('system.theme.data', $current_theme_data);
 
-    $this->reset();
     $this->resetSystem();
 
     // Invoke hook_themes_disabled after the themes have been disabled.
@@ -200,52 +334,63 @@ public function disable(array $theme_list) {
    * {@inheritdoc}
    */
   public function listInfo() {
-    if (empty($this->list)) {
+    if (!isset($this->list)) {
       $this->list = array();
-      try {
-        $themes = $this->systemThemeList();
+      $themes = $this->systemThemeList();
+      foreach ($themes as $theme) {
+        $this->addTheme($theme);
       }
-      catch (\Exception $e) {
-        // If the database is not available, rebuild the theme data.
-        $themes = $this->rebuildThemeData();
+    }
+    return $this->list;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addTheme(Extension $theme) {
+    // @todo Remove this 100% unnecessary duplication of properties.
+    foreach ($theme->info['stylesheets'] as $media => $stylesheets) {
+      foreach ($stylesheets as $stylesheet => $path) {
+        $theme->stylesheets[$media][$stylesheet] = $path;
       }
+    }
+    foreach ($theme->info['libraries'] as $library => $name) {
+      $theme->libraries[$library] = $name;
+    }
+    if (isset($theme->info['engine'])) {
+      $theme->engine = $theme->info['engine'];
+    }
+    if (isset($theme->info['base theme'])) {
+      $theme->base_theme = $theme->info['base theme'];
+    }
+    $this->list[$theme->getName()] = $theme;
+  }
 
-      foreach ($themes as $theme) {
-        foreach ($theme->info['stylesheets'] as $media => $stylesheets) {
-          foreach ($stylesheets as $stylesheet => $path) {
-            $theme->stylesheets[$media][$stylesheet] = $path;
-          }
-        }
-        foreach ($theme->info['libraries'] as $library => $name) {
-          $theme->libraries[$library] = $name;
-        }
-        if (isset($theme->info['engine'])) {
-          $theme->engine = $theme->info['engine'];
-        }
-        if (isset($theme->info['base theme'])) {
-          $theme->base_theme = $theme->info['base theme'];
-        }
-        // Status is normally retrieved from the database. Add zero values when
-        // read from the installation directory to prevent notices.
-        if (!isset($theme->status)) {
-          $theme->status = 0;
-        }
-        $this->list[$theme->getName()] = $theme;
+  /**
+   * {@inheritdoc}
+   */
+  public function refreshInfo() {
+    $this->reset();
+    $extension_config = $this->configFactory->get('core.extension');
+    $enabled = $extension_config->get('theme') ?: array();
+
+    // @todo Avoid re-scanning all themes by retaining the original (unaltered)
+    //   theme info somewhere.
+    $list = $this->rebuildThemeData();
+    foreach ($list as $name => $theme) {
+      if (isset($enabled[$name])) {
+        $this->list[$name] = $theme;
       }
     }
-    return $this->list;
+    $this->state->set('system.theme.data', $this->list);
   }
 
   /**
    * {@inheritdoc}
    */
   public function reset() {
-    // listInfo() calls system_info() which has a lot of side effects that have
-    // to be triggered like the classloading of theme classes.
-    $this->list = array();
     $this->systemListReset();
-    $this->listInfo();
-    $this->list = array();
+    $this->list = NULL;
   }
 
   /**
@@ -255,6 +400,8 @@ public function rebuildThemeData() {
     $listing = $this->getExtensionDiscovery();
     $themes = $listing->scan('theme');
     $engines = $listing->scan('theme_engine');
+    $extension_config = $this->configFactory->get('core.extension');
+    $enabled = $extension_config->get('theme') ?: array();
 
     // Set defaults for theme info.
     $defaults = array(
@@ -279,8 +426,12 @@ public function rebuildThemeData() {
     );
 
     $sub_themes = array();
+    $files = array();
     // Read info files for each theme.
     foreach ($themes as $key => $theme) {
+      // @todo Remove all code that relies on the $status property.
+      $theme->status = (int) isset($enabled[$key]);
+
       $theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults;
 
       // Add the info file modification time, so it becomes available for
@@ -295,6 +446,8 @@ public function rebuildThemeData() {
 
       if (!empty($theme->info['base theme'])) {
         $sub_themes[] = $key;
+        // Add the base theme as a proper dependency.
+        $themes[$key]->info['dependencies'][] = $themes[$key]->info['base theme'];
       }
 
       // Defaults to 'twig' (see $defaults above).
@@ -310,7 +463,17 @@ public function rebuildThemeData() {
       if (!empty($theme->info['screenshot'])) {
         $theme->info['screenshot'] = $path . '/' . $theme->info['screenshot'];
       }
+
+      $files[$key] = $theme->getPathname();
     }
+    // Build dependencies.
+    // @todo Move into a generic ExtensionHandler base class.
+    // @see https://drupal.org/node/2208429
+    $themes = $this->moduleHandler->buildModuleDependencies($themes);
+
+    // Store filenames to allow system_list() and drupal_get_filename() to
+    // retrieve them without having to scan the filesystem.
+    $this->state->set('system.theme.files', $files);
 
     // After establishing the full list of available themes, fill in data for
     // sub-themes.
@@ -457,6 +620,17 @@ protected function resetSystem() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function getName($theme) {
+    $themes = $this->listInfo();
+    if (!isset($themes[$theme])) {
+      throw new \InvalidArgumentException(String::format('Requested the name of a non-existing theme @theme', array('@theme' => $theme)));
+    }
+    return String::checkPlain($themes[$theme]->info['name']);
+  }
+
+  /**
    * Wraps system_list_reset().
    */
   protected function systemListReset() {
diff --git a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php
index 2ba52cd..da3a6cf 100644
--- a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php
+++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php
@@ -17,28 +17,35 @@
    *
    * @param array $theme_list
    *   An array of theme names.
+   * @param bool $enable_dependencies
+   *   (optional) If TRUE, dependencies will automatically be installed in the
+   *   correct order. This incurs a significant performance cost, so use FALSE
+   *   if you know $theme_list is already complete and in the correct order.
+   *
+   * @return bool
+   *   Whether any of the given themes have been enabled.
    *
    * @throws \Drupal\Core\Extension\ExtensionNameLengthException
    *   Thrown when the theme name is to long
    */
-  public function enable(array $theme_list);
+  public function enable(array $theme_list, $enable_dependencies = TRUE);
 
   /**
    * Disables a given list of themes.
    *
    * @param array $theme_list
    *   An array of theme names.
+   *
+   * @return bool
+   *   Whether any of the given themes have been disabled.
    */
   public function disable(array $theme_list);
 
   /**
-   * Returns a list of all currently available themes.
-   *
-   * Retrieved from the database, if available and the site is not in
-   * maintenance mode; otherwise compiled freshly from the filesystem.
+   * Returns a list of currently enabled themes.
    *
    * @return \Drupal\Core\Extension\Extension[]
-   *   An associative array of the currently available themes. The keys are the
+   *   An associative array of the currently enabled themes. The keys are the
    *   themes' machine names and the values are objects having the following
    *   properties:
    *   - filename: The filepath and name of the .info.yml file.
@@ -76,6 +83,14 @@ public function disable(array $theme_list);
   public function listInfo();
 
   /**
+   * Refreshes the theme info data of currently enabled themes.
+   *
+   * Modules can alter theme info, so this is typically called after a module
+   * has been installed or uninstalled.
+   */
+  public function refreshInfo();
+
+  /**
    * Resets the internal state of the theme handler.
    */
   public function reset();
@@ -105,4 +120,15 @@ public function rebuildThemeData();
    */
   public function getBaseThemes(array $themes, $theme);
 
+  /**
+   * Gets the human readable name of a given theme.
+   *
+   * @param string $theme
+   *   The machine name of the theme which title should be shown.
+   *
+   * @return string
+   *   Returns the human readable name of the theme.
+   */
+  public function getName($theme);
+
 }
diff --git a/core/lib/Drupal/Core/Installer/Form/SelectLanguageForm.php b/core/lib/Drupal/Core/Installer/Form/SelectLanguageForm.php
new file mode 100644
index 0000000..d3667f7
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/Form/SelectLanguageForm.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Installer\Form\SelectLanguageForm.
+ */
+
+namespace Drupal\Core\Installer\Form;
+
+use Drupal\Component\Utility\String;
+use Drupal\Component\Utility\UserAgent;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Language\LanguageManager;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides the language selection form.
+ */
+class SelectLanguageForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'install_select_language_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, $install_state = NULL) {
+    if (count($install_state['translations']) > 1) {
+      $files = $install_state['translations'];
+    }
+    else {
+      $files = array();
+    }
+    $standard_languages = LanguageManager::getStandardLanguageList();
+    $select_options = array();
+    $browser_options = array();
+
+    $form['#title'] = $this->t('Choose language');
+
+    // Build a select list with language names in native language for the user
+    // to choose from. And build a list of available languages for the browser
+    // to select the language default from.
+    if (count($files)) {
+      // Select lists based on available language files.
+      foreach ($files as $langcode => $uri) {
+        $select_options[$langcode] = isset($standard_languages[$langcode]) ? $standard_languages[$langcode][1] : $langcode;
+        $browser_options[] = $langcode;
+      }
+    }
+    else {
+      // Select lists based on all standard languages.
+      foreach ($standard_languages as $langcode => $language_names) {
+        $select_options[$langcode] = $language_names[1];
+        $browser_options[] = $langcode;
+      }
+    }
+
+    $request = Request::createFromGlobals();
+    $browser_langcode = UserAgent::getBestMatchingLangcode($request->server->get('HTTP_ACCEPT_LANGUAGE'), $browser_options);
+    $form['langcode'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Choose language'),
+      '#title_display' => 'invisible',
+      '#options' => $select_options,
+      // Use the browser detected language as default or English if nothing found.
+      '#default_value' => !empty($browser_langcode) ? $browser_langcode : 'en',
+    );
+
+    if (empty($files)) {
+      $form['help'] = array(
+        '#type' => 'item',
+        '#markup' => String::format('<p>Translations will be downloaded from the <a href="http://localize.drupal.org">Drupal Translation website</a>.
+        If you do not want this, select <a href="!english">English</a>.</p>', array(
+            '!english' => install_full_redirect_url(array('parameters' => array('langcode' => 'en'))),
+          )),
+        '#states' => array(
+          'invisible' => array(
+            'select[name="langcode"]' => array('value' => 'en'),
+          ),
+        ),
+      );
+    }
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] =  array(
+      '#type' => 'submit',
+      '#value' => $this->t('Save and continue'),
+      '#button_type' => 'primary',
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $install_state = &$form_state['build_info']['args'][0];
+    $install_state['parameters']['langcode'] = $form_state['values']['langcode'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php b/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php
new file mode 100644
index 0000000..38bb96f
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Installer\Form\SelectProfileForm.
+ */
+
+namespace Drupal\Core\Installer\Form;
+
+use Drupal\Core\Form\FormBase;
+
+/**
+ * Provides the profile selection form.
+ */
+class SelectProfileForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'install_select_profile_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, $install_state = NULL) {
+    $form['#title'] = $this->t('Select an installation profile');
+
+    $profiles = array();
+    $names = array();
+    foreach ($install_state['profiles'] as $profile) {
+      /** @var $profile \Drupal\Core\Extension\Extension */
+      $details = install_profile_info($profile->getName());
+      // Skip this extension if its type is not profile.
+      if (!isset($details['type']) || $details['type'] != 'profile') {
+        continue;
+      }
+      // Don't show hidden profiles. This is used by to hide the testing profile,
+      // which only exists to speed up test runs.
+      if ($details['hidden'] === TRUE) {
+        continue;
+      }
+      $profiles[$profile->getName()] = $details;
+
+      // Determine the name of the profile; default to file name if defined name
+      // is unspecified.
+      $name = isset($details['name']) ? $details['name'] : $profile->getName();
+      $names[$profile->getName()] = $name;
+    }
+
+    // Display radio buttons alphabetically by human-readable name, but always
+    // put the core profiles first (if they are present in the filesystem).
+    natcasesort($names);
+    if (isset($names['minimal'])) {
+      // If the expert ("Minimal") core profile is present, put it in front of
+      // any non-core profiles rather than including it with them alphabetically,
+      // since the other profiles might be intended to group together in a
+      // particular way.
+      $names = array('minimal' => $names['minimal']) + $names;
+    }
+    if (isset($names['standard'])) {
+      // If the default ("Standard") core profile is present, put it at the very
+      // top of the list. This profile will have its radio button pre-selected,
+      // so we want it to always appear at the top.
+      $names = array('standard' => $names['standard']) + $names;
+    }
+
+    // The profile name and description are extracted for translation from the
+    // .info file, so we can use $this->t() on them even though they are dynamic
+    // data at this point.
+    $form['profile'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('Select an installation profile'),
+      '#title_display' => 'invisible',
+      '#options' => array_map(array($this, 't'), $names),
+      '#default_value' => 'standard',
+    );
+    foreach (array_keys($names) as $profile_name) {
+      $form['profile'][$profile_name]['#description'] = isset($profiles[$profile_name]['description']) ? $this->t($profiles[$profile_name]['description']) : '';
+    }
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] =  array(
+      '#type' => 'submit',
+      '#value' => $this->t('Save and continue'),
+      '#button_type' => 'primary',
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    global $install_state;
+    $install_state['parameters']['profile'] = $form_state['values']['profile'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php
new file mode 100644
index 0000000..fdb6783
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php
@@ -0,0 +1,278 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Installer\Form\SiteConfigureForm.
+ */
+
+namespace Drupal\Core\Installer\Form;
+
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Locale\CountryManagerInterface;
+use Drupal\Core\State\StateInterface;
+use Drupal\user\UserStorageInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the site configuration form.
+ */
+class SiteConfigureForm extends FormBase {
+
+  /**
+   * The user storage.
+   *
+   * @var \Drupal\user\UserStorageInterface
+   */
+  protected $userStorage;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The country manager.
+   *
+   * @var \Drupal\Core\Locale\CountryManagerInterface
+   */
+  protected $countryManager;
+
+  /**
+   * Constructs a new SiteConfigureForm.
+   *
+   * @param \Drupal\user\UserStorageInterface $user_storage
+   *   The user storage.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Locale\CountryManagerInterface $country_manager
+   *   The country manager.
+   */
+  public function __construct(UserStorageInterface $user_storage, StateInterface $state, ModuleHandlerInterface $module_handler, CountryManagerInterface $country_manager) {
+    $this->userStorage = $user_storage;
+    $this->state = $state;
+    $this->moduleHandler = $module_handler;
+    $this->countryManager = $country_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager')->getStorage('user'),
+      $container->get('state'),
+      $container->get('module_handler'),
+      $container->get('country_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'install_configure_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $form['#title'] = $this->t('Configure site');
+
+    // Warn about settings.php permissions risk
+    $settings_dir = conf_path();
+    $settings_file = $settings_dir . '/settings.php';
+    // Check that $_POST is empty so we only show this message when the form is
+    // first displayed, not on the next page after it is submitted. (We do not
+    // want to repeat it multiple times because it is a general warning that is
+    // not related to the rest of the installation process; it would also be
+    // especially out of place on the last page of the installer, where it would
+    // distract from the message that the Drupal installation has completed
+    // successfully.)
+    $post_params = $this->getRequest()->request->all();
+    if (empty($post_params) && (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_file, FILE_EXIST|FILE_READABLE|FILE_NOT_WRITABLE) || !drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_dir, FILE_NOT_WRITABLE, 'dir'))) {
+      drupal_set_message(t('All necessary changes to %dir and %file have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the <a href="@handbook_url">online handbook</a>.', array('%dir' => $settings_dir, '%file' => $settings_file, '@handbook_url' => 'http://drupal.org/server-permissions')), 'warning');
+    }
+
+    $form['#attached']['library'][] = 'system/drupal.system';
+    // Add JavaScript time zone detection.
+    $form['#attached']['library'][] = 'core/drupal.timezone';
+    // We add these strings as settings because JavaScript translation does not
+    // work during installation.
+    $js = array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail')));
+    $form['#attached']['js'][] = array('data' => $js, 'type' => 'setting');
+
+    // Cache a fully-built schema. This is necessary for any invocation of
+    // index.php because: (1) setting cache table entries requires schema
+    // information, (2) that occurs during bootstrap before any module are
+    // loaded, so (3) if there is no cached schema, drupal_get_schema() will
+    // try to generate one but with no loaded modules will return nothing.
+    //
+    // @todo Move this to the 'install_finished' task?
+    drupal_get_schema(NULL, TRUE);
+
+    $form['site_information'] = array(
+      '#type' => 'fieldgroup',
+      '#title' => $this->t('Site information'),
+    );
+    $form['site_information']['site_name'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Site name'),
+      '#required' => TRUE,
+      '#weight' => -20,
+    );
+    $form['site_information']['site_mail'] = array(
+      '#type' => 'email',
+      '#title' => $this->t('Site e-mail address'),
+      '#default_value' => ini_get('sendmail_from'),
+      '#description' => $this->t("Automated e-mails, such as registration information, will be sent from this address. Use an address ending in your site's domain to help prevent these e-mails from being flagged as spam."),
+      '#required' => TRUE,
+      '#weight' => -15,
+    );
+
+    $form['admin_account'] = array(
+      '#type' => 'fieldgroup',
+      '#title' => $this->t('Site maintenance account'),
+    );
+    $form['admin_account']['account']['#tree'] = TRUE;
+    $form['admin_account']['account']['name'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Username'),
+      '#maxlength' => USERNAME_MAX_LENGTH,
+      '#description' => $this->t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, and underscores.'),
+      '#required' => TRUE,
+      '#weight' => -10,
+      '#attributes' => array('class' => array('username')),
+    );
+    $form['admin_account']['account']['mail'] = array(
+      '#type' => 'email',
+      '#title' => $this->t('E-mail address'),
+      '#required' => TRUE,
+      '#weight' => -5,
+    );
+    $form['admin_account']['account']['pass'] = array(
+      '#type' => 'password_confirm',
+      '#required' => TRUE,
+      '#size' => 25,
+      '#weight' => 0,
+    );
+
+    $form['regional_settings'] = array(
+      '#type' => 'fieldgroup',
+      '#title' => $this->t('Regional settings'),
+    );
+    $countries = $this->countryManager->getList();
+    $form['regional_settings']['site_default_country'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Default country'),
+      '#empty_value' => '',
+      '#default_value' => $this->config('system.date')->get('country.default'),
+      '#options' => $countries,
+      '#description' => $this->t('Select the default country for the site.'),
+      '#weight' => 0,
+    );
+    $form['regional_settings']['date_default_timezone'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Default time zone'),
+      '#default_value' => date_default_timezone_get(),
+      '#options' => system_time_zones(),
+      '#description' => $this->t('By default, dates in this site will be displayed in the chosen time zone.'),
+      '#weight' => 5,
+      '#attributes' => array('class' => array('timezone-detect')),
+    );
+
+    $form['update_notifications'] = array(
+      '#type' => 'fieldgroup',
+      '#title' => $this->t('Update notifications'),
+    );
+    $form['update_notifications']['update_status_module'] = array(
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Update notifications'),
+      '#options' => array(
+        1 => $this->t('Check for updates automatically'),
+        2 => $this->t('Receive e-mail notifications'),
+      ),
+      '#default_value' => array(1, 2),
+      '#description' => $this->t('The system will notify you when updates and important security releases are available for installed components. Anonymous information about your site is sent to <a href="@drupal">Drupal.org</a>.', array('@drupal' => 'http://drupal.org')),
+      '#weight' => 15,
+    );
+    $form['update_notifications']['update_status_module'][2] = array(
+      '#states' => array(
+        'visible' => array(
+          'input[name="update_status_module[1]"]' => array('checked' => TRUE),
+        ),
+      ),
+    );
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Save and continue'),
+      '#weight' => 15,
+      '#button_type' => 'primary',
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+    if ($error = user_validate_name($form_state['values']['account']['name'])) {
+      $this->setFormError('account][name', $form_state, $error);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $this->config('system.site')
+      ->set('name', $form_state['values']['site_name'])
+      ->set('mail', $form_state['values']['site_mail'])
+      ->save();
+
+    $this->config('system.date')
+      ->set('timezone.default', $form_state['values']['date_default_timezone'])
+      ->set('country.default', $form_state['values']['site_default_country'])
+      ->save();
+
+    // Enable update.module if this option was selected.
+    if ($form_state['values']['update_status_module'][1]) {
+      $this->moduleHandler->install(array('file', 'update'), FALSE);
+
+      // Add the site maintenance account's email address to the list of
+      // addresses to be notified when updates are available, if selected.
+      if ($form_state['values']['update_status_module'][2]) {
+        $this->config('update.settings')->set('notification.emails', array($form_state['values']['account']['mail']))->save();
+      }
+    }
+
+    // We precreated user 1 with placeholder values. Let's save the real values.
+    $account = $this->userStorage->load(1);
+    $account->init = $account->mail = $form_state['values']['account']['mail'];
+    $account->roles = $account->getRoles();
+    $account->activate();
+    $account->timezone = $form_state['values']['date_default_timezone'];
+    $account->pass = $form_state['values']['account']['pass'];
+    $account->name = $form_state['values']['account']['name'];
+    $account->save();
+
+    // Record when this install ran.
+    $this->state->set('install_time', $_SERVER['REQUEST_TIME']);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php b/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php
new file mode 100644
index 0000000..0d4f599
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Installer\Form\SiteSettingsForm.
+ */
+
+namespace Drupal\Core\Installer\Form;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Form\FormBase;
+
+/**
+ * Provides a form to configure and rewrite settings.php.
+ */
+class SiteSettingsForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'install_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    global $databases;
+
+    $conf_path = './' . conf_path(FALSE);
+    $settings_file = $conf_path . '/settings.php';
+
+    $form['#title'] = $this->t('Database configuration');
+
+    $drivers = drupal_get_database_types();
+    $drivers_keys = array_keys($drivers);
+
+    // If database connection settings have been prepared in settings.php already,
+    // then the existing values need to be taken over.
+    // Note: The installer even executes this form if there is a valid database
+    // connection already, since the submit handler of this form is responsible
+    // for writing all $settings to settings.php (not limited to $databases).
+    if (isset($databases['default']['default'])) {
+      $default_driver = $databases['default']['default']['driver'];
+      $default_options = $databases['default']['default'];
+    }
+    // Otherwise, use the database connection settings from the form input.
+    // For a non-interactive installation, this is derived from the original
+    // $settings array passed into install_drupal().
+    elseif (isset($form_state['input']['driver'])) {
+      $default_driver = $form_state['input']['driver'];
+      $default_options = $form_state['input'][$default_driver];
+    }
+    // If there is no database information at all yet, just suggest the first
+    // available driver as default value, so that its settings form is made
+    // visible via #states when JavaScript is enabled (see below).
+    else {
+      $default_driver = current($drivers_keys);
+      $default_options = array();
+    }
+
+    $form['driver'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('Database type'),
+      '#required' => TRUE,
+      '#default_value' => $default_driver,
+    );
+    if (count($drivers) == 1) {
+      $form['driver']['#disabled'] = TRUE;
+    }
+
+    // Add driver specific configuration options.
+    foreach ($drivers as $key => $driver) {
+      $form['driver']['#options'][$key] = $driver->name();
+
+      $form['settings'][$key] = $driver->getFormOptions($default_options);
+      $form['settings'][$key]['#prefix'] = '<h2 class="js-hide">' . $this->t('@driver_name settings', array('@driver_name' => $driver->name())) . '</h2>';
+      $form['settings'][$key]['#type'] = 'container';
+      $form['settings'][$key]['#tree'] = TRUE;
+      $form['settings'][$key]['advanced_options']['#parents'] = array($key);
+      $form['settings'][$key]['#states'] = array(
+        'visible' => array(
+          ':input[name=driver]' => array('value' => $key),
+        )
+      );
+    }
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['save'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Save and continue'),
+      '#button_type' => 'primary',
+      '#limit_validation_errors' => array(
+        array('driver'),
+        array($default_driver),
+      ),
+      '#submit' => array(array($this, 'submitForm')),
+    );
+
+    $form['errors'] = array();
+    $form['settings_file'] = array('#type' => 'value', '#value' => $settings_file);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+    $driver = $form_state['values']['driver'];
+    $database = $form_state['values'][$driver];
+    $drivers = drupal_get_database_types();
+    $reflection = new \ReflectionClass($drivers[$driver]);
+    $install_namespace = $reflection->getNamespaceName();
+    // Cut the trailing \Install from namespace.
+    $database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\'));
+    $database['driver'] = $driver;
+
+    $form_state['storage']['database'] = $database;
+    $errors = install_database_errors($database, $form_state['values']['settings_file']);
+    foreach ($errors as $name => $message) {
+      $this->setFormError($name, $form_state, $message);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    global $install_state;
+
+    // Update global settings array and save.
+    $settings = array();
+    $database = $form_state['storage']['database'];
+    $settings['databases']['default']['default'] = (object) array(
+      'value'    => $database,
+      'required' => TRUE,
+    );
+    $settings['settings']['hash_salt'] = (object) array(
+      'value'    => Crypt::randomBytesBase64(55),
+      'required' => TRUE,
+    );
+    // Remember the profile which was used.
+    $settings['settings']['install_profile'] = (object) array(
+      'value' => $install_state['parameters']['profile'],
+      'required' => TRUE,
+    );
+
+    drupal_rewrite_settings($settings);
+
+    // Add the config directories to settings.php.
+    drupal_install_config_directories();
+
+    // Indicate that the settings file has been verified, and check the database
+    // for the last completed task, now that we have a valid connection. This
+    // last step is important since we want to trigger an error if the new
+    // database already has Drupal installed.
+    $install_state['settings_verified'] = TRUE;
+    $install_state['config_verified'] = TRUE;
+    $install_state['database_verified'] = TRUE;
+    $install_state['completed_task'] = install_verify_completed_task();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php
index 02e96d1..f9eb453 100644
--- a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php
+++ b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php
@@ -16,17 +16,23 @@
 class AnnotatedClassDiscovery extends ComponentAnnotatedClassDiscovery {
 
   /**
-   * The subdirectory within a namespace to look for plugins.
+   * A suffix to append to each PSR-4 directory associated with a base
+   * namespace, to form the directories where plugins are found.
    *
-   * If the plugins are in the top level of the namespace and not within a
-   * subdirectory, set this to an empty string.
+   * @var string
+   */
+  protected $directorySuffix = '';
+
+  /**
+   * A suffix to append to each base namespace, to obtain the namespaces where
+   * plugins are found.
    *
    * @var string
    */
-  protected $subdir = '';
+  protected $namespaceSuffix = '';
 
   /**
-   * An object containing the namespaces to look for plugin implementations.
+   * A list of base namespaces with their PSR-4 directories.
    *
    * @var \Traversable
    */
@@ -48,7 +54,13 @@ class AnnotatedClassDiscovery extends ComponentAnnotatedClassDiscovery {
    */
   function __construct($subdir, \Traversable $root_namespaces, $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin') {
     if ($subdir) {
-      $this->subdir = str_replace('/', '\\', $subdir);
+      // Prepend a directory separator to $subdir,
+      // if it does not already have one.
+      if ('/' !== $subdir[0]) {
+        $subdir = '/' . $subdir;
+      }
+      $this->directorySuffix = $subdir;
+      $this->namespaceSuffix = str_replace('/', '\\', $subdir);
     }
     $this->rootNamespacesIterator = $root_namespaces;
     $plugin_namespaces = array();
@@ -104,11 +116,28 @@ protected function getProviderFromNamespace($namespace) {
    */
   protected function getPluginNamespaces() {
     $plugin_namespaces = array();
-    foreach ($this->rootNamespacesIterator as $namespace => $dir) {
-      if ($this->subdir) {
-        $namespace .= "\\{$this->subdir}";
+    if ($this->namespaceSuffix) {
+      foreach ($this->rootNamespacesIterator as $namespace => $dirs) {
+        // Append the namespace suffix to the base namespace, to obtain the
+        // plugin namespace. E.g. 'Drupal\Views' may become
+        // 'Drupal\Views\Plugin\Block'.
+        $namespace .= $this->namespaceSuffix;
+        foreach ((array) $dirs as $dir) {
+          // Append the directory suffix to the PSR-4 base directory, to obtain
+          // the directory where plugins are found.
+          // E.g. DRUPAL_ROOT . '/core/modules/views/src' may become
+          // DRUPAL_ROOT . '/core/modules/views/src/Plugin/Block'.
+          $plugin_namespaces[$namespace][] = $dir . $this->directorySuffix;
+        }
+      }
+    }
+    else {
+      // Both the namespace suffix and the directory suffix are empty,
+      // so the plugin namespaces and directories are the same as the base
+      // directories.
+      foreach ($this->rootNamespacesIterator as $namespace => $dirs) {
+        $plugin_namespaces[$namespace] = (array) $dirs;
       }
-      $plugin_namespaces[$namespace] = array($dir);
     }
 
     return $plugin_namespaces;
diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php
index e84b33b..402c9f7 100644
--- a/core/lib/Drupal/Core/Theme/Registry.php
+++ b/core/lib/Drupal/Core/Theme/Registry.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\DestructableInterface;
+use Drupal\Core\Extension\Extension;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\Core\Utility\ThemeRegistry;
@@ -145,23 +146,17 @@ public function __construct(CacheBackendInterface $cache, LockBackendInterface $
   protected function init($theme_name = NULL) {
     // Unless instantiated for a specific theme, use globals.
     if (!isset($theme_name)) {
-      // #1: The theme registry might get instantiated before the theme was
-      // initialized. Cope with that.
-      if (!isset($GLOBALS['theme_info']) || !isset($GLOBALS['theme'])) {
-        unset($this->runtimeRegistry);
-        unset($this->registry);
-        drupal_theme_initialize();
+      if (isset($GLOBALS['theme']) && isset($GLOBALS['theme_info'])) {
+        $this->theme = $GLOBALS['theme_info'];
+        $this->baseThemes = $GLOBALS['base_theme_info'];
+        $this->engine = $GLOBALS['theme_engine'];
       }
-      // #2: The testing framework only cares for the global $theme variable at
-      // this point. Cope with that.
-      if ($GLOBALS['theme'] != $GLOBALS['theme_info']->getName()) {
-        unset($this->runtimeRegistry);
-        unset($this->registry);
-        $this->initializeTheme();
+      else {
+        // @see drupal_theme_initialize()
+        $this->theme = new Extension('theme', 'core/core.info.yml');
+        $this->baseThemes = array();
+        $this->engine = 'twig';
       }
-      $this->theme = $GLOBALS['theme_info'];
-      $this->baseThemes = $GLOBALS['base_theme_info'];
-      $this->engine = $GLOBALS['theme_engine'];
     }
     // Instead of the global theme, a specific theme was requested.
     else {
diff --git a/core/lib/Drupal/Core/Utility/Token.php b/core/lib/Drupal/Core/Utility/Token.php
index 27a4519..4a1238e 100644
--- a/core/lib/Drupal/Core/Utility/Token.php
+++ b/core/lib/Drupal/Core/Utility/Token.php
@@ -7,7 +7,10 @@
 
 namespace Drupal\Core\Utility;
 
+use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Language\LanguageManagerInterface;
 
 /**
  * Drupal placeholder/token replacement system.
@@ -55,9 +58,28 @@
 class Token {
 
   /**
+   * The tag to cache token info with.
+   */
+  const TOKEN_INFO_CACHE_TAG = 'token_info';
+
+  /**
+   * The token cache.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
    * Token definitions.
    *
-   * @var array|null
+   * @var array[]|null
    *   An array of token definitions, or NULL when the definitions are not set.
    *
    * @see self::setInfo()
@@ -74,11 +96,18 @@ class Token {
   protected $moduleHandler;
 
   /**
-   * Constructor.
+   * Constructs a new class instance.
    *
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The token cache.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
    */
-  public function __construct(ModuleHandlerInterface $module_handler) {
+  public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
+    $this->cache = $cache;
+    $this->languageManager = $language_manager;
     $this->moduleHandler = $module_handler;
   }
 
@@ -282,8 +311,18 @@ public function findWithPrefix(array $tokens, $prefix, $delimiter = ':') {
    */
   public function getInfo() {
     if (is_null($this->tokenInfo)) {
-      $this->tokenInfo = $this->moduleHandler->invokeAll('token_info');
-      $this->moduleHandler->alter('token_info', $this->tokenInfo);
+      $cache_id = 'token_info:' . $this->languageManager->getCurrentLanguage(Language::TYPE_CONTENT)->id;
+      $cache = $this->cache->get($cache_id);
+      if ($cache) {
+        $this->tokenInfo = $cache->data;
+      }
+      else {
+        $this->tokenInfo = $this->moduleHandler->invokeAll('token_info');
+        $this->moduleHandler->alter('token_info', $this->tokenInfo);
+        $this->cache->set($cache_id, $this->tokenInfo, CacheBackendInterface::CACHE_PERMANENT, array(
+          static::TOKEN_INFO_CACHE_TAG => TRUE,
+        ));
+      }
     }
 
     return $this->tokenInfo;
@@ -307,5 +346,8 @@ public function setInfo(array $tokens) {
    */
   public function resetInfo() {
     $this->tokenInfo = NULL;
+    $this->cache->deleteTags(array(
+      static::TOKEN_INFO_CACHE_TAG => TRUE,
+    ));
   }
 }
diff --git a/core/modules/action/action.routing.yml b/core/modules/action/action.routing.yml
index 09e1433..535830a 100644
--- a/core/modules/action/action.routing.yml
+++ b/core/modules/action/action.routing.yml
@@ -10,6 +10,7 @@ action.admin_add:
   path: '/admin/config/system/actions/add/{action_id}'
   defaults:
     _entity_form: 'action.add'
+    _title: 'Add'
   requirements:
     _permission: 'administer actions'
 
@@ -17,6 +18,7 @@ action.admin_configure:
   path: '/admin/config/system/actions/configure/{action}'
   defaults:
     _entity_form: 'action.edit'
+    _title: 'Edit'
   requirements:
     _permission: 'administer actions'
 
@@ -24,6 +26,7 @@ action.delete:
   path: '/admin/config/system/actions/configure/{action}/delete'
   defaults:
     _entity_form: 'action.delete'
+    _title: 'Delete'
   requirements:
     _permission: 'administer actions'
 
diff --git a/core/modules/block/block.routing.yml b/core/modules/block/block.routing.yml
index 0ea084f..1289c68 100644
--- a/core/modules/block/block.routing.yml
+++ b/core/modules/block/block.routing.yml
@@ -2,6 +2,7 @@ block.admin_demo:
   path: '/admin/structure/block/demo/{theme}'
   defaults:
     _content: '\Drupal\block\Controller\BlockController::demo'
+    _title_callback: 'theme_handler:getName'
   requirements:
     _access_theme: 'TRUE'
     _permission: 'administer blocks'
@@ -20,6 +21,7 @@ block.admin_edit:
   path: '/admin/structure/block/manage/{block}'
   defaults:
     _entity_form: 'block.default'
+    _title: 'Configure block'
   requirements:
     _entity_access: 'block.update'
 
diff --git a/core/modules/block/css/block.admin.css b/core/modules/block/css/block.admin.css
index 42ef70d..bd0d7fd 100644
--- a/core/modules/block/css/block.admin.css
+++ b/core/modules/block/css/block.admin.css
@@ -104,11 +104,10 @@ screen and (min-width: 780px),
  * toolbar width (240px). In this case, 240px + 780px.
  */
 @media
-screen and (max-width: 1020px),
-(orientation: landscape) and (max-device-height: 1020px) {
+screen and (max-width: 1020px) {
 
-  .toolbar-vertical .block-list-primary,
-  .toolbar-vertical .block-list-secondary {
+  .toolbar-vertical.toolbar-tray-open .block-list-primary,
+  .toolbar-vertical.toolbar-tray-open .block-list-secondary {
     float: none;
     width: auto;
     padding-right: 0;
diff --git a/core/modules/block/custom_block/custom_block.routing.yml b/core/modules/block/custom_block/custom_block.routing.yml
index 6cea531..e3b0d87 100644
--- a/core/modules/block/custom_block/custom_block.routing.yml
+++ b/core/modules/block/custom_block/custom_block.routing.yml
@@ -2,6 +2,7 @@ custom_block.type_list:
   path: '/admin/structure/block/custom-blocks/types'
   defaults:
     _entity_list: 'custom_block_type'
+    _title: 'Edit'
   requirements:
     _permission: 'administer blocks'
 
@@ -48,6 +49,7 @@ custom_block.delete:
   path: '/block/{custom_block}/delete'
   defaults:
     _entity_form: 'custom_block.delete'
+    _title: 'Delete'
   options:
     _admin_route: TRUE
   requirements:
@@ -57,6 +59,7 @@ custom_block.type_add:
   path: '/admin/structure/block/custom-blocks/types/add'
   defaults:
     _entity_form: 'custom_block_type.add'
+    _title: 'Add'
   requirements:
     _permission: 'administer blocks'
 
@@ -64,6 +67,7 @@ custom_block.type_edit:
   path: '/admin/structure/block/custom-blocks/manage/{custom_block_type}'
   defaults:
     _entity_form: 'custom_block_type.edit'
+    _title: 'Edit'
   requirements:
     _entity_access: 'custom_block_type.update'
 
diff --git a/core/modules/block/lib/Drupal/block/Controller/BlockController.php b/core/modules/block/lib/Drupal/block/Controller/BlockController.php
index 7b713e4..a4e7952 100644
--- a/core/modules/block/lib/Drupal/block/Controller/BlockController.php
+++ b/core/modules/block/lib/Drupal/block/Controller/BlockController.php
@@ -9,6 +9,9 @@
 
 use Drupal\Component\Utility\String;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Extension\ThemeHandler;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -17,6 +20,32 @@
 class BlockController extends ControllerBase {
 
   /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * Constructs a new BlockController instance.
+   *
+   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
+   *   The theme handler.
+   */
+  public function __construct(ThemeHandlerInterface $theme_handler) {
+    $this->themeHandler = $theme_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('theme_handler')
+    );
+  }
+
+  /**
    * Returns a block theme demo page.
    *
    * @param string $theme
@@ -26,9 +55,8 @@ class BlockController extends ControllerBase {
    *   A render array containing the CSS and title for the block region demo.
    */
   public function demo($theme) {
-    $themes = list_themes();
     return array(
-      '#title' => String::checkPlain($themes[$theme]->info['name']),
+      '#title' => String::checkPlain($this->themeHandler->getName($theme)),
       '#attached' => array(
         'js' => array(
           array(
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 539cce8..4115444 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -465,7 +465,7 @@ function comment_node_links_alter(array &$node_links, NodeInterface $node, array
               $links['comment-add'] += $node->urlInfo()->toArray();
             }
           }
-          else {
+          elseif (\Drupal::currentUser()->isAnonymous()) {
             $links['comment-forbidden'] = array(
               'title' => \Drupal::service('comment.manager')->forbiddenMessage($node, $field_name),
               'html' => TRUE,
@@ -500,7 +500,7 @@ function comment_node_links_alter(array &$node_links, NodeInterface $node, array
               }
             }
           }
-          else {
+          elseif (\Drupal::currentUser()->isAnonymous()) {
             $links['comment-forbidden'] = array(
               'title' => \Drupal::service('comment.manager')->forbiddenMessage($node, $field_name),
               'html' => TRUE,
@@ -844,6 +844,10 @@ function comment_translation_configuration_element_submit($form, &$form_state) {
  * @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::propertyDefinitions()
  */
 function comment_entity_load($entities, $entity_type) {
+  // Comments can only be attached to content entities, so skip others.
+  if (!\Drupal::entityManager()->getDefinition($entity_type)->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) {
+    return;
+  }
   if (!\Drupal::service('comment.manager')->getFields($entity_type)) {
     // Do not query database when entity has no comment fields.
     return;
diff --git a/core/modules/comment/comment.services.yml b/core/modules/comment/comment.services.yml
index 8524045..76fdbc4 100644
--- a/core/modules/comment/comment.services.yml
+++ b/core/modules/comment/comment.services.yml
@@ -7,7 +7,7 @@ services:
 
   comment.manager:
     class: Drupal\comment\CommentManager
-    arguments: ['@field.info', '@entity.manager', '@current_user', '@config.factory', '@string_translation', '@url_generator']
+    arguments: ['@field.info', '@entity.manager', '@config.factory', '@string_translation', '@url_generator']
 
   comment.statistics:
     class: Drupal\comment\CommentStatistics
diff --git a/core/modules/comment/lib/Drupal/comment/CommentManager.php b/core/modules/comment/lib/Drupal/comment/CommentManager.php
index c522fd3..fcb3277 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentManager.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentManager.php
@@ -37,13 +37,6 @@ class CommentManager implements CommentManagerInterface {
   protected $entityManager;
 
   /**
-   * The current user.
-   *
-   * @var \Drupal\Core\Session\AccountInterface $current_user
-   */
-  protected $currentUser;
-
-  /**
    * Whether the DRUPAL_AUTHENTICATED_RID can post comments.
    *
    * @var bool
@@ -78,8 +71,6 @@ class CommentManager implements CommentManagerInterface {
    *   The field info service.
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager service.
-   * @param \Drupal\Core\Session\AccountInterface $current_user
-   *   The current user.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
@@ -87,10 +78,9 @@ class CommentManager implements CommentManagerInterface {
    * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
    *   The url generator service.
    */
-  public function __construct(FieldInfo $field_info, EntityManagerInterface $entity_manager, AccountInterface $current_user, ConfigFactoryInterface $config_factory, TranslationInterface $translation_manager, UrlGeneratorInterface $url_generator) {
+  public function __construct(FieldInfo $field_info, EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, TranslationInterface $translation_manager, UrlGeneratorInterface $url_generator) {
     $this->fieldInfo = $field_info;
     $this->entityManager = $entity_manager;
-    $this->currentUser = $current_user;
     $this->userConfig = $config_factory->get('user.settings');
     $this->translationManager = $translation_manager;
     $this->urlGenerator = $url_generator;
@@ -277,39 +267,37 @@ public function getFieldUIPageTitle($commented_entity_type, $field_name) {
    * {@inheritdoc}
    */
   public function forbiddenMessage(EntityInterface $entity, $field_name) {
-    if ($this->currentUser->isAnonymous()) {
-      if (!isset($this->authenticatedCanPostComments)) {
-        // We only output a link if we are certain that users will get the
-        // permission to post comments by logging in.
-        $this->authenticatedCanPostComments = $this->entityManager
-          ->getStorage('user_role')
-          ->load(DRUPAL_AUTHENTICATED_RID)
-          ->hasPermission('post comments');
-      }
+    if (!isset($this->authenticatedCanPostComments)) {
+      // We only output a link if we are certain that users will get the
+      // permission to post comments by logging in.
+      $this->authenticatedCanPostComments = $this->entityManager
+        ->getStorage('user_role')
+        ->load(DRUPAL_AUTHENTICATED_RID)
+        ->hasPermission('post comments');
+    }
 
-      if ($this->authenticatedCanPostComments) {
-        // We cannot use drupal_get_destination() because these links
-        // sometimes appear on /node and taxonomy listing pages.
-        if ($entity->get($field_name)->getFieldDefinition()->getSetting('form_location') == COMMENT_FORM_SEPARATE_PAGE) {
-          $destination = array('destination' => 'comment/reply/' . $entity->getEntityTypeId() . '/' . $entity->id() . '/' . $field_name . '#comment-form');
-        }
-        else {
-          $destination = array('destination' => $entity->getSystemPath() . '#comment-form');
-        }
+    if ($this->authenticatedCanPostComments) {
+      // We cannot use drupal_get_destination() because these links
+      // sometimes appear on /node and taxonomy listing pages.
+      if ($entity->get($field_name)->getFieldDefinition()->getSetting('form_location') == COMMENT_FORM_SEPARATE_PAGE) {
+        $destination = array('destination' => 'comment/reply/' . $entity->getEntityTypeId() . '/' . $entity->id() . '/' . $field_name . '#comment-form');
+      }
+      else {
+        $destination = array('destination' => $entity->getSystemPath() . '#comment-form');
+      }
 
-        if ($this->userConfig->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY) {
-          // Users can register themselves.
-          return $this->t('<a href="@login">Log in</a> or <a href="@register">register</a> to post comments', array(
-            '@login' => $this->urlGenerator->generateFromRoute('user.login', array(), array('query' => $destination)),
-            '@register' => $this->urlGenerator->generateFromRoute('user.register', array(), array('query' => $destination)),
-          ));
-        }
-        else {
-          // Only admins can add new users, no public registration.
-          return $this->t('<a href="@login">Log in</a> to post comments', array(
-            '@login' => $this->urlGenerator->generateFromRoute('user.login', array(), array('query' => $destination)),
-          ));
-        }
+      if ($this->userConfig->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY) {
+        // Users can register themselves.
+        return $this->t('<a href="@login">Log in</a> or <a href="@register">register</a> to post comments', array(
+          '@login' => $this->urlGenerator->generateFromRoute('user.login', array(), array('query' => $destination)),
+          '@register' => $this->urlGenerator->generateFromRoute('user.register', array(), array('query' => $destination)),
+        ));
+      }
+      else {
+        // Only admins can add new users, no public registration.
+        return $this->t('<a href="@login">Log in</a> to post comments', array(
+          '@login' => $this->urlGenerator->generateFromRoute('user.login', array(), array('query' => $destination)),
+        ));
       }
     }
     return '';
diff --git a/core/modules/comment/lib/Drupal/comment/CommentStorage.php b/core/modules/comment/lib/Drupal/comment/CommentStorage.php
index d05b2c3..f59a2be 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentStorage.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentStorage.php
@@ -74,12 +74,12 @@ protected function buildQuery($ids, $revision_id = FALSE) {
   /**
    * {@inheritdoc}
    */
-  protected function postLoad(array &$queried_entities) {
+  protected function mapFromStorageRecords(array $records) {
     // Prepare standard comment fields.
-    foreach ($queried_entities as &$record) {
+    foreach ($records as $record) {
       $record->name = $record->uid ? $record->registered_name : $record->name;
     }
-    parent::postLoad($queried_entities);
+    return parent::mapFromStorageRecords($records);
   }
 
   /**
diff --git a/core/modules/comment/lib/Drupal/comment/CommentViewBuilder.php b/core/modules/comment/lib/Drupal/comment/CommentViewBuilder.php
index 3661080..da28f7b 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentViewBuilder.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentViewBuilder.php
@@ -241,14 +241,14 @@ protected static function buildLinks(CommentInterface $entity, EntityInterface $
           'html' => TRUE,
         );
       }
-      if (empty($links)) {
+      if (empty($links) && \Drupal::currentUser()->isAnonymous()) {
         $links['comment-forbidden']['title'] = \Drupal::service('comment.manager')->forbiddenMessage($commented_entity, $entity->getFieldName());
         $links['comment-forbidden']['html'] = TRUE;
       }
     }
 
     // Add translations link for translation-enabled comment bundles.
-    if ($container->get('module_handler')->moduleExists('content_translation') && content_translation_translate_access($entity)) {
+    if (\Drupal::moduleHandler()->moduleExists('content_translation') && content_translation_translate_access($entity)) {
       $links['comment-translations'] = array(
         'title' => t('Translate'),
         'href' => 'comment/' . $entity->id() . '/translations',
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
index 92b7656..b95a666 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
@@ -211,7 +211,7 @@ function testImport() {
     $this->assertTrue(empty($installed), 'No modules installed during import');
 
     $theme_info = \Drupal::service('theme_handler')->listInfo();
-    $this->assertTrue(isset($theme_info['bartik']) && !$theme_info['bartik']->status, 'Bartik theme disabled during import.');
+    $this->assertFalse(isset($theme_info['bartik']), 'Bartik theme disabled during import.');
 
     // Verify that the action.settings configuration object was only deleted
     // once during the import process.
diff --git a/core/modules/config_translation/config_translation.module b/core/modules/config_translation/config_translation.module
index 74047f7..e30f09a 100644
--- a/core/modules/config_translation/config_translation.module
+++ b/core/modules/config_translation/config_translation.module
@@ -56,6 +56,28 @@ function config_translation_theme() {
 }
 
 /**
+ * Implements hook_themes_enabled().
+ */
+function config_translation_themes_enabled() {
+  // Themes can provide *.config_translation.yml declarations.
+  // @todo Make ThemeHandler trigger an event instead and make
+  //   ConfigMapperManager plugin manager subscribe to it.
+  // @see https://drupal.org/node/2206347
+  \Drupal::service('plugin.manager.config_translation.mapper')->clearCachedDefinitions();
+}
+
+/**
+ * Implements hook_themes_disabled().
+ */
+function config_translation_themes_disabled() {
+  // Themes can provide *.config_translation.yml declarations.
+  // @todo Make ThemeHandler trigger an event instead and make
+  //   ConfigMapperManager plugin manager subscribe to it.
+  // @see https://drupal.org/node/2206347
+  \Drupal::service('plugin.manager.config_translation.mapper')->clearCachedDefinitions();
+}
+
+/**
  * Implements hook_entity_type_alter().
  */
 function config_translation_entity_type_alter(array &$entity_types) {
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php
index bdfbe55..e90c5bf 100644
--- a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php
+++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php
@@ -67,6 +67,14 @@ public function __construct(CacheBackendInterface $cache_backend, LanguageManage
     $this->typedConfigManager = $typed_config_manager;
 
     // Look at all themes and modules.
+    // @todo If the list of enabled modules and themes is changed, new
+    //   definitions are not picked up immediately and obsolete definitions are
+    //   not removed, because the list of search directories is only compiled
+    //   once in this constructor. The current code only works due to
+    //   coincidence: The request that enables e.g. a new theme does not
+    //   instantiate this plugin manager at the beginning of the request; when
+    //   routes are being rebuilt at the end of the request, this service only
+    //   happens to get instantiated with the updated list of enabled themes.
     $directories = array();
     foreach ($module_handler->getModuleList() as $name => $module) {
       $directories[$name] = $module->getPath();
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php
index 551b7f2..46ef47f 100644
--- a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php
@@ -655,25 +655,6 @@ public function testAlterInfo() {
   }
 
   /**
-   * Tests that theme provided *.config_translation.yml files are found.
-   */
-  public function testThemeDiscovery() {
-    // Enable the test theme and rebuild routes.
-    $theme = 'config_translation_test_theme';
-    theme_enable(array($theme));
-    // Enabling a theme will cause the kernel terminate event to rebuild the
-    // router. Simulate that here.
-    \Drupal::service('router.builder')->rebuildIfNeeded();
-
-    $this->drupalLogin($this->admin_user);
-
-    $translation_base_url = 'admin/config/development/performance/translate';
-    $this->drupalGet($translation_base_url);
-    $this->assertResponse(200);
-    $this->assertLinkByHref("$translation_base_url/fr/add");
-  }
-
-  /**
    * Gets translation from locale storage.
    *
    * @param $config_name
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiThemeTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiThemeTest.php
new file mode 100644
index 0000000..b51cee3
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiThemeTest.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigTranslationUiThemeTest.
+ */
+
+namespace Drupal\config_translation\Tests;
+
+use Drupal\Core\Language\Language;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Functional tests for the Language list configuration forms.
+ */
+class ConfigTranslationUiThemeTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('config_translation', 'config_translation_test');
+
+  /**
+   * Languages to enable.
+   *
+   * @var array
+   */
+  protected $langcodes = array('fr', 'ta');
+
+  /**
+   * Administrator user for tests.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $admin_user;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Theme Configuration Translation',
+      'description' => 'Verifies theme configuration translation settings.',
+      'group' => 'Configuration Translation',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+
+    $admin_permissions = array(
+      'administer themes',
+      'administer languages',
+      'administer site configuration',
+      'translate configuration',
+    );
+    // Create and login user.
+    $this->admin_user = $this->drupalCreateUser($admin_permissions);
+
+    // Add languages.
+    foreach ($this->langcodes as $langcode) {
+      $language = new Language(array('id' => $langcode));
+      language_save($language);
+    }
+  }
+
+  /**
+   * Tests that theme provided *.config_translation.yml files are found.
+   */
+  public function testThemeDiscovery() {
+    // Enable the test theme and rebuild routes.
+    $theme = 'config_translation_test_theme';
+
+    $this->drupalLogin($this->admin_user);
+
+    $this->drupalGet('admin/appearance');
+    $elements = $this->xpath('//a[normalize-space()=:label and contains(@href, :theme)]', array(
+      ':label' => 'Enable and set as default',
+      ':theme' => $theme,
+    ));
+    $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
+
+    $translation_base_url = 'admin/config/development/performance/translate';
+    $this->drupalGet($translation_base_url);
+    $this->assertResponse(200);
+    $this->assertLinkByHref("$translation_base_url/fr/add");
+  }
+
+}
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
index c71166f..d38be97 100644
--- a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
@@ -5,6 +5,18 @@
  * Configuration Translation Test module.
  */
 
+use Drupal\Core\Extension\Extension;
+
+/**
+ * Implements hook_system_info_alter().
+ */
+function config_translation_test_system_info_alter(array &$info, Extension $file, $type) {
+  // @see \Drupal\config_translation\Tests\ConfigTranslationUiThemeTest
+  if ($file->getType() == 'theme' && $file->getName() == 'config_translation_test_theme') {
+    $info['hidden'] = FALSE;
+  }
+}
+
 /**
  * Implements hook_entity_type_alter().
  */
diff --git a/core/modules/editor/editor.routing.yml b/core/modules/editor/editor.routing.yml
index b99b8ab..8b5e5c1 100644
--- a/core/modules/editor/editor.routing.yml
+++ b/core/modules/editor/editor.routing.yml
@@ -19,6 +19,7 @@ editor.image_dialog:
   path: '/editor/dialog/image/{filter_format}'
   defaults:
     _form: '\Drupal\editor\Form\EditorImageDialog'
+    _title: 'Upload image'
   requirements:
     _entity_access: 'filter_format.use'
 
@@ -26,5 +27,6 @@ editor.link_dialog:
   path: '/editor/dialog/link/{filter_format}'
   defaults:
     _form: '\Drupal\editor\Form\EditorLinkDialog'
+    _title: 'Add link'
   requirements:
     _entity_access: 'filter_format.use'
diff --git a/core/modules/field/config/install/field.settings.yml b/core/modules/field/config/install/field.settings.yml
index b6172c1..1578b10 100644
--- a/core/modules/field/config/install/field.settings.yml
+++ b/core/modules/field/config/install/field.settings.yml
@@ -1 +1 @@
-purge_batch_size: 10
+purge_batch_size: 50
diff --git a/core/modules/field/field.module b/core/modules/field/field.module
index 029db41..cc30633 100644
--- a/core/modules/field/field.module
+++ b/core/modules/field/field.module
@@ -6,6 +6,7 @@
 
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\Config\ConfigImporter;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Extension\Extension;
 use Drupal\field\Field;
@@ -362,3 +363,49 @@ function field_hook_info() {
 
   return $hooks;
 }
+
+/**
+ * Implements hook_config_import_steps_alter().
+ */
+function field_config_import_steps_alter(&$sync_steps, ConfigImporter $config_importer) {
+  $fields = \Drupal\field\ConfigImporterFieldPurger::getFieldsToPurge(
+    $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension'),
+    $config_importer->getStorageComparer()->getChangelist('delete')
+  );
+  if ($fields) {
+    // Add a step to the beginning of the configuration synchronization process
+    // to purge field data where the module that provides the field is being
+    // uninstalled.
+    array_unshift($sync_steps, array('\Drupal\field\ConfigImporterFieldPurger', 'process'));
+  };
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Adds a warning if field data will be permanently removed by the configuration
+ * synchronization.
+ *
+ * @see \Drupal\field\ConfigImporterFieldPurger
+ */
+function field_form_config_admin_import_form_alter(&$form, &$form_state) {
+  // Only display the message when there is a storage comparer available and the
+  // form is not submitted.
+  if (isset($form_state['storage_comparer']) && empty($form_state['input'])) {
+    $fields = \Drupal\field\ConfigImporterFieldPurger::getFieldsToPurge(
+      $form_state['storage_comparer']->getSourceStorage()->read('core.extension'),
+      $form_state['storage_comparer']->getChangelist('delete')
+    );
+    if ($fields) {
+      foreach ($fields as $field) {
+        $field_labels[] = $field->label();
+      }
+      drupal_set_message(\Drupal::translation()->formatPlural(
+        count($fields),
+        'This synchronization will delete data from the field %fields.',
+        'This synchronization will delete data from the fields: %fields.',
+        array('%fields' => implode(', ', $field_labels))
+      ), 'warning');
+    }
+  }
+}
diff --git a/core/modules/field/field.purge.inc b/core/modules/field/field.purge.inc
index 212c2b5..6a3347b 100644
--- a/core/modules/field/field.purge.inc
+++ b/core/modules/field/field.purge.inc
@@ -68,11 +68,18 @@
  *
  * @param $batch_size
  *   The maximum number of field data records to purge before returning.
+ * @param string $field_uuid
+ *   (optional) Limit the purge to a specific field.
  */
-function field_purge_batch($batch_size) {
+function field_purge_batch($batch_size, $field_uuid = NULL) {
   // Retrieve all deleted field instances. We cannot use field_info_instances()
   // because that function does not return deleted instances.
-  $instances = entity_load_multiple_by_properties('field_instance_config', array('deleted' => TRUE, 'include_deleted' => TRUE));
+  if ($field_uuid) {
+    $instances = entity_load_multiple_by_properties('field_instance_config', array('deleted' => TRUE, 'include_deleted' => TRUE, 'field_uuid' => $field_uuid));
+  }
+  else {
+    $instances = entity_load_multiple_by_properties('field_instance_config', array('deleted' => TRUE, 'include_deleted' => TRUE));
+  }
   $factory = \Drupal::service('entity.query');
   $info = \Drupal::entityManager()->getDefinitions();
   foreach ($instances as $instance) {
@@ -104,6 +111,11 @@ function field_purge_batch($batch_size) {
         $ids->entity_id = $entity_id;
         $entity = _field_create_entity_from_ids($ids);
         \Drupal::entityManager()->getStorage($entity_type)->onFieldItemsPurge($entity, $instance);
+        $batch_size--;
+      }
+      // Only delete up to the maximum number of records.
+      if ($batch_size == 0) {
+        break;
       }
     }
     else {
@@ -116,6 +128,10 @@ function field_purge_batch($batch_size) {
   $deleted_fields = \Drupal::state()->get('field.field.deleted') ?: array();
   foreach ($deleted_fields as $field) {
     $field = new FieldConfig($field);
+    if ($field_uuid && $field->uuid() != $field_uuid) {
+      // If a specific UUID is provided, only purge the corresponding field.
+      continue;
+    }
 
     // We cannot purge anything if the entity type is unknown (e.g. the
     // providing module was uninstalled).
diff --git a/core/modules/field/lib/Drupal/field/ConfigImporterFieldPurger.php b/core/modules/field/lib/Drupal/field/ConfigImporterFieldPurger.php
new file mode 100644
index 0000000..db3d8b9
--- /dev/null
+++ b/core/modules/field/lib/Drupal/field/ConfigImporterFieldPurger.php
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\field\ConfigImporterFieldPurger.
+ */
+
+namespace Drupal\field;
+
+use Drupal\Core\Config\ConfigImporter;
+use Drupal\Core\Config\Entity\ConfigEntityStorage;
+
+/**
+ * Processes field purges before a configuration synchronization.
+ */
+class ConfigImporterFieldPurger {
+
+  /**
+   * Processes fields targeted for purge as part of a configuration sync.
+   *
+   * This takes care of deleting the field if necessary, and purging the data on
+   * the fly.
+   *
+   * @param array $context
+   *   The batch context.
+   * @param \Drupal\Core\Config\ConfigImporter $config_importer
+   *   The config importer.
+   */
+  public static function process(array &$context, ConfigImporter $config_importer) {
+    if (!isset($context['sandbox']['field'])) {
+      static::initializeSandbox($context, $config_importer);
+    }
+
+    // Get the list of fields to purge.
+    $fields = static::getFieldsToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete'));
+    // Get the first field to process.
+    $field = reset($fields);
+    if (!isset($context['sandbox']['field']['current_field_id']) || $context['sandbox']['field']['current_field_id'] != $field->id()) {
+      $context['sandbox']['field']['current_field_id'] = $field->id();
+      // If the field has not been deleted yet we need to do that. This is the
+      // case when the field deletion is staged.
+      if (!$field->deleted) {
+        $field->delete();
+      }
+    }
+    field_purge_batch($context['sandbox']['field']['purge_batch_size'], $field->uuid());
+    $context['sandbox']['field']['current_progress']++;
+    $fields_to_delete_count = count(static::getFieldsToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete')));
+    if ($fields_to_delete_count == 0) {
+      $context['finished'] = 1;
+    }
+    else {
+      $context['finished'] = $context['sandbox']['field']['current_progress'] / $context['sandbox']['field']['steps_to_delete'];
+      $context['message'] = \Drupal::translation()->translate('Purging field @field_label', array('@field_label' => $field->label()));
+    }
+  }
+
+  /**
+   * Initializes the batch context sandbox for processing field deletions.
+   *
+   * This calculates the number of steps necessary to purge all the field data
+   * and saves data for later use.
+   *
+   * @param array $context
+   *   The batch context.
+   * @param \Drupal\Core\Config\ConfigImporter $config_importer
+   *   The config importer.
+   */
+  protected static function initializeSandbox(array &$context, ConfigImporter $config_importer) {
+    $context['sandbox']['field']['purge_batch_size'] = \Drupal::config('field.settings')->get('purge_batch_size');
+    // Save the future list of installed extensions to limit the amount of times
+    // the configuration is read from disk.
+    $context['sandbox']['field']['extensions'] = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
+
+    $context['sandbox']['field']['steps_to_delete'] = 0;
+    $fields = static::getFieldsToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete'));
+    foreach ($fields as $field) {
+      $row_count = $field->entityCount();
+      if ($row_count > 0) {
+        // The number of steps to delete each field is determined by the
+        // purge_batch_size setting. For example if the field has 9 rows and the
+        // batch size is 10 then this will add 1 step to $number_of_steps.
+        $how_many_steps = ceil($row_count / $context['sandbox']['field']['purge_batch_size']);
+        $context['sandbox']['field']['steps_to_delete'] += $how_many_steps;
+      }
+    }
+    // Each field needs one last field_purge_batch() call to remove the last
+    // instance and the field itself.
+    $context['sandbox']['field']['steps_to_delete'] += count($fields);
+
+    $context['sandbox']['field']['current_progress'] = 0;
+  }
+
+  /**
+   * Gets the list of fields to purge before configuration synchronization.
+   *
+   * If, during a configuration synchronization, a field is being deleted and
+   * the module that provides the field type is being uninstalled then the field
+   * data must be purged before the module is uninstalled. Also, if deleted
+   * fields exist whose field types are provided by modules that are being
+   * uninstalled their data need to be purged too.
+   *
+   * @param array $extensions
+   *   The list of extensions that will be enabled after the configuration
+   *   synchronization has finished.
+   * @param array $deletes
+   *   The configuration that will be deleted by the configuration
+   *   synchronization.
+   *
+   * @return \Drupal\field\Entity\FieldConfig[]
+   *   An array of fields that need purging before configuration can be
+   *   synchronized.
+   */
+  public static function getFieldsToPurge(array $extensions, array $deletes) {
+    $providers = array_keys($extensions['module']);
+    $providers[] = 'Core';
+    $fields_to_delete = array();
+
+    // Gather fields that will be deleted during configuration synchronization
+    // where the module that provides the field type is also being uninstalled.
+    $field_ids = array();
+    foreach ($deletes as $config_name) {
+      if (strpos($config_name, 'field.field.') === 0) {
+        $field_ids[] = ConfigEntityStorage::getIDFromConfigName($config_name, 'field.field');
+      }
+    }
+    if (!empty($field_ids)) {
+      $fields = \Drupal::entityQuery('field_config')
+        ->condition('id', $field_ids, 'IN')
+        ->condition('module', $providers, 'NOT IN')
+        ->execute();
+      if (!empty($fields)) {
+        $fields_to_delete = entity_load_multiple('field_config', $fields);
+      }
+    }
+
+    // Gather deleted fields from modules that are being uninstalled.
+    $fields = entity_load_multiple_by_properties('field_config', array('deleted' => TRUE, 'include_deleted' => TRUE));
+    foreach ($fields as $field) {
+      if (!in_array($field->module, $providers)) {
+        $fields_to_delete[$field->id()] = $field;
+      }
+    }
+    return $fields_to_delete;
+  }
+
+}
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallTest.php
new file mode 100644
index 0000000..102c525
--- /dev/null
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallTest.php
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\field\Tests\FieldImportDeleteUninstallTest.
+ */
+
+namespace Drupal\field\Tests;
+
+/**
+ * Tests config sync of deleting fields and instances and uninstalling modules.
+ *
+ * @see \Drupal\field\ConfigImporterFieldPurger
+ * @see field_config_import_steps_alter()
+ */
+class FieldImportDeleteUninstallTest extends FieldUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('telephone');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Field config delete and uninstall tests',
+      'description' => 'Delete field and instances during config synchronization and uninstall module that provides the field type.',
+      'group' => 'Field API',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+    // Module uninstall requires the router and users_data tables.
+    // @see drupal_flush_all_caches()
+    // @see user_modules_uninstalled()
+    $this->installSchema('system', array('router'));
+    $this->installSchema('user', array('users_data'));
+  }
+
+  /**
+   * Tests deleting fields and instances as part of config import.
+   */
+  public function testImportDeleteUninstall() {
+    // Create a field to delete to prove that
+    // \Drupal\field\ConfigImporterFieldPurger does not purge fields that are
+    // not related to the configuration synchronization.
+    $unrelated_field = entity_create('field_config', array(
+      'name' => 'field_int',
+      'entity_type' => 'entity_test',
+      'type' => 'integer',
+    ));
+    $unrelated_field->save();
+    $unrelated_field_uuid = $unrelated_field->uuid();
+    entity_create('field_instance_config', array(
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_int',
+      'bundle' => 'entity_test',
+    ))->save();
+
+    // Create a telephone field and instance for validation.
+    $field = entity_create('field_config', array(
+      'name' => 'field_test',
+      'entity_type' => 'entity_test',
+      'type' => 'telephone',
+    ));
+    $field->save();
+    $field_uuid = $field->uuid();
+    entity_create('field_instance_config', array(
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_test',
+      'bundle' => 'entity_test',
+    ))->save();
+
+    $entity = entity_create('entity_test');
+    $value = '+0123456789';
+    $entity->field_test = $value;
+    $entity->field_int = '99';
+    $entity->name->value = $this->randomName();
+    $entity->save();
+
+    // Verify entity has been created properly.
+    $id = $entity->id();
+    $entity = entity_load('entity_test', $id);
+    $this->assertEqual($entity->field_test->value, $value);
+    $this->assertEqual($entity->field_test[0]->value, $value);
+    $this->assertEqual($entity->field_int->value, '99');
+
+    // Delete unrelated field before copying configuration and running the
+    // synchronization.
+    $unrelated_field->delete();
+
+    $active = $this->container->get('config.storage');
+    $staging = $this->container->get('config.storage.staging');
+    $this->copyConfig($active, $staging);
+
+    // Stage uninstall of the Telephone module.
+    $core_extension = \Drupal::config('core.extension')->get();
+    unset($core_extension['module']['telephone']);
+    $staging->write('core.extension', $core_extension);
+
+    // Stage the field deletion
+    $staging->delete('field.field.entity_test.field_test');
+    $staging->delete('field.instance.entity_test.entity_test.field_test');
+
+    $steps = $this->configImporter()->initialize();
+    $this->assertIdentical($steps[0], array('\Drupal\field\ConfigImporterFieldPurger', 'process'), 'The additional process configuration synchronization step has been added.');
+
+    $this->configImporter()->import();
+
+    // This will purge all the data, delete the field and uninstall the
+    // Telephone module.
+    $this->rebuildContainer();
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('telephone'));
+    $this->assertFalse(entity_load_by_uuid('field_config', $field_uuid), 'The test field has been deleted by the configuration synchronization');
+    $deleted_fields = \Drupal::state()->get('field.field.deleted') ?: array();
+    $this->assertFalse(isset($deleted_fields[$field_uuid]), 'Telephone field has been completed removed from the system.');
+    $this->assertTrue(isset($deleted_fields[$unrelated_field_uuid]), 'Unrelated field not purged by configuration synchronization.');
+  }
+
+  /**
+   * Tests purging already deleted fields and instances during a config import.
+   */
+  public function testImportAlreadyDeletedUninstall() {
+    // Create a telephone field and instance for validation.
+    $field = entity_create('field_config', array(
+      'name' => 'field_test',
+      'entity_type' => 'entity_test',
+      'type' => 'telephone',
+    ));
+    $field->save();
+    $field_uuid = $field->uuid();
+    entity_create('field_instance_config', array(
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_test',
+      'bundle' => 'entity_test',
+    ))->save();
+
+    // Create 12 entities to ensure that the purging works as expected.
+    for ($i=0; $i < 12; $i++) {
+      $entity = entity_create('entity_test');
+      $value = '+0123456789';
+      $entity->field_test = $value;
+      $entity->name->value = $this->randomName();
+      $entity->save();
+
+      // Verify entity has been created properly.
+      $id = $entity->id();
+      $entity = entity_load('entity_test', $id);
+      $this->assertEqual($entity->field_test->value, $value);
+    }
+
+    // Delete the field.
+    $field->delete();
+
+    $active = $this->container->get('config.storage');
+    $staging = $this->container->get('config.storage.staging');
+    $this->copyConfig($active, $staging);
+
+    // Stage uninstall of the Telephone module.
+    $core_extension = \Drupal::config('core.extension')->get();
+    unset($core_extension['module']['telephone']);
+    $staging->write('core.extension', $core_extension);
+
+    $deleted_fields = \Drupal::state()->get('field.field.deleted') ?: array();
+    $this->assertTrue(isset($deleted_fields[$field_uuid]), 'Field has been deleted and needs purging before configuration synchronization.');
+
+    $steps = $this->configImporter()->initialize();
+    $this->assertIdentical($steps[0], array('\Drupal\field\ConfigImporterFieldPurger', 'process'), 'The additional process configuration synchronization step has been added.');
+
+    $this->configImporter()->import();
+
+    // This will purge all the data, delete the field and uninstall the
+    // Telephone module.
+    $this->rebuildContainer();
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('telephone'));
+    $deleted_fields = \Drupal::state()->get('field.field.deleted') ?: array();
+    $this->assertFalse(isset($deleted_fields[$field_uuid]), 'Field has been completed removed from the system.');
+  }
+
+}
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallUiTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallUiTest.php
new file mode 100644
index 0000000..93bdb75
--- /dev/null
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallUiTest.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\field\Tests\FieldImportDeleteUninstallUiTest.
+ */
+
+namespace Drupal\field\Tests;
+
+/**
+ * Tests config sync of deleting fields and instances and uninstalling modules.
+ *
+ * @see \Drupal\field\ConfigImporterFieldPurger
+ * @see field_config_import_steps_alter()
+ * @see field_form_config_admin_import_form_alter()
+ */
+class FieldImportDeleteUninstallUiTest extends FieldTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('entity_test', 'telephone', 'config', 'filter', 'text');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Field config delete and uninstall UI tests',
+      'description' => 'Delete field and instances during config synchronization and uninstall module that provides the field type through the UI.',
+      'group' => 'Field API',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->web_user = $this->drupalCreateUser(array('synchronize configuration'));
+    $this->drupalLogin($this->web_user);
+  }
+
+  /**
+   * Tests deleting fields and instances as part of config import.
+   */
+  public function testImportDeleteUninstall() {
+    // Create a telephone field and instance.
+    $field = entity_create('field_config', array(
+      'name' => 'field_tel',
+      'entity_type' => 'entity_test',
+      'type' => 'telephone',
+    ));
+    $field->save();
+    $tel_field_uuid = $field->uuid();
+    entity_create('field_instance_config', array(
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_tel',
+      'bundle' => 'entity_test',
+    ))->save();
+
+    // Create a text field and instance.
+    $text_field = entity_create('field_config', array(
+      'name' => 'field_text',
+      'entity_type' => 'entity_test',
+      'type' => 'text',
+    ));
+    $text_field->save();
+    $text_field_uuid = $field->uuid();
+    entity_create('field_instance_config', array(
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_text',
+      'bundle' => 'entity_test',
+    ))->save();
+
+    // Create an entity which has values for the telephone and text field.
+    $entity = entity_create('entity_test');
+    $value = '+0123456789';
+    $entity->field_tel = $value;
+    $entity->field_text = $this->randomName(20);
+    $entity->name->value = $this->randomName();
+    $entity->save();
+
+    // Delete the text field before exporting configuration so that we can test
+    // that deleted fields that are provided by modules that will be uninstalled
+    // are also purged and that the UI message includes such fields.
+    $text_field->delete();
+
+    // Verify entity has been created properly.
+    $id = $entity->id();
+    $entity = entity_load('entity_test', $id);
+    $this->assertEqual($entity->field_tel->value, $value);
+    $this->assertEqual($entity->field_tel[0]->value, $value);
+
+    $active = $this->container->get('config.storage');
+    $staging = $this->container->get('config.storage.staging');
+    $this->copyConfig($active, $staging);
+
+    // Stage uninstall of the Telephone module.
+    $core_extension = \Drupal::config('core.extension')->get();
+    unset($core_extension['module']['telephone']);
+    $staging->write('core.extension', $core_extension);
+
+    // Stage the field deletion
+    $staging->delete('field.field.entity_test.field_tel');
+    $staging->delete('field.instance.entity_test.entity_test.field_tel');
+    $this->drupalGet('admin/config/development/configuration');
+    // Test that the message for one field being purged during a configuration
+    // synchronization is correct.
+    $this->assertText('This synchronization will delete data from the field entity_test.field_tel.');
+
+    // Stage an uninstall of the text module to test the message for multiple
+    // fields.
+    unset($core_extension['module']['text']);
+    $staging->write('core.extension', $core_extension);
+    $this->drupalGet('admin/config/development/configuration');
+    $this->assertText('This synchronization will delete data from the fields: entity_test.field_tel, entity_test.field_text.');
+
+    // This will purge all the data, delete the field and uninstall the
+    // Telephone and Text modules.
+    $this->drupalPostForm(NULL, array(), t('Import all'));
+    $this->assertNoText('Field data will be deleted by this synchronization.');
+    $this->rebuildContainer();
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('telephone'));
+    $this->assertFalse(entity_load_by_uuid('field_config', $tel_field_uuid), 'The telephone field has been deleted by the configuration synchronization');
+    $deleted_fields = \Drupal::state()->get('field.field.deleted') ?: array();
+    $this->assertFalse(isset($deleted_fields[$tel_field_uuid]), 'Telephone field has been completed removed from the system.');
+    $this->assertFalse(isset($deleted_fields[$text_field_uuid]), 'Text field has been completed removed from the system.');
+  }
+
+}
diff --git a/core/modules/filter/filter.routing.yml b/core/modules/filter/filter.routing.yml
index 1e261f1..1017e2c 100644
--- a/core/modules/filter/filter.routing.yml
+++ b/core/modules/filter/filter.routing.yml
@@ -35,6 +35,7 @@ filter.format_edit:
   path: '/admin/config/content/formats/manage/{filter_format}'
   defaults:
     _entity_form: filter_format.edit
+    _title_callback: '\Drupal\filter\Controller\FilterController::getLabel'
   requirements:
     _entity_access: 'filter_format.update'
 
diff --git a/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php b/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php
index ae8bc01..fe4d94f 100644
--- a/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php
+++ b/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php
@@ -37,4 +37,17 @@ function filterTips(FilterFormatInterface $filter_format = NULL) {
     return $build;
   }
 
+  /**
+   * Gets the label of a filter format.
+   *
+   * @param \Drupal\filter\FilterFormatInterface $filter_format
+   *   The filter format.
+   *
+   * @return string
+   *   The label of the filter format.
+   */
+  public function getLabel(FilterFormatInterface $filter_format) {
+    return $filter_format->label();
+  }
+
 }
diff --git a/core/modules/help/lib/Drupal/help/Tests/HelpTest.php b/core/modules/help/lib/Drupal/help/Tests/HelpTest.php
index dd21f0e..58de88d 100644
--- a/core/modules/help/lib/Drupal/help/Tests/HelpTest.php
+++ b/core/modules/help/lib/Drupal/help/Tests/HelpTest.php
@@ -88,6 +88,15 @@ public function testHelp() {
    *   An HTTP response code.
    */
   protected function verifyHelp($response = 200) {
+    $this->drupalGet('admin/index');
+    $this->assertResponse($response);
+    if ($response == 200) {
+      $this->assertText('This page shows you all available administration tasks for each module.');
+    }
+    else {
+      $this->assertNoText('This page shows you all available administration tasks for each module.');
+    }
+
     foreach ($this->getModuleList() as $module => $name) {
       // View module help node.
       $this->drupalGet('admin/help/' . $module);
diff --git a/core/modules/language/language.module b/core/modules/language/language.module
index 6a17f73..845407d 100644
--- a/core/modules/language/language.module
+++ b/core/modules/language/language.module
@@ -431,9 +431,7 @@ function language_save($language) {
   $language_entity->direction = isset($language->direction) ? $language->direction : '0';
   $language_entity->locked = !empty($language->locked);
   $language_entity->weight = isset($language->weight) ? $language->weight : 0;
-
-  // Save the record and inform others about the change.
-  $multilingual = \Drupal::languageManager()->isMultilingual();
+  $language_entity->setDefault(!empty($language->default));
   $language_entity->save();
   $t_args = array('%language' => $language->name, '%langcode' => $language->id);
   if ($language->is_new) {
@@ -443,30 +441,6 @@ function language_save($language) {
     watchdog('language', 'The %language (%langcode) language has been updated.', $t_args);
   }
 
-  if (!empty($language->default)) {
-    // Update the config. Saving the configuration fires and event that causes
-    // the container to be rebuilt.
-    \Drupal::config('system.site')->set('langcode', $language->id)->save();
-    \Drupal::service('language.default')->set($language);
-  }
-
-  $language_manager = \Drupal::languageManager();
-  $language_manager->reset();
-  if ($language_manager instanceof ConfigurableLanguageManagerInterface) {
-    $language_manager->updateLockedLanguageWeights();
-  }
-
-  // Update URL Prefixes for all languages after the new default language is
-  // propagated and the LanguageManagerInterface::getLanguages() cache is
-  // flushed.
-  language_negotiation_url_prefixes_update();
-
-  // If after adding this language the site will become multilingual, we need to
-  // rebuild language services.
-  if (!$multilingual && $language->is_new) {
-    ConfigurableLanguageManager::rebuildServices();
-  }
-
   return $language;
 }
 
diff --git a/core/modules/language/language.routing.yml b/core/modules/language/language.routing.yml
index cda5754..d3ad61c 100644
--- a/core/modules/language/language.routing.yml
+++ b/core/modules/language/language.routing.yml
@@ -2,6 +2,7 @@ language.negotiation_url:
   path: '/admin/config/regional/language/detection/url'
   defaults:
     _form: 'Drupal\language\Form\NegotiationUrlForm'
+    _title: 'Configure URL language negotiation'
   requirements:
     _permission: 'administer languages'
 
@@ -9,6 +10,7 @@ language.negotiation_session:
   path: '/admin/config/regional/language/detection/session'
   defaults:
     _form: 'Drupal\language\Form\NegotiationSessionForm'
+    _title: 'Configure session language negotiation'
   requirements:
     _permission: 'administer languages'
 
@@ -16,6 +18,7 @@ language.negotiation_selected:
   path: '/admin/config/regional/language/detection/selected'
   defaults:
     _form: 'Drupal\language\Form\NegotiationSelectedForm'
+    _title: 'Configure selection language negotiation'
   requirements:
     _permission: 'administer languages'
 
@@ -71,6 +74,7 @@ language.negotiation_browser_delete:
   path: '/admin/config/regional/language/detection/browser/delete/{browser_langcode}'
   defaults:
     _form: '\Drupal\language\Form\NegotiationBrowserDeleteForm'
+    _title: 'Delete'
   requirements:
     _permission: 'administer languages'
 
diff --git a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php
index b164cd5..a414119 100644
--- a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php
+++ b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php
@@ -92,7 +92,7 @@ class ConfigurableLanguageManager extends LanguageManager implements Configurabl
   protected $initializing = FALSE;
 
   /**
-   * Rebuild the container to register services needed on multilingual sites.
+   * {@inheritdoc}
    */
   public static function rebuildServices() {
     PhpStorageFactory::get('service_container')->deleteAll();
diff --git a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php
index d2f4f36..9ec0d91 100644
--- a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php
+++ b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php
@@ -17,6 +17,11 @@
 interface ConfigurableLanguageManagerInterface extends LanguageManagerInterface {
 
   /**
+   * Rebuild the container to register services needed on multilingual sites.
+   */
+  public static function rebuildServices();
+
+  /**
    * Injects the request object.
    *
    * @param \Symfony\Component\HttpFoundation\Request
diff --git a/core/modules/language/lib/Drupal/language/Entity/Language.php b/core/modules/language/lib/Drupal/language/Entity/Language.php
index 77015cd..8a2f9fe 100644
--- a/core/modules/language/lib/Drupal/language/Entity/Language.php
+++ b/core/modules/language/lib/Drupal/language/Entity/Language.php
@@ -7,8 +7,10 @@
 
 namespace Drupal\language\Entity;
 
+use Drupal\Core\Language\Language as LanguageObject;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\language\ConfigurableLanguageManagerInterface;
 use Drupal\language\Exception\DeleteDefaultLanguageException;
 use Drupal\language\LanguageInterface;
 
@@ -78,10 +80,65 @@ class Language extends ConfigEntityBase implements LanguageInterface {
   public $locked = FALSE;
 
   /**
+   * Flag to indicate if the language entity is the default site language.
+   *
+   * This property is not saved to the language entity since there can be only
+   * one default language. It is saved to system.site:langcode and set on the
+   * container using the language.default service in when the entity is saved.
+   * The value is set correctly when a language entity is created or loaded.
+   *
+   * @see \Drupal\language\Entity\Language::postSave()
+   * @see \Drupal\language\Entity\Language::isDefault()
+   * @see \Drupal\language\Entity\Language::setDefault()
+   *
+   * @var bool
+   */
+  protected $default;
+
+  /**
+   * Used during saving to detect when the site becomes multilingual.
+   *
+   * This property is not saved to the language entity, but is needed for
+   * detecting when to rebuild the services.
+   *
+   * @see \Drupal\language\Entity\Language::preSave()
+   * @see \Drupal\language\Entity\Language::postSave()
+   *
+   * @var bool
+   */
+  protected $preSaveMultilingual;
+
+  /**
+   * Sets the default flag on the language entity.
+   *
+   * @param bool $default
+   */
+  public function setDefault($default) {
+    $this->default = $default;
+  }
+
+  /**
+   * Checks if the language entity is the site default language.
+   *
+   * @return bool
+   *   TRUE if the language entity is the site default language, FALSE if not.
+   */
+  public function isDefault() {
+    if (!isset($this->default)) {
+      return static::getDefaultLangcode() == $this->id();
+    }
+    return $this->default;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function preSave(EntityStorageInterface $storage) {
     parent::preSave($storage);
+    // Store whether or not the site is already multilingual so that we can
+    // rebuild services if necessary during
+    // \Drupal\language\Entity\Language::postSave().
+    $this->preSaveMultilingual = \Drupal::languageManager()->isMultilingual();
     // Languages are picked from a predefined list which is given in English.
     // For the uncommon case of custom languages the label should be given in
     // English.
@@ -90,15 +147,92 @@ public function preSave(EntityStorageInterface $storage) {
 
   /**
    * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+
+    // Only set the default language and save it to system.site configuration if
+    // it needs to updated.
+    if ($this->isDefault() && static::getDefaultLangcode() != $this->id()) {
+      // Update the config. Saving the configuration fires and event that causes
+      // the container to be rebuilt.
+      \Drupal::config('system.site')->set('langcode', $this->id())->save();
+      \Drupal::service('language.default')->set($this->toLanguageObject());
+    }
+
+    $language_manager = \Drupal::languageManager();
+    $language_manager->reset();
+    if ($language_manager instanceof ConfigurableLanguageManagerInterface) {
+      $language_manager->updateLockedLanguageWeights();
+    }
+
+    // Update URL Prefixes for all languages after the new default language is
+    // propagated and the LanguageManagerInterface::getLanguages() cache is
+    // flushed.
+    language_negotiation_url_prefixes_update();
+
+    // If after adding this language the site will become multilingual, we need
+    // to rebuild language services.
+    if (!$this->preSaveMultilingual && !$update && $language_manager instanceof ConfigurableLanguageManagerInterface) {
+      $language_manager::rebuildServices();
+    }
+  }
+
+  /**
+   * Converts the Language entity to a Language value object.
+   *
+   * @todo fix return type hint after https://drupal.org/node/2246665 and
+   *   https://drupal.org/node/2246679.
+   *
+   * @return \Drupal\Core\Language\Language
+   *   The language configuration entity expressed as a Language value object.
+   */
+  protected function toLanguageObject() {
+    return new LanguageObject(array(
+      'id' => $this->id(),
+      'name' => $this->label(),
+      'direction' => $this->direction,
+      'weight' => $this->weight,
+      'locked' => $this->locked,
+      'default' => $this->default,
+    ));
+  }
+
+  /**
+   * {@inheritdoc}
    *
    * @throws \RuntimeException
    */
   public static function preDelete(EntityStorageInterface $storage, array $entities) {
-    $default_language = \Drupal::service('language.default')->get();
+    $default_langcode = static::getDefaultLangcode();
     foreach ($entities as $entity) {
-      if ($entity->id() == $default_language->id && !$entity->isUninstalling()) {
+      if ($entity->id() == $default_langcode && !$entity->isUninstalling()) {
         throw new DeleteDefaultLanguageException('Can not delete the default language');
       }
     }
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($property_name) {
+    if ($property_name == 'default') {
+      return $this->isDefault();
+    }
+    else {
+      return parent::get($property_name);
+    }
+  }
+
+  /**
+   * Gets the default langcode.
+   *
+   * @return string
+   *   The current default langcode.
+   */
+  protected static function getDefaultLangcode() {
+    $language = \Drupal::service('language.default')->get();
+    return $language->getId();
+  }
+
 }
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageConfigurationElementTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageConfigurationElementTest.php
index 0a507f2..c7c85f1 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageConfigurationElementTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageConfigurationElementTest.php
@@ -88,6 +88,9 @@ public function testDefaultLangcode() {
 
     // Site's default.
     $old_default = \Drupal::languageManager()->getDefaultLanguage();
+    // Ensure the language entity default value is correct.
+    $language_entity = entity_load('language_entity', $old_default->getId());
+    $this->assertTrue($language_entity->get('default'), 'The en language entity is flagged as the default language.');
     $old_default->default = FALSE;
     language_save($old_default);
     $new_default = \Drupal::languageManager()->getLanguage('cc');
@@ -97,6 +100,14 @@ public function testDefaultLangcode() {
     $langcode = language_get_default_langcode('custom_type', 'custom_bundle');
     $this->assertEqual($langcode, 'cc');
 
+    // Ensure the language entity default value is correct.
+    $language_entity = entity_load('language_entity', $old_default->getId());
+    $this->assertFalse($language_entity->get('default'), 'The en language entity is not flagged as the default language.');
+    $language_entity = entity_load('language_entity', 'cc');
+    // Check calling the Drupal\language\Entity\Language::isDefault() method
+    // directly.
+    $this->assertTrue($language_entity->isDefault(), 'The cc language entity is flagged as the default language.');
+
     // Check the default value of a language field when authors preferred option
     // is selected.
     // Create first an user and assign a preferred langcode to him.
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorage.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorage.php
index ea05968..60871d0 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorage.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorage.php
@@ -138,12 +138,10 @@ public function loadUpdatedCustomized(array $router_paths) {
       );
     $query_result = $query->execute();
 
-    if ($class = $this->entityType->getClass()) {
-      // We provide the necessary arguments for PDO to create objects of the
-      // specified entity class.
-      // @see \Drupal\Core\Entity\EntityInterface::__construct()
-      $query_result->setFetchMode(\PDO::FETCH_CLASS, $class, array(array(), $this->entityTypeId));
-    }
+    // We provide the necessary arguments for PDO to create objects of the
+    // specified entity class.
+    // @see \Drupal\Core\Entity\EntityInterface::__construct()
+    $query_result->setFetchMode(\PDO::FETCH_CLASS, $this->entityClass, array(array(), $this->entityTypeId));
 
     return $query_result->fetchAllAssoc($this->idKey);
   }
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateActionConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateActionConfigsTest.php
index 0d15565..7cb9862 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateActionConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateActionConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_action_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6ActionSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6ActionSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateAggregatorConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateAggregatorConfigsTest.php
index 4a13937..42b8633 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateAggregatorConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateAggregatorConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_aggregator_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6AggregatorSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6AggregatorSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateBookConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateBookConfigsTest.php
index 228b537..5a5fbd4 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateBookConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateBookConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_book_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6BookSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6BookSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateContactConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateContactConfigsTest.php
index 3609266..21fc8b6 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateContactConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateContactConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_contact_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6ContactSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6ContactSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateDblogConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateDblogConfigsTest.php
index 7bfda84..333f815 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateDblogConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateDblogConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_dblog_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6DblogSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6DblogSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFieldConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFieldConfigsTest.php
index d5647e3..9f02830 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFieldConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFieldConfigsTest.php
@@ -33,7 +33,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_field_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6FieldSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6FieldSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFileConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFileConfigsTest.php
index e35fff9..a09f17f 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFileConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFileConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_file_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6FileSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6FileSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateForumConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateForumConfigsTest.php
index 9c564f9..9ebecf1 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateForumConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateForumConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_forum_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6ForumSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6ForumSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateLocaleConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateLocaleConfigsTest.php
index 4c633e1..c84aee2 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateLocaleConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateLocaleConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_locale_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6LocaleSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6LocaleSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateMenuConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateMenuConfigsTest.php
index 7c26613..b14cfc5 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateMenuConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateMenuConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_menu_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6MenuSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6MenuSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateNodeConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateNodeConfigsTest.php
index a20ba71..39edf2e 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateNodeConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateNodeConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_node_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6NodeSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6NodeSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSearchConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSearchConfigsTest.php
index f1b86bd..f34891d 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSearchConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSearchConfigsTest.php
@@ -41,7 +41,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_search_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6SearchSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6SearchSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSimpletestConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSimpletestConfigsTest.php
index 24115e3..00b3512 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSimpletestConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSimpletestConfigsTest.php
@@ -41,7 +41,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_simpletest_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6SimpletestSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6SimpletestSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateStatisticsConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateStatisticsConfigsTest.php
index 36c47d2..0be390e 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateStatisticsConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateStatisticsConfigsTest.php
@@ -41,7 +41,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_statistics_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6StatisticsSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6StatisticsSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSyslogConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSyslogConfigsTest.php
index 971cf87..86b3892 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSyslogConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSyslogConfigsTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_syslog_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6SyslogSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6SyslogSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTaxonomyConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTaxonomyConfigsTest.php
index 4a6b7d6..07b60cc 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTaxonomyConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTaxonomyConfigsTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_taxonomy_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6TaxonomySettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6TaxonomySettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTextConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTextConfigsTest.php
index d33015e..8fc8489 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTextConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTextConfigsTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_text_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6TextSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6TextSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUpdateConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUpdateConfigsTest.php
index c685edf..2296598 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUpdateConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUpdateConfigsTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_update_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6UpdateSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6UpdateSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserConfigsTest.php
index 37211ea..6c41f5d 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserConfigsTest.php
@@ -33,7 +33,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_user_mail');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6UserMail.php',
+      dirname(__DIR__) . '/Dump/Drupal6UserMail.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
index a70a207..46147f7 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
@@ -49,10 +49,9 @@ public function setUp() {
 
     /** @var \Drupal\migrate\entity\Migration $migration */
     $migration = entity_load('migration', 'd6_user_role');
-    $path = drupal_get_path('module', 'migrate_drupal');
     $dumps = array(
-      $path . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6UserRole.php',
-      $path . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6FilterFormat.php',
+      dirname(__DIR__) . '/Dump/Drupal6UserRole.php',
+      dirname(__DIR__) . '/Dump/Drupal6FilterFormat.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/node/css/node.module.css b/core/modules/node/css/node.module.css
index 1f92354..5ae0e5f 100644
--- a/core/modules/node/css/node.module.css
+++ b/core/modules/node/css/node.module.css
@@ -56,12 +56,11 @@
  * toolbar width (240px). In this case, 240px + 780px.
  */
 @media
-  screen and (max-width: 1020px),
-  (orientation: landscape) and (max-device-height: 1020px) {
+  screen and (max-width: 1020px) {
 
-  .toolbar-vertical .layout-region-node-main,
-  .toolbar-vertical .layout-region-node-footer,
-  .toolbar-vertical .layout-region-node-secondary {
+  .toolbar-vertical.toolbar-tray-open .layout-region-node-main,
+  .toolbar-vertical.toolbar-tray-open .layout-region-node-footer,
+  .toolbar-vertical.toolbar-tray-open .layout-region-node-secondary {
     float: none;
     width: auto;
     padding-right: 0;
diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml
index 617b16a..e7ba52d 100644
--- a/core/modules/node/node.routing.yml
+++ b/core/modules/node/node.routing.yml
@@ -56,6 +56,7 @@ node.delete_confirm:
   path: '/node/{node}/delete'
   defaults:
     _entity_form: 'node.delete'
+    _title: 'Delete'
   requirements:
     _entity_access: 'node.delete'
   options:
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php
index 3add3aa..a1ae295 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php
@@ -19,7 +19,7 @@ class AuthTest extends RESTTestBase {
    *
    * @var array
    */
-  public static $modules = array('basic_auth', 'hal', 'rest', 'entity_test');
+  public static $modules = array('basic_auth', 'hal', 'rest', 'entity_test', 'comment');
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchPageTextTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPageTextTest.php
index 771976f..91019e6 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchPageTextTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchPageTextTest.php
@@ -66,7 +66,8 @@ function testSearchText() {
     $limit = \Drupal::config('search.settings')->get('and_or_limit');
     $keys = array();
     for ($i = 0; $i < $limit + 1; $i++) {
-      $keys[] = $this->randomName(3);
+      // Use a key of 4 characters to ensure we never generate 'AND' or 'OR'.
+      $keys[] = $this->randomName(4);
       if ($i % 2 == 0) {
         $keys[] = 'OR';
       }
diff --git a/core/modules/search/search.routing.yml b/core/modules/search/search.routing.yml
index b75ae6e..38afb9f 100644
--- a/core/modules/search/search.routing.yml
+++ b/core/modules/search/search.routing.yml
@@ -57,6 +57,7 @@ search.delete:
   path: '/admin/config/search/pages/manage/{search_page}/delete'
   defaults:
     _entity_form: 'search_page.delete'
+    _title: 'Delete'
   requirements:
     _entity_access: 'search_page.delete'
 
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Access/LinkAccessCheck.php b/core/modules/shortcut/lib/Drupal/shortcut/Access/LinkAccessCheck.php
deleted file mode 100644
index b85fb9f..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Access/LinkAccessCheck.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\shortcut\Access\LinkAccessCheck.
- */
-
-namespace Drupal\shortcut\Access;
-
-use Drupal\Core\Routing\Access\AccessInterface;
-use Drupal\Core\Session\AccountInterface;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Provides an access check for shortcut link delete routes.
- */
-class LinkAccessCheck implements AccessInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access(Route $route, Request $request, AccountInterface $account) {
-    $menu_link = $request->attributes->get('menu_link');
-    $set_name = str_replace('shortcut-', '', $menu_link['menu_name']);
-    if ($shortcut_set = shortcut_set_load($set_name)) {
-      return shortcut_set_edit_access($shortcut_set) ? static::ALLOW : static::DENY;
-    }
-    return static::DENY;
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Access/ShortcutSetEditAccessCheck.php b/core/modules/shortcut/lib/Drupal/shortcut/Access/ShortcutSetEditAccessCheck.php
deleted file mode 100644
index 21bbda8..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Access/ShortcutSetEditAccessCheck.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\shortcut\Access\ShortcutSetEditAccessCheck.
- */
-
-namespace Drupal\shortcut\Access;
-
-use Drupal\Core\Routing\Access\AccessInterface;
-use Drupal\Core\Session\AccountInterface;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Provides an access check for shortcut link delete routes.
- */
-class ShortcutSetEditAccessCheck implements AccessInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access(Route $route, Request $request, AccountInterface $account) {
-    $account = \Drupal::currentUser();
-    $shortcut_set = $request->attributes->get('shortcut_set');
-    // Sufficiently-privileged users can edit their currently displayed shortcut
-    // set, but not other sets. Shortcut administrators can edit any set.
-    if ($account->hasPermission('administer shortcuts')) {
-      return static::ALLOW;
-    }
-    if ($account->hasPermission('customize shortcut links')) {
-      return !isset($shortcut_set) || $shortcut_set == shortcut_current_displayed_set() ? static::ALLOW : static::DENY;
-    }
-    return static::DENY;
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Access/ShortcutSetSwitchAccessCheck.php b/core/modules/shortcut/lib/Drupal/shortcut/Access/ShortcutSetSwitchAccessCheck.php
deleted file mode 100644
index 6d844ed..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Access/ShortcutSetSwitchAccessCheck.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\shortcut\Access\ShortcutSetSwitchAccessCheck.
- */
-
-namespace Drupal\shortcut\Access;
-
-use Drupal\Core\Routing\Access\AccessInterface;
-use Drupal\Core\Session\AccountInterface;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Provides an access check for shortcut link delete routes.
- */
-class ShortcutSetSwitchAccessCheck implements AccessInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access(Route $route, Request $request, AccountInterface $account) {
-    if ($account->hasPermission('administer shortcuts')) {
-      // Administrators can switch anyone's shortcut set.
-      return static::ALLOW;
-    }
-
-    if (!$account->hasPermission('switch shortcut sets')) {
-      // The user has no permission to switch anyone's shortcut set.
-      return static::DENY;
-    }
-
-    $user = $request->attributes->get('account');
-    if (!isset($user) || $user->id() == $account->id()) {
-      // Users with the 'switch shortcut sets' permission can switch their own
-      // shortcuts sets.
-      return static::ALLOW;
-    }
-    return static::DENY;
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutController.php b/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutController.php
deleted file mode 100644
index 8f3b481..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutController.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Controller\ShortcutController.
- */
-
-namespace Drupal\shortcut\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\shortcut\ShortcutSetInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Provides route responses for taxonomy.module.
- */
-class ShortcutController extends ControllerBase {
-
-  /**
-   * Returns a rendered edit form to create a new shortcut associated to the
-   * given shortcut set.
-   *
-   * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set
-   *   The shortcut set this shortcut will be added to.
-   *
-   * @return array
-   *   The shortcut add form.
-   */
-  public function addForm(ShortcutSetInterface $shortcut_set) {
-    $shortcut = $this->entityManager()->getStorage('shortcut')->create(array('shortcut_set' => $shortcut_set->id()));
-    if ($this->moduleHandler()->moduleExists('language')) {
-      $shortcut->langcode = language_get_default_langcode('shortcut', $shortcut_set->id());
-    }
-    return $this->entityFormBuilder()->getForm($shortcut, 'add');
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php b/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php
deleted file mode 100644
index e63e027..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Controller\ShortcutSetController.
- */
-
-namespace Drupal\shortcut\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\shortcut\ShortcutSetInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
-
-/**
- * Builds the page for administering shortcut sets.
- */
-class ShortcutSetController extends ControllerBase {
-
-  /**
-   * Creates a new link in the provided shortcut set.
-   *
-   * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set
-   *   The shortcut set to add a link to.
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   *
-   * @return \Symfony\Component\HttpFoundation\RedirectResponse
-   *   A redirect response to the front page, or the previous location.
-   *
-   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
-   */
-  public function addShortcutLinkInline(ShortcutSetInterface $shortcut_set, Request $request) {
-    $link = $request->query->get('link');
-    $name = $request->query->get('name');
-    if (shortcut_valid_link($link)) {
-      $shortcut = $this->entityManager()->getStorage('shortcut')->create(array(
-        'title' => $name,
-        'shortcut_set' => $shortcut_set->id(),
-        'path' => $link,
-      ));
-
-      try {
-        $shortcut->save();
-        drupal_set_message($this->t('Added a shortcut for %title.', array('%title' => $shortcut->label())));
-      }
-      catch (\Exception $e) {
-        drupal_set_message($this->t('Unable to add a shortcut for %title.', array('%title' => $shortcut->label())));
-      }
-
-      return $this->redirect('<front>');
-    }
-
-    throw new AccessDeniedHttpException();
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Entity/Shortcut.php b/core/modules/shortcut/lib/Drupal/shortcut/Entity/Shortcut.php
deleted file mode 100644
index 00d0dd3..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Entity/Shortcut.php
+++ /dev/null
@@ -1,202 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Entity\Shortcut.
- */
-
-namespace Drupal\shortcut\Entity;
-
-use Drupal\Core\Entity\ContentEntityBase;
-use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\FieldDefinition;
-use Drupal\Core\Url;
-use Drupal\shortcut\ShortcutInterface;
-
-/**
- * Defines the shortcut entity class.
- *
- * @ContentEntityType(
- *   id = "shortcut",
- *   label = @Translation("Shortcut link"),
- *   controllers = {
- *     "access" = "Drupal\shortcut\ShortcutAccessController",
- *     "form" = {
- *       "default" = "Drupal\shortcut\ShortcutFormController",
- *       "add" = "Drupal\shortcut\ShortcutFormController",
- *       "delete" = "Drupal\shortcut\Form\ShortcutDeleteForm"
- *     },
- *     "translation" = "Drupal\content_translation\ContentTranslationHandler"
- *   },
- *   base_table = "shortcut",
- *   data_table = "shortcut_field_data",
- *   translatable = TRUE,
- *   entity_keys = {
- *     "id" = "id",
- *     "uuid" = "uuid",
- *     "bundle" = "shortcut_set",
- *     "label" = "title"
- *   },
- *   links = {
- *     "delete-form" = "shortcut.link_delete",
- *     "edit-form" = "shortcut.link_edit"
- *   }
- * )
- */
-class Shortcut extends ContentEntityBase implements ShortcutInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getTitle() {
-    return $this->get('title')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setTitle($link_title) {
-    $this->set('title', $link_title);
-   return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getWeight() {
-    return $this->get('weight')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setWeight($weight) {
-    $this->set('weight', $weight);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRouteName() {
-    return $this->get('route_name')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRouteName($route_name) {
-    $this->set('route_name', $route_name);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRouteParams() {
-    $value = $this->get('route_parameters')->getValue();
-    return reset($value);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRouteParams($route_parameters) {
-    $this->set('route_parameters', array('value' => $route_parameters));
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function preCreate(EntityStorageInterface $storage, array &$values) {
-    parent::preCreate($storage, $values);
-
-    if (!isset($values['shortcut_set'])) {
-      $values['shortcut_set'] = 'default';
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function preSave(EntityStorageInterface $storage) {
-    parent::preSave($storage);
-
-    $url = Url::createFromPath($this->path->value);
-    $this->setRouteName($url->getRouteName());
-    $this->setRouteParams($url->getRouteParameters());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
-    $fields['id'] = FieldDefinition::create('integer')
-      ->setLabel(t('ID'))
-      ->setDescription(t('The ID of the shortcut.'))
-      ->setReadOnly(TRUE)
-      ->setSetting('unsigned', TRUE);
-
-    $fields['uuid'] = FieldDefinition::create('uuid')
-      ->setLabel(t('UUID'))
-      ->setDescription(t('The UUID of the shortcut.'))
-      ->setReadOnly(TRUE);
-
-    $fields['shortcut_set'] = FieldDefinition::create('entity_reference')
-      ->setLabel(t('Shortcut set'))
-      ->setDescription(t('The bundle of the shortcut.'))
-      ->setSetting('target_type', 'shortcut_set')
-      ->setRequired(TRUE);
-
-    $fields['title'] = FieldDefinition::create('string')
-      ->setLabel(t('Name'))
-      ->setDescription(t('The name of the shortcut.'))
-      ->setRequired(TRUE)
-      ->setTranslatable(TRUE)
-      ->setSettings(array(
-        'default_value' => '',
-        'max_length' => 255,
-      ))
-      ->setDisplayOptions('form', array(
-        'type' => 'string',
-        'weight' => -10,
-        'settings' => array(
-          'size' => 40,
-        ),
-      ));
-
-    $fields['weight'] = FieldDefinition::create('integer')
-      ->setLabel(t('Weight'))
-      ->setDescription(t('Weight among shortcuts in the same shortcut set.'));
-
-    $fields['route_name'] = FieldDefinition::create('string')
-      ->setLabel(t('Route name'))
-      ->setDescription(t('The machine name of a defined Route this shortcut represents.'));
-
-    $fields['route_parameters'] = FieldDefinition::create('map')
-      ->setLabel(t('Route parameters'))
-      ->setDescription(t('A serialized array of route parameters of this shortcut.'));
-
-    $fields['langcode'] = FieldDefinition::create('language')
-      ->setLabel(t('Language code'))
-      ->setDescription(t('The language code of the shortcut.'));
-
-    $fields['default_langcode'] = FieldDefinition::create('boolean')
-      ->setLabel(t('Default language'))
-      ->setDescription(t('Flag to indicate whether this is the default language.'));
-
-    $fields['path'] = FieldDefinition::create('string')
-      ->setLabel(t('Path'))
-      ->setDescription(t('The computed shortcut path.'))
-      ->setComputed(TRUE);
-
-    $item_definition = $fields['path']->getItemDefinition();
-    $item_definition->setClass('\Drupal\shortcut\ShortcutPathItem');
-    $fields['path']->setItemDefinition($item_definition);
-
-    return $fields;
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Entity/ShortcutSet.php b/core/modules/shortcut/lib/Drupal/shortcut/Entity/ShortcutSet.php
deleted file mode 100644
index f4d5d59..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Entity/ShortcutSet.php
+++ /dev/null
@@ -1,119 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Entity\ShortcutSet.
- */
-
-namespace Drupal\shortcut\Entity;
-
-use Drupal\Core\Config\Entity\ConfigEntityBase;
-use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\shortcut\ShortcutSetInterface;
-
-/**
- * Defines the Shortcut set configuration entity.
- *
- * @ConfigEntityType(
- *   id = "shortcut_set",
- *   label = @Translation("Shortcut set"),
- *   controllers = {
- *     "storage" = "Drupal\shortcut\ShortcutSetStorage",
- *     "access" = "Drupal\shortcut\ShortcutSetAccessController",
- *     "list_builder" = "Drupal\shortcut\ShortcutSetListBuilder",
- *     "form" = {
- *       "default" = "Drupal\shortcut\ShortcutSetFormController",
- *       "add" = "Drupal\shortcut\ShortcutSetFormController",
- *       "edit" = "Drupal\shortcut\ShortcutSetFormController",
- *       "customize" = "Drupal\shortcut\Form\SetCustomize",
- *       "delete" = "Drupal\shortcut\Form\ShortcutSetDeleteForm"
- *     }
- *   },
- *   config_prefix = "set",
- *   entity_keys = {
- *     "id" = "id",
- *     "label" = "label"
- *   },
- *   links = {
- *     "customize-form" = "shortcut.set_customize",
- *     "delete-form" = "shortcut.set_delete",
- *     "edit-form" = "shortcut.set_edit"
- *   }
- * )
- */
-class ShortcutSet extends ConfigEntityBase implements ShortcutSetInterface {
-
-  /**
-   * The machine name for the configuration entity.
-   *
-   * @var string
-   */
-  public $id;
-
-  /**
-   * The human-readable name of the configuration entity.
-   *
-   * @var string
-   */
-  public $label;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function postCreate(EntityStorageInterface $storage) {
-    parent::postCreate($storage);
-
-    // Generate menu-compatible set name.
-    if (!$this->getOriginalId()) {
-      // Save a new shortcut set with links copied from the user's default set.
-      $default_set = shortcut_default_set();
-      foreach ($default_set->getShortcuts() as $shortcut) {
-        $shortcut = $shortcut->createDuplicate();
-        $shortcut->enforceIsNew();
-        $shortcut->shortcut_set->target_id = $this->id();
-        $shortcut->save();
-      }
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function preDelete(EntityStorageInterface $storage, array $entities) {
-    parent::preDelete($storage, $entities);
-
-    foreach ($entities as $entity) {
-      $storage->deleteAssignedShortcutSets($entity);
-
-      // Next, delete the shortcuts for this set.
-      $shortcut_ids = \Drupal::entityQuery('shortcut')
-        ->condition('shortcut_set', $entity->id(), '=')
-        ->execute();
-
-      $controller = \Drupal::entityManager()->getStorage('shortcut');
-      $entities = $controller->loadMultiple($shortcut_ids);
-      $controller->delete($entities);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function resetLinkWeights() {
-    $weight = -50;
-    foreach ($this->getShortcuts() as $shortcut) {
-      $shortcut->setWeight(++$weight);
-      $shortcut->save();
-    }
-
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getShortcuts() {
-    return \Drupal::entityManager()->getStorage('shortcut')->loadByProperties(array('shortcut_set' => $this->id()));
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Form/SetCustomize.php b/core/modules/shortcut/lib/Drupal/shortcut/Form/SetCustomize.php
deleted file mode 100644
index 0d427be..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Form/SetCustomize.php
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Form\SetCustomize.
- */
-
-namespace Drupal\shortcut\Form;
-
-use Drupal\Core\Entity\EntityFormController;
-use Drupal\Core\Entity\EntityManagerInterface;
-use Drupal\Core\Render\Element;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Builds the shortcut set customize form.
- */
-class SetCustomize extends EntityFormController {
-
-  /**
-   * The entity being used by this form.
-   *
-   * @var \Drupal\shortcut\ShortcutSetInterface
-   */
-  protected $entity;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function form(array $form, array &$form_state) {
-    $form = parent::form($form, $form_state);
-    $form['shortcuts'] = array(
-      '#tree' => TRUE,
-      '#weight' => -20,
-    );
-
-    $form['shortcuts']['links'] = array(
-      '#type' => 'table',
-      '#header' => array(t('Name'), t('Weight'), t('Operations')),
-      '#empty' => $this->t('No shortcuts available. <a href="@link">Add a shortcut</a>', array('@link' => $this->urlGenerator()->generateFromRoute('shortcut.link_add', array('shortcut_set' => $this->entity->id())))),
-      '#attributes' => array('id' => 'shortcuts'),
-      '#tabledrag' => array(
-        array(
-          'action' => 'order',
-          'relationship' => 'sibling',
-          'group' => 'shortcut-weight',
-        ),
-      ),
-    );
-
-    foreach ($this->entity->getShortcuts() as $shortcut) {
-      $id = $shortcut->id();
-      $form['shortcuts']['links'][$id]['#attributes']['class'][] = 'draggable';
-      $form['shortcuts']['links'][$id]['name']['#markup'] = l($shortcut->getTitle(), $shortcut->path->value);
-      $form['shortcuts']['links'][$id]['#weight'] = $shortcut->getWeight();
-      $form['shortcuts']['links'][$id]['weight'] = array(
-        '#type' => 'weight',
-        '#title' => t('Weight for @title', array('@title' => $shortcut->getTitle())),
-        '#title_display' => 'invisible',
-        '#default_value' => $shortcut->getWeight(),
-        '#attributes' => array('class' => array('shortcut-weight')),
-      );
-
-      $links['edit'] = array(
-        'title' => t('Edit'),
-        'href' => "admin/config/user-interface/shortcut/link/$id",
-      );
-      $links['delete'] = array(
-        'title' => t('Delete'),
-        'href' => "admin/config/user-interface/shortcut/link/$id/delete",
-      );
-      $form['shortcuts']['links'][$id]['operations'] = array(
-        '#type' => 'operations',
-        '#links' => $links,
-      );
-    }
-    // Sort the list so the output is ordered by weight.
-    uasort($form['shortcuts']['links'], array('\Drupal\Component\Utility\SortArray', 'sortByWeightProperty'));
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function actions(array $form, array &$form_state) {
-    // Only includes a Save action for the entity, no direct Delete button.
-    return array(
-      'submit' => array(
-        '#value' => t('Save changes'),
-        '#access' => (bool) Element::getVisibleChildren($form['shortcuts']['links']),
-        '#submit' => array(
-          array($this, 'submit'),
-          array($this, 'save'),
-        ),
-      ),
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function save(array $form, array &$form_state) {
-    foreach ($this->entity->getShortcuts() as $shortcut) {
-      $shortcut->setWeight($form_state['values']['shortcuts']['links'][$shortcut->id()]['weight']);
-      $shortcut->save();
-    }
-    drupal_set_message(t('The shortcut set has been updated.'));
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutDeleteForm.php b/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutDeleteForm.php
deleted file mode 100644
index b5bbabe..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutDeleteForm.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Form\ShortcutDeleteForm.
- */
-
-namespace Drupal\shortcut\Form;
-
-use Drupal\Core\Entity\ContentEntityConfirmFormBase;
-
-/**
- * Builds the shortcut link deletion form.
- */
-class ShortcutDeleteForm extends ContentEntityConfirmFormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'shortcut_confirm_delete';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getQuestion() {
-    return $this->t('Are you sure you want to delete the shortcut %title?', array('%title' => $this->entity->title->value));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCancelRoute() {
-    return array(
-      'route_name' => 'shortcut.set_customize',
-      'route_parameters' => array(
-        'shortcut_set' => $this->entity->bundle(),
-      ),
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getConfirmText() {
-    return $this->t('Delete');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submit(array $form, array &$form_state) {
-    $this->entity->delete();
-    $form_state['redirect_route'] = array(
-      'route_name' => 'shortcut.set_customize',
-      'route_parameters' => array(
-        'shortcut_set' => $this->entity->bundle(),
-      ),
-    );
-    drupal_set_message($this->t('The shortcut %title has been deleted.', array('%title' => $this->entity->title->value)));
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutForm.php b/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutForm.php
deleted file mode 100644
index 4f556b7..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutForm.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Form\ShortcutForm.
- */
-
-namespace Drupal\shortcut\Form;
-
-use Drupal\user\UserInterface;
-
-/**
- * Temporary form controller for shortcut module.
- */
-class ShortcutForm {
-
-  /**
-   * Wraps shortcut_set_switch().
-   *
-   * @todo Remove shortcut_set_switch().
-   */
-  public function overview(UserInterface $user) {
-    module_load_include('admin.inc', 'shortcut');
-    return \Drupal::formBuilder()->getForm('shortcut_set_switch', $user);
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutSetDeleteForm.php b/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutSetDeleteForm.php
deleted file mode 100644
index 504b81e..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Form/ShortcutSetDeleteForm.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Form\ShortcutSetDeleteForm.
- */
-
-namespace Drupal\shortcut\Form;
-
-use Drupal\Core\Entity\EntityConfirmFormBase;
-use Drupal\shortcut\ShortcutSetStorageInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Database\Connection;
-
-/**
- * Builds the shortcut set deletion form.
- */
-class ShortcutSetDeleteForm extends EntityConfirmFormBase {
-
-  /**
-   * The database connection.
-   *
-   * @var \Drupal\Core\Database\Connection
-   */
-  protected $database;
-
-  /**
-   * The shortcut storage.
-   *
-   * @var \Drupal\shortcut\ShortcutSetStorageInterface
-   */
-  protected $storage;
-
-  /**
-   * Constructs a ShortcutSetDeleteForm object.
-   */
-  public function __construct(Connection $database, ShortcutSetStorageInterface $storage) {
-    $this->database = $database;
-    $this->storage = $storage;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('database'),
-      $container->get('entity.manager')->getStorage('shortcut_set')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getQuestion() {
-    return t('Are you sure you want to delete the shortcut set %title?', array('%title' => $this->entity->label()));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCancelRoute() {
-    return array(
-      'route_name' => 'shortcut.set_customize',
-      'route_parameters' => array(
-        'shortcut_set' => $this->entity->id(),
-      ),
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getConfirmText() {
-    return t('Delete');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, array &$form_state) {
-    // Find out how many users are directly assigned to this shortcut set, and
-    // make a message.
-    $number = $this->storage->countAssignedUsers($this->entity);
-    $info = '';
-    if ($number) {
-      $info .= '<p>' . format_plural($number,
-        '1 user has chosen or been assigned to this shortcut set.',
-        '@count users have chosen or been assigned to this shortcut set.') . '</p>';
-    }
-
-    // Also, if a module implements hook_shortcut_default_set(), it's possible
-    // that this set is being used as a default set. Add a message about that too.
-    if ($this->moduleHandler->getImplementations('shortcut_default_set')) {
-      $info .= '<p>' . t('If you have chosen this shortcut set as the default for some or all users, they may also be affected by deleting it.') . '</p>';
-    }
-
-    $form['info'] = array(
-      '#markup' => $info,
-    );
-
-    return parent::buildForm($form, $form_state);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submit(array $form, array &$form_state) {
-    $this->entity->delete();
-    $form_state['redirect_route']['route_name'] = 'shortcut.set_admin';
-    drupal_set_message(t('The shortcut set %title has been deleted.', array('%title' => $this->entity->label())));
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Block/ShortcutsBlock.php b/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Block/ShortcutsBlock.php
deleted file mode 100644
index cb64ec0..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Block/ShortcutsBlock.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Plugin\Block\ShortcutsBlock.
- */
-
-namespace Drupal\shortcut\Plugin\Block;
-
-use Drupal\block\BlockBase;
-
-/**
- * Provides a 'Shortcut' block.
- *
- * @Block(
- *   id = "shortcuts",
- *   admin_label = @Translation("Shortcuts"),
- *   category = @Translation("Menus")
- * )
- */
-class ShortcutsBlock extends BlockBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function build() {
-    return array(
-      shortcut_renderable_links(shortcut_current_displayed_set()),
-    );
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutAccessController.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutAccessController.php
deleted file mode 100644
index 402aca7..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutAccessController.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutAccessController.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Entity\EntityAccessController;
-use Drupal\Core\Entity\EntityControllerInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Session\AccountInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Defines the access controller for the test entity type.
- */
-class ShortcutAccessController extends EntityAccessController implements EntityControllerInterface {
-
-  /**
-   * The shortcut_set storage.
-   *
-   * @var \Drupal\shortcut\ShortcutSetStorage
-   */
-  protected $shortcutSetStorage;
-
-  /**
-   * Constructs a ShortcutAccessController object.
-   *
-   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
-   *   The entity type definition.
-   * @param \Drupal\shortcut\ShortcutSetStorage $shortcut_set_storage
-   *   The shortcut_set storage.
-   */
-  public function __construct(EntityTypeInterface $entity_type, ShortcutSetStorage $shortcut_set_storage) {
-    parent::__construct($entity_type);
-    $this->shortcutSetStorage = $shortcut_set_storage;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
-    return new static(
-      $entity_type,
-      $container->get('entity.manager')->getStorage('shortcut_set')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
-    if ($shortcut_set = $this->shortcutSetStorage->load($entity->bundle())) {
-      return shortcut_set_edit_access($shortcut_set, $account);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
-    if ($shortcut_set = $this->shortcutSetStorage->load($entity_bundle)) {
-      return shortcut_set_edit_access($shortcut_set, $account);
-    }
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutFormController.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutFormController.php
deleted file mode 100644
index c34f876..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutFormController.php
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutFormController.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Entity\ContentEntityFormController;
-use Drupal\Core\Language\Language;
-
-/**
- * Form controller for the shortcut entity forms.
- */
-class ShortcutFormController extends ContentEntityFormController {
-
-  /**
-   * The entity being used by this form.
-   *
-   * @var \Drupal\shortcut\ShortcutInterface
-   */
-  protected $entity;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function form(array $form, array &$form_state) {
-    $form = parent::form($form, $form_state);
-
-    $form['path'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Path'),
-      '#size' => 40,
-      '#maxlength' => 255,
-      '#field_prefix' => $this->url('<front>', array(), array('absolute' => TRUE)),
-      '#default_value' => $this->entity->path->value,
-    );
-
-    $form['langcode'] = array(
-      '#title' => t('Language'),
-      '#type' => 'language_select',
-      '#default_value' => $this->entity->getUntranslated()->language()->id,
-      '#languages' => Language::STATE_ALL,
-    );
-
-    return $form;
-  }
-
-  /**
-   * Overrides EntityFormController::buildEntity().
-   */
-  public function buildEntity(array $form, array &$form_state) {
-    $entity = parent::buildEntity($form, $form_state);
-
-    // Set the computed 'path' value so it can used in the preSave() method to
-    // derive the route name and parameters.
-    $entity->path->value = $form_state['values']['path'];
-
-    return $entity;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validate(array $form, array &$form_state) {
-    if (!shortcut_valid_link($form_state['values']['path'])) {
-      $this->setFormError('path', $form_state, $this->t('The shortcut must correspond to a valid path on the site.'));
-    }
-
-    parent::validate($form, $form_state);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function save(array $form, array &$form_state) {
-    $entity = $this->entity;
-    $entity->save();
-
-    if ($entity->isNew()) {
-      $message = $this->t('The shortcut %link has been updated.', array('%link' => $entity->getTitle()));
-    }
-    else {
-      $message = $this->t('Added a shortcut for %title.', array('%title' => $entity->getTitle()));
-    }
-    drupal_set_message($message);
-
-    $form_state['redirect_route'] = array(
-      'route_name' => 'shortcut.set_customize',
-      'route_parameters' => array('shortcut_set' => $entity->bundle()),
-    );
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutInterface.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutInterface.php
deleted file mode 100644
index 9a6bb98..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutInterface.php
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutInterface.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Entity\ContentEntityInterface;
-
-/**
- * Provides an interface defining a shortcut entity.
- */
-interface ShortcutInterface extends ContentEntityInterface {
-
-  /**
-   * Returns the title of this shortcut.
-   *
-   * @return string
-   *   The title of this shortcut.
-   */
-  public function getTitle();
-
-  /**
-   * Sets the title of this shortcut.
-   *
-   * @param string $title
-   *   The title of this shortcut.
-   *
-   * @return \Drupal\shortcut\ShortcutInterface
-   *   The called shortcut entity.
-   */
-  public function setTitle($title);
-
-  /**
-   * Returns the weight among shortcuts with the same depth.
-   *
-   * @return int
-   *   The shortcut weight.
-   */
-  public function getWeight();
-
-  /**
-   * Sets the weight among shortcuts with the same depth.
-   *
-   * @param int $weight
-   *   The shortcut weight.
-   *
-   * @return \Drupal\shortcut\ShortcutInterface
-   *   The called shortcut entity.
-   */
-  public function setWeight($weight);
-
-  /**
-   * Returns the route name associated with this shortcut, if any.
-   *
-   * @return string|null
-   *   The route name of this shortcut.
-   */
-  public function getRouteName();
-
-  /**
-   * Sets the route name associated with this shortcut.
-   *
-   * @param string|null $route_name
-   *   The route name associated with this shortcut.
-   *
-   * @return \Drupal\shortcut\ShortcutInterface
-   *   The called shortcut entity.
-   */
-  public function setRouteName($route_name);
-
-  /**
-   * Returns the route parameters associated with this shortcut, if any.
-   *
-   * @return array
-   *   The route parameters of this shortcut.
-   */
-  public function getRouteParams();
-
-  /**
-   * Sets the route parameters associated with this shortcut.
-   *
-   * @param array $route_parameters
-   *   The route parameters associated with this shortcut.
-   *
-   * @return \Drupal\shortcut\ShortcutInterface
-   *   The called shortcut entity.
-  */
-  public function setRouteParams($route_parameters);
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutPathItem.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutPathItem.php
deleted file mode 100644
index 8ad82d3..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutPathItem.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutPathItem.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
-use Drupal\Core\TypedData\DataDefinition;
-
-/**
- * The field item for the 'path' field.
- */
-class ShortcutPathItem extends StringItem {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
-    $properties['value'] = DataDefinition::create('string')
-      ->setLabel(t('String value'))
-      ->setComputed(TRUE)
-      ->setClass('\Drupal\shortcut\ShortcutPathValue');
-    return $properties;
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutPathValue.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutPathValue.php
deleted file mode 100644
index 2c8b4e5..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutPathValue.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutPathValue.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\TypedData\TypedData;
-
-/**
- * A computed property for the string value of the path field.
- */
-class ShortcutPathValue extends TypedData {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getValue() {
-    if (!isset($this->value)) {
-      if (!isset($this->parent)) {
-        throw new \InvalidArgumentException('Computed properties require context for computation.');
-      }
-
-      $entity = $this->parent->getEntity();
-      if ($route_name = $entity->getRouteName()) {
-        $path = \Drupal::urlGenerator()->getPathFromRoute($route_name, $entity->getRouteParams());
-        $this->value = trim($path, '/');
-      }
-      else {
-        $this->value = NULL;
-      }
-    }
-    return $this->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setValue($value, $notify = TRUE) {
-    // Normalize the path in case it is an alias.
-    $value = \Drupal::service('path.alias_manager')->getSystemPath($value);
-    if (empty($value)) {
-      $value = '<front>';
-    }
-
-    parent::setValue($value, $notify);
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetAccessController.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetAccessController.php
deleted file mode 100644
index c60d52a..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetAccessController.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutSetAccessController.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityAccessController;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Defines the access controller for the shortcut entity type.
- */
-class ShortcutSetAccessController extends EntityAccessController {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
-    switch ($operation) {
-      case 'update':
-        if ($account->hasPermission('administer shortcuts')) {
-          return TRUE;
-        }
-        if ($account->hasPermission('customize shortcut links')) {
-          return $entity == shortcut_current_displayed_set($account);
-        }
-        return FALSE;
-        break;
-
-      case 'delete':
-        if (!$account->hasPermission('administer shortcuts')) {
-          return FALSE;
-        }
-        return $entity->id() != 'default';
-        break;
-    }
-  }
-
-  /**
-  * {@inheritdoc}
-   */
-  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
-    return $account->hasPermission('administer shortcuts') || $account->hasPermission('customize shortcut links');
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetFormController.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetFormController.php
deleted file mode 100644
index 5360b5f..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetFormController.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutSetFormController.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Entity\EntityFormController;
-
-/**
- * Form controller for the shortcut set entity edit forms.
- */
-class ShortcutSetFormController extends EntityFormController {
-
-  /**
-   * Overrides \Drupal\Core\Entity\EntityFormController::form().
-   */
-  public function form(array $form, array &$form_state) {
-    $form = parent::form($form, $form_state);
-
-    $entity = $this->entity;
-    $form['label'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Set name'),
-      '#description' => t('The new set is created by copying items from your default shortcut set.'),
-      '#required' => TRUE,
-      '#default_value' => $entity->label(),
-    );
-    $form['id'] = array(
-      '#type' => 'machine_name',
-      '#machine_name' => array(
-        'exists' => 'shortcut_set_load',
-        'source' => array('label'),
-        'replace_pattern' => '[^a-z0-9-]+',
-        'replace' => '-',
-      ),
-      '#default_value' => $entity->id(),
-      '#disabled' => !$entity->isNew(),
-      // This id could be used for menu name.
-      '#maxlength' => 23,
-    );
-
-    $form['actions']['submit']['#value'] = t('Create new set');
-
-    return $form;
-  }
-
-  /**
-   * Overrides \Drupal\Core\Entity\EntityFormController::actions().
-   */
-  protected function actions(array $form, array &$form_state) {
-    // Disable delete of default shortcut set.
-    $actions = parent::actions($form, $form_state);
-    $actions['delete']['#access'] = $this->entity->access('delete');
-    return $actions;
-  }
-
-  /**
-   * Overrides \Drupal\Core\Entity\EntityFormController::validate().
-   */
-  public function validate(array $form, array &$form_state) {
-    parent::validate($form, $form_state);
-    $entity = $this->entity;
-    // Check to prevent a duplicate title.
-    if ($form_state['values']['label'] != $entity->label() && shortcut_set_title_exists($form_state['values']['label'])) {
-      $this->setFormError('label', $form_state, $this->t('The shortcut set %name already exists. Choose another name.', array('%name' => $form_state['values']['label'])));
-    }
-  }
-
-  /**
-   * Overrides \Drupal\Core\Entity\EntityFormController::save().
-   */
-  public function save(array $form, array &$form_state) {
-    $entity = $this->entity;
-    $is_new = !$entity->getOriginalId();
-    $entity->save();
-
-    if ($is_new) {
-      drupal_set_message(t('The %set_name shortcut set has been created. You can edit it from this page.', array('%set_name' => $entity->label())));
-    }
-    else {
-      drupal_set_message(t('Updated set name to %set-name.', array('%set-name' => $entity->label())));
-    }
-    $form_state['redirect_route'] = $this->entity->urlInfo('customize-form');
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetInterface.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetInterface.php
deleted file mode 100644
index 21cc4f3..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetInterface.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutSetInterface.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
-
-/**
- * Provides an interface defining a shortcut set entity.
- */
-interface ShortcutSetInterface extends ConfigEntityInterface {
-
-  /**
-   * Resets the link weights in a shortcut set to match their current order.
-   *
-   * This function can be used, for example, when a new shortcut link is added
-   * to the set. If the link is added to the end of the array and this function
-   * is called, it will force that link to display at the end of the list.
-   *
-   * @return \Drupal\shortcut\ShortcutSetInterface
-   *   The shortcut set.
-   */
-  public function resetLinkWeights();
-
-  /**
-   * Returns all the shortcuts from a shortcut set.
-   *
-   * @return \Drupal\shortcut\ShortcutInterface[]
-   *   An array of shortcut entities.
-   */
-  public function getShortcuts();
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetListBuilder.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetListBuilder.php
deleted file mode 100644
index 907f470..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetListBuilder.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutSetListBuilder.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Defines a class to build a listing of shortcut set entities.
- *
- * @see \Drupal\shortcut\Entity\ShortcutSet
- */
-class ShortcutSetListBuilder extends ConfigEntityListBuilder {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildHeader() {
-    $header['name'] = t('Name');
-    return $header + parent::buildHeader();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDefaultOperations(EntityInterface $entity) {
-    $operations = parent::getDefaultOperations($entity);
-
-    if (isset($operations['edit'])) {
-      $operations['edit']['title'] = t('Edit shortcut set');
-    }
-
-    $operations['list'] = array(
-      'title' => t('List links'),
-    ) + $entity->urlInfo('customize-form')->toArray();
-    return $operations;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildRow(EntityInterface $entity) {
-    $row['name'] = $this->getLabel($entity);
-    return $row + parent::buildRow($entity);
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetStorage.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetStorage.php
deleted file mode 100644
index ea723d6..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetStorage.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutSetStorage.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Config\Entity\ConfigEntityStorage;
-
-/**
- * Defines a storage for shortcut_set entities.
- */
-class ShortcutSetStorage extends ConfigEntityStorage implements ShortcutSetStorageInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function deleteAssignedShortcutSets(ShortcutSetInterface $entity) {
-    // First, delete any user assignments for this set, so that each of these
-    // users will go back to using whatever default set applies.
-    db_delete('shortcut_set_users')
-      ->condition('set_name', $entity->id())
-      ->execute();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function assignUser(ShortcutSetInterface $shortcut_set, $account) {
-    db_merge('shortcut_set_users')
-      ->key('uid', $account->id())
-      ->fields(array('set_name' => $shortcut_set->id()))
-      ->execute();
-    drupal_static_reset('shortcut_current_displayed_set');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function unassignUser($account) {
-    $deleted = db_delete('shortcut_set_users')
-      ->condition('uid', $account->id())
-      ->execute();
-    return (bool) $deleted;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAssignedToUser($account) {
-    $query = db_select('shortcut_set_users', 'ssu');
-    $query->fields('ssu', array('set_name'));
-    $query->condition('ssu.uid', $account->id());
-    return $query->execute()->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function countAssignedUsers(ShortcutSetInterface $shortcut_set) {
-    return db_query('SELECT COUNT(*) FROM {shortcut_set_users} WHERE set_name = :name', array(':name' => $shortcut_set->id()))->fetchField();
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetStorageInterface.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetStorageInterface.php
deleted file mode 100644
index 11af402..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutSetStorageInterface.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\ShortcutSetStorageInterface.
- */
-
-namespace Drupal\shortcut;
-
-use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
-
-/**
- * Defines a common interface for shortcut entity controller classes.
- */
-interface ShortcutSetStorageInterface extends ConfigEntityStorageInterface {
-
-  /**
-   * Assigns a user to a particular shortcut set.
-   *
-   * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set
-   *   An object representing the shortcut set.
-   * @param $account
-   *   A user account that will be assigned to use the set.
-   */
-  public function assignUser(ShortcutSetInterface $shortcut_set, $account);
-
-  /**
-   * Unassigns a user from any shortcut set they may have been assigned to.
-   *
-   * The user will go back to using whatever default set applies.
-   *
-   * @param $account
-   *   A user account that will be removed from the shortcut set assignment.
-   *
-   * @return bool
-   *   TRUE if the user was previously assigned to a shortcut set and has been
-   *   successfully removed from it. FALSE if the user was already not assigned
-   *   to any set.
-   */
-  public function unassignUser($account);
-
-  /**
-   * Delete shortcut sets assigned to users.
-   *
-   * @param \Drupal\shortcut\ShortcutSetInterface $entity
-   *   Delete the user assigned sets belonging to this shortcut.
-   */
-  public function deleteAssignedShortcutSets(ShortcutSetInterface $entity);
-
-  /**
-   * Get the name of the set assigned to this user.
-   *
-   * @param \Drupal\user\Entity\User
-   *   The user account.
-   *
-   * @return string
-   *   The name of the shortcut set assigned to this user.
-   */
-  public function getAssignedToUser($account);
-
-  /**
-   * Get the number of users who have this set assigned to them.
-   *
-   * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set
-   *   The shortcut to count the users assigned to.
-   *
-   * @return int
-   *   The number of users who have this set assigned to them.
-   */
-  public function countAssignedUsers(ShortcutSetInterface $shortcut_set);
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php
deleted file mode 100644
index 67f40ad..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\shortcut\Tests\ShortcutLinksTest.
- */
-
-namespace Drupal\shortcut\Tests;
-
-/**
- * Defines shortcut links test cases.
- */
-class ShortcutLinksTest extends ShortcutTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('router_test', 'views');
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Shortcut link functionality',
-      'description' => 'Create, view, edit, delete, and change shortcut links.',
-      'group' => 'Shortcut',
-    );
-  }
-
-  /**
-   * Tests that creating a shortcut works properly.
-   */
-  public function testShortcutLinkAdd() {
-    $set = $this->set;
-
-    // Create an alias for the node so we can test aliases.
-    $path = array(
-      'source' => 'node/' . $this->node->id(),
-      'alias' => $this->randomName(8),
-    );
-    $this->container->get('path.alias_storage')->save($path['source'], $path['alias']);
-
-    // Create some paths to test.
-    $test_cases = array(
-      array('path' => ''),
-      array('path' => 'admin'),
-      array('path' => 'admin/config/system/site-information'),
-      array('path' => 'node/' . $this->node->id() . '/edit'),
-      array('path' => $path['alias']),
-      array('path' => 'router_test/test2'),
-      array('path' => 'router_test/test3/value'),
-    );
-
-    // Check that each new shortcut links where it should.
-    foreach ($test_cases as $test) {
-      $title = $this->randomName();
-      $form_data = array(
-        'title[0][value]' => $title,
-        'path' => $test['path'],
-      );
-      $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save'));
-      $this->assertResponse(200);
-      $saved_set = shortcut_set_load($set->id());
-      $paths = $this->getShortcutInformation($saved_set, 'path');
-      $this->assertTrue(in_array($this->container->get('path.alias_manager')->getSystemPath($test['path']), $paths), 'Shortcut created: ' . $test['path']);
-      $this->assertLink($title, 0, 'Shortcut link found on the page.');
-    }
-  }
-
-  /**
-   * Tests that the "add to shortcut" and "remove from shortcut" links work.
-   */
-  public function testShortcutQuickLink() {
-    theme_enable(array('seven'));
-    \Drupal::config('system.theme')->set('admin', 'seven')->save();
-    $this->container->get('config.factory')->get('node.settings')->set('use_admin_theme', '1')->save();
-    $this->container->get('router.builder')->rebuild();
-
-    $this->drupalLogin($this->root_user);
-    $this->drupalGet('admin/config/system/cron');
-
-    // Test the "Add to shortcuts" link.
-    $this->clickLink('Add to Default shortcuts');
-    $this->assertText('Added a shortcut for Cron.');
-    $this->assertLink('Cron', 0, 'Shortcut link found on page');
-
-    $this->drupalGet('admin/structure');
-    $this->assertLink('Cron', 0, 'Shortcut link found on different page');
-
-    // Test the "Remove from shortcuts" link.
-    $this->clickLink('Cron');
-    $this->clickLink('Remove from Default shortcuts');
-    $this->drupalPostForm(NULL, array(), 'Delete');
-    $this->assertText('The shortcut Cron has been deleted.');
-    $this->assertNoLink('Cron', 'Shortcut link removed from page');
-
-    $this->drupalGet('admin/structure');
-    $this->assertNoLink('Cron', 'Shortcut link removed from different page');
-  }
-
-  /**
-   * Tests that shortcut links can be renamed.
-   */
-  public function testShortcutLinkRename() {
-    $set = $this->set;
-
-    // Attempt to rename shortcut link.
-    $new_link_name = $this->randomName();
-
-    $shortcuts = $set->getShortcuts();
-    $shortcut = reset($shortcuts);
-    $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $new_link_name, 'path' => $shortcut->path->value), t('Save'));
-    $saved_set = shortcut_set_load($set->id());
-    $titles = $this->getShortcutInformation($saved_set, 'title');
-    $this->assertTrue(in_array($new_link_name, $titles), 'Shortcut renamed: ' . $new_link_name);
-    $this->assertLink($new_link_name, 0, 'Renamed shortcut link appears on the page.');
-  }
-
-  /**
-   * Tests that changing the path of a shortcut link works.
-   */
-  public function testShortcutLinkChangePath() {
-    $set = $this->set;
-
-    // Tests changing a shortcut path.
-    $new_link_path = 'admin/config';
-
-    $shortcuts = $set->getShortcuts();
-    $shortcut = reset($shortcuts);
-    $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $shortcut->getTitle(), 'path' => $new_link_path), t('Save'));
-    $saved_set = shortcut_set_load($set->id());
-    $paths = $this->getShortcutInformation($saved_set, 'path');
-    $this->assertTrue(in_array($new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path);
-    $this->assertLinkByHref($new_link_path, 0, 'Shortcut with new path appears on the page.');
-  }
-
-  /**
-   * Tests that changing the route of a shortcut link works.
-   */
-  public function testShortcutLinkChangeRoute() {
-    $this->drupalLogin($this->root_user);
-    $this->drupalGet('admin/content');
-    $this->assertResponse(200);
-    // Disable the view.
-    entity_load('view', 'content')->disable()->save();
-    $this->drupalGet('admin/content');
-    $this->assertResponse(200);
-  }
-
-  /**
-   * Tests deleting a shortcut link.
-   */
-  public function testShortcutLinkDelete() {
-    $set = $this->set;
-
-    $shortcuts = $set->getShortcuts();
-    $shortcut = reset($shortcuts);
-    $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id() . '/delete', array(), 'Delete');
-    $saved_set = shortcut_set_load($set->id());
-    $ids = $this->getShortcutInformation($saved_set, 'id');
-    $this->assertFalse(in_array($shortcut->id(), $ids), 'Successfully deleted a shortcut.');
-
-    // Delete all the remaining shortcut links.
-    entity_delete_multiple('shortcut', array_filter($ids));
-
-    // Get the front page to check that no exceptions occur.
-    $this->drupalGet('');
-  }
-
-  /**
-   * Tests that the add shortcut link is not displayed for 404/403 errors.
-   *
-   * Tests that the "Add to shortcuts" link is not displayed on a page not
-   * found or a page the user does not have access to.
-   */
-  public function testNoShortcutLink() {
-    // Change to a theme that displays shortcuts.
-    theme_enable(array('seven'));
-    \Drupal::config('system.theme')
-      ->set('default', 'seven')
-      ->save();
-
-    $this->drupalGet('page-that-does-not-exist');
-    $result = $this->xpath('//div[contains(@class, "add-shortcut")]');
-    $this->assertTrue(empty($result), 'Add to shortcuts link was not shown on a page not found.');
-
-    // The user does not have access to this path.
-    $this->drupalGet('admin/modules');
-    $result = $this->xpath('//div[contains(@class, "add-shortcut")]');
-    $this->assertTrue(empty($result), 'Add to shortcuts link was not shown on a page the user does not have access to.');
-
-    // Verify that the testing mechanism works by verifying the shortcut
-    // link appears on admin/people.
-    $this->drupalGet('admin/people');
-    $result = $this->xpath('//div[contains(@class, "remove-shortcut")]');
-    $this->assertTrue(!empty($result), 'Remove from shortcuts link was shown on a page the user does have access to.');
-
-    // Verify that the shortcut link appears on routing only pages.
-    $this->drupalGet('router_test/test2');
-    $result = $this->xpath('//div[contains(@class, "add-shortcut")]');
-    $this->assertTrue(!empty($result), 'Add to shortcuts link was shown on a page the user does have access to.');
-  }
-
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutSetsTest.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutSetsTest.php
deleted file mode 100644
index 2a31535..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutSetsTest.php
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\shortcut\Tests\ShortcutSetsTest.
- */
-
-namespace Drupal\shortcut\Tests;
-
-/**
- * Defines shortcut set test cases.
- */
-class ShortcutSetsTest extends ShortcutTestBase {
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Shortcut set functionality',
-      'description' => 'Create, view, edit, delete, and change shortcut sets.',
-      'group' => 'Shortcut',
-    );
-  }
-
-  /**
-   * Tests creating a shortcut set.
-   */
-  function testShortcutSetAdd() {
-    $this->drupalGet('admin/config/user-interface/shortcut');
-    $this->clickLink(t('Add shortcut set'));
-    $edit = array(
-      'label' => $this->randomName(),
-      'id' => strtolower($this->randomName()),
-    );
-    $this->drupalPostForm(NULL, $edit, t('Save'));
-    $new_set = $this->container->get('entity.manager')->getStorage('shortcut_set')->load($edit['id']);
-    $this->assertIdentical($new_set->id(), $edit['id'], 'Successfully created a shortcut set.');
-    $this->drupalGet('user/' . $this->admin_user->id() . '/shortcuts');
-    $this->assertText($new_set->label(), 'Generated shortcut set was listed as a choice on the user account page.');
-  }
-
-  /**
-   * Tests switching a user's own shortcut set.
-   */
-  function testShortcutSetSwitchOwn() {
-    $new_set = $this->generateShortcutSet($this->randomName());
-
-    // Attempt to switch the default shortcut set to the newly created shortcut
-    // set.
-    $this->drupalPostForm('user/' . $this->admin_user->id() . '/shortcuts', array('set' => $new_set->id()), t('Change set'));
-    $this->assertResponse(200);
-    $current_set = shortcut_current_displayed_set($this->admin_user);
-    $this->assertTrue($new_set->id() == $current_set->id(), 'Successfully switched own shortcut set.');
-  }
-
-  /**
-   * Tests switching another user's shortcut set.
-   */
-  function testShortcutSetAssign() {
-    $new_set = $this->generateShortcutSet($this->randomName());
-
-    shortcut_set_assign_user($new_set, $this->shortcut_user);
-    $current_set = shortcut_current_displayed_set($this->shortcut_user);
-    $this->assertTrue($new_set->id() == $current_set->id(), "Successfully switched another user's shortcut set.");
-  }
-
-  /**
-   * Tests switching a user's shortcut set and creating one at the same time.
-   */
-  function testShortcutSetSwitchCreate() {
-    $edit = array(
-      'set' => 'new',
-      'id' => strtolower($this->randomName()),
-      'label' => $this->randomString(),
-    );
-    $this->drupalPostForm('user/' . $this->admin_user->id() . '/shortcuts', $edit, t('Change set'));
-    $current_set = shortcut_current_displayed_set($this->admin_user);
-    $this->assertNotEqual($current_set->id(), $this->set->id(), 'A shortcut set can be switched to at the same time as it is created.');
-    $this->assertEqual($current_set->label(), $edit['label'], 'The new set is correctly assigned to the user.');
-  }
-
-  /**
-   * Tests switching a user's shortcut set without providing a new set name.
-   */
-  function testShortcutSetSwitchNoSetName() {
-    $edit = array('set' => 'new');
-    $this->drupalPostForm('user/' . $this->admin_user->id() . '/shortcuts', $edit, t('Change set'));
-    $this->assertText(t('The new set label is required.'));
-    $current_set = shortcut_current_displayed_set($this->admin_user);
-    $this->assertEqual($current_set->id(), $this->set->id(), 'Attempting to switch to a new shortcut set without providing a set name does not succeed.');
-  }
-
-  /**
-   * Tests renaming a shortcut set.
-   */
-  function testShortcutSetRename() {
-    $set = $this->set;
-
-    $new_label = $this->randomName();
-    $this->drupalGet('admin/config/user-interface/shortcut');
-    $this->clickLink(t('Edit shortcut set'));
-    $this->drupalPostForm(NULL, array('label' => $new_label), t('Save'));
-    $set = shortcut_set_load($set->id());
-    $this->assertTrue($set->label() == $new_label, 'Shortcut set has been successfully renamed.');
-  }
-
-  /**
-   * Tests renaming a shortcut set to the same name as another set.
-   */
-  function testShortcutSetRenameAlreadyExists() {
-    $set = $this->generateShortcutSet($this->randomName());
-    $existing_label = $this->set->label();
-    $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id(), array('label' => $existing_label), t('Save'));
-    $this->assertRaw(t('The shortcut set %name already exists. Choose another name.', array('%name' => $existing_label)));
-    $set = shortcut_set_load($set->id());
-    $this->assertNotEqual($set->label(), $existing_label, format_string('The shortcut set %title cannot be renamed to %new-title because a shortcut set with that title already exists.', array('%title' => $set->label(), '%new-title' => $existing_label)));
-  }
-
-  /**
-   * Tests unassigning a shortcut set.
-   */
-  function testShortcutSetUnassign() {
-    $new_set = $this->generateShortcutSet($this->randomName());
-
-    shortcut_set_assign_user($new_set, $this->shortcut_user);
-    shortcut_set_unassign_user($this->shortcut_user);
-    $current_set = shortcut_current_displayed_set($this->shortcut_user);
-    $default_set = shortcut_default_set($this->shortcut_user);
-    $this->assertTrue($current_set->id() == $default_set->id(), "Successfully unassigned another user's shortcut set.");
-  }
-
-  /**
-   * Tests deleting a shortcut set.
-   */
-  function testShortcutSetDelete() {
-    $new_set = $this->generateShortcutSet($this->randomName());
-
-    $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $new_set->id() . '/delete', array(), t('Delete'));
-    $sets = entity_load_multiple('shortcut_set');
-    $this->assertFalse(isset($sets[$new_set->id()]), 'Successfully deleted a shortcut set.');
-  }
-
-  /**
-   * Tests deleting the default shortcut set.
-   */
-  function testShortcutSetDeleteDefault() {
-    $this->drupalGet('admin/config/user-interface/shortcut/manage/default/delete');
-    $this->assertResponse(403);
-  }
-
-  /**
-   * Tests creating a new shortcut set with a defined set name.
-   */
-  function testShortcutSetCreateWithSetName() {
-    $random_name = $this->randomName();
-    $new_set = $this->generateShortcutSet($random_name, $random_name);
-    $sets = entity_load_multiple('shortcut_set');
-    $this->assertTrue(isset($sets[$random_name]), 'Successfully created a shortcut set with a defined set name.');
-    $this->drupalGet('user/' . $this->admin_user->id() . '/shortcuts');
-    $this->assertText($new_set->label(), 'Generated shortcut set was listed as a choice on the user account page.');
-  }
-}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
deleted file mode 100644
index de9e110..0000000
--- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
+++ /dev/null
@@ -1,121 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\shortcut\Tests\ShortcutTestBase.
- */
-
-namespace Drupal\shortcut\Tests;
-
-use Drupal\shortcut\ShortcutSetInterface;
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Defines base class for shortcut test cases.
- */
-abstract class ShortcutTestBase extends WebTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('node', 'toolbar', 'shortcut');
-
-  /**
-   * User with permission to administer shortcuts.
-   */
-  protected $admin_user;
-
-  /**
-   * User with permission to use shortcuts, but not administer them.
-   */
-  protected $shortcut_user;
-
-  /**
-   * Generic node used for testing.
-   */
-  protected $node;
-
-  /**
-   * Site-wide default shortcut set.
-   *
-   * @var \Drupal\shortcut\ShortcutSetInterface
-   */
-  protected $set;
-
-  function setUp() {
-    parent::setUp();
-
-    if ($this->profile != 'standard') {
-      // Create Basic page and Article node types.
-      $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
-      $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
-
-      // Populate the default shortcut set.
-      $shortcut = entity_create('shortcut', array(
-        'set' => 'default',
-        'title' => t('Add content'),
-        'weight' => -20,
-        'path' => 'node/add',
-      ));
-      $shortcut->save();
-
-      $shortcut = entity_create('shortcut', array(
-        'set' => 'default',
-        'title' => t('All content'),
-        'weight' => -19,
-        'path' => 'admin/content',
-      ));
-      $shortcut->save();
-    }
-
-    // Create users.
-    $this->admin_user = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview', 'administer users'));
-    $this->shortcut_user = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets'));
-
-    // Create a node.
-    $this->node = $this->drupalCreateNode(array('type' => 'article'));
-
-    // Log in as admin and grab the default shortcut set.
-    $this->drupalLogin($this->admin_user);
-    $this->set = shortcut_set_load('default');
-    shortcut_set_assign_user($this->set, $this->admin_user);
-  }
-
-  /**
-   * Creates a generic shortcut set.
-   */
-  function generateShortcutSet($label = '', $id = NULL) {
-    $set = entity_create('shortcut_set', array(
-      'id' => isset($id) ? $id : strtolower($this->randomName()),
-      'label' => empty($label) ? $this->randomString() : $label,
-    ));
-    $set->save();
-    return $set;
-  }
-
-  /**
-   * Extracts information from shortcut set links.
-   *
-   * @param \Drupal\shortcut\ShortcutSetInterface $set
-   *   The shortcut set object to extract information from.
-   * @param string $key
-   *   The array key indicating what information to extract from each link:
-   *    - 'title': Extract shortcut titles.
-   *    - 'path': Extract shortcut paths.
-   *    - 'id': Extract the shortcut ID.
-   *
-   * @return array
-   *   Array of the requested information from each link.
-   */
-  function getShortcutInformation(ShortcutSetInterface $set, $key) {
-    $info = array();
-    \Drupal::entityManager()->getStorage('shortcut')->resetCache();
-    foreach ($set->getShortcuts() as $shortcut) {
-      $info[] = $shortcut->{$key}->value;
-    }
-    return $info;
-  }
-
-}
diff --git a/core/modules/shortcut/shortcut.routing.yml b/core/modules/shortcut/shortcut.routing.yml
index d0721c0..100fbc5 100644
--- a/core/modules/shortcut/shortcut.routing.yml
+++ b/core/modules/shortcut/shortcut.routing.yml
@@ -50,6 +50,7 @@ shortcut.link_add:
   path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}/add-link'
   defaults:
     _content: '\Drupal\shortcut\Controller\ShortcutController::addForm'
+    _title: 'Add link'
   requirements:
     _entity_create_access: 'shortcut:{shortcut_set}'
 
@@ -57,6 +58,7 @@ shortcut.link_edit:
   path: '/admin/config/user-interface/shortcut/link/{shortcut}'
   defaults:
     _entity_form: 'shortcut.default'
+    _title: 'Edit'
   requirements:
     _entity_access: 'shortcut.update'
 
@@ -64,6 +66,7 @@ shortcut.link_delete:
   path: '/admin/config/user-interface/shortcut/link/{shortcut}/delete'
   defaults:
     _entity_form: 'shortcut.delete'
+    _title: 'Delete'
   requirements:
     _entity_access: 'shortcut.delete'
 
diff --git a/core/modules/shortcut/src/Access/LinkAccessCheck.php b/core/modules/shortcut/src/Access/LinkAccessCheck.php
new file mode 100644
index 0000000..b85fb9f
--- /dev/null
+++ b/core/modules/shortcut/src/Access/LinkAccessCheck.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\shortcut\Access\LinkAccessCheck.
+ */
+
+namespace Drupal\shortcut\Access;
+
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an access check for shortcut link delete routes.
+ */
+class LinkAccessCheck implements AccessInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request, AccountInterface $account) {
+    $menu_link = $request->attributes->get('menu_link');
+    $set_name = str_replace('shortcut-', '', $menu_link['menu_name']);
+    if ($shortcut_set = shortcut_set_load($set_name)) {
+      return shortcut_set_edit_access($shortcut_set) ? static::ALLOW : static::DENY;
+    }
+    return static::DENY;
+  }
+
+}
diff --git a/core/modules/shortcut/src/Access/ShortcutSetEditAccessCheck.php b/core/modules/shortcut/src/Access/ShortcutSetEditAccessCheck.php
new file mode 100644
index 0000000..21bbda8
--- /dev/null
+++ b/core/modules/shortcut/src/Access/ShortcutSetEditAccessCheck.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\shortcut\Access\ShortcutSetEditAccessCheck.
+ */
+
+namespace Drupal\shortcut\Access;
+
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an access check for shortcut link delete routes.
+ */
+class ShortcutSetEditAccessCheck implements AccessInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request, AccountInterface $account) {
+    $account = \Drupal::currentUser();
+    $shortcut_set = $request->attributes->get('shortcut_set');
+    // Sufficiently-privileged users can edit their currently displayed shortcut
+    // set, but not other sets. Shortcut administrators can edit any set.
+    if ($account->hasPermission('administer shortcuts')) {
+      return static::ALLOW;
+    }
+    if ($account->hasPermission('customize shortcut links')) {
+      return !isset($shortcut_set) || $shortcut_set == shortcut_current_displayed_set() ? static::ALLOW : static::DENY;
+    }
+    return static::DENY;
+  }
+
+}
diff --git a/core/modules/shortcut/src/Access/ShortcutSetSwitchAccessCheck.php b/core/modules/shortcut/src/Access/ShortcutSetSwitchAccessCheck.php
new file mode 100644
index 0000000..6d844ed
--- /dev/null
+++ b/core/modules/shortcut/src/Access/ShortcutSetSwitchAccessCheck.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\shortcut\Access\ShortcutSetSwitchAccessCheck.
+ */
+
+namespace Drupal\shortcut\Access;
+
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an access check for shortcut link delete routes.
+ */
+class ShortcutSetSwitchAccessCheck implements AccessInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request, AccountInterface $account) {
+    if ($account->hasPermission('administer shortcuts')) {
+      // Administrators can switch anyone's shortcut set.
+      return static::ALLOW;
+    }
+
+    if (!$account->hasPermission('switch shortcut sets')) {
+      // The user has no permission to switch anyone's shortcut set.
+      return static::DENY;
+    }
+
+    $user = $request->attributes->get('account');
+    if (!isset($user) || $user->id() == $account->id()) {
+      // Users with the 'switch shortcut sets' permission can switch their own
+      // shortcuts sets.
+      return static::ALLOW;
+    }
+    return static::DENY;
+  }
+
+}
diff --git a/core/modules/shortcut/src/Controller/ShortcutController.php b/core/modules/shortcut/src/Controller/ShortcutController.php
new file mode 100644
index 0000000..8f3b481
--- /dev/null
+++ b/core/modules/shortcut/src/Controller/ShortcutController.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Controller\ShortcutController.
+ */
+
+namespace Drupal\shortcut\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\shortcut\ShortcutSetInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides route responses for taxonomy.module.
+ */
+class ShortcutController extends ControllerBase {
+
+  /**
+   * Returns a rendered edit form to create a new shortcut associated to the
+   * given shortcut set.
+   *
+   * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set
+   *   The shortcut set this shortcut will be added to.
+   *
+   * @return array
+   *   The shortcut add form.
+   */
+  public function addForm(ShortcutSetInterface $shortcut_set) {
+    $shortcut = $this->entityManager()->getStorage('shortcut')->create(array('shortcut_set' => $shortcut_set->id()));
+    if ($this->moduleHandler()->moduleExists('language')) {
+      $shortcut->langcode = language_get_default_langcode('shortcut', $shortcut_set->id());
+    }
+    return $this->entityFormBuilder()->getForm($shortcut, 'add');
+  }
+
+}
diff --git a/core/modules/shortcut/src/Controller/ShortcutSetController.php b/core/modules/shortcut/src/Controller/ShortcutSetController.php
new file mode 100644
index 0000000..e63e027
--- /dev/null
+++ b/core/modules/shortcut/src/Controller/ShortcutSetController.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Controller\ShortcutSetController.
+ */
+
+namespace Drupal\shortcut\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\shortcut\ShortcutSetInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+
+/**
+ * Builds the page for administering shortcut sets.
+ */
+class ShortcutSetController extends ControllerBase {
+
+  /**
+   * Creates a new link in the provided shortcut set.
+   *
+   * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set
+   *   The shortcut set to add a link to.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect response to the front page, or the previous location.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+   */
+  public function addShortcutLinkInline(ShortcutSetInterface $shortcut_set, Request $request) {
+    $link = $request->query->get('link');
+    $name = $request->query->get('name');
+    if (shortcut_valid_link($link)) {
+      $shortcut = $this->entityManager()->getStorage('shortcut')->create(array(
+        'title' => $name,
+        'shortcut_set' => $shortcut_set->id(),
+        'path' => $link,
+      ));
+
+      try {
+        $shortcut->save();
+        drupal_set_message($this->t('Added a shortcut for %title.', array('%title' => $shortcut->label())));
+      }
+      catch (\Exception $e) {
+        drupal_set_message($this->t('Unable to add a shortcut for %title.', array('%title' => $shortcut->label())));
+      }
+
+      return $this->redirect('<front>');
+    }
+
+    throw new AccessDeniedHttpException();
+  }
+
+}
diff --git a/core/modules/shortcut/src/Entity/Shortcut.php b/core/modules/shortcut/src/Entity/Shortcut.php
new file mode 100644
index 0000000..00d0dd3
--- /dev/null
+++ b/core/modules/shortcut/src/Entity/Shortcut.php
@@ -0,0 +1,202 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Entity\Shortcut.
+ */
+
+namespace Drupal\shortcut\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldDefinition;
+use Drupal\Core\Url;
+use Drupal\shortcut\ShortcutInterface;
+
+/**
+ * Defines the shortcut entity class.
+ *
+ * @ContentEntityType(
+ *   id = "shortcut",
+ *   label = @Translation("Shortcut link"),
+ *   controllers = {
+ *     "access" = "Drupal\shortcut\ShortcutAccessController",
+ *     "form" = {
+ *       "default" = "Drupal\shortcut\ShortcutFormController",
+ *       "add" = "Drupal\shortcut\ShortcutFormController",
+ *       "delete" = "Drupal\shortcut\Form\ShortcutDeleteForm"
+ *     },
+ *     "translation" = "Drupal\content_translation\ContentTranslationHandler"
+ *   },
+ *   base_table = "shortcut",
+ *   data_table = "shortcut_field_data",
+ *   translatable = TRUE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "bundle" = "shortcut_set",
+ *     "label" = "title"
+ *   },
+ *   links = {
+ *     "delete-form" = "shortcut.link_delete",
+ *     "edit-form" = "shortcut.link_edit"
+ *   }
+ * )
+ */
+class Shortcut extends ContentEntityBase implements ShortcutInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    return $this->get('title')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTitle($link_title) {
+    $this->set('title', $link_title);
+   return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWeight() {
+    return $this->get('weight')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setWeight($weight) {
+    $this->set('weight', $weight);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteName() {
+    return $this->get('route_name')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRouteName($route_name) {
+    $this->set('route_name', $route_name);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteParams() {
+    $value = $this->get('route_parameters')->getValue();
+    return reset($value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRouteParams($route_parameters) {
+    $this->set('route_parameters', array('value' => $route_parameters));
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageInterface $storage, array &$values) {
+    parent::preCreate($storage, $values);
+
+    if (!isset($values['shortcut_set'])) {
+      $values['shortcut_set'] = 'default';
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    parent::preSave($storage);
+
+    $url = Url::createFromPath($this->path->value);
+    $this->setRouteName($url->getRouteName());
+    $this->setRouteParams($url->getRouteParameters());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields['id'] = FieldDefinition::create('integer')
+      ->setLabel(t('ID'))
+      ->setDescription(t('The ID of the shortcut.'))
+      ->setReadOnly(TRUE)
+      ->setSetting('unsigned', TRUE);
+
+    $fields['uuid'] = FieldDefinition::create('uuid')
+      ->setLabel(t('UUID'))
+      ->setDescription(t('The UUID of the shortcut.'))
+      ->setReadOnly(TRUE);
+
+    $fields['shortcut_set'] = FieldDefinition::create('entity_reference')
+      ->setLabel(t('Shortcut set'))
+      ->setDescription(t('The bundle of the shortcut.'))
+      ->setSetting('target_type', 'shortcut_set')
+      ->setRequired(TRUE);
+
+    $fields['title'] = FieldDefinition::create('string')
+      ->setLabel(t('Name'))
+      ->setDescription(t('The name of the shortcut.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(TRUE)
+      ->setSettings(array(
+        'default_value' => '',
+        'max_length' => 255,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'string',
+        'weight' => -10,
+        'settings' => array(
+          'size' => 40,
+        ),
+      ));
+
+    $fields['weight'] = FieldDefinition::create('integer')
+      ->setLabel(t('Weight'))
+      ->setDescription(t('Weight among shortcuts in the same shortcut set.'));
+
+    $fields['route_name'] = FieldDefinition::create('string')
+      ->setLabel(t('Route name'))
+      ->setDescription(t('The machine name of a defined Route this shortcut represents.'));
+
+    $fields['route_parameters'] = FieldDefinition::create('map')
+      ->setLabel(t('Route parameters'))
+      ->setDescription(t('A serialized array of route parameters of this shortcut.'));
+
+    $fields['langcode'] = FieldDefinition::create('language')
+      ->setLabel(t('Language code'))
+      ->setDescription(t('The language code of the shortcut.'));
+
+    $fields['default_langcode'] = FieldDefinition::create('boolean')
+      ->setLabel(t('Default language'))
+      ->setDescription(t('Flag to indicate whether this is the default language.'));
+
+    $fields['path'] = FieldDefinition::create('string')
+      ->setLabel(t('Path'))
+      ->setDescription(t('The computed shortcut path.'))
+      ->setComputed(TRUE);
+
+    $item_definition = $fields['path']->getItemDefinition();
+    $item_definition->setClass('\Drupal\shortcut\ShortcutPathItem');
+    $fields['path']->setItemDefinition($item_definition);
+
+    return $fields;
+  }
+
+}
diff --git a/core/modules/shortcut/src/Entity/ShortcutSet.php b/core/modules/shortcut/src/Entity/ShortcutSet.php
new file mode 100644
index 0000000..f4d5d59
--- /dev/null
+++ b/core/modules/shortcut/src/Entity/ShortcutSet.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Entity\ShortcutSet.
+ */
+
+namespace Drupal\shortcut\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\shortcut\ShortcutSetInterface;
+
+/**
+ * Defines the Shortcut set configuration entity.
+ *
+ * @ConfigEntityType(
+ *   id = "shortcut_set",
+ *   label = @Translation("Shortcut set"),
+ *   controllers = {
+ *     "storage" = "Drupal\shortcut\ShortcutSetStorage",
+ *     "access" = "Drupal\shortcut\ShortcutSetAccessController",
+ *     "list_builder" = "Drupal\shortcut\ShortcutSetListBuilder",
+ *     "form" = {
+ *       "default" = "Drupal\shortcut\ShortcutSetFormController",
+ *       "add" = "Drupal\shortcut\ShortcutSetFormController",
+ *       "edit" = "Drupal\shortcut\ShortcutSetFormController",
+ *       "customize" = "Drupal\shortcut\Form\SetCustomize",
+ *       "delete" = "Drupal\shortcut\Form\ShortcutSetDeleteForm"
+ *     }
+ *   },
+ *   config_prefix = "set",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   },
+ *   links = {
+ *     "customize-form" = "shortcut.set_customize",
+ *     "delete-form" = "shortcut.set_delete",
+ *     "edit-form" = "shortcut.set_edit"
+ *   }
+ * )
+ */
+class ShortcutSet extends ConfigEntityBase implements ShortcutSetInterface {
+
+  /**
+   * The machine name for the configuration entity.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the configuration entity.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(EntityStorageInterface $storage) {
+    parent::postCreate($storage);
+
+    // Generate menu-compatible set name.
+    if (!$this->getOriginalId()) {
+      // Save a new shortcut set with links copied from the user's default set.
+      $default_set = shortcut_default_set();
+      foreach ($default_set->getShortcuts() as $shortcut) {
+        $shortcut = $shortcut->createDuplicate();
+        $shortcut->enforceIsNew();
+        $shortcut->shortcut_set->target_id = $this->id();
+        $shortcut->save();
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageInterface $storage, array $entities) {
+    parent::preDelete($storage, $entities);
+
+    foreach ($entities as $entity) {
+      $storage->deleteAssignedShortcutSets($entity);
+
+      // Next, delete the shortcuts for this set.
+      $shortcut_ids = \Drupal::entityQuery('shortcut')
+        ->condition('shortcut_set', $entity->id(), '=')
+        ->execute();
+
+      $controller = \Drupal::entityManager()->getStorage('shortcut');
+      $entities = $controller->loadMultiple($shortcut_ids);
+      $controller->delete($entities);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetLinkWeights() {
+    $weight = -50;
+    foreach ($this->getShortcuts() as $shortcut) {
+      $shortcut->setWeight(++$weight);
+      $shortcut->save();
+    }
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getShortcuts() {
+    return \Drupal::entityManager()->getStorage('shortcut')->loadByProperties(array('shortcut_set' => $this->id()));
+  }
+
+}
diff --git a/core/modules/shortcut/src/Form/SetCustomize.php b/core/modules/shortcut/src/Form/SetCustomize.php
new file mode 100644
index 0000000..0d427be
--- /dev/null
+++ b/core/modules/shortcut/src/Form/SetCustomize.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Form\SetCustomize.
+ */
+
+namespace Drupal\shortcut\Form;
+
+use Drupal\Core\Entity\EntityFormController;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Render\Element;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Builds the shortcut set customize form.
+ */
+class SetCustomize extends EntityFormController {
+
+  /**
+   * The entity being used by this form.
+   *
+   * @var \Drupal\shortcut\ShortcutSetInterface
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form = parent::form($form, $form_state);
+    $form['shortcuts'] = array(
+      '#tree' => TRUE,
+      '#weight' => -20,
+    );
+
+    $form['shortcuts']['links'] = array(
+      '#type' => 'table',
+      '#header' => array(t('Name'), t('Weight'), t('Operations')),
+      '#empty' => $this->t('No shortcuts available. <a href="@link">Add a shortcut</a>', array('@link' => $this->urlGenerator()->generateFromRoute('shortcut.link_add', array('shortcut_set' => $this->entity->id())))),
+      '#attributes' => array('id' => 'shortcuts'),
+      '#tabledrag' => array(
+        array(
+          'action' => 'order',
+          'relationship' => 'sibling',
+          'group' => 'shortcut-weight',
+        ),
+      ),
+    );
+
+    foreach ($this->entity->getShortcuts() as $shortcut) {
+      $id = $shortcut->id();
+      $form['shortcuts']['links'][$id]['#attributes']['class'][] = 'draggable';
+      $form['shortcuts']['links'][$id]['name']['#markup'] = l($shortcut->getTitle(), $shortcut->path->value);
+      $form['shortcuts']['links'][$id]['#weight'] = $shortcut->getWeight();
+      $form['shortcuts']['links'][$id]['weight'] = array(
+        '#type' => 'weight',
+        '#title' => t('Weight for @title', array('@title' => $shortcut->getTitle())),
+        '#title_display' => 'invisible',
+        '#default_value' => $shortcut->getWeight(),
+        '#attributes' => array('class' => array('shortcut-weight')),
+      );
+
+      $links['edit'] = array(
+        'title' => t('Edit'),
+        'href' => "admin/config/user-interface/shortcut/link/$id",
+      );
+      $links['delete'] = array(
+        'title' => t('Delete'),
+        'href' => "admin/config/user-interface/shortcut/link/$id/delete",
+      );
+      $form['shortcuts']['links'][$id]['operations'] = array(
+        '#type' => 'operations',
+        '#links' => $links,
+      );
+    }
+    // Sort the list so the output is ordered by weight.
+    uasort($form['shortcuts']['links'], array('\Drupal\Component\Utility\SortArray', 'sortByWeightProperty'));
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, array &$form_state) {
+    // Only includes a Save action for the entity, no direct Delete button.
+    return array(
+      'submit' => array(
+        '#value' => t('Save changes'),
+        '#access' => (bool) Element::getVisibleChildren($form['shortcuts']['links']),
+        '#submit' => array(
+          array($this, 'submit'),
+          array($this, 'save'),
+        ),
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, array &$form_state) {
+    foreach ($this->entity->getShortcuts() as $shortcut) {
+      $shortcut->setWeight($form_state['values']['shortcuts']['links'][$shortcut->id()]['weight']);
+      $shortcut->save();
+    }
+    drupal_set_message(t('The shortcut set has been updated.'));
+  }
+
+}
diff --git a/core/modules/shortcut/src/Form/ShortcutDeleteForm.php b/core/modules/shortcut/src/Form/ShortcutDeleteForm.php
new file mode 100644
index 0000000..b5bbabe
--- /dev/null
+++ b/core/modules/shortcut/src/Form/ShortcutDeleteForm.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Form\ShortcutDeleteForm.
+ */
+
+namespace Drupal\shortcut\Form;
+
+use Drupal\Core\Entity\ContentEntityConfirmFormBase;
+
+/**
+ * Builds the shortcut link deletion form.
+ */
+class ShortcutDeleteForm extends ContentEntityConfirmFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'shortcut_confirm_delete';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete the shortcut %title?', array('%title' => $this->entity->title->value));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelRoute() {
+    return array(
+      'route_name' => 'shortcut.set_customize',
+      'route_parameters' => array(
+        'shortcut_set' => $this->entity->bundle(),
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array $form, array &$form_state) {
+    $this->entity->delete();
+    $form_state['redirect_route'] = array(
+      'route_name' => 'shortcut.set_customize',
+      'route_parameters' => array(
+        'shortcut_set' => $this->entity->bundle(),
+      ),
+    );
+    drupal_set_message($this->t('The shortcut %title has been deleted.', array('%title' => $this->entity->title->value)));
+  }
+
+}
diff --git a/core/modules/shortcut/src/Form/ShortcutForm.php b/core/modules/shortcut/src/Form/ShortcutForm.php
new file mode 100644
index 0000000..4f556b7
--- /dev/null
+++ b/core/modules/shortcut/src/Form/ShortcutForm.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Form\ShortcutForm.
+ */
+
+namespace Drupal\shortcut\Form;
+
+use Drupal\user\UserInterface;
+
+/**
+ * Temporary form controller for shortcut module.
+ */
+class ShortcutForm {
+
+  /**
+   * Wraps shortcut_set_switch().
+   *
+   * @todo Remove shortcut_set_switch().
+   */
+  public function overview(UserInterface $user) {
+    module_load_include('admin.inc', 'shortcut');
+    return \Drupal::formBuilder()->getForm('shortcut_set_switch', $user);
+  }
+
+}
diff --git a/core/modules/shortcut/src/Form/ShortcutSetDeleteForm.php b/core/modules/shortcut/src/Form/ShortcutSetDeleteForm.php
new file mode 100644
index 0000000..504b81e
--- /dev/null
+++ b/core/modules/shortcut/src/Form/ShortcutSetDeleteForm.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Form\ShortcutSetDeleteForm.
+ */
+
+namespace Drupal\shortcut\Form;
+
+use Drupal\Core\Entity\EntityConfirmFormBase;
+use Drupal\shortcut\ShortcutSetStorageInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Database\Connection;
+
+/**
+ * Builds the shortcut set deletion form.
+ */
+class ShortcutSetDeleteForm extends EntityConfirmFormBase {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The shortcut storage.
+   *
+   * @var \Drupal\shortcut\ShortcutSetStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Constructs a ShortcutSetDeleteForm object.
+   */
+  public function __construct(Connection $database, ShortcutSetStorageInterface $storage) {
+    $this->database = $database;
+    $this->storage = $storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('database'),
+      $container->get('entity.manager')->getStorage('shortcut_set')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to delete the shortcut set %title?', array('%title' => $this->entity->label()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelRoute() {
+    return array(
+      'route_name' => 'shortcut.set_customize',
+      'route_parameters' => array(
+        'shortcut_set' => $this->entity->id(),
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    // Find out how many users are directly assigned to this shortcut set, and
+    // make a message.
+    $number = $this->storage->countAssignedUsers($this->entity);
+    $info = '';
+    if ($number) {
+      $info .= '<p>' . format_plural($number,
+        '1 user has chosen or been assigned to this shortcut set.',
+        '@count users have chosen or been assigned to this shortcut set.') . '</p>';
+    }
+
+    // Also, if a module implements hook_shortcut_default_set(), it's possible
+    // that this set is being used as a default set. Add a message about that too.
+    if ($this->moduleHandler->getImplementations('shortcut_default_set')) {
+      $info .= '<p>' . t('If you have chosen this shortcut set as the default for some or all users, they may also be affected by deleting it.') . '</p>';
+    }
+
+    $form['info'] = array(
+      '#markup' => $info,
+    );
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array $form, array &$form_state) {
+    $this->entity->delete();
+    $form_state['redirect_route']['route_name'] = 'shortcut.set_admin';
+    drupal_set_message(t('The shortcut set %title has been deleted.', array('%title' => $this->entity->label())));
+  }
+
+}
diff --git a/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php b/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php
new file mode 100644
index 0000000..cb64ec0
--- /dev/null
+++ b/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Plugin\Block\ShortcutsBlock.
+ */
+
+namespace Drupal\shortcut\Plugin\Block;
+
+use Drupal\block\BlockBase;
+
+/**
+ * Provides a 'Shortcut' block.
+ *
+ * @Block(
+ *   id = "shortcuts",
+ *   admin_label = @Translation("Shortcuts"),
+ *   category = @Translation("Menus")
+ * )
+ */
+class ShortcutsBlock extends BlockBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    return array(
+      shortcut_renderable_links(shortcut_current_displayed_set()),
+    );
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutAccessController.php b/core/modules/shortcut/src/ShortcutAccessController.php
new file mode 100644
index 0000000..402aca7
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutAccessController.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutAccessController.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Entity\EntityAccessController;
+use Drupal\Core\Entity\EntityControllerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines the access controller for the test entity type.
+ */
+class ShortcutAccessController extends EntityAccessController implements EntityControllerInterface {
+
+  /**
+   * The shortcut_set storage.
+   *
+   * @var \Drupal\shortcut\ShortcutSetStorage
+   */
+  protected $shortcutSetStorage;
+
+  /**
+   * Constructs a ShortcutAccessController object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\shortcut\ShortcutSetStorage $shortcut_set_storage
+   *   The shortcut_set storage.
+   */
+  public function __construct(EntityTypeInterface $entity_type, ShortcutSetStorage $shortcut_set_storage) {
+    parent::__construct($entity_type);
+    $this->shortcutSetStorage = $shortcut_set_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager')->getStorage('shortcut_set')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
+    if ($shortcut_set = $this->shortcutSetStorage->load($entity->bundle())) {
+      return shortcut_set_edit_access($shortcut_set, $account);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    if ($shortcut_set = $this->shortcutSetStorage->load($entity_bundle)) {
+      return shortcut_set_edit_access($shortcut_set, $account);
+    }
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutFormController.php b/core/modules/shortcut/src/ShortcutFormController.php
new file mode 100644
index 0000000..c34f876
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutFormController.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutFormController.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Entity\ContentEntityFormController;
+use Drupal\Core\Language\Language;
+
+/**
+ * Form controller for the shortcut entity forms.
+ */
+class ShortcutFormController extends ContentEntityFormController {
+
+  /**
+   * The entity being used by this form.
+   *
+   * @var \Drupal\shortcut\ShortcutInterface
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form = parent::form($form, $form_state);
+
+    $form['path'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Path'),
+      '#size' => 40,
+      '#maxlength' => 255,
+      '#field_prefix' => $this->url('<front>', array(), array('absolute' => TRUE)),
+      '#default_value' => $this->entity->path->value,
+    );
+
+    $form['langcode'] = array(
+      '#title' => t('Language'),
+      '#type' => 'language_select',
+      '#default_value' => $this->entity->getUntranslated()->language()->id,
+      '#languages' => Language::STATE_ALL,
+    );
+
+    return $form;
+  }
+
+  /**
+   * Overrides EntityFormController::buildEntity().
+   */
+  public function buildEntity(array $form, array &$form_state) {
+    $entity = parent::buildEntity($form, $form_state);
+
+    // Set the computed 'path' value so it can used in the preSave() method to
+    // derive the route name and parameters.
+    $entity->path->value = $form_state['values']['path'];
+
+    return $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array $form, array &$form_state) {
+    if (!shortcut_valid_link($form_state['values']['path'])) {
+      $this->setFormError('path', $form_state, $this->t('The shortcut must correspond to a valid path on the site.'));
+    }
+
+    parent::validate($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, array &$form_state) {
+    $entity = $this->entity;
+    $entity->save();
+
+    if ($entity->isNew()) {
+      $message = $this->t('The shortcut %link has been updated.', array('%link' => $entity->getTitle()));
+    }
+    else {
+      $message = $this->t('Added a shortcut for %title.', array('%title' => $entity->getTitle()));
+    }
+    drupal_set_message($message);
+
+    $form_state['redirect_route'] = array(
+      'route_name' => 'shortcut.set_customize',
+      'route_parameters' => array('shortcut_set' => $entity->bundle()),
+    );
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutInterface.php b/core/modules/shortcut/src/ShortcutInterface.php
new file mode 100644
index 0000000..9a6bb98
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutInterface.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutInterface.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+
+/**
+ * Provides an interface defining a shortcut entity.
+ */
+interface ShortcutInterface extends ContentEntityInterface {
+
+  /**
+   * Returns the title of this shortcut.
+   *
+   * @return string
+   *   The title of this shortcut.
+   */
+  public function getTitle();
+
+  /**
+   * Sets the title of this shortcut.
+   *
+   * @param string $title
+   *   The title of this shortcut.
+   *
+   * @return \Drupal\shortcut\ShortcutInterface
+   *   The called shortcut entity.
+   */
+  public function setTitle($title);
+
+  /**
+   * Returns the weight among shortcuts with the same depth.
+   *
+   * @return int
+   *   The shortcut weight.
+   */
+  public function getWeight();
+
+  /**
+   * Sets the weight among shortcuts with the same depth.
+   *
+   * @param int $weight
+   *   The shortcut weight.
+   *
+   * @return \Drupal\shortcut\ShortcutInterface
+   *   The called shortcut entity.
+   */
+  public function setWeight($weight);
+
+  /**
+   * Returns the route name associated with this shortcut, if any.
+   *
+   * @return string|null
+   *   The route name of this shortcut.
+   */
+  public function getRouteName();
+
+  /**
+   * Sets the route name associated with this shortcut.
+   *
+   * @param string|null $route_name
+   *   The route name associated with this shortcut.
+   *
+   * @return \Drupal\shortcut\ShortcutInterface
+   *   The called shortcut entity.
+   */
+  public function setRouteName($route_name);
+
+  /**
+   * Returns the route parameters associated with this shortcut, if any.
+   *
+   * @return array
+   *   The route parameters of this shortcut.
+   */
+  public function getRouteParams();
+
+  /**
+   * Sets the route parameters associated with this shortcut.
+   *
+   * @param array $route_parameters
+   *   The route parameters associated with this shortcut.
+   *
+   * @return \Drupal\shortcut\ShortcutInterface
+   *   The called shortcut entity.
+  */
+  public function setRouteParams($route_parameters);
+
+}
diff --git a/core/modules/shortcut/src/ShortcutPathItem.php b/core/modules/shortcut/src/ShortcutPathItem.php
new file mode 100644
index 0000000..8ad82d3
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutPathItem.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutPathItem.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * The field item for the 'path' field.
+ */
+class ShortcutPathItem extends StringItem {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['value'] = DataDefinition::create('string')
+      ->setLabel(t('String value'))
+      ->setComputed(TRUE)
+      ->setClass('\Drupal\shortcut\ShortcutPathValue');
+    return $properties;
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutPathValue.php b/core/modules/shortcut/src/ShortcutPathValue.php
new file mode 100644
index 0000000..2c8b4e5
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutPathValue.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutPathValue.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\TypedData\TypedData;
+
+/**
+ * A computed property for the string value of the path field.
+ */
+class ShortcutPathValue extends TypedData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    if (!isset($this->value)) {
+      if (!isset($this->parent)) {
+        throw new \InvalidArgumentException('Computed properties require context for computation.');
+      }
+
+      $entity = $this->parent->getEntity();
+      if ($route_name = $entity->getRouteName()) {
+        $path = \Drupal::urlGenerator()->getPathFromRoute($route_name, $entity->getRouteParams());
+        $this->value = trim($path, '/');
+      }
+      else {
+        $this->value = NULL;
+      }
+    }
+    return $this->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($value, $notify = TRUE) {
+    // Normalize the path in case it is an alias.
+    $value = \Drupal::service('path.alias_manager')->getSystemPath($value);
+    if (empty($value)) {
+      $value = '<front>';
+    }
+
+    parent::setValue($value, $notify);
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutSetAccessController.php b/core/modules/shortcut/src/ShortcutSetAccessController.php
new file mode 100644
index 0000000..c60d52a
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutSetAccessController.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutSetAccessController.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityAccessController;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access controller for the shortcut entity type.
+ */
+class ShortcutSetAccessController extends EntityAccessController {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
+    switch ($operation) {
+      case 'update':
+        if ($account->hasPermission('administer shortcuts')) {
+          return TRUE;
+        }
+        if ($account->hasPermission('customize shortcut links')) {
+          return $entity == shortcut_current_displayed_set($account);
+        }
+        return FALSE;
+        break;
+
+      case 'delete':
+        if (!$account->hasPermission('administer shortcuts')) {
+          return FALSE;
+        }
+        return $entity->id() != 'default';
+        break;
+    }
+  }
+
+  /**
+  * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return $account->hasPermission('administer shortcuts') || $account->hasPermission('customize shortcut links');
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutSetFormController.php b/core/modules/shortcut/src/ShortcutSetFormController.php
new file mode 100644
index 0000000..5360b5f
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutSetFormController.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutSetFormController.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Entity\EntityFormController;
+
+/**
+ * Form controller for the shortcut set entity edit forms.
+ */
+class ShortcutSetFormController extends EntityFormController {
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state) {
+    $form = parent::form($form, $form_state);
+
+    $entity = $this->entity;
+    $form['label'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Set name'),
+      '#description' => t('The new set is created by copying items from your default shortcut set.'),
+      '#required' => TRUE,
+      '#default_value' => $entity->label(),
+    );
+    $form['id'] = array(
+      '#type' => 'machine_name',
+      '#machine_name' => array(
+        'exists' => 'shortcut_set_load',
+        'source' => array('label'),
+        'replace_pattern' => '[^a-z0-9-]+',
+        'replace' => '-',
+      ),
+      '#default_value' => $entity->id(),
+      '#disabled' => !$entity->isNew(),
+      // This id could be used for menu name.
+      '#maxlength' => 23,
+    );
+
+    $form['actions']['submit']['#value'] = t('Create new set');
+
+    return $form;
+  }
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityFormController::actions().
+   */
+  protected function actions(array $form, array &$form_state) {
+    // Disable delete of default shortcut set.
+    $actions = parent::actions($form, $form_state);
+    $actions['delete']['#access'] = $this->entity->access('delete');
+    return $actions;
+  }
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityFormController::validate().
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($form, $form_state);
+    $entity = $this->entity;
+    // Check to prevent a duplicate title.
+    if ($form_state['values']['label'] != $entity->label() && shortcut_set_title_exists($form_state['values']['label'])) {
+      $this->setFormError('label', $form_state, $this->t('The shortcut set %name already exists. Choose another name.', array('%name' => $form_state['values']['label'])));
+    }
+  }
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $entity = $this->entity;
+    $is_new = !$entity->getOriginalId();
+    $entity->save();
+
+    if ($is_new) {
+      drupal_set_message(t('The %set_name shortcut set has been created. You can edit it from this page.', array('%set_name' => $entity->label())));
+    }
+    else {
+      drupal_set_message(t('Updated set name to %set-name.', array('%set-name' => $entity->label())));
+    }
+    $form_state['redirect_route'] = $this->entity->urlInfo('customize-form');
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutSetInterface.php b/core/modules/shortcut/src/ShortcutSetInterface.php
new file mode 100644
index 0000000..21cc4f3
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutSetInterface.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutSetInterface.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface defining a shortcut set entity.
+ */
+interface ShortcutSetInterface extends ConfigEntityInterface {
+
+  /**
+   * Resets the link weights in a shortcut set to match their current order.
+   *
+   * This function can be used, for example, when a new shortcut link is added
+   * to the set. If the link is added to the end of the array and this function
+   * is called, it will force that link to display at the end of the list.
+   *
+   * @return \Drupal\shortcut\ShortcutSetInterface
+   *   The shortcut set.
+   */
+  public function resetLinkWeights();
+
+  /**
+   * Returns all the shortcuts from a shortcut set.
+   *
+   * @return \Drupal\shortcut\ShortcutInterface[]
+   *   An array of shortcut entities.
+   */
+  public function getShortcuts();
+
+}
diff --git a/core/modules/shortcut/src/ShortcutSetListBuilder.php b/core/modules/shortcut/src/ShortcutSetListBuilder.php
new file mode 100644
index 0000000..907f470
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutSetListBuilder.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutSetListBuilder.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Defines a class to build a listing of shortcut set entities.
+ *
+ * @see \Drupal\shortcut\Entity\ShortcutSet
+ */
+class ShortcutSetListBuilder extends ConfigEntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['name'] = t('Name');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOperations(EntityInterface $entity) {
+    $operations = parent::getDefaultOperations($entity);
+
+    if (isset($operations['edit'])) {
+      $operations['edit']['title'] = t('Edit shortcut set');
+    }
+
+    $operations['list'] = array(
+      'title' => t('List links'),
+    ) + $entity->urlInfo('customize-form')->toArray();
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $row['name'] = $this->getLabel($entity);
+    return $row + parent::buildRow($entity);
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutSetStorage.php b/core/modules/shortcut/src/ShortcutSetStorage.php
new file mode 100644
index 0000000..ea723d6
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutSetStorage.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutSetStorage.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Config\Entity\ConfigEntityStorage;
+
+/**
+ * Defines a storage for shortcut_set entities.
+ */
+class ShortcutSetStorage extends ConfigEntityStorage implements ShortcutSetStorageInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteAssignedShortcutSets(ShortcutSetInterface $entity) {
+    // First, delete any user assignments for this set, so that each of these
+    // users will go back to using whatever default set applies.
+    db_delete('shortcut_set_users')
+      ->condition('set_name', $entity->id())
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function assignUser(ShortcutSetInterface $shortcut_set, $account) {
+    db_merge('shortcut_set_users')
+      ->key('uid', $account->id())
+      ->fields(array('set_name' => $shortcut_set->id()))
+      ->execute();
+    drupal_static_reset('shortcut_current_displayed_set');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function unassignUser($account) {
+    $deleted = db_delete('shortcut_set_users')
+      ->condition('uid', $account->id())
+      ->execute();
+    return (bool) $deleted;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAssignedToUser($account) {
+    $query = db_select('shortcut_set_users', 'ssu');
+    $query->fields('ssu', array('set_name'));
+    $query->condition('ssu.uid', $account->id());
+    return $query->execute()->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countAssignedUsers(ShortcutSetInterface $shortcut_set) {
+    return db_query('SELECT COUNT(*) FROM {shortcut_set_users} WHERE set_name = :name', array(':name' => $shortcut_set->id()))->fetchField();
+  }
+
+}
diff --git a/core/modules/shortcut/src/ShortcutSetStorageInterface.php b/core/modules/shortcut/src/ShortcutSetStorageInterface.php
new file mode 100644
index 0000000..11af402
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutSetStorageInterface.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutSetStorageInterface.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
+
+/**
+ * Defines a common interface for shortcut entity controller classes.
+ */
+interface ShortcutSetStorageInterface extends ConfigEntityStorageInterface {
+
+  /**
+   * Assigns a user to a particular shortcut set.
+   *
+   * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set
+   *   An object representing the shortcut set.
+   * @param $account
+   *   A user account that will be assigned to use the set.
+   */
+  public function assignUser(ShortcutSetInterface $shortcut_set, $account);
+
+  /**
+   * Unassigns a user from any shortcut set they may have been assigned to.
+   *
+   * The user will go back to using whatever default set applies.
+   *
+   * @param $account
+   *   A user account that will be removed from the shortcut set assignment.
+   *
+   * @return bool
+   *   TRUE if the user was previously assigned to a shortcut set and has been
+   *   successfully removed from it. FALSE if the user was already not assigned
+   *   to any set.
+   */
+  public function unassignUser($account);
+
+  /**
+   * Delete shortcut sets assigned to users.
+   *
+   * @param \Drupal\shortcut\ShortcutSetInterface $entity
+   *   Delete the user assigned sets belonging to this shortcut.
+   */
+  public function deleteAssignedShortcutSets(ShortcutSetInterface $entity);
+
+  /**
+   * Get the name of the set assigned to this user.
+   *
+   * @param \Drupal\user\Entity\User
+   *   The user account.
+   *
+   * @return string
+   *   The name of the shortcut set assigned to this user.
+   */
+  public function getAssignedToUser($account);
+
+  /**
+   * Get the number of users who have this set assigned to them.
+   *
+   * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set
+   *   The shortcut to count the users assigned to.
+   *
+   * @return int
+   *   The number of users who have this set assigned to them.
+   */
+  public function countAssignedUsers(ShortcutSetInterface $shortcut_set);
+}
diff --git a/core/modules/shortcut/src/Tests/ShortcutLinksTest.php b/core/modules/shortcut/src/Tests/ShortcutLinksTest.php
new file mode 100644
index 0000000..67f40ad
--- /dev/null
+++ b/core/modules/shortcut/src/Tests/ShortcutLinksTest.php
@@ -0,0 +1,204 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\shortcut\Tests\ShortcutLinksTest.
+ */
+
+namespace Drupal\shortcut\Tests;
+
+/**
+ * Defines shortcut links test cases.
+ */
+class ShortcutLinksTest extends ShortcutTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('router_test', 'views');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Shortcut link functionality',
+      'description' => 'Create, view, edit, delete, and change shortcut links.',
+      'group' => 'Shortcut',
+    );
+  }
+
+  /**
+   * Tests that creating a shortcut works properly.
+   */
+  public function testShortcutLinkAdd() {
+    $set = $this->set;
+
+    // Create an alias for the node so we can test aliases.
+    $path = array(
+      'source' => 'node/' . $this->node->id(),
+      'alias' => $this->randomName(8),
+    );
+    $this->container->get('path.alias_storage')->save($path['source'], $path['alias']);
+
+    // Create some paths to test.
+    $test_cases = array(
+      array('path' => ''),
+      array('path' => 'admin'),
+      array('path' => 'admin/config/system/site-information'),
+      array('path' => 'node/' . $this->node->id() . '/edit'),
+      array('path' => $path['alias']),
+      array('path' => 'router_test/test2'),
+      array('path' => 'router_test/test3/value'),
+    );
+
+    // Check that each new shortcut links where it should.
+    foreach ($test_cases as $test) {
+      $title = $this->randomName();
+      $form_data = array(
+        'title[0][value]' => $title,
+        'path' => $test['path'],
+      );
+      $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save'));
+      $this->assertResponse(200);
+      $saved_set = shortcut_set_load($set->id());
+      $paths = $this->getShortcutInformation($saved_set, 'path');
+      $this->assertTrue(in_array($this->container->get('path.alias_manager')->getSystemPath($test['path']), $paths), 'Shortcut created: ' . $test['path']);
+      $this->assertLink($title, 0, 'Shortcut link found on the page.');
+    }
+  }
+
+  /**
+   * Tests that the "add to shortcut" and "remove from shortcut" links work.
+   */
+  public function testShortcutQuickLink() {
+    theme_enable(array('seven'));
+    \Drupal::config('system.theme')->set('admin', 'seven')->save();
+    $this->container->get('config.factory')->get('node.settings')->set('use_admin_theme', '1')->save();
+    $this->container->get('router.builder')->rebuild();
+
+    $this->drupalLogin($this->root_user);
+    $this->drupalGet('admin/config/system/cron');
+
+    // Test the "Add to shortcuts" link.
+    $this->clickLink('Add to Default shortcuts');
+    $this->assertText('Added a shortcut for Cron.');
+    $this->assertLink('Cron', 0, 'Shortcut link found on page');
+
+    $this->drupalGet('admin/structure');
+    $this->assertLink('Cron', 0, 'Shortcut link found on different page');
+
+    // Test the "Remove from shortcuts" link.
+    $this->clickLink('Cron');
+    $this->clickLink('Remove from Default shortcuts');
+    $this->drupalPostForm(NULL, array(), 'Delete');
+    $this->assertText('The shortcut Cron has been deleted.');
+    $this->assertNoLink('Cron', 'Shortcut link removed from page');
+
+    $this->drupalGet('admin/structure');
+    $this->assertNoLink('Cron', 'Shortcut link removed from different page');
+  }
+
+  /**
+   * Tests that shortcut links can be renamed.
+   */
+  public function testShortcutLinkRename() {
+    $set = $this->set;
+
+    // Attempt to rename shortcut link.
+    $new_link_name = $this->randomName();
+
+    $shortcuts = $set->getShortcuts();
+    $shortcut = reset($shortcuts);
+    $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $new_link_name, 'path' => $shortcut->path->value), t('Save'));
+    $saved_set = shortcut_set_load($set->id());
+    $titles = $this->getShortcutInformation($saved_set, 'title');
+    $this->assertTrue(in_array($new_link_name, $titles), 'Shortcut renamed: ' . $new_link_name);
+    $this->assertLink($new_link_name, 0, 'Renamed shortcut link appears on the page.');
+  }
+
+  /**
+   * Tests that changing the path of a shortcut link works.
+   */
+  public function testShortcutLinkChangePath() {
+    $set = $this->set;
+
+    // Tests changing a shortcut path.
+    $new_link_path = 'admin/config';
+
+    $shortcuts = $set->getShortcuts();
+    $shortcut = reset($shortcuts);
+    $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $shortcut->getTitle(), 'path' => $new_link_path), t('Save'));
+    $saved_set = shortcut_set_load($set->id());
+    $paths = $this->getShortcutInformation($saved_set, 'path');
+    $this->assertTrue(in_array($new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path);
+    $this->assertLinkByHref($new_link_path, 0, 'Shortcut with new path appears on the page.');
+  }
+
+  /**
+   * Tests that changing the route of a shortcut link works.
+   */
+  public function testShortcutLinkChangeRoute() {
+    $this->drupalLogin($this->root_user);
+    $this->drupalGet('admin/content');
+    $this->assertResponse(200);
+    // Disable the view.
+    entity_load('view', 'content')->disable()->save();
+    $this->drupalGet('admin/content');
+    $this->assertResponse(200);
+  }
+
+  /**
+   * Tests deleting a shortcut link.
+   */
+  public function testShortcutLinkDelete() {
+    $set = $this->set;
+
+    $shortcuts = $set->getShortcuts();
+    $shortcut = reset($shortcuts);
+    $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id() . '/delete', array(), 'Delete');
+    $saved_set = shortcut_set_load($set->id());
+    $ids = $this->getShortcutInformation($saved_set, 'id');
+    $this->assertFalse(in_array($shortcut->id(), $ids), 'Successfully deleted a shortcut.');
+
+    // Delete all the remaining shortcut links.
+    entity_delete_multiple('shortcut', array_filter($ids));
+
+    // Get the front page to check that no exceptions occur.
+    $this->drupalGet('');
+  }
+
+  /**
+   * Tests that the add shortcut link is not displayed for 404/403 errors.
+   *
+   * Tests that the "Add to shortcuts" link is not displayed on a page not
+   * found or a page the user does not have access to.
+   */
+  public function testNoShortcutLink() {
+    // Change to a theme that displays shortcuts.
+    theme_enable(array('seven'));
+    \Drupal::config('system.theme')
+      ->set('default', 'seven')
+      ->save();
+
+    $this->drupalGet('page-that-does-not-exist');
+    $result = $this->xpath('//div[contains(@class, "add-shortcut")]');
+    $this->assertTrue(empty($result), 'Add to shortcuts link was not shown on a page not found.');
+
+    // The user does not have access to this path.
+    $this->drupalGet('admin/modules');
+    $result = $this->xpath('//div[contains(@class, "add-shortcut")]');
+    $this->assertTrue(empty($result), 'Add to shortcuts link was not shown on a page the user does not have access to.');
+
+    // Verify that the testing mechanism works by verifying the shortcut
+    // link appears on admin/people.
+    $this->drupalGet('admin/people');
+    $result = $this->xpath('//div[contains(@class, "remove-shortcut")]');
+    $this->assertTrue(!empty($result), 'Remove from shortcuts link was shown on a page the user does have access to.');
+
+    // Verify that the shortcut link appears on routing only pages.
+    $this->drupalGet('router_test/test2');
+    $result = $this->xpath('//div[contains(@class, "add-shortcut")]');
+    $this->assertTrue(!empty($result), 'Add to shortcuts link was shown on a page the user does have access to.');
+  }
+
+}
diff --git a/core/modules/shortcut/src/Tests/ShortcutSetsTest.php b/core/modules/shortcut/src/Tests/ShortcutSetsTest.php
new file mode 100644
index 0000000..2a31535
--- /dev/null
+++ b/core/modules/shortcut/src/Tests/ShortcutSetsTest.php
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\shortcut\Tests\ShortcutSetsTest.
+ */
+
+namespace Drupal\shortcut\Tests;
+
+/**
+ * Defines shortcut set test cases.
+ */
+class ShortcutSetsTest extends ShortcutTestBase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Shortcut set functionality',
+      'description' => 'Create, view, edit, delete, and change shortcut sets.',
+      'group' => 'Shortcut',
+    );
+  }
+
+  /**
+   * Tests creating a shortcut set.
+   */
+  function testShortcutSetAdd() {
+    $this->drupalGet('admin/config/user-interface/shortcut');
+    $this->clickLink(t('Add shortcut set'));
+    $edit = array(
+      'label' => $this->randomName(),
+      'id' => strtolower($this->randomName()),
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $new_set = $this->container->get('entity.manager')->getStorage('shortcut_set')->load($edit['id']);
+    $this->assertIdentical($new_set->id(), $edit['id'], 'Successfully created a shortcut set.');
+    $this->drupalGet('user/' . $this->admin_user->id() . '/shortcuts');
+    $this->assertText($new_set->label(), 'Generated shortcut set was listed as a choice on the user account page.');
+  }
+
+  /**
+   * Tests switching a user's own shortcut set.
+   */
+  function testShortcutSetSwitchOwn() {
+    $new_set = $this->generateShortcutSet($this->randomName());
+
+    // Attempt to switch the default shortcut set to the newly created shortcut
+    // set.
+    $this->drupalPostForm('user/' . $this->admin_user->id() . '/shortcuts', array('set' => $new_set->id()), t('Change set'));
+    $this->assertResponse(200);
+    $current_set = shortcut_current_displayed_set($this->admin_user);
+    $this->assertTrue($new_set->id() == $current_set->id(), 'Successfully switched own shortcut set.');
+  }
+
+  /**
+   * Tests switching another user's shortcut set.
+   */
+  function testShortcutSetAssign() {
+    $new_set = $this->generateShortcutSet($this->randomName());
+
+    shortcut_set_assign_user($new_set, $this->shortcut_user);
+    $current_set = shortcut_current_displayed_set($this->shortcut_user);
+    $this->assertTrue($new_set->id() == $current_set->id(), "Successfully switched another user's shortcut set.");
+  }
+
+  /**
+   * Tests switching a user's shortcut set and creating one at the same time.
+   */
+  function testShortcutSetSwitchCreate() {
+    $edit = array(
+      'set' => 'new',
+      'id' => strtolower($this->randomName()),
+      'label' => $this->randomString(),
+    );
+    $this->drupalPostForm('user/' . $this->admin_user->id() . '/shortcuts', $edit, t('Change set'));
+    $current_set = shortcut_current_displayed_set($this->admin_user);
+    $this->assertNotEqual($current_set->id(), $this->set->id(), 'A shortcut set can be switched to at the same time as it is created.');
+    $this->assertEqual($current_set->label(), $edit['label'], 'The new set is correctly assigned to the user.');
+  }
+
+  /**
+   * Tests switching a user's shortcut set without providing a new set name.
+   */
+  function testShortcutSetSwitchNoSetName() {
+    $edit = array('set' => 'new');
+    $this->drupalPostForm('user/' . $this->admin_user->id() . '/shortcuts', $edit, t('Change set'));
+    $this->assertText(t('The new set label is required.'));
+    $current_set = shortcut_current_displayed_set($this->admin_user);
+    $this->assertEqual($current_set->id(), $this->set->id(), 'Attempting to switch to a new shortcut set without providing a set name does not succeed.');
+  }
+
+  /**
+   * Tests renaming a shortcut set.
+   */
+  function testShortcutSetRename() {
+    $set = $this->set;
+
+    $new_label = $this->randomName();
+    $this->drupalGet('admin/config/user-interface/shortcut');
+    $this->clickLink(t('Edit shortcut set'));
+    $this->drupalPostForm(NULL, array('label' => $new_label), t('Save'));
+    $set = shortcut_set_load($set->id());
+    $this->assertTrue($set->label() == $new_label, 'Shortcut set has been successfully renamed.');
+  }
+
+  /**
+   * Tests renaming a shortcut set to the same name as another set.
+   */
+  function testShortcutSetRenameAlreadyExists() {
+    $set = $this->generateShortcutSet($this->randomName());
+    $existing_label = $this->set->label();
+    $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id(), array('label' => $existing_label), t('Save'));
+    $this->assertRaw(t('The shortcut set %name already exists. Choose another name.', array('%name' => $existing_label)));
+    $set = shortcut_set_load($set->id());
+    $this->assertNotEqual($set->label(), $existing_label, format_string('The shortcut set %title cannot be renamed to %new-title because a shortcut set with that title already exists.', array('%title' => $set->label(), '%new-title' => $existing_label)));
+  }
+
+  /**
+   * Tests unassigning a shortcut set.
+   */
+  function testShortcutSetUnassign() {
+    $new_set = $this->generateShortcutSet($this->randomName());
+
+    shortcut_set_assign_user($new_set, $this->shortcut_user);
+    shortcut_set_unassign_user($this->shortcut_user);
+    $current_set = shortcut_current_displayed_set($this->shortcut_user);
+    $default_set = shortcut_default_set($this->shortcut_user);
+    $this->assertTrue($current_set->id() == $default_set->id(), "Successfully unassigned another user's shortcut set.");
+  }
+
+  /**
+   * Tests deleting a shortcut set.
+   */
+  function testShortcutSetDelete() {
+    $new_set = $this->generateShortcutSet($this->randomName());
+
+    $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $new_set->id() . '/delete', array(), t('Delete'));
+    $sets = entity_load_multiple('shortcut_set');
+    $this->assertFalse(isset($sets[$new_set->id()]), 'Successfully deleted a shortcut set.');
+  }
+
+  /**
+   * Tests deleting the default shortcut set.
+   */
+  function testShortcutSetDeleteDefault() {
+    $this->drupalGet('admin/config/user-interface/shortcut/manage/default/delete');
+    $this->assertResponse(403);
+  }
+
+  /**
+   * Tests creating a new shortcut set with a defined set name.
+   */
+  function testShortcutSetCreateWithSetName() {
+    $random_name = $this->randomName();
+    $new_set = $this->generateShortcutSet($random_name, $random_name);
+    $sets = entity_load_multiple('shortcut_set');
+    $this->assertTrue(isset($sets[$random_name]), 'Successfully created a shortcut set with a defined set name.');
+    $this->drupalGet('user/' . $this->admin_user->id() . '/shortcuts');
+    $this->assertText($new_set->label(), 'Generated shortcut set was listed as a choice on the user account page.');
+  }
+}
diff --git a/core/modules/shortcut/src/Tests/ShortcutTestBase.php b/core/modules/shortcut/src/Tests/ShortcutTestBase.php
new file mode 100644
index 0000000..de9e110
--- /dev/null
+++ b/core/modules/shortcut/src/Tests/ShortcutTestBase.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\shortcut\Tests\ShortcutTestBase.
+ */
+
+namespace Drupal\shortcut\Tests;
+
+use Drupal\shortcut\ShortcutSetInterface;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Defines base class for shortcut test cases.
+ */
+abstract class ShortcutTestBase extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('node', 'toolbar', 'shortcut');
+
+  /**
+   * User with permission to administer shortcuts.
+   */
+  protected $admin_user;
+
+  /**
+   * User with permission to use shortcuts, but not administer them.
+   */
+  protected $shortcut_user;
+
+  /**
+   * Generic node used for testing.
+   */
+  protected $node;
+
+  /**
+   * Site-wide default shortcut set.
+   *
+   * @var \Drupal\shortcut\ShortcutSetInterface
+   */
+  protected $set;
+
+  function setUp() {
+    parent::setUp();
+
+    if ($this->profile != 'standard') {
+      // Create Basic page and Article node types.
+      $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
+      $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
+
+      // Populate the default shortcut set.
+      $shortcut = entity_create('shortcut', array(
+        'set' => 'default',
+        'title' => t('Add content'),
+        'weight' => -20,
+        'path' => 'node/add',
+      ));
+      $shortcut->save();
+
+      $shortcut = entity_create('shortcut', array(
+        'set' => 'default',
+        'title' => t('All content'),
+        'weight' => -19,
+        'path' => 'admin/content',
+      ));
+      $shortcut->save();
+    }
+
+    // Create users.
+    $this->admin_user = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview', 'administer users'));
+    $this->shortcut_user = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets'));
+
+    // Create a node.
+    $this->node = $this->drupalCreateNode(array('type' => 'article'));
+
+    // Log in as admin and grab the default shortcut set.
+    $this->drupalLogin($this->admin_user);
+    $this->set = shortcut_set_load('default');
+    shortcut_set_assign_user($this->set, $this->admin_user);
+  }
+
+  /**
+   * Creates a generic shortcut set.
+   */
+  function generateShortcutSet($label = '', $id = NULL) {
+    $set = entity_create('shortcut_set', array(
+      'id' => isset($id) ? $id : strtolower($this->randomName()),
+      'label' => empty($label) ? $this->randomString() : $label,
+    ));
+    $set->save();
+    return $set;
+  }
+
+  /**
+   * Extracts information from shortcut set links.
+   *
+   * @param \Drupal\shortcut\ShortcutSetInterface $set
+   *   The shortcut set object to extract information from.
+   * @param string $key
+   *   The array key indicating what information to extract from each link:
+   *    - 'title': Extract shortcut titles.
+   *    - 'path': Extract shortcut paths.
+   *    - 'id': Extract the shortcut ID.
+   *
+   * @return array
+   *   Array of the requested information from each link.
+   */
+  function getShortcutInformation(ShortcutSetInterface $set, $key) {
+    $info = array();
+    \Drupal::entityManager()->getStorage('shortcut')->resetCache();
+    foreach ($set->getShortcuts() as $shortcut) {
+      $info[] = $shortcut->{$key}->value;
+    }
+    return $info;
+  }
+
+}
diff --git a/core/modules/shortcut/tests/Drupal/shortcut/Tests/Menu/ShortcutLocalTasksTest.php b/core/modules/shortcut/tests/Drupal/shortcut/Tests/Menu/ShortcutLocalTasksTest.php
deleted file mode 100644
index c4b4564..0000000
--- a/core/modules/shortcut/tests/Drupal/shortcut/Tests/Menu/ShortcutLocalTasksTest.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\shortcut\Tests\Menu\ShortcutLocalTasksTest.
- */
-
-namespace Drupal\shortcut\Tests\Menu;
-
-use Drupal\Tests\Core\Menu\LocalTaskIntegrationTest;
-
-/**
- * Tests existence of shortcut local tasks.
- *
- * @group Drupal
- * @group Shortcut
- */
-class ShortcutLocalTasksTest extends LocalTaskIntegrationTest {
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Shortcut local tasks test',
-      'description' => 'Test shortcut local tasks.',
-      'group' => 'Shortcut',
-    );
-  }
-
-  public function setUp() {
-    $this->directoryList = array(
-      'shortcut' => 'core/modules/shortcut',
-      'user' => 'core/modules/user',
-    );
-    parent::setUp();
-  }
-
-  /**
-   * Checks shortcut listing local tasks.
-   *
-   * @dataProvider getShortcutPageRoutes
-   */
-  public function testShortcutPageLocalTasks($route) {
-    $tasks = array(
-      0 => array('shortcut.overview', 'user.view', 'user.edit',),
-    );
-    $this->assertLocalTasks($route, $tasks);
-  }
-
-  /**
-   * Provides a list of routes to test.
-   */
-  public function getShortcutPageRoutes() {
-    return array(
-      array('user.view'),
-      array('user.edit'),
-      array('shortcut.overview'),
-    );
-  }
-
-}
diff --git a/core/modules/shortcut/tests/src/Menu/ShortcutLocalTasksTest.php b/core/modules/shortcut/tests/src/Menu/ShortcutLocalTasksTest.php
new file mode 100644
index 0000000..c4b4564
--- /dev/null
+++ b/core/modules/shortcut/tests/src/Menu/ShortcutLocalTasksTest.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\Tests\Menu\ShortcutLocalTasksTest.
+ */
+
+namespace Drupal\shortcut\Tests\Menu;
+
+use Drupal\Tests\Core\Menu\LocalTaskIntegrationTest;
+
+/**
+ * Tests existence of shortcut local tasks.
+ *
+ * @group Drupal
+ * @group Shortcut
+ */
+class ShortcutLocalTasksTest extends LocalTaskIntegrationTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Shortcut local tasks test',
+      'description' => 'Test shortcut local tasks.',
+      'group' => 'Shortcut',
+    );
+  }
+
+  public function setUp() {
+    $this->directoryList = array(
+      'shortcut' => 'core/modules/shortcut',
+      'user' => 'core/modules/user',
+    );
+    parent::setUp();
+  }
+
+  /**
+   * Checks shortcut listing local tasks.
+   *
+   * @dataProvider getShortcutPageRoutes
+   */
+  public function testShortcutPageLocalTasks($route) {
+    $tasks = array(
+      0 => array('shortcut.overview', 'user.view', 'user.edit',),
+    );
+    $this->assertLocalTasks($route, $tasks);
+  }
+
+  /**
+   * Provides a list of routes to test.
+   */
+  public function getShortcutPageRoutes() {
+    return array(
+      array('user.view'),
+      array('user.edit'),
+      array('shortcut.overview'),
+    );
+  }
+
+}
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
index a9245ae..1579e4e 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -1048,6 +1048,9 @@ private function prepareEnvironment() {
 
     $this->generatedTestFiles = FALSE;
 
+    // Ensure the configImporter is refreshed for each test.
+    $this->configImporter = NULL;
+
     // Unregister all custom stream wrappers of the parent site.
     // Availability of Drupal stream wrappers varies by test base class:
     // - UnitTestBase operates in a completely empty environment.
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index adc5bda..dbf1dff 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -466,21 +466,25 @@ function simpletest_test_get_all($module = NULL) {
       }
       $classes = array();
       foreach ($all_data as $name => $data) {
-        // Build directory in which the test files would reside.
-        $tests_dir = DRUPAL_ROOT . '/' . $data->getPath() . '/lib/Drupal/' . $name . '/Tests';
+        $extension_dir = DRUPAL_ROOT . '/' . $data->getPath();
+
+        // Build directories in which the test files would reside.
+        $tests_dirs = array(
+          $extension_dir . '/lib/Drupal/' . $name . '/Tests',
+          $extension_dir . '/src/Tests',
+        );
+
+        $namespace = 'Drupal\\' . $name . '\Tests\\';
         // Scan it for test files if it exists.
-        if (is_dir($tests_dir)) {
-          $files = file_scan_directory($tests_dir, '/\.php$/');
-          if (!empty($files)) {
-            $basedir = DRUPAL_ROOT . '/' . $data->getPath() . '/lib/';
-            foreach ($files as $file) {
-              // Convert the file name into the namespaced class name.
-              $replacements = array(
-                '/' => '\\',
-                $basedir => '',
-                '.php' => '',
-              );
-              $classes[] = strtr($file->uri, $replacements);
+        foreach ($tests_dirs as $tests_dir) {
+          if (is_dir($tests_dir)) {
+            $files = file_scan_directory($tests_dir, '/\.php$/');
+            if (!empty($files)) {
+              $strlen = strlen($tests_dir) + 1;
+              // Convert the file names into the namespaced class names.
+              foreach ($files as $file) {
+                $classes[] = $namespace . str_replace('/', '\\', substr($file->uri, $strlen, -4));
+              }
             }
           }
         }
@@ -569,7 +573,10 @@ function simpletest_classloader_register() {
   foreach ($types as $type) {
     foreach ($extensions[$type] as $name => $uri) {
       drupal_classloader_register($name, dirname($uri));
-      $classloader->add('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($uri) . '/tests');
+      $classloader->addPsr4('Drupal\\' . $name . '\\Tests\\', array(
+        DRUPAL_ROOT . '/' . dirname($uri) . '/tests/Drupal/' . $name . '/Tests',
+        DRUPAL_ROOT . '/' . dirname($uri) . '/tests/src',
+      ));
       // While being there, prime drupal_get_filename().
       drupal_get_filename($type, $name, $uri);
     }
diff --git a/core/modules/simpletest/tests/Drupal/simpletest/Tests/PhpUnitErrorTest.php b/core/modules/simpletest/tests/Drupal/simpletest/Tests/PhpUnitErrorTest.php
index 49fcb02..afe2b52 100644
--- a/core/modules/simpletest/tests/Drupal/simpletest/Tests/PhpUnitErrorTest.php
+++ b/core/modules/simpletest/tests/Drupal/simpletest/Tests/PhpUnitErrorTest.php
@@ -22,7 +22,14 @@ public static function getInfo() {
    * Test errors reported.
    */
   public function testPhpUnitXmlParsing() {
-    require_once __DIR__ . '/../../../../simpletest.module';
+    // This test class could be either in tests/Drupal/simpletest/Tests/, or in
+    // tests/src/, after the PSR-4 transition.
+    if (file_exists(__DIR__ . '/../../simpletest.module')) {
+      require_once __DIR__ . '/../../simpletest.module';
+    }
+    else {
+      require_once __DIR__ . '/../../../../simpletest.module';
+    }
     $phpunit_error_xml = __DIR__ . '/phpunit_error.xml';
     $res = simpletest_phpunit_xml_to_rows(1, $phpunit_error_xml);
     $this->assertEquals(count($res), 4, 'All testcases got extracted');
diff --git a/core/modules/system/lib/Drupal/system/Controller/SystemController.php b/core/modules/system/lib/Drupal/system/Controller/SystemController.php
index eda620d..923fe6a 100644
--- a/core/modules/system/lib/Drupal/system/Controller/SystemController.php
+++ b/core/modules/system/lib/Drupal/system/Controller/SystemController.php
@@ -183,16 +183,19 @@ public function systemAdminMenuBlockPage() {
    *
    * @return string
    *   An HTML string of the theme listing page.
+   *
+   * @todo Move into ThemeController.
    */
   public function themesPage() {
     $config = $this->config('system.theme');
-    // Get current list of themes.
-    $themes = $this->themeHandler->listInfo();
+    // Get all available themes.
+    $themes = $this->themeHandler->rebuildThemeData();
     uasort($themes, 'system_sort_modules_by_info_name');
 
     $theme_default = $config->get('default');
-    $theme_groups  = array();
+    $theme_groups  = array('enabled' => array(), 'disabled' => array());
     $admin_theme = $config->get('admin');
+    $admin_theme_options = array();
 
     foreach ($themes as &$theme) {
       if (!empty($theme->info['hidden'])) {
diff --git a/core/modules/system/lib/Drupal/system/Controller/ThemeController.php b/core/modules/system/lib/Drupal/system/Controller/ThemeController.php
index 86f6a4d..51d91f9 100644
--- a/core/modules/system/lib/Drupal/system/Controller/ThemeController.php
+++ b/core/modules/system/lib/Drupal/system/Controller/ThemeController.php
@@ -115,12 +115,8 @@ public function enable(Request $request) {
     $theme = $request->get('theme');
 
     if (isset($theme)) {
-      // Get current list of themes.
-      $themes = $this->themeHandler->listInfo();
-
-      // Check if the specified theme is one recognized by the system.
-      if (!empty($themes[$theme])) {
-        $this->themeHandler->enable(array($theme));
+      if ($this->themeHandler->enable(array($theme))) {
+        $themes = $this->themeHandler->listInfo();
         drupal_set_message($this->t('The %theme theme has been enabled.', array('%theme' => $themes[$theme]->info['name'])));
       }
       else {
@@ -154,11 +150,9 @@ public function setDefaultTheme(Request $request) {
       $themes = $this->themeHandler->listInfo();
 
       // Check if the specified theme is one recognized by the system.
-      if (!empty($themes[$theme])) {
-        // Enable the theme if it is currently disabled.
-        if (empty($themes[$theme]->status)) {
-          $this->themeHandler->enable(array($theme));
-        }
+      // Or try to enable the theme.
+      if (isset($themes[$theme]) || $this->themeHandler->enable(array($theme))) {
+        $themes = $this->themeHandler->listInfo();
 
         // Set the default theme.
         $config->set('default', $theme)->save();
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php
index 5794603..7105a30 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php
@@ -91,8 +91,8 @@ public function access(AccountInterface $account) {
   protected function getActiveHelp(Request $request) {
     $output = '';
     $router_path = $request->attributes->get('_system_path');
-    // We will always have a path unless we are on a 403 or 404.
-    if (!$router_path) {
+    // Do not show on a 403 or 404 page.
+    if ($request->attributes->has('exception')) {
       return '';
     }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php b/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php
index f268e02..9c15510 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php
@@ -35,11 +35,12 @@ public static function getInfo() {
   function testBatchProgressPageTheme() {
     // Make sure that the page which starts the batch (an administrative page)
     // is using a different theme than would normally be used by the batch API.
-    \Drupal::config('system.theme')
+    $this->container->get('theme_handler')->enable(array('seven', 'bartik'));
+    $this->container->get('config.factory')->get('system.theme')
       ->set('default', 'bartik')
+      ->set('admin', 'seven')
       ->save();
-    theme_enable(array('seven'));
-    \Drupal::config('system.theme')->set('admin', 'seven')->save();
+
     // Log in as an administrator who can see the administrative theme.
     $admin_user = $this->drupalCreateUser(array('view the administration theme'));
     $this->drupalLogin($admin_user);
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/RenderElementTypesTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/RenderElementTypesTest.php
index ef76a07..15a1904 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/RenderElementTypesTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/RenderElementTypesTest.php
@@ -33,7 +33,6 @@ public static function getInfo() {
   protected function setUp() {
     parent::setUp();
     $this->installConfig(array('system'));
-    $this->container->get('theme_handler')->enable(array('stark'));
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTypedDataDefinitionTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTypedDataDefinitionTest.php
index 3704dd9..30eb212 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTypedDataDefinitionTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTypedDataDefinitionTest.php
@@ -114,6 +114,10 @@ public function testEntities() {
     // entity data types variants.
     $this->assertEqual($this->typedDataManager->createDataDefinition('entity'), EntityDataDefinition::create());
     $this->assertEqual($this->typedDataManager->createDataDefinition('entity:node'), EntityDataDefinition::create('node'));
+
+    // Config entities don't support typed data.
+    $entity_definition = EntityDataDefinition::create('node_type');
+    $this->assertEqual(array(), $entity_definition->getPropertyDefinitions());
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Extension/ThemeHandlerTest.php b/core/modules/system/lib/Drupal/system/Tests/Extension/ThemeHandlerTest.php
new file mode 100644
index 0000000..0f5ecdc
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Extension/ThemeHandlerTest.php
@@ -0,0 +1,426 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Extension\ThemeHandlerTest.
+ */
+
+namespace Drupal\system\Tests\Extension;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Extension\ExtensionNameLengthException;
+use Drupal\simpletest\DrupalUnitTestBase;
+
+/**
+ * Tests installing/enabling, disabling, and uninstalling of themes.
+ */
+class ThemeHandlerTest extends DrupalUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Theme handler',
+      'description' => 'Tests installing/enabling, disabling, and uninstalling of themes.',
+      'group' => 'Extension',
+    );
+  }
+
+  public function containerBuild(ContainerBuilder $container) {
+    parent::containerBuild($container);
+    // Some test methods involve ModuleHandler operations, which attempt to
+    // rebuild and dump routes.
+    $container
+      ->register('router.dumper', 'Drupal\Core\Routing\NullMatcherDumper');
+  }
+
+  function setUp() {
+    parent::setUp();
+    $this->installConfig(array('system'));
+  }
+
+  /**
+   * Verifies that no themes are installed/enabled/disabled by default.
+   */
+  function testEmpty() {
+    $this->assertFalse($this->extensionConfig()->get('theme'));
+    $this->assertFalse($this->extensionConfig()->get('disabled.theme'));
+
+    $this->assertFalse(array_keys($this->themeHandler()->listInfo()));
+    $this->assertFalse(array_keys(system_list('theme')));
+
+    // Rebuilding available themes should always yield results though.
+    $this->assertTrue($this->themeHandler()->rebuildThemeData()['stark'], 'ThemeHandler::rebuildThemeData() yields all available themes.');
+
+    // theme_get_setting() should return global default theme settings.
+    $this->assertIdentical(theme_get_setting('features.favicon'), TRUE);
+  }
+
+  /**
+   * Tests enabling a theme.
+   */
+  function testEnable() {
+    $name = 'test_basetheme';
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(isset($themes[$name]));
+
+    $this->themeHandler()->enable(array($name));
+
+    $this->assertIdentical($this->extensionConfig()->get("theme.$name"), 0);
+    $this->assertNull($this->extensionConfig()->get("disabled.theme.$name"));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->assertEqual($themes[$name]->getName(), $name);
+
+    $this->assertEqual(array_keys(system_list('theme')), array_keys($themes));
+
+    // Verify that test_basetheme.settings is active.
+    $this->assertIdentical(theme_get_setting('features.favicon', $name), FALSE);
+    $this->assertEqual(theme_get_setting('base', $name), 'only');
+    $this->assertEqual(theme_get_setting('override', $name), 'base');
+  }
+
+  /**
+   * Tests enabling a sub-theme.
+   */
+  function testEnableSubTheme() {
+    $name = 'test_subtheme';
+    $base_name = 'test_basetheme';
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(array_keys($themes));
+
+    $this->themeHandler()->enable(array($name));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->assertTrue(isset($themes[$base_name]));
+
+    $this->themeHandler()->disable(array($name));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(isset($themes[$name]));
+    $this->assertTrue(isset($themes[$base_name]));
+  }
+
+  /**
+   * Tests enabling a non-existing theme.
+   */
+  function testEnableNonExisting() {
+    $name = 'non_existing_theme';
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(array_keys($themes));
+
+    try {
+      $message = 'ThemeHandler::enable() throws InvalidArgumentException upon enabling a non-existing theme.';
+      $this->themeHandler()->enable(array($name));
+      $this->fail($message);
+    }
+    catch (\InvalidArgumentException $e) {
+      $this->pass(get_class($e) . ': ' . $e->getMessage());
+    }
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(array_keys($themes));
+  }
+
+  /**
+   * Tests enabling a theme with a too long name.
+   */
+  function testEnableNameTooLong() {
+    $name = 'test_theme_having_veery_long_name_which_is_too_long';
+
+    try {
+      $message = 'ThemeHandler::enable() throws ExtensionNameLengthException upon enabling a theme with a too long name.';
+      $this->themeHandler()->enable(array($name));
+      $this->fail($message);
+    }
+    catch (ExtensionNameLengthException $e) {
+      $this->pass(get_class($e) . ': ' . $e->getMessage());
+    }
+  }
+
+  /**
+   * Tests disabling a theme.
+   */
+  function testDisable() {
+    $name = 'test_basetheme';
+    $this->themeHandler()->enable(array($name));
+
+    // Prime the relevant drupal_static()s.
+    $this->assertEqual(array_keys(system_list('theme')), array($name));
+    $this->assertIdentical(theme_get_setting('features.favicon', $name), FALSE);
+
+    $this->themeHandler()->disable(array($name));
+
+    $this->assertIdentical($this->extensionConfig()->get('theme'), array());
+    $this->assertIdentical($this->extensionConfig()->get("disabled.theme.$name"), 0);
+
+    $this->assertFalse(array_keys($this->themeHandler()->listInfo()));
+    $this->assertFalse(array_keys(system_list('theme')));
+
+    // Verify that test_basetheme.settings no longer applies, even though the
+    // configuration still exists.
+    $this->assertIdentical(theme_get_setting('features.favicon', $name), TRUE);
+    $this->assertNull(theme_get_setting('base', $name));
+    $this->assertNull(theme_get_setting('override', $name));
+
+    // The theme is not uninstalled, so its configuration must still exist.
+    $this->assertTrue($this->config("$name.settings")->get());
+  }
+
+  /**
+   * Tests disabling and enabling a theme.
+   *
+   * Verifies that
+   * - themes can be re-enabled
+   * - default configuration is not re-imported upon re-enabling an already
+   *   installed theme.
+   */
+  function testDisableEnable() {
+    $name = 'test_basetheme';
+
+    $this->themeHandler()->enable(array($name));
+    $this->themeHandler()->disable(array($name));
+
+    $this->assertIdentical($this->config("$name.settings")->get('base'), 'only');
+    $this->assertIdentical($this->config('system.date_format.fancy')->get('label'), 'Fancy date');
+
+    // Default configuration never overwrites custom configuration, so just
+    // changing values in existing configuration will cause ConfigInstaller to
+    // simply skip those files. To ensure that no default configuration is
+    // re-imported, the custom configuration has to be deleted.
+    $this->configStorage()->delete("$name.settings");
+    $this->configStorage()->delete('system.date_format.fancy');
+    // Reflect direct storage operations in ConfigFactory.
+    $this->container->get('config.factory')->reset();
+
+    $this->themeHandler()->enable(array($name));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->assertEqual($themes[$name]->getName(), $name);
+
+    $this->assertEqual(array_keys(system_list('theme')), array_keys($themes));
+
+    $this->assertFalse($this->config("$name.settings")->get());
+    $this->assertNull($this->config('system.date_format.fancy')->get('label'));
+  }
+
+  /**
+   * Tests disabling the default theme.
+   */
+  function testDisableDefault() {
+    $name = 'stark';
+    $other_name = 'bartik';
+    $this->themeHandler()->enable(array($name, $other_name));
+    $this->themeHandler()->setDefault($name);
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->assertTrue(isset($themes[$other_name]));
+
+    try {
+      $message = 'ThemeHandler::disable() throws InvalidArgumentException upon disabling default theme.';
+      $this->themeHandler()->disable(array($name));
+      $this->fail($message);
+    }
+    catch (\InvalidArgumentException $e) {
+      $this->pass(get_class($e) . ': ' . $e->getMessage());
+    }
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->assertTrue(isset($themes[$other_name]));
+  }
+
+  /**
+   * Tests disabling the admin theme.
+   */
+  function testDisableAdmin() {
+    $name = 'stark';
+    $other_name = 'bartik';
+    $this->themeHandler()->enable(array($name, $other_name));
+    $this->config('system.theme')->set('admin', $name)->save();
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->assertTrue(isset($themes[$other_name]));
+
+    try {
+      $message = 'ThemeHandler::disable() throws InvalidArgumentException upon disabling admin theme.';
+      $this->themeHandler()->disable(array($name));
+      $this->fail($message);
+    }
+    catch (\InvalidArgumentException $e) {
+      $this->pass(get_class($e) . ': ' . $e->getMessage());
+    }
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->assertTrue(isset($themes[$other_name]));
+  }
+
+  /**
+   * Tests disabling a sub-theme.
+   */
+  function testDisableSubTheme() {
+    $name = 'test_subtheme';
+    $base_name = 'test_basetheme';
+
+    $this->themeHandler()->enable(array($name));
+    $this->themeHandler()->disable(array($name));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(isset($themes[$name]));
+    $this->assertTrue(isset($themes[$base_name]));
+  }
+
+  /**
+   * Tests disabling a base theme before its sub-theme.
+   */
+  function testDisableBaseBeforeSubTheme() {
+    $name = 'test_basetheme';
+    $sub_name = 'test_subtheme';
+
+    $this->themeHandler()->enable(array($sub_name));
+
+    try {
+      $message = 'ThemeHandler::disable() throws InvalidArgumentException upon disabling base theme before sub theme.';
+      $this->themeHandler()->disable(array($name));
+      $this->fail($message);
+    }
+    catch (\InvalidArgumentException $e) {
+      $this->pass(get_class($e) . ': ' . $e->getMessage());
+    }
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->assertTrue(isset($themes[$sub_name]));
+
+    // Verify that disabling both at the same time works.
+    $this->themeHandler()->disable(array($name, $sub_name));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(isset($themes[$name]));
+    $this->assertFalse(isset($themes[$sub_name]));
+  }
+
+  /**
+   * Tests disabling a non-existing theme.
+   */
+  function testDisableNonExisting() {
+    $name = 'non_existing_theme';
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(array_keys($themes));
+
+    try {
+      $message = 'ThemeHandler::disable() throws InvalidArgumentException upon disabling a non-existing theme.';
+      $this->themeHandler()->disable(array($name));
+      $this->fail($message);
+    }
+    catch (\InvalidArgumentException $e) {
+      $this->pass(get_class($e) . ': ' . $e->getMessage());
+    }
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(array_keys($themes));
+  }
+
+  /**
+   * Tests that theme info can be altered by a module.
+   *
+   * @see module_test_system_info_alter()
+   */
+  function testThemeInfoAlter() {
+    $name = 'seven';
+    $this->container->get('state')->set('module_test.hook_system_info_alter', TRUE);
+    $module_handler = $this->container->get('module_handler');
+
+    $this->themeHandler()->enable(array($name));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(isset($themes[$name]->info['regions']['test_region']));
+
+    $module_handler->install(array('module_test'), FALSE);
+    $this->assertTrue($module_handler->moduleExists('module_test'));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]->info['regions']['test_region']));
+
+    // Legacy assertions.
+    // @todo Remove once theme initialization/info has been modernized.
+    // @see https://drupal.org/node/2228093
+    $info = system_get_info('theme', $name);
+    $this->assertTrue(isset($info['regions']['test_region']));
+    $regions = system_region_list($name);
+    $this->assertTrue(isset($regions['test_region']));
+    $system_list = system_list('theme');
+    $this->assertTrue(isset($system_list[$name]->info['regions']['test_region']));
+
+    $module_handler->uninstall(array('module_test'));
+    $this->assertFalse($module_handler->moduleExists('module_test'));
+
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(isset($themes[$name]->info['regions']['test_region']));
+
+    // Legacy assertions.
+    // @todo Remove once theme initialization/info has been modernized.
+    // @see https://drupal.org/node/2228093
+    $info = system_get_info('theme', $name);
+    $this->assertFalse(isset($info['regions']['test_region']));
+    $regions = system_region_list($name);
+    $this->assertFalse(isset($regions['test_region']));
+    $system_list = system_list('theme');
+    $this->assertFalse(isset($system_list[$name]->info['regions']['test_region']));
+  }
+
+  /**
+   * Returns the theme handler service.
+   *
+   * @return \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected function themeHandler() {
+    return $this->container->get('theme_handler');
+  }
+
+  /**
+   * Returns the system.theme config object.
+   *
+   * @return \Drupal\Core\Config\Config
+   */
+  protected function extensionConfig() {
+    return $this->config('core.extension');
+  }
+
+  /**
+   * Returns a given config object.
+   *
+   * @param string $name
+   *   The name of the config object to load.
+   *
+   * @return \Drupal\Core\Config\Config
+   */
+  protected function config($name) {
+    return $this->container->get('config.factory')->get($name);
+  }
+
+  /**
+   * Returns the active configuration storage.
+   *
+   * @return \Drupal\Core\Config\ConfigStorageInterface
+   */
+  protected function configStorage() {
+    return $this->container->get('config.storage');
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php
index bda7efe..849aadc 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php
@@ -138,7 +138,7 @@ function testBreadCrumbs() {
     // @todo Remove this part once we have a _title_callback, see
     //   https://drupal.org/node/2076085.
     $trail += array(
-      "admin/config/content/formats/manage/$format_id" => Unicode::ucfirst(Unicode::strtolower($format->name)),
+      "admin/config/content/formats/manage/$format_id" => $format->label(),
     );
     $this->assertBreadcrumb("admin/config/content/formats/manage/$format_id/disable", $trail);
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
index 03f8ca1..3779f25 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
@@ -35,13 +35,6 @@ class MenuRouterTest extends WebTestBase {
    */
   protected $default_theme;
 
-  /**
-   * Name of an alternate theme to use for tests.
-   *
-   * @var string
-   */
-  protected $alternate_theme;
-
   public static function getInfo() {
     return array(
       'name' => 'Menu router',
@@ -359,42 +352,32 @@ public function testAuthUserUserLogin() {
    * Tests theme integration.
    */
   public function testThemeIntegration() {
-    $this->initializeTestThemeConfiguration();
+    $this->default_theme = 'bartik';
+    $this->admin_theme = 'seven';
+
+    $theme_handler = $this->container->get('theme_handler');
+    $theme_handler->enable(array($this->default_theme, $this->admin_theme));
+    $this->container->get('config.factory')->get('system.theme')
+      ->set('default', $this->default_theme)
+      ->set('admin', $this->admin_theme)
+      ->save();
+    $theme_handler->disable(array('stark'));
+
     $this->doTestThemeCallbackMaintenanceMode();
 
-    $this->initializeTestThemeConfiguration();
     $this->doTestThemeCallbackFakeTheme();
 
-    $this->initializeTestThemeConfiguration();
     $this->doTestThemeCallbackAdministrative();
 
-    $this->initializeTestThemeConfiguration();
     $this->doTestThemeCallbackNoThemeRequested();
 
-    $this->initializeTestThemeConfiguration();
     $this->doTestThemeCallbackOptionalTheme();
   }
 
   /**
-   * Explicitly set the default and admin themes.
-   */
-  protected function initializeTestThemeConfiguration() {
-    $this->default_theme = 'bartik';
-    $this->admin_theme = 'seven';
-    $this->alternate_theme = 'stark';
-    theme_enable(array($this->default_theme));
-    \Drupal::config('system.theme')
-      ->set('default', $this->default_theme)
-      ->set('admin', $this->admin_theme)
-      ->save();
-    theme_disable(array($this->alternate_theme));
-  }
-
-  /**
    * Test the theme negotiation when it is set to use an administrative theme.
    */
   protected function doTestThemeCallbackAdministrative() {
-    theme_enable(array($this->admin_theme));
     $this->drupalGet('menu-test/theme-callback/use-admin-theme');
     $this->assertText('Active theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme negotiation.');
     $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
@@ -405,7 +388,6 @@ protected function doTestThemeCallbackAdministrative() {
    */
   protected function doTestThemeCallbackMaintenanceMode() {
     $this->container->get('state')->set('system.maintenance_mode', TRUE);
-    theme_enable(array($this->admin_theme));
 
     // For a regular user, the fact that the site is in maintenance mode means
     // we expect the theme callback system to be bypassed entirely.
@@ -432,10 +414,14 @@ protected function doTestThemeCallbackOptionalTheme() {
     $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page.");
 
     // Now enable the theme and request it again.
-    theme_enable(array($this->alternate_theme));
+    $theme_handler = $this->container->get('theme_handler');
+    $theme_handler->enable(array('stark'));
+
     $this->drupalGet('menu-test/theme-callback/use-stark-theme');
     $this->assertText('Active theme: stark. Actual theme: stark.', 'The theme negotiation system uses an optional theme once it has been enabled.');
     $this->assertRaw('stark/css/layout.css', "The optional theme's CSS appears on the page.");
+
+    $theme_handler->disable(array('stark'));
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/AnnotatedClassDiscoveryTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/AnnotatedClassDiscoveryTest.php
index d2fba9b..a2fa299 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/AnnotatedClassDiscoveryTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/AnnotatedClassDiscoveryTest.php
@@ -57,7 +57,13 @@ public function setUp() {
         'provider' => 'plugin_test',
       ),
     );
-    $namespaces = new \ArrayObject(array('Drupal\plugin_test' => DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib'));
+    $namespaces = new \ArrayObject(array(
+      'Drupal\plugin_test' => array(
+        // @todo Remove lib/Drupal/$module, once the switch to PSR-4 is complete.
+        DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test',
+        DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/src',
+      ),
+    ));
     $this->discovery = new AnnotatedClassDiscovery('Plugin/plugin_test/fruit', $namespaces);
     $this->emptyDiscovery = new AnnotatedClassDiscovery('Plugin/non_existing_module/non_existing_plugin_type', $namespaces);
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomAnnotationClassDiscoveryTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomAnnotationClassDiscoveryTest.php
index d61c013..09358cc 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomAnnotationClassDiscoveryTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomAnnotationClassDiscoveryTest.php
@@ -41,7 +41,13 @@ protected function setUp() {
         'provider' => 'plugin_test',
       ),
     );
-    $root_namespaces = new \ArrayObject(array('Drupal\plugin_test' => DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib'));
+    $root_namespaces = new \ArrayObject(array(
+      'Drupal\plugin_test' => array(
+        // @todo Remove lib/Drupal/$module, once the switch to PSR-4 is complete.
+        DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test',
+        DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/src',
+      ),
+    ));
 
     $this->discovery = new AnnotatedClassDiscovery('Plugin/plugin_test/custom_annotation', $root_namespaces, 'Drupal\plugin_test\Plugin\Annotation\PluginExample');
     $this->emptyDiscovery = new AnnotatedClassDiscovery('Plugin/non_existing_module/non_existing_plugin_type', $root_namespaces, 'Drupal\plugin_test\Plugin\Annotation\PluginExample');
diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomDirectoryAnnotatedClassDiscoveryTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomDirectoryAnnotatedClassDiscoveryTest.php
index 52d4fea..1071e77 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomDirectoryAnnotatedClassDiscoveryTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomDirectoryAnnotatedClassDiscoveryTest.php
@@ -70,7 +70,21 @@ protected function setUp() {
         'provider' => 'plugin_test',
       ),
     );
-    $namespaces = new \ArrayObject(array('Drupal\plugin_test' => DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib'));
+    // Due to the transition from PSR-0 to PSR-4, plugin classes can be in
+    // either one of
+    // - core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/
+    // - core/modules/system/tests/modules/plugin_test/src/
+    // To avoid false positives with "Drupal\plugin_test\Drupal\plugin_test\..",
+    // only one of them can be registered.
+    // Note: This precaution is only needed if the plugin namespace is identical
+    // with the module namespace. Usually this is not the case, because every
+    // plugin namespace is like "Drupal\$module\Plugin\..".
+    // @todo Clean this up, once the transition to PSR-4 is complete.
+    $extension_dir = DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test';
+    $base_directory = is_dir($extension_dir . '/lib/Drupal/plugin_test')
+      ? $extension_dir . '/lib/Drupal/plugin_test'
+      : $extension_dir . '/src';
+    $namespaces = new \ArrayObject(array('Drupal\plugin_test' => $base_directory));
     $this->discovery = new AnnotatedClassDiscovery('', $namespaces);
     $empty_namespaces = new \ArrayObject();
     $this->emptyDiscovery = new AnnotatedClassDiscovery('', $empty_namespaces);
diff --git a/core/modules/system/lib/Drupal/system/Tests/System/InfoAlterTest.php b/core/modules/system/lib/Drupal/system/Tests/System/InfoAlterTest.php
index 5175b7f..31387d0 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/InfoAlterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/InfoAlterTest.php
@@ -7,12 +7,15 @@
 
 namespace Drupal\system\Tests\System;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\simpletest\DrupalUnitTestBase;
 
 /**
  * Tests the effectiveness of hook_system_info_alter().
  */
-class InfoAlterTest extends WebTestBase {
+class InfoAlterTest extends DrupalUnitTestBase {
+
+  public static $modules = array('system');
+
   public static function getInfo() {
     return array(
       'name' => 'System info alter',
@@ -32,24 +35,12 @@ function testSystemInfoAlter() {
     \Drupal::state()->set('module_test.hook_system_info_alter', TRUE);
     $info = system_rebuild_module_data();
     $this->assertFalse(isset($info['node']->info['required']), 'Before the module_test is installed the node module is not required.');
-    // Enable seven and the test module.
-    theme_enable(array('seven'));
+
+    // Enable the test module.
     \Drupal::moduleHandler()->install(array('module_test'), FALSE);
     $this->assertTrue(\Drupal::moduleHandler()->moduleExists('module_test'), 'Test module is enabled.');
 
-    // Verify that the rebuilt and altered theme info is returned.
-    $info = system_get_info('theme', 'seven');
-    $this->assertTrue(isset($info['regions']['test_region']), 'Altered theme info was returned by system_get_info().');
-    $seven_regions = system_region_list('seven');
-    $this->assertTrue(isset($seven_regions['test_region']), 'Altered theme info was returned by system_region_list().');
-    $system_list_themes = system_list('theme');
-    $info = $system_list_themes['seven']->info;
-    $this->assertTrue(isset($info['regions']['test_region']), 'Altered theme info was returned by system_list().');
-    $list_themes = list_themes();
-    $this->assertTrue(isset($list_themes['seven']->info['regions']['test_region']), 'Altered theme info was returned by list_themes().');
-    system_list_reset();
     $info = system_rebuild_module_data();
     $this->assertTrue($info['node']->info['required'], 'After the module_test is installed the node module is required.');
-    \Drupal::state()->set('module_test.hook_system_info_alter', FALSE);
   }
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ScriptTest.php
index 3069aab..b6314b9 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ScriptTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ScriptTest.php
@@ -30,10 +30,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $path_parts = explode(DIRECTORY_SEPARATOR, __DIR__);
-    // This file is 8 levels below the Drupal root.
-    $root = implode(DIRECTORY_SEPARATOR, array_slice($path_parts, 0, -8));
-    chdir($root);
+    chdir(DRUPAL_ROOT);
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
index 520cb11..c7911b5 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
@@ -178,7 +178,7 @@ function testThemeSettings() {
    * Test the administration theme functionality.
    */
   function testAdministrationTheme() {
-    theme_enable(array('bartik', 'seven'));
+    $this->container->get('theme_handler')->enable(array('seven'));
 
     // Enable an administration theme and show it on the node admin pages.
     $edit = array(
@@ -212,9 +212,6 @@ function testAdministrationTheme() {
     $this->assertRaw('core/themes/stark', 'Site default theme used on the add content page.');
 
     // Reset to the default theme settings.
-    \Drupal::config('system.theme')
-      ->set('default', 'bartik')
-      ->save();
     $edit = array(
       'admin_theme' => '0',
       'use_admin_theme' => FALSE,
@@ -222,10 +219,10 @@ function testAdministrationTheme() {
     $this->drupalPostForm('admin/appearance', $edit, t('Save configuration'));
 
     $this->drupalGet('admin');
-    $this->assertRaw('core/themes/bartik', 'Site default theme used on administration page.');
+    $this->assertRaw('core/themes/stark', 'Site default theme used on administration page.');
 
     $this->drupalGet('node/add');
-    $this->assertRaw('core/themes/bartik', 'Site default theme used on the add content page.');
+    $this->assertRaw('core/themes/stark', 'Site default theme used on the add content page.');
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
index 76edbcd..228e296 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
@@ -202,11 +202,13 @@ function testFunctionOverride() {
    * Test the list_themes() function.
    */
   function testListThemes() {
-    $themes = list_themes();
+    $theme_handler = $this->container->get('theme_handler');
+    $theme_handler->enable(array('test_subtheme'));
+    $themes = $theme_handler->listInfo();
+
     // Check if drupal_theme_access() retrieves enabled themes properly from list_themes().
     $this->assertTrue(drupal_theme_access('test_theme'), 'Enabled theme detected');
-    // Check if list_themes() returns disabled themes.
-    $this->assertTrue(array_key_exists('test_basetheme', $themes), 'Disabled theme detected');
+
     // Check for base theme and subtheme lists.
     $base_theme_list = array('test_basetheme' => 'Theme test base theme');
     $sub_theme_list = array('test_subtheme' => 'Theme test subtheme');
@@ -223,6 +225,7 @@ function testListThemes() {
    * Test the theme_get_setting() function.
    */
   function testThemeGetSetting() {
+    $this->container->get('theme_handler')->enable(array('test_subtheme'));
     $GLOBALS['theme_key'] = 'test_theme';
     $this->assertIdentical(theme_get_setting('theme_test_setting'), 'default value', 'theme_get_setting() uses the default theme automatically.');
     $this->assertNotEqual(theme_get_setting('subtheme_override', 'test_basetheme'), theme_get_setting('subtheme_override', 'test_subtheme'), 'Base theme\'s default settings values can be overridden by subtheme.');
@@ -280,28 +283,22 @@ function testPreprocessHtml() {
   }
 
   /**
-   * Test that themes can be disabled programmatically but admin theme and default theme can not.
+   * Test that classes can be added into region.tpl.php via #attributes.
    */
-  function testDisableTheme() {
-    // Enable Bartik, Seven and Stark.
-    \Drupal::service('theme_handler')->enable(array('bartik', 'seven', 'stark'));
-
-    // Set Bartik as the default theme and Seven as the admin theme.
-    \Drupal::config('system.theme')
-      ->set('default', 'bartik')
-      ->set('admin', 'seven')
-      ->save();
-
-    $theme_list = array_keys(\Drupal::service('theme_handler')->listInfo());
-    // Attempt to disable all themes. theme_disable() ensures that the default
-    // theme and the admin theme will not be disabled.
-    \Drupal::service('theme_handler')->disable($theme_list);
-
-    $theme_list = \Drupal::service('theme_handler')->listInfo();
-
-    // Ensure Bartik and Seven are still enabled and Stark is disabled.
-    $this->assertTrue($theme_list['bartik']->status == 1, 'Default theme is enabled.');
-    $this->assertTrue($theme_list['seven']->status == 1, 'Admin theme is enabled.');
-    $this->assertTrue($theme_list['stark']->status == 0, 'Stark is disabled.');
+  function testRegionClass() {
+    global $theme_key;
+    \Drupal::moduleHandler()->install(array('system', 'block', 'system_region_test'));
+    $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
+    $this->node = $this->drupalCreateNode(array('title' => t('Hello, world!'), 'type' => 'article'), NODE_PROMOTED);
+
+    // Explicitly set the default and admin themes.
+    $theme = 'bartik';
+    theme_enable(array($theme));
+    \Drupal::config('system.theme')->set('default', $theme)->save();
+    // Place a block.
+    $block = $this->drupalPlaceBlock('system_main_block');
+    \Drupal::service('router.builder')->rebuild();
+    $this->drupalGet('node/1');
+    $this->assertRaw('new_class', 'New class found.');
   }
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php
index e33f79b..6dff9ac 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php
@@ -77,23 +77,21 @@ function testTwigDebugOverride() {
    */
   function testTwigCacheOverride() {
     $extension = twig_extension();
-    theme_enable(array('test_theme'));
-    \Drupal::config('system.theme')
-      ->set('default', 'test_theme')
-      ->save();
-
-    // Unset the global variables, so \Drupal\Core\Theme\Registry::init() fires
-    // drupal_theme_initialize, which fills up the global variables  properly
-    // and chosen the current active theme.
-    unset($GLOBALS['theme_info']);
-    unset($GLOBALS['theme']);
+    $theme_handler = $this->container->get('theme_handler');
+    $theme_handler->enable(array('test_theme'));
+    $theme_handler->setDefault('test_theme');
+
+    // The registry still works on theme globals, so set them here.
+    $GLOBALS['theme'] = 'test_theme';
+    $GLOBALS['theme_info'] = $theme_handler->listInfo()['test_theme'];
+
     // Reset the theme registry, so that the new theme is used.
     $this->container->set('theme.registry', NULL);
 
     // Load array of Twig templates.
+    // reset() is necessary to invalidate caches tagged with 'theme_registry'.
     $registry = $this->container->get('theme.registry');
     $registry->reset();
-
     $templates = $registry->getRuntime();
 
     // Get the template filename and the cache filename for
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 146fe2f..e420027 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -2899,8 +2899,11 @@ function hook_link_alter(&$variables) {
  * @see callback_batch_operation()
  * @see \Drupal\Core\Config\ConfigImporter::initialize()
  */
-function hook_config_import_steps_alter(&$sync_steps) {
-  $sync_steps[] = '_additional_configuration_step';
+function hook_config_import_steps_alter(&$sync_steps, \Drupal\Core\Config\ConfigImporter $config_importer) {
+  $deletes = $config_importer->getUnprocessedConfiguration('delete');
+  if (isset($deletes['field.field.node.body'])) {
+    $sync_steps[] = '_additional_configuration_step';
+  }
 }
 
 /**
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index ca5d128..421491a 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -567,13 +567,6 @@ function system_requirements($phase) {
  * Implements hook_install().
  */
 function system_install() {
-  // Enable and set the default theme. Can't use theme_enable() this early in
-  // installation.
-  \Drupal::service('config.installer')->installDefaultConfig('theme', 'stark');
-  \Drupal::config('system.theme')
-    ->set('default', 'stark')
-    ->save();
-
   // Populate the cron key state variable.
   $cron_key = Crypt::randomBytesBase64(55);
   \Drupal::state()->set('system.cron_key', $cron_key);
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index d719b5a..50cd796 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1290,51 +1290,16 @@ function system_rebuild_module_data() {
 }
 
 /**
- * Helper function to scan and collect theme .info.yml data and their engines.
- *
- * @return \Drupal\Core\Extension\Extension[]
- *   An associative array of themes information.
- *
- * @see \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData()
- *
- * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
- *   Use \Drupal::service('theme_handler')->rebuildThemeData().
- */
-function _system_rebuild_theme_data() {
-  return \Drupal::service('theme_handler')->rebuildThemeData();
-}
-
-/**
  * Rebuild, save, and return data about all currently available themes.
  *
  * @return \Drupal\Core\Extension\Extension[]
  *   Array of all available themes and their data.
+ *
+ * @deprecated 8.x
+ *   Use \Drupal::service('theme_handler')->rebuildThemeData().
  */
 function system_rebuild_theme_data() {
-  $themes = _system_rebuild_theme_data();
-  ksort($themes);
-  // @todo This function has no business in determining/setting the status of
-  //   a theme, but various other functions expect it to return themes with a
-  //   $status property. system_list() stores the return value of this function
-  //   in state, and ensures to set/override the $status property for each theme
-  //   based on the current config. Remove this code when themes have a proper
-  //   installation status.
-  // @see http://drupal.org/node/1067408
-  $enabled_themes = \Drupal::config('core.extension')->get('theme') ?: array();
-  $files = array();
-  foreach ($themes as $name => $theme) {
-    $theme->status = (int) isset($enabled_themes[$name]);
-    $files[$name] = $theme->getPathname();
-  }
-  // Replace last known theme data state.
-  // @todo Obsolete with proper installation status for themes.
-  \Drupal::state()->set('system.theme.data', $themes);
-
-  // Store filenames to allow system_list() and drupal_get_filename() to
-  // retrieve them without having to rebuild or scan the filesystem.
-  \Drupal::state()->set('system.theme.files', $files);
-
-  return $themes;
+  return \Drupal::service('theme_handler')->rebuildThemeData();
 }
 
 /**
@@ -1357,22 +1322,24 @@ function _system_default_theme_features() {
 /**
  * Get a list of available regions from a specified theme.
  *
- * @param $theme_key
- *   The name of a theme.
+ * @param \Drupal\Core\Extension\Extension|string $theme
+ *   A theme extension object, or the name of a theme.
  * @param $show
  *   Possible values: REGIONS_ALL or REGIONS_VISIBLE. Visible excludes hidden
  *   regions.
  * @return
  *   An array of regions in the form $region['name'] = 'description'.
  */
-function system_region_list($theme_key, $show = REGIONS_ALL) {
-  $themes = list_themes();
-  if (!isset($themes[$theme_key])) {
-    return array();
+function system_region_list($theme, $show = REGIONS_ALL) {
+  if (!$theme instanceof Extension) {
+    $themes = \Drupal::service('theme_handler')->listInfo();
+    if (!isset($themes[$theme])) {
+      return array();
+    }
+    $theme = $themes[$theme];
   }
-
   $list = array();
-  $info = $themes[$theme_key]->info;
+  $info = $theme->info;
   // If requested, suppress hidden regions. See block_admin_display_form().
   foreach ($info['regions'] as $name => $label) {
     if ($show == REGIONS_ALL || !isset($info['regions_hidden']) || !in_array($name, $info['regions_hidden'])) {
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 49561c7..b7eabf2 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -341,6 +341,7 @@ system.theme_settings_theme:
   path: '/admin/appearance/settings/{theme}'
   defaults:
     _form: '\Drupal\system\Form\ThemeSettingsForm'
+    _title_callback: 'theme_handler:getName'
   requirements:
     _permission: 'administer themes'
 
diff --git a/core/modules/system/templates/status-report.html.twig b/core/modules/system/templates/status-report.html.twig
index d01f163..7d0d96d 100644
--- a/core/modules/system/templates/status-report.html.twig
+++ b/core/modules/system/templates/status-report.html.twig
@@ -1,5 +1,6 @@
 {#
 /**
+ * @file
  * Default theme implementation for the status report.
  *
  * Available variables:
diff --git a/core/modules/system/tests/modules/system_region_test/system_region_test.info.yml b/core/modules/system/tests/modules/system_region_test/system_region_test.info.yml
new file mode 100644
index 0000000..63e1e45
--- /dev/null
+++ b/core/modules/system/tests/modules/system_region_test/system_region_test.info.yml
@@ -0,0 +1,7 @@
+name: 'System region test'
+type: module
+description: 'Provides hook implementations for testing regions.'
+package: Testing
+version: VERSION
+core: 8.x
+hidden: true
diff --git a/core/modules/system/tests/modules/system_region_test/system_region_test.module b/core/modules/system/tests/modules/system_region_test/system_region_test.module
new file mode 100644
index 0000000..4302fd7
--- /dev/null
+++ b/core/modules/system/tests/modules/system_region_test/system_region_test.module
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Provides System module hook implementations for testing purposes.
+ */
+
+/**
+ * Add a classes can into region.tpl.php via #attributes.
+ */
+function bartik_preprocess_region(&$variables) {
+  $variables['attributes']['class'][] = 'new_class';
+}
diff --git a/core/modules/system/tests/themes/test_basetheme/config/install/system.date_format.fancy.yml b/core/modules/system/tests/themes/test_basetheme/config/install/system.date_format.fancy.yml
new file mode 100644
index 0000000..12ae886
--- /dev/null
+++ b/core/modules/system/tests/themes/test_basetheme/config/install/system.date_format.fancy.yml
@@ -0,0 +1,11 @@
+# Themes are not supposed to provide/install this kind of config normally.
+# This exists for testing purposes only.
+# @see \Drupal\system\Tests\Extension\ThemeHandlerTest
+id: fancy
+label: 'Fancy date'
+status: true
+langcode: en
+locked: false
+pattern:
+  php: 'U'
+  intl: 'EEEE, LLLL d, yyyy - kk:mm'
diff --git a/core/modules/system/tests/themes/test_basetheme/config/install/test_basetheme.settings.yml b/core/modules/system/tests/themes/test_basetheme/config/install/test_basetheme.settings.yml
index 2d5de34..4a06f9a 100644
--- a/core/modules/system/tests/themes/test_basetheme/config/install/test_basetheme.settings.yml
+++ b/core/modules/system/tests/themes/test_basetheme/config/install/test_basetheme.settings.yml
@@ -1,3 +1,4 @@
 features:
   favicon: false
 base: only
+override: base
diff --git a/core/modules/system/tests/themes/test_basetheme/config/schema/test_basetheme.schema.yml b/core/modules/system/tests/themes/test_basetheme/config/schema/test_basetheme.schema.yml
index 8a4bd45..e22c3c5 100644
--- a/core/modules/system/tests/themes/test_basetheme/config/schema/test_basetheme.schema.yml
+++ b/core/modules/system/tests/themes/test_basetheme/config/schema/test_basetheme.schema.yml
@@ -5,3 +5,6 @@ test_basetheme.settings:
     base:
       type: string
       label: 'Base theme setting'
+    override:
+      type: string
+      label: 'Whether the setting has been overridden'
diff --git a/core/modules/system/tests/themes/test_subtheme/config/install/test_subtheme.settings.yml b/core/modules/system/tests/themes/test_subtheme/config/install/test_subtheme.settings.yml
new file mode 100644
index 0000000..0589cab
--- /dev/null
+++ b/core/modules/system/tests/themes/test_subtheme/config/install/test_subtheme.settings.yml
@@ -0,0 +1 @@
+override: sub
diff --git a/core/modules/system/tests/themes/test_subtheme/config/schema/test_subtheme.schema.yml b/core/modules/system/tests/themes/test_subtheme/config/schema/test_subtheme.schema.yml
new file mode 100644
index 0000000..9c7b0ce
--- /dev/null
+++ b/core/modules/system/tests/themes/test_subtheme/config/schema/test_subtheme.schema.yml
@@ -0,0 +1,7 @@
+test_subtheme.settings:
+  type: theme_settings
+  label: 'Test sub theme settings'
+  mapping:
+    override:
+      type: string
+      label: 'Whether the setting has been overridden'
diff --git a/core/modules/system/tests/themes/test_theme_having_veery_long_name_which_is_too_long/test_theme_having_veery_long_name_which_is_too_long.info.yml b/core/modules/system/tests/themes/test_theme_having_veery_long_name_which_is_too_long/test_theme_having_veery_long_name_which_is_too_long.info.yml
new file mode 100644
index 0000000..fa0a207
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_having_veery_long_name_which_is_too_long/test_theme_having_veery_long_name_which_is_too_long.info.yml
@@ -0,0 +1,5 @@
+type: theme
+core: 8.x
+name: 'Test theme with a too long name'
+version: VERSION
+hidden: true
diff --git a/core/modules/taxonomy/taxonomy.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml
index 014f957..1182589 100644
--- a/core/modules/taxonomy/taxonomy.routing.yml
+++ b/core/modules/taxonomy/taxonomy.routing.yml
@@ -62,6 +62,7 @@ taxonomy.vocabulary_reset:
   path: '/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/reset'
   defaults:
     _entity_form: 'taxonomy_vocabulary.reset'
+    _title: 'Reset'
   requirements:
     _permission: 'administer taxonomy'
 
diff --git a/core/modules/tour/lib/Drupal/tour/Tests/TourTestBasic.php b/core/modules/tour/lib/Drupal/tour/Tests/TourTestBasic.php
index e6da900..49dafdc 100644
--- a/core/modules/tour/lib/Drupal/tour/Tests/TourTestBasic.php
+++ b/core/modules/tour/lib/Drupal/tour/Tests/TourTestBasic.php
@@ -49,15 +49,13 @@ protected function setUp() {
 
     // Make sure we are using distinct default and administrative themes for
     // the duration of these tests.
+    $this->container->get('theme_handler')->enable(array('bartik', 'seven'));
     $this->container->get('config.factory')
       ->get('system.theme')
       ->set('default', 'bartik')
-      ->save();
-    theme_enable(array('seven'));
-    $this->container->get('config.factory')
-      ->get('system.theme')
       ->set('admin', 'seven')
       ->save();
+
     $this->permissions[] = 'view the administration theme';
 
     //Create an admin user to view tour tips.
diff --git a/core/modules/user/lib/Drupal/user/UserStorage.php b/core/modules/user/lib/Drupal/user/UserStorage.php
index d3f4990..afd53b3 100644
--- a/core/modules/user/lib/Drupal/user/UserStorage.php
+++ b/core/modules/user/lib/Drupal/user/UserStorage.php
@@ -79,23 +79,20 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
   /**
    * {@inheritdoc}
    */
-  function postLoad(array &$queried_users) {
-    foreach ($queried_users as $key => $record) {
-      $queried_users[$key]->roles = array();
+  function mapFromStorageRecords(array $records) {
+    foreach ($records as $record) {
+      $record->roles = array();
       if ($record->uid) {
-        $queried_users[$record->uid]->roles[] = DRUPAL_AUTHENTICATED_RID;
+        $record->roles[] = DRUPAL_AUTHENTICATED_RID;
       }
       else {
-        $queried_users[$record->uid]->roles[] = DRUPAL_ANONYMOUS_RID;
+        $record->roles[] = DRUPAL_ANONYMOUS_RID;
       }
     }
 
     // Add any additional roles from the database.
-    $this->addRoles($queried_users);
-
-    // Call the default postLoad() method. This will add fields and call
-    // hook_user_load().
-    parent::postLoad($queried_users);
+    $this->addRoles($records);
+    return parent::mapFromStorageRecords($records);
   }
 
   /**
@@ -129,9 +126,11 @@ public function saveRoles(UserInterface $account) {
    * {@inheritdoc}
    */
   public function addRoles(array $users) {
-    $result = $this->database->query('SELECT rid, uid FROM {users_roles} WHERE uid IN (:uids)', array(':uids' => array_keys($users)));
-    foreach ($result as $record) {
-      $users[$record->uid]->roles[] = $record->rid;
+    if ($users) {
+      $result = $this->database->query('SELECT rid, uid FROM {users_roles} WHERE uid IN (:uids)', array(':uids' => array_keys($users)));
+      foreach ($result as $record) {
+        $users[$record->uid]->roles[] = $record->rid;
+      }
     }
   }
 
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 7812eea..55c57d1 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -103,6 +103,7 @@ user.role_edit:
   path: '/admin/people/roles/manage/{user_role}'
   defaults:
     _entity_form: user_role.default
+    _title: 'Edit role'
   requirements:
     _entity_access: user_role.update
 
@@ -110,7 +111,7 @@ user.role_delete:
   path: '/admin/people/roles/manage/{user_role}/delete'
   defaults:
     _entity_form: user_role.delete
-    _title: 'Edit role'
+    _title: 'Delete role'
   requirements:
     _entity_access: user_role.delete
 
diff --git a/core/modules/views/config/schema/views.area.schema.yml b/core/modules/views/config/schema/views.area.schema.yml
index 086a512..b75deb1 100644
--- a/core/modules/views/config/schema/views.area.schema.yml
+++ b/core/modules/views/config/schema/views.area.schema.yml
@@ -4,6 +4,23 @@ views.area.*:
   type: views_area
   label: 'Default area'
 
+views.area.entity:
+  type: views_area
+  label: 'Entity'
+  mapping:
+    entity_id:
+      type: string
+      label: 'ID'
+    view_mode:
+      type: string
+      label: 'View mode'
+    tokenize:
+      type: boolean
+      label: 'Should replacement tokens be used from the first row'
+    bypass_access:
+      type: boolean
+      label: 'Bypass access checks'
+
 views.area.text:
   type: views_area
   label: 'Text'
diff --git a/core/modules/views_ui/views_ui.routing.yml b/core/modules/views_ui/views_ui.routing.yml
index 638decd..b92b149 100644
--- a/core/modules/views_ui/views_ui.routing.yml
+++ b/core/modules/views_ui/views_ui.routing.yml
@@ -126,6 +126,7 @@ views_ui.break_lock:
   path: '/admin/structure/views/view/{view}/break-lock'
   defaults:
     _entity_form: 'view.break_lock'
+    _title: 'Break lock'
   requirements:
     _entity_access: view.break-lock
 
diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist
index aacbc2f..32b3384 100644
--- a/core/phpunit.xml.dist
+++ b/core/phpunit.xml.dist
@@ -18,7 +18,9 @@
       <!-- Exclude Drush tests. -->
       <exclude>./drush/tests</exclude>
       <!-- Exclude special-case files from config's test modules. -->
+      <!-- @todo Remove /lib/Drupal/config_test after the transition to PSR-4. -->
       <exclude>./modules/config/tests/config_test/lib/Drupal/config_test</exclude>
+      <exclude>./modules/config/tests/config_test/src</exclude>
     </testsuite>
   </testsuites>
   <!-- Filter for coverage reports. -->
diff --git a/core/profiles/standard/config/install/system.theme.yml b/core/profiles/standard/config/install/system.theme.yml
new file mode 100644
index 0000000..57dadd4
--- /dev/null
+++ b/core/profiles/standard/config/install/system.theme.yml
@@ -0,0 +1,2 @@
+admin: seven
+default: bartik
diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml
index 30f9c26..3f8116b 100644
--- a/core/profiles/standard/standard.info.yml
+++ b/core/profiles/standard/standard.info.yml
@@ -35,3 +35,6 @@ dependencies:
   - views
   - views_ui
   - tour
+themes:
+  - bartik
+  - seven
diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install
index 60c6f72..73ea074 100644
--- a/core/profiles/standard/standard.install
+++ b/core/profiles/standard/standard.install
@@ -14,15 +14,6 @@
  * @see system_install()
  */
 function standard_install() {
-  // Enable Bartik theme and set it as default theme instead of Stark.
-  // @see system_install()
-  $default_theme = 'bartik';
-  \Drupal::config('system.theme')
-    ->set('default', $default_theme)
-    ->save();
-  theme_enable(array($default_theme));
-  theme_disable(array('stark'));
-
   // Set front page to "node".
   \Drupal::config('system.site')->set('page.front', 'node')->save();
 
@@ -78,7 +69,5 @@ function standard_install() {
   $shortcut->save();
 
   // Enable the admin theme.
-  theme_enable(array('seven'));
-  \Drupal::config('system.theme')->set('admin', 'seven')->save();
   \Drupal::config('node.settings')->set('use_admin_theme', '1')->save();
 }
diff --git a/core/scripts/switch-psr4.sh b/core/scripts/switch-psr4.sh
new file mode 100644
index 0000000..0eebbe5
--- /dev/null
+++ b/core/scripts/switch-psr4.sh
@@ -0,0 +1,319 @@
+#!/bin/php
+<?php
+
+namespace Drupal\Core\SwitchPsr4;
+
+/**
+ * @file
+ * Moves module-provided class files to their PSR-4 location.
+ *
+ * E.g.:
+ * core/modules/action/{lib/Drupal/action → src}/ActionAccessController.php
+ * core/modules/action/{lib/Drupal/action → src}/ActionAddFormController.php
+ */
+
+// Determine DRUPAL_ROOT.
+$dir = dirname(__FILE__);
+while (!defined('DRUPAL_ROOT')) {
+  if (is_dir($dir . '/core')) {
+    define('DRUPAL_ROOT', $dir);
+  }
+  $dir = dirname($dir);
+}
+
+// Run the script.
+run();
+
+/**
+ * Runs the script.
+ */
+function run() {
+  $cmd_arguments = $_SERVER['argv'];
+  // The first argument is the script name.
+  $scriptname = array_shift($cmd_arguments);
+  if (in_array('--help', $cmd_arguments)) {
+    print get_help_text($scriptname);
+  }
+  elseif (!empty($cmd_arguments)) {
+    // If one or more arguments are given, treat those arguments as directories,
+    // and process all modules found within these directories.
+    $directories = array();
+    foreach ($cmd_arguments as $arg) {
+      if ('-' === $arg{0}) {
+        // The only valid option is '--help'.
+        print "Invalid option '$arg'";
+        return;
+      }
+      if (!is_dir($arg)) {
+        print "The argument '$arg' is not a directory.";
+        continue;
+      }
+      $directories[] = $arg;
+    }
+    // Process all directories that were found in the argument list.
+    foreach ($directories as $dir) {
+      process_candidate_dir($dir);
+    }
+  }
+  else {
+    // If no arguments are given, process all modules and profiles in the core
+    // directories instead.
+    process_candidate_dir(DRUPAL_ROOT . '/core/modules');
+    process_candidate_dir(DRUPAL_ROOT . '/core/profiles');
+  }
+}
+
+/**
+ * @param string $scriptname
+ *
+ * @return string
+ *   Help text in case the "--help" option is present.
+ */
+function get_help_text($scriptname) {
+  $script = basename($scriptname);
+  return <<<EOF
+
+Move module class files from PSR-0 to PSR-4.
+
+E.g. the following files would be moved.
+  - core/modules/action/{lib/Drupal/action → src}/ActionListController.php
+  - core/modules/action/tests/{Drupal/action/Tests → src}/Menu/ActionLocalTasksTest.php
+
+Class files which are already in the PSR-4 path remain where they are.
+
+Warning: Classes with an underscore in the class name (after the last namespace
+separator) will end up in an incorrect location, and need to be fixed manually.
+Such class names are not allowed in Drupal coding standards, but they may still
+occur in some custom and contrib modules.
+
+The script takes any number of arguments which, if present, specify the
+directories to scan for modules and module class files.
+
+If no arguments are given, the following directories will be processed:
+  - core/modules/
+  - core/profiles/
+
+See:
+  - https://drupal.org/node/2083547 Drupal issue
+  - https://drupal.org/node/2156625 Documentation of PSR-4 in Drupal
+
+Usage:        {$script} [OPTIONS] [DIRECTORIES]
+Examples:     {$script}
+              {$script} core/modules/views
+              {$script} modules/contrib modules/custom
+              {$script} modules/contrib/devel
+
+Options:
+  --help      Display this help page and exit.
+
+
+EOF;
+}
+
+/**
+ * Scans all subdirectories of a given directory for Drupal extensions, and runs
+ * process_extension() for each one that it finds.
+ *
+ * @param string $dir
+ *   A directory whose subdirectories could contain Drupal extensions.
+ */
+function process_extensions_base_dir($dir) {
+  /**
+   * @var \DirectoryIterator $fileinfo
+   */
+  foreach (new \DirectoryIterator($dir) as $fileinfo) {
+    if ($fileinfo->isDot()) {
+      // do nothing
+    }
+    elseif ($fileinfo->isDir()) {
+      process_candidate_dir($fileinfo->getPathname());
+    }
+  }
+}
+
+/**
+ * Recursively scans a directory for Drupal extensions, and runs
+ * process_extension() for each one that it finds.
+ *
+ * @param string $dir
+ *   A directory that could be a Drupal extension directory.
+ */
+function process_candidate_dir($dir) {
+  /**
+   * @var \DirectoryIterator $fileinfo
+   */
+  foreach (new \DirectoryIterator($dir) as $fileinfo) {
+    if ($fileinfo->isDot()) {
+      // Ignore "." and "..".
+    }
+    elseif ($fileinfo->isDir()) {
+      // It's a directory.
+      switch ($fileinfo->getFilename()) {
+        case 'lib':
+        case 'src':
+          // Ignore these directory names.
+          continue;
+        default:
+          // Look for more extensions in subdirectories.
+          process_candidate_dir($fileinfo->getPathname());
+      }
+    }
+    else {
+      // It's a file.
+      if (preg_match('/^(.+).info.yml$/', $fileinfo->getFilename(), $m)) {
+        // It's a *.info.yml file, so we found an extension directory.
+        $extension_name = $m[1];
+      }
+    }
+  }
+  if (isset($extension_name)) {
+    process_extension($extension_name, $dir);
+    process_extension_phpunit($extension_name, $dir);
+  }
+}
+
+/**
+ * Process a Drupal extension (module, theme) in a directory.
+ *
+ * This will move all class files in this extension from
+ * lib/Drupal/$extension_name/$path to src/$path.
+ *
+ * @param string $name
+ *   Name of the extension.
+ * @param string $dir
+ *   Directory of the extension.
+ * @throws \Exception
+ */
+function process_extension($name, $dir) {
+
+  if (!is_dir($source = "$dir/lib/Drupal/$name")) {
+    // Nothing to do in this module.
+    return;
+  }
+
+  if (!is_dir($destination = "$dir/src")) {
+    mkdir($destination);
+  }
+
+  // Move class files two levels up.
+  move_directory_contents($source, $destination);
+
+  // Clean up.
+  require_dir_empty("$dir/lib/Drupal");
+  rmdir("$dir/lib/Drupal");
+}
+
+/**
+ * Process a Drupal extension (module, theme) in a directory.
+ *
+ * This will move all PHPUnit class files in this extension from
+ * tests/Drupal/$name/Tests/ to tests/src/.
+ *
+ * @param string $name
+ *   Name of the extension.
+ * @param string $dir
+ *   Directory of the extension.
+ */
+function process_extension_phpunit($name, $dir) {
+
+  if (!is_dir($source = "$dir/tests/Drupal/$name/Tests")) {
+    // Nothing to do in this module.
+    return;
+  }
+
+  if (!is_dir($dest = "$dir/tests/src")) {
+    mkdir($dest);
+  }
+
+  // Move class files two levels up.
+  move_directory_contents($source, $dest);
+
+  // Clean up.
+  require_dir_empty("$dir/tests/Drupal/$name");
+  rmdir("$dir/tests/Drupal/$name");
+  require_dir_empty("$dir/tests/Drupal");
+  rmdir("$dir/tests/Drupal");
+}
+
+/**
+ * Move directory contents from an existing source directory to an existing
+ * destination directory.
+ *
+ * @param string $source
+ *   An existing source directory.
+ * @param string $destination
+ *   An existing destination directory.
+ *
+ * @throws \Exception
+ */
+function move_directory_contents($source, $destination) {
+
+  if (!is_dir($source)) {
+    throw new \Exception("The source '$source' is not a directory.");
+  }
+
+  if (!is_dir($destination)) {
+    throw new \Exception("The destination '$destination' is not a directory.");
+  }
+
+  /**
+   * @var \DirectoryIterator $fileinfo
+   */
+  foreach (new \DirectoryIterator($source) as $fileinfo) {
+    if ($fileinfo->isDot()) {
+      continue;
+    }
+    $dest_path = $destination . '/' . $fileinfo->getFilename();
+    if (!file_exists($dest_path)) {
+      rename($fileinfo->getPathname(), $dest_path);
+    }
+    elseif ($fileinfo->isFile()) {
+      throw new \Exception("Destination '$dest_path' already exists, cannot overwrite.");
+    }
+    elseif ($fileinfo->isDir()) {
+      if (!is_dir($dest_path)) {
+        throw new \Exception("Destination '$dest_path' is not a directory.");
+      }
+      move_directory_contents($fileinfo->getPathname(), $dest_path);
+    }
+  }
+
+  require_dir_empty($source);
+
+  rmdir($source);
+}
+
+/**
+ * Throws an exception if a directory is not empty.
+ *
+ * @param string $dir
+ *   Directory to check.
+ *
+ * @throws \Exception
+ */
+function require_dir_empty($dir) {
+  if (is_file($dir)) {
+    throw new \Exception("The path '$dir' is a file, when it should be a directory.");
+  }
+  if (!is_dir($dir)) {
+    throw new \Exception("The directory '$dir' does not exist.");
+  }
+  if (!is_readable($dir)) {
+    throw new \Exception("The directory '$dir' is not readable.");
+  }
+  /**
+   * @var \DirectoryIterator $fileinfo
+   */
+  foreach (new \DirectoryIterator($dir) as $fileinfo) {
+    if ($fileinfo->isDot()) {
+      continue;
+    }
+    $path = $fileinfo->getPathname();
+    if ($fileinfo->isFile()) {
+      throw new \Exception("File '$path' found in a directory that should be empty.");
+    }
+    elseif ($fileinfo->isDir()) {
+      throw new \Exception("Subdirectory '$path' found in a directory that should be empty.");
+    }
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Access/CsrfTokenGeneratorTest.php b/core/tests/Drupal/Tests/Core/Access/CsrfTokenGeneratorTest.php
index 766d641..8607304 100644
--- a/core/tests/Drupal/Tests/Core/Access/CsrfTokenGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Access/CsrfTokenGeneratorTest.php
@@ -71,23 +71,6 @@ public function testValidate() {
 
     $token = $this->generator->get('bar');
     $this->assertTrue($this->generator->validate($token, 'bar'));
-
-    // Check the skip_anonymous option with both a anonymous user and a real
-    // user.
-    $account = $this->getMock('Drupal\Core\Session\AccountInterface');
-    $account->expects($this->once())
-      ->method('isAnonymous')
-      ->will($this->returnValue(TRUE));
-    $this->generator->setCurrentUser($account);
-    $this->assertTrue($this->generator->validate($token, 'foo', TRUE));
-
-    $account = $this->getMock('Drupal\Core\Session\AccountInterface');
-    $account->expects($this->once())
-      ->method('isAnonymous')
-      ->will($this->returnValue(FALSE));
-    $this->generator->setCurrentUser($account);
-
-    $this->assertFalse($this->generator->validate($token, 'foo', TRUE));
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
index 20f4a09..37019f5 100644
--- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
@@ -110,6 +110,8 @@ public static function getInfo() {
 
   /**
    * {@inheritdoc}
+   *
+   * @covers ::__construct()
    */
   protected function setUp() {
     parent::setUp();
@@ -128,6 +130,10 @@ protected function setUp() {
     $this->entityType->expects($this->any())
       ->method('getConfigPrefix')
       ->will($this->returnValue('the_config_prefix'));
+    $this->entityType->expects($this->any())
+      ->method('getClass')
+      ->will($this->returnValue(get_class($this->getMockEntity())));
+
 
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
 
@@ -171,12 +177,9 @@ protected function setUp() {
 
   /**
    * @covers ::create()
+   * @covers ::doCreate()
    */
   public function testCreateWithPredefinedUuid() {
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $this->cacheBackend->expects($this->never())
       ->method('invalidateTags');
 
@@ -197,14 +200,11 @@ public function testCreateWithPredefinedUuid() {
 
   /**
    * @covers ::create()
+   * @covers ::doCreate()
    *
    * @return \Drupal\Core\Entity\EntityInterface
    */
   public function testCreate() {
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $this->cacheBackend->expects($this->never())
       ->method('invalidateTags');
 
@@ -227,6 +227,7 @@ public function testCreate() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *
@@ -252,7 +253,7 @@ public function testSaveInsert(EntityInterface $entity) {
         $this->entityTypeId . 's' => TRUE, // List cache tag.
       ));
 
-    $this->configFactory->expects($this->once())
+    $this->configFactory->expects($this->exactly(2))
       ->method('get')
       ->with('the_config_prefix.foo')
       ->will($this->returnValue($config_object));
@@ -285,6 +286,7 @@ public function testSaveInsert(EntityInterface $entity) {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *
@@ -315,7 +317,7 @@ public function testSaveUpdate(EntityInterface $entity) {
       ->method('loadMultiple')
       ->with(array('the_config_prefix.foo'))
       ->will($this->returnValue(array()));
-    $this->configFactory->expects($this->once())
+    $this->configFactory->expects($this->exactly(2))
       ->method('get')
       ->with('the_config_prefix.foo')
       ->will($this->returnValue($config_object));
@@ -348,6 +350,7 @@ public function testSaveUpdate(EntityInterface $entity) {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @depends testSaveInsert
    */
@@ -416,6 +419,7 @@ public function testSaveInvalid() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @expectedException \Drupal\Core\Entity\EntityStorageException
    */
@@ -447,6 +451,7 @@ public function testSaveDuplicate() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @expectedException \Drupal\Core\Config\ConfigDuplicateUUIDException
    * @expectedExceptionMessage when this UUID is already used for
@@ -482,6 +487,7 @@ public function testSaveMismatch() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    */
   public function testSaveNoMismatch() {
     $config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
@@ -522,6 +528,7 @@ public function testSaveNoMismatch() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @expectedException \Drupal\Core\Config\ConfigDuplicateUUIDException
    * @expectedExceptionMessage when this entity already exists with UUID
@@ -573,9 +580,6 @@ public function testSaveChangedUuid() {
       ->will($this->returnValue(array('foo')));
 
     $entity = $this->getMockEntity(array('id' => 'foo'));
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($entity)));
 
     $entity->set('uuid', 'baz');
     $this->entityStorage->save($entity);
@@ -584,7 +588,8 @@ public function testSaveChangedUuid() {
   /**
    * @covers ::load()
    * @covers ::postLoad()
-   * @covers ::buildQuery()
+   * @covers ::mapFromStorageRecords()
+   * @covers ::doLoadMultiple()
    */
   public function testLoad() {
     $config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
@@ -605,10 +610,6 @@ public function testLoad() {
       ->method('getImplementations')
       ->will($this->returnValue(array()));
 
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $entity = $this->entityStorage->load('foo');
     $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity);
     $this->assertSame('foo', $entity->id());
@@ -617,7 +618,8 @@ public function testLoad() {
   /**
    * @covers ::loadMultiple()
    * @covers ::postLoad()
-   * @covers ::buildQuery()
+   * @covers ::mapFromStorageRecords()
+   * @covers ::doLoadMultiple()
    */
   public function testLoadMultipleAll() {
     $foo_config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
@@ -647,10 +649,6 @@ public function testLoadMultipleAll() {
       ->method('getImplementations')
       ->will($this->returnValue(array()));
 
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $entities = $this->entityStorage->loadMultiple();
     $expected['foo'] = 'foo';
     $expected['bar'] = 'bar';
@@ -664,7 +662,8 @@ public function testLoadMultipleAll() {
   /**
    * @covers ::loadMultiple()
    * @covers ::postLoad()
-   * @covers ::buildQuery()
+   * @covers ::mapFromStorageRecords()
+   * @covers ::doLoadMultiple()
    */
   public function testLoadMultipleIds() {
     $config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
@@ -684,9 +683,6 @@ public function testLoadMultipleIds() {
     $this->moduleHandler->expects($this->exactly(2))
       ->method('getImplementations')
       ->will($this->returnValue(array()));
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
 
     $entities = $this->entityStorage->loadMultiple(array('foo'));
     foreach ($entities as $id => $entity) {
@@ -714,12 +710,9 @@ public function testDeleteRevision() {
 
   /**
    * @covers ::delete()
+   * @covers ::doDelete()
    */
   public function testDelete() {
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $entities = array();
     $configs = array();
     $config_map = array();
@@ -776,6 +769,7 @@ public function testDelete() {
 
   /**
    * @covers ::delete()
+   * @covers ::doDelete()
    */
   public function testDeleteNothing() {
     $this->moduleHandler->expects($this->never())
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
index 1c260a3..5b3a7e1 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
@@ -695,6 +695,10 @@ protected function setUpEntityWithFieldDefinition($custom_invoke_all = FALSE, $f
     $entity_type->expects($this->any())
       ->method('getKeys')
       ->will($this->returnValue(array()));
+    $entity_type->expects($this->any())
+      ->method('isSubclassOf')
+      ->with($this->equalTo('\Drupal\Core\Entity\ContentEntityInterface'))
+      ->will($this->returnValue(TRUE));
     $field_definition = $this->getMockBuilder('Drupal\Core\Field\FieldDefinition')
       ->disableOriginalConstructor()
       ->getMock();
diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
index b9637fe..cdbd0f6 100644
--- a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
@@ -11,6 +11,8 @@
 use Drupal\Core\Extension\InfoParser;
 use Drupal\Core\Extension\ThemeHandler;
 use Drupal\Core\Config\ConfigInstaller;
+use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
+use Drupal\Core\State\State;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -38,11 +40,11 @@ class ThemeHandlerTest extends UnitTestCase {
   protected $infoParser;
 
   /**
-   * The mocked cache backend.
+   * The mocked state backend.
    *
-   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
+   * @var \Drupal\Core\State\StateInterface|\PHPUnit_Framework_MockObject_MockObject
    */
-  protected $cacheBackend;
+  protected $state;
 
   /**
    * The mocked config factory.
@@ -104,7 +106,7 @@ protected function setUp() {
       ),
     ));
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
-    $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
+    $this->state = new State(new KeyValueMemoryFactory());
     $this->infoParser = $this->getMock('Drupal\Core\Extension\InfoParserInterface');
     $this->configInstaller = $this->getMock('Drupal\Core\Config\ConfigInstallerInterface');
     $this->routeBuilder = $this->getMockBuilder('Drupal\Core\Routing\RouteBuilder')
@@ -113,131 +115,10 @@ protected function setUp() {
     $this->extensionDiscovery = $this->getMockBuilder('Drupal\Core\Extension\ExtensionDiscovery')
       ->disableOriginalConstructor()
       ->getMock();
-    $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->cacheBackend, $this->infoParser, $this->configInstaller, $this->routeBuilder, $this->extensionDiscovery);
+    $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->state, $this->infoParser, $this->configInstaller, $this->routeBuilder, $this->extensionDiscovery);
 
-    $this->getContainerWithCacheBins($this->cacheBackend);
-  }
-
-  /**
-   * Tests enabling a theme with a name longer than 50 chars.
-   *
-   * @expectedException \Drupal\Core\Extension\ExtensionNameLengthException
-   * @expectedExceptionMessage Theme name <em class="placeholder">thisNameIsFarTooLong0000000000000000000000000000051</em> is over the maximum allowed length of 50 characters.
-   */
-  public function testThemeEnableWithTooLongName() {
-    $this->themeHandler->enable(array('thisNameIsFarTooLong0000000000000000000000000000051'));
-  }
-
-  /**
-   * Tests enabling a single theme.
-   *
-   * @see \Drupal\Core\Extension\ThemeHandler::enable()
-   */
-  public function testEnableSingleTheme() {
-    $theme_list = array('theme_test');
-
-    $this->configFactory->get('core.extension')
-      ->expects($this->once())
-      ->method('set')
-      ->with('theme.theme_test', 0)
-      ->will($this->returnSelf());
-    $this->configFactory->get('core.extension')
-      ->expects($this->once())
-      ->method('save');
-
-    $this->configFactory->get('core.extension')
-      ->expects($this->once())
-      ->method('clear')
-      ->with('disabled.theme.theme_test')
-      ->will($this->returnSelf());
-    $this->configFactory->get('core.extension')
-      ->expects($this->once())
-      ->method('save');
-
-    $this->extensionDiscovery->expects($this->any())
-      ->method('scan')
-      ->will($this->returnValue(array()));
-
-    // Ensure that the themes_enabled hook is fired.
-    $this->moduleHandler->expects($this->at(0))
-      ->method('invokeAll')
-      ->with('themes_enabled', array($theme_list));
-
-    // Ensure the config installer will be called.
-    $this->configInstaller->expects($this->once())
-      ->method('installDefaultConfig')
-      ->with('theme', $theme_list[0]);
-
-    $this->themeHandler->enable($theme_list);
-
-    $this->assertTrue($this->themeHandler->clearedCssCache);
-    $this->assertTrue($this->themeHandler->registryRebuild);
-  }
-
-  /**
-   * Ensures that enabling a theme does clear the theme info listing.
-   *
-   * @see \Drupal\Core\Extension\ThemeHandler::listInfo()
-   */
-  public function testEnableAndListInfo() {
-    $this->configFactory->get('core.extension')
-      ->expects($this->exactly(2))
-      ->method('set')
-      ->will($this->returnSelf());
-
-    $this->configFactory->get('core.extension')
-      ->expects($this->exactly(2))
-      ->method('clear')
-      ->will($this->returnSelf());
-
-    $this->extensionDiscovery->expects($this->any())
-      ->method('scan')
-      ->will($this->returnValue(array()));
-
-    $this->themeHandler->enable(array('bartik'));
-    $this->themeHandler->systemList['bartik'] = new Extension('theme', DRUPAL_ROOT . '/core/themes/bartik/bartik.info.yml', 'bartik.info.yml');
-    $this->themeHandler->systemList['bartik']->info = array(
-      'stylesheets' => array(
-        'all' => array(
-          'css/layout.css',
-          'css/style.css',
-          'css/colors.css',
-        ),
-      ),
-      'libraries' => array(
-        'example/theme',
-      ),
-      'engine' => 'twig',
-      'base theme' => 'stark',
-    );
-
-    $list_info = $this->themeHandler->listInfo();
-    $this->assertCount(1, $list_info);
-
-    $this->assertEquals($this->themeHandler->systemList['bartik']->info['stylesheets'], $list_info['bartik']->stylesheets);
-    $this->assertEquals($this->themeHandler->systemList['bartik']->libraries, $list_info['bartik']->libraries);
-    $this->assertEquals('twig', $list_info['bartik']->engine);
-    $this->assertEquals('stark', $list_info['bartik']->base_theme);
-    $this->assertEquals(0, $list_info['bartik']->status);
-
-    $this->themeHandler->systemList['seven'] = new Extension('theme', DRUPAL_ROOT . '/core/themes/seven/seven.info.yml', 'seven.info.yml');
-    $this->themeHandler->systemList['seven']->info = array(
-      'stylesheets' => array(
-        'screen' => array(
-          'style.css',
-        ),
-      ),
-      'libraries' => array(),
-    );
-    $this->themeHandler->systemList['seven']->status = 1;
-
-    $this->themeHandler->enable(array('seven'));
-
-    $list_info = $this->themeHandler->listInfo();
-    $this->assertCount(2, $list_info);
-
-    $this->assertEquals($this->themeHandler->systemList['seven']->info['stylesheets'], $list_info['seven']->stylesheets);
-    $this->assertEquals(1, $list_info['seven']->status);
+    $cache_backend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
+    $this->getContainerWithCacheBins($cache_backend);
   }
 
   /**
@@ -265,6 +146,9 @@ public function testRebuildThemeData() {
         $info_parser = new InfoParser();
         return $info_parser->parse($file);
       }));
+    $this->moduleHandler->expects($this->once())
+      ->method('buildModuleDependencies')
+      ->will($this->returnArgument(0));
 
     $this->moduleHandler->expects($this->once())
       ->method('alter');
@@ -328,6 +212,9 @@ public function testRebuildThemeDataWithThemeParents() {
         $info_parser = new InfoParser();
         return $info_parser->parse($file);
       }));
+    $this->moduleHandler->expects($this->once())
+      ->method('buildModuleDependencies')
+      ->will($this->returnArgument(0));
 
     $theme_data = $this->themeHandler->rebuildThemeData();
     $this->assertCount(2, $theme_data);
diff --git a/core/tests/Drupal/Tests/Core/Utility/TokenUnitTest.php b/core/tests/Drupal/Tests/Core/Utility/TokenUnitTest.php
new file mode 100644
index 0000000..f2d0a2d
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Utility/TokenUnitTest.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Utility\TokenUnitTest.
+ */
+
+namespace Drupal\Tests\Core\Utility;
+
+use Drupal\Core\Language\Language;
+use Drupal\Core\Utility\Token;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Tests\Core\Utility\Token
+ */
+class TokenUnitTest extends UnitTestCase {
+
+  /**
+   * The cache used for testing.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $cache;
+
+  /**
+   * The language manager used for testing.
+   *
+   * @var \Drupal\Core\Language\LanguageManager|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $languageManager;
+
+  /**
+   * The module handler service used for testing.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * The token service under test.
+   *
+   * @var \Drupal\Core\Utility\Token|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $token;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'description' => '',
+      'name' => '\Drupal\Tests\Core\Utility\Token unit test',
+      'group' => 'System',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    $this->cache = $this->getMock('\Drupal\Core\Cache\CacheBackendInterface');
+
+    $this->languageManager = $this->getMockBuilder('\Drupal\Core\Language\LanguageManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
+
+    $this->token = new Token($this->moduleHandler, $this->cache, $this->languageManager);
+  }
+
+  /**
+   * @covers getInfo
+   */
+  public function testGetInfo() {
+    $token_info = array(
+      'types' => array(
+        'foo' => array(
+          'name' => $this->randomName(),
+        ),
+      ),
+    );
+
+    $language = $this->getMock('\Drupal\Core\Language\Language');
+    $language->id = $this->randomName();
+
+    $this->languageManager->expects($this->once())
+      ->method('getCurrentLanguage')
+      ->with(Language::TYPE_CONTENT)
+      ->will($this->returnValue($language));
+
+    // The persistent cache must only be hit once, after which the info is
+    // cached statically.
+    $this->cache->expects($this->once())
+      ->method('get');
+    $this->cache->expects($this->once())
+      ->method('set')
+      ->with('token_info:' . $language->id, $token_info);
+
+    $this->moduleHandler->expects($this->once())
+      ->method('invokeAll')
+      ->with('token_info')
+      ->will($this->returnValue($token_info));
+    $this->moduleHandler->expects($this->once())
+      ->method('alter')
+      ->with('token_info', $token_info);
+
+    // Get the information for the first time. The cache should be checked, the
+    // hooks invoked, and the info should be set to the cache should.
+    $this->token->getInfo();
+    // Get the information for the second time. The data must be returned from
+    // the static cache, so the persistent cache must not be accessed and the
+    // hooks must not be invoked.
+    $this->token->getInfo();
+  }
+
+}
diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php
index c69a52f..ccfe5f4 100644
--- a/core/tests/bootstrap.php
+++ b/core/tests/bootstrap.php
@@ -56,6 +56,8 @@ function drupal_phpunit_contrib_extension_directory_roots() {
  */
 function drupal_phpunit_register_extension_dirs(Composer\Autoload\ClassLoader $loader, $dirs) {
   foreach ($dirs as $extension => $dir) {
+    // Register PSR-0 test directories.
+    // @todo Remove this, when the transition to PSR-4 is complete.
     $lib_path = $dir . '/lib';
     if (is_dir($lib_path)) {
       $loader->add('Drupal\\' . $extension, $lib_path);
@@ -64,6 +66,13 @@ function drupal_phpunit_register_extension_dirs(Composer\Autoload\ClassLoader $l
     if (is_dir($tests_path)) {
       $loader->add('Drupal\\' . $extension, $tests_path);
     }
+    // Register PSR-4 test directories.
+    if (is_dir($dir . '/src')) {
+      $loader->addPsr4('Drupal\\' . $extension . '\\', $dir . '/src');
+    }
+    if (is_dir($dir . '/tests/src')) {
+      $loader->addPsr4('Drupal\\' . $extension . '\Tests\\', $dir . '/tests/src');
+    }
   }
 }
 
diff --git a/core/vendor/composer/autoload_namespaces.php b/core/vendor/composer/autoload_namespaces.php
index 80f7f7a..b6e4ab8 100644
--- a/core/vendor/composer/autoload_namespaces.php
+++ b/core/vendor/composer/autoload_namespaces.php
@@ -27,9 +27,6 @@
     'Psr\\Log\\' => array($vendorDir . '/psr/log'),
     'Gliph' => array($vendorDir . '/sdboyer/gliph/src'),
     'EasyRdf_' => array($vendorDir . '/easyrdf/easyrdf/lib'),
-    'Drupal\\Driver' => array($baseDir . '/drivers/lib'),
-    'Drupal\\Core' => array($baseDir . '/core/lib'),
-    'Drupal\\Component' => array($baseDir . '/core/lib'),
     'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib'),
     'Doctrine\\Common\\Inflector\\' => array($vendorDir . '/doctrine/inflector/lib'),
     'Doctrine\\Common\\Collections\\' => array($vendorDir . '/doctrine/collections/lib'),
diff --git a/core/vendor/composer/autoload_psr4.php b/core/vendor/composer/autoload_psr4.php
index e53db41..f832317 100644
--- a/core/vendor/composer/autoload_psr4.php
+++ b/core/vendor/composer/autoload_psr4.php
@@ -8,4 +8,7 @@
 return array(
     'GuzzleHttp\\Stream\\' => array($vendorDir . '/guzzlehttp/streams/src'),
     'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
+    'Drupal\\Driver\\' => array($baseDir . '/drivers/lib/Drupal/Driver'),
+    'Drupal\\Core\\' => array($baseDir . '/core/lib/Drupal/Core'),
+    'Drupal\\Component\\' => array($baseDir . '/core/lib/Drupal/Component'),
 );
