diff --git a/core/core.services.yml b/core/core.services.yml
index 9a36139..afcd12e 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -154,6 +154,9 @@ services:
   plugin.manager.archiver:
     class: Drupal\Core\Archiver\ArchiverManager
     arguments: ['@container.namespaces']
+  plugin.manager.operation:
+    class: Drupal\Core\Operation\OperationManager
+    arguments: ['@container.namespaces']
   request:
     class: Symfony\Component\HttpFoundation\Request
   event_dispatcher:
diff --git a/core/lib/Drupal/Core/Annotation/Operation.php b/core/lib/Drupal/Core/Annotation/Operation.php
new file mode 100644
index 0000000..a7cf69d
--- /dev/null
+++ b/core/lib/Drupal/Core/Annotation/Operation.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Annotation\Operation.
+ */
+
+namespace Drupal\Core\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines an Operation annotation object.
+ *
+ * This hook enables modules to inject custom operations into the mass
+ * operations dropdown found at admin/content, by associating a callback
+ * function with the operation, which is called when the form is submitted. The
+ * callback function receives one initial argument, which is an array of the
+ * checked nodes.
+ *
+ * @return
+ *   An array of operations. Each operation is an associative array that may
+ *   contain the following key-value pairs:
+ *   - label: (required) The label for the operation, displayed in the dropdown
+ *     menu.
+ *   - callback: (required) The function to call for the operation.
+ *   - callback arguments: (optional) An array of additional arguments to pass
+ *     to the callback function.
+ *
+ * @see \Drupal\Core\Operation\OperationInterface
+ * @see \Drupal\Core\Operation\OperationManager
+ *
+ * @Annotation
+ */
+class Operation extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the operation plugin.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * A URL to redirect to after processing the operation.
+   *
+   * @var string (optional)
+   */
+  public $redirect = '';
+
+  /**
+   * The entity type the operation can apply to.
+   *
+   * @todo Replace with \Drupal\Core\Plugin\Context\Context.
+   *
+   * @var string
+   */
+  public $type = '';
+
+}
diff --git a/core/lib/Drupal/Core/Operation/ConfigurableOperationInterface.php b/core/lib/Drupal/Core/Operation/ConfigurableOperationInterface.php
new file mode 100644
index 0000000..581760a
--- /dev/null
+++ b/core/lib/Drupal/Core/Operation/ConfigurableOperationInterface.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Operation\ConfigurableOperationInterface.
+ */
+
+namespace Drupal\Core\Operation;
+
+/**
+ * Provides an interface for an Operation plugin.
+ *
+ * @see \Drupal\Core\Annotation\Operation
+ * @see \Drupal\Core\Operation\OperationManager
+ */
+interface ConfigurableOperationInterface extends OperationInterface {
+
+  /**
+   * Form constructor.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   *
+   * @return array
+   *   The form structure.
+   */
+  public function form(array $form, array &$form_state);
+
+  /**
+   * Form validation handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   */
+  public function validate(array &$form, array &$form_state);
+
+  /**
+   * Form submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   */
+  public function submit(array &$form, array &$form_state);
+
+}
diff --git a/core/lib/Drupal/Core/Operation/OperationBag.php b/core/lib/Drupal/Core/Operation/OperationBag.php
new file mode 100644
index 0000000..4a85bbb
--- /dev/null
+++ b/core/lib/Drupal/Core/Operation/OperationBag.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Operation\OperationBag.
+ */
+
+namespace Drupal\Core\Operation;
+
+use Drupal\Component\Plugin\PluginBag;
+use Drupal\Component\Plugin\PluginManagerInterface;
+
+/**
+ * Provides a container for lazily loading Operation plugins.
+ */
+class OperationBag extends PluginBag {
+
+  /**
+   * The manager used to instantiate the plugins.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * Constructs a new OperationBag object.
+   *
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $manager
+   *   The manager to be used for instantiating plugins.
+   * @param array $instance_ids
+   *   The ids of the plugin instances with which we are dealing.
+   * @param array $configuration
+   *   An array of configuration.
+   */
+  public function __construct(PluginManagerInterface $manager, array $instance_ids, array $configuration) {
+    $this->manager = $manager;
+    $this->instanceIDs = drupal_map_assoc($instance_ids);
+    $this->configuration = $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initializePlugin($instance_id) {
+    if (isset($this->pluginInstances[$instance_id])) {
+      return;
+    }
+
+    $this->pluginInstances[$instance_id] = $this->manager->createInstance($instance_id, $this->configuration);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Operation/OperationBase.php b/core/lib/Drupal/Core/Operation/OperationBase.php
new file mode 100644
index 0000000..26dbef7
--- /dev/null
+++ b/core/lib/Drupal/Core/Operation/OperationBase.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Operation\OperationBase.
+ */
+
+namespace Drupal\Core\Operation;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginBase;
+
+/**
+ * Provides a base implementation for an Operation plugin.
+ */
+abstract class OperationBase extends ContainerFactoryPluginBase implements OperationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->configuration += $this->getDefaultConfiguration();
+  }
+
+  /**
+   * Returns default configuration for this operation.
+   *
+   * @return array
+   */
+  protected function getDefaultConfiguration() {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    foreach ($entities as $entity) {
+      $this->executeSingle($entity);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($object) {
+    $this->execute(array($object));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Operation/OperationInterface.php b/core/lib/Drupal/Core/Operation/OperationInterface.php
new file mode 100644
index 0000000..7092181
--- /dev/null
+++ b/core/lib/Drupal/Core/Operation/OperationInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Operation\OperationInterface.
+ */
+
+namespace Drupal\Core\Operation;
+
+/**
+ * Provides an interface for an Operation plugin.
+ *
+ * @todo Extend \Drupal\Core\Executable\ExecutableInterface after
+ *   http://drupal.org/node/1920822 is resolved.
+ *
+ * @see \Drupal\Core\Annotation\Operation
+ * @see \Drupal\Core\Operation\OperationManager
+ */
+interface OperationInterface {
+
+  /**
+   * Executes the plugin for an array of objects.
+   *
+   * @var array $objects
+   *   An array of entities.
+   */
+  public function execute(array $objects);
+
+  /**
+   * Executes the plugin for a single object.
+   *
+   * @var mixed $object
+   */
+  public function executeSingle($object);
+
+  /**
+   * Returns this plugin's configuration.
+   *
+   * @return array
+   *   An array of this operation plugin's configuration.
+   */
+  public function getConfiguration();
+
+}
diff --git a/core/lib/Drupal/Core/Operation/OperationManager.php b/core/lib/Drupal/Core/Operation/OperationManager.php
new file mode 100644
index 0000000..76411ca
--- /dev/null
+++ b/core/lib/Drupal/Core/Operation/OperationManager.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Operation\OperationManager.
+ */
+
+namespace Drupal\Core\Operation;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Plugin\Discovery\AlterDecorator;
+use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Factory\ContainerFactory;
+
+/**
+ * Provides an Operation plugin manager.
+ *
+ * @see \Drupal\Core\Annotation\Operation
+ * @see \Drupal\Core\Operation\OperationInterface
+ */
+class OperationManager extends PluginManagerBase {
+
+  /**
+   * Constructs a OperationManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   */
+  public function __construct(\Traversable $namespaces) {
+    $this->discovery = new AnnotatedClassDiscovery('Operation', $namespaces, array(), 'Drupal\Core\Annotation\Operation');
+    $this->discovery = new AlterDecorator($this->discovery, 'operation_info');
+
+    $this->factory = new ContainerFactory($this);
+  }
+
+  /**
+   * Gets the plugin definitions for this entity type.
+   *
+   * @param string $type
+   *   The entity type name.
+   *
+   * @return array
+   *   An array of plugin definitions for this entity type.
+   */
+  public function getDefinitionsByType($type) {
+    return array_filter($this->getDefinitions(), function ($definition) use ($type) {
+      return $definition['type'] === $type;
+    });
+  }
+
+}
diff --git a/core/modules/action/action.admin.inc b/core/modules/action/action.admin.inc
deleted file mode 100644
index 89202c3..0000000
--- a/core/modules/action/action.admin.inc
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Admin page callbacks for the Action module.
- */
-
-/**
- * Post-deletion operations for deleting action orphans.
- *
- * @param $orphaned
- *   An array of orphaned actions.
- */
-function action_admin_delete_orphans_post($orphaned) {
-  foreach ($orphaned as $callback) {
-    drupal_set_message(t("Deleted orphaned action (%action).", array('%action' => $callback)));
-  }
-}
diff --git a/core/modules/action/action.api.php b/core/modules/action/action.api.php
index 8e92c62..911c9d0 100644
--- a/core/modules/action/action.api.php
+++ b/core/modules/action/action.api.php
@@ -6,84 +6,6 @@
  */
 
 /**
- * Declares information about actions.
- *
- * Any module can define actions, and then call actions_do() to make those
- * actions happen in response to events.
- *
- * An action consists of two or three parts:
- * - an action definition (returned by this hook)
- * - a function which performs the action (which by convention is named
- *   MODULE_description-of-function_action)
- * - an optional form definition function that defines a configuration form
- *   (which has the name of the action function with '_form' appended to it.)
- *
- * The action function takes two to four arguments, which come from the input
- * arguments to actions_do().
- *
- * @return
- *   An associative array of action descriptions. The keys of the array
- *   are the names of the action functions, and each corresponding value
- *   is an associative array with the following key-value pairs:
- *   - 'type': The type of object this action acts upon. Core actions have types
- *     'node', 'user', 'comment', and 'system'.
- *   - 'label': The human-readable name of the action, which should be passed
- *     through the t() function for translation.
- *   - 'configurable': If FALSE, then the action doesn't require any extra
- *     configuration. If TRUE, then your module must define a form function with
- *     the same name as the action function with '_form' appended (e.g., the
- *     form for 'node_assign_owner_action' is 'node_assign_owner_action_form'.)
- *     This function takes $context as its only parameter, and is paired with
- *     the usual _submit function, and possibly a _validate function.
- *   - 'triggers': An array of the events (that is, hooks) that can trigger this
- *     action. For example: array('node_insert', 'user_update'). You can also
- *     declare support for any trigger by returning array('any') for this value.
- *   - 'behavior': (optional) A machine-readable array of behaviors of this
- *     action, used to signal additionally required actions that may need to be
- *     triggered. Modules that are processing actions should take special care
- *     for the "presave" hook, in which case a dependent "save" action should
- *     NOT be invoked.
- *
- * @ingroup actions
- */
-function hook_action_info() {
-  return array(
-    'comment_unpublish_action' => array(
-      'type' => 'comment',
-      'label' => t('Unpublish comment'),
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
-    ),
-    'comment_unpublish_by_keyword_action' => array(
-      'type' => 'comment',
-      'label' => t('Unpublish comment containing keyword(s)'),
-      'configurable' => TRUE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
-    ),
-    'comment_save_action' => array(
-      'type' => 'comment',
-      'label' => t('Save comment'),
-      'configurable' => FALSE,
-      'triggers' => array('comment_insert', 'comment_update'),
-    ),
-  );
-}
-
-/**
- * Alters the actions declared by another module.
- *
- * Called by action_list() to allow modules to alter the return values from
- * implementations of hook_action_info().
- *
- * @ingroup actions
- */
-function hook_action_info_alter(&$actions) {
-  $actions['node_unpublish_action']['label'] = t('Unpublish and remove from public view.');
-}
-
-/**
  * Executes code after an action is deleted.
  *
  * @param $aid
diff --git a/core/modules/action/action.install b/core/modules/action/action.install
deleted file mode 100644
index 4e500fd..0000000
--- a/core/modules/action/action.install
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-/**
- * @file
- * Install, update and uninstall functions for the Actions module.
- */
-
-/**
- * Implements hook_schema().
- */
-function action_schema() {
-  // 'action' is a reserved SQL keyword.
-  $schema['actions'] = array(
-    'description' => 'Stores action information.',
-    'fields' => array(
-      'aid' => array(
-        'description' => 'Primary Key: Unique action ID.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '0',
-      ),
-      'type' => array(
-        'description' => 'The object that that action acts on (node, user, comment, system or custom types.)',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'callback' => array(
-        'description' => 'The callback function that executes when the action runs.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'parameters' => array(
-        'description' => 'Parameters to be passed to the callback function.',
-        'type' => 'blob',
-        'not null' => TRUE,
-        'size' => 'big',
-      ),
-      'label' => array(
-        'description' => 'Label of the action.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '0',
-      ),
-    ),
-    'primary key' => array('aid'),
-  );
-  return $schema;
-}
diff --git a/core/modules/action/action.module b/core/modules/action/action.module
index 0cd4397..b139eed 100644
--- a/core/modules/action/action.module
+++ b/core/modules/action/action.module
@@ -5,14 +5,11 @@
  * This is the Actions module for executing stored actions.
  */
 
-use Drupal\Component\Utility\Crypt;
-
 /**
  * @defgroup actions Actions
  * @{
  * Functions that perform an action on a certain system object.
  *
- * Action functions are declared by modules by implementing hook_action_info().
  * Modules can cause action functions to run by calling actions_do().
  *
  * Each action function takes two to four arguments:
@@ -69,12 +66,17 @@ function action_menu() {
     'description' => 'Manage the actions defined for your site.',
     'type' => MENU_DEFAULT_LOCAL_TASK,
   );
+  $items['admin/config/system/actions/add'] = array(
+    'title' => 'Create an advanced action',
+    'type' => MENU_VISIBLE_IN_BREADCRUMB,
+    'route_name' => 'action_admin_add',
+  );
   $items['admin/config/system/actions/configure'] = array(
     'title' => 'Configure an advanced action',
     'type' => MENU_VISIBLE_IN_BREADCRUMB,
     'route_name' => 'action_admin_configure',
   );
-  $items['admin/config/system/actions/delete/%action'] = array(
+  $items['admin/config/system/actions/configure/%/delete'] = array(
     'title' => 'Delete action',
     'description' => 'Delete an action.',
     'route_name' => 'action_delete',
@@ -83,45 +85,26 @@ function action_menu() {
 }
 
 /**
- * Implements hook_rebuild().
+ * Implements hook_entity_info().
  */
-function action_rebuild() {
-  // Synchronize any actions that were added or removed.
-  action_synchronize();
+function action_entity_info(&$entity_info) {
+  $entity_info['action']['controllers']['form']['add'] = 'Drupal\action\ActionAddFormController';
+  $entity_info['action']['controllers']['form']['edit'] = 'Drupal\action\ActionEditFormController';
+  $entity_info['action']['controllers']['list'] = 'Drupal\action\ActionListController';
 }
 
 /**
- * Performs a given list of actions by executing their callback functions.
+ * Performs a given list of actions.
  *
- * Given the IDs of actions to perform, this function finds out what the
- * callback functions for the actions are by querying the database. Then
- * it calls each callback using the function call $function($object, $context,
- * $a1, $a2), passing the input arguments of this function (see below) to the
- * action function.
- *
- * @param $action_ids
+ * @param string|array $action_ids
  *   The IDs of the actions to perform. Can be a single action ID or an array
- *   of IDs. IDs of configurable actions must be given as numeric action IDs;
- *   IDs of non-configurable actions may be given as action function names.
+ *   of IDs.
  * @param $object
  *   The object that the action will act on: a node, user, or comment object.
- * @param $context
- *   Associative array containing extra information about what triggered
- *   the action call, with $context['hook'] giving the name of the hook
- *   that resulted in this call to actions_do(). Additional parameters
- *   will be used as the data for token replacement.
- * @param $a1
- *   Passed along to the callback.
- * @param $a2
- *   Passed along to the callback.
- *
- * @return
- *   An associative array containing the results of the functions that
- *   perform the actions, keyed on action ID.
  *
  * @ingroup actions
  */
-function actions_do($action_ids, $object = NULL, $context = NULL, $a1 = NULL, $a2 = NULL) {
+function actions_do($action_ids, $object) {
   // $stack tracks the number of recursive calls.
   static $stack;
   $stack++;
@@ -130,568 +113,9 @@ function actions_do($action_ids, $object = NULL, $context = NULL, $a1 = NULL, $a
     watchdog('action', 'Stack overflow: recursion limit for actions_do() has been reached. Stack is limited by %limit calls.', array('%limit' => $recursion_limit), WATCHDOG_ERROR);
     return;
   }
-  $actions = array();
-  $available_actions = action_list();
-  $result = array();
-  if (is_array($action_ids)) {
-    $conditions = array();
-    foreach ($action_ids as $action_id) {
-      if (is_numeric($action_id)) {
-        $conditions[] = $action_id;
-      }
-      elseif (isset($available_actions[$action_id])) {
-        $actions[$action_id] = $available_actions[$action_id];
-      }
-    }
-
-    // When we have action instances we must go to the database to retrieve
-    // instance data.
-    if (!empty($conditions)) {
-      $query = db_select('actions');
-      $query->addField('actions', 'aid');
-      $query->addField('actions', 'type');
-      $query->addField('actions', 'callback');
-      $query->addField('actions', 'parameters');
-      $query->condition('aid', $conditions, 'IN');
-      foreach ($query->execute() as $action) {
-        $actions[$action->aid] = $action->parameters ? unserialize($action->parameters) : array();
-        $actions[$action->aid]['callback'] = $action->callback;
-        $actions[$action->aid]['type'] = $action->type;
-      }
-    }
-
-    // Fire actions, in no particular order.
-    foreach ($actions as $action_id => $params) {
-      // Configurable actions need parameters.
-      if (is_numeric($action_id)) {
-        $function = $params['callback'];
-        if (function_exists($function)) {
-          $context = array_merge($context, $params);
-          $result[$action_id] = $function($object, $context, $a1, $a2);
-        }
-        else {
-          $result[$action_id] = FALSE;
-        }
-      }
-      // Singleton action; $action_id is the function name.
-      else {
-        $result[$action_id] = $action_id($object, $context, $a1, $a2);
-      }
-    }
-  }
-  // Optimized execution of a single action.
-  else {
-    // If it's a configurable action, retrieve stored parameters.
-    if (is_numeric($action_ids)) {
-      $action = db_query("SELECT callback, parameters FROM {actions} WHERE aid = :aid", array(':aid' => $action_ids))->fetchObject();
-      $function = $action->callback;
-      if (function_exists($function)) {
-        $context = array_merge($context, unserialize($action->parameters));
-        $result[$action_ids] = $function($object, $context, $a1, $a2);
-      }
-      else {
-        $result[$action_ids] = FALSE;
-      }
-    }
-    // Singleton action; $action_ids is the function name.
-    else {
-      if (function_exists($action_ids)) {
-        $result[$action_ids] = $action_ids($object, $context, $a1, $a2);
-      }
-      else {
-        // Set to avoid undefined index error messages later.
-        $result[$action_ids] = FALSE;
-      }
-    }
+  $actions = entity_load_multiple('action', (array) $action_ids);
+  foreach ($actions as $action) {
+    $action->execute(array($object));
   }
   $stack--;
-  return $result;
-}
-
-/**
- * Discovers all available actions by invoking hook_action_info().
- *
- * This function contrasts with action_get_all_actions(); see the
- * documentation of action_get_all_actions() for an explanation.
- *
- * @param $reset
- *   Reset the action info static cache.
- *
- * @return
- *   An associative array keyed on action function name, with the same format
- *   as the return value of hook_action_info(), containing all
- *   modules' hook_action_info() return values as modified by any
- *   hook_action_info_alter() implementations.
- *
- * @see hook_action_info()
- */
-function action_list($reset = FALSE) {
-  $actions = &drupal_static(__FUNCTION__);
-  if (!isset($actions) || $reset) {
-    $actions = module_invoke_all('action_info');
-    drupal_alter('action_info', $actions);
-  }
-
-  // See module_implements() for an explanation of this cast.
-  return (array) $actions;
-}
-
-/**
- * Retrieves all action instances from the database.
- *
- * This function differs from the action_list() function, which gathers
- * actions by invoking hook_action_info(). The actions returned by this
- * function and the actions returned by action_list() are partially
- * synchronized. Non-configurable actions from hook_action_info()
- * implementations are put into the database when action_synchronize() is
- * called, which happens when admin/config/system/actions is visited.
- * Configurable actions are not added to the database until they are configured
- * in the user interface, in which case a database row is created for each
- * configuration of each action.
- *
- * @return
- *   Associative array keyed by numeric action ID. Each value is an associative
- *   array with keys 'callback', 'label', 'type' and 'configurable'.
- */
-function action_get_all_actions() {
-  $actions = db_query("SELECT aid, type, callback, parameters, label FROM {actions}")->fetchAllAssoc('aid', PDO::FETCH_ASSOC);
-  foreach ($actions as &$action) {
-    $action['configurable'] = (bool) $action['parameters'];
-    unset($action['parameters']);
-    unset($action['aid']);
-  }
-  return $actions;
-}
-
-/**
- * Creates an associative array keyed by hashes of function names or IDs.
- *
- * Hashes are used to prevent actual function names from going out into HTML
- * forms and coming back.
- *
- * @param $actions
- *   An associative array with function names or action IDs as keys
- *   and associative arrays with keys 'label', 'type', etc. as values.
- *   This is usually the output of action_list() or action_get_all_actions().
- *
- * @return
- *   An associative array whose keys are hashes of the input array keys, and
- *   whose corresponding values are associative arrays with components
- *   'callback', 'label', 'type', and 'configurable' from the input array.
- */
-function action_actions_map($actions) {
-  $actions_map = array();
-  foreach ($actions as $callback => $array) {
-    $key = Crypt::hashBase64($callback);
-    $actions_map[$key]['callback']     = isset($array['callback']) ? $array['callback'] : $callback;
-    $actions_map[$key]['label']        = $array['label'];
-    $actions_map[$key]['type']         = $array['type'];
-    $actions_map[$key]['configurable'] = $array['configurable'];
-  }
-  return $actions_map;
-}
-
-/**
- * Returns an action array key (function or ID), given its hash.
- *
- * Faster than action_actions_map() when you only need the function name or ID.
- *
- * @param $hash
- *   Hash of a function name or action ID array key. The array key
- *   is a key into the return value of action_list() (array key is the action
- *   function name) or action_get_all_actions() (array key is the action ID).
- *
- * @return
- *   The corresponding array key, or FALSE if no match is found.
- */
-function action_function_lookup($hash) {
-  // Check for a function name match.
-  $actions_list = action_list();
-  foreach ($actions_list as $function => $array) {
-    if (Crypt::hashBase64($function) == $hash) {
-      return $function;
-    }
-  }
-  $aid = FALSE;
-  // Must be a configurable action; check database.
-  $result = db_query("SELECT aid FROM {actions} WHERE parameters <> ''")->fetchAll(PDO::FETCH_ASSOC);
-  foreach ($result as $row) {
-    if (Crypt::hashBase64($row['aid']) == $hash) {
-      $aid = $row['aid'];
-      break;
-    }
-  }
-  return $aid;
-}
-
-/**
- * Synchronizes actions that are provided by modules in hook_action_info().
- *
- * Actions provided by modules in hook_action_info() implementations are
- * synchronized with actions that are stored in the actions database table.
- * This is necessary so that actions that do not require configuration can
- * receive action IDs.
- *
- * @param $delete_orphans
- *   If TRUE, any actions that exist in the database but are no longer
- *   found in the code (for example, because the module that provides them has
- *   been disabled) will be deleted.
- */
-function action_synchronize($delete_orphans = FALSE) {
-  $actions_in_code = action_list(TRUE);
-  $actions_in_db = db_query("SELECT aid, callback, label FROM {actions} WHERE parameters = ''")->fetchAllAssoc('callback', PDO::FETCH_ASSOC);
-
-  // Go through all the actions provided by modules.
-  foreach ($actions_in_code as $callback => $array) {
-    // Ignore configurable actions since their instances get put in when the
-    // user adds the action.
-    if (!$array['configurable']) {
-      // If we already have an action ID for this action, no need to assign aid.
-      if (isset($actions_in_db[$callback])) {
-        unset($actions_in_db[$callback]);
-      }
-      else {
-        // This is a new singleton that we don't have an aid for; assign one.
-        db_insert('actions')
-          ->fields(array(
-            'aid' => $callback,
-            'type' => $array['type'],
-            'callback' => $callback,
-            'parameters' => '',
-            'label' => $array['label'],
-            ))
-          ->execute();
-        watchdog('action', "Action '%action' added.", array('%action' => $array['label']));
-      }
-    }
-  }
-
-  // Any actions that we have left in $actions_in_db are orphaned.
-  if ($actions_in_db) {
-    $orphaned = array_keys($actions_in_db);
-
-    if ($delete_orphans) {
-      $actions = db_query('SELECT aid, label FROM {actions} WHERE callback IN (:orphaned)', array(':orphaned' => $orphaned))->fetchAll();
-      foreach ($actions as $action) {
-        action_delete($action->aid);
-        watchdog('action', "Removed orphaned action '%action' from database.", array('%action' => $action->label));
-      }
-    }
-    else {
-      $link = l(t('Remove orphaned actions'), 'admin/config/system/actions/orphan');
-      $count = count($actions_in_db);
-      $orphans = implode(', ', $orphaned);
-      watchdog('action', '@count orphaned actions (%orphans) exist in the actions table. !link', array('@count' => $count, '%orphans' => $orphans, '!link' => $link), WATCHDOG_INFO);
-    }
-  }
-}
-
-/**
- * Saves an action and its user-supplied parameter values to the database.
- *
- * @param $function
- *   The name of the function to be called when this action is performed.
- * @param $type
- *   The type of action, to describe grouping and/or context, e.g., 'node',
- *   'user', 'comment', or 'system'.
- * @param $params
- *   An associative array with parameter names as keys and parameter values as
- *   values.
- * @param $label
- *   A user-supplied label of this particular action, e.g., 'Send e-mail
- *   to Jim'.
- * @param $aid
- *   The ID of this action. If omitted, a new action is created.
- *
- * @return
- *   The ID of the action.
- */
-function action_save($function, $type, $params, $label, $aid = NULL) {
-  // aid is the callback for singleton actions so we need to keep a separate
-  // table for numeric aids.
-  if (!$aid) {
-    $aid = db_next_id();
-  }
-
-  db_merge('actions')
-    ->key(array('aid' => $aid))
-    ->fields(array(
-      'callback' => $function,
-      'type' => $type,
-      'parameters' => serialize($params),
-      'label' => $label,
-    ))
-    ->execute();
-
-  watchdog('action', 'Action %action saved.', array('%action' => $label));
-  return $aid;
-}
-
-/**
- * Retrieves a single action from the database.
- *
- * @param $aid
- *   The ID of the action to retrieve.
- *
- * @return
- *   The appropriate action row from the database as an object.
- */
-function action_load($aid) {
-  return db_query("SELECT aid, type, callback, parameters, label FROM {actions} WHERE aid = :aid", array(':aid' => $aid))->fetchObject();
-}
-
-/**
- * Deletes a single action from the database.
- *
- * @param $aid
- *   The ID of the action to delete.
- */
-function action_delete($aid) {
-  db_delete('actions')
-    ->condition('aid', $aid)
-    ->execute();
-  module_invoke_all('action_delete', $aid);
-}
-
-/**
- * Implements hook_action_info().
- *
- * @ingroup actions
- */
-function action_action_info() {
-  return array(
-    'action_message_action' => array(
-      'type' => 'system',
-      'label' => t('Display a message to the user'),
-      'configurable' => TRUE,
-      'triggers' => array('any'),
-    ),
-    'action_send_email_action' => array(
-      'type' => 'system',
-      'label' => t('Send e-mail'),
-      'configurable' => TRUE,
-      'triggers' => array('any'),
-    ),
-    'action_goto_action' => array(
-      'type' => 'system',
-      'label' => t('Redirect to URL'),
-      'configurable' => TRUE,
-      'triggers' => array('any'),
-    ),
-  );
-}
-
-/**
- * Return a form definition so the Send email action can be configured.
- *
- * @param $context
- *   Default values (if we are editing an existing action instance).
- *
- * @return
- *   Form definition.
- *
- * @see action_send_email_action_validate()
- * @see action_send_email_action_submit()
- */
-function action_send_email_action_form($context) {
-  // Set default values for form.
-  if (!isset($context['recipient'])) {
-    $context['recipient'] = '';
-  }
-  if (!isset($context['subject'])) {
-    $context['subject'] = '';
-  }
-  if (!isset($context['message'])) {
-    $context['message'] = '';
-  }
-
-  $form['recipient'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Recipient'),
-    '#default_value' => $context['recipient'],
-    '#maxlength' => '254',
-    '#description' => t('The e-mail address to which the message should be sent OR enter [node:author:mail], [comment:author:mail], etc. if you would like to send an e-mail to the author of the original post.'),
-  );
-  $form['subject'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Subject'),
-    '#default_value' => $context['subject'],
-    '#maxlength' => '254',
-    '#description' => t('The subject of the message.'),
-  );
-  $form['message'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Message'),
-    '#default_value' => $context['message'],
-    '#cols' => '80',
-    '#rows' => '20',
-    '#description' => t('The message that should be sent. You may include placeholders like [node:title], [user:name], and [comment:body] to represent data that will be different each time message is sent. Not all placeholders will be available in all contexts.'),
-  );
-  return $form;
-}
-
-/**
- * Validates action_send_email_action() form submissions.
- */
-function action_send_email_action_validate($form, $form_state) {
-  $form_values = $form_state['values'];
-  // Validate the configuration form.
-  if (!valid_email_address($form_values['recipient']) && strpos($form_values['recipient'], ':mail') === FALSE) {
-    // We want the literal %author placeholder to be emphasized in the error message.
-    form_set_error('recipient', t('Enter a valid email address or use a token e-mail address such as %author.', array('%author' => '[node:author:mail]')));
-  }
-}
-
-/**
- * Processes action_send_email_action() form submissions.
- */
-function action_send_email_action_submit($form, $form_state) {
-  $form_values = $form_state['values'];
-  // Process the HTML form to store configuration. The keyed array that
-  // we return will be serialized to the database.
-  $params = array(
-    'recipient' => $form_values['recipient'],
-    'subject'   => $form_values['subject'],
-    'message'   => $form_values['message'],
-  );
-  return $params;
-}
-
-/**
- * Sends an e-mail message.
- *
- * @param object $entity
- *   An optional node entity, which will be added as $context['node'] if
- *   provided.
- * @param array $context
- *   Array with the following elements:
- *   - 'recipient': E-mail message recipient. This will be passed through
- *     \Drupal\Core\Utility\Token::replace().
- *   - 'subject': The subject of the message. This will be passed through
- *     \Drupal\Core\Utility\Token::replace().
- *   - 'message': The message to send. This will be passed through
- *     \Drupal\Core\Utility\Token::replace().
- *   - Other elements will be used as the data for token replacement.
- *
- * @ingroup actions
- */
-function action_send_email_action($entity, $context) {
-  if (empty($context['node'])) {
-    $context['node'] = $entity;
-  }
-
-  $recipient = Drupal::token()->replace($context['recipient'], $context);
-
-  // If the recipient is a registered user with a language preference, use
-  // the recipient's preferred language. Otherwise, use the system default
-  // language.
-  $recipient_account = user_load_by_mail($recipient);
-  if ($recipient_account) {
-    $langcode = user_preferred_langcode($recipient_account);
-  }
-  else {
-    $langcode = language_default()->langcode;
-  }
-  $params = array('context' => $context);
-
-  if (drupal_mail('system', 'action_send_email', $recipient, $langcode, $params)) {
-    watchdog('action', 'Sent email to %recipient', array('%recipient' => $recipient));
-  }
-  else {
-    watchdog('error', 'Unable to send email to %recipient', array('%recipient' => $recipient));
-  }
-}
-
-/**
- * Constructs the settings form for action_message_action().
- *
- * @see action_message_action_submit()
- */
-function action_message_action_form($context) {
-  $form['message'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Message'),
-    '#default_value' => isset($context['message']) ? $context['message'] : '',
-    '#required' => TRUE,
-    '#rows' => '8',
-    '#description' => t('The message to be displayed to the current user. You may include placeholders like [node:title], [user:name], and [comment:body] to represent data that will be different each time message is sent. Not all placeholders will be available in all contexts.'),
-  );
-  return $form;
-}
-
-/**
- * Processes action_message_action form submissions.
- */
-function action_message_action_submit($form, $form_state) {
-  return array('message' => $form_state['values']['message']);
-}
-
-/**
- * Sends a message to the current user's screen.
- *
- * @param object $entity
- *   An optional node entity, which will be added as $context['node'] if
- *   provided.
- * @param array $context
- *   Array with the following elements:
- *   - 'message': The message to send. This will be passed through
- *     \Drupal\Core\Utility\Token::replace().
- *   - Other elements will be used as the data for token replacement in
- *     the message.
- *
- * @ingroup actions
- */
-function action_message_action(&$entity, $context = array()) {
-  if (empty($context['node'])) {
-    $context['node'] = $entity;
-  }
-
-  $context['message'] = Drupal::token()->replace(filter_xss_admin($context['message']), $context);
-  drupal_set_message($context['message']);
-}
-
-/**
- * Constructs the settings form for action_goto_action().
- *
- * @see action_goto_action_submit()
- */
-function action_goto_action_form($context) {
-  $form['url'] = array(
-    '#type' => 'textfield',
-    '#title' => t('URL'),
-    '#description' => t('The URL to which the user should be redirected. This can be an internal URL like node/1234 or an external URL like @url.', array('@url' => 'http://drupal.org')),
-    '#default_value' => isset($context['url']) ? $context['url'] : '',
-    '#required' => TRUE,
-  );
-  return $form;
-}
-
-/**
- * Processes action_goto_action form submissions.
- */
-function action_goto_action_submit($form, $form_state) {
-  return array(
-    'url' => $form_state['values']['url']
-  );
-}
-
-/**
- * Redirects to a different URL.
- *
- * Action functions are declared by modules by implementing hook_action_info().
- * Modules can cause action functions to run by calling actions_do().
- *
- * @param object $entity
- *   An optional node entity, which will be added as $context['node'] if
- *   provided.
- * @param array $context
- *   Array with the following elements:
- *   - 'url': URL to redirect to. This will be passed through
- *     \Drupal\Core\Utility\Token::replace().
- *   - Other elements will be used as the data for token replacement.
- *
- * @ingroup actions.
- */
-function action_goto_action($entity, $context) {
-  drupal_goto(Drupal::token()->replace($context['url'], $context));
 }
diff --git a/core/modules/action/action.routing.yml b/core/modules/action/action.routing.yml
index a7ed7b9..2853395 100644
--- a/core/modules/action/action.routing.yml
+++ b/core/modules/action/action.routing.yml
@@ -1,26 +1,27 @@
 action_admin:
   pattern: '/admin/config/system/actions'
   defaults:
-    _content: '\Drupal\action\Controller\ActionController::adminManage'
+    _content: '\Drupal\Core\Entity\Controller\EntityListController::listing'
+    entity_type: 'action'
   requirements:
     _permission: 'administer actions'
 
-action_admin_orphans_remove:
-  pattern: '/admin/config/system/actions/orphan'
+action_admin_add:
+  pattern: '/admin/config/system/actions/add/{operation_id}'
   defaults:
-    _content: '\Drupal\action\Controller\ActionController::adminRemoveOrphans'
+    _entity_form: 'action.add'
   requirements:
     _permission: 'administer actions'
 
 action_admin_configure:
   pattern: '/admin/config/system/actions/configure/{action}'
   defaults:
-    _form: '\Drupal\action\Form\ActionAdminConfigureForm'
+    _entity_form: 'action.edit'
   requirements:
     _permission: 'administer actions'
 
 action_delete:
-  pattern: 'admin/config/system/actions/delete/{action}'
+  pattern: 'admin/config/system/actions/configure/{action}/delete'
   defaults:
     _form: '\Drupal\action\Form\DeleteForm'
   requirements:
diff --git a/core/modules/action/lib/Drupal/action/ActionAddFormController.php b/core/modules/action/lib/Drupal/action/ActionAddFormController.php
new file mode 100644
index 0000000..e54d62c
--- /dev/null
+++ b/core/modules/action/lib/Drupal/action/ActionAddFormController.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\action\ActionAddFormController.
+ */
+
+namespace Drupal\action;
+
+use Drupal\Component\Utility\Crypt;
+
+/**
+ * Provides a form controller for action add forms.
+ */
+class ActionAddFormController extends ActionFormControllerBase {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @param string $operation_id
+   *   The hashed version of the operation ID.
+   */
+  public function buildForm(array $form, array &$form_state, $operation_id = NULL) {
+    // @todo Inject the OperationManager after https://drupal.org/node/1909418.
+    // In \Drupal\action\Form\ActionAdminManageForm::buildForm() the operations
+    // are hashed. Here we have to decrypt it to find the desired operation ID.
+    foreach (\Drupal::service('plugin.manager.operation')->getDefinitions() as $id => $definition) {
+      $key = Crypt::hashBase64($id);
+      if ($key === $operation_id) {
+        $this->entity->setPlugin($id);
+        // Derive the label and type from the operation definition.
+        $this->entity->set('label', $definition['label']);
+        $this->entity->set('type', $definition['type']);
+        break;
+      }
+    }
+
+    return parent::buildForm($form, $form_state);
+  }
+
+}
diff --git a/core/modules/action/lib/Drupal/action/ActionEditFormController.php b/core/modules/action/lib/Drupal/action/ActionEditFormController.php
new file mode 100644
index 0000000..ba758f5
--- /dev/null
+++ b/core/modules/action/lib/Drupal/action/ActionEditFormController.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\action\ActionEditFormController.
+ */
+
+namespace Drupal\action;
+
+/**
+ * Provides a form controller for action edit forms.
+ */
+class ActionEditFormController extends ActionFormControllerBase {
+
+}
diff --git a/core/modules/action/lib/Drupal/action/ActionFormControllerBase.php b/core/modules/action/lib/Drupal/action/ActionFormControllerBase.php
new file mode 100644
index 0000000..5f0f191
--- /dev/null
+++ b/core/modules/action/lib/Drupal/action/ActionFormControllerBase.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\action\ActionEditFormController.
+ */
+
+namespace Drupal\action;
+
+use Drupal\Core\Entity\EntityFormController;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+use Drupal\Component\Utility\Crypt;
+
+/**
+ * Provides a base form controller for action forms.
+ */
+class ActionFormControllerBase extends EntityFormController {
+
+  /**
+   * @var \Drupal\system\ActionInterface
+   */
+  protected $entity;
+
+  /**
+   * @var \Drupal\Core\Operation\OperationInterface
+   */
+  protected $plugin;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $this->plugin = $this->entity->getPlugin();
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form['label'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Label'),
+      '#default_value' => $this->entity->label(),
+      '#maxlength' => '255',
+      '#description' => t('A unique label for this advanced action. This label will be displayed in the interface of modules that integrate with actions.'),
+      '#weight' => -10,
+    );
+
+    $form['id'] = array(
+      '#type' => 'machine_name',
+      '#title' => t('Machine name'),
+      '#default_value' => $this->entity->id(),
+      '#disabled' => !$this->entity->isNew(),
+      '#maxlength' => 64,
+      '#description' => t('A unique name for this action. It must only contain lowercase letters, numbers and underscores.'),
+      '#machine_name' => array(
+        'exists' => array($this, 'exists'),
+      ),
+    );
+    $form['plugin'] = array(
+      '#type' => 'value',
+      '#value' => $this->entity->get('plugin'),
+    );
+    $form['type'] = array(
+      '#type' => 'value',
+      '#value' => $this->entity->getType(),
+    );
+
+    if ($this->plugin instanceof ConfigurableOperationInterface) {
+      $form += $this->plugin->form($form, $form_state);
+    }
+
+    return parent::form($form, $form_state);
+  }
+
+  /**
+   * Determines if the action already exists.
+   */
+  public function exists($id) {
+    return (bool) entity_load('action', $id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, array &$form_state) {
+    $actions = parent::actions($form, $form_state);
+    unset($actions['delete']);
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($form, $form_state);
+
+    if ($this->plugin instanceof ConfigurableOperationInterface) {
+      $this->plugin->validate($form, $form_state);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array $form, array &$form_state) {
+    parent::submit($form, $form_state);
+
+    if ($this->plugin instanceof ConfigurableOperationInterface) {
+      $this->plugin->submit($form, $form_state);
+    }
+    return $this->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, array &$form_state) {
+    $this->entity->save();
+    drupal_set_message(t('The action has been successfully saved.'));
+
+    $form_state['redirect'] = 'admin/config/system/actions';
+  }
+
+}
diff --git a/core/modules/action/lib/Drupal/action/ActionListController.php b/core/modules/action/lib/Drupal/action/ActionListController.php
new file mode 100644
index 0000000..dd51d47
--- /dev/null
+++ b/core/modules/action/lib/Drupal/action/ActionListController.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\action\ActionListController.
+ */
+
+namespace Drupal\action;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Config\Entity\ConfigEntityListController;
+use Drupal\action\Form\ActionAdminManageForm;
+
+/**
+ * Provides a listing of Actions.
+ */
+class ActionListController extends ConfigEntityListController {
+
+  /**
+   * @var bool
+   */
+  protected $hasConfigurableActions = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load() {
+    $entities = parent::load();
+    foreach ($entities as $entity) {
+      if ($entity->isConfigurable()) {
+        $this->hasConfigurableActions = TRUE;
+        continue;
+      }
+    }
+    return $entities;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $row['type'] = $entity->getType();
+    $row['label'] = $entity->label();
+    if ($this->hasConfigurableActions) {
+      $row['operations']['data'] = $this->buildOperations($entity);
+    }
+    return $row;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header = array(
+      'type' => t('Action type'),
+      'label' => t('Label'),
+      'operations' => t('Operations'),
+    );
+    return $header;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOperations(EntityInterface $entity) {
+    $operations = array();
+    if ($entity->isConfigurable()) {
+      $uri = $entity->uri();
+      $operations['edit'] = array(
+        'title' => t('configure'),
+        'href' => $uri['path'],
+        'options' => $uri['options'],
+        'weight' => 10,
+      );
+      $operations['delete'] = array(
+        'title' => t('delete'),
+        'href' => $uri['path'] . '/delete',
+        'options' => $uri['options'],
+        'weight' => 100,
+      );
+    }
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build['action_header']['#markup'] = '<h3>' . t('Available actions:') . '</h3>';
+    $build['action_table'] = parent::render();
+    if (!$this->hasConfigurableActions) {
+      unset($build['action_table']['#header']['operations']);
+    }
+    $build['action_admin_manage_form'] = drupal_get_form(new ActionAdminManageForm(\Drupal::service('plugin.manager.operation')));
+    return $build;
+  }
+
+}
diff --git a/core/modules/action/lib/Drupal/action/Controller/ActionController.php b/core/modules/action/lib/Drupal/action/Controller/ActionController.php
deleted file mode 100644
index 09423fb..0000000
--- a/core/modules/action/lib/Drupal/action/Controller/ActionController.php
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\action\Controller\ActionController.
- */
-
-namespace Drupal\action\Controller;
-
-use Drupal\action\Form\ActionAdminManageForm;
-use Drupal\Core\ControllerInterface;
-use Drupal\Core\Database\Connection;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-
-/**
- * Controller providing page callbacks for the action admin interface.
- */
-class ActionController implements ControllerInterface {
-
-  /**
-   * The database connection object for this controller.
-   *
-   * @var \Drupal\Core\Database\Connection
-   */
-  protected $database;
-
-  /**
-   * Constructs a new ActionController.
-   *
-   * @param \Drupal\Core\Database\Connection $database
-   *   The database connection object to be used by this controller.
-   */
-  public function __construct(Connection $database) {
-    $this->database = $database;
-  }
-
-  /**
-   * Implements \Drupal\Core\ControllerInterface::create().
-   */
-  public static function create(ContainerInterface $container) {
-    return new static($container->get('database'));
-  }
-
-  /**
-   * Displays an overview of available and configured actions.
-   *
-   * @return
-   *   A render array containing a table of existing actions and the advanced
-   *   action creation form.
-   */
-  public function adminManage() {
-    action_synchronize();
-    $actions = action_list();
-    $actions_map = action_actions_map($actions);
-    $options = array();
-    $unconfigurable = array();
-
-    foreach ($actions_map as $key => $array) {
-      if ($array['configurable']) {
-        $options[$key] = $array['label'] . '...';
-      }
-      else {
-        $unconfigurable[] = $array;
-      }
-    }
-
-    $row = array();
-    $instances_present = $this->database->query("SELECT aid FROM {actions} WHERE parameters <> ''")->fetchField();
-    $header = array(
-      array('data' => t('Action type'), 'field' => 'type'),
-      array('data' => t('Label'), 'field' => 'label'),
-      $instances_present ? t('Operations') : '',
-    );
-    $query = $this->database->select('actions')
-      ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
-      ->extend('Drupal\Core\Database\Query\TableSortExtender');
-    $result = $query
-      ->fields('actions')
-      ->limit(50)
-      ->orderByHeader($header)
-      ->execute();
-
-    foreach ($result as $action) {
-      $row = array();
-      $row[] = $action->type;
-      $row[] = check_plain($action->label);
-      $links = array();
-      if ($action->parameters) {
-        $links['configure'] = array(
-          'title' => t('configure'),
-          'href' => "admin/config/system/actions/configure/$action->aid",
-        );
-        $links['delete'] = array(
-          'title' => t('delete'),
-          'href' => "admin/config/system/actions/delete/$action->aid",
-        );
-      }
-      $row[] = array(
-        'data' => array(
-          '#type' => 'operations',
-          '#links' => $links,
-        ),
-      );
-
-      $rows[] = $row;
-    }
-
-    if ($rows) {
-      $pager = theme('pager');
-      if (!empty($pager)) {
-        $rows[] = array(array('data' => $pager, 'colspan' => '3'));
-      }
-      $build['action_header'] = array(
-        '#markup' => '<h3>' . t('Available actions:') . '</h3>'
-      );
-      $build['action_table'] = array(
-        '#theme' => 'table',
-        '#header' => $header,
-        '#rows' => $rows,
-      );
-    }
-
-    if ($actions_map) {
-      $build['action_admin_manage_form'] = drupal_get_form(new ActionAdminManageForm(), $options);
-    }
-
-    return $build;
-  }
-
-  /**
-   * Removes actions that are in the database but not supported by any enabled module.
-   */
-  public function adminRemoveOrphans() {
-    action_synchronize(TRUE);
-    return new RedirectResponse(url('admin/config/system/actions', array('absolute' => TRUE)));
-  }
-
-}
diff --git a/core/modules/action/lib/Drupal/action/Form/ActionAdminConfigureForm.php b/core/modules/action/lib/Drupal/action/Form/ActionAdminConfigureForm.php
deleted file mode 100644
index 7d48af4..0000000
--- a/core/modules/action/lib/Drupal/action/Form/ActionAdminConfigureForm.php
+++ /dev/null
@@ -1,125 +0,0 @@
-<?php
-/**
- * @file
- * Contains \Drupal\action\Form\ActionAdminConfigureForm.
- */
-
-namespace Drupal\action\Form;
-
-use Drupal\Component\Utility\Crypt;
-use Drupal\Core\Form\FormInterface;
-
-/**
- * Provides a form for configuring an action.
- */
-class ActionAdminConfigureForm implements FormInterface {
-
-  /**
-   * Implements \Drupal\Core\Form\FormInterface::getFormID().
-   */
-  public function getFormID() {
-    return 'action_admin_configure';
-  }
-
-  /**
-   * Implements \Drupal\Core\Form\FormInterface::buildForm().
-   */
-  public function buildForm(array $form, array &$form_state, $action = NULL) {
-    if ($action === NULL) {
-      drupal_goto('admin/config/system/actions');
-    }
-
-    $actions_map = action_actions_map(action_list());
-    $edit = array();
-
-    // Numeric action denotes saved instance of a configurable action.
-    if (is_numeric($action)) {
-      $aid = $action;
-      // Load stored parameter values from database.
-      $data = db_query("SELECT * FROM {actions} WHERE aid = :aid", array(':aid' => $aid))->fetch();
-      $edit['action_label'] = $data->label;
-      $edit['action_type'] = $data->type;
-      $function = $data->callback;
-      $action = Crypt::hashBase64($data->callback);
-      $params = unserialize($data->parameters);
-      if ($params) {
-        foreach ($params as $name => $val) {
-          $edit[$name] = $val;
-        }
-      }
-    }
-    // Otherwise, we are creating a new action instance.
-    else {
-      $function = $actions_map[$action]['callback'];
-      $edit['action_label'] = $actions_map[$action]['label'];
-      $edit['action_type'] = $actions_map[$action]['type'];
-    }
-
-    $form['action_label'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Label'),
-      '#default_value' => $edit['action_label'],
-      '#maxlength' => '255',
-      '#description' => t('A unique label for this advanced action. This label will be displayed in the interface of modules that integrate with actions.'),
-      '#weight' => -10,
-    );
-    $action_form = $function . '_form';
-    $form = array_merge($form, $action_form($edit));
-    $form['action_type'] = array(
-      '#type' => 'value',
-      '#value' => $edit['action_type'],
-    );
-    $form['action_action'] = array(
-      '#type' => 'hidden',
-      '#value' => $action,
-    );
-    // $aid is set when configuring an existing action instance.
-    if (isset($aid)) {
-      $form['action_aid'] = array(
-        '#type' => 'hidden',
-        '#value' => $aid,
-      );
-    }
-    $form['action_configured'] = array(
-      '#type' => 'hidden',
-      '#value' => '1',
-    );
-    $form['actions'] = array('#type' => 'actions');
-    $form['actions']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => t('Save'),
-      '#weight' => 13,
-    );
-
-    return $form;
-  }
-
-  /**
-   * Implements \Drupal\Core\Form\FormInterface::validateForm().
-   */
-  public function validateForm(array &$form, array &$form_state) {
-    $function = action_function_lookup($form_state['values']['action_action']) . '_validate';
-    // Hand off validation to the action.
-    if (function_exists($function)) {
-      $function($form, $form_state);
-    }
-  }
-
-  /**
-   * Implements \Drupal\Core\Form\FormInterface::submitForm().
-   */
-  public function submitForm(array &$form, array &$form_state) {
-    $function = action_function_lookup($form_state['values']['action_action']);
-    $submit_function = $function . '_submit';
-
-    // Action will return keyed array of values to store.
-    $params = $submit_function($form, $form_state);
-    $aid = isset($form_state['values']['action_aid']) ? $form_state['values']['action_aid'] : NULL;
-
-    action_save($function, $form_state['values']['action_type'], $params, $form_state['values']['action_label'], $aid);
-    drupal_set_message(t('The action has been successfully saved.'));
-
-    $form_state['redirect'] = 'admin/config/system/actions';
-  }
-
-}
diff --git a/core/modules/action/lib/Drupal/action/Form/ActionAdminManageForm.php b/core/modules/action/lib/Drupal/action/Form/ActionAdminManageForm.php
index 269ce65..79bff34 100644
--- a/core/modules/action/lib/Drupal/action/Form/ActionAdminManageForm.php
+++ b/core/modules/action/lib/Drupal/action/Form/ActionAdminManageForm.php
@@ -7,12 +7,37 @@
 
 namespace Drupal\action\Form;
 
+use Drupal\Core\Controller\ControllerInterface;
 use Drupal\Core\Form\FormInterface;
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Operation\OperationManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a configuration form for configurable actions.
  */
-class ActionAdminManageForm implements FormInterface {
+class ActionAdminManageForm implements FormInterface, ControllerInterface {
+
+  /**
+   * @var \Drupal\Core\Operation\OperationManager
+   */
+  protected $manager;
+
+  /**
+   * @param \Drupal\Core\Operation\OperationManager $manager
+   */
+  public function __construct(OperationManager $manager) {
+    $this->manager = $manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.operation')
+    );
+  }
 
   /**
    * Implements \Drupal\Core\Form\FormInterface::getFormID().
@@ -22,12 +47,16 @@ public function getFormID() {
   }
 
   /**
-   * Implements \Drupal\Core\Form\FormInterface::buildForm().
-   *
-   * @param array $options
-   *   An array of configurable actions.
+   * {@inheritdoc}
    */
-  public function buildForm(array $form, array &$form_state, array $options = array()) {
+  public function buildForm(array $form, array &$form_state) {
+    $actions = array();
+    foreach ($this->manager->getDefinitions() as $id => $definition) {
+      if (is_subclass_of($definition['class'], '\Drupal\Core\Operation\ConfigurableOperationInterface')) {
+        $key = Crypt::hashBase64($id);
+        $actions[$key] = $definition['label'] . '...';
+      }
+    }
     $form['parent'] = array(
       '#type' => 'details',
       '#title' => t('Create an advanced action'),
@@ -37,7 +66,7 @@ public function buildForm(array $form, array &$form_state, array $options = arra
       '#type' => 'select',
       '#title' => t('Action'),
       '#title_display' => 'invisible',
-      '#options' => $options,
+      '#options' => $actions,
       '#empty_option' => t('Choose an advanced action'),
     );
     $form['parent']['actions'] = array(
@@ -61,7 +90,7 @@ public function validateForm(array &$form, array &$form_state) {
    */
   public function submitForm(array &$form, array &$form_state) {
     if ($form_state['values']['action']) {
-      $form_state['redirect'] = 'admin/config/system/actions/configure/' . $form_state['values']['action'];
+      $form_state['redirect'] = 'admin/config/system/actions/add/' . $form_state['values']['action'];
     }
   }
 
diff --git a/core/modules/action/lib/Drupal/action/Form/DeleteForm.php b/core/modules/action/lib/Drupal/action/Form/DeleteForm.php
index 1d1885a..0222a1a 100644
--- a/core/modules/action/lib/Drupal/action/Form/DeleteForm.php
+++ b/core/modules/action/lib/Drupal/action/Form/DeleteForm.php
@@ -8,6 +8,7 @@
 namespace Drupal\action\Form;
 
 use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\system\ActionInterface;
 
 /**
  * Builds a form to delete an action.
@@ -17,7 +18,7 @@ class DeleteForm extends ConfirmFormBase {
   /**
    * The action to be deleted.
    *
-   * @var \stdClass
+   * @var \Drupal\system\ActionInterface
    */
   protected $action;
 
@@ -25,7 +26,7 @@ class DeleteForm extends ConfirmFormBase {
    * {@inheritdoc}
    */
   protected function getQuestion() {
-    return t('Are you sure you want to delete the action %action?', array('%action' => $this->action->label));
+    return t('Are you sure you want to delete the action %action?', array('%action' => $this->action->label()));
   }
 
   /**
@@ -40,7 +41,7 @@ protected function getConfirmText() {
    * {@inheritdoc}
    */
   protected function getCancelPath() {
-    return 'admin/config/system/actions/manage';
+    return 'admin/config/system/actions';
   }
 
   /**
@@ -53,9 +54,8 @@ public function getFormID() {
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, array &$form_state, $action = NULL) {
-
-    $this->action = action_load($action);
+  public function buildForm(array $form, array &$form_state, ActionInterface $action = NULL) {
+    $this->action = $action;
 
     return parent::buildForm($form, $form_state);
   }
@@ -64,13 +64,12 @@ public function buildForm(array $form, array &$form_state, $action = NULL) {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, array &$form_state) {
+    $this->action->delete();
 
-    action_delete($this->action->aid);
-
-    watchdog('user', 'Deleted action %aid (%action)', array('%aid' => $this->action->aid, '%action' => $this->action->label));
-    drupal_set_message(t('Action %action was deleted', array('%action' => $this->action->label)));
+    watchdog('user', 'Deleted action %aid (%action)', array('%aid' => $this->action->id(), '%action' => $this->action->label()));
+    drupal_set_message(t('Action %action was deleted', array('%action' => $this->action->label())));
 
-    $form_state['redirect'] = 'admin/config/system/actions/manage';
+    $form_state['redirect'] = 'admin/config/system/actions';
   }
 
 }
diff --git a/core/modules/action/lib/Drupal/action/Plugin/Operation/EmailAction.php b/core/modules/action/lib/Drupal/action/Plugin/Operation/EmailAction.php
new file mode 100644
index 0000000..42ecec5
--- /dev/null
+++ b/core/modules/action/lib/Drupal/action/Plugin/Operation/EmailAction.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\action\Plugin\Operation\EmailAction.
+ */
+
+namespace Drupal\action\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+use Drupal\Core\Utility\Token;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Sends an e-mail message.
+ *
+ * @Operation(
+ *   id = "action_send_email_action",
+ *   label = @Translation("Send e-mail"),
+ *   type = "system"
+ * )
+ */
+class EmailAction extends OperationBase implements ConfigurableOperationInterface {
+
+  /**
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $token;
+
+  /**
+   * Constructs a EmailAction object.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, Token $token) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->token = $token;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('token'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($entity) {
+    if (empty($this->configuration['node'])) {
+      $this->configuration['node'] = $entity;
+    }
+
+    $recipient = $this->token->replace($this->configuration['recipient'], $this->configuration);
+
+    // If the recipient is a registered user with a language preference, use
+    // the recipient's preferred language. Otherwise, use the system default
+    // language.
+    $recipient_account = user_load_by_mail($recipient);
+    if ($recipient_account) {
+      $langcode = user_preferred_langcode($recipient_account);
+    }
+    else {
+      $langcode = language_default()->langcode;
+    }
+    $params = array('context' => $this->configuration);
+
+    if (drupal_mail('system', 'action_send_email', $recipient, $langcode, $params)) {
+      watchdog('action', 'Sent email to %recipient', array('%recipient' => $recipient));
+    }
+    else {
+      watchdog('error', 'Unable to send email to %recipient', array('%recipient' => $recipient));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultConfiguration() {
+    return array(
+      'recipient' => '',
+      'subject' => '',
+      'message' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form['recipient'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Recipient'),
+      '#default_value' => $this->configuration['recipient'],
+      '#maxlength' => '254',
+      '#description' => t('The e-mail address to which the message should be sent OR enter [node:author:mail], [comment:author:mail], etc. if you would like to send an e-mail to the author of the original post.'),
+    );
+    $form['subject'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Subject'),
+      '#default_value' => $this->configuration['subject'],
+      '#maxlength' => '254',
+      '#description' => t('The subject of the message.'),
+    );
+    $form['message'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Message'),
+      '#default_value' => $this->configuration['message'],
+      '#cols' => '80',
+      '#rows' => '20',
+      '#description' => t('The message that should be sent. You may include placeholders like [node:title], [user:name], and [comment:body] to represent data that will be different each time message is sent. Not all placeholders will be available in all contexts.'),
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array &$form, array &$form_state) {
+    if (!valid_email_address($form_state['values']['recipient']) && strpos($form_state['values']['recipient'], ':mail') === FALSE) {
+      // We want the literal %author placeholder to be emphasized in the error message.
+      form_set_error('recipient', t('Enter a valid email address or use a token e-mail address such as %author.', array('%author' => '[node:author:mail]')));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array &$form, array &$form_state) {
+    $this->configuration['recipient'] = $form_state['values']['recipient'];
+    $this->configuration['subject'] = $form_state['values']['subject'];
+    $this->configuration['message'] = $form_state['values']['message'];
+  }
+
+}
diff --git a/core/modules/action/lib/Drupal/action/Plugin/Operation/GotoAction.php b/core/modules/action/lib/Drupal/action/Plugin/Operation/GotoAction.php
new file mode 100644
index 0000000..1e3a6b2
--- /dev/null
+++ b/core/modules/action/lib/Drupal/action/Plugin/Operation/GotoAction.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\action\Plugin\Operation\GotoAction.
+ */
+
+namespace Drupal\action\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+
+/**
+ * Redirects to a different URL.
+ *
+ * @Operation(
+ *   id = "action_goto_action",
+ *   label = @Translation("Redirect to URL"),
+ *   type = "system"
+ * )
+ */
+class GotoAction extends OperationBase implements ConfigurableOperationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($object) {
+    drupal_goto($this->configuration['url']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultConfiguration() {
+    return array(
+      'url' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form['url'] = array(
+      '#type' => 'textfield',
+      '#title' => t('URL'),
+      '#description' => t('The URL to which the user should be redirected. This can be an internal URL like node/1234 or an external URL like @url.', array('@url' => 'http://drupal.org')),
+      '#default_value' => $this->configuration['url'],
+      '#required' => TRUE,
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array &$form, array &$form_state) {
+    $this->configuration['url'] = $form_state['values']['url'];
+  }
+
+}
diff --git a/core/modules/action/lib/Drupal/action/Plugin/Operation/MessageAction.php b/core/modules/action/lib/Drupal/action/Plugin/Operation/MessageAction.php
new file mode 100644
index 0000000..932c7d3
--- /dev/null
+++ b/core/modules/action/lib/Drupal/action/Plugin/Operation/MessageAction.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\action\Plugin\Operation\MessageAction.
+ */
+
+namespace Drupal\action\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Sends a message to the current user's screen.
+ *
+ * @Operation(
+ *   id = "action_message_action",
+ *   label = @Translation("Display a message to the user"),
+ *   type = "system"
+ * )
+ */
+class MessageAction extends OperationBase implements ConfigurableOperationInterface {
+
+  /**
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $token;
+
+  /**
+   * Constructs a MessageAction object.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, Token $token) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->token = $token;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('token'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($entity) {
+    if (empty($this->configuration['node'])) {
+      $this->configuration['node'] = $entity;
+    }
+    $message = $this->token->replace(filter_xss_admin($this->configuration['message']), $this->configuration);
+    drupal_set_message($message);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultConfiguration() {
+    return array(
+      'message' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form['message'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Message'),
+      '#default_value' => $this->configuration['message'],
+      '#required' => TRUE,
+      '#rows' => '8',
+      '#description' => t('The message to be displayed to the current user. You may include placeholders like [node:title], [user:name], and [comment:body] to represent data that will be different each time message is sent. Not all placeholders will be available in all contexts.'),
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array &$form, array &$form_state) {
+    $this->configuration['message'] = $form_state['values']['message'];
+    unset($this->configuration['node']);
+  }
+
+}
diff --git a/core/modules/action/lib/Drupal/action/Plugin/views/field/BulkForm.php b/core/modules/action/lib/Drupal/action/Plugin/views/field/BulkForm.php
index 83f93b9..3d9f92e 100644
--- a/core/modules/action/lib/Drupal/action/Plugin/views/field/BulkForm.php
+++ b/core/modules/action/lib/Drupal/action/Plugin/views/field/BulkForm.php
@@ -71,11 +71,10 @@ public function validateOptionsForm(&$form, &$form_state) {
    */
   protected function getBulkOptions($filtered = TRUE) {
     // Get all available actions.
-    $actions = action_get_all_actions();
     $entity_type = $this->getEntityType();
     $options = array();
     // Filter the action list.
-    foreach ($actions as $id => $action) {
+    foreach ($this->actions as $id => $action) {
       if ($filtered) {
         $in_selected = in_array($id, $this->options['selected_actions']);
         // If the field is configured to include only the selected actions,
@@ -90,8 +89,8 @@ protected function getBulkOptions($filtered = TRUE) {
         }
       }
       // Only allow actions that are valid for this entity type.
-      if (($action['type'] == $entity_type) && empty($action['configurable'])) {
-        $options[$id] = $action['label'];
+      if (($action->getType() == $entity_type)) {
+        $options[$id] = $action->label();
       }
     }
 
@@ -102,26 +101,13 @@ protected function getBulkOptions($filtered = TRUE) {
    * Implements \Drupal\system\Plugin\views\field\BulkFormBase::views_form_submit().
    */
   public function views_form_submit(&$form, &$form_state) {
+    parent::views_form_submit($form, $form_state);
     if ($form_state['step'] == 'views_form_views_form') {
-      $action = $form_state['values']['action'];
-      $action = action_load($action);
-      $count = 0;
-
-      // Filter only selected checkboxes.
-      $selected = array_filter($form_state['values'][$this->options['id']]);
-
-      if (!empty($selected)) {
-        foreach (array_keys($selected) as $row_index) {
-          $entity = $this->get_entity($this->view->result[$row_index]);
-          actions_do($action->aid, $entity);
-          $entity->save();
-          $count++;
-        }
-      }
-
+      $count = count(array_filter($form_state['values'][$this->options['id']]));
+      $action = $this->actions[$form_state['values']['action']];
       if ($count) {
         drupal_set_message(format_plural($count, '%action was applied to @count item.', '%action was applied to @count items.', array(
-          '%action' => $action->label,
+          '%action' => $action->label(),
         )));
       }
     }
diff --git a/core/modules/action/lib/Drupal/action/Tests/ConfigurationTest.php b/core/modules/action/lib/Drupal/action/Tests/ConfigurationTest.php
index 8df7efa..1edb22e 100644
--- a/core/modules/action/lib/Drupal/action/Tests/ConfigurationTest.php
+++ b/core/modules/action/lib/Drupal/action/Tests/ConfigurationTest.php
@@ -42,13 +42,16 @@ function testActionConfiguration() {
     $edit = array();
     $edit['action'] = Crypt::hashBase64('action_goto_action');
     $this->drupalPost('admin/config/system/actions', $edit, t('Create'));
+    $this->assertResponse(200);
 
     // Make a POST request to the individual action configuration page.
     $edit = array();
     $action_label = $this->randomName();
-    $edit['action_label'] = $action_label;
+    $edit['label'] = $action_label;
+    $edit['id'] = strtolower($action_label);
     $edit['url'] = 'admin';
-    $this->drupalPost('admin/config/system/actions/configure/' . Crypt::hashBase64('action_goto_action'), $edit, t('Save'));
+    $this->drupalPost('admin/config/system/actions/add/' . Crypt::hashBase64('action_goto_action'), $edit, t('Save'));
+    $this->assertResponse(200);
 
     // Make sure that the new complex action was saved properly.
     $this->assertText(t('The action has been successfully saved.'), "Make sure we get a confirmation that we've successfully saved the complex action.");
@@ -56,29 +59,39 @@ function testActionConfiguration() {
 
     // Make another POST request to the action edit page.
     $this->clickLink(t('configure'));
-    preg_match('|admin/config/system/actions/configure/(\d+)|', $this->getUrl(), $matches);
+    preg_match('|admin/config/system/actions/configure/(.+)|', $this->getUrl(), $matches);
     $aid = $matches[1];
     $edit = array();
     $new_action_label = $this->randomName();
-    $edit['action_label'] = $new_action_label;
+    $edit['label'] = $new_action_label;
     $edit['url'] = 'admin';
     $this->drupalPost(NULL, $edit, t('Save'));
+    $this->assertResponse(200);
 
     // Make sure that the action updated properly.
     $this->assertText(t('The action has been successfully saved.'), "Make sure we get a confirmation that we've successfully updated the complex action.");
     $this->assertNoText($action_label, "Make sure the old action label does NOT appear on the configuration page after we've updated the complex action.");
     $this->assertText($new_action_label, "Make sure the action label appears on the configuration page after we've updated the complex action.");
 
+    $this->clickLink(t('configure'));
+    $element = $this->xpath('//input[@type="text" and @value="admin"]');
+    $this->assertTrue(!empty($element), 'Make sure the URL appears when re-editing the action.');
+
     // Make sure that deletions work properly.
+    $this->drupalGet('admin/config/system/actions');
     $this->clickLink(t('delete'));
+    $this->assertResponse(200);
     $edit = array();
-    $this->drupalPost("admin/config/system/actions/delete/$aid", $edit, t('Delete'));
+    $this->drupalPost("admin/config/system/actions/configure/$aid/delete", $edit, t('Delete'));
+    $this->assertResponse(200);
 
     // Make sure that the action was actually deleted.
     $this->assertRaw(t('Action %action was deleted', array('%action' => $new_action_label)), 'Make sure that we get a delete confirmation message.');
     $this->drupalGet('admin/config/system/actions');
+    $this->assertResponse(200);
     $this->assertNoText($new_action_label, "Make sure the action label does not appear on the overview page after we've deleted the action.");
-    $exists = db_query('SELECT aid FROM {actions} WHERE callback = :callback', array(':callback' => 'drupal_goto_action'))->fetchField();
-    $this->assertFalse($exists, 'Make sure the action is gone from the database after being deleted.');
+
+    $action = entity_load('action', $aid);
+    $this->assertFalse($action, 'Make sure the action is gone after being deleted.');
   }
 }
diff --git a/core/modules/action/lib/Drupal/action/Tests/LoopTest.php b/core/modules/action/lib/Drupal/action/Tests/LoopTest.php
index 87fa08e..741e11a 100644
--- a/core/modules/action/lib/Drupal/action/Tests/LoopTest.php
+++ b/core/modules/action/lib/Drupal/action/Tests/LoopTest.php
@@ -38,8 +38,14 @@ function testActionLoop() {
     $user = $this->drupalCreateUser(array('administer actions'));
     $this->drupalLogin($user);
 
-    $info = action_loop_test_action_info();
-    $this->aid = action_save('action_loop_test_log', $info['action_loop_test_log']['type'], array(), $info['action_loop_test_log']['label']);
+    $action = entity_create('action', array(
+      'id' => 'action_loop_test_log',
+      'label' => t('Write a message to the log.'),
+      'type' => 'system',
+      'plugin' => 'action_loop_test_log',
+    ));
+    $action->save();
+    $this->aid = $action->id();
 
     // Delete any existing watchdog messages to clear the plethora of
     // "Action added" messages from when Drupal was installed.
diff --git a/core/modules/action/tests/action_loop_test/action_loop_test.module b/core/modules/action/tests/action_loop_test/action_loop_test.module
index 36f6d9b..70f6deb 100644
--- a/core/modules/action/tests/action_loop_test/action_loop_test.module
+++ b/core/modules/action/tests/action_loop_test/action_loop_test.module
@@ -29,29 +29,6 @@ function action_loop_test_init() {
 }
 
 /**
- * Implements hook_action_info().
- */
-function action_loop_test_action_info() {
-  return array(
-    'action_loop_test_log' => array(
-      'label' => t('Write a message to the log.'),
-      'type' => 'system',
-      'configurable' => FALSE,
-      'triggers' => array('any'),
-    ),
-  );
-}
-
-/**
- * Write a message to the log.
- */
-function action_loop_test_log() {
-  $count = &drupal_static(__FUNCTION__, 0);
-  $count++;
-  watchdog_skip_semaphore('action_loop_test', "Test log #$count");
-}
-
-/**
  * Replacement of the watchdog() function that eliminates the use of semaphores
  * so that we can test the abortion of an action loop.
  */
diff --git a/core/modules/action/tests/action_loop_test/lib/Drupal/action_loop_test/Plugin/Operation/LoopTestOperation.php b/core/modules/action/tests/action_loop_test/lib/Drupal/action_loop_test/Plugin/Operation/LoopTestOperation.php
new file mode 100644
index 0000000..651f065
--- /dev/null
+++ b/core/modules/action/tests/action_loop_test/lib/Drupal/action_loop_test/Plugin/Operation/LoopTestOperation.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\action_loop_test\Plugin\Operation\LoopTestOperation.
+ */
+
+namespace Drupal\action_loop_test\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Provides a test operation.
+ *
+ * @Operation(
+ *   id = "action_loop_test_log",
+ *   label = @Translation("Write a message to the log")
+ * )
+ */
+class LoopTestOperation extends OperationBase {
+
+  /**
+   * Keeps track of how many times this operation is executed.
+   *
+   * @var int
+   */
+  protected static $count = 0;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($entity) {
+    static::$count++;
+    watchdog_skip_semaphore('action_loop_test', 'Test log #' . static::$count);
+  }
+
+}
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 717a25b..80b950b 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -1770,161 +1770,6 @@ function comment_alphadecimal_to_int($c = '00') {
 }
 
 /**
- * Implements hook_action_info().
- */
-function comment_action_info() {
-  return array(
-    'comment_publish_action' => array(
-      'label' => t('Publish comment'),
-      'type' => 'comment',
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
-    ),
-    'comment_unpublish_action' => array(
-      'label' => t('Unpublish comment'),
-      'type' => 'comment',
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
-    ),
-    'comment_unpublish_by_keyword_action' => array(
-      'label' => t('Unpublish comment containing keyword(s)'),
-      'type' => 'comment',
-      'configurable' => TRUE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
-    ),
-    'comment_save_action' => array(
-      'label' => t('Save comment'),
-      'type' => 'comment',
-      'configurable' => FALSE,
-      'triggers' => array('comment_insert', 'comment_update'),
-    ),
-  );
-}
-
-/**
- * Publishes a comment.
- *
- * @param Drupal\comment\Comment $comment
- *   (optional) A comment object to publish.
- * @param array $context
- *   Array with components:
- *   - 'cid': Comment ID. Required if $comment is not given.
- *
- * @ingroup actions
- */
-function comment_publish_action(Comment $comment = NULL, $context = array()) {
-  if (isset($comment->subject->value)) {
-    $subject = $comment->subject->value;
-    $comment->status->value = COMMENT_PUBLISHED;
-  }
-  else {
-    $cid = $context['cid'];
-    $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
-    db_update('comment')
-      ->fields(array('status' => COMMENT_PUBLISHED))
-      ->condition('cid', $cid)
-      ->execute();
-  }
-  watchdog('action', 'Published comment %subject.', array('%subject' => $subject));
-}
-
-/**
- * Unpublishes a comment.
- *
- * @param Drupal\comment\Comment|null $comment
- *   (optional) A comment object to unpublish.
- * @param array $context
- *   Array with components:
- *   - 'cid': Comment ID. Required if $comment is not given.
- *
- * @ingroup actions
- */
-function comment_unpublish_action(Comment $comment = NULL, $context = array()) {
-  if (isset($comment->subject->value)) {
-    $subject = $comment->subject->value;
-    $comment->status->value = COMMENT_NOT_PUBLISHED;
-  }
-  else {
-    $cid = $context['cid'];
-    $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
-    db_update('comment')
-      ->fields(array('status' => COMMENT_NOT_PUBLISHED))
-      ->condition('cid', $cid)
-      ->execute();
-  }
-  watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject));
-}
-
-/**
- * Unpublishes a comment if it contains certain keywords.
- *
- * @param Drupal\comment\Comment $comment
- *   Comment object to modify.
- * @param array $context
- *   Array with components:
- *   - 'keywords': Keywords to look for. If the comment contains at least one
- *     of the keywords, it is unpublished.
- *
- * @ingroup actions
- * @see comment_unpublish_by_keyword_action_form()
- * @see comment_unpublish_by_keyword_action_submit()
- */
-function comment_unpublish_by_keyword_action(Comment $comment, $context) {
-  $build = comment_view($comment);
-  $text = drupal_render($build);
-  foreach ($context['keywords'] as $keyword) {
-    if (strpos($text, $keyword) !== FALSE) {
-      $comment->status->value = COMMENT_NOT_PUBLISHED;
-      watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject->value));
-      break;
-    }
-  }
-}
-
-/**
- * Form constructor for the blacklisted keywords form.
- *
- * @ingroup forms
- * @see comment_unpublish_by_keyword_action()
- * @see comment_unpublish_by_keyword_action_submit()
- */
-function comment_unpublish_by_keyword_action_form($context) {
-  $form['keywords'] = array(
-    '#title' => t('Keywords'),
-    '#type' => 'textarea',
-    '#description' => t('The comment will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'),
-    '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
-  );
-
-  return $form;
-}
-
-/**
- * Form submission handler for comment_unpublish_by_keyword_action_form().
- *
- * @see comment_unpublish_by_keyword_action()
- */
-function comment_unpublish_by_keyword_action_submit($form, $form_state) {
-  return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
-}
-
-/**
- * Saves a comment.
- *
- * @param Drupal\comment\Comment $comment
- *
- * @ingroup actions
- */
-function comment_save_action(Comment $comment) {
-  comment_save($comment);
-  cache_invalidate_tags(array('content' => TRUE));
-  watchdog('action', 'Saved comment %title', array('%title' => $comment->subject->value));
-}
-
-/**
  * Implements hook_ranking().
  */
 function comment_ranking() {
diff --git a/core/modules/comment/config/action.action.comment_publish_action.yml b/core/modules/comment/config/action.action.comment_publish_action.yml
new file mode 100644
index 0000000..e29edfa
--- /dev/null
+++ b/core/modules/comment/config/action.action.comment_publish_action.yml
@@ -0,0 +1,6 @@
+id: comment_publish_action
+label: 'Publish comment'
+status: '1'
+langcode: en
+type: comment
+plugin: comment_publish_action
diff --git a/core/modules/comment/config/action.action.comment_save_action.yml b/core/modules/comment/config/action.action.comment_save_action.yml
new file mode 100644
index 0000000..47f8c39
--- /dev/null
+++ b/core/modules/comment/config/action.action.comment_save_action.yml
@@ -0,0 +1,6 @@
+id: comment_save_action
+label: 'Save comment'
+status: '1'
+langcode: en
+type: comment
+plugin: comment_save_action
diff --git a/core/modules/comment/config/action.action.comment_unpublish_action.yml b/core/modules/comment/config/action.action.comment_unpublish_action.yml
new file mode 100644
index 0000000..0ac26fd
--- /dev/null
+++ b/core/modules/comment/config/action.action.comment_unpublish_action.yml
@@ -0,0 +1,6 @@
+id: comment_unpublish_action
+label: 'Unpublish comment'
+status: '1'
+langcode: en
+type: comment
+plugin: comment_unpublish_action
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Operation/PublishComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Operation/PublishComment.php
new file mode 100644
index 0000000..ca2d5fc
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Operation/PublishComment.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\Plugin\Operation\PublishComment.
+ */
+
+namespace Drupal\comment\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Publishes a comment.
+ *
+ * @Operation(
+ *   id = "comment_publish_action",
+ *   label = @Translation("Publish comment"),
+ *   type = "comment"
+ * )
+ */
+class PublishComment extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($comment) {
+    $comment->status->value = COMMENT_PUBLISHED;
+    $comment->save();
+  }
+
+}
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Operation/SaveComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Operation/SaveComment.php
new file mode 100644
index 0000000..3e8531a
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Operation/SaveComment.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\Plugin\Operation\SaveComment.
+ */
+
+namespace Drupal\comment\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Saves a comment.
+ *
+ * @Operation(
+ *   id = "comment_save_action",
+ *   label = @Translation("Save comment"),
+ *   type = "comment"
+ * )
+ */
+class SaveComment extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($comment) {
+    $comment->save();
+    Cache::invalidateTags(array('content' => TRUE));
+  }
+
+}
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Operation/UnpublishByKeywordComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Operation/UnpublishByKeywordComment.php
new file mode 100644
index 0000000..b749478
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Operation/UnpublishByKeywordComment.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\Plugin\Operation\UnpublishByKeywordComment.
+ */
+
+namespace Drupal\comment\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+
+/**
+ * Unpublishes a comment containing certain keywords.
+ *
+ * @Operation(
+ *   id = "comment_unpublish_by_keyword_action",
+ *   label = @Translation("Unpublish comment containing keyword(s)"),
+ *   type = "comment"
+ * )
+ */
+class UnpublishByKeywordComment extends OperationBase implements ConfigurableOperationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($comment) {
+    $build = comment_view($comment);
+    $text = drupal_render($build);
+    foreach ($this->configuration['keywords'] as $keyword) {
+      if (strpos($text, $keyword) !== FALSE) {
+        $comment->status->value = COMMENT_NOT_PUBLISHED;
+        $comment->save();
+        break;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultConfiguration() {
+    return array(
+      'keywords' => array(),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form['keywords'] = array(
+      '#title' => t('Keywords'),
+      '#type' => 'textarea',
+      '#description' => t('The comment will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'),
+      '#default_value' => drupal_implode_tags($this->configuration['keywords']),
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array &$form, array &$form_state) {
+    $this->configuration['keywords'] = drupal_explode_tags($form_state['values']['keywords']);
+  }
+
+}
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Operation/UnpublishComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Operation/UnpublishComment.php
new file mode 100644
index 0000000..ba3bd07
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Operation/UnpublishComment.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\Plugin\Operation\UnpublishComment.
+ */
+
+namespace Drupal\comment\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Unpublishes a comment.
+ *
+ * @Operation(
+ *   id = "comment_unpublish_action",
+ *   label = @Translation("Unpublish comment"),
+ *   type = "comment"
+ * )
+ */
+class UnpublishComment extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($comment) {
+    $comment->status->value = COMMENT_NOT_PUBLISHED;
+    $comment->save();
+  }
+
+}
diff --git a/core/modules/comment/lib/Drupal/comment/Tests/CommentActionsTest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentActionsTest.php
index a942d7c..d926f49 100644
--- a/core/modules/comment/lib/Drupal/comment/Tests/CommentActionsTest.php
+++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentActionsTest.php
@@ -36,28 +36,13 @@ function testCommentPublishUnpublishActions() {
     $subject = $this->randomName();
     $comment = $this->postComment($this->node, $comment_text, $subject);
 
-    // Unpublish a comment (direct form: doesn't actually save the comment).
-    comment_unpublish_action($comment);
+    // Unpublish a comment.
+    actions_do('comment_unpublish_action', $comment);
     $this->assertEqual($comment->status->value, COMMENT_NOT_PUBLISHED, 'Comment was unpublished');
-    $this->assertWatchdogMessage('Unpublished comment %subject.', array('%subject' => $subject), 'Found watchdog message');
-    $this->clearWatchdog();
 
-    // Unpublish a comment (indirect form: modify the comment in the database).
-    comment_unpublish_action(NULL, array('cid' => $comment->id()));
-    $this->assertEqual(comment_load($comment->id())->status->value, COMMENT_NOT_PUBLISHED, 'Comment was unpublished');
-    $this->assertWatchdogMessage('Unpublished comment %subject.', array('%subject' => $subject), 'Found watchdog message');
-
-    // Publish a comment (direct form: doesn't actually save the comment).
-    comment_publish_action($comment);
+    // Publish a comment.
+    actions_do('comment_publish_action', $comment);
     $this->assertEqual($comment->status->value, COMMENT_PUBLISHED, 'Comment was published');
-    $this->assertWatchdogMessage('Published comment %subject.', array('%subject' => $subject), 'Found watchdog message');
-    $this->clearWatchdog();
-
-    // Publish a comment (indirect form: modify the comment in the database).
-    comment_publish_action(NULL, array('cid' => $comment->id()));
-    $this->assertEqual(comment_load($comment->id())->status->value, COMMENT_PUBLISHED, 'Comment was published');
-    $this->assertWatchdogMessage('Published comment %subject.', array('%subject' => $subject), 'Found watchdog message');
-    $this->clearWatchdog();
   }
 
   /**
@@ -67,9 +52,16 @@ function testCommentUnpublishByKeyword() {
     $this->drupalLogin($this->admin_user);
     $keyword_1 = $this->randomName();
     $keyword_2 = $this->randomName();
-    $aid = action_save('comment_unpublish_by_keyword_action', 'comment', array('keywords' => array($keyword_1, $keyword_2)), $this->randomName());
-
-    $this->assertTrue(action_load($aid), 'The action could be loaded.');
+    $action = entity_create('action', array(
+      'id' => 'comment_unpublish_by_keyword_action',
+      'label' => $this->randomName(),
+      'type' => 'comment',
+      'configuration' => array(
+        'keywords' => array($keyword_1, $keyword_2),
+      ),
+      'plugin' => 'comment_unpublish_by_keyword_action',
+    ));
+    $action->save();
 
     $comment = $this->postComment($this->node, $keyword_2, $this->randomName());
 
@@ -78,29 +70,8 @@ function testCommentUnpublishByKeyword() {
 
     $this->assertTrue($comment->status->value == COMMENT_PUBLISHED, 'The comment status was set to published.');
 
-    actions_do($aid, $comment, array());
+    actions_do($action->id(), $comment, array());
     $this->assertTrue($comment->status->value == COMMENT_NOT_PUBLISHED, 'The comment status was set to not published.');
   }
 
-  /**
-   * Verifies that a watchdog message has been entered.
-   *
-   * @param $watchdog_message
-   *   The watchdog message.
-   * @param $variables
-   *   The array of variables passed to watchdog().
-   * @param $message
-   *   The assertion message.
-   */
-  function assertWatchdogMessage($watchdog_message, $variables, $message) {
-    $status = (bool) db_query_range("SELECT 1 FROM {watchdog} WHERE message = :message AND variables = :variables", 0, 1, array(':message' => $watchdog_message, ':variables' => serialize($variables)))->fetchField();
-    return $this->assert($status, format_string('@message', array('@message'=> $message)));
-  }
-
-  /**
-   * Clears watchdog.
-   */
-  function clearWatchdog() {
-    db_truncate('watchdog')->execute();
-  }
 }
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php
index d69d083..9a953bb 100644
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php
@@ -209,6 +209,7 @@ function testContentTypeDirLang() {
    *  Test filtering Node content by language.
    */
   function testNodeAdminLanguageFilter() {
+    module_enable(array('views'));
     // User to add and remove language.
     $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'access content overview', 'administer nodes', 'bypass node access'));
 
@@ -223,14 +224,8 @@ function testNodeAdminLanguageFilter() {
     $node_en = $this->drupalCreateNode(array('langcode' => 'en'));
     $node_zh_hant = $this->drupalCreateNode(array('langcode' => 'zh-hant'));
 
-    $this->drupalGet('admin/content');
-
     // Verify filtering by language.
-    $edit = array(
-      'langcode' => 'zh-hant',
-    );
-    $this->drupalPost(NULL, $edit, t('Filter'));
-
+    $this->drupalGet('admin/content', array('query' => array('langcode' => 'zh-hant')));
     $this->assertLinkByHref('node/' . $node_zh_hant->nid . '/edit');
     $this->assertNoLinkByHref('node/' . $node_en->nid . '/edit');
   }
diff --git a/core/modules/node/config/action.action.node_delete_action.yml b/core/modules/node/config/action.action.node_delete_action.yml
new file mode 100644
index 0000000..3adc607
--- /dev/null
+++ b/core/modules/node/config/action.action.node_delete_action.yml
@@ -0,0 +1,6 @@
+id: node_delete_action
+label: 'Delete selected content'
+status: '1'
+langcode: en
+type: node
+plugin: node_delete_action
diff --git a/core/modules/node/config/action.action.node_make_sticky_action.yml b/core/modules/node/config/action.action.node_make_sticky_action.yml
new file mode 100644
index 0000000..14e731a
--- /dev/null
+++ b/core/modules/node/config/action.action.node_make_sticky_action.yml
@@ -0,0 +1,6 @@
+id: node_make_sticky_action
+label: 'Make content sticky'
+status: '1'
+langcode: en
+type: node
+plugin: node_make_sticky_action
diff --git a/core/modules/node/config/action.action.node_make_unsticky_action.yml b/core/modules/node/config/action.action.node_make_unsticky_action.yml
new file mode 100644
index 0000000..e8e3a25
--- /dev/null
+++ b/core/modules/node/config/action.action.node_make_unsticky_action.yml
@@ -0,0 +1,6 @@
+id: node_make_unsticky_action
+label: 'Make content unsticky'
+status: '1'
+langcode: en
+type: node
+plugin: node_make_unsticky_action
diff --git a/core/modules/node/config/action.action.node_promote_action.yml b/core/modules/node/config/action.action.node_promote_action.yml
new file mode 100644
index 0000000..3d56d92
--- /dev/null
+++ b/core/modules/node/config/action.action.node_promote_action.yml
@@ -0,0 +1,6 @@
+id: node_promote_action
+label: 'Promote content to front page'
+status: '1'
+langcode: en
+type: node
+plugin: node_promote_action
diff --git a/core/modules/node/config/action.action.node_publish_action.yml b/core/modules/node/config/action.action.node_publish_action.yml
new file mode 100644
index 0000000..220a944
--- /dev/null
+++ b/core/modules/node/config/action.action.node_publish_action.yml
@@ -0,0 +1,6 @@
+id: node_publish_action
+label: 'Publish content'
+status: '1'
+langcode: en
+type: node
+plugin: node_publish_action
diff --git a/core/modules/node/config/action.action.node_save_action.yml b/core/modules/node/config/action.action.node_save_action.yml
new file mode 100644
index 0000000..46472cc
--- /dev/null
+++ b/core/modules/node/config/action.action.node_save_action.yml
@@ -0,0 +1,6 @@
+id: node_save_action
+label: 'Save content'
+status: '1'
+langcode: en
+type: node
+plugin: node_save_action
diff --git a/core/modules/node/config/action.action.node_unpromote_action.yml b/core/modules/node/config/action.action.node_unpromote_action.yml
new file mode 100644
index 0000000..86d11a7
--- /dev/null
+++ b/core/modules/node/config/action.action.node_unpromote_action.yml
@@ -0,0 +1,6 @@
+id: node_unpromote_action
+label: 'Remove content from front page'
+status: '1'
+langcode: en
+type: node
+plugin: node_unpromote_action
diff --git a/core/modules/node/config/action.action.node_unpublish_action.yml b/core/modules/node/config/action.action.node_unpublish_action.yml
new file mode 100644
index 0000000..5853069
--- /dev/null
+++ b/core/modules/node/config/action.action.node_unpublish_action.yml
@@ -0,0 +1,6 @@
+id: node_unpublish_action
+label: 'Unpublish content'
+status: '1'
+langcode: en
+type: node
+plugin: node_unpublish_action
diff --git a/core/modules/node/config/views.view.content.yml b/core/modules/node/config/views.view.content.yml
new file mode 100644
index 0000000..fed61e6
--- /dev/null
+++ b/core/modules/node/config/views.view.content.yml
@@ -0,0 +1,359 @@
+base_field: nid
+base_table: node
+core: 8.x
+description: 'Find and manage content.'
+status: '1'
+display:
+  default:
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'access content overview'
+      cache:
+        type: none
+      query:
+        type: views_query
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Filter
+          reset_button: '0'
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: '1'
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: full
+        options:
+          items_per_page: '50'
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: '1'
+          row_class_special: '1'
+          override: '1'
+          sticky: '1'
+          summary: ''
+          columns:
+            node_bulk_form: node_bulk_form
+            title: title
+            type: type
+            name: name
+            status: status
+            changed: changed
+            edit_node: edit_node
+            delete_node: delete_node
+            translation_link: translation_link
+            dropbutton: dropbutton
+          info:
+            node_bulk_form:
+              sortable: '0'
+              default_sort_order: asc
+              responsive: ''
+            title:
+              sortable: '1'
+              default_sort_order: asc
+            type:
+              sortable: '1'
+              default_sort_order: asc
+            name:
+              sortable: '0'
+              default_sort_order: asc
+              responsive: priority-low
+            status:
+              sortable: '1'
+              default_sort_order: asc
+              responsive: ''
+            changed:
+              sortable: '1'
+              default_sort_order: desc
+              responsive: priority-low
+            edit_node:
+              sortable: '0'
+              default_sort_order: asc
+              responsive: ''
+            delete_node:
+              sortable: '0'
+              default_sort_order: asc
+              responsive: ''
+            translation_link:
+              sortable: '0'
+              default_sort_order: asc
+              responsive: ''
+            dropbutton:
+              sortable: '0'
+              default_sort_order: asc
+              responsive: ''
+          default: changed
+          empty_table: '1'
+      row:
+        type: fields
+      fields:
+        node_bulk_form:
+          id: node_bulk_form
+          table: node
+          field: node_bulk_form
+          label: ''
+          exclude: '0'
+          alter:
+            alter_text: '0'
+          element_class: ''
+          element_default_classes: '1'
+          empty: ''
+          hide_empty: '0'
+          empty_zero: '0'
+          hide_alter_empty: '1'
+          plugin_id: node_bulk_form
+        title:
+          id: title
+          table: node
+          field: title
+          label: Title
+          exclude: '0'
+          alter:
+            alter_text: '0'
+          element_class: ''
+          element_default_classes: '1'
+          empty: ''
+          hide_empty: '0'
+          empty_zero: '0'
+          hide_alter_empty: '1'
+          link_to_node: '1'
+          plugin_id: node
+        type:
+          id: type
+          table: node
+          field: type
+          label: 'Content Type'
+          exclude: '0'
+          alter:
+            alter_text: '0'
+          element_class: ''
+          element_default_classes: '1'
+          empty: ''
+          hide_empty: '0'
+          empty_zero: '0'
+          hide_alter_empty: '1'
+          link_to_node: '0'
+          machine_name: '0'
+          plugin_id: node_type
+        name:
+          id: name
+          table: users
+          field: name
+          relationship: uid
+          label: Author
+          exclude: '0'
+          alter:
+            alter_text: '0'
+          element_class: ''
+          element_default_classes: '1'
+          empty: ''
+          hide_empty: '0'
+          empty_zero: '0'
+          hide_alter_empty: '1'
+          link_to_user: '1'
+          overwrite_anonymous: '0'
+          anonymous_text: ''
+          format_username: '1'
+          plugin_id: user_name
+        status:
+          id: status
+          table: node
+          field: status
+          label: Status
+          exclude: '0'
+          alter:
+            alter_text: '0'
+          element_class: ''
+          element_default_classes: '1'
+          empty: ''
+          hide_empty: '0'
+          empty_zero: '0'
+          hide_alter_empty: '1'
+          type: published-notpublished
+          type_custom_true: ''
+          type_custom_false: ''
+          not: '0'
+          plugin_id: boolean
+        changed:
+          id: changed
+          table: node
+          field: changed
+          label: Updated
+          exclude: '0'
+          alter:
+            alter_text: '0'
+          element_class: ''
+          element_default_classes: '1'
+          empty: ''
+          hide_empty: '0'
+          empty_zero: '0'
+          hide_alter_empty: '1'
+          date_format: short
+          custom_date_format: ''
+          timezone: ''
+          plugin_id: date
+        edit_node:
+          id: edit_node
+          table: views_entity_node
+          field: edit_node
+          label: ''
+          exclude: '1'
+          text: Edit
+          plugin_id: node_link_edit
+        delete_node:
+          id: delete_node
+          table: views_entity_node
+          field: delete_node
+          label: ''
+          exclude: '1'
+          text: Delete
+          plugin_id: node_link_delete
+        translation_link:
+          id: translation_link
+          table: node
+          field: translation_link
+          label: ''
+          exclude: '1'
+          alter:
+            alter_text: '0'
+          element_class: ''
+          element_default_classes: '1'
+          hide_alter_empty: '1'
+          hide_empty: '0'
+          empty_zero: '0'
+          empty: ''
+          text: Translate
+          optional: '1'
+          plugin_id: translation_entity_link
+        dropbutton:
+          id: dropbutton
+          table: views
+          field: dropbutton
+          label: Operations
+          fields:
+            edit_node: edit_node
+            delete_node: delete_node
+            translation_link: translation_link
+          destination: '1'
+          plugin_id: dropbutton
+      filters:
+        status_extra:
+          id: status_extra
+          table: node
+          field: status_extra
+          operator: '='
+          value: ''
+          plugin_id: node_status
+        status:
+          id: status
+          table: node
+          field: status
+          operator: '='
+          value: All
+          exposed: '1'
+          expose:
+            operator_id: ''
+            label: Status
+            description: ''
+            use_operator: '0'
+            operator: status_op
+            identifier: status
+            required: '0'
+            remember: '0'
+            multiple: '0'
+            remember_roles:
+              authenticated: authenticated
+          plugin_id: boolean
+        type:
+          id: type
+          table: node
+          field: type
+          operator: in
+          value: {  }
+          exposed: '1'
+          expose:
+            operator_id: type_op
+            label: Type
+            description: ''
+            use_operator: '0'
+            operator: type_op
+            identifier: type
+            required: '0'
+            remember: '0'
+            multiple: '0'
+            remember_roles:
+              authenticated: authenticated
+            reduce: '0'
+          plugin_id: bundle
+        langcode:
+          id: langcode
+          table: node
+          field: langcode
+          operator: in
+          value: {  }
+          group: '1'
+          exposed: '1'
+          expose:
+            operator_id: langcode_op
+            label: Language
+            operator: langcode_op
+            identifier: langcode
+            remember_roles:
+              authenticated: authenticated
+          optional: '1'
+          plugin_id: language
+      sorts: {  }
+      title: Content
+      empty:
+        area_text_custom:
+          id: area_text_custom
+          table: views
+          field: area_text_custom
+          empty: '1'
+          content: 'No content available.'
+          plugin_id: text_custom
+      arguments: {  }
+      relationships:
+        uid:
+          id: uid
+          table: node
+          field: uid
+          admin_label: author
+          required: '1'
+          plugin_id: standard
+      show_admin_links: '0'
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: '0'
+  page_1:
+    display_options:
+      path: admin/content/node
+      menu:
+        type: 'default tab'
+        title: Content
+        description: ''
+        name: admin
+        weight: '-10'
+        context: '0'
+      tab_options:
+        type: normal
+        title: Content
+        description: 'Find and manage content'
+        name: admin
+        weight: '-10'
+    display_plugin: page
+    display_title: Page
+    id: page_1
+    position: '1'
+label: Content
+module: node
+id: content
+tag: default
+langcode: en
diff --git a/core/modules/node/lib/Drupal/node/Form/DeleteMultiple.php b/core/modules/node/lib/Drupal/node/Form/DeleteMultiple.php
new file mode 100644
index 0000000..68fd7d6
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Form/DeleteMultiple.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Form\DeleteMultiple.
+ */
+
+namespace Drupal\node\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\ControllerInterface;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Component\Utility\String;
+use Drupal\user\TempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a node deletion confirmation form.
+ */
+class DeleteMultiple extends ConfirmFormBase implements ControllerInterface {
+
+  /**
+   * The array of nodes to delete.
+   *
+   * @var array
+   */
+  protected $nodes = array();
+
+  /**
+   * The temp store object.
+   *
+   * @var \Drupal\user\TempStore
+   */
+  protected $tempStore;
+
+  /**
+   * The node storage controller.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageControllerInterface
+   */
+  protected $manager;
+
+  /**
+   * Constructs a DeleteMultiple form object.
+   *
+   * @param \Drupal\user\TempStoreFactory $temp_store_factory
+   *   The temp store factory.
+   * @param \Drupal\Core\Entity\EntityManager $manager
+   *   The entity manager.
+   */
+  public function __construct(TempStoreFactory $temp_store_factory, EntityManager $manager) {
+    $this->tempStore = $temp_store_factory->get('node_multiple_delete_confirm');
+    $this->storageController = $manager->getStorageController('node');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.tempstore'),
+      $container->get('plugin.manager.entity')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'node_multiple_delete_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getQuestion() {
+    return format_plural(count($this->nodes), 'Are you sure you want to delete this item?', 'Are you sure you want to delete these items?');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCancelPath() {
+    return 'admin/content';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $this->nodes = $this->tempStore->get($GLOBALS['user']->uid);
+    if (empty($this->nodes)) {
+      drupal_goto($this->getCancelPath());
+    }
+
+    $form['nodes'] = array(
+      '#theme' => 'item_list',
+      '#items' => array_map(function ($node) {
+        return String::checkPlain($node->label());
+      }, $this->nodes),
+    );
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    if ($form_state['values']['confirm'] && !empty($this->nodes)) {
+      $this->storageController->delete($this->nodes);
+      $this->tempStore->delete($GLOBALS['user']->uid);
+      $count = count($this->nodes);
+      watchdog('content', 'Deleted @count posts.', array('@count' => $count));
+      drupal_set_message(format_plural($count, 'Deleted 1 post.', 'Deleted @count posts.'));
+    }
+    $form_state['redirect'] = 'admin/content';
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/NodeBCDecorator.php b/core/modules/node/lib/Drupal/node/NodeBCDecorator.php
new file mode 100644
index 0000000..9fa6bbf
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/NodeBCDecorator.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\NodeBCDecorator.
+ */
+
+namespace Drupal\node;
+
+use Drupal\Core\Entity\EntityBCDecorator;
+
+/**
+ * Defines the node specific entity BC decorator.
+ */
+class NodeBCDecorator extends EntityBCDecorator implements NodeInterface {
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
index 7a9ffa9..3d69e6a 100644
--- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\node\NodeInterface;
+use Drupal\node\NodeBCDecorator;
 
 /**
  * Defines the node entity class.
@@ -233,4 +234,15 @@ public function getRevisionId() {
     return $this->get('vid')->value;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getBCEntity() {
+    if (!isset($this->bcEntity)) {
+      $this->getPropertyDefinitions();
+      $this->bcEntity = new NodeBCDecorator($this, $this->fieldDefinitions);
+    }
+    return $this->bcEntity;
+  }
+
 }
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/AssignOwnerNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/AssignOwnerNode.php
new file mode 100644
index 0000000..1aef687
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/AssignOwnerNode.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\AssignOwnerNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+
+/**
+ * Assigns ownership of a node to a user.
+ *
+ * @Operation(
+ *   id = "node_assign_owner_action",
+ *   label = @Translation("Change the author of content"),
+ *   type = "node"
+ * )
+ */
+class AssignOwnerNode extends OperationBase implements ConfigurableOperationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($entity) {
+    $entity->uid = $this->configuration['owner_uid'];
+    $entity->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultConfiguration() {
+    return array(
+      'owner_uid' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $description = t('The username of the user to which you would like to assign ownership.');
+    $count = db_query("SELECT COUNT(*) FROM {users}")->fetchField();
+    $owner_name = '';
+    if (is_numeric($this->configuration['owner_uid'])) {
+      $owner_name = db_query("SELECT name FROM {users} WHERE uid = :uid", array(':uid' => $this->configuration['owner_uid']))->fetchField();
+    }
+
+    // Use dropdown for fewer than 200 users; textbox for more than that.
+    if (intval($count) < 200) {
+      $options = array();
+      $result = db_query("SELECT uid, name FROM {users} WHERE uid > 0 ORDER BY name");
+      foreach ($result as $data) {
+        $options[$data->name] = $data->name;
+      }
+      $form['owner_name'] = array(
+        '#type' => 'select',
+        '#title' => t('Username'),
+        '#default_value' => $owner_name,
+        '#options' => $options,
+        '#description' => $description,
+      );
+    }
+    else {
+      $form['owner_name'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Username'),
+        '#default_value' => $owner_name,
+        '#autocomplete_path' => 'user/autocomplete',
+        '#size' => '6',
+        '#maxlength' => '60',
+        '#description' => $description,
+      );
+    }
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array &$form, array &$form_state) {
+    $exists = (bool) db_query_range('SELECT 1 FROM {users} WHERE name = :name', 0, 1, array(':name' => $form_state['values']['owner_name']))->fetchField();
+    if (!$exists) {
+      form_set_error('owner_name', t('Enter a valid username.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array &$form, array &$form_state) {
+    $this->configuration['owner_uid'] = db_query('SELECT uid from {users} WHERE name = :name', array(':name' => $form_state['values']['owner_name']))->fetchField();
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/DeleteNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/DeleteNode.php
new file mode 100644
index 0000000..25f2480
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/DeleteNode.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\DeleteNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\user\TempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Redirects to a node deletion form.
+ *
+ * @Operation(
+ *   id = "node_delete_action",
+ *   label = @Translation("Delete selected content"),
+ *   type = "node",
+ *   redirect = "admin/content/node/delete"
+ * )
+ */
+class DeleteNode extends OperationBase {
+
+  /**
+   * @var \Drupal\user\TempStore
+   */
+  protected $tempStore;
+
+  /**
+   * Constructs a DeleteNode object.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, TempStoreFactory $temp_store_factory) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->tempStore = $temp_store_factory->get('node_multiple_delete_confirm');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('user.tempstore'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    $this->tempStore->set($GLOBALS['user']->uid, $entities);
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/DemoteNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/DemoteNode.php
new file mode 100644
index 0000000..d07f24e
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/DemoteNode.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\DemoteNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Demotes a node.
+ *
+ * @Operation(
+ *   id = "node_unpromote_action",
+ *   label = @Translation("Demote selected content from front page"),
+ *   type = "node"
+ * )
+ */
+class DemoteNode extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    node_mass_update($entities, array('promote' => NODE_NOT_PROMOTED));
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/PromoteNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/PromoteNode.php
new file mode 100644
index 0000000..4758edc
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/PromoteNode.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\PromoteNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Promotes a node.
+ *
+ * @Operation(
+ *   id = "node_promote_action",
+ *   label = @Translation("Promote selected content to front page"),
+ *   type = "node"
+ * )
+ */
+class PromoteNode extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    node_mass_update($entities, array('status' => NODE_PUBLISHED, 'promote' => NODE_PROMOTED));
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/PublishNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/PublishNode.php
new file mode 100644
index 0000000..4193648
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/PublishNode.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\PublishNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Publishes a node.
+ *
+ * @Operation(
+ *   id = "node_publish_action",
+ *   label = @Translation("Publish selected content"),
+ *   type = "node"
+ * )
+ */
+class PublishNode extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    node_mass_update($entities, array('status' => NODE_PUBLISHED));
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/SaveNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/SaveNode.php
new file mode 100644
index 0000000..0a4def7
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/SaveNode.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\SaveNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Provides an operation that can save any entity.
+ *
+ * @Operation(
+ *   id = "node_save_action",
+ *   label = @Translation("Save content"),
+ *   type = "node"
+ * )
+ */
+class SaveNode extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($entity) {
+    $entity->save();
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/StickyNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/StickyNode.php
new file mode 100644
index 0000000..0448c6d
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/StickyNode.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\StickyNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Makes a node sticky.
+ *
+ * @Operation(
+ *   id = "node_make_sticky_action",
+ *   label = @Translation("Make selected content sticky"),
+ *   type = {
+ *     "node"
+ *   }
+ * )
+ */
+class StickyNode extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    node_mass_update($entities, array('status' => NODE_PUBLISHED, 'sticky' => NODE_STICKY));
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/UnpublishByKeywordNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/UnpublishByKeywordNode.php
new file mode 100644
index 0000000..b0cab00
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/UnpublishByKeywordNode.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\UnpublishByKeywordNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+
+/**
+ * Unpublishes a node containing certain keywords.
+ *
+ * @Operation(
+ *   id = "node_unpublish_by_keyword_action",
+ *   label = @Translation("Unpublish content containing keyword(s)"),
+ *   type = "node"
+ * )
+ */
+class UnpublishByKeywordNode extends OperationBase implements ConfigurableOperationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($node) {
+    foreach ($this->configuration['keywords'] as $keyword) {
+      $elements = node_view(clone $node);
+      if (strpos(drupal_render($elements), $keyword) !== FALSE || strpos($node->label(), $keyword) !== FALSE) {
+        $node->status = NODE_NOT_PUBLISHED;
+        $node->save();
+        break;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultConfiguration() {
+    return array(
+      'keywords' => array(),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form['keywords'] = array(
+      '#title' => t('Keywords'),
+      '#type' => 'textarea',
+      '#description' => t('The content will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'),
+      '#default_value' => drupal_implode_tags($this->configuration['keywords']),
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array &$form, array &$form_state) {
+    $this->configuration['keywords'] = drupal_explode_tags($form_state['values']['keywords']);
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/UnpublishNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/UnpublishNode.php
new file mode 100644
index 0000000..c406b89
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/UnpublishNode.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\UnpublishNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Unpublishes a node.
+ *
+ * @Operation(
+ *   id = "node_unpublish_action",
+ *   label = @Translation("Unpublish selected content"),
+ *   type = "node"
+ * )
+ */
+class UnpublishNode extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    node_mass_update($entities, array('status' => NODE_NOT_PUBLISHED));
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Operation/UnstickyNode.php b/core/modules/node/lib/Drupal/node/Plugin/Operation/UnstickyNode.php
new file mode 100644
index 0000000..0201228
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Operation/UnstickyNode.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Operation\UnstickyNode.
+ */
+
+namespace Drupal\node\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Makes a node not sticky.
+ *
+ * @Operation(
+ *   id = "node_make_unsticky_action",
+ *   label = @Translation("Make selected content not sticky"),
+ *   type = "node"
+ * )
+ */
+class UnstickyNode extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    node_mass_update($entities, array('sticky' => NODE_NOT_STICKY));
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/NodeBulkForm.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/NodeBulkForm.php
new file mode 100644
index 0000000..da633d7
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/NodeBulkForm.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\views\field\NodeBulkForm.
+ */
+
+namespace Drupal\node\Plugin\views\field;
+
+use Drupal\Component\Annotation\PluginID;
+use Drupal\system\Plugin\views\field\BulkFormBase;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Entity\EntityManager;
+
+/**
+ * Defines a node operations bulk form element.
+ *
+ * @PluginID("node_bulk_form")
+ */
+class NodeBulkForm extends BulkFormBase {
+
+  /**
+   * Constructs a new NodeBulkForm object.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManager $manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $manager);
+
+    // Filter the actions to only include those for the 'node' entity type.
+    $this->actions = array_filter($this->actions, function ($action) {
+      return $action->getType() == 'node';
+    });
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getBulkOptions() {
+    return array_map(function ($action) {
+      return $action->label();
+    }, $this->actions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function views_form_validate(&$form, &$form_state) {
+    $selected = array_filter($form_state['values'][$this->options['id']]);
+    if (empty($selected)) {
+      form_set_error('', t('No items selected.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function views_form_submit(&$form, &$form_state) {
+    parent::views_form_submit($form, $form_state);
+    if ($form_state['step'] == 'views_form_views_form') {
+      Cache::invalidateTags(array('content' => TRUE));
+    }
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php
index bcfbfcd..ece54f7 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php
@@ -12,6 +12,13 @@
  */
 class NodeAdminTest extends NodeTestBase {
 
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('views');
+
   public static function getInfo() {
     return array(
       'name' => 'Node administration',
@@ -39,38 +46,42 @@ function setUp() {
    */
   function testContentAdminSort() {
     $this->drupalLogin($this->admin_user);
+
+    // Create nodes that have different node.changed values.
+    $this->container->get('state')->set('node_test.storage_controller', TRUE);
+    module_enable(array('node_test'));
+    $changed = REQUEST_TIME;
     foreach (array('dd', 'aa', 'DD', 'bb', 'cc', 'CC', 'AA', 'BB') as $prefix) {
-      $this->drupalCreateNode(array('title' => $prefix . $this->randomName(6)));
+      $changed += 1000;
+      $this->drupalCreateNode(array('title' => $prefix . $this->randomName(6), 'changed' => $changed));
     }
 
     // Test that the default sort by node.changed DESC actually fires properly.
     $nodes_query = db_select('node', 'n')
-      ->fields('n', array('nid'))
+      ->fields('n', array('title'))
       ->orderBy('changed', 'DESC')
       ->execute()
       ->fetchCol();
 
-    $nodes_form = array();
     $this->drupalGet('admin/content');
-    foreach ($this->xpath('//table/tbody/tr/td/div/input/@value') as $input) {
-      $nodes_form[] = $input;
+    foreach ($nodes_query as $delta => $string) {
+      $elements = $this->xpath('//table[contains(@class, :class)]//tr[' . ($delta + 1) . ']/td[2]/a[normalize-space(text())=:label]', array(':class' => 'views-table', ':label' => $string));
+      $this->assertTrue(!empty($elements), 'The node was found in the correct order.');
     }
-    $this->assertEqual($nodes_query, $nodes_form, 'Nodes are sorted in the form according to the default query.');
 
     // Compare the rendered HTML node list to a query for the nodes ordered by
     // title to account for possible database-dependent sort order.
     $nodes_query = db_select('node', 'n')
-      ->fields('n', array('nid'))
+      ->fields('n', array('title'))
       ->orderBy('title')
       ->execute()
       ->fetchCol();
 
-    $nodes_form = array();
-    $this->drupalGet('admin/content', array('query' => array('sort' => 'asc', 'order' => 'Title')));
-    foreach ($this->xpath('//table/tbody/tr/td/div/input/@value') as $input) {
-      $nodes_form[] = $input;
+    $this->drupalGet('admin/content', array('query' => array('sort' => 'asc', 'order' => 'title')));
+    foreach ($nodes_query as $delta => $string) {
+      $elements = $this->xpath('//table[contains(@class, :class)]//tr[' . ($delta + 1) . ']/td[2]/a[normalize-space(text())=:label]', array(':class' => 'views-table', ':label' => $string));
+      $this->assertTrue(!empty($elements), 'The node was found in the correct order.');
     }
-    $this->assertEqual($nodes_query, $nodes_form, 'Nodes are sorted in the form the same as they are in the query.');
   }
 
   /**
@@ -95,30 +106,17 @@ function testContentAdminPages() {
       $this->assertLinkByHref('node/' . $node->nid);
       $this->assertLinkByHref('node/' . $node->nid . '/edit');
       $this->assertLinkByHref('node/' . $node->nid . '/delete');
-      // Verify tableselect.
-      $this->assertFieldByName('nodes[' . $node->nid . ']', '', 'Tableselect found.');
     }
 
     // Verify filtering by publishing status.
-    $edit = array(
-      'status' => 'status-1',
-    );
-    $this->drupalPost(NULL, $edit, t('Filter'));
-
-    $this->assertRaw(t('where %property is %value', array('%property' => t('status'), '%value' => 'published')), 'Content list is filtered by status.');
+    $this->drupalGet('admin/content', array('query' => array('status' => TRUE)));
 
     $this->assertLinkByHref('node/' . $nodes['published_page']->nid . '/edit');
     $this->assertLinkByHref('node/' . $nodes['published_article']->nid . '/edit');
     $this->assertNoLinkByHref('node/' . $nodes['unpublished_page_1']->nid . '/edit');
 
     // Verify filtering by status and content type.
-    $edit = array(
-      'type' => 'page',
-    );
-    $this->drupalPost(NULL, $edit, t('Refine'));
-
-    $this->assertRaw(t('where %property is %value', array('%property' => t('status'), '%value' => 'published')), 'Content list is filtered by status.');
-    $this->assertRaw(t('and where %property is %value', array('%property' => t('type'), '%value' => 'Basic page')), 'Content list is filtered by content type.');
+    $this->drupalGet('admin/content', array('query' => array('status' => TRUE, 'type' => 'page')));
 
     $this->assertLinkByHref('node/' . $nodes['published_page']->nid . '/edit');
     $this->assertNoLinkByHref('node/' . $nodes['published_article']->nid . '/edit');
diff --git a/core/modules/node/lib/Drupal/node/Tests/Views/BulkFormTest.php b/core/modules/node/lib/Drupal/node/Tests/Views/BulkFormTest.php
new file mode 100644
index 0000000..838c553
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Tests/Views/BulkFormTest.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Tests\Views\BulkFormTest.
+ */
+
+namespace Drupal\node\Tests\Views;
+
+/**
+ * Tests the views bulk form test.
+ *
+ * @see \Drupal\node\Plugin\views\field\BulkForm
+ */
+class BulkFormTest extends NodeTestBase {
+
+  /**
+   * Views used by this test.
+   *
+   * @var array
+   */
+  public static $testViews = array('test_node_bulk_form');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Node: Bulk form',
+      'description' => 'Tests a node bulk form.',
+      'group' => 'Views Modules',
+    );
+  }
+
+  /**
+   * Tests the node bulk form.
+   */
+  public function testBulkForm() {
+    $this->drupalLogin($this->drupalCreateUser(array('administer nodes')));
+    $node = $this->drupalCreateNode();
+
+    $this->drupalGet('test-node-bulk-form');
+    $elements = $this->xpath('//select[@id="edit-action"]//option');
+    $this->assertIdentical(count($elements), 8, 'All node operations are found.');
+
+    // Block a node using the bulk form.
+    $this->assertTrue($node->status);
+    $edit = array(
+      'node_bulk_form[0]' => TRUE,
+      'action' => 'node_unpublish_action',
+    );
+    $this->drupalPost(NULL, $edit, t('Apply'));
+    // Re-load the node and check their status.
+    $node = entity_load('node', $node->id());
+    $this->assertFalse($node->status);
+  }
+
+}
diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc
index 139b2d2..96f18d5 100644
--- a/core/modules/node/node.admin.inc
+++ b/core/modules/node/node.admin.inc
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\node\NodeInterface;
 
 /**
  * Page callback: Form constructor for the permission rebuild confirmation form.
@@ -31,295 +32,19 @@ function node_configure_rebuild_confirm_submit($form, &$form_state) {
 }
 
 /**
- * Implements hook_node_operations().
- */
-function node_node_operations() {
-  $operations = array(
-    'publish' => array(
-      'label' => t('Publish selected content'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED)),
-    ),
-    'unpublish' => array(
-      'label' => t('Unpublish selected content'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('status' => NODE_NOT_PUBLISHED)),
-    ),
-    'promote' => array(
-      'label' => t('Promote selected content to front page'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'promote' => NODE_PROMOTED)),
-    ),
-    'demote' => array(
-      'label' => t('Demote selected content from front page'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('promote' => NODE_NOT_PROMOTED)),
-    ),
-    'sticky' => array(
-      'label' => t('Make selected content sticky'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'sticky' => NODE_STICKY)),
-    ),
-    'unsticky' => array(
-      'label' => t('Make selected content not sticky'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('sticky' => NODE_NOT_STICKY)),
-    ),
-    'delete' => array(
-      'label' => t('Delete selected content'),
-      'callback' => NULL,
-    ),
-  );
-  return $operations;
-}
-
-/**
- * Lists node administration filters that can be applied.
- *
- * @return
- *   An associative array of filters.
- */
-function node_filters() {
-  // Regular filters
-  $filters['status'] = array(
-    'title' => t('status'),
-    'options' => array(
-      '[any]' => t('any'),
-      'status-1' => t('published'),
-      'status-0' => t('not published'),
-      'promote-1' => t('promoted'),
-      'promote-0' => t('not promoted'),
-      'sticky-1' => t('sticky'),
-      'sticky-0' => t('not sticky'),
-    ),
-  );
-  // Include translation states if we have this module enabled
-  if (module_exists('translation')) {
-    $filters['status']['options'] += array(
-      'translate-0' => t('Up to date translation'),
-      'translate-1' => t('Outdated translation'),
-    );
-  }
-
-  $filters['type'] = array(
-    'title' => t('type'),
-    'options' => array(
-      '[any]' => t('any'),
-    ) + node_type_get_names(),
-  );
-
-  // Language filter if language support is present.
-  if (language_multilingual()) {
-    $languages = language_list(LANGUAGE_ALL);
-    foreach ($languages as $langcode => $language) {
-      // Make locked languages appear special in the list.
-      $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name;
-    }
-    $filters['langcode'] = array(
-      'title' => t('language'),
-      'options' => array(
-        '[any]' => t('- Any -'),
-      ) + $language_options,
-    );
-  }
-  return $filters;
-}
-
-/**
- * Applies filters for the node administration overview based on session.
- *
- * @param Drupal\Core\Database\Query\SelectInterface $query
- *   A SelectQuery to which the filters should be applied.
- */
-function node_build_filter_query(SelectInterface $query) {
-  // Build query
-  $filter_data = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array();
-  foreach ($filter_data as $index => $filter) {
-    list($key, $value) = $filter;
-    switch ($key) {
-      case 'status':
-        // Note: no exploitable hole as $key/$value have already been checked when submitted
-        list($key, $value) = explode('-', $value, 2);
-      case 'type':
-      case 'langcode':
-        $query->condition('n.' . $key, $value);
-        break;
-    }
-  }
-}
-
-/**
- * Returns the node administration filters form array to node_admin_content().
- *
- * @see node_admin_nodes()
- * @see node_admin_nodes_submit()
- * @see node_admin_nodes_validate()
- * @see node_filter_form_submit()
- * @see node_multiple_delete_confirm()
- * @see node_multiple_delete_confirm_submit()
- *
- * @ingroup forms
- */
-function node_filter_form() {
-  $session = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array();
-  $filters = node_filters();
-
-  $i = 0;
-  $form['filters'] = array(
-    '#type' => 'details',
-    '#title' => t('Show only items where'),
-    '#theme' => 'exposed_filters__node',
-  );
-  foreach ($session as $filter) {
-    list($type, $value) = $filter;
-    if ($type == 'term') {
-      // Load term name from DB rather than search and parse options array.
-      $value = module_invoke('taxonomy', 'term_load', $value);
-      $value = $value->name;
-    }
-    elseif ($type == 'langcode') {
-      $value = language_name($value);
-    }
-    else {
-      $value = $filters[$type]['options'][$value];
-    }
-    $t_args = array('%property' => $filters[$type]['title'], '%value' => $value);
-    if ($i++) {
-      $form['filters']['current'][] = array('#markup' => t('and where %property is %value', $t_args));
-    }
-    else {
-      $form['filters']['current'][] = array('#markup' => t('where %property is %value', $t_args));
-    }
-    if (in_array($type, array('type', 'langcode'))) {
-      // Remove the option if it is already being filtered on.
-      unset($filters[$type]);
-    }
-  }
-
-  $form['filters']['status'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('clearfix')),
-    '#prefix' => ($i ? '<div class="additional-filters">' . t('and where') . '</div>' : ''),
-  );
-  $form['filters']['status']['filters'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('filters')),
-  );
-  foreach ($filters as $key => $filter) {
-    $form['filters']['status']['filters'][$key] = array(
-      '#type' => 'select',
-      '#options' => $filter['options'],
-      '#title' => $filter['title'],
-      '#default_value' => '[any]',
-    );
-  }
-
-  $form['filters']['status']['actions'] = array(
-    '#type' => 'actions',
-    '#attributes' => array('class' => array('container-inline')),
-  );
-  $form['filters']['status']['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => count($session) ? t('Refine') : t('Filter'),
-  );
-  if (count($session)) {
-    $form['filters']['status']['actions']['undo'] = array('#type' => 'submit', '#value' => t('Undo'));
-    $form['filters']['status']['actions']['reset'] = array('#type' => 'submit', '#value' => t('Reset'));
-  }
-
-  $form['#attached']['library'][] = array('system', 'drupal.form');
-
-  return $form;
-}
-
-/**
- * Form submission handler for node_filter_form().
- *
- * @see node_admin_content()
- * @see node_admin_nodes()
- * @see node_admin_nodes_submit()
- * @see node_admin_nodes_validate()
- * @see node_filter_form()
- * @see node_multiple_delete_confirm()
- * @see node_multiple_delete_confirm_submit()
- */
-function node_filter_form_submit($form, &$form_state) {
-  $filters = node_filters();
-  switch ($form_state['values']['op']) {
-    case t('Filter'):
-    case t('Refine'):
-      // Apply every filter that has a choice selected other than 'any'.
-      foreach ($filters as $filter => $options) {
-        if (isset($form_state['values'][$filter]) && $form_state['values'][$filter] != '[any]') {
-          $_SESSION['node_overview_filter'][] = array($filter, $form_state['values'][$filter]);
-        }
-      }
-      break;
-    case t('Undo'):
-      array_pop($_SESSION['node_overview_filter']);
-      break;
-    case t('Reset'):
-      $_SESSION['node_overview_filter'] = array();
-      break;
-  }
-}
-
-/**
- * Updates all nodes in the passed-in array with the passed-in field values.
- *
- * IMPORTANT NOTE: This function is intended to work when called from a form
- * submission handler. Calling it outside of the form submission process may not
- * work correctly.
- *
- * @param array $nodes
- *   Array of node nids to update.
- * @param array $updates
- *   Array of key/value pairs with node field names and the value to update that
- *   field to.
- */
-function node_mass_update($nodes, $updates) {
-  // We use batch processing to prevent timeout when updating a large number
-  // of nodes.
-  if (count($nodes) > 10) {
-    $batch = array(
-      'operations' => array(
-        array('_node_mass_update_batch_process', array($nodes, $updates))
-      ),
-      'finished' => '_node_mass_update_batch_finished',
-      'title' => t('Processing'),
-      // We use a single multi-pass operation, so the default
-      // 'Remaining x of y operations' message will be confusing here.
-      'progress_message' => '',
-      'error_message' => t('The update has encountered an error.'),
-      // The operations do not live in the .module file, so we need to
-      // tell the batch engine which file to load before calling them.
-      'file' => drupal_get_path('module', 'node') . '/node.admin.inc',
-    );
-    batch_set($batch);
-  }
-  else {
-    foreach ($nodes as $nid) {
-      _node_mass_update_helper($nid, $updates);
-    }
-    drupal_set_message(t('The update has been performed.'));
-  }
-}
-
-/**
  * Updates individual nodes when fewer than 10 are queued.
  *
- * @param $nid
- *   ID of node to update.
- * @param $updates
+ * @param \Drupal\node\NodeInterface $node
+ *   A node to update.
+ * @param array $updates
  *   Associative array of updates.
  *
- * @return object
+ * @return \Drupal\node\NodeInterface
  *   An updated node object.
  *
  * @see node_mass_update()
  */
-function _node_mass_update_helper($nid, $updates) {
-  $node = node_load($nid, TRUE);
+function _node_mass_update_helper(NodeInterface $node, array $updates) {
   // For efficiency manually save the original node before applying any changes.
   $node->original = clone $node;
   foreach ($updates as $name => $value) {
@@ -336,10 +61,13 @@ function _node_mass_update_helper($nid, $updates) {
  *   An array of node IDs.
  * @param array $updates
  *   Associative array of updates.
+ * @param bool $load
+ *   TRUE if $nodes contains an array of node IDs to be loaded, FALSE if it
+ *   contains fully loaded nodes.
  * @param array $context
  *   An array of contextual key/values.
  */
-function _node_mass_update_batch_process($nodes, $updates, &$context) {
+function _node_mass_update_batch_process(array $nodes, array $updates, $load, array &$context) {
   if (!isset($context['sandbox']['progress'])) {
     $context['sandbox']['progress'] = 0;
     $context['sandbox']['max'] = count($nodes);
@@ -350,8 +78,11 @@ function _node_mass_update_batch_process($nodes, $updates, &$context) {
   $count = min(5, count($context['sandbox']['nodes']));
   for ($i = 1; $i <= $count; $i++) {
     // For each nid, load the node, reset the values, and save it.
-    $nid = array_shift($context['sandbox']['nodes']);
-    $node = _node_mass_update_helper($nid, $updates);
+    $node = array_shift($context['sandbox']['nodes']);
+    if ($load) {
+      $node = entity_load('node', $node);
+    }
+    $node = _node_mass_update_helper($node, $updates);
 
     // Store result for post-processing in the finished callback.
     $context['results'][] = l($node->label(), 'node/' . $node->nid);
@@ -391,68 +122,11 @@ function _node_mass_update_batch_finished($success, $results, $operations) {
 }
 
 /**
- * Page callback: Form constructor for the content administration form.
- *
- * @see node_admin_nodes()
- * @see node_admin_nodes_submit()
- * @see node_admin_nodes_validate()
- * @see node_filter_form()
- * @see node_filter_form_submit()
- * @see node_menu()
- * @see node_multiple_delete_confirm()
- * @see node_multiple_delete_confirm_submit()
- * @ingroup forms
- */
-function node_admin_content($form, $form_state) {
-  if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') {
-    return node_multiple_delete_confirm($form, $form_state, array_filter($form_state['values']['nodes']));
-  }
-  $form['filter'] = node_filter_form();
-  $form['#submit'][] = 'node_filter_form_submit';
-  $form['admin'] = node_admin_nodes();
-
-  return $form;
-}
-
-/**
  * Returns the admin form object to node_admin_content().
  *
- * @see node_admin_nodes_submit()
- * @see node_filter_form()
- * @see node_filter_form_submit()
- * @see node_multiple_delete_confirm()
- * @see node_multiple_delete_confirm_submit()
- *
  * @ingroup forms
  */
 function node_admin_nodes() {
-  $admin_access = user_access('administer nodes');
-
-  // Build the 'Update options' form.
-  $form['options'] = array(
-    '#type' => 'details',
-    '#title' => t('Update options'),
-    '#attributes' => array('class' => array('container-inline')),
-    '#access' => $admin_access,
-  );
-  $options = array();
-  foreach (module_invoke_all('node_operations') as $operation => $array) {
-    $options[$operation] = $array['label'];
-  }
-  $form['options']['operation'] = array(
-    '#type' => 'select',
-    '#title' => t('Operation'),
-    '#title_display' => 'invisible',
-    '#options' => $options,
-    '#default_value' => 'approve',
-  );
-  $form['options']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Update'),
-    '#tableselect' => TRUE,
-    '#submit' => array('node_admin_nodes_submit'),
-  );
-
   // Enable language column and filter if multiple languages are enabled.
   $multilingual = language_multilingual();
 
@@ -490,7 +164,6 @@ function node_admin_nodes() {
   $query = db_select('node', 'n')
     ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
     ->extend('Drupal\Core\Database\Query\TableSortExtender');
-  node_build_filter_query($query);
 
   if (!user_access('bypass node access')) {
     // If the user is able to view their own unpublished nodes, allow them
@@ -519,35 +192,35 @@ function node_admin_nodes() {
   // Prepare the list of nodes.
   $languages = language_list(LANGUAGE_ALL);
   $destination = drupal_get_destination();
-  $form['nodes'] = array(
+  $build['nodes'] = array(
     '#type' => 'table',
     '#header' => $header,
     '#empty' => t('No content available.'),
   );
   foreach ($nodes as $node) {
     $l_options = $node->langcode != LANGUAGE_NOT_SPECIFIED && isset($languages[$node->langcode]) ? array('language' => $languages[$node->langcode]) : array();
-    $form['nodes'][$node->nid]['title'] = array(
+    $build['nodes'][$node->nid]['title'] = array(
       '#type' => 'link',
       '#title' => $node->label(),
       '#href' => 'node/' . $node->nid,
       '#options' => $l_options,
       '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))),
     );
-    $form['nodes'][$node->nid]['type'] = array(
+    $build['nodes'][$node->nid]['type'] = array(
       '#markup' => check_plain(node_get_type_label($node)),
     );
-    $form['nodes'][$node->nid]['author'] = array(
+    $build['nodes'][$node->nid]['author'] = array(
       '#theme' => 'username',
       '#account' => $node,
     );
-    $form['nodes'][$node->nid]['status'] = array(
+    $build['nodes'][$node->nid]['status'] = array(
       '#markup' => $node->status ? t('published') : t('not published'),
     );
-    $form['nodes'][$node->nid]['changed'] = array(
+    $build['nodes'][$node->nid]['changed'] = array(
       '#markup' => format_date($node->changed, 'short'),
     );
     if ($multilingual) {
-      $form['nodes'][$node->nid]['language_name'] = array(
+      $build['nodes'][$node->nid]['language_name'] = array(
         '#markup' => language_name($node->langcode),
       );
     }
@@ -575,10 +248,10 @@ function node_admin_nodes() {
         'query' => $destination,
       );
     }
-    $form['nodes'][$node->nid]['operations'] = array();
+    $build['nodes'][$node->nid]['operations'] = array();
     if (count($operations) > 1) {
       // Render an unordered list of operations links.
-      $form['nodes'][$node->nid]['operations'] = array(
+      $build['nodes'][$node->nid]['operations'] = array(
         '#type' => 'operations',
         '#subtype' => 'node',
         '#links' => $operations,
@@ -587,7 +260,7 @@ function node_admin_nodes() {
     elseif (!empty($operations)) {
       // Render the first and only operation as a link.
       $link = reset($operations);
-      $form['nodes'][$node->nid]['operations'] = array(
+      $build['nodes'][$node->nid]['operations'] = array(
         '#type' => 'link',
         '#title' => $link['title'],
         '#href' => $link['href'],
@@ -596,102 +269,6 @@ function node_admin_nodes() {
     }
   }
 
-  // Only use a tableselect when the current user is able to perform any
-  // operations.
-  if ($admin_access) {
-    $form['nodes']['#tableselect'] = TRUE;
-  }
-
-  $form['pager'] = array('#theme' => 'pager');
-  return $form;
-}
-
-/**
- * Form submission handler for node_admin_nodes().
- *
- * Executes the chosen 'Update option' on the selected nodes.
- *
- * @see node_admin_nodes()
- * @see node_admin_nodes_validate()
- * @see node_filter_form()
- * @see node_filter_form_submit()
- * @see node_multiple_delete_confirm()
- * @see node_multiple_delete_confirm_submit()
- */
-function node_admin_nodes_submit($form, &$form_state) {
-  $operations = module_invoke_all('node_operations');
-  $operation = $operations[$form_state['values']['operation']];
-  // Filter out unchecked nodes
-  $nodes = array_filter($form_state['values']['nodes']);
-  if ($function = $operation['callback']) {
-    // Add in callback arguments if present.
-    if (isset($operation['callback arguments'])) {
-      $args = array_merge(array($nodes), $operation['callback arguments']);
-    }
-    else {
-      $args = array($nodes);
-    }
-    call_user_func_array($function, $args);
-
-    cache_invalidate_tags(array('content' => TRUE));
-  }
-  else {
-    // We need to rebuild the form to go to a second step. For example, to
-    // show the confirmation form for the deletion of nodes.
-    $form_state['rebuild'] = TRUE;
-  }
-}
-
-/**
- * Multiple node deletion confirmation form for node_admin_content().
- *
- * @see node_admin_nodes()
- * @see node_admin_nodes_submit()
- * @see node_admin_nodes_validate()
- * @see node_filter_form()
- * @see node_filter_form_submit()
- * @see node_multiple_delete_confirm_submit()
- * @ingroup forms
- */
-function node_multiple_delete_confirm($form, &$form_state, $nodes) {
-  $form['nodes'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
-  // array_filter returns only elements with TRUE values
-  foreach ($nodes as $nid => $value) {
-    $title = db_query('SELECT title FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchField();
-    $form['nodes'][$nid] = array(
-      '#type' => 'hidden',
-      '#value' => $nid,
-      '#prefix' => '<li>',
-      '#suffix' => check_plain($title) . "</li>\n",
-    );
-  }
-  $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
-  $form['#submit'][] = 'node_multiple_delete_confirm_submit';
-  $confirm_question = format_plural(count($nodes),
-                                  'Are you sure you want to delete this item?',
-                                  'Are you sure you want to delete these items?');
-  return confirm_form($form,
-                    $confirm_question,
-                    'admin/content', t('This action cannot be undone.'),
-                    t('Delete'), t('Cancel'));
-}
-
-/**
- * Form submission handler for node_multiple_delete_confirm().
- *
- * @see node_admin_nodes()
- * @see node_admin_nodes_submit()
- * @see node_admin_nodes_validate()
- * @see node_filter_form()
- * @see node_filter_form_submit()
- * @see node_multiple_delete_confirm()
- */
-function node_multiple_delete_confirm_submit($form, &$form_state) {
-  if ($form_state['values']['confirm']) {
-    node_delete_multiple(array_keys($form_state['values']['nodes']));
-    $count = count($form_state['values']['nodes']);
-    watchdog('content', 'Deleted @count posts.', array('@count' => $count));
-    drupal_set_message(format_plural($count, 'Deleted 1 post.', 'Deleted @count posts.'));
-  }
-  $form_state['redirect'] = 'admin/content';
+  $build['pager'] = array('#theme' => 'pager');
+  return $build;
 }
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index 78a3139..88e1ddf 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -402,64 +402,6 @@ function hook_node_grants_alter(&$grants, $account, $op) {
 }
 
 /**
- * Add mass node operations.
- *
- * This hook enables modules to inject custom operations into the mass
- * operations dropdown found at admin/content, by associating a callback
- * function with the operation, which is called when the form is submitted. The
- * callback function receives one initial argument, which is an array of the
- * checked nodes.
- *
- * @return
- *   An array of operations. Each operation is an associative array that may
- *   contain the following key-value pairs:
- *   - label: (required) The label for the operation, displayed in the dropdown
- *     menu.
- *   - callback: (required) The function to call for the operation.
- *   - callback arguments: (optional) An array of additional arguments to pass
- *     to the callback function.
- */
-function hook_node_operations() {
-  $operations = array(
-    'publish' => array(
-      'label' => t('Publish selected content'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED)),
-    ),
-    'unpublish' => array(
-      'label' => t('Unpublish selected content'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('status' => NODE_NOT_PUBLISHED)),
-    ),
-    'promote' => array(
-      'label' => t('Promote selected content to front page'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'promote' => NODE_PROMOTED)),
-    ),
-    'demote' => array(
-      'label' => t('Demote selected content from front page'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('promote' => NODE_NOT_PROMOTED)),
-    ),
-    'sticky' => array(
-      'label' => t('Make selected content sticky'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'sticky' => NODE_STICKY)),
-    ),
-    'unsticky' => array(
-      'label' => t('Make selected content not sticky'),
-      'callback' => 'node_mass_update',
-      'callback arguments' => array('updates' => array('sticky' => NODE_NOT_STICKY)),
-    ),
-    'delete' => array(
-      'label' => t('Delete selected content'),
-      'callback' => NULL,
-    ),
-  );
-  return $operations;
-}
-
-/**
  * Act before node deletion.
  *
  * This hook is invoked from node_delete_multiple() after the type-specific
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index ae25bb1..7fa870e 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -1426,24 +1426,22 @@ function node_user_cancel($edit, $account, $method) {
   switch ($method) {
     case 'user_cancel_block_unpublish':
       // Unpublish nodes (current revisions).
-      module_load_include('inc', 'node', 'node.admin');
       $nodes = db_select('node', 'n')
         ->fields('n', array('nid'))
         ->condition('uid', $account->uid)
         ->execute()
         ->fetchCol();
-      node_mass_update($nodes, array('status' => 0));
+      node_mass_update($nodes, array('status' => 0), TRUE);
       break;
 
     case 'user_cancel_reassign':
       // Anonymize nodes (current revisions).
-      module_load_include('inc', 'node', 'node.admin');
       $nodes = db_select('node', 'n')
         ->fields('n', array('nid'))
         ->condition('uid', $account->uid)
         ->execute()
         ->fetchCol();
-      node_mass_update($nodes, array('uid' => 0));
+      node_mass_update($nodes, array('uid' => 0), TRUE);
       // Anonymize old revisions.
       db_update('node_revision')
         ->fields(array('uid' => 0))
@@ -1454,6 +1452,54 @@ function node_user_cancel($edit, $account, $method) {
 }
 
 /**
+ * Updates all nodes in the passed-in array with the passed-in field values.
+ *
+ * IMPORTANT NOTE: This function is intended to work when called from a form
+ * submission handler. Calling it outside of the form submission process may not
+ * work correctly.
+ *
+ * @param array $nodes
+ *   Array of node nids or nodes to update.
+ * @param array $updates
+ *   Array of key/value pairs with node field names and the value to update that
+ *   field to.
+ * @param bool $load
+ *   (optional) TRUE if $nodes contains an array of node IDs to be loaded, FALSE
+ *   if it contains fully loaded nodes. Defaults to FALSE.
+ */
+function node_mass_update(array $nodes, array $updates, $load = FALSE) {
+  module_load_include('admin.inc', 'node');
+  // We use batch processing to prevent timeout when updating a large number
+  // of nodes.
+  if (count($nodes) > 10) {
+    $batch = array(
+      'operations' => array(
+        array('_node_mass_update_batch_process', array($nodes, $updates, $load))
+      ),
+      'finished' => '_node_mass_update_batch_finished',
+      'title' => t('Processing'),
+      // We use a single multi-pass operation, so the default
+      // 'Remaining x of y operations' message will be confusing here.
+      'progress_message' => '',
+      'error_message' => t('The update has encountered an error.'),
+      // The operations do not live in the .module file, so we need to
+      // tell the batch engine which file to load before calling them.
+      'file' => drupal_get_path('module', 'node') . '/node.admin.inc',
+    );
+    batch_set($batch);
+  }
+  else {
+    if ($load) {
+      $nodes = entity_load_multiple('node', $nodes);
+    }
+    foreach ($nodes as $node) {
+      _node_mass_update_helper($node, $updates);
+    }
+    drupal_set_message(t('The update has been performed.'));
+  }
+}
+
+/**
  * Implements hook_user_predelete().
  */
 function node_user_predelete($account) {
@@ -1613,8 +1659,7 @@ function node_menu() {
   $items['admin/content'] = array(
     'title' => 'Content',
     'description' => 'Find and manage content.',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('node_admin_content'),
+    'page callback' => 'node_admin_nodes',
     'access arguments' => array('access content overview'),
     'weight' => -10,
     'file' => 'node.admin.inc',
@@ -3126,332 +3171,6 @@ function node_content_form(EntityInterface $node, $form_state) {
  */
 
 /**
- * Implements hook_action_info().
- */
-function node_action_info() {
-  return array(
-    'node_publish_action' => array(
-      'type' => 'node',
-      'label' => t('Publish content'),
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('node_presave', 'comment_insert', 'comment_update', 'comment_delete'),
-    ),
-    'node_unpublish_action' => array(
-      'type' => 'node',
-      'label' => t('Unpublish content'),
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('node_presave', 'comment_insert', 'comment_update', 'comment_delete'),
-    ),
-    'node_make_sticky_action' => array(
-      'type' => 'node',
-      'label' => t('Make content sticky'),
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('node_presave', 'comment_insert', 'comment_update', 'comment_delete'),
-    ),
-    'node_make_unsticky_action' => array(
-      'type' => 'node',
-      'label' => t('Make content unsticky'),
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('node_presave', 'comment_insert', 'comment_update', 'comment_delete'),
-    ),
-    'node_promote_action' => array(
-      'type' => 'node',
-      'label' => t('Promote content to front page'),
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('node_presave', 'comment_insert', 'comment_update', 'comment_delete'),
-    ),
-    'node_unpromote_action' => array(
-      'type' => 'node',
-      'label' => t('Remove content from front page'),
-      'configurable' => FALSE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('node_presave', 'comment_insert', 'comment_update', 'comment_delete'),
-    ),
-    'node_assign_owner_action' => array(
-      'type' => 'node',
-      'label' => t('Change the author of content'),
-      'configurable' => TRUE,
-      'behavior' => array('changes_property'),
-      'triggers' => array('node_presave', 'comment_insert', 'comment_update', 'comment_delete'),
-    ),
-    'node_save_action' => array(
-      'type' => 'node',
-      'label' => t('Save content'),
-      'configurable' => FALSE,
-      'triggers' => array('comment_insert', 'comment_update', 'comment_delete'),
-    ),
-    'node_unpublish_by_keyword_action' => array(
-      'type' => 'node',
-      'label' => t('Unpublish content containing keyword(s)'),
-      'configurable' => TRUE,
-      'triggers' => array('node_presave', 'node_insert', 'node_update'),
-    ),
-  );
-}
-
-/**
- * Sets the status of a node to 1 (published).
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity.
- * @param $context
- *   (optional) Array of additional information about what triggered the action.
- *   Not used for this action.
- *
- * @ingroup actions
- */
-function node_publish_action(EntityInterface $node, $context = array()) {
-  $node->status = NODE_PUBLISHED;
-  watchdog('action', 'Set @type %title to published.', array('@type' => node_get_type_label($node), '%title' => $node->label()));
-}
-
-/**
- * Sets the status of a node to 0 (unpublished).
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity.
- * @param $context
- *   (optional) Array of additional information about what triggered the action.
- *   Not used for this action.
- *
- * @ingroup actions
- */
-function node_unpublish_action(EntityInterface $node, $context = array()) {
-  $node->status = NODE_NOT_PUBLISHED;
-  watchdog('action', 'Set @type %title to unpublished.', array('@type' => node_get_type_label($node), '%title' => $node->label()));
-}
-
-/**
- * Sets the sticky-at-top-of-list property of a node to 1.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity.
- * @param $context
- *   (optional) Array of additional information about what triggered the action.
- *   Not used for this action.
- *
- * @ingroup actions
- */
-function node_make_sticky_action(EntityInterface $node, $context = array()) {
-  $node->sticky = NODE_STICKY;
-  watchdog('action', 'Set @type %title to sticky.', array('@type' => node_get_type_label($node), '%title' => $node->label()));
-}
-
-/**
- * Sets the sticky-at-top-of-list property of a node to 0.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity.
- * @param $context
- *   (optional) Array of additional information about what triggered the action.
- *   Not used for this action.
- *
- * @ingroup actions
- */
-function node_make_unsticky_action(EntityInterface $node, $context = array()) {
-  $node->sticky = NODE_NOT_STICKY;
-  watchdog('action', 'Set @type %title to unsticky.', array('@type' => node_get_type_label($node), '%title' => $node->label()));
-}
-
-/**
- * Sets the promote property of a node to 1.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity.
- * @param $context
- *   (optional) Array of additional information about what triggered the action.
- *   Not used for this action.
- *
- * @ingroup actions
- */
-function node_promote_action(EntityInterface $node, $context = array()) {
-  $node->promote = NODE_PROMOTED;
-  watchdog('action', 'Promoted @type %title to front page.', array('@type' => node_get_type_label($node), '%title' => $node->label()));
-}
-
-/**
- * Sets the promote property of a node to 0.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity.
- * @param $context
- *   (optional) Array of additional information about what triggered the action.
- *   Not used for this action.
- *
- * @ingroup actions
- */
-function node_unpromote_action(EntityInterface $node, $context = array()) {
-  $node->promote = NODE_NOT_PROMOTED;
-  watchdog('action', 'Removed @type %title from front page.', array('@type' => node_get_type_label($node), '%title' => $node->label()));
-}
-
-/**
- * Saves a node.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   The node to be saved.
- *
- * @ingroup actions
- */
-function node_save_action(EntityInterface $node) {
-  $node->save();
-  watchdog('action', 'Saved @type %title', array('@type' => node_get_type_label($node), '%title' => $node->label()));
-}
-
-/**
- * Assigns ownership of a node to a user.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity to modify.
- * @param $context
- *   Array of additional information about what triggered the action. Includes
- *   the following elements:
- *   - owner_uid: User ID to assign to the node.
- *
- * @see node_assign_owner_action_form()
- * @see node_assign_owner_action_validate()
- * @see node_assign_owner_action_submit()
- * @ingroup actions
- */
-function node_assign_owner_action(EntityInterface $node, $context) {
-  $node->uid = $context['owner_uid'];
-  $owner_name = db_query("SELECT name FROM {users} WHERE uid = :uid", array(':uid' => $context['owner_uid']))->fetchField();
-  watchdog('action', 'Changed owner of @type %title to uid %name.', array('@type' =>  node_get_type_label($node), '%title' => $node->label(), '%name' => $owner_name));
-}
-
-/**
- * Form constructor for the settings form for node_assign_owner_action().
- *
- * @param $context
- *   Array of additional information about what triggered the action. Includes
- *   the following elements:
- *   - owner_uid: User ID to assign to the node.
- *
- * @see node_assign_owner_action_submit()
- * @see node_assign_owner_action_validate()
- * @ingroup forms
- */
-function node_assign_owner_action_form($context) {
-  $description = t('The username of the user to which you would like to assign ownership.');
-  $count = db_query("SELECT COUNT(*) FROM {users}")->fetchField();
-  $owner_name = '';
-  if (isset($context['owner_uid'])) {
-    $owner_name = db_query("SELECT name FROM {users} WHERE uid = :uid", array(':uid' => $context['owner_uid']))->fetchField();
-  }
-
-  // Use dropdown for fewer than 200 users; textbox for more than that.
-  if (intval($count) < 200) {
-    $options = array();
-    $result = db_query("SELECT uid, name FROM {users} WHERE uid > 0 ORDER BY name");
-    foreach ($result as $data) {
-      $options[$data->name] = $data->name;
-    }
-    $form['owner_name'] = array(
-      '#type' => 'select',
-      '#title' => t('Username'),
-      '#default_value' => $owner_name,
-      '#options' => $options,
-      '#description' => $description,
-    );
-  }
-  else {
-    $form['owner_name'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Username'),
-      '#default_value' => $owner_name,
-      '#autocomplete_path' => 'user/autocomplete',
-      '#size' => '6',
-      '#maxlength' => '60',
-      '#description' => $description,
-    );
-  }
-  return $form;
-}
-
-/**
- * Form validation handler for node_assign_owner_action_form().
- *
- * @see node_assign_owner_action_submit()
- */
-function node_assign_owner_action_validate($form, $form_state) {
-  $exists = (bool) db_query_range('SELECT 1 FROM {users} WHERE name = :name', 0, 1, array(':name' => $form_state['values']['owner_name']))->fetchField();
-  if (!$exists) {
-    form_set_error('owner_name', t('Enter a valid username.'));
-  }
-}
-
-/**
- * Form submission handler for node_assign_owner_action_form().
- *
- * @see node_assign_owner_action_validate()
- */
-function node_assign_owner_action_submit($form, $form_state) {
-  // Username can change, so we need to store the ID, not the username.
-  $uid = db_query('SELECT uid from {users} WHERE name = :name', array(':name' => $form_state['values']['owner_name']))->fetchField();
-  return array('owner_uid' => $uid);
-}
-
-/**
- * Generates settings form for node_unpublish_by_keyword_action().
- *
- * @param array $context
- *   Array of additional information about what triggered this action.
- *
- * @return array
- *   A form array.
- *
- * @see node_unpublish_by_keyword_action_submit()
- */
-function node_unpublish_by_keyword_action_form($context) {
-  $form['keywords'] = array(
-    '#title' => t('Keywords'),
-    '#type' => 'textarea',
-    '#description' => t('The content will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'),
-    '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
-  );
-  return $form;
-}
-
-/**
- * Form submission handler for node_unpublish_by_keyword_action().
- */
-function node_unpublish_by_keyword_action_submit($form, $form_state) {
-  return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
-}
-
-/**
- * Unpublishes a node containing certain keywords.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   A node entity to modify.
- * @param $context
- *   Array of additional information about what triggered the action. Includes
- *   the following elements:
- *   - keywords: Array of keywords. If any keyword is present in the rendered
- *     node, the node's status flag is set to unpublished.
- *
- * @see node_unpublish_by_keyword_action_form()
- * @see node_unpublish_by_keyword_action_submit()
- *
- * @ingroup actions
- */
-function node_unpublish_by_keyword_action(EntityInterface $node, $context) {
-  foreach ($context['keywords'] as $keyword) {
-    $elements = node_view(clone $node);
-    if (strpos(drupal_render($elements), $keyword) !== FALSE || strpos($node->label(), $keyword) !== FALSE) {
-      $node->status = NODE_NOT_PUBLISHED;
-      watchdog('action', 'Set @type %title to unpublished.', array('@type' => node_get_type_label($node), '%title' => $node->label()));
-      break;
-    }
-  }
-}
-
-/**
  * Implements hook_requirements().
  */
 function node_requirements($phase) {
diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml
new file mode 100644
index 0000000..0ba0ec2
--- /dev/null
+++ b/core/modules/node/node.routing.yml
@@ -0,0 +1,7 @@
+node_multiple_delete_confirm:
+  pattern: '/admin/content/node/delete'
+  defaults:
+    _form: '\Drupal\node\Form\DeleteMultiple'
+  requirements:
+    _permission: 'administer nodes'
+
diff --git a/core/modules/node/node.views.inc b/core/modules/node/node.views.inc
index 6f3320f..f344cfe 100644
--- a/core/modules/node/node.views.inc
+++ b/core/modules/node/node.views.inc
@@ -219,6 +219,14 @@ function node_views_data() {
     );
   }
 
+  $data['node']['node_bulk_form'] = array(
+    'title' => t('Node operations bulk form'),
+    'help' => t('Add a form element that lets you run operations on multiple nodes.'),
+    'field' => array(
+      'id' => 'node_bulk_form',
+    ),
+  );
+
   // Define some fields based upon views_handler_field_entity in the entity
   // table so they can be re-used with other query backends.
   // @see views_handler_field_entity
diff --git a/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php b/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php
new file mode 100644
index 0000000..3e58980
--- /dev/null
+++ b/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node_test\NodeTestStorageController.
+ */
+
+namespace Drupal\node_test;
+
+use Drupal\node\NodeStorageController;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Provides a test storage controller for nodes.
+ */
+class NodeTestStorageController extends NodeStorageController {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function preSave(EntityInterface $node) {
+    // Allow test nodes to specify their updated ('changed') time.
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_test/node_test.module b/core/modules/node/tests/modules/node_test/node_test.module
index f3fd04b..cc795dc 100644
--- a/core/modules/node/tests/modules/node_test/node_test.module
+++ b/core/modules/node/tests/modules/node_test/node_test.module
@@ -180,3 +180,12 @@ function node_test_node_insert(EntityInterface $node) {
     node_save($node);
   }
 }
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function node_test_entity_info_alter(&$entity_info) {
+  if (Drupal::state()->get('node_test.storage_controller')) {
+    $entity_info['node']['controllers']['storage'] = 'Drupal\node_test\NodeTestStorageController';
+  }
+}
diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml
new file mode 100644
index 0000000..9a0bec6
--- /dev/null
+++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml
@@ -0,0 +1,45 @@
+base_field: nid
+base_table: node
+core: 8.x
+description: ''
+status: '1'
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: ''
+    display_options:
+      style:
+        type: table
+      row:
+        type: fields
+      fields:
+        node_bulk_form:
+          id: node_bulk_form
+          table: node
+          field: node_bulk_form
+          plugin_id: node_bulk_form
+        title:
+          id: title
+          table: node
+          field: title
+          plugin_id: node
+      sorts:
+        nid:
+          id: nid
+          table: node
+          field: nid
+          order: ASC
+          plugin_id: standard
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: ''
+    display_options:
+      path: test-node-bulk-form
+label: ''
+module: views
+id: test_node_bulk_form
+tag: ''
diff --git a/core/modules/openid/lib/Drupal/openid/Tests/OpenIDFunctionalTest.php b/core/modules/openid/lib/Drupal/openid/Tests/OpenIDFunctionalTest.php
index 0479793..176c215 100644
--- a/core/modules/openid/lib/Drupal/openid/Tests/OpenIDFunctionalTest.php
+++ b/core/modules/openid/lib/Drupal/openid/Tests/OpenIDFunctionalTest.php
@@ -262,13 +262,9 @@ function testBlockedUserLogin() {
     // Log in as an admin user and block the account.
     $admin_user = $this->drupalCreateUser(array('administer users'));
     $this->drupalLogin($admin_user);
-    $this->drupalGet('admin/people');
-    $edit = array(
-      'operation' => 'block',
-      'accounts[' . $this->web_user->uid . ']' => TRUE,
-    );
-    $this->drupalPost('admin/people', $edit, t('Update'));
-    $this->assertRaw('The update has been performed.', 'Account was blocked.');
+
+    $this->drupalPost("user/{$this->web_user->id()}/cancel", array(), t('Cancel account'));
+    $this->assertRaw(t('%user has been disabled.', array('%user' => $this->web_user->name) ), 'Account was blocked.');
     $this->drupalLogout();
 
     $this->submitLoginForm($identity);
diff --git a/core/modules/system/lib/Drupal/system/ActionInterface.php b/core/modules/system/lib/Drupal/system/ActionInterface.php
new file mode 100644
index 0000000..a014933
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/ActionInterface.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\ActionInterface.
+ */
+
+namespace Drupal\system;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface defining a action entity.
+ */
+interface ActionInterface extends ConfigEntityInterface {
+
+  /**
+   * Returns whether or not this action is configurable.
+   *
+   * @return bool
+   */
+  public function isConfigurable();
+
+  /**
+   * Returns the operation type.
+   *
+   * @return string
+   */
+  public function getType();
+
+  /**
+   * Returns the operation plugin.
+   *
+   * @return \Drupal\Core\Operation\OperationInterface
+   */
+  public function getPlugin();
+
+}
diff --git a/core/modules/system/lib/Drupal/system/ActionStorageController.php b/core/modules/system/lib/Drupal/system/ActionStorageController.php
new file mode 100644
index 0000000..51c30e7
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/ActionStorageController.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\ActionStorageController.
+ */
+
+namespace Drupal\system;
+
+use Drupal\Core\Config\Entity\ConfigStorageController;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Defines the storage controller class for Action entities.
+ */
+class ActionStorageController extends ConfigStorageController {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function preSave(EntityInterface $entity) {
+    parent::preSave($entity);
+
+    $entity->set('configuration', $entity->getPlugin()->getConfiguration());
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Core/Entity/Action.php b/core/modules/system/lib/Drupal/system/Plugin/Core/Entity/Action.php
new file mode 100644
index 0000000..555489e
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Plugin/Core/Entity/Action.php
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Plugin\Core\Entity\Action.
+ */
+
+namespace Drupal\system\Plugin\Core\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\Annotation\EntityType;
+use Drupal\Core\Annotation\Translation;
+use Drupal\system\ActionInterface;
+use Drupal\Core\Operation\OperationBag;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+
+/**
+ * Defines the configured action entity.
+ *
+ * @EntityType(
+ *   id = "action",
+ *   label = @Translation("Action"),
+ *   module = "system",
+ *   controllers = {
+ *     "storage" = "Drupal\system\ActionStorageController"
+ *   },
+ *   config_prefix = "action.action",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *     "uuid" = "uuid"
+ *   }
+ * )
+ */
+class Action extends ConfigEntityBase implements ActionInterface {
+
+  /**
+   * The name (plugin ID) of the action.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The label of the action.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * The UUID of the action.
+   *
+   * @var string
+   */
+  public $uuid;
+
+  /**
+   * The operation type.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * The configuration of the operation.
+   *
+   * @var array
+   */
+  protected $configuration = array();
+
+  /**
+   * The plugin ID of the operation.
+   *
+   * @var string
+   */
+  protected $plugin;
+
+  /**
+   * The plugin bag that stores operation plugins.
+   *
+   * @var \Drupal\Core\Operation\OperationBag
+   */
+  protected $pluginBag;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $values, $entity_type) {
+    parent::__construct($values, $entity_type);
+
+    $this->pluginBag = new OperationBag(\Drupal::service('plugin.manager.operation'), array($this->plugin), $this->configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPlugin() {
+    return $this->pluginBag->get($this->plugin);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPlugin($plugin_id) {
+    $this->plugin = $plugin_id;
+    $this->pluginBag->addInstanceID($plugin_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginDefinition() {
+    return $this->getPlugin()->getDefinition();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    return $this->getPlugin()->execute($entities);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isConfigurable() {
+    return $this->getPlugin() instanceof ConfigurableOperationInterface;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uri() {
+    return array(
+      'path' => 'admin/config/system/actions/configure/' . $this->id(),
+      'options' => array(
+        'entity_type' => $this->entityType,
+        'entity' => $this,
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function sort($a, $b) {
+    $a_type = $a->getType();
+    $b_type = $b->getType();
+    if ($a_type != $b_type) {
+      return strnatcasecmp($a_type, $b_type);
+    }
+    return parent::sort($a, $b);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExportProperties() {
+    $properties = parent::getExportProperties();
+    $names = array(
+      'type',
+      'plugin',
+      'configuration',
+    );
+    foreach ($names as $name) {
+      $properties[$name] = $this->get($name);
+    }
+    return $properties;
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Plugin/views/field/BulkFormBase.php b/core/modules/system/lib/Drupal/system/Plugin/views/field/BulkFormBase.php
index fda3479..6cb16c9 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/views/field/BulkFormBase.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/views/field/BulkFormBase.php
@@ -8,8 +8,10 @@
 namespace Drupal\system\Plugin\views\field;
 
 use Drupal\Component\Annotation\Plugin;
+use Drupal\Core\Entity\EntityManager;
 use Drupal\views\Plugin\views\field\FieldPluginBase;
 use Drupal\views\Plugin\views\style\Table;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a generic bulk operation form element.
@@ -17,6 +19,27 @@
 abstract class BulkFormBase extends FieldPluginBase {
 
   /**
+   * @var array
+   */
+  protected $actions = array();
+
+  /**
+   * Constructs a new BulkForm object.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManager $manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->actions = $manager->getStorageController('action')->load();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('plugin.manager.entity'));
+  }
+
+  /**
    * Overrides \Drupal\views\Plugin\views\Plugin\field\FieldPluginBase::render().
    */
   public function render($values) {
@@ -51,41 +74,48 @@ public function views_form(&$form, &$form_state) {
     // Add the tableselect javascript.
     $form['#attached']['library'][] = array('system', 'drupal.tableselect');
 
-    // Render checkboxes for all rows.
-    $form[$this->options['id']]['#tree'] = TRUE;
-    foreach ($this->view->result as $row_index => $row) {
-      $form[$this->options['id']][$row_index] = array(
-        '#type' => 'checkbox',
-        // We are not able to determine a main "title" for each row, so we can
-        // only output a generic label.
-        '#title' => t('Update this item'),
-        '#title_display' => 'invisible',
-        '#default_value' => !empty($form_state['values'][$this->options['id']][$row_index]) ? 1 : NULL,
+    // Only add the bulk form options and buttons if there are results.
+    if (!empty($this->view->result)) {
+      // Render checkboxes for all rows.
+      $form[$this->options['id']]['#tree'] = TRUE;
+      foreach ($this->view->result as $row_index => $row) {
+        $form[$this->options['id']][$row_index] = array(
+          '#type' => 'checkbox',
+          // We are not able to determine a main "title" for each row, so we can
+          // only output a generic label.
+          '#title' => t('Update this item'),
+          '#title_display' => 'invisible',
+          '#default_value' => !empty($form_state['values'][$this->options['id']][$row_index]) ? 1 : NULL,
+        );
+      }
+
+      // Replace the form submit button label.
+      $form['actions']['submit']['#value'] = t('Apply');
+
+      // Ensure a consistent container for filters/operations in the view header.
+      $form['header'] = array(
+        '#type' => 'container',
+        '#weight' => -100,
+      );
+
+      // Build the bulk operations action widget for the header.
+      // Allow themes to apply .container-inline on this separate container.
+      $form['header'][$this->options['id']] = array(
+        '#type' => 'container',
+      );
+      $form['header'][$this->options['id']]['action'] = array(
+        '#type' => 'select',
+        '#title' => t('With selection'),
+        '#options' => $this->getBulkOptions(),
       );
-    }
 
-    // Replace the form submit button label.
-    $form['actions']['submit']['#value'] = t('Apply');
-
-    // Ensure a consistent container for filters/operations in the view header.
-    $form['header'] = array(
-      '#type' => 'container',
-      '#weight' => -100,
-    );
-
-    // Build the bulk operations action widget for the header.
-    // Allow themes to apply .container-inline on this separate container.
-    $form['header'][$this->options['id']] = array(
-      '#type' => 'container',
-    );
-    $form['header'][$this->options['id']]['action'] = array(
-      '#type' => 'select',
-      '#title' => t('With selection'),
-      '#options' => $this->getBulkOptions(),
-    );
-
-    // Duplicate the form actions into the action container in the header.
-    $form['header'][$this->options['id']]['actions'] = $form['actions'];
+      // Duplicate the form actions into the action container in the header.
+      $form['header'][$this->options['id']]['actions'] = $form['actions'];
+    }
+    else {
+      // Remove the default actions build array.
+      unset($form['actions']);
+    }
   }
 
   /**
@@ -104,7 +134,25 @@ public function views_form(&$form, &$form_state) {
    * @param array $form_state
    *   An associative array containing the current state of the form.
    */
-  abstract public function views_form_submit(&$form, &$form_state);
+  public function views_form_submit(&$form, &$form_state) {
+    if ($form_state['step'] == 'views_form_views_form') {
+      // Filter only selected checkboxes.
+      $selected = array_filter($form_state['values'][$this->options['id']]);
+      $entities = array();
+      foreach (array_intersect_key($this->view->result, $selected) as $row) {
+        $entity = $this->get_entity($row);
+        $entities[$entity->id()] = $entity;
+      }
+
+      $action = $this->actions[$form_state['values']['action']];
+      $action->execute($entities);
+
+      $operation_definition = $action->getPluginDefinition();
+      if (!empty($operation_definition['redirect'])) {
+        $form_state['redirect'] = $operation_definition['redirect'];
+      }
+    }
+  }
 
   /**
    * Overrides \Drupal\views\Plugin\views\Plugin\field\FieldPluginBase::query().
@@ -112,4 +160,11 @@ public function views_form(&$form, &$form_state) {
   public function query() {
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function click_sortable() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Operation/OperationUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Operation/OperationUnitTest.php
new file mode 100644
index 0000000..94b3a80
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Operation/OperationUnitTest.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Operation\OperationUnitTest.
+ */
+
+namespace Drupal\system\Tests\Operation;
+
+use Drupal\simpletest\DrupalUnitTestBase;
+use Drupal\Core\Operation\OperationInterface;
+
+/**
+ * Tests operation plugins.
+ */
+class OperationUnitTest extends DrupalUnitTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = array('system', 'field', 'user', 'operation_test');
+
+  /**
+   * The operation manager.
+   *
+   * @var \Drupal\Core\Operation\OperationManager
+   */
+  protected $operationManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Operation Plugins',
+      'description' => 'Tests Operation plugins.',
+      'group' => 'Operation Plugin API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->operationManager = $this->container->get('plugin.manager.operation');
+    $this->installSchema('user', array('users', 'users_roles'));
+    $this->installSchema('system', array('sequences'));
+  }
+
+  /**
+   * Tests the functionality of test operations.
+   */
+  public function testOperations() {
+    // Test that operations can be discovered.
+    $definitions = $this->operationManager->getDefinitions();
+    $this->assertTrue(count($definitions) > 1, 'Operation definitions are found.');
+    $this->assertTrue(!empty($definitions['operation_test_no_type']), 'The test operation is among the definitions found.');
+
+    $definition = $this->operationManager->getDefinition('operation_test_no_type');
+    $this->assertTrue(!empty($definition), 'The test operation definition is found.');
+
+    $definitions = $this->operationManager->getDefinitionsByType('user');
+    $this->assertTrue(empty($definitions['operation_test_no_type']), 'An operation with no type is not found.');
+
+    // Create an instance of the 'save entity' operation.
+    $operation = $this->operationManager->createInstance('operation_test_save_entity');
+    $this->assertTrue($operation instanceof OperationInterface, 'The operation implements the correct interface.');
+
+    // Create a new unsaved user.
+    $name = $this->randomName();
+    $user_storage = $this->container->get('plugin.manager.entity')->getStorageController('user');
+    $account = $user_storage->create(array('name' => $name, 'bundle' => 'user'));
+    $loaded_accounts = $user_storage->load();
+    $this->assertEqual(count($loaded_accounts), 0);
+
+    // Execute the 'save entity' operation.
+    $operation->executeSingle($account);
+    $loaded_accounts = $user_storage->load();
+    $this->assertEqual(count($loaded_accounts), 1);
+    $account = reset($loaded_accounts);
+    $this->assertEqual($name, $account->label());
+  }
+
+}
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index c3a9cda..192ef91 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1920,11 +1920,11 @@ function system_update_8045() {
  */
 function system_update_8046() {
   $front_page = config('system.site')->get('page.front');
-  if (!isset($front_page) || $front_page == 'node') {
+  $module_list = drupal_container()->getParameter('container.modules');
+  if (isset($module_list['node']) && (!isset($front_page) || $front_page == 'node')) {
     update_module_enable(array('views'));
 
     // Register views to the container, so views can use it's services.
-    $module_list = drupal_container()->getParameter('container.modules');
     drupal_load('module', 'views');
 
     drupal_container()->get('kernel')->updateModules($module_list, array('views' => 'core/modules/views/views.module'));
diff --git a/core/modules/system/tests/modules/operation_test/lib/Drupal/operation_test/Plugin/Operation/NoType.php b/core/modules/system/tests/modules/operation_test/lib/Drupal/operation_test/Plugin/Operation/NoType.php
new file mode 100644
index 0000000..8a11ac2
--- /dev/null
+++ b/core/modules/system/tests/modules/operation_test/lib/Drupal/operation_test/Plugin/Operation/NoType.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\operation_test\Plugin\Operation\NoType.
+ */
+
+namespace Drupal\operation_test\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Provides an operation with no type specified.
+ *
+ * @Operation(
+ *   id = "operation_test_no_type",
+ *   label = @Translation("An operation with no type specified")
+ * )
+ */
+class NoType extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+  }
+
+}
diff --git a/core/modules/system/tests/modules/operation_test/lib/Drupal/operation_test/Plugin/Operation/SaveEntity.php b/core/modules/system/tests/modules/operation_test/lib/Drupal/operation_test/Plugin/Operation/SaveEntity.php
new file mode 100644
index 0000000..5013d08
--- /dev/null
+++ b/core/modules/system/tests/modules/operation_test/lib/Drupal/operation_test/Plugin/Operation/SaveEntity.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\operation_test\Plugin\Operation\SaveEntity.
+ */
+
+namespace Drupal\operation_test\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Provides an operation to save user entities.
+ *
+ * @Operation(
+ *   id = "operation_test_save_entity",
+ *   label = @Translation("Save a user"),
+ *   type = "user"
+ * )
+ */
+class SaveEntity extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSingle($entity) {
+    $entity->save();
+  }
+
+}
diff --git a/core/modules/system/tests/modules/operation_test/operation_test.info.yml b/core/modules/system/tests/modules/operation_test/operation_test.info.yml
new file mode 100644
index 0000000..4bdac30
--- /dev/null
+++ b/core/modules/system/tests/modules/operation_test/operation_test.info.yml
@@ -0,0 +1,7 @@
+name: 'Operation test'
+type: module
+description: 'Support module for operation testing.'
+package: Testing
+version: VERSION
+core: 8.x
+hidden: true
diff --git a/core/modules/system/tests/modules/operation_test/operation_test.module b/core/modules/system/tests/modules/operation_test/operation_test.module
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/core/modules/system/tests/modules/operation_test/operation_test.module
@@ -0,0 +1 @@
+<?php
diff --git a/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php b/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php
index 0e7c429..f2ee05b 100644
--- a/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php
+++ b/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php
@@ -256,6 +256,7 @@ function testTrackerCronIndexing() {
    * Tests that publish/unpublish works at admin/content/node.
    */
   function testTrackerAdminUnpublish() {
+    module_enable(array('views'));
     $admin_user = $this->drupalCreateUser(array('access content overview', 'administer nodes', 'bypass node access'));
     $this->drupalLogin($admin_user);
 
@@ -270,10 +271,10 @@ function testTrackerAdminUnpublish() {
 
     // Unpublish the node and ensure that it's no longer displayed.
     $edit = array(
-      'operation' => 'unpublish',
-      'nodes[' . $node->nid . ']' => $node->nid,
+      'action' => 'node_unpublish_action',
+      'node_bulk_form[0]' => TRUE,
     );
-    $this->drupalPost('admin/content', $edit, t('Update'));
+    $this->drupalPost('admin/content', $edit, t('Apply'));
 
     $this->drupalGet('tracker');
     $this->assertText(t('No content available.'), 'Node is displayed on the tracker listing pages.');
diff --git a/core/modules/user/config/action.action.user_block_user_action.yml b/core/modules/user/config/action.action.user_block_user_action.yml
new file mode 100644
index 0000000..2c4ed88
--- /dev/null
+++ b/core/modules/user/config/action.action.user_block_user_action.yml
@@ -0,0 +1,6 @@
+id: user_block_user_action
+label: 'Block the selected user(s)'
+status: '1'
+langcode: en
+type: user
+plugin: user_block_user_action
diff --git a/core/modules/user/config/action.action.user_cancel_user_action.yml b/core/modules/user/config/action.action.user_cancel_user_action.yml
new file mode 100644
index 0000000..b69d2d9
--- /dev/null
+++ b/core/modules/user/config/action.action.user_cancel_user_action.yml
@@ -0,0 +1,6 @@
+id: user_cancel_user_action
+label: 'Cancel the selected user account(s)'
+status: '1'
+langcode: en
+type: user
+plugin: user_cancel_user_action
diff --git a/core/modules/user/config/action.action.user_unblock_user_action.yml b/core/modules/user/config/action.action.user_unblock_user_action.yml
new file mode 100644
index 0000000..20a6fd5
--- /dev/null
+++ b/core/modules/user/config/action.action.user_unblock_user_action.yml
@@ -0,0 +1,6 @@
+id: user_unblock_user_action
+label: 'Unblock the selected user(s)'
+status: '1'
+langcode: en
+type: user
+plugin: user_unblock_user_action
diff --git a/core/modules/user/config/views.view.user_admin_people.yml b/core/modules/user/config/views.view.user_admin_people.yml
new file mode 100644
index 0000000..72eaee1
--- /dev/null
+++ b/core/modules/user/config/views.view.user_admin_people.yml
@@ -0,0 +1,281 @@
+base_field: uid
+base_table: users
+core: 8.x
+description: 'Find and manage people interacting with your site.'
+status: '1'
+display:
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: ''
+    display_options:
+      path: admin/people/list
+      show_admin_links: '0'
+      menu:
+        type: 'default tab'
+        title: List
+        description: 'Find and manage people interacting with your site.'
+        name: admin
+        weight: '-10'
+        context: '0'
+      tab_options:
+        type: normal
+        title: People
+        description: 'Manage user accounts, roles, and permissions.'
+        name: admin
+        weight: '0'
+      defaults:
+        show_admin_links: '0'
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: ''
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'administer users'
+      cache:
+        type: none
+      query:
+        type: views_query
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Filter
+          reset_button: '1'
+          reset_button_label: Reset
+      pager:
+        type: full
+        options:
+          items_per_page: '50'
+      style:
+        type: table
+        options:
+          columns:
+            user_bulk_form: user_bulk_form
+            name: name
+            status: status
+            rid: rid
+            created: created
+            access: access
+            edit_node: edit_node
+            translation_link: translation_link
+            dropbutton: dropbutton
+          info:
+            user_bulk_form:
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: ''
+            name:
+              sortable: '1'
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: ''
+            status:
+              sortable: '1'
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: priority-low
+            rid:
+              sortable: '0'
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: priority-low
+            created:
+              sortable: '1'
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: priority-low
+            access:
+              sortable: '1'
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: priority-low
+            edit_node:
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: priority-low
+            translation_link:
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: ''
+            dropbutton:
+              sortable: '0'
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: '0'
+              responsive: ''
+          default: created
+          empty_table: '1'
+      row:
+        type: fields
+      fields:
+        user_bulk_form:
+          id: user_bulk_form
+          table: users
+          field: user_bulk_form
+          label: 'Bulk update'
+          plugin_id: user_bulk_form
+        name:
+          id: name
+          table: users
+          field: name
+          label: Username
+          link_to_user: '1'
+          format_username: '1'
+          plugin_id: user_name
+        status:
+          id: status
+          table: users
+          field: status
+          label: Status
+          type: active-blocked
+          plugin_id: boolean
+        rid:
+          id: rid
+          table: users_roles
+          field: rid
+          label: Roles
+          type: ul
+          plugin_id: user_roles
+        created:
+          id: created
+          table: users
+          field: created
+          label: 'Member for'
+          date_format: 'raw time ago'
+          plugin_id: date
+        access:
+          id: access
+          table: users
+          field: access
+          label: 'Last access'
+          date_format: 'time ago'
+          plugin_id: date
+        edit_node:
+          id: edit_node
+          table: users
+          field: edit_node
+          exclude: '1'
+          text: Edit
+          plugin_id: user_link_edit
+        translation_link:
+          id: translation_link
+          table: users
+          field: translation_link
+          label: 'Translation link'
+          exclude: '1'
+          alter:
+            alter_text: '0'
+          element_class: ''
+          element_default_classes: '1'
+          empty: ''
+          hide_empty: '0'
+          empty_zero: '0'
+          hide_alter_empty: '1'
+          text: Translate
+          optional: '1'
+          plugin_id: translation_entity_link
+        dropbutton:
+          id: dropbutton
+          table: views
+          field: dropbutton
+          label: Operations
+          fields:
+            edit_node: edit_node
+            translation_link: translation_link
+          destination: '1'
+          plugin_id: dropbutton
+      filters:
+        rid:
+          id: rid
+          table: users_roles
+          field: rid
+          operator: or
+          value: {  }
+          group: '1'
+          exposed: '1'
+          expose:
+            operator_id: rid_op
+            label: Role
+            operator: rid_op
+            identifier: role
+          plugin_id: user_roles
+        permission:
+          id: permission
+          table: role_permission
+          field: permission
+          operator: or
+          value: {  }
+          group: '1'
+          exposed: '1'
+          expose:
+            operator_id: permission_op
+            label: Permission
+            operator: permission_op
+            identifier: permission
+          plugin_id: user_permissions
+        status:
+          id: status
+          table: users
+          field: status
+          operator: '='
+          value: All
+          group: '1'
+          exposed: '1'
+          expose:
+            operator_id: ''
+            label: Active
+            operator: status_op
+            identifier: status
+          plugin_id: boolean
+        uid_raw:
+          id: uid_raw
+          table: users
+          field: uid_raw
+          operator: '!='
+          value:
+            min: ''
+            max: ''
+            value: '0'
+          group: '1'
+          exposed: '0'
+          plugin_id: numeric
+      sorts:
+        created:
+          id: created
+          table: users
+          field: created
+          order: DESC
+          plugin_id: date
+      title: People
+      empty:
+        area_text_custom:
+          id: area_text_custom
+          table: views
+          field: area_text_custom
+          empty: '1'
+          content: 'No people available.'
+          plugin_id: text_custom
+label: People
+module: views
+id: user_admin_people
+tag: default
+langcode: und
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Operation/AddRoleUser.php b/core/modules/user/lib/Drupal/user/Plugin/Operation/AddRoleUser.php
new file mode 100644
index 0000000..acf6da0
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Plugin/Operation/AddRoleUser.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Plugin\Operation\AddRoleUser.
+ */
+
+namespace Drupal\user\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+
+/**
+ * Adds a role to a user.
+ *
+ * @Operation(
+ *   id = "user_add_role_action",
+ *   label = @Translation("Add a role to the selected users"),
+ *   type = "user"
+ * )
+ */
+class AddRoleUser extends OperationBase implements ConfigurableOperationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    user_multiple_role_edit($entities, 'add_role', $this->configuration['rid']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultConfiguration() {
+    return array(
+      'rid' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $roles = user_role_names(TRUE);
+    unset($roles[DRUPAL_AUTHENTICATED_RID]);
+    $form['rid'] = array(
+      '#type' => 'radios',
+      '#title' => t('Role'),
+      '#options' => $roles,
+      '#default_value' => $this->configuration['rid'],
+      '#required' => TRUE,
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array &$form, array &$form_state) {
+    $this->configuration['rid'] = $form_state['values']['rid'];
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Operation/BlockUser.php b/core/modules/user/lib/Drupal/user/Plugin/Operation/BlockUser.php
new file mode 100644
index 0000000..d5f4cda
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Plugin/Operation/BlockUser.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Plugin\Operation\BlockUser.
+ */
+
+namespace Drupal\user\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Blocks a user.
+ *
+ * @Operation(
+ *   id = "user_block_user_action",
+ *   label = @Translation("Block the selected users"),
+ *   type = "user"
+ * )
+ */
+class BlockUser extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    user_user_operations_block($entities);
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Operation/CancelUser.php b/core/modules/user/lib/Drupal/user/Plugin/Operation/CancelUser.php
new file mode 100644
index 0000000..4b05786
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Plugin/Operation/CancelUser.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Plugin\Operation\CancelUser.
+ */
+
+namespace Drupal\user\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\user\TempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Cancels a user account.
+ *
+ * @Operation(
+ *   id = "user_cancel_user_action",
+ *   label = @Translation("Cancel the selected user accounts"),
+ *   type = "user",
+ *   redirect = "admin/people/cancel"
+ * )
+ */
+class CancelUser extends OperationBase {
+
+  /**
+   * @var \Drupal\user\TempStore
+   */
+  protected $tempStore;
+
+  /**
+   * Constructs a DeleteNode object.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, TempStoreFactory $temp_store_factory) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->tempStore = $temp_store_factory->get('user_user_operations_cancel');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('user.tempstore'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    $this->tempStore->set($GLOBALS['user']->uid, $entities);
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Operation/RemoveRoleUser.php b/core/modules/user/lib/Drupal/user/Plugin/Operation/RemoveRoleUser.php
new file mode 100644
index 0000000..11c6134
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Plugin/Operation/RemoveRoleUser.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Plugin\Operation\RemoveRoleUser.
+ */
+
+namespace Drupal\user\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+use Drupal\Core\Operation\ConfigurableOperationInterface;
+
+/**
+ * Removes a role from a user.
+ *
+ * @Operation(
+ *   id = "user_remove_role_action",
+ *   label = @Translation("Remove a role from the selected users"),
+ *   type = "user"
+ * )
+ */
+class RemoveRoleUser extends OperationBase implements ConfigurableOperationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    user_multiple_role_edit($entities, 'remove_role', $this->configuration['rid']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultConfiguration() {
+    return array(
+      'rid' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $roles = user_role_names(TRUE);
+    unset($roles[DRUPAL_AUTHENTICATED_RID]);
+    $form['rid'] = array(
+      '#type' => 'radios',
+      '#title' => t('Role'),
+      '#options' => $roles,
+      '#default_value' => $this->configuration['rid'],
+      '#required' => TRUE,
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array &$form, array &$form_state) {
+    $this->configuration['rid'] = $form_state['values']['rid'];
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Operation/UnblockUser.php b/core/modules/user/lib/Drupal/user/Plugin/Operation/UnblockUser.php
new file mode 100644
index 0000000..75602db
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Plugin/Operation/UnblockUser.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Plugin\Operation\UnblockUser.
+ */
+
+namespace Drupal\user\Plugin\Operation;
+
+use Drupal\Core\Annotation\Operation;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Operation\OperationBase;
+
+/**
+ * Unblocks a user.
+ *
+ * @Operation(
+ *   id = "user_unblock_user_action",
+ *   label = @Translation("Unblock the selected users"),
+ *   type = "user"
+ * )
+ */
+class UnblockUser extends OperationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(array $entities) {
+    user_user_operations_unblock($entities);
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/Link.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/Link.php
index 39b2a9b..afc67c5 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/views/field/Link.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/Link.php
@@ -48,7 +48,7 @@ public function buildOptionsForm(&$form, &$form_state) {
 
   // An example of field level access control.
   public function access() {
-    return user_access('access user profiles');
+    return user_access('administer users') || user_access('access user profiles');
   }
 
   public function query() {
diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkEdit.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkEdit.php
index e7f63be..8954610 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkEdit.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkEdit.php
@@ -23,7 +23,7 @@ class LinkEdit extends Link {
    * Overrides \Drupal\user\Plugin\views\field\Link::render_link().
    */
   public function render_link(EntityInterface $entity, \stdClass $values) {
-    if ($entity && $entity->access('edit')) {
+    if ($entity && $entity->access('update')) {
       $this->options['alter']['make_link'] = TRUE;
 
       $text = !empty($this->options['text']) ? $this->options['text'] : t('Edit');
diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/UserBulkForm.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/UserBulkForm.php
new file mode 100644
index 0000000..ef67075
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/UserBulkForm.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Plugin\views\field\UserBulkForm.
+ */
+
+namespace Drupal\user\Plugin\views\field;
+
+use Drupal\Component\Annotation\PluginID;
+use Drupal\system\Plugin\views\field\BulkFormBase;
+use Drupal\user\UserInterface;
+use Drupal\Core\Entity\EntityManager;
+
+/**
+ * Defines a user operations bulk form element.
+ *
+ * @PluginID("user_bulk_form")
+ */
+class UserBulkForm extends BulkFormBase {
+
+  /**
+   * Constructs a new UserBulkForm object.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManager $manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $manager);
+
+    // Filter the actions to only include those for the 'user' entity type.
+    $this->actions = array_filter($this->actions, function ($action) {
+      return $action->getType() == 'user';
+    });
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getBulkOptions() {
+    return array_map(function ($action) {
+      return $action->label();
+    }, $this->actions);
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Provide a more useful title to improve the accessibility.
+   */
+  public function views_form(&$form, &$form_state) {
+    parent::views_form($form, $form_state);
+
+    if (!empty($this->view->result)) {
+      foreach ($this->view->result as $row_index => $row) {
+        $account = $row->_entity;
+        if ($account instanceof UserInterface) {
+          $form[$this->options['id']][$row_index]['#title'] = t('Update the user %name', array('%name' => $account->label()));
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function views_form_validate(&$form, &$form_state) {
+    $selected = array_filter($form_state['values'][$this->options['id']]);
+    if (empty($selected)) {
+      form_set_error('', t('No users selected.'));
+    }
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserAdminListingTest.php b/core/modules/user/lib/Drupal/user/Tests/UserAdminListingTest.php
new file mode 100644
index 0000000..e1e20e6
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Tests/UserAdminListingTest.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Tests\UserAdminListingTest.
+ */
+
+namespace Drupal\user\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Defines a test for the fallback user admin listing.
+ *
+ * @see user_admin_account()
+ */
+class UserAdminListingTest extends WebTestBase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'User people listing',
+      'description' => 'Test the user admin listing if views is not enabled.',
+      'group' => 'User'
+    );
+  }
+
+  /**
+   * Tests the listing.
+   */
+  public function testUserListing() {
+    $this->drupalGet('admin/people');
+    $this->assertResponse(403, 'Anonymous user does not have access to the user admin listing.');
+
+    // Create a bunch of users.
+    $accounts = array();
+    for ($i = 0; $i < 3; $i++) {
+      $account = $this->drupalCreateUser();
+      $accounts[$account->name] = $account;
+    }
+    // Create a blocked user.
+    $account = $this->drupalCreateUser();
+    $account->status = 0;
+    $account->save();
+    $accounts[$account->name] = $account;
+
+    // Create a user at a certain timestamp.
+    $account = $this->drupalCreateUser();
+    $account->created = 1363219200;
+    $account->save();
+    $accounts[$account->name] = $account;
+    $timestamp_user = $account->name;
+
+    $rid_1 = $this->drupalCreateRole(array(), 'custom_role_1', 'custom_role_1');
+    $rid_2 = $this->drupalCreateRole(array(), 'custom_role_2', 'custom_role_2');
+
+    $account = $this->drupalCreateUser();
+    $account->roles[$rid_1] = $rid_1;
+    $account->roles[$rid_2] = $rid_2;
+    $account->save();
+    $accounts[$account->name] = $account;
+    $role_account_name = $account->name;
+
+    // Create an admin user and look at the listing.
+    $admin_user = $this->drupalCreateUser(array('administer users'));
+    $accounts[$admin_user->name] = $admin_user;
+
+    $accounts['admin'] = user_load('1');
+
+    $this->drupalLogin($admin_user);
+
+    $this->drupalGet('admin/people');
+    $this->assertResponse(200, 'The admin user has access to the user admin listing.');
+
+    $result = $this->xpath('//table[contains(@class, "responsive-enabled")]/tbody/tr');
+    $result_accounts = array();
+    foreach ($result as $account) {
+      $name = (string) $account->td[0]->span;
+      $roles = array();
+      if (isset($account->td[2]->div->ul)) {
+        foreach ($account->td[2]->div->ul->li as $element) {
+          $roles[] = (string) $element;
+        }
+      }
+      $result_accounts[$name] = array(
+        'name' => $name,
+        'status' => (string) $account->td[1],
+        'roles' => $roles,
+        'member_for' => (string) $account->td[3],
+      );
+    }
+
+    $this->assertFalse(array_diff(array_keys($result_accounts), array_keys($accounts)), 'Ensure all accounts are listed.');
+    foreach ($result_accounts as $name => $values) {
+      $this->assertEqual($values['status'] == t('active'), $accounts[$name]->status, 'Ensure the status is displayed properly.');
+    }
+
+    $expected_roles = array('custom_role_1', 'custom_role_2');
+    $this->assertEqual($result_accounts[$role_account_name]['roles'], $expected_roles, 'Ensure roles are listed properly.');
+
+    $this->assertEqual($result_accounts[$timestamp_user]['member_for'], format_interval(REQUEST_TIME - $accounts[$timestamp_user]->created), 'Ensure the right member time is displayed.');
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php b/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php
index 3b83b52..2200c85 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php
@@ -16,7 +16,7 @@ class UserAdminTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('taxonomy');
+  public static $modules = array('taxonomy', 'views');
 
   public static function getInfo() {
     return array(
@@ -49,9 +49,7 @@ function testUserAdmin() {
     $this->assertRaw($link, 'Found user A edit link on admin users page');
 
     // Filter the users by permission 'administer taxonomy'.
-    $edit = array();
-    $edit['permission'] = 'administer taxonomy';
-    $this->drupalPost('admin/people', $edit, t('Filter'));
+    $this->drupalGet('admin/people', array('query' => array('permission' => 'administer taxonomy')));
 
     // Check if the correct users show up.
     $this->assertNoText($user_a->name, 'User A not on filtered by perm admin users page');
@@ -61,8 +59,7 @@ function testUserAdmin() {
     // Filter the users by role. Grab the system-generated role name for User C.
     $roles = $user_c->roles;
     unset($roles[DRUPAL_AUTHENTICATED_RID]);
-    $edit['role'] = key($roles);
-    $this->drupalPost('admin/people', $edit, t('Refine'));
+    $this->drupalGet('admin/people', array('query' => array('role' => key($roles))));
 
     // Check if the correct users show up when filtered by role.
     $this->assertNoText($user_a->name, 'User A not on filtered by role on admin users page');
@@ -73,17 +70,17 @@ function testUserAdmin() {
     $account = user_load($user_c->uid);
     $this->assertEqual($account->status, 1, 'User C not blocked');
     $edit = array();
-    $edit['operation'] = 'block';
-    $edit['accounts[' . $account->uid . ']'] = TRUE;
-    $this->drupalPost('admin/people', $edit, t('Update'));
+    $edit['action'] = 'user_block_user_action';
+    $edit['user_bulk_form[1]'] = TRUE;
+    $this->drupalPost('admin/people', $edit, t('Apply'));
     $account = user_load($user_c->uid, TRUE);
     $this->assertEqual($account->status, 0, 'User C blocked');
 
     // Test unblocking of a user from /admin/people page and sending of activation mail
     $editunblock = array();
-    $editunblock['operation'] = 'unblock';
-    $editunblock['accounts[' . $account->uid . ']'] = TRUE;
-    $this->drupalPost('admin/people', $editunblock, t('Update'));
+    $editunblock['action'] = 'user_unblock_user_action';
+    $editunblock['user_bulk_form[1]'] = TRUE;
+    $this->drupalPost('admin/people', $editunblock, t('Apply'));
     $account = user_load($user_c->uid, TRUE);
     $this->assertEqual($account->status, 1, 'User C unblocked');
     $this->assertMail("to", $account->mail, "Activation mail sent to user C");
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php b/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php
index 83aa5b2..6ecee28 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php
@@ -66,6 +66,7 @@ function testUserCancelWithoutPermission() {
    * administer the site.
    */
   function testUserCancelUid1() {
+    module_enable(array('views'));
     // Update uid 1's name and password to we know it.
     $password = user_password();
     $account = array(
@@ -87,10 +88,10 @@ function testUserCancelUid1() {
     $this->admin_user = $this->drupalCreateUser(array('administer users'));
     $this->drupalLogin($this->admin_user);
     $edit = array(
-      'operation' => 'cancel',
-      'accounts[1]' => TRUE,
+      'action' => 'user_cancel_user_action',
+      'user_bulk_form[0]' => TRUE,
     );
-    $this->drupalPost('admin/people', $edit, t('Update'));
+    $this->drupalPost('admin/people', $edit, t('Apply'));
 
     // Verify that uid 1's account was not cancelled.
     $user1 = user_load(1, TRUE);
@@ -390,6 +391,7 @@ function testUserWithoutEmailCancelByAdmin() {
    * Create an administrative user and mass-delete other users.
    */
   function testMassUserCancelByAdmin() {
+    module_enable(array('views'));
     config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
     // Enable account cancellation notification.
     config('user.settings')->set('notify.status_canceled', TRUE)->save();
@@ -407,14 +409,11 @@ function testMassUserCancelByAdmin() {
 
     // Cancel user accounts, including own one.
     $edit = array();
-    $edit['operation'] = 'cancel';
-    foreach ($users as $uid => $account) {
-      $edit['accounts[' . $uid . ']'] = TRUE;
+    $edit['action'] = 'user_cancel_user_action';
+    for ($i = 0; $i <= 4; $i++) {
+      $edit['user_bulk_form[' . $i . ']'] = TRUE;
     }
-    $edit['accounts[' . $admin_user->uid . ']'] = TRUE;
-    // Also try to cancel uid 1.
-    $edit['accounts[1]'] = TRUE;
-    $this->drupalPost('admin/people', $edit, t('Update'));
+    $this->drupalPost('admin/people', $edit, t('Apply'));
     $this->assertText(t('Are you sure you want to cancel these user accounts?'), 'Confirmation form to cancel accounts displayed.');
     $this->assertText(t('When cancelling these accounts'), 'Allows to select account cancellation method.');
     $this->assertText(t('Require e-mail confirmation to cancel account.'), 'Allows to send confirmation mail.');
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php b/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php
index 171a976..d12c898 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php
@@ -24,7 +24,7 @@ class UserTranslationUITest extends EntityTranslationUITest {
    *
    * @var array
    */
-  public static $modules = array('language', 'translation_entity', 'user');
+  public static $modules = array('language', 'translation_entity', 'user', 'views');
 
   public static function getInfo() {
     return array(
diff --git a/core/modules/user/lib/Drupal/user/Tests/Views/BulkFormTest.php b/core/modules/user/lib/Drupal/user/Tests/Views/BulkFormTest.php
new file mode 100644
index 0000000..1ff4d0d
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Tests/Views/BulkFormTest.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Tests\Views\BulkFormTest.
+ */
+
+namespace Drupal\user\Tests\Views;
+
+/**
+ * Tests the views bulk form test.
+ *
+ * @see \Drupal\user\Plugin\views\field\BulkForm
+ */
+class BulkFormTest extends UserTestBase {
+
+  /**
+   * Views used by this test.
+   *
+   * @var array
+   */
+  public static $testViews = array('test_user_bulk_form');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'User: Bulk form',
+      'description' => 'Tests a user bulk form.',
+      'group' => 'Views module integration',
+    );
+  }
+
+  /**
+   * Tests the user bulk form.
+   */
+  public function testBulkForm() {
+    $this->drupalLogin($this->drupalCreateUser(array('administer permissions')));
+
+    $this->drupalGet('test-user-bulk-form');
+    $elements = $this->xpath('//select[@id="edit-action"]//option');
+    // @todo Replace with loadByProperties().
+    $actions = $this->container->get('plugin.manager.entity')->getStorageController('action')->load();
+    foreach ($actions as $id =>$action) {
+      if ($action->getType() != 'user') {
+        unset($actions[$id]);
+      }
+    }
+    $this->assertIdentical(count($elements), count($actions), 'All user operations are found.');
+
+    // Test submitting the page with no selection.
+    $edit = array(
+      'action' => 'user_block_user_action',
+    );
+    $this->drupalPost(NULL, $edit, t('Apply'));
+    // @todo Validation errors are only shown on page refresh.
+    $this->drupalGet('test-user-bulk-form');
+    $this->assertText(t('No users selected.'));
+
+    // Assign a role to a user.
+    $account = $this->users[0];
+    $roles = user_role_names(TRUE);
+    unset($roles[DRUPAL_AUTHENTICATED_RID]);
+    $role = key($roles);
+
+    $this->assertTrue(!isset($account->roles[$role]), 'The user currently does not have a custom role.');
+    $edit = array(
+      'user_bulk_form[1]' => TRUE,
+      'action' => "user_add_role_action.$role",
+    );
+    $this->drupalPost(NULL, $edit, t('Apply'));
+    // Re-load the user and check their roles.
+    $account = entity_load('user', $account->id(), TRUE);
+    $this->assertTrue(isset($account->roles[$role]), 'The user now has the custom role.');
+
+    $edit = array(
+      'user_bulk_form[1]' => TRUE,
+      'action' => "user_remove_role_action.$role",
+    );
+    $this->drupalPost(NULL, $edit, t('Apply'));
+    // Re-load the user and check their roles.
+    $account = entity_load('user', $account->id(), TRUE);
+    $this->assertTrue(!isset($account->roles[$role]), 'The user no longer has the custom role.');
+
+    // Block a user using the bulk form.
+    $this->assertTrue($account->status);
+    $this->assertRaw($account->label(), 'The user is found in the table.');
+    $edit = array(
+      'user_bulk_form[1]' => TRUE,
+      'action' => 'user_block_user_action',
+    );
+    $this->drupalPost(NULL, $edit, t('Apply'));
+    // Re-load the user and check their status.
+    $account = entity_load('user', $account->id(), TRUE);
+    $this->assertFalse($account->status);
+    $this->assertNoRaw($account->label(), 'The user is not found in the table.');
+
+    // Remove the user status filter from the view.
+    $view = views_get_view('test_user_bulk_form');
+    $view->removeItem('default', 'filter', 'status');
+    $view->storage->save();
+
+    // Ensure the anonymous user is found.
+    $this->drupalGet('test-user-bulk-form');
+    $this->assertText(config('user.settings')->get('anonymous'));
+
+    // Attempt to block the anonymous user.
+    $edit = array(
+      'user_bulk_form[0]' => TRUE,
+      'action' => 'user_block_user_action',
+    );
+    $this->drupalPost(NULL, $edit, t('Apply'));
+    $anonymous_account = user_load(0);
+    $this->assertFalse($anonymous_account->status, 0, 'Ensure the anonymous user got blocked.');
+  }
+
+}
diff --git a/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_user_bulk_form.yml b/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_user_bulk_form.yml
new file mode 100644
index 0000000..ca11a38
--- /dev/null
+++ b/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_user_bulk_form.yml
@@ -0,0 +1,53 @@
+base_field: uid
+base_table: users
+core: 8.x
+description: ''
+status: '1'
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: ''
+    display_options:
+      style:
+        type: table
+      row:
+        type: fields
+      fields:
+        user_bulk_form:
+          id: user_bulk_form
+          table: users
+          field: user_bulk_form
+          plugin_id: user_bulk_form
+        name:
+          id: name
+          table: users
+          field: name
+          plugin_id: user_name
+      sorts:
+        uid:
+          id: uid
+          table: users
+          field: uid
+          order: ASC
+          plugin_id: user
+      filters:
+        status:
+          id: status
+          table: users
+          field: status
+          operator: '='
+          value: '1'
+          plugin_id: boolean
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: ''
+    display_options:
+      path: test-user-bulk-form
+label: ''
+module: views
+id: test_user_bulk_form
+tag: ''
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index 9946ae2..4484ea1 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -6,158 +6,9 @@
  */
 
 /**
- * Page callback: Generates the appropriate user administration form.
- *
- * This function generates the user registration, multiple user cancellation,
- * or filtered user list admin form, depending on the argument and the POST
- * form values.
- *
- * @param string $callback_arg
- *   (optional) Indicates which form to build. Defaults to '', which will
- *   trigger the user filter form. If the POST value 'op' is present, this
- *   function uses that value as the callback argument.
- *
- * @return string
- *   A renderable form array for the respective request.
- */
-function user_admin($callback_arg = '') {
-  $op = isset($_POST['op']) ? $_POST['op'] : $callback_arg;
-
-  switch ($op) {
-    case t('Create new account'):
-    case 'create':
-      $account = entity_create('user', array());
-      $build['user_register'] = entity_get_form($account, 'register');
-      break;
-    default:
-      if (!empty($_POST['accounts']) && isset($_POST['operation']) && ($_POST['operation'] == 'cancel')) {
-        $build['user_multiple_cancel_confirm'] = drupal_get_form('user_multiple_cancel_confirm');
-      }
-      else {
-        $build['user_filter_form'] = drupal_get_form('user_filter_form');
-        $build['user_admin_account'] = drupal_get_form('user_admin_account');
-      }
-  }
-  return $build;
-}
-
-/**
- * Form builder; Return form for user administration filters.
- *
- * @ingroup forms
- * @see user_filter_form_submit()
- */
-function user_filter_form() {
-  $session = isset($_SESSION['user_overview_filter']) ? $_SESSION['user_overview_filter'] : array();
-  $filters = user_filters();
-
-  $i = 0;
-  $form['filters'] = array(
-    '#type' => 'details',
-    '#title' => t('Show only users where'),
-    '#theme' => 'exposed_filters__user',
-  );
-  foreach ($session as $filter) {
-    list($type, $value) = $filter;
-    if ($type == 'permission') {
-      // Merge arrays of module permissions into one.
-      // Slice past the first element '[any]' whose value is not an array.
-      $options = call_user_func_array('array_merge', array_slice($filters[$type]['options'], 1));
-      $value = $options[$value];
-    }
-    else {
-      $value = $filters[$type]['options'][$value];
-    }
-    $t_args = array('%property' => $filters[$type]['title'], '%value' => $value);
-    if ($i++) {
-      $form['filters']['current'][] = array('#markup' => t('and where %property is %value', $t_args));
-    }
-    else {
-      $form['filters']['current'][] = array('#markup' => t('%property is %value', $t_args));
-    }
-  }
-
-  $form['filters']['status'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('clearfix')),
-    '#prefix' => ($i ? '<div class="additional-filters">' . t('and where') . '</div>' : ''),
-  );
-  $form['filters']['status']['filters'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('filters')),
-  );
-  foreach ($filters as $key => $filter) {
-    $form['filters']['status']['filters'][$key] = array(
-      '#type' => 'select',
-      '#options' => $filter['options'],
-      '#title' => $filter['title'],
-      '#default_value' => '[any]',
-    );
-  }
-
-  $form['filters']['status']['actions'] = array(
-    '#type' => 'actions',
-    '#attributes' => array('class' => array('container-inline')),
-  );
-  $form['filters']['status']['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => (count($session) ? t('Refine') : t('Filter')),
-  );
-  if (count($session)) {
-    $form['filters']['status']['actions']['undo'] = array(
-      '#type' => 'submit',
-      '#value' => t('Undo'),
-    );
-    $form['filters']['status']['actions']['reset'] = array(
-      '#type' => 'submit',
-      '#value' => t('Reset'),
-    );
-  }
-
-  drupal_add_library('system', 'drupal.form');
-
-  return $form;
-}
-
-/**
- * Process result from user administration filter form.
- */
-function user_filter_form_submit($form, &$form_state) {
-  $op = $form_state['values']['op'];
-  $filters = user_filters();
-  switch ($op) {
-    case t('Filter'):
-    case t('Refine'):
-      // Apply every filter that has a choice selected other than 'any'.
-      foreach ($filters as $filter => $options) {
-        if (isset($form_state['values'][$filter]) && $form_state['values'][$filter] != '[any]') {
-          $_SESSION['user_overview_filter'][] = array($filter, $form_state['values'][$filter]);
-        }
-      }
-      break;
-    case t('Undo'):
-      array_pop($_SESSION['user_overview_filter']);
-      break;
-    case t('Reset'):
-      $_SESSION['user_overview_filter'] = array();
-      break;
-    case t('Update'):
-      return;
-  }
-
-  $form_state['redirect'] = 'admin/people';
-  return;
-}
-
-/**
- * Form builder; User administration page.
- *
- * @ingroup forms
- * @see user_admin_account_validate()
- * @see user_admin_account_submit()
+ * Page callback: User administration page.
  */
 function user_admin_account() {
-
   $header = array(
     'username' => array('data' => t('Username'), 'field' => 'u.name'),
     'status' => array('data' => t('Status'), 'field' => 'u.status', 'class' => array(RESPONSIVE_PRIORITY_LOW)),
@@ -184,28 +35,6 @@ function user_admin_account() {
     ->setCountQuery($count_query);
   $result = $query->execute();
 
-  $form['options'] = array(
-    '#type' => 'details',
-    '#title' => t('Update options'),
-    '#attributes' => array('class' => array('container-inline')),
-  );
-  $options = array();
-  foreach (module_invoke_all('user_operations') as $operation => $array) {
-    $options[$operation] = $array['label'];
-  }
-  $form['options']['operation'] = array(
-    '#type' => 'select',
-    '#title' => t('Operation'),
-    '#title_display' => 'invisible',
-    '#options' => $options,
-    '#default_value' => 'unblock',
-  );
-  $options = array();
-  $form['options']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Update'),
-  );
-
   $destination = drupal_get_destination();
   $status = array(t('blocked'), t('active'));
   $roles = array_map('check_plain', user_role_names(TRUE));
@@ -248,44 +77,23 @@ function user_admin_account() {
 
   }
 
-  $form['accounts'] = array(
-    '#type' => 'tableselect',
+  $build['accounts'] = array(
+    '#theme' => 'table',
     '#header' => $header,
-    '#options' => $options,
+    '#rows' => $options,
     '#empty' => t('No people available.'),
   );
-  $form['pager'] = array('#markup' => theme('pager'));
+  $build['pager'] = array('#markup' => theme('pager'));
 
-  return $form;
+  return $build;
 }
 
 /**
- * Submit the user administration update form.
+ * Page callback: Generates user create administration form.
  */
-function user_admin_account_submit($form, &$form_state) {
-  $operations = module_invoke_all('user_operations', $form, $form_state);
-  $operation = $operations[$form_state['values']['operation']];
-  // Filter out unchecked accounts.
-  $accounts = array_filter($form_state['values']['accounts']);
-  if ($function = $operation['callback']) {
-    // Add in callback arguments if present.
-    if (isset($operation['callback arguments'])) {
-      $args = array_merge(array($accounts), $operation['callback arguments']);
-    }
-    else {
-      $args = array($accounts);
-    }
-    call_user_func_array($function, $args);
-
-    drupal_set_message(t('The update has been performed.'));
-  }
-}
-
-function user_admin_account_validate($form, &$form_state) {
-  $form_state['values']['accounts'] = array_filter($form_state['values']['accounts']);
-  if (count($form_state['values']['accounts']) == 0) {
-    form_set_error('', t('No users selected.'));
-  }
+function user_admin_create() {
+  $account = entity_create('user', array());
+  return entity_get_form($account, 'register');
 }
 
 /**
diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php
index 652c5b5..f944c71 100644
--- a/core/modules/user/user.api.php
+++ b/core/modules/user/user.api.php
@@ -124,7 +124,7 @@ function hook_user_cancel($edit, $account, $method) {
         ->condition('uid', $account->uid)
         ->execute()
         ->fetchCol();
-      node_mass_update($nodes, array('status' => 0));
+      node_mass_update($nodes, array('status' => 0), TRUE);
       break;
 
     case 'user_cancel_reassign':
@@ -135,7 +135,7 @@ function hook_user_cancel($edit, $account, $method) {
         ->condition('uid', $account->uid)
         ->execute()
         ->fetchCol();
-      node_mass_update($nodes, array('uid' => 0));
+      node_mass_update($nodes, array('uid' => 0), TRUE);
       // Anonymize old revisions.
       db_update('node_revision')
         ->fields(array('uid' => 0))
@@ -206,40 +206,6 @@ function hook_user_format_name_alter(&$name, $account) {
 }
 
 /**
- * Add mass user operations.
- *
- * This hook enables modules to inject custom operations into the mass operations
- * dropdown found at admin/people, by associating a callback function with
- * the operation, which is called when the form is submitted. The callback function
- * receives one initial argument, which is an array of the checked users.
- *
- * @return
- *   An array of operations. Each operation is an associative array that may
- *   contain the following key-value pairs:
- *   - "label": Required. The label for the operation, displayed in the dropdown menu.
- *   - "callback": Required. The function to call for the operation.
- *   - "callback arguments": Optional. An array of additional arguments to pass to
- *     the callback function.
- *
- */
-function hook_user_operations() {
-  $operations = array(
-    'unblock' => array(
-      'label' => t('Unblock the selected users'),
-      'callback' => 'user_user_operations_unblock',
-    ),
-    'block' => array(
-      'label' => t('Block the selected users'),
-      'callback' => 'user_user_operations_block',
-    ),
-    'cancel' => array(
-      'label' => t('Cancel the selected user accounts'),
-    ),
-  );
-  return $operations;
-}
-
-/**
  * Act on a user account being inserted or updated.
  *
  * This hook is invoked before the user account is saved to the database.
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 322311f..057e292 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -7,7 +7,7 @@
 use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
 use Drupal\file\Plugin\Core\Entity\File;
 use Drupal\user\Plugin\Core\Entity\User;
-use Drupal\user\UserRole;
+use Drupal\user\RoleInterface;
 use Drupal\Core\Template\Attribute;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
@@ -917,21 +917,12 @@ function user_menu() {
   $items['admin/people'] = array(
     'title' => 'People',
     'description' => 'Manage user accounts, roles, and permissions.',
-    'page callback' => 'user_admin',
-    'page arguments' => array('list'),
+    'page callback' => 'user_admin_account',
     'access arguments' => array('administer users'),
     'position' => 'left',
     'weight' => -4,
     'file' => 'user.admin.inc',
   );
-  $items['admin/people/people'] = array(
-    'title' => 'List',
-    'description' => 'Find and manage people interacting with your site.',
-    'access arguments' => array('administer users'),
-    'type' => MENU_DEFAULT_LOCAL_TASK,
-    'file' => 'user.admin.inc',
-  );
-
   // Permissions and role forms.
   $items['admin/people/permissions'] = array(
     'title' => 'Permissions',
@@ -971,10 +962,18 @@ function user_menu() {
 
   $items['admin/people/create'] = array(
     'title' => 'Add user',
-    'page arguments' => array('create'),
+    'page callback' => 'user_admin_create',
     'access arguments' => array('administer users'),
+    'file' => 'user.admin.inc',
     'type' => MENU_LOCAL_ACTION,
   );
+  $items['admin/people/cancel'] = array(
+    'title' => 'Add user',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('user_multiple_cancel_confirm'),
+    'access arguments' => array('administer users'),
+    'file' => 'user.admin.inc',
+  );
 
   // Administration pages.
   $items['admin/config/people'] = array(
@@ -1816,6 +1815,55 @@ function user_role_names($membersonly = FALSE, $permission = NULL) {
 }
 
 /**
+ * Implements hook_user_role_insert().
+ */
+function user_user_role_insert(RoleInterface $role) {
+  // Ignore the authenticated and anonymous roles.
+  if (in_array($role->id(), array(DRUPAL_AUTHENTICATED_RID, DRUPAL_ANONYMOUS_RID))) {
+    return;
+  }
+
+  $action = entity_create('action', array(
+    'id' => 'user_add_role_action.' . $role->id(),
+    'type' => 'user',
+    'label' => t('Add the @label role to the selected users', array('@label' => $role->label())),
+    'configuration' => array(
+      'rid' => $role->id(),
+    ),
+    'plugin' => 'user_add_role_action',
+  ));
+  $action->save();
+  $action = entity_create('action', array(
+    'id' => 'user_remove_role_action.' . $role->id(),
+    'type' => 'user',
+    'label' => t('Remove the @label role from the selected users', array('@label' => $role->label())),
+    'configuration' => array(
+      'rid' => $role->id(),
+    ),
+    'plugin' => 'user_remove_role_action',
+  ));
+  $action->save();
+}
+
+/**
+ * Implements hook_user_role_delete().
+ */
+function user_user_role_delete(RoleInterface $role) {
+  // Ignore the authenticated and anonymous roles.
+  if (in_array($role->id(), array(DRUPAL_AUTHENTICATED_RID, DRUPAL_ANONYMOUS_RID))) {
+    return;
+  }
+
+  $actions = entity_load_multiple('action', array(
+    'user_add_role_action.' . $role->id(),
+    'user_remove_role_action.' . $role->id(),
+  ));
+  foreach ($actions as $action) {
+    $action->delete();
+  }
+}
+
+/**
  * Retrieve an array of roles matching specified conditions.
  *
  * @param $membersonly
@@ -1989,79 +2037,9 @@ function user_role_revoke_permissions($rid, array $permissions = array()) {
 }
 
 /**
- * Implements hook_user_operations().
- */
-function user_user_operations($form = array(), $form_state = array()) {
-  $operations = array(
-    'unblock' => array(
-      'label' => t('Unblock the selected users'),
-      'callback' => 'user_user_operations_unblock',
-    ),
-    'block' => array(
-      'label' => t('Block the selected users'),
-      'callback' => 'user_user_operations_block',
-    ),
-    'cancel' => array(
-      'label' => t('Cancel the selected user accounts'),
-    ),
-  );
-
-  if (user_access('administer permissions')) {
-    $roles = user_role_names(TRUE);
-    unset($roles[DRUPAL_AUTHENTICATED_RID]);  // Can't edit authenticated role.
-
-    $add_roles = array();
-    foreach ($roles as $key => $value) {
-      $add_roles['add_role-' . $key] = $value;
-    }
-
-    $remove_roles = array();
-    foreach ($roles as $key => $value) {
-      $remove_roles['remove_role-' . $key] = $value;
-    }
-
-    if (count($roles)) {
-      $role_operations = array(
-        t('Add a role to the selected users') => array(
-          'label' => $add_roles,
-        ),
-        t('Remove a role from the selected users') => array(
-          'label' => $remove_roles,
-        ),
-      );
-
-      $operations += $role_operations;
-    }
-  }
-
-  // If the form has been posted, we need to insert the proper data for
-  // role editing if necessary.
-  if (!empty($form_state['submitted'])) {
-    $operation_rid = explode('-', $form_state['values']['operation']);
-    $operation = $operation_rid[0];
-    if ($operation == 'add_role' || $operation == 'remove_role') {
-      $rid = $operation_rid[1];
-      if (user_access('administer permissions')) {
-        $operations[$form_state['values']['operation']] = array(
-          'callback' => 'user_multiple_role_edit',
-          'callback arguments' => array($operation, $rid),
-        );
-      }
-      else {
-        watchdog('security', 'Detected malicious attempt to alter protected user fields.', array(), WATCHDOG_WARNING);
-        return;
-      }
-    }
-  }
-
-  return $operations;
-}
-
-/**
  * Callback function for admin mass unblocking users.
  */
 function user_user_operations_unblock($accounts) {
-  $accounts = user_load_multiple($accounts);
   foreach ($accounts as $account) {
     // Skip unblocking user if they are already unblocked.
     if ($account !== FALSE && $account->status == 0) {
@@ -2075,7 +2053,6 @@ function user_user_operations_unblock($accounts) {
  * Callback function for admin mass blocking users.
  */
 function user_user_operations_block($accounts) {
-  $accounts = user_load_multiple($accounts);
   foreach ($accounts as $account) {
     // Skip blocking user if they are already blocked.
     if ($account !== FALSE && $account->status == 1) {
@@ -2092,15 +2069,12 @@ function user_user_operations_block($accounts) {
  * Callback function for admin mass adding/deleting a user role.
  */
 function user_multiple_role_edit($accounts, $operation, $rid) {
-  $role_name = entity_load('user_role', $rid)->label();
-
   switch ($operation) {
     case 'add_role':
-      $accounts = user_load_multiple($accounts);
       foreach ($accounts as $account) {
         // Skip adding the role to the user if they already have it.
         if ($account !== FALSE && !isset($account->roles[$rid])) {
-          $roles = $account->roles + array($rid => $role_name);
+          $roles = $account->roles + array($rid => $rid);
           // For efficiency manually save the original account before applying
           // any changes.
           $account->original = clone $account;
@@ -2110,11 +2084,10 @@ function user_multiple_role_edit($accounts, $operation, $rid) {
       }
       break;
     case 'remove_role':
-      $accounts = user_load_multiple($accounts);
       foreach ($accounts as $account) {
         // Skip removing the role from the user if they already don't have it.
         if ($account !== FALSE && isset($account->roles[$rid])) {
-          $roles = array_diff($account->roles, array($rid => $role_name));
+          $roles = array_diff($account->roles, array($rid => $rid));
           // For efficiency manually save the original account before applying
           // any changes.
           $account->original = clone $account;
@@ -2127,11 +2100,13 @@ function user_multiple_role_edit($accounts, $operation, $rid) {
 }
 
 function user_multiple_cancel_confirm($form, &$form_state) {
-  $edit = $form_state['input'];
+  global $user;
+  // Retrieve the accounts to be canceled from the temp store.
+  $accounts = Drupal::service('user.tempstore')->get('user_user_operations_cancel')->get($user->uid);
 
   $form['accounts'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
-  $accounts = user_load_multiple(array_keys(array_filter($edit['accounts'])));
-  foreach ($accounts as $uid => $account) {
+  foreach ($accounts as $account) {
+    $uid = $account->id();
     // Prevent user 1 from being canceled.
     if ($uid <= 1) {
       continue;
@@ -2194,6 +2169,8 @@ function user_multiple_cancel_confirm($form, &$form_state) {
  */
 function user_multiple_cancel_confirm_submit($form, &$form_state) {
   global $user;
+  // Clear out the accounts from the temp store.
+  Drupal::service('user.tempstore')->get('user_user_operations_cancel')->delete($user->uid);
 
   if ($form_state['values']['confirm']) {
     foreach ($form_state['values']['accounts'] as $uid => $value) {
@@ -2492,44 +2469,6 @@ function user_node_load($nodes, $types) {
 }
 
 /**
- * Implements hook_action_info().
- */
-function user_action_info() {
-  return array(
-    'user_block_user_action' => array(
-      'label' => t('Block current user'),
-      'type' => 'user',
-      'configurable' => FALSE,
-      'triggers' => array('any'),
-    ),
-  );
-}
-
-/**
- * Blocks the current user.
- *
- * @ingroup actions
- */
-function user_block_user_action(&$entity, $context = array()) {
-  // First priority: If there is a $entity->uid, block that user.
-  // This is most likely a user object or the author if a node or comment.
-  if (isset($entity->uid)) {
-    $uid = $entity->uid;
-  }
-  elseif (isset($context['uid'])) {
-    $uid = $context['uid'];
-  }
-  // If neither of those are valid, then block the current user.
-  else {
-    $uid = $GLOBALS['user']->uid;
-  }
-  $account = user_load($uid);
-  $account->status = 0;
-  $account->save();
-  watchdog('action', 'Blocked user %name.', array('%name' => $account->name));
-}
-
-/**
  * Implements hook_form_FORM_ID_alter() for 'field_ui_field_instance_edit_form'.
  *
  * Add a checkbox for the 'user_register_form' instance settings on the 'Edit
diff --git a/core/modules/user/user.views.inc b/core/modules/user/user.views.inc
index 8a27f82..194d13a 100644
--- a/core/modules/user/user.views.inc
+++ b/core/modules/user/user.views.inc
@@ -318,6 +318,14 @@ function user_views_data() {
     ),
   );
 
+  $data['users']['user_bulk_form'] = array(
+    'title' => t('Bulk update'),
+    'help' => t('Add a form element that lets you run operations on multiple users.'),
+    'field' => array(
+      'id' => 'user_bulk_form',
+    ),
+  );
+
   // Define the base group of this table. Fields that don't have a group defined
   // will go into this field by default.
   $data['users_roles']['table']['group']  = t('User');
diff --git a/core/modules/views/lib/Drupal/views/Plugin/ViewsHandlerManager.php b/core/modules/views/lib/Drupal/views/Plugin/ViewsHandlerManager.php
index 5768aa9..7e154ce 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/ViewsHandlerManager.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/ViewsHandlerManager.php
@@ -8,7 +8,7 @@
 namespace Drupal\views\Plugin;
 
 use Drupal\Component\Plugin\PluginManagerBase;
-use Drupal\Component\Plugin\Factory\DefaultFactory;
+use Drupal\Core\Plugin\Factory\ContainerFactory;
 use Drupal\Core\Plugin\Discovery\CacheDecorator;
 use Drupal\views\Plugin\Discovery\ViewsHandlerDiscovery;
 
@@ -30,7 +30,7 @@ public function __construct($type, \Traversable $namespaces) {
     $this->discovery = new ViewsHandlerDiscovery($type, $namespaces);
     $this->discovery = new CacheDecorator($this->discovery, "views:$type", 'views_info');
 
-    $this->factory = new DefaultFactory($this->discovery);
+    $this->factory = new ContainerFactory($this);
   }
 
 }
diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/query/Sql.php b/core/modules/views/lib/Drupal/views/Plugin/views/query/Sql.php
index 2262f26..7e392fc 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/views/query/Sql.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/views/query/Sql.php
@@ -1604,7 +1604,7 @@ function load_entities(&$results) {
 
       foreach ($results as $index => $result) {
         // Store the entity id if it was found.
-        if (!empty($result->$id_alias)) {
+        if (isset($result->{$id_alias}) && $result->{$id_alias} != '') {
           $ids_by_table[$table_alias][$index] = $result->$id_alias;
         }
       }
