diff --git a/core/modules/action/action.info.yml b/core/modules/action_ui/action_ui.info.yml
similarity index 100%
rename from core/modules/action/action.info.yml
rename to core/modules/action_ui/action_ui.info.yml
diff --git a/core/modules/action_ui/action_ui.local_tasks.yml b/core/modules/action_ui/action_ui.local_tasks.yml
new file mode 100644
index 0000000000..0ca44138bf
--- /dev/null
+++ b/core/modules/action_ui/action_ui.local_tasks.yml
@@ -0,0 +1,4 @@
+action.admin:
+ route_name: action.admin
+ title: 'Manage actions'
+ base_route: action.admin
diff --git a/core/modules/action/action.module b/core/modules/action_ui/action_ui.module
similarity index 100%
rename from core/modules/action/action.module
rename to core/modules/action_ui/action_ui.module
diff --git a/core/modules/action/action.routing.yml b/core/modules/action_ui/action_ui.routing.yml
similarity index 100%
rename from core/modules/action/action.routing.yml
rename to core/modules/action_ui/action_ui.routing.yml
diff --git a/core/modules/action_ui/action_ui.views.inc b/core/modules/action_ui/action_ui.views.inc
new file mode 100644
index 0000000000..5650b14164
--- /dev/null
+++ b/core/modules/action_ui/action_ui.views.inc
@@ -0,0 +1,27 @@
+ [],
+ ];
+ $data['action']['action_bulk_form'] = [
+ 'title' => t('Bulk update'),
+ 'help' => t('Allows users to apply an action to one or more items.'),
+ 'field' => [
+ 'id' => 'action_bulk_form',
+ ],
+ ];
+ return $data;
+}
diff --git a/core/modules/action_ui/action_ui.views_execution.inc b/core/modules/action_ui/action_ui.views_execution.inc
new file mode 100644
index 0000000000..1c348e74ed
--- /dev/null
+++ b/core/modules/action_ui/action_ui.views_execution.inc
@@ -0,0 +1,22 @@
+');
+ $select_all = [
+ '#type' => 'checkbox',
+ '#default_value' => FALSE,
+ '#attributes' => ['class' => ['action-table-select-all']],
+ ];
+ return [
+ $select_all_placeholder => drupal_render($select_all),
+ ];
+}
diff --git a/core/modules/action_ui/lib/Drupal/action_ui/ActionAddFormController.php b/core/modules/action_ui/lib/Drupal/action_ui/ActionAddFormController.php
new file mode 100644
index 0000000000..7ffa6f095f
--- /dev/null
+++ b/core/modules/action_ui/lib/Drupal/action_ui/ActionAddFormController.php
@@ -0,0 +1,69 @@
+actionManager = $action_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager')->getStorageController('action'),
+ $container->get('plugin.manager.action')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $action_id
+ * The hashed version of the action ID.
+ */
+ public function buildForm(array $form, array &$form_state, $action_id = NULL) {
+ // In \Drupal\action_ui\Form\ActionAdminManageForm::buildForm() the action
+ // are hashed. Here we have to decrypt it to find the desired action ID.
+ foreach ($this->actionManager->getDefinitions() as $id => $definition) {
+ $key = Crypt::hashBase64($id);
+ if ($key === $action_id) {
+ $this->entity->setPlugin($id);
+ // Derive the label and type from the action 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_ui/lib/Drupal/action_ui/ActionEditFormController.php b/core/modules/action_ui/lib/Drupal/action_ui/ActionEditFormController.php
new file mode 100644
index 0000000000..acfd85ed4f
--- /dev/null
+++ b/core/modules/action_ui/lib/Drupal/action_ui/ActionEditFormController.php
@@ -0,0 +1,10 @@
+storageController = $storage_controller;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager')->getStorageController('action')
+ );
+ }
+
+ /**
+ * {@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'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#default_value' => $this->entity->label(),
+ '#maxlength' => '255',
+ '#description' => $this->t('A unique label for this advanced action. This label will be displayed in the interface of modules that integrate with actions.'),
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $this->entity->id(),
+ '#disabled' => !$this->entity->isNew(),
+ '#maxlength' => 64,
+ '#description' => $this->t('A unique name for this action. It must only contain lowercase letters, numbers and underscores.'),
+ '#machine_name' => [
+ 'exists' => [$this, 'exists'],
+ ],
+ ];
+ $form['plugin'] = [
+ '#type' => 'value',
+ '#value' => $this->entity->get('plugin'),
+ ];
+ $form['type'] = [
+ '#type' => 'value',
+ '#value' => $this->entity->getType(),
+ ];
+
+ if ($this->plugin instanceof PluginFormInterface) {
+ $form += $this->plugin->buildConfigurationForm($form, $form_state);
+ }
+
+ return parent::form($form, $form_state);
+ }
+
+ /**
+ * Determines if the action already exists.
+ *
+ * @param string $id
+ * The action ID.
+ *
+ * @return bool
+ * TRUE if the action exists, FALSE otherwise.
+ */
+ public function exists($id) {
+ $action = $this->storageController->load($id);
+ return !empty($action);
+ }
+
+ /**
+ * {@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 PluginFormInterface) {
+ $this->plugin->validateConfigurationForm($form, $form_state);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submit(array $form, array &$form_state) {
+ parent::submit($form, $form_state);
+
+ if ($this->plugin instanceof PluginFormInterface) {
+ $this->plugin->submitConfigurationForm($form, $form_state);
+ }
+ return $this->entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, array &$form_state) {
+ $this->entity->save();
+ drupal_set_message($this->t('The action has been successfully saved.'));
+
+ $form_state['redirect_route'] = [
+ 'route_name' => 'action.admin',
+ ];
+ }
+
+}
diff --git a/core/modules/action_ui/lib/Drupal/action_ui/ActionListController.php b/core/modules/action_ui/lib/Drupal/action_ui/ActionListController.php
new file mode 100644
index 0000000000..2b8f4abc12
--- /dev/null
+++ b/core/modules/action_ui/lib/Drupal/action_ui/ActionListController.php
@@ -0,0 +1,122 @@
+actionManager = $action_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_info) {
+ return new static(
+ $entity_info,
+ $container->get('entity.manager')->getStorageController($entity_info->id()),
+ $container->get('plugin.manager.action'),
+ $container->get('module_handler')
+ );
+ }
+
+ /**
+ * {@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'] = $this->getLabel($entity);
+ if ($this->hasConfigurableActions) {
+ $row += parent::buildRow($entity);
+ }
+ return $row;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildHeader() {
+ $header = [
+ 'type' => t('Action type'),
+ 'label' => t('Label'),
+ ] + parent::buildHeader();
+ return $header;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOperations(EntityInterface $entity) {
+ $operations = $entity->isConfigurable() ? parent::getOperations($entity) : [];
+ if (isset($operations['edit'])) {
+ $operations['edit']['title'] = t('Configure');
+ }
+ return $operations;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ $build['action_header']['#markup'] = '
' . t('Available actions:') . '
';
+ $build['action_table'] = parent::render();
+ if (!$this->hasConfigurableActions) {
+ unset($build['action_table']['#header']['operations']);
+ }
+ $build['action_admin_manage_form'] = drupal_get_form('Drupal\action_ui\Form\ActionAdminManageForm');
+ return $build;
+ }
+
+}
diff --git a/core/modules/action_ui/lib/Drupal/action_ui/Form/ActionAdminManageForm.php b/core/modules/action_ui/lib/Drupal/action_ui/Form/ActionAdminManageForm.php
new file mode 100644
index 0000000000..dedc609190
--- /dev/null
+++ b/core/modules/action_ui/lib/Drupal/action_ui/Form/ActionAdminManageForm.php
@@ -0,0 +1,93 @@
+manager = $manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.action')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'action_admin_manage';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, array &$form_state) {
+ $actions = [];
+ foreach ($this->manager->getDefinitions() as $id => $definition) {
+ if (is_subclass_of($definition['class'], '\Drupal\Core\Plugin\PluginFormInterface')) {
+ $key = Crypt::hashBase64($id);
+ $actions[$key] = $definition['label'] . '...';
+ }
+ }
+ $form['parent'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Create an advanced action'),
+ '#attributes' => ['class' => ['container-inline']],
+ ];
+ $form['parent']['action'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Action'),
+ '#title_display' => 'invisible',
+ '#options' => $actions,
+ '#empty_option' => $this->t('Choose an advanced action'),
+ ];
+ $form['parent']['actions'] = [
+ '#type' => 'actions',
+ ];
+ $form['parent']['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Create'),
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, array &$form_state) {
+ if ($form_state['values']['action']) {
+ $form_state['redirect_route'] = [
+ 'route_name' => 'action.admin_add',
+ 'route_parameters' => ['action_id' => $form_state['values']['action']],
+ ];
+ }
+ }
+
+}
diff --git a/core/modules/action_ui/lib/Drupal/action_ui/Form/ActionDeleteForm.php b/core/modules/action_ui/lib/Drupal/action_ui/Form/ActionDeleteForm.php
new file mode 100644
index 0000000000..ee4c567a04
--- /dev/null
+++ b/core/modules/action_ui/lib/Drupal/action_ui/Form/ActionDeleteForm.php
@@ -0,0 +1,49 @@
+t('Are you sure you want to delete the action %action?', ['%action' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelRoute() {
+ return [
+ 'route_name' => 'action.admin',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submit(array $form, array &$form_state) {
+ $this->entity->delete();
+
+ watchdog('user', 'Deleted action %aid (%action)', ['%aid' => $this->entity->id(), '%action' => $this->entity->label()]);
+ drupal_set_message($this->t('Action %action was deleted', ['%action' => $this->entity->label()]));
+
+ $form_state['redirect_route'] = [
+ 'route_name' => 'action.admin',
+ ];
+ }
+
+}
diff --git a/core/modules/action_ui/lib/Drupal/action_ui/Tests/ActionUninstallTest.php b/core/modules/action_ui/lib/Drupal/action_ui/Tests/ActionUninstallTest.php
new file mode 100644
index 0000000000..9ab9b399ef
--- /dev/null
+++ b/core/modules/action_ui/lib/Drupal/action_ui/Tests/ActionUninstallTest.php
@@ -0,0 +1,47 @@
+ 'Uninstall Action UI test',
+ 'description' => "Tests that uninstalling Action UI does not remove other module's actions.",
+ 'group' => 'Action UI',
+ ];
+ }
+
+ /**
+ * Tests Action uninstall.
+ */
+ public function testActionUninstall() {
+ \Drupal::moduleHandler()->uninstall(['action_ui']);
+
+ $this->assertTrue(entity_load('action', 'user_block_user_action', TRUE), 'Configuration entity \'user_block_user_action\' still exists after uninstalling action module.');
+
+ $admin_user = $this->drupalCreateUser(['administer users']);
+ $this->drupalLogin($admin_user);
+
+ $this->drupalGet('admin/people');
+ // Ensure we have the user_block_user_action listed.
+ $this->assertRaw('');
+
+ }
+
+}
diff --git a/core/modules/action_ui/lib/Drupal/action_ui/Tests/ConfigurationTest.php b/core/modules/action_ui/lib/Drupal/action_ui/Tests/ConfigurationTest.php
new file mode 100644
index 0000000000..63adbde405
--- /dev/null
+++ b/core/modules/action_ui/lib/Drupal/action_ui/Tests/ConfigurationTest.php
@@ -0,0 +1,87 @@
+drupalCreateUser(['administer actions']);
+ $this->drupalLogin($user);
+
+ // Make a POST request to admin/config/system/actions.
+ $edit = [];
+ $edit['action'] = Crypt::hashBase64('system_goto_action');
+ $this->drupalPostForm('admin/config/system/actions', $edit, t('Create'));
+ $this->assertResponse(200);
+
+ // Make a POST request to the individual action configuration page.
+ $edit = [];
+ $action_label = $this->randomName();
+ $edit['label'] = $action_label;
+ $edit['id'] = strtolower($action_label);
+ $edit['url'] = 'admin';
+ $this->drupalPostForm('admin/config/system/actions/add/' . Crypt::hashBase64('system_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.");
+ $this->assertText($action_label, "Make sure the action label appears on the configuration page after we've saved the complex action.");
+
+ // Make another POST request to the action edit page.
+ $this->clickLink(t('Configure'));
+ preg_match('|admin/config/system/actions/configure/(.+)|', $this->getUrl(), $matches);
+ $aid = $matches[1];
+ $edit = [];
+ $new_action_label = $this->randomName();
+ $edit['label'] = $new_action_label;
+ $edit['url'] = 'admin';
+ $this->drupalPostForm(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 = [];
+ $this->drupalPostForm("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', ['%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.");
+
+ $action = entity_load('action', $aid);
+ $this->assertFalse($action, 'Make sure the action is gone after being deleted.');
+ }
+
+}
diff --git a/core/modules/action_ui/tests/Drupal/action_ui/Tests/Menu/ActionLocalTasksTest.php b/core/modules/action_ui/tests/Drupal/action_ui/Tests/Menu/ActionLocalTasksTest.php
new file mode 100644
index 0000000000..7f5ebfd441
--- /dev/null
+++ b/core/modules/action_ui/tests/Drupal/action_ui/Tests/Menu/ActionLocalTasksTest.php
@@ -0,0 +1,29 @@
+directoryList = ['action_ui' => 'core/modules/action_ui'];
+ parent::setUp();
+ }
+
+ /**
+ * Tests local task existence.
+ */
+ public function testActionLocalTasks() {
+ $this->assertLocalTasks('action.admin', [['action.admin']]);
+ }
+
+}
diff --git a/core/modules/action_ui/tests/action_bulk_test/action_bulk_test.info.yml b/core/modules/action_ui/tests/action_bulk_test/action_bulk_test.info.yml
new file mode 100644
index 0000000000..6159cb8806
--- /dev/null
+++ b/core/modules/action_ui/tests/action_bulk_test/action_bulk_test.info.yml
@@ -0,0 +1,10 @@
+name: 'Action bulk form test'
+type: module
+description: 'Support module for action bulk form testing.'
+package: Testing
+version: VERSION
+core: 8.x
+hidden: true
+dependencies:
+ - drupal:action_ui
+ - drupal:views
diff --git a/core/modules/action_ui/tests/action_bulk_test/action_bulk_test.module b/core/modules/action_ui/tests/action_bulk_test/action_bulk_test.module
new file mode 100644
index 0000000000..fca6766c5e
--- /dev/null
+++ b/core/modules/action_ui/tests/action_bulk_test/action_bulk_test.module
@@ -0,0 +1,5 @@
+drupalLogin($this->web_user);
+ $comment_text = $this->randomName();
+ $subject = $this->randomName();
+ $comment = $this->postComment($this->node, $comment_text, $subject);
+
+ // Unpublish a comment.
+ $action = entity_load('action', 'comment_unpublish_action');
+ $action->execute([$comment]);
+ $this->assertEqual($comment->status->value, CommentInterface::NOT_PUBLISHED, 'Comment was unpublished');
+
+ // Publish a comment.
+ $action = entity_load('action', 'comment_publish_action');
+ $action->execute([$comment]);
+ $this->assertEqual($comment->status->value, CommentInterface::PUBLISHED, 'Comment was published');
+ }
+
+ /**
+ * Tests the unpublish comment by keyword action.
+ */
+ public function testCommentUnpublishByKeyword() {
+ $this->drupalLogin($this->admin_user);
+ $keyword_1 = $this->randomName();
+ $keyword_2 = $this->randomName();
+ $action = entity_create('action', [
+ 'id' => 'comment_unpublish_by_keyword_action',
+ 'label' => $this->randomName(),
+ 'type' => 'comment',
+ 'configuration' => [
+ 'keywords' => [$keyword_1, $keyword_2],
+ ],
+ 'plugin' => 'comment_unpublish_by_keyword_action',
+ ]);
+ $action->save();
+
+ $comment = $this->postComment($this->node, $keyword_2, $this->randomName());
+
+ // Load the full comment so that status is available.
+ $comment = comment_load($comment->id());
+
+ $this->assertTrue($comment->status->value == CommentInterface::PUBLISHED, 'The comment status was set to published.');
+
+ $action->execute([$comment]);
+ $this->assertTrue($comment->status->value == CommentInterface::NOT_PUBLISHED, 'The comment status was set to not published.');
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Action/EmailAction.php b/core/modules/system/lib/Drupal/system/Plugin/Action/EmailAction.php
new file mode 100644
index 0000000000..619709ddc4
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Plugin/Action/EmailAction.php
@@ -0,0 +1,157 @@
+token = $token;
+ $this->storageController = $entity_manager->getStorageController('user');
+ }
+
+ /**
+ * {@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'),
+ $container->get('entity.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute($entity = NULL) {
+ 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_accounts = $this->storageController->loadByProperties(['mail' => $recipient]);
+ $recipient_account = reset($recipient_accounts);
+ if ($recipient_account) {
+ $langcode = $recipient_account->getPreferredLangcode();
+ }
+ else {
+ $langcode = language_default()->id;
+ }
+ $params = ['context' => $this->configuration];
+
+ if (drupal_mail('system', 'action_send_email', $recipient, $langcode, $params)) {
+ watchdog('action', 'Sent email to %recipient', ['%recipient' => $recipient]);
+ }
+ else {
+ watchdog('error', 'Unable to send email to %recipient', ['%recipient' => $recipient]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'recipient' => '',
+ 'subject' => '',
+ 'message' => '',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, array &$form_state) {
+ $form['recipient'] = [
+ '#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'] = [
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => $this->configuration['subject'],
+ '#maxlength' => '254',
+ '#description' => t('The subject of the message.'),
+ ];
+ $form['message'] = [
+ '#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 validateConfigurationForm(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', $form_state, t('Enter a valid email address or use a token e-mail address such as %author.', ['%author' => '[node:author:mail]']));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(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/system/lib/Drupal/system/Plugin/Action/GotoAction.php b/core/modules/system/lib/Drupal/system/Plugin/Action/GotoAction.php
new file mode 100644
index 0000000000..2fe0822519
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Plugin/Action/GotoAction.php
@@ -0,0 +1,110 @@
+dispatcher = $dispatcher;
+ $this->urlGenerator = $url_generator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+ return new static($configuration, $plugin_id, $plugin_definition, $container->get('event_dispatcher'), $container->get('url_generator'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute($object = NULL) {
+ $url = $this->urlGenerator
+ ->generateFromPath($this->configuration['url'], ['absolute' => TRUE]);
+ $response = new RedirectResponse($url);
+ $listener = function ($event) use ($response) {
+ $event->setResponse($response);
+ };
+ // Add the listener to the event dispatcher.
+ $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'url' => '',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, array &$form_state) {
+ $form['url'] = [
+ '#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.', ['@url' => 'http://drupal.org']),
+ '#default_value' => $this->configuration['url'],
+ '#required' => TRUE,
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, array &$form_state) {
+ $this->configuration['url'] = $form_state['values']['url'];
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Action/MessageAction.php b/core/modules/system/lib/Drupal/system/Plugin/Action/MessageAction.php
new file mode 100644
index 0000000000..9779141690
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Plugin/Action/MessageAction.php
@@ -0,0 +1,86 @@
+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 execute($entity = NULL) {
+ if (empty($this->configuration['node'])) {
+ $this->configuration['node'] = $entity;
+ }
+ $message = $this->token->replace(Xss::filterAdmin($this->configuration['message']), $this->configuration);
+ drupal_set_message($message);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'message' => '',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, array &$form_state) {
+ $form['message'] = [
+ '#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 submitConfigurationForm(array &$form, array &$form_state) {
+ $this->configuration['message'] = $form_state['values']['message'];
+ unset($this->configuration['node']);
+ }
+
+}
diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/BulkFormTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/BulkFormTest.php
new file mode 100644
index 0000000000..ac5cd3d89d
--- /dev/null
+++ b/core/modules/views/lib/Drupal/views/Tests/Handler/BulkFormTest.php
@@ -0,0 +1,122 @@
+drupalCreateNode([
+ 'sticky' => FALSE,
+ 'created' => $timestamp,
+ 'changed' => $timestamp,
+ ]);
+ }
+
+ $this->drupalGet('test_bulk_form');
+
+ // Test that the views edit header appears first.
+ $first_form_element = $this->xpath('//form/div/div[1][@id = :id]', [':id' => 'edit-header']);
+ $this->assertTrue($first_form_element, 'The views form edit header appears first.');
+
+ $this->assertFieldById('edit-action', NULL, 'The action select field appears.');
+
+ // Make sure a checkbox appears on all rows.
+ $edit = [];
+ for ($i = 0; $i < 10; $i++) {
+ $this->assertFieldById('edit-action-bulk-form-' . $i, NULL, format_string('The checkbox on row @row appears.', ['@row' => $i]));
+ $edit["action_bulk_form[$i]"] = TRUE;
+ }
+
+ // Set all nodes to sticky and check that.
+ $edit += ['action' => 'node_make_sticky_action'];
+ $this->drupalPostForm(NULL, $edit, t('Apply'));
+
+ foreach ($nodes as $node) {
+ $changed_node = node_load($node->id());
+ $this->assertTrue($changed_node->isSticky(), format_string('Node @nid got marked as sticky.', ['@nid' => $node->id()]));
+ }
+
+ $this->assertText('Make content sticky was applied to 10 items.');
+
+ // Unpublish just one node.
+ $node = node_load($nodes[0]->id());
+ $this->assertTrue($node->isPublished(), 'The node is published.');
+
+ $edit = ['action_bulk_form[0]' => TRUE, 'action' => 'node_unpublish_action'];
+ $this->drupalPostForm(NULL, $edit, t('Apply'));
+
+ $this->assertText('Unpublish content was applied to 1 item.');
+
+ // Load the node again.
+ $node = node_load($node->id(), TRUE);
+ $this->assertFalse($node->isPublished(), 'A single node has been unpublished.');
+
+ // The second node should still be published.
+ $node = node_load($nodes[1]->id(), TRUE);
+ $this->assertTrue($node->isPublished(), 'An unchecked node is still published.');
+
+ // Set up to include just the sticky actions.
+ $view = views_get_view('test_bulk_form');
+ $display = &$view->storage->getDisplay('default');
+ $display['display_options']['fields']['action_bulk_form']['include_exclude'] = 'include';
+ $display['display_options']['fields']['action_bulk_form']['selected_actions']['node_make_sticky_action'] = 'node_make_sticky_action';
+ $display['display_options']['fields']['action_bulk_form']['selected_actions']['node_make_unsticky_action'] = 'node_make_unsticky_action';
+ $view->save();
+
+ $this->drupalGet('test_bulk_form');
+ $options = $this->xpath('//select[@id=:id]/option', [':id' => 'edit-action']);
+ $this->assertEqual(count($options), 2);
+ $this->assertOption('edit-action', 'node_make_sticky_action');
+ $this->assertOption('edit-action', 'node_make_unsticky_action');
+
+ // Set up to exclude the sticky actions.
+ $view = views_get_view('test_bulk_form');
+ $display = &$view->storage->getDisplay('default');
+ $display['display_options']['fields']['action_bulk_form']['include_exclude'] = 'exclude';
+ $view->save();
+
+ $this->drupalGet('test_bulk_form');
+ $this->assertNoOption('edit-action', 'node_make_sticky_action');
+ $this->assertNoOption('edit-action', 'node_make_unsticky_action');
+
+ // Check the default title.
+ $this->drupalGet('test_bulk_form');
+ $result = $this->xpath('//label[@for="edit-action"]');
+ $this->assertEqual('With selection', (string) $result[0]);
+
+ // Setup up a different bulk form title.
+ $view = views_get_view('test_bulk_form');
+ $display = &$view->storage->getDisplay('default');
+ $display['display_options']['fields']['action_bulk_form']['action_title'] = 'Test title';
+ $view->save();
+
+ $this->drupalGet('test_bulk_form');
+ $result = $this->xpath('//label[@for="edit-action"]');
+ $this->assertEqual('Test title', (string) $result[0]);
+ }
+
+}