first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
name: Actions UI
type: module
description: 'Allows configuration of tasks to be executed in response to events.'
package: Core
# version: VERSION
lifecycle: deprecated
lifecycle_link: https://www.drupal.org/node/3223395#s-action
configure: entity.action.collection
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,5 @@
action.admin:
title: Actions
description: 'Create tasks that the system can execute.'
route_name: entity.action.collection
parent: system.admin_config_system

View File

@@ -0,0 +1,4 @@
action.admin:
route_name: entity.action.collection
title: 'Manage actions'
base_route: entity.action.collection

View File

@@ -0,0 +1,72 @@
<?php
/**
* @file
* This is the Actions UI module for executing stored actions.
*/
use Drupal\Core\Url;
use Drupal\action\Form\ActionAddForm;
use Drupal\action\Form\ActionEditForm;
use Drupal\system\Plugin\migrate\source\Action;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function action_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.action':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Actions UI module provides tasks that can be executed by the site such as unpublishing content, sending email messages, or blocking a user. Other modules can trigger these actions when specific system events happen; for example, when new content is posted or when a user logs in. Modules can also provide additional actions. For more information, see the <a href=":documentation">online documentation for the Actions UI module</a>.', [':documentation' => 'https://www.drupal.org/documentation/modules/action']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Using simple actions') . '</dt>';
$output .= '<dd>' . t('<em>Simple actions</em> do not require configuration and are listed automatically as available on the <a href=":actions">Actions administration page</a>.', [':actions' => Url::fromRoute('entity.action.collection')->toString()]) . '</dd>';
$output .= '<dt>' . t('Creating and configuring advanced actions') . '</dt>';
$output .= '<dd>' . t('<em>Advanced actions</em> are user-created and have to be configured individually. Create an advanced action on the <a href=":actions">Actions administration page</a> by selecting an action type from the drop-down list. Then configure your action, for example by specifying the recipient of an automated email message.', [':actions' => Url::fromRoute('entity.action.collection')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.action.collection':
$output = '<p>' . t('There are two types of actions: simple and advanced. Simple actions do not require any additional configuration and are listed here automatically. Advanced actions need to be created and configured before they can be used because they have options that need to be specified; for example, sending an email to a specified address or unpublishing content containing certain words. To create an advanced action, select the action from the drop-down list in the advanced action section below and click the <em>Create</em> button.') . '</p>';
return $output;
case 'action.admin_add':
case 'entity.action.edit_form':
return '<p>' . t('An advanced action offers additional configuration options which may be filled out below. Changing the <em>Label</em> field is recommended in order to better identify the precise action taking place.') . '</p>';
}
}
/**
* Implements hook_entity_type_build().
*/
function action_entity_type_build(array &$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
$entity_types['action']
->setFormClass('add', ActionAddForm::class)
->setFormClass('edit', ActionEditForm::class)
->setFormClass('delete', 'Drupal\action\Form\ActionDeleteForm')
->setListBuilderClass('Drupal\action\ActionListBuilder')
->setLinkTemplate('delete-form', '/admin/config/system/actions/configure/{action}/delete')
->setLinkTemplate('edit-form', '/admin/config/system/actions/configure/{action}')
->setLinkTemplate('collection', '/admin/config/system/actions');
}
/**
* Implements hook_migration_plugins_alter().
*/
function action_migration_plugins_alter(array &$migrations) {
foreach ($migrations as $migration_id => $migration) {
// Add Actions plugins in actions module.
/** @var \Drupal\migrate\Plugin\migrate\source\SqlBase $source_plugin */
$source_plugin = \Drupal::service('plugin.manager.migration')
->createStubMigration($migration)
->getSourcePlugin();
if (is_a($source_plugin, Action::class) && isset($migration['process']['plugin'])) {
$migrations[$migration_id]['process']['plugin'][0]['map']['comment_unpublish_by_keyword_action'] = 'comment_unpublish_by_keyword_action';
$migrations[$migration_id]['process']['plugin'][0]['map']['node_unpublish_by_keyword_action'] = 'node_unpublish_by_keyword_action';
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @file
* Post update functions for Actions UI module.
*/
/**
* Implements hook_removed_post_updates().
*/
function action_removed_post_updates() {
return [
'action_post_update_move_plugins' => '10.0.0',
'action_post_update_remove_settings' => '10.0.0',
];
}

View File

@@ -0,0 +1,31 @@
entity.action.collection:
path: '/admin/config/system/actions'
defaults:
_title: 'Actions'
_entity_list: 'action'
requirements:
_permission: 'administer actions'
action.admin_add:
path: '/admin/config/system/actions/add/{action_id}'
defaults:
_entity_form: 'action.add'
_title: 'Add action'
requirements:
_permission: 'administer actions'
entity.action.edit_form:
path: '/admin/config/system/actions/configure/{action}'
defaults:
_entity_form: 'action.edit'
_title: 'Edit action'
requirements:
_permission: 'administer actions'
entity.action.delete_form:
path: '/admin/config/system/actions/configure/{action}/delete'
defaults:
_entity_form: 'action.delete'
_title: 'Delete action'
requirements:
_permission: 'administer actions'

View File

@@ -0,0 +1,28 @@
---
label: 'Creating an advanced action'
related:
- action.overview
- views_ui.bulk_operations
---
{% set actions_link_text %}
{% trans %}Actions{% endtrans %}
{% endset %}
{% set actions = render_var(help_route_link(actions_link_text, 'entity.action.collection')) %}
{% set action_permissions_link_text %}
{% trans %}Administer actions{% endtrans %}
{% endset %}
{% set action_permissions = render_var(help_route_link(action_permissions_link_text, 'user.admin_permissions.module', {'modules': 'action'})) %}
{% set action_overview = render_var(help_topic_link('action.overview')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create an advanced action. You can, for example, create an action to change the author of multiple content items. See {{ action_overview }} for more about actions.{% endtrans %}</p>
<h2>{% trans %}Who can create actions?{% endtrans %}</h2>
<p>{% trans %}Users with the <em>{{ action_permissions }}</em> permission (typically administrators) can create actions.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>System</em> &gt; <em>{{ actions }}</em>. A list of all actions is shown.{% endtrans %}</li>
<li>{% trans %}Choose an advanced action from the dropdown and click <em>Create</em>.{% endtrans %}</li>
<li>{% trans %}Enter a name for the action in the <em>Label</em> field. This label will be visible for the user.{% endtrans %}</li>
<li>{% trans %}Configure any of the other available options. These will depend on the kind of action that you have chosen.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. You will be returned to the list of actions, with your new action added to the list.{% endtrans %}</li>
<li>{% trans %}To edit an action you have previously created, click <em>Configure</em> in the <em>Operations</em> drop-down list. To delete an action you have previously created, click <em>Delete</em> in the <em>Operations</em> drop-down list.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,25 @@
---
label: 'Configuring actions'
top_level: true
related:
- views.overview
- views_ui.bulk_operations
---
{% set actions_link_text %}
{% trans %}Actions administration page{% endtrans %}
{% endset %}
{% set actions_page = render_var(help_route_link(actions_link_text, 'entity.action.collection')) %}
<h2>{% trans %}What are actions?{% endtrans %}</h2>
<p>{% trans %}Actions are module-defined tasks that can be executed on the site; for example, unpublishing content, sending an email message, or blocking a user.{% endtrans %}</p>
<h2>{% trans %}What are simple actions?{% endtrans %}</h2>
<p>{% trans %}Simple actions do not require configuration. They are automatically available to be executed, and are always listed as available on the {{ actions_page }}.{% endtrans %}</p>
<h2>{% trans %}What are advanced actions?{% endtrans %}</h2>
<p>{% trans %}Advanced actions require configuration. Before they are available for listing and execution, they need to be created and configured. For example, for an action that sends email, you would need to configure the email address.{% endtrans %}</p>
<h2>{% trans %}How are actions executed?{% endtrans %}</h2>
<p>{% trans %}In the core software, actions can be executed through a <em>bulk operations form</em> added to a view; if you have the core Views module installed, see the related topic "Managing content listings (views)" for more information about views and bulk operations.{% endtrans %}</p>
<h2>{% trans %}Configuring actions overview{% endtrans %}</h2>
<p>{% trans %}The Actions UI module provides a user interface for listing and configuring actions. The core Views UI module provides a user interface for creating views, which may include bulk operations forms for executing actions. See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/documentation/modules/action">{% trans %}Online documentation for the Actions UI module{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,134 @@
<?php
namespace Drupal\action;
use Drupal\action\Form\ActionAdminManageForm;
use Drupal\Core\Action\ActionManager;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of action entities.
*
* @see \Drupal\system\Entity\Action
* @see action_entity_type_build()
*/
class ActionListBuilder extends ConfigEntityListBuilder {
/**
* @var bool
*/
protected $hasConfigurableActions = FALSE;
/**
* The action plugin manager.
*
* @var \Drupal\Core\Action\ActionManager
*/
protected $actionManager;
/**
* Constructs a new ActionListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The action storage.
* @param \Drupal\Core\Action\ActionManager $action_manager
* The action plugin manager.
* @param \Drupal\Core\Form\FormBuilderInterface $formBuilder
* The form builder.
*/
public function __construct(
EntityTypeInterface $entity_type,
EntityStorageInterface $storage,
ActionManager $action_manager,
protected ?FormBuilderInterface $formBuilder = NULL,
) {
parent::__construct($entity_type, $storage);
$this->actionManager = $action_manager;
if (!$formBuilder) {
@trigger_error('Calling ' . __METHOD__ . ' without the $formBuilder argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3159776', E_USER_DEPRECATED);
$this->formBuilder = \Drupal::service('form_builder');
}
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('plugin.manager.action'),
$container->get('form_builder')
);
}
/**
* {@inheritdoc}
*/
public function load() {
$entities = parent::load();
foreach ($entities as $entity) {
if ($entity->isConfigurable()) {
$this->hasConfigurableActions = TRUE;
break;
}
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['type'] = $entity->getType();
$row['label'] = $entity->label();
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 getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
if (isset($operations['edit'])) {
$operations['edit']['title'] = t('Configure');
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function render() {
$build['action_admin_manage_form'] = $this->formBuilder->getForm(ActionAdminManageForm::class);
$build['action_header']['#markup'] = '<h3>' . $this->t('Available actions:') . '</h3>';
$build['action_table'] = parent::render();
if (!$this->hasConfigurableActions) {
unset($build['action_table']['table']['#header']['operations']);
}
return $build;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\action\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form for action add forms.
*
* @internal
*/
class ActionAddForm extends ActionFormBase {
/**
* {@inheritdoc}
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $action_id
* The action ID.
*/
public function buildForm(array $form, FormStateInterface $form_state, $action_id = NULL) {
$this->entity->setPlugin($action_id);
// Derive the label and type from the action definition.
$definition = $this->entity->getPluginDefinition();
$this->entity->set('label', $definition['label']);
$this->entity->set('type', $definition['type']);
return parent::buildForm($form, $form_state);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\action\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Action\ActionManager;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a configuration form for configurable actions.
*
* @internal
*/
class ActionAdminManageForm extends FormBase {
/**
* The action plugin manager.
*
* @var \Drupal\Core\Action\ActionManager
*/
protected $manager;
/**
* Constructs a new ActionAdminManageForm.
*
* @param \Drupal\Core\Action\ActionManager $manager
* The action plugin manager.
*/
public function __construct(ActionManager $manager) {
$this->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, FormStateInterface $form_state) {
$actions = [];
foreach ($this->manager->getDefinitions() as $id => $definition) {
$actions[$id] = $definition['label'];
}
asort($actions);
$form['parent'] = [
'#type' => 'details',
'#title' => $this->t('Create an advanced action'),
'#attributes' => ['class' => ['container-inline']],
'#open' => TRUE,
];
$form['parent']['action'] = [
'#type' => 'select',
'#title' => $this->t('Action'),
'#title_display' => 'invisible',
'#options' => $actions,
'#empty_option' => $this->t('- Select -'),
];
$form['parent']['actions'] = [
'#type' => 'actions',
];
$form['parent']['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Create'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getValue('action')) {
$form_state->setRedirect(
'action.admin_add',
['action_id' => $form_state->getValue('action')]
);
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Drupal\action\Form;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Url;
/**
* Builds a form to delete an action.
*
* @internal
*/
class ActionDeleteForm extends EntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.action.collection');
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Drupal\action\Form;
/**
* Provides a form for action edit forms.
*
* @internal
*/
class ActionEditForm extends ActionFormBase {
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Drupal\action\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base form for action forms.
*/
abstract class ActionFormBase extends EntityForm {
/**
* The action storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* The action entity.
*
* @var \Drupal\system\ActionConfigEntityInterface
*/
protected $entity;
/**
* Constructs a new action form.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The action storage.
*/
public function __construct(EntityStorageInterface $storage) {
$this->storage = $storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('action')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $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 ($plugin = $this->getPlugin()) {
$form += $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->storage->load($id);
return !empty($action);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
unset($actions['delete']);
return $actions;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
if ($plugin = $this->getPlugin()) {
$plugin->validateConfigurationForm($form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
if ($plugin = $this->getPlugin()) {
$plugin->submitConfigurationForm($form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$this->entity->save();
$this->messenger()->addStatus($this->t('The action has been successfully saved.'));
$form_state->setRedirect('entity.action.collection');
}
/**
* Gets the action plugin while ensuring it implements configuration form.
*
* @return \Drupal\Core\Action\ActionInterface|\Drupal\Core\Plugin\PluginFormInterface|null
* The action plugin, or NULL if it does not implement configuration forms.
*/
protected function getPlugin() {
if ($this->entity->getPlugin() instanceof PluginFormInterface) {
return $this->entity->getPlugin();
}
return NULL;
}
}

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace Drupal\action\Plugin\Action;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Database\Connection;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Assigns ownership of a node to a user.
*/
#[Action(
id: 'node_assign_owner_action',
label: new TranslatableMarkup('Change the author of content'),
type: 'node'
)]
class AssignOwnerNode extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Constructs a new AssignOwnerNode action.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $connection) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->connection = $connection;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition,
$container->get('database')
);
}
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
/** @var \Drupal\node\NodeInterface $entity */
$entity->setOwnerId($this->configuration['owner_uid'])->save();
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'owner_uid' => '',
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$description = $this->t('The username of the user to which you would like to assign ownership.');
$count = $this->connection->query("SELECT COUNT(*) FROM {users}")->fetchField();
// Use dropdown for fewer than 200 users; textbox for more than that.
if (intval($count) < 200) {
$options = [];
$result = $this->connection->query("SELECT [uid], [name] FROM {users_field_data} WHERE [uid] > 0 AND [default_langcode] = 1 ORDER BY [name]");
foreach ($result as $data) {
$options[$data->uid] = $data->name;
}
$form['owner_uid'] = [
'#type' => 'select',
'#title' => $this->t('Username'),
'#default_value' => $this->configuration['owner_uid'],
'#options' => $options,
'#description' => $description,
];
}
else {
$form['owner_uid'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Username'),
'#target_type' => 'user',
'#selection_settings' => [
'include_anonymous' => FALSE,
],
'#default_value' => User::load($this->configuration['owner_uid']),
// Validation is done in static::validateConfigurationForm().
'#validate_reference' => FALSE,
'#size' => '6',
'#maxlength' => '60',
'#description' => $description,
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
$exists = (bool) $this->connection->queryRange('SELECT 1 FROM {users_field_data} WHERE [uid] = :uid AND [default_langcode] = 1', 0, 1, [':uid' => $form_state->getValue('owner_uid')])->fetchField();
if (!$exists) {
$form_state->setErrorByName('owner_uid', $this->t('Enter a valid username.'));
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['owner_uid'] = $form_state->getValue('owner_uid');
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$result = $object->access('update', $account, TRUE)
->andIf($object->getOwner()->access('edit', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\action\Plugin\Action;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Unpublishes a comment containing certain keywords.
*/
#[Action(
id: 'comment_unpublish_by_keyword_action',
label: new TranslatableMarkup('Unpublish comment containing keyword(s)'),
type: 'comment'
)]
class UnpublishByKeywordComment extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
/**
* The comment entity builder handler.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
protected $viewBuilder;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs an UnpublishByKeywordComment object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityViewBuilderInterface $comment_view_builder
* The comment entity builder handler.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityViewBuilderInterface $comment_view_builder, RendererInterface $renderer) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->viewBuilder = $comment_view_builder;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getViewBuilder('comment'),
$container->get('renderer')
);
}
/**
* {@inheritdoc}
*/
public function execute($comment = NULL) {
$build = $this->viewBuilder->view($comment);
$text = (string) $this->renderer->renderInIsolation($build);
foreach ($this->configuration['keywords'] as $keyword) {
if (str_contains($text, $keyword)) {
$comment->setUnpublished();
$comment->save();
break;
}
}
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'keywords' => [],
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['keywords'] = [
'#title' => $this->t('Keywords'),
'#type' => 'textarea',
'#description' => $this->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' => Tags::implode($this->configuration['keywords']),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords'));
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
$result = $object->access('update', $account, TRUE)
->andIf($object->status->access('edit', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\action\Plugin\Action;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Unpublishes a node containing certain keywords.
*/
#[Action(
id: 'node_unpublish_by_keyword_action',
label: new TranslatableMarkup('Unpublish content containing keyword(s)'),
type: 'node'
)]
class UnpublishByKeywordNode extends ConfigurableActionBase {
/**
* {@inheritdoc}
*/
public function execute($node = NULL) {
$elements = \Drupal::entityTypeManager()
->getViewBuilder('node')
->view(clone $node);
$render = (string) \Drupal::service('renderer')->renderInIsolation($elements);
foreach ($this->configuration['keywords'] as $keyword) {
if (str_contains($render, $keyword) || str_contains($node->label(), $keyword)) {
$node->setUnpublished();
$node->save();
break;
}
}
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'keywords' => [],
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['keywords'] = [
'#title' => $this->t('Keywords'),
'#type' => 'textarea',
'#description' => $this->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' => Tags::implode($this->configuration['keywords']),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords'));
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$access = $object->access('update', $account, TRUE)
->andIf($object->status->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}

View File

@@ -0,0 +1,11 @@
name: action_form_ajax_test
type: module
description: 'module used for testing ajax in action config entity forms.'
package: Core
# version: VERSION
hidden: true
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\action_form_ajax_test\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin used for testing AJAX in action config entity forms.
*/
#[Action(
id: 'action_form_ajax_test',
label: new TranslatableMarkup('action_form_ajax_test'),
type: 'system'
)]
class ActionAjaxTest extends ConfigurableActionBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'party_time' => '',
];
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowed();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function execute() {
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$having_a_party = $form_state->getValue('having_a_party', !empty($this->configuration['party_time']));
$form['having_a_party'] = [
'#type' => 'checkbox',
'#title' => $this->t('Are we having a party?'),
'#ajax' => [
'wrapper' => 'party-container',
'callback' => [$this, 'partyCallback'],
],
'#default_value' => $having_a_party,
];
$form['container'] = [
'#type' => 'container',
'#prefix' => '<div id="party-container">',
'#suffix' => '</div>',
];
if ($having_a_party) {
$form['container']['party_time'] = [
'#type' => 'textfield',
'#title' => $this->t('Party time'),
'#default_value' => $this->configuration['party_time'],
];
}
return $form;
}
/**
* Callback for party checkbox.
*/
public function partyCallback(array $form, FormStateInterface $form_state) {
return $form['container'];
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['party_time'] = $form_state->getValue('party_time');
}
}

View File

@@ -0,0 +1 @@
recursion_limit: 35

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Test behaviors when visiting the action listing page.
*
* @group action
* @group legacy
*/
class ActionListTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['action', 'user'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the behavior when there are no actions to list in the admin page.
*/
public function testEmptyActionList(): void {
// Create a user with permission to view the actions administration pages.
$this->drupalLogin($this->drupalCreateUser(['administer actions']));
// Ensure the empty text appears on the action list page.
/** @var \Drupal\Core\Entity\EntityStorageInterface $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('action');
$actions = $storage->loadMultiple();
$storage->delete($actions);
$this->drupalGet('/admin/config/system/actions');
$this->assertSession()->pageTextContains('There are no actions yet.');
}
/**
* Tests that non-configurable actions can be created by the UI.
*/
public function testNonConfigurableActionsCanBeCreated(): void {
$this->drupalLogin($this->drupalCreateUser(['administer actions']));
$this->drupalGet('/admin/config/system/actions');
$this->assertSession()->elementExists('css', 'select > option[value="user_block_user_action"]');
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that uninstalling Actions UI does not remove other modules' actions.
*
* @group action
* @group legacy
* @see \Drupal\views\Plugin\views\field\BulkForm
* @see \Drupal\user\Plugin\Action\BlockUser
*/
class ActionUninstallTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['views', 'action'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests Actions UI uninstall.
*/
public function testActionUninstall(): void {
\Drupal::service('module_installer')->uninstall(['action']);
$storage = $this->container->get('entity_type.manager')->getStorage('action');
$storage->resetCache(['user_block_user_action']);
$this->assertNotEmpty($storage->load('user_block_user_action'), '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->assertSession()->responseContains('<option value="user_block_user_action">Block the selected user(s)</option>');
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Functional;
use Drupal\system\Entity\Action;
use Drupal\Tests\BrowserTestBase;
/**
* Tests complex actions configuration.
*
* @group action
* @group legacy
*/
class ConfigurationTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['action'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests configuration of advanced actions through administration interface.
*/
public function testActionConfiguration(): void {
// Create a user with permission to view the actions administration pages.
$user = $this->drupalCreateUser(['administer actions']);
$this->drupalLogin($user);
// Make a POST request to admin/config/system/actions.
$edit = [];
$edit['action'] = 'action_goto_action';
$this->drupalGet('admin/config/system/actions');
$this->submitForm($edit, 'Create');
$this->assertSession()->statusCodeEquals(200);
// Make a POST request to the individual action configuration page.
$edit = [];
$action_label = $this->randomMachineName();
$edit['label'] = $action_label;
$edit['id'] = strtolower($action_label);
$edit['url'] = 'admin';
$this->drupalGet('admin/config/system/actions/add/action_goto_action');
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$action_id = $edit['id'];
// Make sure that the new complex action was saved properly.
$this->assertSession()->pageTextContains('The action has been successfully saved.');
// The action label appears on the configuration page.
$this->assertSession()->pageTextContains($action_label);
// Make another POST request to the action edit page.
$this->clickLink('Configure');
$edit = [];
$new_action_label = $this->randomMachineName();
$edit['label'] = $new_action_label;
$edit['url'] = 'admin';
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
// Make sure that the action updated properly.
$this->assertSession()->pageTextContains('The action has been successfully saved.');
// The old action label does NOT appear on the configuration page.
$this->assertSession()->pageTextNotContains($action_label);
// The action label appears on the configuration page after we've updated
// the complex action.
$this->assertSession()->pageTextContains($new_action_label);
// Make sure the URL appears when re-editing the action.
$this->clickLink('Configure');
$this->assertSession()->fieldValueEquals('url', 'admin');
// Make sure that deletions work properly.
$this->drupalGet('admin/config/system/actions');
$this->clickLink('Delete');
$this->assertSession()->statusCodeEquals(200);
$edit = [];
$this->submitForm($edit, 'Delete');
$this->assertSession()->statusCodeEquals(200);
// Make sure that the action was actually deleted.
$this->assertSession()->pageTextContains("The action $new_action_label has been deleted.");
$this->drupalGet('admin/config/system/actions');
$this->assertSession()->statusCodeEquals(200);
// The action label does not appear on the overview page.
$this->assertSession()->pageTextNotContains($new_action_label);
$action = Action::load($action_id);
$this->assertNull($action, 'Make sure the action is gone after being deleted.');
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for action.
*
* @group action
* @group legacy
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Functional\Node;
use Drupal\Component\Serialization\Json;
use Drupal\Tests\BrowserTestBase;
use Drupal\system\Entity\Action;
use Drupal\user\Entity\User;
/**
* Tests configuration of actions provided by the Node module.
*
* @group action
* @group legacy
*/
class NodeActionsConfigurationTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['action', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests configuration of the node_assign_owner_action action.
*/
public function testAssignOwnerNodeActionConfiguration(): void {
// Create a user with permission to view the actions administration pages.
$user = $this->drupalCreateUser(['administer actions']);
$this->drupalLogin($user);
// Make a POST request to admin/config/system/actions.
$edit = [];
$edit['action'] = 'node_assign_owner_action';
$this->drupalGet('admin/config/system/actions');
$this->submitForm($edit, 'Create');
$this->assertSession()->statusCodeEquals(200);
// Make a POST request to the individual action configuration page.
$edit = [];
$action_label = $this->randomMachineName();
$edit['label'] = $action_label;
$edit['id'] = strtolower($action_label);
$edit['owner_uid'] = $user->id();
$this->drupalGet('admin/config/system/actions/add/node_assign_owner_action');
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$action_id = $edit['id'];
$action_configure = sprintf('/admin/config/system/actions/configure/%s', $action_id);
$action_delete = sprintf('/admin/config/system/actions/configure/%s/delete', $action_id);
// Make sure that the new action was saved properly.
$this->assertSession()->pageTextContains('The action has been successfully saved.');
// Check that the label of the node_assign_owner_action action appears on
// the actions administration page after saving.
$this->assertSession()->pageTextContains($action_label);
// Make another POST request to the action edit page.
$this->assertSession()->linkByHrefExists($action_configure);
$this->drupalGet($action_configure);
$edit = [];
$new_action_label = $this->randomMachineName();
$edit['label'] = $new_action_label;
$edit['owner_uid'] = $user->id();
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
// Make sure that the action updated properly.
$this->assertSession()->pageTextContains('The action has been successfully saved.');
// Check that the old label for the node_assign_owner_action action does not
// appear on the actions administration page after updating.
$this->assertSession()->pageTextNotContains($action_label);
// Check that the new label for the node_assign_owner_action action appears
// on the actions administration page after updating.
$this->assertSession()->pageTextContains($new_action_label);
// Make sure that deletions work properly.
$this->drupalGet('admin/config/system/actions');
$this->assertSession()->linkByHrefExists($action_delete);
$this->drupalGet($action_delete);
$this->assertSession()->statusCodeEquals(200);
$edit = [];
$this->submitForm($edit, 'Delete');
$this->assertSession()->statusCodeEquals(200);
// Make sure that the action was actually deleted.
$this->assertSession()->pageTextContains("The action {$new_action_label} has been deleted.");
$this->drupalGet('admin/config/system/actions');
$this->assertSession()->statusCodeEquals(200);
// Check that the label for the node_assign_owner_action action does not
// appear on the actions administration page after deleting.
$this->assertSession()->pageTextNotContains($new_action_label);
$action = Action::load($action_id);
$this->assertNull($action, 'The node_assign_owner_action action is not available after being deleted.');
}
/**
* Tests the autocomplete field when configuring the AssignOwnerNode action.
*/
public function testAssignOwnerNodeActionAutocomplete(): void {
// Create 200 users to force the action's configuration page to use an
// autocomplete field instead of a select field. See
// \Drupal\node\Plugin\Action\AssignOwnerNode::buildConfigurationForm().
for ($i = 0; $i < 200; $i++) {
$this->drupalCreateUser();
}
// Create a user with permission to view the actions administration pages
// and additionally permission to administer users. Otherwise the user would
// not be able to reference the anonymous user.
$this->drupalLogin($this->drupalCreateUser(['administer actions', 'administer users']));
// Create AssignOwnerNode action.
$this->drupalGet('admin/config/system/actions');
$this->submitForm(['action' => 'node_assign_owner_action'], 'Create');
// Get the autocomplete URL of the owner_uid textfield.
$autocomplete_field = $this->getSession()->getPage()->findField('owner_uid');
$autocomplete_url = $this->getAbsoluteUrl($autocomplete_field->getAttribute('data-autocomplete-path'));
// Make sure that autocomplete works.
$user = $this->drupalCreateUser();
$data = Json::decode($this->drupalGet($autocomplete_url, ['query' => ['q' => $user->getDisplayName(), '_format' => 'json']]));
$this->assertNotEmpty($data);
$anonymous = User::getAnonymousUser();
// Ensure that the anonymous user exists.
$this->assertNotNull($anonymous);
// Make sure the autocomplete does not show the anonymous user.
$data = Json::decode($this->drupalGet($autocomplete_url, ['query' => ['q' => $anonymous->getDisplayName(), '_format' => 'json']]));
$this->assertEmpty($data);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\system\Entity\Action;
/**
* Tests action plugins using JavaScript.
*
* @group action
* @group legacy
*/
class ActionFormAjaxTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['action', 'action_form_ajax_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$user = $this->drupalCreateUser(['administer actions']);
$this->drupalLogin($user);
}
/**
* Tests action plugins with AJAX save their configuration.
*/
public function testActionConfigurationWithAjax(): void {
$url = Url::fromRoute('action.admin_add', ['action_id' => 'action_form_ajax_test']);
$this->drupalGet($url);
$page = $this->getSession()->getPage();
$id = 'test_plugin';
$this->assertSession()->waitForElementVisible('named', ['button', 'Edit'])->press();
$this->assertSession()->waitForElementVisible('css', '[name="id"]')->setValue($id);
$page->find('css', '[name="having_a_party"]')
->check();
$this->assertSession()->waitForElementVisible('css', '[name="party_time"]');
$party_time = 'Evening';
$page->find('css', '[name="party_time"]')
->setValue($party_time);
$page->find('css', '[value="Save"]')
->click();
$url = Url::fromRoute('entity.action.collection');
$this->assertSession()->pageTextContains('The action has been successfully saved.');
$this->assertSession()->addressEquals($url);
// Check storage.
$instance = Action::load($id);
$configuration = $instance->getPlugin()->getConfiguration();
$this->assertEquals(['party_time' => $party_time], $configuration);
// Configuration should be shown in edit form.
$this->drupalGet($instance->toUrl('edit-form'));
$this->assertSession()->checkboxChecked('having_a_party');
$this->assertSession()->fieldValueEquals('party_time', $party_time);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Kernel\Migrate\d6;
use Drupal\system\Entity\Action;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Tests migration of action items.
*
* @group migrate_drupal_6
*/
class MigrateActionsTest extends MigrateDrupal6TestBase {
protected static $modules = ['action', 'comment', 'node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('d6_action');
}
/**
* Tests Drupal 6 action migration to Drupal 8.
*/
public function testActions(): void {
// Test advanced actions.
$this->assertEntity('unpublish_comment_containing_keyword_s_', 'Unpublish comment containing keyword(s)', 'comment', ["keywords" => [0 => "drupal"]]);
$this->assertEntity('unpublish_post_containing_keyword_s_', 'Unpublish post containing keyword(s)', 'node', ["keywords" => [0 => "drupal"]]);
}
/**
* Asserts various aspects of an Action entity.
*
* @param string $id
* The expected Action ID.
* @param string $label
* The expected Action label.
* @param string $type
* The expected Action type.
* @param array $configuration
* The expected Action configuration.
*
* @internal
*/
protected function assertEntity(string $id, string $label, string $type, array $configuration): void {
$action = Action::load($id);
$this->assertInstanceOf(Action::class, $action);
/** @var \Drupal\system\Entity\Action $action */
$this->assertSame($id, $action->id());
$this->assertSame($label, $action->label());
$this->assertSame($type, $action->getType());
$this->assertSame($configuration, $action->get('configuration'));
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Kernel\Migrate\d7;
use Drupal\system\Entity\Action;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests migration of action items.
*
* @group action
*/
class MigrateActionsTest extends MigrateDrupal7TestBase {
protected static $modules = ['action', 'comment', 'node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('d7_action');
}
/**
* Tests Drupal 7 action migration to Drupal 8.
*/
public function testActions(): void {
// Test advanced actions.
$this->assertEntity('unpublish_comment_containing_keyword_s_', 'Unpublish comment containing keyword(s)', 'comment', ["keywords" => [0 => "drupal"]]);
$this->assertEntity('unpublish_content_containing_keyword_s_', 'Unpublish content containing keyword(s)', 'node', ["keywords" => [0 => "drupal"]]);
}
/**
* Asserts various aspects of an Action entity.
*
* @param string $id
* The expected Action ID.
* @param string $label
* The expected Action label.
* @param string $type
* The expected Action type.
* @param array $configuration
* The expected Action configuration.
*
* @internal
*/
protected function assertEntity(string $id, string $label, string $type, array $configuration): void {
$action = Action::load($id);
$this->assertInstanceOf(Action::class, $action);
/** @var \Drupal\system\Entity\Action $action */
$this->assertSame($id, $action->id());
$this->assertSame($label, $action->label());
$this->assertSame($type, $action->getType());
$this->assertSame($configuration, $action->get('configuration'));
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Kernel;
use Drupal\Core\Render\RenderContext;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\system\Entity\Action;
/**
* @group action
*/
class UnpublishByKeywordActionTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['action', 'node', 'system', 'user', 'field'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installSchema('node', ['node_access']);
// Install system's configuration as default date formats are needed.
$this->installConfig(['system']);
// Create a node type for testing.
$type = NodeType::create(['type' => 'page', 'name' => 'page', 'display_submitted' => FALSE]);
$type->save();
}
/**
* Tests creating an action using the node_unpublish_by_keyword_action plugin.
*/
public function testUnpublishByKeywordAction(): void {
/** @var \Drupal\node\Plugin\Action\UnpublishByKeywordNode $action */
$action = Action::create([
'id' => 'foo',
'label' => 'Foo',
'plugin' => 'node_unpublish_by_keyword_action',
'configuration' => [
'keywords' => ['test'],
],
]);
$action->save();
$node1 = Node::create([
'type' => 'page',
'title' => 'test',
'uid' => 1,
]);
$node1->setPublished();
$node1->save();
$node2 = Node::create([
'type' => 'page',
'title' => 'Another node',
'uid' => 1,
]);
$node2->setPublished();
$node2->save();
$this->container->get('renderer')->executeInRenderContext(new RenderContext(), function () use (&$node1, &$node2, $action) {
$action->execute([$node1, $node2]);
});
$this->assertFalse($node1->isPublished());
$this->assertTrue($node2->isPublished());
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Kernel;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Entity\CommentType;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\filter\Entity\FilterFormat;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\system\Entity\Action;
/**
* {@inheritdoc}
*
* @group action
*/
class UnpublishByKeywordCommentTest extends EntityKernelTestBase {
use CommentTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['action', 'comment', 'entity_test'];
/**
* Keywords used for testing.
*
* @var string[]
*/
protected $keywords;
/**
* The comment entity.
*
* @var \Drupal\comment\CommentInterface
*/
protected $comment;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['user', 'comment']);
$this->installSchema('comment', ['comment_entity_statistics']);
// Create a comment type.
CommentType::create([
'id' => 'comment',
'label' => 'Default comments',
'description' => 'Default comment field',
'target_entity_type_id' => 'entity_test',
])->save();
$this->addDefaultCommentField('entity_test', 'entity_test', 'comment');
// Setup date format to render comment date.
DateFormat::create([
'id' => 'fallback',
'pattern' => 'D, m/d/Y - H:i',
'label' => 'Fallback',
])->save();
// Create format without filters to prevent filtering.
FilterFormat::create([
'format' => 'no_filters',
'name' => 'No filters',
'filters' => [],
])->save();
// Set current user to allow filters display comment body.
$this->drupalSetCurrentUser($this->drupalCreateUser());
$this->keywords = [$this->randomMachineName(), $this->randomMachineName()];
// Create a comment against a test entity.
$host = EntityTest::create();
$host->save();
$this->comment = Comment::create([
'entity_type' => 'entity_test',
'name' => $this->randomString(),
'hostname' => 'magic.example.com',
'mail' => 'tonythemagicalpony@example.com',
'subject' => $this->keywords[0],
'comment_body' => $this->keywords[1],
'entity_id' => $host->id(),
'comment_type' => 'comment',
'field_name' => 'comment',
]);
$this->comment->get('comment_body')->format = 'no_filters';
$this->comment->setPublished();
}
/**
* Tests comment module's default config actions.
*
* @see \Drupal\Core\Entity\Form\DeleteMultipleForm::submitForm()
* @see \Drupal\Core\Action\Plugin\Action\DeleteAction
* @see \Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver
* @see \Drupal\Core\Action\Plugin\Action\PublishAction
* @see \Drupal\Core\Action\Plugin\Action\SaveAction
*/
public function testCommentDefaultConfigActions(): void {
$this->assertTrue($this->comment->isNew());
$action = Action::load('comment_save_action');
$action->execute([$this->comment]);
$this->assertFalse($this->comment->isNew());
$this->assertTrue($this->comment->isPublished());
// Tests comment unpublish.
$action = Action::load('comment_unpublish_action');
$action->execute([$this->comment]);
$this->assertFalse($this->comment->isPublished(), 'Comment was unpublished');
$this->assertSame(['module' => ['comment']], $action->getDependencies());
// Tests comment publish.
$action = Action::load('comment_publish_action');
$action->execute([$this->comment]);
$this->assertTrue($this->comment->isPublished(), 'Comment was published');
$action = Action::load('comment_delete_action');
$action->execute([$this->comment]);
/** @var \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store */
$temp_store = $this->container->get('tempstore.private');
$account_id = $this->container->get('current_user')->id();
$store_entries = $temp_store->get('entity_delete_multiple_confirm')->get($account_id . ':comment');
$this->assertSame([$account_id => ['en' => 'en']], $store_entries);
}
/**
* Tests the unpublish comment by keyword action.
*
* @see \Drupal\comment\Plugin\Action\UnpublishByKeywordComment
*/
public function testCommentUnpublishByKeyword(): void {
$this->comment->save();
$action = Action::create([
'id' => 'comment_unpublish_by_keyword_action',
'label' => $this->randomMachineName(),
'type' => 'comment',
'plugin' => 'comment_unpublish_by_keyword_action',
]);
// Tests no keywords.
$action->execute([$this->comment]);
$this->assertTrue($this->comment->isPublished(), 'The comment status was set to published.');
// Tests keyword in subject.
$action->set('configuration', ['keywords' => [$this->keywords[0]]]);
$action->execute([$this->comment]);
$this->assertFalse($this->comment->isPublished(), 'The comment status was set to not published.');
// Tests keyword in comment body.
$this->comment->setPublished();
$action->set('configuration', ['keywords' => [$this->keywords[1]]]);
$action->execute([$this->comment]);
$this->assertFalse($this->comment->isPublished(), 'The comment status was set to not published.');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\action\Unit\Menu;
use Drupal\Tests\Core\Menu\LocalTaskIntegrationTestBase;
/**
* Tests action local tasks.
*
* @group action
* @group legacy
*/
class ActionLocalTasksTest extends LocalTaskIntegrationTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->directoryList = ['action' => 'core/modules/action'];
parent::setUp();
}
/**
* Tests local task existence.
*/
public function testActionLocalTasks(): void {
$this->assertLocalTasks('entity.action.collection', [['action.admin']]);
}
}