diff --git a/core/core.services.yml b/core/core.services.yml index 856c198..d74a524 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.action: + class: Drupal\Core\Action\ActionManager + arguments: ['@container.namespaces'] request: class: Symfony\Component\HttpFoundation\Request event_dispatcher: diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 74d0cb2..7208f28 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -2914,16 +2914,12 @@ function drupal_classloader_register($name, $path) { * * Example: * @code - * function actions_do(...) { - * // $stack tracks the number of recursive calls. - * static $stack; - * $stack++; - * if ($stack > variable_get('action_max_stack', 35)) { - * ... - * return; + * function system_get_module_info($property) { + * static $info; + * if (!isset($info)) { + * $info = new ModuleInfo('system_info', 'cache'); * } - * ... - * $stack--; + * return $info[$property]; * } * @endcode * diff --git a/core/lib/Drupal/Core/Action/ActionBag.php b/core/lib/Drupal/Core/Action/ActionBag.php new file mode 100644 index 0000000..a8210fe --- /dev/null +++ b/core/lib/Drupal/Core/Action/ActionBag.php @@ -0,0 +1,52 @@ +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/Action/ActionBase.php b/core/lib/Drupal/Core/Action/ActionBase.php new file mode 100644 index 0000000..8b6c571 --- /dev/null +++ b/core/lib/Drupal/Core/Action/ActionBase.php @@ -0,0 +1,27 @@ +execute($entity); + } + } + +} diff --git a/core/lib/Drupal/Core/Action/ActionInterface.php b/core/lib/Drupal/Core/Action/ActionInterface.php new file mode 100644 index 0000000..0620416 --- /dev/null +++ b/core/lib/Drupal/Core/Action/ActionInterface.php @@ -0,0 +1,28 @@ +discovery = new AnnotatedClassDiscovery('Action', $namespaces, array(), 'Drupal\Core\Annotation\Action'); + $this->discovery = new AlterDecorator($this->discovery, 'action_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/lib/Drupal/Core/Action/ConfigurableActionBase.php b/core/lib/Drupal/Core/Action/ConfigurableActionBase.php new file mode 100644 index 0000000..04ca4fe --- /dev/null +++ b/core/lib/Drupal/Core/Action/ConfigurableActionBase.php @@ -0,0 +1,49 @@ +configuration += $this->getDefaultConfiguration(); + } + + /** + * Returns default configuration for this action. + * + * @return array + */ + protected function getDefaultConfiguration() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function validate(array &$form, array &$form_state) { + } + +} diff --git a/core/lib/Drupal/Core/Action/ConfigurableActionInterface.php b/core/lib/Drupal/Core/Action/ConfigurableActionInterface.php new file mode 100644 index 0000000..c0a66a9 --- /dev/null +++ b/core/lib/Drupal/Core/Action/ConfigurableActionInterface.php @@ -0,0 +1,61 @@ +getControllerClass($entity_type, 'form', $operation); if (in_array('Drupal\Core\Entity\EntityControllerInterface', class_implements($class))) { $this->controllers['form'][$operation][$entity_type] = $class::createInstance($this->container, $entity_type, $this->getDefinition($entity_type)); + $this->controllers['form'][$operation][$entity_type]->setOperation($operation); } else { $this->controllers['form'][$operation][$entity_type] = new $class($operation); 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 @@ - $callback))); - } -} diff --git a/core/modules/action/action.api.php b/core/modules/action/action.api.php index 8e92c62..bdd7c0f 100644 --- a/core/modules/action/action.api.php +++ b/core/modules/action/action.api.php @@ -6,90 +6,10 @@ */ /** - * 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 * The action ID. - * - * @ingroup actions */ function hook_action_delete($aid) { db_delete('actions_assignments') 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 @@ - '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..e9f6dc5 100644 --- a/core/modules/action/action.module +++ b/core/modules/action/action.module @@ -5,26 +5,6 @@ * 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: - * - $entity: The object that the action acts on, such as a node, comment, or - * user. - * - $context: Array of additional information about what triggered the action. - * - $a1, $a2: Optional additional information, which can be passed into - * actions_do() and will be passed along to the action function. - * - * @} End of "defgroup actions". - */ - /** * Implements hook_help(). */ @@ -69,12 +49,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,615 +68,10 @@ function action_menu() { } /** - * Implements hook_rebuild(). - */ -function action_rebuild() { - // Synchronize any actions that were added or removed. - action_synchronize(); -} - -/** - * Performs a given list of actions by executing their callback functions. - * - * 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 - * 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. - * @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) { - // $stack tracks the number of recursive calls. - static $stack; - $stack++; - $recursion_limit = config('action.settings')->get('recursion_limit'); - if ($stack > $recursion_limit) { - 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; - } - } - } - $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. + * Implements hook_entity_info(). */ -function action_goto_action($entity, $context) { - drupal_goto(Drupal::token()->replace($context['url'], $context)); +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'; } diff --git a/core/modules/action/action.routing.yml b/core/modules/action/action.routing.yml index a7ed7b9..b8a0980 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/{action_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..85db8fe --- /dev/null +++ b/core/modules/action/lib/Drupal/action/ActionAddFormController.php @@ -0,0 +1,75 @@ +actionManager = $action_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { + return new static( + $container->get('plugin.manager.entity')->getStorageController($entity_type), + $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\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/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 @@ +storageController = $storage_controller; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { + return new static( + $container->get('plugin.manager.entity')->getStorageController($entity_type) + ); + } + + /** + * {@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.'), + ); + + $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 ConfigurableActionInterface) { + $form += $this->plugin->form($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) { + $actions = $this->storageController->load(array($id)); + return isset($actions[$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 ConfigurableActionInterface) { + $this->plugin->validate($form, $form_state); + } + } + + /** + * {@inheritdoc} + */ + public function submit(array $form, array &$form_state) { + parent::submit($form, $form_state); + + if ($this->plugin instanceof ConfigurableActionInterface) { + $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..2af2f12 --- /dev/null +++ b/core/modules/action/lib/Drupal/action/ActionListController.php @@ -0,0 +1,137 @@ +actionManager = $action_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { + return new static( + $entity_type, + $container->get('plugin.manager.entity')->getStorageController($entity_type), + $container->get('plugin.manager.action') + ); + } + + /** + * {@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'] = String::checkPlain($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'] = '

' . 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(new ActionAdminManageForm($this->actionManager)); + 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 @@ -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' => '

' . t('Available actions:') . '

' - ); - $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 @@ - $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..d0244b1 100644 --- a/core/modules/action/lib/Drupal/action/Form/ActionAdminManageForm.php +++ b/core/modules/action/lib/Drupal/action/Form/ActionAdminManageForm.php @@ -7,27 +7,61 @@ namespace Drupal\action\Form; +use Drupal\Core\Controller\ControllerInterface; use Drupal\Core\Form\FormInterface; +use Drupal\Component\Utility\Crypt; +use Drupal\Core\Action\ActionManager; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a configuration form for configurable actions. */ -class ActionAdminManageForm implements FormInterface { +class ActionAdminManageForm implements FormInterface, ControllerInterface { /** - * Implements \Drupal\Core\Form\FormInterface::getFormID(). + * 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'; } /** - * 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\Action\ConfigurableActionInterface')) { + $key = Crypt::hashBase64($id); + $actions[$key] = $definition['label'] . '...'; + } + } $form['parent'] = array( '#type' => 'details', '#title' => t('Create an advanced action'), @@ -37,7 +71,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( @@ -51,17 +85,17 @@ public function buildForm(array $form, array &$form_state, array $options = arra } /** - * Implements \Drupal\Core\Form\FormInterface::validateForm(). + * {@inheritdoc} */ public function validateForm(array &$form, array &$form_state) { } /** - * Implements \Drupal\Core\Form\FormInterface::submitForm(). + * {@inheritdoc} */ 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..a3d2b3c 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\ActionConfigEntityInterface; /** * 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\ActionConfigEntityInterface */ 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, ActionConfigEntityInterface $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/Action/EmailAction.php b/core/modules/action/lib/Drupal/action/Plugin/Action/EmailAction.php new file mode 100644 index 0000000..91432ab --- /dev/null +++ b/core/modules/action/lib/Drupal/action/Plugin/Action/EmailAction.php @@ -0,0 +1,163 @@ +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('plugin.manager.entity') + ); + } + + /** + * {@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(array('mail' => $recipient)); + $recipient_account = reset($recipient_accounts); + 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/Action/GotoAction.php b/core/modules/action/lib/Drupal/action/Plugin/Action/GotoAction.php new file mode 100644 index 0000000..aff20b0 --- /dev/null +++ b/core/modules/action/lib/Drupal/action/Plugin/Action/GotoAction.php @@ -0,0 +1,62 @@ +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 submit(array &$form, array &$form_state) { + $this->configuration['url'] = $form_state['values']['url']; + } + +} diff --git a/core/modules/action/lib/Drupal/action/Plugin/Action/MessageAction.php b/core/modules/action/lib/Drupal/action/Plugin/Action/MessageAction.php new file mode 100644 index 0000000..3bc59b9 --- /dev/null +++ b/core/modules/action/lib/Drupal/action/Plugin/Action/MessageAction.php @@ -0,0 +1,92 @@ +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} + */ + 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 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..3d62bbe 100644 --- a/core/modules/action/lib/Drupal/action/Tests/ConfigurationTest.php +++ b/core/modules/action/lib/Drupal/action/Tests/ConfigurationTest.php @@ -42,43 +42,56 @@ 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."); $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/(\d+)|', $this->getUrl(), $matches); + $this->clickLink(t('Configure')); + 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->clickLink(t('delete')); + $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 deleted file mode 100644 index 87fa08e..0000000 --- a/core/modules/action/lib/Drupal/action/Tests/LoopTest.php +++ /dev/null @@ -1,82 +0,0 @@ - 'Actions executing in a potentially infinite loop', - 'description' => 'Tests actions executing in a loop, and makes sure they abort properly.', - 'group' => 'Action', - ); - } - - /** - * Sets up a loop with 3 - 12 recursions, and sees if it aborts properly. - */ - 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']); - - // Delete any existing watchdog messages to clear the plethora of - // "Action added" messages from when Drupal was installed. - db_delete('watchdog')->execute(); - // To prevent this test from failing when xdebug is enabled, the maximum - // recursion level should be kept low enough to prevent the xdebug - // infinite recursion protection mechanism from aborting the request. - // See http://drupal.org/node/587634. - config('action.settings') - ->set('recursion_limit', 7) - ->save(); - $this->triggerActions(); - } - - /** - * Loops watchdog messages up to actions_max_stack times. - * - * Creates an infinite loop by causing a watchdog message to be set, - * which causes the actions to be triggered again, up to action_max_stack - * times. - */ - protected function triggerActions() { - $this->drupalGet('', array('query' => array('trigger_action_on_watchdog' => $this->aid))); - $expected = array(); - $expected[] = 'Triggering action loop'; - $recursion_limit = config('action.settings')->get('recursion_limit'); - for ($i = 1; $i <= $recursion_limit; $i++) { - $expected[] = "Test log #$i"; - } - $expected[] = 'Stack overflow: recursion limit for actions_do() has been reached. Stack is limited by %limit calls.'; - - $result = db_query("SELECT message FROM {watchdog} WHERE type = 'action_loop_test' OR type = 'action' ORDER BY wid"); - $loop_started = FALSE; - foreach ($result as $row) { - $expected_message = array_shift($expected); - $this->assertEqual($row->message, $expected_message, format_string('Expected message %expected, got %message.', array('%expected' => $expected_message, '%message' => $row->message))); - } - $this->assertTrue(empty($expected), 'All expected messages found.'); - } -} diff --git a/core/modules/action/tests/action_loop_test/action_loop_test.info.yml b/core/modules/action/tests/action_loop_test/action_loop_test.info.yml deleted file mode 100644 index fefa4ee..0000000 --- a/core/modules/action/tests/action_loop_test/action_loop_test.info.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: 'Action loop test' -type: module -description: 'Support module for action loop testing.' -package: Testing -version: VERSION -core: 8.x -hidden: true -dependencies: - - action diff --git a/core/modules/action/tests/action_loop_test/action_loop_test.install b/core/modules/action/tests/action_loop_test/action_loop_test.install deleted file mode 100644 index 7085aed..0000000 --- a/core/modules/action/tests/action_loop_test/action_loop_test.install +++ /dev/null @@ -1,8 +0,0 @@ - 'watchdog', - ); - // Fire the actions on the associated object ($log_entry) and the context - // variable. - $aids = (array) $_GET['trigger_action_on_watchdog']; - actions_do($aids, $log_entry, $context); -} - -/** - * Implements hook_custom_theme(). - * - * We need to check wheter a loop should be triggered and we do this as early - * possible, in the first hook that fires. - */ -function action_loop_test_custom_theme() { - if (!empty($_GET['trigger_action_on_watchdog'])) { - watchdog_skip_semaphore('action_loop_test', 'Triggering action loop'); - } -} - -/** - * 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. - */ -function watchdog_skip_semaphore($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE, $link = NULL) { - global $user, $base_root; - - // Prepare the fields to be logged - $log_entry = array( - 'type' => $type, - 'message' => $message, - 'variables' => $variables, - 'severity' => $severity, - 'link' => $link, - 'user' => $user, - 'uid' => isset($user->uid) ? $user->uid : 0, - 'request_uri' => $base_root . request_uri(), - 'referer' => $_SERVER['HTTP_REFERER'], - 'ip' => Drupal::request()->getClientIP(), - 'timestamp' => REQUEST_TIME, - ); - - // Call the logging hooks to log/process the message - foreach (module_implements('watchdog') as $module) { - module_invoke($module, 'watchdog', $log_entry); - } -} diff --git a/core/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc index 51158a9..a8389de 100644 --- a/core/modules/comment/comment.admin.inc +++ b/core/modules/comment/comment.admin.inc @@ -59,7 +59,7 @@ function comment_admin_overview($form, &$form_state, $arg) { $form['options']['operation'] = array( '#type' => 'select', - '#title' => t('Operation'), + '#title' => t('Action'), '#title_display' => 'invisible', '#options' => $options, '#default_value' => 'publish', diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 0415f3a..6580c85 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -1806,161 +1806,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/Action/PublishComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Action/PublishComment.php new file mode 100644 index 0000000..93b9a99 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/Action/PublishComment.php @@ -0,0 +1,33 @@ +status->value = COMMENT_PUBLISHED; + $comment->save(); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Action/SaveComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Action/SaveComment.php new file mode 100644 index 0000000..6127c8d --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/Action/SaveComment.php @@ -0,0 +1,34 @@ +save(); + Cache::invalidateTags(array('content' => TRUE)); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Action/UnpublishByKeywordComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Action/UnpublishByKeywordComment.php new file mode 100644 index 0000000..d123cfc --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/Action/UnpublishByKeywordComment.php @@ -0,0 +1,69 @@ +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 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/Action/UnpublishComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Action/UnpublishComment.php new file mode 100644 index 0000000..4857d3e --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/Action/UnpublishComment.php @@ -0,0 +1,33 @@ +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..94a1651 100644 --- a/core/modules/comment/lib/Drupal/comment/Tests/CommentActionsTest.php +++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentActionsTest.php @@ -36,28 +36,15 @@ 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. + $action = entity_load('action', 'comment_unpublish_action'); + $action->execute(array($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. + $action = entity_load('action', 'comment_publish_action'); + $action->execute(array($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 +54,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 +72,8 @@ function testCommentUnpublishByKeyword() { $this->assertTrue($comment->status->value == COMMENT_PUBLISHED, 'The comment status was set to published.'); - actions_do($aid, $comment, array()); + $action->execute(array($comment)); $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/comment/lib/Drupal/comment/Tests/CommentTestBase.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentTestBase.php index 22132c1..f15666c 100644 --- a/core/modules/comment/lib/Drupal/comment/Tests/CommentTestBase.php +++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentTestBase.php @@ -283,27 +283,27 @@ function commentContactInfoAvailable() { } /** - * Performs the specified operation on the specified comment. + * Performs the specified action on the specified comment. * * @param object $comment - * Comment to perform operation on. - * @param string $operation - * Operation to perform. + * Comment to perform action on. + * @param string $action + * Action to perform. * @param boolean $aproval - * Operation is found on approval page. + * Action is found on approval page. */ - function performCommentOperation($comment, $operation, $approval = FALSE) { + function performCommentOperation($comment, $action, $approval = FALSE) { $edit = array(); - $edit['operation'] = $operation; + $edit['operation'] = $action; $edit['comments[' . $comment->id() . ']'] = TRUE; $this->drupalPost('admin/content/comment' . ($approval ? '/approval' : ''), $edit, t('Update')); - if ($operation == 'delete') { + if ($action == 'delete') { $this->drupalPost(NULL, array(), t('Delete comments')); - $this->assertRaw(format_plural(1, 'Deleted 1 comment.', 'Deleted @count comments.'), format_string('Operation "@operation" was performed on comment.', array('@operation' => $operation))); + $this->assertRaw(format_plural(1, 'Deleted 1 comment.', 'Deleted @count comments.'), format_string('Action "@action" was performed on comment.', array('@action' => $action))); } else { - $this->assertText(t('The update has been performed.'), format_string('Operation "@operation" was performed on comment.', array('@operation' => $operation))); + $this->assertText(t('The update has been performed.'), format_string('Action "@action" was performed on comment.', array('@action' => $action))); } } diff --git a/core/modules/menu_link/menu_link.module b/core/modules/menu_link/menu_link.module index 45e1923..d9b136f 100644 --- a/core/modules/menu_link/menu_link.module +++ b/core/modules/menu_link/menu_link.module @@ -130,7 +130,7 @@ function menu_link_save(MenuLink $menu_link) { * @param string $module * The name of the module that owns the link. * @param string $op - * Operation to perform: insert, update, enable, disable, or delete. + * Action to perform: insert, update, enable, disable, or delete. * @param string $link_path * The path this link points to. * @param string $link_title 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/lib/Drupal/node/Form/DeleteMultiple.php b/core/modules/node/lib/Drupal/node/Form/DeleteMultiple.php new file mode 100644 index 0000000..c11dbd8 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Form/DeleteMultiple.php @@ -0,0 +1,126 @@ +tempStoreFactory = $temp_store_factory; + $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->tempStoreFactory->get('node_multiple_delete_confirm')->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->tempStoreFactory->get('node_multiple_delete_confirm')->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/Plugin/Action/AssignOwnerNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/AssignOwnerNode.php new file mode 100644 index 0000000..94d85de --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/AssignOwnerNode.php @@ -0,0 +1,135 @@ +connection = $connection; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { + return new static($configuration, $plugin_id, $plugin_definition, + $container->get('database') + ); + } + + /** + * {@inheritdoc} + */ + public function execute($entity = NULL) { + $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 = $this->connection->query("SELECT COUNT(*) FROM {users}")->fetchField(); + $owner_name = ''; + if (is_numeric($this->configuration['owner_uid'])) { + $owner_name = $this->connection->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 = $this->connection->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) $this->connection->queryRange('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'] = $this->connection->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/Action/DeleteNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/DeleteNode.php new file mode 100644 index 0000000..f4a5eff --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/DeleteNode.php @@ -0,0 +1,74 @@ +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 executeMultiple(array $entities) { + $this->tempStore->set($GLOBALS['user']->uid, $entities); + } + + /** + * {@inheritdoc} + */ + public function execute($object = NULL) { + $this->executeMultiple(array($object)); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Action/DemoteNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/DemoteNode.php new file mode 100644 index 0000000..7676a76 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/DemoteNode.php @@ -0,0 +1,33 @@ +promote = NODE_NOT_PROMOTED; + $entity->save(); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Action/PromoteNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/PromoteNode.php new file mode 100644 index 0000000..56f8658 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/PromoteNode.php @@ -0,0 +1,34 @@ +status = NODE_PUBLISHED; + $entity->promote = NODE_PROMOTED; + $entity->save(); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Action/PublishNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/PublishNode.php new file mode 100644 index 0000000..132e045 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/PublishNode.php @@ -0,0 +1,33 @@ +status = NODE_PUBLISHED; + $entity->save(); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Action/SaveNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/SaveNode.php new file mode 100644 index 0000000..d374afc --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/SaveNode.php @@ -0,0 +1,32 @@ +save(); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Action/StickyNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/StickyNode.php new file mode 100644 index 0000000..a170067 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/StickyNode.php @@ -0,0 +1,34 @@ +status = NODE_PUBLISHED; + $entity->sticky = NODE_STICKY; + $entity->save(); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Action/UnpublishByKeywordNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/UnpublishByKeywordNode.php new file mode 100644 index 0000000..2d0f4ed --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/UnpublishByKeywordNode.php @@ -0,0 +1,68 @@ +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 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/Action/UnpublishNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/UnpublishNode.php new file mode 100644 index 0000000..968404f --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/UnpublishNode.php @@ -0,0 +1,33 @@ +status = NODE_NOT_PUBLISHED; + $entity->save(); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Action/UnstickyNode.php b/core/modules/node/lib/Drupal/node/Plugin/Action/UnstickyNode.php new file mode 100644 index 0000000..ea93058 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Action/UnstickyNode.php @@ -0,0 +1,33 @@ +sticky = NODE_NOT_STICKY; + $entity->save(); + } + +} diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc index 39b54f1..219e92d 100644 --- a/core/modules/node/node.admin.inc +++ b/core/modules/node/node.admin.inc @@ -32,49 +32,6 @@ 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 @@ -446,12 +403,13 @@ function node_admin_nodes() { '#access' => $admin_access, ); $options = array(); - foreach (module_invoke_all('node_operations') as $operation => $array) { - $options[$operation] = $array['label']; + $actions = entity_load_multiple_by_properties('action', array('type' => 'node')); + foreach ($actions as $id => $action) { + $options[$id] = $action->label(); } $form['options']['operation'] = array( '#type' => 'select', - '#title' => t('Operation'), + '#title' => t('Action'), '#title_display' => 'invisible', '#options' => $options, '#default_value' => 'approve', @@ -630,20 +588,13 @@ function node_admin_nodes() { * @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']); + if ($action = entity_load('action', $form_state['values']['operation'])) { + $nodes = entity_load_multiple('node', array_filter($form_state['values']['nodes'])); + $action->execute($nodes); + $operation_definition = $action->getPluginDefinition(); + if (!empty($operation_definition['confirm_form_path'])) { + $form_state['redirect'] = $operation_definition['confirm_form_path']; } - else { - $args = array($nodes); - } - call_user_func_array($function, $args); - cache_invalidate_tags(array('content' => TRUE)); } else { diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index b458392..80efa99 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 entity_delete_multiple() after the type-specific diff --git a/core/modules/node/node.module b/core/modules/node/node.module index d4cd961..78346cd 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -3112,332 +3112,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..02e887b --- /dev/null +++ b/core/modules/node/node.routing.yml @@ -0,0 +1,6 @@ +node_multiple_delete_confirm: + pattern: '/admin/content/node/delete' + defaults: + _form: '\Drupal\node\Form\DeleteMultiple' + requirements: + _permission: 'administer nodes' diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Form/SimpletestTestForm.php b/core/modules/simpletest/lib/Drupal/simpletest/Form/SimpletestTestForm.php index 229211c..17ce75f 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/Form/SimpletestTestForm.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/Form/SimpletestTestForm.php @@ -54,7 +54,7 @@ public function buildForm(array $form, array &$form_state) { } } - // Operation buttons. + // Action buttons. $form['tests']['op'] = array( '#type' => 'submit', '#value' => t('Run tests'), diff --git a/core/modules/system/lib/Drupal/system/ActionConfigEntityInterface.php b/core/modules/system/lib/Drupal/system/ActionConfigEntityInterface.php new file mode 100644 index 0000000..32b41b0 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/ActionConfigEntityInterface.php @@ -0,0 +1,38 @@ +getPlugin(); + // If this plugin has any configuration, ensure that it is set. + if ($plugin instanceof ConfigurableActionInterface) { + $entity->set('configuration', $plugin->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..1264777 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/Core/Entity/Action.php @@ -0,0 +1,179 @@ +pluginBag = new ActionBag(\Drupal::service('plugin.manager.action'), 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()->executeMultiple($entities); + } + + /** + * {@inheritdoc} + */ + public function isConfigurable() { + return $this->getPlugin() instanceof ConfigurableActionInterface; + } + + /** + * {@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..348085c 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 @@ -7,9 +7,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 +18,38 @@ abstract class BulkFormBase extends FieldPluginBase { /** + * An array of actions that can be executed. + * + * @var array + */ + protected $actions = array(); + + /** + * Constructs a new BulkForm 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 array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityManager $manager + * The entity manager. + */ + 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) { @@ -104,7 +137,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['confirm_form_path'])) { + $form_state['confirm_form_path'] = $operation_definition['confirm_form_path']; + } + } + } /** * Overrides \Drupal\views\Plugin\views\Plugin\field\FieldPluginBase::query(). diff --git a/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php new file mode 100644 index 0000000..1c08aca --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php @@ -0,0 +1,86 @@ + 'Action Plugins', + 'description' => 'Tests Action plugins.', + 'group' => 'Action', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->actionManager = $this->container->get('plugin.manager.action'); + $this->installSchema('user', array('users', 'users_roles')); + $this->installSchema('system', array('sequences')); + } + + /** + * Tests the functionality of test actions. + */ + public function testOperations() { + // Test that actions can be discovered. + $definitions = $this->actionManager->getDefinitions(); + $this->assertTrue(count($definitions) > 1, 'Action definitions are found.'); + $this->assertTrue(!empty($definitions['action_test_no_type']), 'The test action is among the definitions found.'); + + $definition = $this->actionManager->getDefinition('action_test_no_type'); + $this->assertTrue(!empty($definition), 'The test action definition is found.'); + + $definitions = $this->actionManager->getDefinitionsByType('user'); + $this->assertTrue(empty($definitions['action_test_no_type']), 'An action with no type is not found.'); + + // Create an instance of the 'save entity' action. + $action = $this->actionManager->createInstance('action_test_save_entity'); + $this->assertTrue($action instanceof ActionInterface, 'The action 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' action. + $action->execute($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/lib/Drupal/system/Tests/Upgrade/ActionUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/ActionUpgradePathTest.php new file mode 100644 index 0000000..aa9f268 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/ActionUpgradePathTest.php @@ -0,0 +1,42 @@ + 'Action upgrade test', + 'description' => 'Upgrade tests with action data.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'system') . '/tests/upgrade/drupal-7.bare.minimal.database.php.gz', + ); + parent::setUp(); + } + + /** + * Tests to see if actions were upgrade. + */ + public function testActionUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + $this->drupalGet('admin/people'); + $elements = $this->xpath('//select[@name="operation"]/option'); + $this->assertTrue(!empty($elements), 'The user actions were upgraded.'); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index e7a2a34..1774adf 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1921,11 +1921,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')); @@ -2194,6 +2194,32 @@ function system_update_8056() { } /** + * Convert actions to configuration. + * + * @ingroup config_upgrade + */ +function system_update_8057() { + $actions = db_query("SELECT * FROM {actions}")->fetchAllAssoc('aid', PDO::FETCH_ASSOC); + $action_plugins = Drupal::service('plugin.manager.action')->getDefinitions(); + foreach ($actions as $action) { + if (isset($action_plugins[$action['callback']])) { + if (is_numeric($action['aid'])) { + $action['aid'] = $action['callback'] . '_' . $action['aid']; + } + $configuration = unserialize($action['parameters']) ?: array(); + config('action.action.' . $action['aid']) + ->set('id', $action['aid']) + ->set('label', $action['label']) + ->set('status', '1') + ->set('type', $action['type']) + ->set('plugin', $action['callback']) + ->set('configuration', $configuration) + ->save(); + } + } +} + +/** * @} End of "defgroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/system/tests/modules/action_test/action_test.info.yml b/core/modules/system/tests/modules/action_test/action_test.info.yml new file mode 100644 index 0000000..c639b27 --- /dev/null +++ b/core/modules/system/tests/modules/action_test/action_test.info.yml @@ -0,0 +1,7 @@ +name: 'Action test' +type: module +description: 'Support module for action testing.' +package: Testing +version: VERSION +core: 8.x +hidden: true diff --git a/core/modules/system/tests/modules/action_test/action_test.module b/core/modules/system/tests/modules/action_test/action_test.module new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/core/modules/system/tests/modules/action_test/action_test.module @@ -0,0 +1 @@ +save(); + } + +} diff --git a/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php b/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php index 26492f6..d2d992c 100644 --- a/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php +++ b/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php @@ -271,7 +271,7 @@ function testTrackerAdminUnpublish() { // Unpublish the node and ensure that it's no longer displayed. $edit = array( - 'operation' => 'unpublish', + 'operation' => 'node_unpublish_action', 'nodes[' . $node->nid . ']' => $node->nid, ); $this->drupalPost('admin/content', $edit, t('Update')); 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/lib/Drupal/user/Plugin/Action/AddRoleUser.php b/core/modules/user/lib/Drupal/user/Plugin/Action/AddRoleUser.php new file mode 100644 index 0000000..f1d1f37 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Action/AddRoleUser.php @@ -0,0 +1,41 @@ +configuration['rid']; + // Skip adding the role to the user if they already have it. + if ($account !== FALSE && !isset($account->roles[$rid])) { + $roles = $account->roles + array($rid => $rid); + // For efficiency manually save the original account before applying + // any changes. + $account->original = clone $account; + $account->roles = $roles; + $account->save(); + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Action/BlockUser.php b/core/modules/user/lib/Drupal/user/Plugin/Action/BlockUser.php new file mode 100644 index 0000000..17a80bf --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Action/BlockUser.php @@ -0,0 +1,39 @@ +status == 1) { + // For efficiency manually save the original account before applying any + // changes. + $account->original = clone $account; + $account->status = 0; + $account->save(); + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Action/CancelUser.php b/core/modules/user/lib/Drupal/user/Plugin/Action/CancelUser.php new file mode 100644 index 0000000..58e9e37 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Action/CancelUser.php @@ -0,0 +1,74 @@ +tempStoreFactory = $temp_store_factory; + } + + /** + * {@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 executeMultiple(array $entities) { + $this->tempStoreFactory->get('user_user_operations_cancel')->set($GLOBALS['user']->uid, $entities); + } + + /** + * {@inheritdoc} + */ + public function execute($object = NULL) { + $this->executeMultiple(array($object)); + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Action/ChangeUserRoleBase.php b/core/modules/user/lib/Drupal/user/Plugin/Action/ChangeUserRoleBase.php new file mode 100644 index 0000000..9dfb056 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Action/ChangeUserRoleBase.php @@ -0,0 +1,49 @@ + '', + ); + } + + /** + * {@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 submit(array &$form, array &$form_state) { + $this->configuration['rid'] = $form_state['values']['rid']; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Action/RemoveRoleUser.php b/core/modules/user/lib/Drupal/user/Plugin/Action/RemoveRoleUser.php new file mode 100644 index 0000000..a2b2616 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Action/RemoveRoleUser.php @@ -0,0 +1,41 @@ +configuration['rid']; + // 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 => $rid)); + // For efficiency manually save the original account before applying + // any changes. + $account->original = clone $account; + $account->roles = $roles; + $account->save(); + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Action/UnblockUser.php b/core/modules/user/lib/Drupal/user/Plugin/Action/UnblockUser.php new file mode 100644 index 0000000..0a4d912 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Action/UnblockUser.php @@ -0,0 +1,36 @@ +status == 0) { + $account->status = 1; + $account->save(); + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php b/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php index 3b83b52..9e1e201 100644 --- a/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php +++ b/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php @@ -73,7 +73,7 @@ function testUserAdmin() { $account = user_load($user_c->uid); $this->assertEqual($account->status, 1, 'User C not blocked'); $edit = array(); - $edit['operation'] = 'block'; + $edit['operation'] = 'user_block_user_action'; $edit['accounts[' . $account->uid . ']'] = TRUE; $this->drupalPost('admin/people', $edit, t('Update')); $account = user_load($user_c->uid, TRUE); @@ -81,7 +81,7 @@ function testUserAdmin() { // Test unblocking of a user from /admin/people page and sending of activation mail $editunblock = array(); - $editunblock['operation'] = 'unblock'; + $editunblock['operation'] = 'user_unblock_user_action'; $editunblock['accounts[' . $account->uid . ']'] = TRUE; $this->drupalPost('admin/people', $editunblock, t('Update')); $account = user_load($user_c->uid, TRUE); diff --git a/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php b/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php index 5b622e1..3861468 100644 --- a/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php +++ b/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php @@ -88,7 +88,7 @@ function testUserCancelUid1() { $this->admin_user = $this->drupalCreateUser(array('administer users')); $this->drupalLogin($this->admin_user); $edit = array( - 'operation' => 'cancel', + 'operation' => 'user_cancel_user_action', 'accounts[1]' => TRUE, ); $this->drupalPost('admin/people', $edit, t('Update')); @@ -408,7 +408,7 @@ function testMassUserCancelByAdmin() { // Cancel user accounts, including own one. $edit = array(); - $edit['operation'] = 'cancel'; + $edit['operation'] = 'user_cancel_user_action'; foreach ($users as $uid => $account) { $edit['accounts[' . $uid . ']'] = TRUE; } diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc index adb5259..bb3d5f4 100644 --- a/core/modules/user/user.admin.inc +++ b/core/modules/user/user.admin.inc @@ -190,12 +190,13 @@ function user_admin_account() { '#attributes' => array('class' => array('container-inline')), ); $options = array(); - foreach (module_invoke_all('user_operations') as $operation => $array) { - $options[$operation] = $array['label']; + $actions = entity_load_multiple_by_properties('action', array('type' => 'user')); + foreach ($actions as $id => $action) { + $options[$id] = $action->label(); } $form['options']['operation'] = array( '#type' => 'select', - '#title' => t('Operation'), + '#title' => t('Action'), '#title_display' => 'invisible', '#options' => $options, '#default_value' => 'unblock', @@ -263,20 +264,13 @@ function user_admin_account() { * Submit the user administration update 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']); + if ($action = entity_load('action', $form_state['values']['operation'])) { + $accounts = entity_load_multiple('user', array_filter($form_state['values']['accounts'])); + $action->execute($accounts); + $operation_definition = $action->getPluginDefinition(); + if (!empty($operation_definition['confirm_form_path'])) { + $form_state['redirect'] = $operation_definition['confirm_form_path']; } - else { - $args = array($accounts); - } - call_user_func_array($function, $args); - drupal_set_message(t('The update has been performed.')); } } diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php index 5309f81..cc69e05 100644 --- a/core/modules/user/user.api.php +++ b/core/modules/user/user.api.php @@ -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 abfe84b..c354a9e 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; @@ -973,6 +973,13 @@ function user_menu() { 'access arguments' => array('administer users'), 'type' => MENU_LOCAL_ACTION, ); + $items['admin/people/cancel'] = array( + 'title' => 'Cancel 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( @@ -1820,6 +1827,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 @@ -1992,150 +2048,14 @@ function user_role_revoke_permissions($rid, array $permissions = array()) { drupal_static_reset('user_role_permissions'); } -/** - * 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) { - $account->status = 1; - $account->save(); - } - } -} - -/** - * 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) { - // For efficiency manually save the original account before applying any - // changes. - $account->original = clone $account; - $account->status = 0; - $account->save(); - } - } -} - -/** - * 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); - // For efficiency manually save the original account before applying - // any changes. - $account->original = clone $account; - $account->roles = $roles; - $account->save(); - } - } - 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)); - // For efficiency manually save the original account before applying - // any changes. - $account->original = clone $account; - $account->roles = $roles; - $account->save(); - } - } - break; - } -} - function user_multiple_cancel_confirm($form, &$form_state) { $edit = $form_state['input']; + // Retrieve the accounts to be canceled from the temp store. + $accounts = Drupal::service('user.tempstore')->get('user_user_operations_cancel')->get($GLOBALS['user']->uid); $form['accounts'] = array('#prefix' => '', '#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; @@ -2199,6 +2119,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) { // Prevent programmatic form submissions from cancelling user 1. @@ -2496,44 +2418,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/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); } }