Commit 2348e7de authored by Dries's avatar Dries

- Patch #148410 by jvandyk: added rewrite of the actions module!

  This is a very important patch, but one that is merely an enabler.
  Hopefully we'll see more people submitting "action patches" in the
  near future.

  Thanks for the hard work and persistence, John.  *If* you decide
  to update the Drupal Pro Development book to Drupal 6, make sure
  to add a chapter on actions. ;)
parent 8dd8b0c2
......@@ -58,6 +58,7 @@ Drupal 6.0, xxxx-xx-xx (development version)
* Entries in the files table are now keyed to a user, and not a node.
* Added re-usable validation functions to check for uploaded file sizes, extensions, and image resolution.
- Added support for OpenID.
- Added support for configurable actions.
- Made user profiles easier to theme by using array rendering and supplying template files.
- Added Deletion API to allow modules to alter and respond to any delete operation.
......
<?php
// $Id$
/**
* @file
* This is the actions engine for executing stored actions.
*/
/**
* Given the IDs of actions to perform, find out what the callbacks
* for the actions are by querying the database. Then call each callback
* using the function call $function('do', $object, $context, $a1, $2)
* where $function is the name of a function written in compliance with
* the action specification; that is, foo($object, $context). The $params
* parameter is an array of stored parameters that have been previously
* configured through the web using actions.module.
*
* @param $action_ids
* The ID of the action to perform. Can be a single action ID or an array
* of IDs. IDs of instances will be numeric; IDs of singletons will be
* function names.
* @param $object
* Parameter that will be passed along to the callback. Typically the
* object that the action will act on; a node, user or comment object.
* @param $context
* Parameter that will be passed along to the callback. $context is a
* keyed array containing extra information about what is currently
* happening at the time of the call. Typically $context['hook'] and
* $context['op'] will tell which hook-op combination resulted in this
* call to actions_do().
* @param $a1
* Parameter that will be passed along to the callback.
* @param $a2
* Parameter that will be passed along to the callback.
*
* @return
* An associative array containing the result of the function that
* performs the action, keyed on action ID.
*/
function actions_do($action_ids, $object = NULL, $context = NULL, $a1 = NULL, $a2 = NULL) {
static $stack;
$stack++;
if ($stack > variable_get('actions_max_stack', 35)) {
watchdog('actions', 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.', WATCHDOG_ERROR);
return;
}
$actions = array();
$available_actions = actions_list();
$result = array();
if (is_array($action_ids)) {
$where = array();
$where_values = array();
foreach ($action_ids as $action_id) {
if (is_numeric($action_id)) {
$where[] = 'OR aid = %d';
$where_values[] = $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 ($where) {
$where_clause = implode(' ', $where);
// Strip off leading 'OR '.
$where_clause = '('. strstr($where_clause, " ") .')';
$result_db = db_query('SELECT * FROM {actions} WHERE '. $where_clause, $where_values);
while ($action = db_fetch_object($result_db)) {
$action_id = $action->action_id;
$actions[$action_id] = $action->params ? unserialize($data->parameters) : array();
$actions[$action_id]['callback'] = $action->callback;
$actions[$action_id]['type'] = $action->type;
}
}
// Fire actions, in no particular order.
foreach ($actions as $action_id => $params) {
if (is_numeric($action_id)) { // Configurable actions need parameters.
$function = $params['callback'];
$context = array_merge($context, $params);
$result[$action_id] = $function($object, $context, $a1, $a2);
}
// Singleton action; $action_id is the function name.
else {
$result[$action_id] = $action_id($object, $context, $a1, $a2);
}
}
}
// Optimized execution of single action.
else {
// If it's a configurable action, retrieve stored parameters.
if (is_numeric($action_ids)) {
$action = db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", $action_ids));
$function = $action->callback;
$context = array_merge($context, unserialize($action->parameters));
$result[$action_ids] = $function($object, $context, $a1, $a2);
}
// Singleton action; $action_ids is the function name.
else {
$result[$action_ids] = $action_ids($object, $context, $a1, $a2);
}
}
return $result;
}
/**
* Discover all action functions by invoking hook_action_info().
*
* mymodule_action_info() {
* return array(
* 'mymodule_functiondescription_action' => array(
* 'type' => 'node',
* 'description' => t('Save node'),
* 'configurable' => FALSE,
* 'hooks' => array(
* 'nodeapi' => array('delete','insert','update', 'view'),
* 'comment' => array('delete','insert','update', 'view'),
* )
* )
* );
* }
*
* The description is used in presenting possible actions to the user for
* configuration. The type is used to present these actions in a logical
* grouping and to denote context. Some types are 'node', 'user', 'comment',
* and 'system'. If an action is configurable it will provide form,
* validation and submission functions. The hooks the action supports
* are declared in the 'hooks' array.
*
* @return
* An associative array keyed on function name. The value of each key is
* an array containing information about the action, such as type of
* action and description of the action, e.g.,
*
* $actions['actions_node_publish'] = ('description' => 'Publish a node' ... )
*/
function actions_list() {
static $actions;
if (isset($actions)) {
return $actions;
}
$actions = module_invoke_all('action_info');
drupal_alter('action_info', $actions);
return $actions;
}
/**
* Retrieve all action instances from the database.
* Compare with actions_list() which gathers actions by
* invoking hook_action_info(). The two are synchronized
* by visiting /admin/build/actions (when actions.module is
* enabled) which runs actions_synchronize().
*
* @return
* Associative array keyed by action ID. Each value is
* an associative array with keys 'callback', 'description',
* 'type' and 'configurable'.
*/
function actions_get_all_actions() {
$actions = array();
$result = db_query("SELECT * FROM {actions}");
while ($action = db_fetch_object($result)) {
$actions[$action->aid] = array(
'callback' => $action->callback,
'description' => $action->description,
'type' => $action->type,
'configurable' => (bool) $action->parameters,
);
}
return $actions;
}
/**
* Create an associative array keyed by md5 hashes of function names.
* 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 as keys and associative
* arrays with keys 'description', 'type', etc. as values. Generally
* the output of actions_list() or actions_get_all_actions() is given
* as input to this function.
*
* @return
* An associative array keyed on md5 hash of function name. The value of
* each key is an associative array of function, description, and type
* for the action.
*/
function actions_actions_map($actions) {
$actions_map = array();
foreach ($actions as $callback => $array) {
$key = md5($callback);
$actions_map[$key]['callback'] = isset($array['callback']) ? $array['callback'] : $callback;
$actions_map[$key]['description'] = $array['description'];
$actions_map[$key]['type'] = $array['type'];
$actions_map[$key]['configurable'] = $array['configurable'];
}
return $actions_map;
}
/**
* Given an md5 hash of a function name, return the function name.
* Faster than actions_actions_map() when you only need the function name.
*
* @param $hash
* MD5 hash of a function name
*
* @return
* Function name
*/
function actions_function_lookup($hash) {
$actions_list = actions_list();
foreach ($actions_list as $function => $array) {
if (md5($function) == $hash) {
return $function;
}
}
// Must be an instance; must check database.
$aid = db_result(db_query("SELECT aid FROM {actions} WHERE MD5(aid) = '%s' AND parameters != ''", $hash));
return $aid;
}
......@@ -2255,6 +2255,7 @@ function _drupal_bootstrap_full() {
require_once './includes/image.inc';
require_once './includes/form.inc';
require_once './includes/mail.inc';
require_once './includes/actions.inc';
// Set the Drupal custom error handler.
set_error_handler('drupal_error_handler');
// Emit the correct charset HTTP header.
......@@ -3480,3 +3481,44 @@ function watchdog_severity_levels() {
WATCHDOG_DEBUG => t('debug'),
);
}
/**
* Explode a string of given tags into an array.
*/
function drupal_explode_tags($tags) {
// This regexp allows the following types of user input:
// this, "somecompany, llc", "and ""this"" w,o.rks", foo bar
$regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
preg_match_all($regexp, $tags, $matches);
$typed_tags = array_unique($matches[1]);
$tags = array();
foreach ($typed_tags as $tag) {
// If a user has escaped a term (to demonstrate that it is a group,
// or includes a comma or quote character), we remove the escape
// formatting so to save the term into the database as the user intends.
$tag = trim(str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $tag)));
if ($tag != "") {
$tags[] = $tag;
}
}
return $tags;
}
/**
* Implode an array of tags into a string.
*/
function drupal_implode_tags($tags) {
$encoded_tags = array();
foreach ($tags as $tag) {
// Commas and quotes in tag names are special cases, so encode them.
if (strpos($tag, ',') !== FALSE || strpos($tag, '"') !== FALSE) {
$tag = '"'. str_replace('"', '""', $tag) .'"';
}
$encoded_tags[] = $tag;
}
return implode(', ', $encoded_tags);
}
\ No newline at end of file
; $Id$
name = Actions
description = Enables triggerable Drupal actions.
package = Core - optional
version = VERSION
core = 6.x
<?php
// $Id$
/**
* Implementation of hook_install().
*/
function actions_install() {
// Create tables.
drupal_install_schema('actions');
variable_set('actions_next_id', 0);
// Do initial sychronization of actions in code and the database.
// For that we need to run code from actions.module.
drupal_load('module', 'actions');
actions_synchronize(actions_list());
}
/**
* Implementation of hook_uninstall().
*/
function actions_uninstall() {
// Remove tables.
drupal_uninstall_schema('actions');
variable_del('actions_next_id');
}
This diff is collapsed.
<?php
// $Id$
function actions_schema() {
$schema['actions'] = array(
'fields' => array(
'aid' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '0'),
'type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''),
'callback' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
'parameters' => array('type' => 'text', 'not null' => TRUE, 'size' => 'big', 'default' => ''),
'description' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '0'),
),
'primary key' => array('aid'),
);
$schema['actions_assignments'] = array(
'fields' => array(
'hook' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''),
'op' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''),
'aid' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
),
'index keys' => array(
'hook_op' => array('hook', 'op'))
);
return $schema;
}
......@@ -2086,3 +2086,107 @@ function int2vancode($i = 0) {
function vancode2int($c = '00') {
return base_convert(substr($c, 1), 36, 10);
}
/**
* Implementation of hook_hook_info().
*/
function comment_hook_info() {
return array(
'comment' => array(
'comment' => array(
'insert' => array(
'runs when' => t('When a comment has been created'),
),
'update' => array(
'runs when' => t('When a comment has been updated'),
),
'delete' => array(
'runs when' => t('When a comment has been deleted')
),
'view' => array(
'runs when' => t('When a comment is being viewed')
),
),
),
);
}
/**
* Implementation of hook_action_info().
*/
function comment_action_info() {
return array(
'comment_unpublish_action' => array(
'description' => t('Unpublish comment'),
'type' => 'comment',
'configurable' => FALSE,
'hooks' => array(
'comment' => array('insert', 'update', 'view'),
)
),
'comment_unpublish_by_keyword_action' => array(
'description' => t('Unpublish comment containing keyword(s)'),
'type' => 'comment',
'configurable' => TRUE,
'hooks' => array(
'comment' => array('insert', 'update'),
)
)
);
}
/**
* Drupal action to unpublish a comment.
*
* @param $context
* Keyed array. Must contain the id of the comment if $comment is not passed.
* @param $comment
* An optional comment object.
*/
function comment_unpublish_action($comment, $context = array()) {
if (isset($comment->cid)) {
$cid = $comment->cid;
$subject = $comment->subject;
}
else {
$cid = $context['cid'];
$subject = db_result(db_query("SELECT subject FROM {comments} WHERE cid = %d", $cid));
}
db_query('UPDATE {comments} SET status = %d WHERE cid = %d', COMMENT_NOT_PUBLISHED, $cid);
watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject));
}
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 character sequences above. Use a comma-separated list of character sequences. Example: funny, bungee jumping, "Company, Inc.".'),
'#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
);
return $form;
}
function comment_unpublish_by_keyword_action_submit($form, $form_state) {
return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
}
/**
* Implementation of a configurable Drupal action.
* Unpublish a comment if it contains a certain string.
*
* @param $context
* An array providing more information about the context of the call to this action.
* Unused here since this action currently only supports the insert and update ops of
* the comment hook, both of which provide a complete $comment object.
* @param $comment
* A comment object.
*/
function comment_unpublish_by_keyword_action($comment, $context) {
foreach ($context['keywords'] as $keyword) {
if (strstr($comment->comment, $keyword) || strstr($comment->subject, $keyword)) {
db_query('UPDATE {comments} SET status = %d WHERE cid = %d', COMMENT_NOT_PUBLISHED, $comment->cid);
watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
break;
}
}
}
\ No newline at end of file
......@@ -616,6 +616,8 @@ function node_load($param = array(), $revision = NULL, $reset = NULL) {
* Save a node object into the database.
*/
function node_save(&$node) {
// Let modules modify the node before it is saved to the database.
node_invoke_nodeapi($node, 'presave');
global $user;
$node->is_new = FALSE;
......@@ -654,7 +656,7 @@ function node_save(&$node) {
'title' => "'%s'", 'body' => "'%s'",
'teaser' => "'%s'", 'timestamp' => '%d',
'uid' => '%d', 'format' => '%d');
if (!empty($node->log) || $node->is_new || $node->revision) {
if (!empty($node->log) || $node->is_new || (isset($node->revision) && $node->revision)) {
// Only store the log message if there's something to store; this prevents
// existing log messages from being unintentionally overwritten by a blank
// message. A new revision will have an empty log message (or $node->log).
......@@ -3173,3 +3175,278 @@ function node_forms() {
}
return $forms;
}
/**
* Implementation of hook_hook_info().
*/
function node_hook_info() {
return array(
'node' => array(
'nodeapi' => array(
'presave' => array(
'runs when' => t('When content is about to be saved'),
),
'insert' => array(
'runs when' => t('When content has been created'),
),
'update' => array(
'runs when' => t('When content has been updated'),
),
'delete' => array(
'runs when' => t('When content has been deleted')
),
'view' => array(
'runs when' => t('When content is viewed by an authenticated user')
),
),
),
);
}
/**
* Implementation of hook_action_info().
*/
function node_action_info() {
return array(
'node_publish_action' => array(
'type' => 'node',
'description' => t('Publish post'),
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('presave','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
),
),
'node_unpublish_action' => array(
'type' => 'node',
'description' => t('Unpublish post'),
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('presave','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
),
),
'node_make_sticky_action' => array(
'type' => 'node',
'description' => t('Make post sticky'),
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('presave','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
),
),
'node_make_unsticky_action' => array(
'type' => 'node',
'description' => t('Make post unsticky'),
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('presave','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
),
),
'node_promote_action' => array(
'type' => 'node',
'description' => t('Promote post to front page'),
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('presave','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
'user' => array('login'),
),
),
'node_unpromote_action' => array(
'type' => 'node',
'description' => t('Remove post from front page'),
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('presave','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
),
),
'node_assign_owner_action' => array(
'type' => 'node',
'description' => t('Change the author of a post'),
'configurable' => TRUE,
'hooks' => array(
'any' => TRUE,
'nodeapi' => array('presave','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
),
),
'node_save_action' => array(
'type' => 'node',
'description' => t('Save post'),
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('delete','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
'user' => array('login'),
),
),
'node_unpublish_by_keyword_action' => array(
'type' => 'node',
'description' => t('Unpublish post containing keyword(s)'),
'configurable' => TRUE,
'hooks' => array(
'nodeapi' => array('presave','insert','update', 'view'),
'comment' => array('delete','insert','update', 'view'),
),
),
);
}
/**
* Implementation of a Drupal action.
* Sets the status of a node to 1, meaning published.
*/
function node_publish_action(&$node, $context = array()) {
$node->status = 1;
watchdog('action', 'Set @type %title to published.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
}
/**
* Implementation of a Drupal action.
* Sets the status of a node to 0, meaning unpublished.
*/
function node_unpublish_