diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php
index bf515de..16b5a56 100644
--- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php
+++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php
@@ -117,7 +117,14 @@ public function __construct($entityType) {
     $this->entityInfo = entity_get_info($entityType);
     $this->entityCache = array();
     $this->hookLoadArguments = array();
-    $this->idKey = $this->entityInfo['entity_keys']['id'];
+
+    // Check if the entity type supports IDs.
+    if (isset($this->entityInfo['entity_keys']['id'])) {
+      $this->idKey = $this->entityInfo['entity_keys']['id'];
+    }
+    else {
+      $this->idKey = FALSE;
+    }
 
     // Check if the entity type supports UUIDs.
     if (!empty($this->entityInfo['entity_keys']['uuid'])) {
diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php
index f4fc459..4e0fc6a 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormController.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormController.php
@@ -184,6 +184,9 @@ public function validate(array $form, array &$form_state) {
    *   A reference to a keyed array containing the current state of the form.
    */
   public function submit(array $form, array &$form_state) {
+    // Remove button and internal Form API values from submitted values.
+    form_state_values_clean($form_state);
+
     $this->submitEntityLanguage($form, $form_state);
     $entity = $this->buildEntity($form, $form_state);
     $this->setEntity($entity, $form_state);
diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module
index 585f011..cfc5405 100644
--- a/core/modules/contact/contact.module
+++ b/core/modules/contact/contact.module
@@ -74,6 +74,8 @@ function contact_menu() {
   );
   $items['admin/structure/contact/manage/%contact_category'] = array(
     'title' => 'Edit contact category',
+    'title callback' => 'contact_menu_title_category',
+    'title arguments' => array(4),
     'page callback' => 'contact_category_edit',
     'page arguments' => array(4),
     'access arguments' => array('administer contact forms'),
@@ -90,21 +92,37 @@ function contact_menu() {
     'page arguments' => array('contact_category_delete_form', 4),
     'access arguments' => array('administer contact forms'),
     'type' => MENU_LOCAL_TASK,
+    'weight' => 10,
     'file' => 'contact.admin.inc',
   );
+
   $items['contact'] = array(
     'title' => 'Contact',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('contact_site_form'),
+    'page callback' => 'contact_site_page',
     'access arguments' => array('access site-wide contact form'),
     'menu_name' => 'footer',
     'type' => MENU_SUGGESTED_ITEM,
     'file' => 'contact.pages.inc',
   );
+  $items['contact/default'] = array(
+    'title' => 'Default contact form',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['contact/%contact_category'] = array(
+    'title' => 'Contact category form',
+    'title callback' => 'contact_menu_title_category',
+    'title arguments' => array(1),
+    'page callback' => 'contact_site_page',
+    'page arguments' => array(1),
+    'access arguments' => array('access site-wide contact form'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'contact.pages.inc',
+  );
   $items['user/%user/contact'] = array(
     'title' => 'Contact',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('contact_personal_form', 1),
+    'page callback' => 'contact_personal_page',
+    'page arguments' => array(1),
     'type' => MENU_LOCAL_TASK,
     'access callback' => '_contact_personal_tab_access',
     'access arguments' => array(1),
@@ -115,6 +133,77 @@ function contact_menu() {
 }
 
 /**
+ * Menu title callback: Returns the link title for editing a category.
+ *
+ * @param \Drupal\category\Category $category
+ *   A contact category entity.
+ *
+ * @return string
+ *   The unsanitized contact category label.
+ */
+function contact_menu_title_category(Category $category) {
+  return $category->label();
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function contact_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+  if ($root_path === 'contact' || $root_path === 'contact/%') {
+    $tabs = &$data['tabs'][0]['output'];
+    // Determine the currently selected tab, if any.
+    $selected_index = -1;
+    $selected_id = '';
+    foreach ($tabs as $index => &$tab) {
+      if (isset($tab['#link']['path']) && $tab['#link']['path'] == 'contact/%') {
+        $selected_index = $index;
+        $selected_id = $router_item['original_map'][1];
+      }
+    }
+    // Expand the dynamic argument into a tab for each type.
+    $entities = entity_load_multiple('contact_category');
+    $default = config('contact.settings')->get('default_category');
+    foreach ($entities as $entity) {
+      if ($entity->id() === $default) {
+        // If the current router path is the dynamic path, then we need to unset
+        // the default local task.
+        if ($selected_id === $default) {
+          unset($tabs[0]);
+        }
+        // Otherwise, the current page corresponds to a different entity, and we
+        // replace the label of the default local task.
+        else {
+          $tabs[0]['#link']['title'] = $entity->label();
+          continue;
+        }
+      }
+      // If the current page is the active tab registered in hook_menu(), then
+      // the menu router item with the dynamic argument will be exposed already.
+      // We must not duplicate that tab, but in order to ensure that all of our
+      // tabs appear in a consistent order when switching between tabs, we need
+      // to re-inject it.
+      if ($entity->id() === $selected_id) {
+        $tabs[$selected_index]['#link']['title'] = $entity->label();
+        $tabs[] = $tabs[$selected_index];
+        unset($tabs[$selected_index]);
+        continue;
+      }
+      $tabs[] = array(
+        '#theme' => 'menu_local_task',
+        '#link' => array(
+          'title' => $entity->label(),
+          'href' => 'contact/' . $entity->id(),
+          'localized_options' => array('html' => FALSE),
+        ),
+      );
+    }
+    if (count($entities) > 1) {
+      $data['tabs'][0]['count']++;
+    }
+  }
+}
+
+/**
  * Access callback: Checks access for a user's personal contact form.
  *
  * @param $account
@@ -205,6 +294,24 @@ function contact_config_import_delete($name, $new_config, $old_config) {
 }
 
 /**
+ * Implements hook_entity_info().
+ */
+function contact_entity_info(&$types) {
+  foreach (config_get_storage_names_with_prefix('contact.category.') as $config_name) {
+    $config = config($config_name);
+    $types['contact_message']['bundles'][$config->get('id')] = array(
+      'label' => $config->get('label'),
+      'admin' => array(
+        'path' => 'admin/structure/contact/manage/%contact_category',
+        'real path' => 'admin/structure/contact/manage/' . $config->get('id'),
+        'bundle argument' => 4,
+        'access arguments' => array('administer contact forms'),
+      ),
+    );
+  }
+}
+
+/**
  * Loads a contact category.
  *
  * @param $id
@@ -236,27 +343,40 @@ function contact_category_uri(Category $category) {
  * Implements hook_mail().
  */
 function contact_mail($key, &$message, $params) {
+  $contact_message = $params['contact_message'];
+  $sender = $params['sender'];
   $language = language_load($message['langcode']);
+
   $variables = array(
     '!site-name' => config('system.site')->get('name'),
-    '!subject' => $params['subject'],
-    '!category' => isset($params['category']) ? $params['category']->label() : '',
+    '!subject' => $contact_message->subject,
+    '!category' => isset($params['contact_category']) ? $params['contact_category']->label() : NULL,
     '!form-url' => url(current_path(), array('absolute' => TRUE, 'language' => $language)),
-    '!sender-name' => user_format_name($params['sender']),
-    '!sender-url' => $params['sender']->uid ? url('user/' . $params['sender']->uid, array('absolute' => TRUE, 'language' => $language)) : $params['sender']->mail,
+    '!sender-name' => user_format_name($sender),
   );
+  if ($sender->uid) {
+    $sender_uri = $sender->uri();
+    $variables['!sender-url'] = url($sender_uri['path'], array('absolute' => TRUE, 'language' => $language) + $sender_uri['options']);
+  }
+  else {
+    $variables['!sender-url'] = $params['sender']->mail;
+  }
+
+  $options = array('langcode' => $language->langcode);
 
+  // @todo Separate mail keys make sense for alterability, but 'recipient' and
+  //   'recipient-edit-url' ought to be unified for both keys.
   switch ($key) {
     case 'page_mail':
     case 'page_copy':
-      $message['subject'] .= t('[!category] !subject', $variables, array('langcode' => $language->langcode));
-      $message['body'][] = t("!sender-name (!sender-url) sent a message using the contact form at !form-url.", $variables, array('langcode' => $language->langcode));
-      $message['body'][] = $params['message'];
+      $message['subject'] .= t('[!category] !subject', $variables, $options);
+      $message['body'][] = t("!sender-name (!sender-url) sent a message using the contact form at !form-url.", $variables, $options);
+      $message['body'][] = $contact_message->message;
       break;
 
     case 'page_autoreply':
-      $message['subject'] .= t('[!category] !subject', $variables, array('langcode' => $language->langcode));
-      $message['body'][] = $params['category']->reply;
+      $message['subject'] .= t('[!category] !subject', $variables, $options);
+      $message['body'][] = $params['contact_category']->reply;
       break;
 
     case 'user_mail':
@@ -265,12 +385,12 @@ function contact_mail($key, &$message, $params) {
         '!recipient-name' => user_format_name($params['recipient']),
         '!recipient-edit-url' => url('user/' . $params['recipient']->uid . '/edit', array('absolute' => TRUE, 'language' => $language)),
       );
-      $message['subject'] .= t('[!site-name] !subject', $variables, array('langcode' => $language->langcode));
-      $message['body'][] = t('Hello !recipient-name,', $variables, array('langcode' => $language->langcode));
-      $message['body'][] = t("!sender-name (!sender-url) has sent you a message via your contact form at !site-name.", $variables, array('langcode' => $language->langcode));
-      $message['body'][] = t("If you don't want to receive such e-mails, you can change your settings at !recipient-edit-url.", $variables, array('langcode' => $language->langcode));
-      $message['body'][] = t('Message:', array(), array('langcode' => $language->langcode));
-      $message['body'][] = $params['message'];
+      $message['subject'] .= t('[!site-name] !subject', $variables, $options);
+      $message['body'][] = t('Hello !recipient-name,', $variables, $options);
+      $message['body'][] = t("!sender-name (!sender-url) has sent you a message via your contact form at !site-name.", $variables, $options);
+      $message['body'][] = t("If you don't want to receive such e-mails, you can change your settings at !recipient-edit-url.", $variables, $options);
+      $message['body'][] = t('Message:', array(), $options);
+      $message['body'][] = $contact_message->message;
       break;
   }
 }
diff --git a/core/modules/contact/contact.pages.inc b/core/modules/contact/contact.pages.inc
index b93d995..cf573c8 100644
--- a/core/modules/contact/contact.pages.inc
+++ b/core/modules/contact/contact.pages.inc
@@ -5,11 +5,15 @@
  * Page callbacks for the Contact module.
  */
 
+use Drupal\contact\Category;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
- * Page callback: Form constructor for the site-wide contact form.
+ * Page callback: Presents the site-wide contact form.
+ *
+ * @param \Drupal\contact\Category $category
+ *   (optional) The contact category to use.
  *
  * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
  * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
@@ -18,161 +22,34 @@
  * @see contact_site_form_submit()
  * @ingroup forms
  */
-function contact_site_form($form, &$form_state) {
-  global $user;
-
+function contact_site_page(Category $category = NULL) {
   // Check if flood control has been activated for sending e-mails.
-  $config = config('contact.settings');
-  $limit = $config->get('flood.limit');
-  $interval = $config->get('flood.interval');
-  if (!drupal_container()->get('flood')->isAllowed('contact', $limit, $interval) && !user_access('administer contact forms')) {
-    drupal_set_message(t("You cannot send more than %limit messages in @interval. Try again later.", array('%limit' => $limit, '@interval' => format_interval($interval))), 'error');
-    throw new AccessDeniedHttpException();
+  if (!user_access('administer contact forms')) {
+    contact_flood_control();
   }
 
-  // Get an array of the categories and the current default category.
-  $categories = entity_load_multiple('contact_category');
-
-  // If there are no categories, do not display the form.
-  if (!$categories) {
-    if (user_access('administer contact forms')) {
-      drupal_set_message(t('The contact form has not been configured. <a href="@add">Add one or more categories</a> to the form.', array('@add' => url('admin/structure/contact/add'))), 'error');
+  if (!isset($category)) {
+    $categories = entity_load_multiple('contact_category');
+    $default_category = config('contact.settings')->get('default_category');
+    if (isset($categories[$default_category])) {
+      $category = $categories[$default_category];
     }
+    // If there are no categories, do not display the form.
     else {
-      throw new NotFoundHttpException();
+      if (user_access('administer contact forms')) {
+        drupal_set_message(t('The contact form has not been configured. <a href="@add">Add one or more categories</a> to the form.', array('@add' => url('admin/structure/contact/add'))), 'error');
+        return array();
+      }
+      else {
+        throw new NotFoundHttpException();
+      }
     }
   }
-
-  // Prepare array for select options.
-  uasort($categories, 'Drupal\Core\Config\Entity\ConfigEntityBase::sort');
-  $options = array();
-  foreach ($categories as $category) {
-    $options[$category->id()] = $category->label();
-  }
-
-  // If there is more than one category available and no default category has
-  // been selected, prepend a default placeholder value.
-  $default_category = $config->get('default_category');
-  if (!$default_category) {
-    $default_category = !empty($categories) ? key($categories) : NULL;
-  }
-
-  if (!$user->uid) {
-    $form['#attached']['library'][] = array('system', 'jquery.cookie');
-    $form['#attributes']['class'][] = 'user-info-from-cookie';
-  }
-
-  $form['#attributes']['class'][] = 'contact-form';
-  $form['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Your name'),
-    '#maxlength' => 255,
-    '#default_value' => $user->uid ? user_format_name($user) : '',
-    '#required' => TRUE,
-  );
-  $form['mail'] = array(
-    '#type' => 'email',
-    '#title' => t('Your e-mail address'),
-    '#default_value' => $user->uid ? $user->mail : '',
-    '#required' => TRUE,
-  );
-
-  // Do not allow authenticated users to alter the name or e-mail values to
-  // prevent the impersonation of other users.
-  if ($user->uid) {
-    // Hide the original name and e-mail address fields and display read-only
-    // versions in their place.
-    $form['name']['#access'] = $form['mail']['#access'] = FALSE;
-    $form['name_display'] = array(
-      '#type' => 'item',
-      '#title' => t('Your name'),
-      '#markup' => check_plain($form['name']['#default_value']),
-    );
-    $form['mail_display'] = array(
-      '#type' => 'item',
-      '#title' => t('Your e-mail address'),
-      '#markup' => check_plain($form['mail']['#default_value']),
-    );
-  }
-  $form['subject'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Subject'),
-    '#maxlength' => 255,
-    '#required' => TRUE,
-  );
-  $form['category'] = array(
-    '#type' => 'select',
-    '#title' => t('Category'),
-    '#default_value' => $default_category,
-    '#options' => $options,
-    '#empty_value' => 0,
-    '#required' => TRUE,
-    '#access' => count($categories) > 1,
-  );
-  $form['message'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Message'),
-    '#required' => TRUE,
-  );
-  // Do not allow anonymous users to send themselves a copy because it can be
-  // abused to spam people.
-  $form['copy'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Send yourself a copy.'),
-    '#access' => $user->uid,
-  );
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Send message'),
-  );
-
-  return $form;
-}
-
-/**
- * Form submission handler for contact_site_form().
- */
-function contact_site_form_submit($form, &$form_state) {
-  global $user;
-  $language_interface = language(LANGUAGE_TYPE_INTERFACE);
-
-  $values = $form_state['values'];
-  $values['sender'] = $user;
-  $values['sender']->name = $values['name'];
-  $values['sender']->mail = $values['mail'];
-  $values['category'] = entity_load('contact_category', $values['category']);
-
-  if (!$user->uid) {
-    $values['sender']->name .= ' (' . t('not verified') . ')';
-    // Save the anonymous user information to a cookie for reuse.
-    user_cookie_save(array_intersect_key($values, array_flip(array('name', 'mail'))));
-  }
-
-  // Get the to and from e-mail addresses.
-  $to = implode(', ', $values['category']->recipients);
-  $from = $values['sender']->mail;
-
-  // Send the e-mail to the recipients using the site default language.
-  drupal_mail('contact', 'page_mail', $to, language_default()->langcode, $values, $from);
-
-  // If the user requests it, send a copy using the current language.
-  if ($values['copy']) {
-    drupal_mail('contact', 'page_copy', $from, $language_interface->langcode, $values, $from);
-  }
-
-  // Send an auto-reply if necessary using the current language.
-  if ($values['category']->reply) {
-    drupal_mail('contact', 'page_autoreply', $from, $language_interface->langcode, $values, $to);
-  }
-
-  drupal_container()->get('flood')->register('contact', config('contact.settings')->get('flood.interval'));
-  watchdog('mail', '%sender-name (@sender-from) sent an e-mail regarding %category.', array('%sender-name' => $values['name'], '@sender-from' => $from, '%category' => $values['category']->label()));
-
-  // Jump to home page rather than back to contact page to avoid
-  // contradictory messages if flood control has been activated.
-  drupal_set_message(t('Your message has been sent.'));
-  $form_state['redirect'] = '';
+  // @todo Add 'uid' or 'sender' property and set property here?
+  $message = entity_create('contact_message', array(
+    'category' => $category->id(),
+  ));
+  return entity_get_form($message);
 }
 
 /**
@@ -188,126 +65,39 @@ function contact_site_form_submit($form, &$form_state) {
  *
  * @ingroup forms
  */
-function contact_personal_form($form, &$form_state, $recipient) {
+function contact_personal_page($recipient) {
   global $user;
 
   // Check if flood control has been activated for sending e-mails.
-  $config = config('contact.settings');
-  $limit = $config->get('flood.limit');
-  $interval = $config->get('flood.interval');
-  if (!drupal_container()->get('flood')->isAllowed('contact', $limit, $interval) && !user_access('administer contact forms') && !user_access('administer users')) {
-    drupal_set_message(t("You cannot send more than %limit messages in @interval. Try again later.", array('%limit' => $limit, '@interval' => format_interval($interval))), 'error');
-    throw new AccessDeniedHttpException();
+  if (!user_access('administer contact forms') && !user_access('administer users')) {
+    contact_flood_control();
   }
 
   drupal_set_title(t('Contact @username', array('@username' => user_format_name($recipient))), PASS_THROUGH);
 
-  if (!$user->uid) {
-    $form['#attached']['library'][] = array('system', 'jquery.cookie');
-    $form['#attributes']['class'][] = 'user-info-from-cookie';
-  }
-
-  $form['#attributes']['class'][] = 'contact-form';
-  $form['recipient'] = array(
-    '#type' => 'value',
-    '#value' => $recipient,
-  );
-  $form['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Your name'),
-    '#maxlength' => 255,
-    '#default_value' => $user->uid ? user_format_name($user) : '',
-    '#required' => TRUE,
-  );
-  $form['mail'] = array(
-    '#type' => 'email',
-    '#title' => t('Your e-mail address'),
-    '#default_value' => $user->uid ? $user->mail : '',
-    '#required' => TRUE,
-  );
-  // Do not allow authenticated users to alter the name or e-mail values to
-  // prevent the impersonation of other users.
-  if ($user->uid) {
-    // Hide the original name and e-mail address fields and display read-only
-    // versions in their place.
-    $form['name']['#access'] = $form['mail']['#access'] = FALSE;
-    $form['name_display'] = array(
-      '#type' => 'item',
-      '#title' => t('Your name'),
-      '#markup' => check_plain($form['name']['#default_value']),
-    );
-    $form['mail_display'] = array(
-      '#type' => 'item',
-      '#title' => t('Your e-mail address'),
-      '#markup' => check_plain($form['mail']['#default_value']),
-    );
-  }
-  $form['to'] = array(
-    '#type' => 'item',
-    '#title' => t('To'),
-    '#markup' => theme('username', array('account' => $recipient)),
-  );
-  $form['subject'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Subject'),
-    '#maxlength' => 50,
-    '#required' => TRUE,
-  );
-  $form['message'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Message'),
-    '#rows' => 15,
-    '#required' => TRUE,
-  );
-  // Do not allow anonymous users to send themselves a copy
-  // because it can be abused to spam people.
-  $form['copy'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Send yourself a copy.'),
-    '#access' => $user->uid,
-  );
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Send message'),
-  );
-  return $form;
+  $message = entity_create('contact_message', array(
+    'recipient' => $recipient,
+  ));
+  return entity_get_form($message);
 }
 
 /**
- * Form submission handler for contact_personal_form().
+ * Throws an exception if the current user is not allowed to submit a contact form.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+ *
+ * @see contact_site_page()
+ * @see contact_personal_page()
  */
-function contact_personal_form_submit($form, &$form_state) {
-  global $user;
-  $language_interface = language(LANGUAGE_TYPE_INTERFACE);
-
-  $values = $form_state['values'];
-  $values['sender'] = $user;
-  $values['sender']->name = $values['name'];
-  $values['sender']->mail = $values['mail'];
-
-  if (!$user->uid) {
-    $values['sender']->name .= ' (' . t('not verified') . ')';
-    // Save the anonymous user information to a cookie for reuse.
-    user_cookie_save(array_intersect_key($values, array_flip(array('name', 'mail'))));
-  }
-
-  // Get the to and from e-mail addresses.
-  $to = $values['recipient']->mail;
-  $from = $values['sender']->mail;
-
-  // Send the e-mail in the requested user language.
-  drupal_mail('contact', 'user_mail', $to, user_preferred_langcode($values['recipient']), $values, $from);
-
-  // Send a copy if requested, using current page language.
-  if ($values['copy']) {
-    drupal_mail('contact', 'user_copy', $from, $language_interface->langcode, $values, $from);
+function contact_flood_control() {
+  $config = config('contact.settings');
+  $limit = $config->get('flood.limit');
+  $interval = $config->get('flood.interval');
+  if (!drupal_container()->get('flood')->isAllowed('contact', $limit, $interval)) {
+    drupal_set_message(t("You cannot send more than %limit messages in @interval. Try again later.", array(
+      '%limit' => $limit,
+      '@interval' => format_interval($interval),
+    )), 'error');
+    throw new AccessDeniedHttpException();
   }
-
-  drupal_container()->get('flood')->register('contact', config('contact.settings')->get('flood.interval'));
-  watchdog('mail', '%sender-name (@sender-from) sent %recipient-name an e-mail.', array('%sender-name' => $values['name'], '@sender-from' => $from, '%recipient-name' => $values['recipient']->name));
-
-  // Jump to the contacted user's profile page.
-  drupal_set_message(t('Your message has been sent.'));
-  $form_state['redirect'] = user_access('access user profiles') ? 'user/' . $values['recipient']->uid : '';
 }
diff --git a/core/modules/contact/lib/Drupal/contact/CategoryListController.php b/core/modules/contact/lib/Drupal/contact/CategoryListController.php
index 617fa93..a3ec886 100644
--- a/core/modules/contact/lib/Drupal/contact/CategoryListController.php
+++ b/core/modules/contact/lib/Drupal/contact/CategoryListController.php
@@ -15,6 +15,29 @@
 class CategoryListController extends ConfigEntityListController {
 
   /**
+   * Overrides Drupal\Core\Entity\EntityListController::getOperations().
+   */
+  public function getOperations(EntityInterface $entity) {
+    $operations = parent::getOperations($entity);
+    if (module_exists('field_ui')) {
+      $uri = $entity->uri();
+      $operations['manage-fields'] = array(
+        'title' => t('Manage fields'),
+        'href' => $uri['path'] . '/fields',
+        'options' => $uri['options'],
+        'weight' => 11,
+      );
+      $operations['manage-display'] = array(
+        'title' => t('Manage display'),
+        'href' => $uri['path'] . '/display',
+        'options' => $uri['options'],
+        'weight' => 12,
+      );
+    }
+    return $operations;
+  }
+
+  /**
    * Overrides Drupal\Core\Entity\EntityListController::buildHeader().
    */
   public function buildHeader() {
diff --git a/core/modules/contact/lib/Drupal/contact/CategoryStorageController.php b/core/modules/contact/lib/Drupal/contact/CategoryStorageController.php
new file mode 100644
index 0000000..bc31a24
--- /dev/null
+++ b/core/modules/contact/lib/Drupal/contact/CategoryStorageController.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\contact\CategoryStorageController.
+ */
+
+namespace Drupal\contact;
+
+use Drupal\Core\Config\Entity\ConfigStorageController;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Controller class for contact categories.
+ */
+class CategoryStorageController extends ConfigStorageController {
+
+  /**
+   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
+   */
+  protected function postSave(EntityInterface $entity, $update) {
+    parent::postSave($entity, $update);
+
+    if (!$update) {
+      field_attach_create_bundle('contact_message', $entity->id());
+    }
+    elseif ($entity->original->id() != $entity->id()) {
+      field_attach_rename_bundle('contact_message', $entity->original->id(), $entity->id());
+    }
+  }
+
+  /**
+   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
+   */
+  protected function postDelete($entities) {
+    parent::postDelete($entities);
+
+    foreach ($entities as $entity) {
+      field_attach_delete_bundle('contact_message', $entity->id());
+    }
+  }
+
+}
diff --git a/core/modules/contact/lib/Drupal/contact/MessageFormController.php b/core/modules/contact/lib/Drupal/contact/MessageFormController.php
new file mode 100644
index 0000000..b845ae0
--- /dev/null
+++ b/core/modules/contact/lib/Drupal/contact/MessageFormController.php
@@ -0,0 +1,252 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\contact\MessageFormController.
+ */
+
+namespace Drupal\contact;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityFormController;
+use Drupal\user\User;
+
+/**
+ * Form controller for contact message forms.
+ */
+class MessageFormController extends EntityFormController {
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state, EntityInterface $message) {
+    global $user;
+    $form = parent::form($form, $form_state, $message);
+    $form['#attributes']['class'][] = 'contact-form';
+
+    if (!empty($message->preview)) {
+      $form['preview'] = array(
+        '#theme_wrappers' => array('container__preview'),
+        '#attributes' => array('class' => array('preview')),
+      );
+      $form['preview']['message'] = entity_view($message, 'preview');
+
+      $form['preview']['mail'] = entity_view($message, 'mail');
+      $form['preview']['mail']['#prefix'] = '<pre>';
+      $form['preview']['mail']['#suffix'] = '</pre>';
+    }
+
+    $form['name'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Your name'),
+      '#maxlength' => 255,
+      '#required' => TRUE,
+    );
+    $form['mail'] = array(
+      '#type' => 'email',
+      '#title' => t('Your e-mail address'),
+      '#required' => TRUE,
+    );
+    if (!$user->uid) {
+      $form['#attached']['library'][] = array('system', 'jquery.cookie');
+      $form['#attributes']['class'][] = 'user-info-from-cookie';
+    }
+    // Do not allow authenticated users to alter the name or e-mail values to
+    // prevent the impersonation of other users.
+    else {
+      $form['name']['#type'] = 'item';
+      $form['name']['#value'] = $user->name;
+      $form['name']['#required'] = FALSE;
+      $form['name']['#markup'] = check_plain(user_format_name($user));
+      $form['mail']['#type'] = 'item';
+      $form['mail']['#value'] = $user->mail;
+      $form['mail']['#required'] = FALSE;
+      $form['mail']['#markup'] = check_plain($user->mail);
+    }
+
+    // If there is a recipient, then this is a user contact form and there is no
+    // category.
+    if ($message->recipient instanceof User) {
+      $form['recipient'] = array(
+        '#type' => 'item',
+        '#title' => t('To'),
+        '#value' => $message->recipient,
+        'name' => array(
+          '#theme' => 'username',
+          '#account' => $message->recipient,
+        ),
+      );
+    }
+    // The site-wide contact form always passes a category and defaults to the
+    // configured default category.
+    elseif (!empty($message->category)) {
+      $form['category'] = array(
+        '#type' => 'value',
+        '#value' => $message->category,
+      );
+    }
+    // There may be other callers, which request this form without a category.
+    // In this case, the Message entity automatically sets 'fieldable' to FALSE.
+    // @todo Consider to remove this fallback.
+    else {
+      // Prepare array for select options.
+      $categories = entity_load_multiple('contact_category');
+      uasort($categories, 'Drupal\Core\Config\Entity\ConfigEntityBase::sort');
+      $options = array();
+      foreach ($categories as $category) {
+        $options[$category->id()] = $category->label();
+      }
+      // If there is more than one category available and no default category has
+      // been selected, prepend a default placeholder value.
+      $config = config('contact.settings');
+      $default_category = $config->get('default_category');
+      if (!$default_category) {
+        $default_category = !empty($categories) ? key($categories) : NULL;
+      }
+      $form['category'] = array(
+        '#type' => 'select',
+        '#title' => t('Category'),
+        '#default_value' => $default_category,
+        '#options' => $options,
+        '#empty_value' => 0,
+        '#required' => TRUE,
+        '#access' => count($categories) > 1,
+      );
+    }
+
+    $form['subject'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Subject'),
+      '#maxlength' => 100,
+      '#required' => TRUE,
+    );
+    $form['message'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Message'),
+      '#required' => TRUE,
+      '#rows' => 12,
+    );
+
+    // Do not allow anonymous users to send themselves a copy because it can be
+    // abused to spam people.
+    $form['copy'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Send yourself a copy.'),
+      '#access' => $user->uid,
+    );
+    return $form;
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::actions().
+   */
+  public function actions(array $form, array &$form_state) {
+    $elements = parent::actions($form, $form_state);
+    $elements['submit']['#value'] = t('Send message');
+    $elements['delete']['#access'] = FALSE;
+    $elements['preview'] = array(
+      '#value' => t('Preview'),
+      '#validate' => array(
+        array($this, 'validate'),
+      ),
+      '#submit' => array(
+        array($this, 'submit'),
+        array($this, 'preview'),
+      ),
+    );
+    return $elements;
+  }
+
+  public function preview(array $form, array &$form_state) {
+    $message = $this->getEntity($form_state);
+    $message->preview = TRUE;
+    $form_state['rebuild'] = TRUE;
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    global $user;
+    $language_interface = language(LANGUAGE_TYPE_INTERFACE);
+    $message = $this->getEntity($form_state);
+
+    $sender = clone user_load($user->uid);
+    if (!$user->uid) {
+      // At this point, $message->sender contains drupal_anonymous_user(), so we
+      // need to take over the submitted form values.
+      $sender->name = $message->name;
+      $sender->mail = $message->mail;
+      // Save the anonymous user information to a cookie for reuse.
+      user_cookie_save(array('name' => $message->name, 'mail' => $message->mail));
+      // For the e-mail message, clarify that the sender name is not verified; it
+      // could potentially clash with a username on this site.
+      $sender->name = t('!name (not verified)', array('!name' => $message->name));
+    }
+
+    // Build e-mail parameters.
+    $params['contact_message'] = $message;
+    $params['sender'] = $sender;
+
+    // @todo Category should be available in $form_state to hook_form_alter() already.
+    if ($message->category) {
+      $category = entity_load('contact_category', $message->category);
+      $params['contact_category'] = $category;
+
+      // Send to configured recipient(s), using the site's default language.
+      $to = implode(', ', $category->recipients);
+      $recipient_langcode = language_default()->langcode;
+    }
+    elseif ($message->recipient instanceof User) {
+      // Send to the user in the user's preferred language.
+      $to = $message->recipient->mail;
+      $recipient_langcode = user_preferred_langcode($message->recipient);
+    }
+    else {
+      throw new \RuntimeException('Unable to determine message recipient.');
+    }
+
+    // Send e-mail to the recipient(s).
+    drupal_mail('contact', 'page_mail', $to, $recipient_langcode, $params, $sender->mail);
+
+    // If requested, send a copy to the user, using the current language.
+    if ($message->copy) {
+      drupal_mail('contact', 'page_copy', $sender->mail, $language_interface->langcode, $params, $sender->mail);
+    }
+
+    // If configured, send an auto-reply, using the current language.
+    if ($message->category && $category->reply) {
+      // User contact forms do not support an auto-reply message, so this
+      // message always originates from the site.
+      drupal_mail('contact', 'page_autoreply', $sender->mail, $language_interface->langcode, $params);
+    }
+
+    drupal_container()->get('flood')->register('contact', config('contact.settings')->get('flood.interval'));
+    if ($message->category) {
+      watchdog('contact', '%sender-name (@sender-from) sent an e-mail regarding %category.', array(
+        '%sender-name' => $sender->name,
+        '@sender-from' => $sender->mail,
+        '%category' => $category->label(),
+      ));
+    }
+    else {
+      watchdog('contact', '%sender-name (@sender-from) sent %recipient-name an e-mail.', array(
+        '%sender-name' => $sender->name,
+        '@sender-from' => $sender->mail,
+        '%recipient-name' => $message->recipient->name,
+      ));
+    }
+
+    drupal_set_message(t('Your message has been sent.'));
+
+    // To avoid false error messages caused by flood control, redirect away from
+    // the contact form; either to the contacted user account or the front page.
+    if ($message->recipient instanceof User && user_access('access user profiles')) {
+      $uri = $message->recipient->uri();
+      $form_state['redirect'] = array($uri['path'], $uri['options']);
+    }
+    else {
+      $form_state['redirect'] = '';
+    }
+  }
+}
diff --git a/core/modules/contact/lib/Drupal/contact/MessageRenderController.php b/core/modules/contact/lib/Drupal/contact/MessageRenderController.php
new file mode 100644
index 0000000..a2163b9
--- /dev/null
+++ b/core/modules/contact/lib/Drupal/contact/MessageRenderController.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\contact\MessageRenderController.
+ */
+
+namespace Drupal\contact;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityRenderController;
+
+/**
+ * Render controller for contact messages.
+ */
+class MessageRenderController extends EntityRenderController {
+
+  public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
+    $build = parent::view($entity, $view_mode, $langcode);
+
+    if ($view_mode == 'mail') {
+      // Convert field labels into headings, as long as drupal_html_to_text()
+      // fails to convert DIVs correctly.
+      foreach (element_children($build) as $key) {
+        if (isset($build[$key]['#label_display']) && $build[$key]['#label_display'] == 'above') {
+          $build[$key] += array('#prefix' => '');
+          $build[$key]['#prefix'] = $build[$key]['#title'] . ":\n";
+          $build[$key]['#label_display'] = 'hidden';
+        }
+      }
+      $build = array(
+        '#markup' => drupal_html_to_text(drupal_render($build)),
+      );
+    }
+    return $build;
+  }
+
+}
diff --git a/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Category.php b/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Category.php
index c2d456b..0004b00 100644
--- a/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Category.php
+++ b/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Category.php
@@ -16,9 +16,9 @@
  *
  * @Plugin(
  *   id = "contact_category",
- *   label = @Translation("Category"),
+ *   label = @Translation("Contact category"),
  *   module = "contact",
- *   controller_class = "Drupal\Core\Config\Entity\ConfigStorageController",
+ *   controller_class = "Drupal\contact\CategoryStorageController",
  *   list_controller_class = "Drupal\contact\CategoryListController",
  *   form_controller_class = {
  *     "default" = "Drupal\contact\CategoryFormController"
diff --git a/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Message.php b/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Message.php
new file mode 100644
index 0000000..0cb6cff
--- /dev/null
+++ b/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Message.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\contact\Plugin\Core\Entity\Message.
+ */
+
+namespace Drupal\contact\Plugin\Core\Entity;
+
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\Entity;
+
+/**
+ * Defines the contact category entity.
+ *
+ * @Plugin(
+ *   id = "contact_message",
+ *   label = @Translation("Contact message"),
+ *   module = "contact",
+ *   list_controller_class = "Drupal\contact\MessageListController",
+ *   form_controller_class = {
+ *     "default" = "Drupal\contact\MessageFormController"
+ *   },
+ *   render_controller_class = "Drupal\contact\MessageRenderController",
+ *   entity_keys = {
+ *     "bundle" = "category"
+ *   },
+ *   fieldable = TRUE,
+ *   bundle_keys = {
+ *     "bundle" = "id"
+ *   }
+ * )
+ */
+class Message extends Entity {
+
+  /**
+   * The contact category ID of this message.
+   *
+   * @var string
+   */
+  public $category;
+
+  /**
+   * The sender's name.
+   *
+   * @var string
+   */
+  public $name;
+
+  /**
+   * The sender's e-mail address.
+   *
+   * @var string
+   */
+  public $mail;
+
+  /**
+   * The message recipient.
+   *
+   * Only applies to user contact forms.
+   *
+   * @todo This technically does not belong here, since it only belongs to user
+   *   contact forms, whereas the recipient(s) of site contact form are
+   *   configured per Category. However, while a wildcard contact.category.user
+   *   would be possible, it would require a entity storage controller override
+   *   in order to dynamically replace the Category::$recipients with the user
+   *   account's e-mail address upon Entity::create(). Doing so would inherently
+   *   break the vertical extensibility of entity storage controllers (as we
+   *   would have to extend from DatabaseStorageController or some other
+   *   hard-coded base class) and there is no horizontal extensibility in
+   *   Drupal, nor support for mixins in PHP yet.
+   *
+   * @var \Drupal\user\User
+   */
+  public $recipient;
+
+  /**
+   * The message subject.
+   *
+   * @var string
+   */
+  public $subject;
+
+  /**
+   * The message text.
+   *
+   * @var string
+   */
+  public $message;
+
+  /**
+   * Whether to send a copy of the message to the sender.
+   *
+   * @var bool
+   */
+  public $copy;
+
+  /**
+   * Overrides Drupal\Core\Entity\Entity::id().
+   */
+  public function id() {
+    return NULL;
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\Entity::bundle().
+   */
+  public function bundle() {
+    return $this->category;
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\Entity::entityInfo().
+   */
+  public function entityInfo() {
+    // If this entity was created without category, we do not have a bundle and
+    // we need to prevent EntityFormController from calling into Field Attach
+    // functions, since those will bail out with a fatal error due to the bundle
+    // being NULL.
+    $info = entity_get_info($this->entityType);
+    if (!isset($this->category)) {
+      $info['fieldable'] = FALSE;
+    }
+    return $info;
+  }
+
+}
diff --git a/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php b/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
index 9692698..8ca0047 100644
--- a/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
+++ b/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
@@ -199,7 +199,7 @@ function testAutoReply() {
     $this->submitContact($this->randomName(16), $email, $subject, 'foo', $this->randomString(128));
 
     // We are testing the auto-reply, so there should be one e-mail going to the sender.
-    $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'foo@example.com'));
+    $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email));
     $this->assertEqual(count($captured_emails), 1);
     $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($foo_autoreply));
 
@@ -208,14 +208,14 @@ function testAutoReply() {
     $this->submitContact($this->randomName(16), $email, $this->randomString(64), 'bar', $this->randomString(128));
 
     // Auto-reply for category 'bar' should result in one auto-reply e-mail to the sender.
-    $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'bar@example.com'));
+    $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email));
     $this->assertEqual(count($captured_emails), 1);
     $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($bar_autoreply));
 
     // Verify that no auto-reply is sent when the auto-reply field is left blank.
     $email = $this->randomName(32) . '@example.com';
     $this->submitContact($this->randomName(16), $email, $this->randomString(64), 'no_autoreply', $this->randomString(128));
-    $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'no_autoreply@example.com'));
+    $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email));
     $this->assertEqual(count($captured_emails), 0);
   }
 
diff --git a/core/modules/field/field.module b/core/modules/field/field.module
index bb948ac..b1c6ab1 100644
--- a/core/modules/field/field.module
+++ b/core/modules/field/field.module
@@ -1019,6 +1019,11 @@ function field_has_data($field) {
   $columns = array_keys($field['columns']);
   $factory = drupal_container()->get('entity.query');
   foreach ($field['bundles'] as $entity_type => $bundle) {
+    // Entities without a base table (which are not stored) cannot be queried.
+    $entity_info = entity_get_info($entity_type);
+    if (!isset($entity_info['base_table'])) {
+      continue;
+    }
     $query = $factory->get($entity_type);
     $group = $query->orConditionGroup();
     foreach ($columns as $column) {
@@ -1278,7 +1283,9 @@ function theme_field($variables) {
 function _field_create_entity_from_ids($ids) {
   $id_properties = array();
   $info = entity_get_info($ids->entity_type);
-  $id_properties[$info['entity_keys']['id']] = $ids->entity_id;
+  if (isset($info['entity_keys']['id'])) {
+    $id_properties[$info['entity_keys']['id']] = $ids->entity_id;
+  }
   if (!empty($info['entity_keys']['revision']) && isset($ids->revision_id)) {
     $id_properties[$info['entity_keys']['revision']] = $ids->revision_id;
   }
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 950fee6..4b34395 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -503,6 +503,11 @@ function system_element_info() {
 
   // Form structure.
   $types['item'] = array(
+    // There are at least two use-cases in core, Comment and Contact module,
+    // which need to dynamically switch between textfields and items for
+    // anonymous and registered users, so submitted form values for #type 'item'
+    // should be processed by Form API.
+    '#input' => TRUE,
     '#markup' => '',
     '#theme_wrappers' => array('form_element'),
   );
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php b/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php
index 8b5db87..948053f 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php
@@ -14,15 +14,6 @@
  */
 class UserCancelTest extends WebTestBase {
 
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('comment');
-
-  protected $profile = 'standard';
-
   public static function getInfo() {
     return array(
       'name' => 'Cancel account',
@@ -31,6 +22,12 @@ public static function getInfo() {
     );
   }
 
+  function setUp() {
+    parent::setUp();
+
+    $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
+  }
+
   /**
    * Attempt to cancel account without permission.
    */
@@ -278,6 +275,8 @@ function testUserAnonymize() {
    */
   function testUserDelete() {
     config('user.settings')->set('cancel_method', 'user_cancel_delete')->save();
+    module_enable(array('comment'));
+    $this->resetAll();
 
     // Create a user.
     $account = $this->drupalCreateUser(array('cancel account', 'post comments', 'skip comment approval'));
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index bab307b..7abaa60 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -346,23 +346,20 @@ function user_admin_settings($form, &$form_state) {
     '#default_value' => $config->get('verify_mail'),
     '#description' => t('New users will be required to validate their e-mail address prior to logging into the site, and will be assigned a system-generated password. With this setting disabled, users will be logged in immediately upon registering, and may select their own passwords during registration.')
   );
-  module_load_include('inc', 'user', 'user.pages');
+  form_load_include($form_state, 'inc', 'user', 'user.pages');
   $form['registration_cancellation']['user_cancel_method'] = array(
-    '#type' => 'item',
+    '#type' => 'radios',
     '#title' => t('When cancelling a user account'),
     '#default_value' => $config->get('cancel_method'),
     '#description' => t('Users with the %select-cancel-method or %administer-users <a href="@permissions-url">permissions</a> can override this default method.', array('%select-cancel-method' => t('Select method for cancelling account'), '%administer-users' => t('Administer users'), '@permissions-url' => url('admin/people/permissions'))),
   );
   $form['registration_cancellation']['user_cancel_method'] += user_cancel_methods();
-  foreach (element_children($form['registration_cancellation']['user_cancel_method']) as $element) {
-    // Remove all account cancellation methods that have #access defined, as
-    // those cannot be configured as default method.
-    if (isset($form['registration_cancellation']['user_cancel_method'][$element]['#access'])) {
-      $form['registration_cancellation']['user_cancel_method'][$element]['#access'] = FALSE;
-    }
-    // Remove the description (only displayed on the confirmation form).
-    else {
-      unset($form['registration_cancellation']['user_cancel_method'][$element]['#description']);
+  foreach (element_children($form['registration_cancellation']['user_cancel_method']) as $key) {
+    // All account cancellation methods that specify #access cannot be
+    // configured as default method.
+    // @see hook_user_cancel_methods_alter()
+    if (isset($form['registration_cancellation']['user_cancel_method'][$key]['#access'])) {
+      $form['registration_cancellation']['user_cancel_method'][$key]['#access'] = FALSE;
     }
   }
 
diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php
index 8714d16..3d87c08 100644
--- a/core/modules/user/user.api.php
+++ b/core/modules/user/user.api.php
@@ -143,8 +143,8 @@ function hook_user_cancel($edit, $account, $method) {
  *   description is NOT used for the radio button, but instead should provide
  *   additional explanation to the user seeking to cancel their account.
  * - access: (optional) A boolean value indicating whether the user can access
- *   a method. If #access is defined, the method cannot be configured as default
- *   method.
+ *   a method. If 'access' is defined, the method cannot be configured as
+ *   default method.
  *
  * @param $methods
  *   An array containing user account cancellation methods, keyed by method id.
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index ac3e225..2af80ac 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2560,16 +2560,12 @@ function user_multiple_cancel_confirm($form, &$form_state) {
 
   $form['operation'] = array('#type' => 'hidden', '#value' => 'cancel');
 
-  module_load_include('inc', 'user', 'user.pages');
+  form_load_include($form_state, 'inc', 'user', 'user.pages');
   $form['user_cancel_method'] = array(
-    '#type' => 'item',
+    '#type' => 'radios',
     '#title' => t('When cancelling these accounts'),
   );
   $form['user_cancel_method'] += user_cancel_methods();
-  // Remove method descriptions.
-  foreach (element_children($form['user_cancel_method']) as $element) {
-    unset($form['user_cancel_method'][$element]['#description']);
-  }
 
   // Allow to send the account cancellation confirmation mail.
   $form['user_cancel_confirm'] = array(
diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc
index 20af863..39273d0 100644
--- a/core/modules/user/user.pages.inc
+++ b/core/modules/user/user.pages.inc
@@ -232,7 +232,7 @@ function user_cancel_confirm_form($form, &$form_state, $account) {
   $admin_access = user_access('administer users');
   $can_select_method = $admin_access || user_access('select account cancellation method');
   $form['user_cancel_method'] = array(
-    '#type' => 'item',
+    '#type' => 'radios',
     '#title' => ($account->uid == $user->uid ? t('When cancelling your account') : t('When cancelling the account')),
     '#access' => $can_select_method,
   );
@@ -266,22 +266,15 @@ function user_cancel_confirm_form($form, &$form_state, $account) {
   else {
     $question = t('Are you sure you want to cancel the account %name?', array('%name' => $account->name));
   }
-  $description = '';
+  $default_method = config('user.settings')->get('cancel_method');
+  $description = NULL;
   if ($can_select_method) {
     $description = t('Select the method to cancel the account above.');
-    foreach (element_children($form['user_cancel_method']) as $element) {
-      unset($form['user_cancel_method'][$element]['#description']);
-    }
   }
-  else {
-    // The radio button #description is used as description for the confirmation
-    // form.
-    foreach (element_children($form['user_cancel_method']) as $element) {
-      if ($form['user_cancel_method'][$element]['#default_value'] == $form['user_cancel_method'][$element]['#return_value']) {
-        $description = $form['user_cancel_method'][$element]['#description'];
-      }
-      unset($form['user_cancel_method'][$element]['#description']);
-    }
+  // Options supplied via user_cancel_methods() can have a custom
+  // #confirm_description property for the confirmation form description.
+  elseif (isset($form['user_cancel_method'][$default_method]['#confirm_description'])) {
+    $description = $form['user_cancel_method'][$default_method]['#confirm_description'];
   }
 
   // Always provide entity id in the same form key as in the entity edit form.
@@ -363,15 +356,22 @@ function user_cancel_methods() {
   drupal_alter('user_cancel_methods', $methods);
 
   // Turn all methods into real form elements.
+  $default_method = config('user.settings')->get('cancel_method');
+  $form = array(
+    '#options' => array(),
+    '#default_value' => $default_method,
+  );
   foreach ($methods as $name => $method) {
-    $form[$name] = array(
-      '#type' => 'radio',
-      '#title' => $method['title'],
-      '#description' => (isset($method['description']) ? $method['description'] : NULL),
-      '#return_value' => $name,
-      '#default_value' => config('user.settings')->get('cancel_method'),
-      '#parents' => array('user_cancel_method'),
-    );
+    $form['#options'][$name] = $method['title'];
+    // Add the description for the confirmation form. This description is never
+    // shown for the cancel method option, only on the confirmation form.
+    // Therefore, we use a custom #confirm_description property.
+    if (isset($method['description'])) {
+      $form[$name]['#confirm_description'] = $method['description'];
+    }
+    if (isset($method['access'])) {
+      $form[$name]['#access'] = $method['access'];
+    }
   }
   return $form;
 }
