Index: includes/actions.inc
===================================================================
RCS file: includes/actions.inc
diff -N includes/actions.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/actions.inc	12 Jun 2007 20:06:56 -0000
@@ -0,0 +1,218 @@
+<?php
+
+// $Id$
+
+/**
+* @file
+* This is the actions engine for executing stored actions.
+*/
+
+/**
+ * Given the IDs of actions to perform, find out what the functions
+ * for the actions are by querying the database. Then call each function
+ * using the function call $function('do', $params, $a1, $a2, $a3, $a4)
+ * where $function is the name of a function written in compliance with
+ * the action specification. 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 $a1
+ *   Parameter that will be passed along to the callback.
+ * @param $a2
+ *   Parameter that will be passed along to the callback.
+ * @param $a3
+ *   Parameter that will be passed along to the callback.
+ * @param $a4
+ *   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, $context = NULL, $object = NULL, $a3 = NULL, $a4 = NULL) {
+  static $stack;
+  $stack++;
+  if ($stack > variable_get('actions_max_stack', 35)) {
+    watchdog('actions', t('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 = '';
+    foreach ($action_ids as $action_id) {
+      if (is_numeric($action_id)) {
+        $where .= 'OR aid = ' . $action_id . ' ';
+      }
+      elseif (isset($available_actions[$action_id])) {
+        $actions[$action_id] = $available_actions[$action_id];
+      }
+    }
+
+    if ($where) { // We must go to the database to retrieve instance data.
+      // Strip off leading 'OR '.
+      $where = $where ? '(' . strstr($where, " ") . ')' : '';
+      $result_db = db_query("SELECT * FROM {actions} WHERE $where");
+      while ($data = db_fetch_object($result_db)) {
+        $action_id = $data->action_id;
+        $actions[$action_id] = $data->params ? unserialize($data->params) : array();
+        $actions[$action_id]['function']  = $data->func;
+        $actions[$action_id]['type']      = $data->type;
+      }
+    }
+
+    // Fire actions, in no particular order.
+    foreach ($actions as $action_id => $params) {
+      if (is_numeric($action_id)) { // Configurable actions need parameters.
+        $function = $params['function'];
+        $context = array_merge($context, $params);
+        $result[$action_id] = $function('do', $context, $object, $a3, $a4);
+      }
+      else { // Singleton action; $action_id is the function name.
+        $result[$action_id] = $action_id($context, $object, $a3, $a4);
+      }
+    }
+  }
+  else { // Optimized execution of single action.
+    if (is_numeric($action_ids)) { // Configurable action; retrieve stored parameters.
+      $data = db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", $action_ids));
+      $function = $data->func;
+      $context = array_merge($context, unserialize($data->params));
+      $result[$action_ids] = $function($context, $object, $a3, $a4);
+    }
+    else { // Singleton action; $action_ids is the function name.
+      $result[$action_ids] = $action_ids($context, $object, $a4, $a4);
+    }
+  }
+  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');
+  return $actions;
+}
+
+/**
+ * Retrieve all action instances from the database.
+ * Compare with actions_list() which gathers actions from
+ * the PHP function namespace. 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 'function', 'description',
+ *   and 'type'.
+ */
+function actions_get_all_actions() {
+  $actions = array();
+  $result = db_query("SELECT * FROM {actions}");
+  while ($data = db_fetch_object($result)) {
+    $actions[$data->aid] = array(
+      'function' => $data->func,
+      'description' => $data->description,
+      'type' => $data->type,
+      'configurable' => $data->params ? TRUE : FALSE,
+    );
+  }
+  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 $func => $array) {
+    $key = md5($func);
+    $actions_map[$key]['function']     = isset($array['function']) ? $array['function'] : $func;
+    $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.
+  $result = db_query("SELECT aid FROM {actions} WHERE params != ''");
+  while ($data = db_fetch_object($result)) {
+    if (md5($data->aid) == $hash) {
+      return $data->aid;
+    }
+  }
+}
\ No newline at end of file
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.653
diff -u -u -p -r1.653 common.inc
--- includes/common.inc	11 Jun 2007 15:21:14 -0000	1.653
+++ includes/common.inc	12 Jun 2007 20:06:57 -0000
@@ -2228,6 +2228,7 @@ function _drupal_bootstrap_full() {
   require_once './includes/unicode.inc';
   require_once './includes/image.inc';
   require_once './includes/form.inc';
+  require_once './includes/actions.inc';
   // Set the Drupal custom error handler.
   set_error_handler('drupal_error_handler');
   // Emit the correct charset HTTP header.
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.552
diff -u -u -p -r1.552 comment.module
--- modules/comment/comment.module	11 Jun 2007 14:56:40 -0000	1.552
+++ modules/comment/comment.module	12 Jun 2007 20:06:57 -0000
@@ -2063,3 +2063,91 @@ function int2vancode($i = 0) {
 function vancode2int($c = '00') {
   return base_convert(substr($c, 1), 36, 10);
 }
+
+/**
+ * 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_PUBLISHED, $cid);
+  watchdog('action', t('Unpublished comment %subject.', array('%subject' => $subject)));
+}
+
+function comment_unpublish_by_keyword_action_form($context) {
+  if (!module_exists('taxonomy')) {
+    $form['warning'] = array(
+      '#prefix' => "<div class='error'>",
+      '#suffix' => '</div>',
+      '#value' => t('This action requires the !taxonomy_module to be enabled.', array('!taxonomy_module' => l('taxonomy module', 'admin/build/modules'))),
+      '#weight' => -8,
+    );
+  }
+  $form['keywords'] = array(
+    '#title' => t('Keywords'),
+    '#type' => 'textfield',
+    '#description' => t('The comment will be unpublished if it contains any of the keywords above. Use a comma-separated list of keywords. Example: funny, bungee jumping, "Company, Inc.".'),
+    '#default_value' => isset($context['keywords']) ? implode(',', $context['keywords']) : '',
+  );
+  return $form;
+}
+
+function comment_unpublish_by_keyword_action_submit($form, $form_state) {
+  return array('keywords' => taxonomy_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)) {
+      db_query('UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d', $comment->cid);
+      watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
+      break;
+    }
+  }
+}
\ No newline at end of file
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.829
diff -u -u -p -r1.829 node.module
--- modules/node/node.module	7 Jun 2007 03:45:57 -0000	1.829
+++ modules/node/node.module	12 Jun 2007 20:06:57 -0000
@@ -613,6 +613,8 @@ function node_load($param = array(), $re
  * 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;
@@ -3100,3 +3102,198 @@ function node_forms() {
   }
   return $forms;
 }
+
+/**
+ * Implementation of hook_action_info().
+ */
+function node_action_info() {
+  return array(
+    'node_publish_action' => array(
+      'type' => 'node',
+      'description' => t('Publish node'),
+      '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 node'),
+      '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 node 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 node 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 node to front page'),
+      'configurable' => FALSE,
+      'hooks' => array(
+        'nodeapi' => array('presave','insert','update', 'view'),
+        'comment' => array('delete','insert','update', 'view'),
+      ),
+    ),
+    'node_unpromote_action' => array(
+      'type' => 'node',
+      'description' => t('Remove post from node 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 node'),
+      'configurable' => TRUE,
+      'hooks' => array(
+        'nodeapi' => array('presave','insert','update', 'view'),
+        'comment' => array('delete','insert','update', 'view'),
+      ),
+    ),
+    'node_save_action' => array(
+      'type' => 'node',
+      'description' => t('Save node'),
+      'configurable' => FALSE,
+      'hooks' => array(
+        'nodeapi' => array('delete','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', t('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_action(&$node, $context = array()) {
+  $node->status = 0;
+  watchdog('action', t('Set @type %title to unpublished.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
+}
+
+/**
+ * Implementation of a Drupal action.
+ * Sets the sticky-at-top-of-list property of a node to 1.
+ */
+function node_make_sticky_action(&$node, $context = array()) {
+  $node->sticky = 1;
+  watchdog('action', t('Set @type %title to sticky.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
+}
+
+/**
+ * Implementation of a Drupal action.
+ * Sets the sticky-at-top-of-list property of a node to 1.
+ */
+function node_make_unsticky_action(&$node, $context = array()) {
+  $node->sticky = 0;
+  watchdog('action', t('Set @type %title to unsticky.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
+}
+
+/**
+ * Implementation of a Drupal action.
+ * Sets the promote property of a node to 1.
+ */
+function node_promote_action($op, &$node, $context = array()) {
+  $node->promote = 1;
+  watchdog('action', t('Promoted @type %title to front page.', array('@type' => node_get_types('type', $node), '%title' => $node->title)));
+}
+
+/**
+ * Implementation of a Drupal action.
+ * Sets the promote property of a node to 0.
+ */
+function node_unpromote_action($op, &$node, $context = array()) {
+  $node->promote = 1;
+  watchdog('action', t('Removed @type %title from front page.', array('@type' => node_get_types('type', $node), '%title' => $node->title)));
+}
+
+/**
+ * Implementation of a configurable Drupal action.
+ * Assigns ownership of a node to a user.
+ */
+function node_assign_owner_action(&$node, $context = array()) {
+  $uid = db_result(db_query("SELECT uid FROM {users} WHERE name = '%s'", $context['owner_name']));
+  $node->uid = $uid;
+  watchdog('action', t('Changed owner of @type %title to uid %name.', array('@type' => node_get_types('type', $node), '%title' => $node->title, '%name' => $context['owner_name'])));
+}
+
+function node_assign_owner_action_form($context) {
+  $description = t('The username of the user to which you would like to assign ownership.');
+  $count = db_result(db_query("SELECT COUNT(*) FROM {users}"));
+  // 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");
+    while ($data = db_fetch_object($result)) {
+      $options[$data->name] = $data->name;
+    }
+    $form['owner_name'] = array(
+      '#type' => 'select',
+      '#title' => t('Username'),
+      '#default_value' => isset($context['owner_name']) ? $context['owner_name'] : '',
+      '#options' => $options,
+      '#description' => $description,
+     );
+    }
+    else {
+      $form['owner_name'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Username'),
+        '#default_value' => isset($context['owner_name']) ? $context['owner_name'] : '',
+        '#size' => '6',
+        '#maxlength' => '7',
+        '#description' => $description,
+      );
+    }
+    return $form;
+}
+
+function node_assign_owner_action_validate($form_id, $form_values) {
+  $count = db_result(db_query("SELECT COUNT(*) FROM {users} WHERE name = '%s'", $context['owner_name']));
+  if (intval($count) != 1) {
+    form_set_error('owner_name', t('Please enter a valid username.'));
+  }
+}
+
+function node_assign_owner_action_submit($form_id, $form_values) {
+  return array('owner_name' => $form_values['owner_name']);
+}
+
+/**
+ * Implementation of a configurable Drupal action.
+ * Saves a node.
+ */
+function node_save_action($node) {
+  node_save($node);
+  watchdog('action', t('Saved @type %title', array('@type' => node_get_types('type', $node), '%title' => $node->title)));
+}
\ No newline at end of file
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.491
diff -u -u -p -r1.491 system.module
--- modules/system/system.module	8 Jun 2007 12:51:59 -0000	1.491
+++ modules/system/system.module	12 Jun 2007 20:06:57 -0000
@@ -2718,3 +2718,190 @@ function system_batch_page() {
     print theme('page', $output, FALSE, FALSE);
   }
 }
+
+/**
+ * Implementation of hook_action_info().
+ */
+function system_action_info() {
+  return array(
+    'system_message_action' => array(
+      'type' => 'system',
+      'description' => t('Display a message to the user'),
+      'configurable' => TRUE,
+      'hooks' => array(
+        'nodeapi' => array('view', 'insert', 'update', 'delete'),
+        'comment' => array('view', 'insert', 'update', 'delete'),
+        'user' => array('view', 'insert', 'update', 'delete', 'login'),
+      ),
+    ),
+    'system_send_email_action' => array(
+      'description' => t('Send e-mail'),
+      'type' => 'system',
+      'configurable' => TRUE,
+      'hooks' => array(
+        'nodeapi' => array('view', 'insert', 'update', 'delete'),
+        'comment' => array('view', 'insert', 'update', 'delete'),
+        'user' => array('view', 'insert', 'update', 'delete', 'login'),
+      )
+    )
+  );
+}
+
+/**
+ * Implementation of a configurable Drupal action.
+ * Sends an email.
+ */
+function system_send_email_action($object, $context) {
+  switch($context['hook']) {
+    case 'node':
+      $node = $object;
+      break;
+    // The comment hook also provides the node, in context.
+    case 'comment':
+      $node = $context['node'];
+    case 'user':
+      $user = $object;
+      break;
+  }
+
+  // Note this is the user who owns the node, not global $user.
+  if (!isset($user)) {
+    $user = user_load(array('uid' => $node->uid));
+  }
+  $from = variable_get('site_mail', ini_get('sendmail_from'));
+  $subject = $context['subject'];
+  $message = $context['message'];
+  if ($context['recipient'] == '%author') {
+    $recipient = $user->mail;
+  } else {
+    $recipient = $context['recipient'];
+  }
+  $variables['%site_name'] = variable_get('site_name', 'Drupal');
+  $variables['%username'] = $user->name;
+
+  // Node-based variable translation is only available if we have a node.
+  if (isset($node) && is_object($node)) {
+    $variables = array_merge($variables, array(
+      '%username' => check_plain($user->name),
+      '%uid' => $node->uid,
+      '%node_url' => url('node/' . $node->nid, NULL, NULL, TRUE),
+      '%node_type' => check_plain($node->type),
+      '%title' => filter_xss($node->title),
+      '%teaser' => filter_xss($node->teaser),
+      '%body' => filter_xss($node->body)
+      )
+    );
+
+    $subject = strtr($subject, $variables);
+    $subject = str_replace(array("\r", "\n"), '', $subject);
+    $message = strtr($message, $variables);
+  }
+  if (drupal_mail('action_send_email', $recipient, $subject, $message, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from" )) {
+    watchdog('action', t('Sent email to %recipient', array('%recipient' => $recipient)));
+  }
+  else {
+    watchdog('error', t('Unable to send email to %recipient', array('%recipient' => $recipient)));
+  }
+}
+
+function system_send_email_action_form($context) {
+  // Return a form definition for the configuration form.
+  // 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'],
+    '#size' => '20',
+    '#maxlength' => '254',
+    '#description' => t('The email address to which the message should be sent OR enter %author if you would like to send an e-mail to the original author of the post.', array('%author' => t('%author'))),
+  );
+  $form['subject'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Subject'),
+    '#default_value' => $context['subject'],
+    '#size' => '20',
+    '#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 the following variables: %site_name, %username, %node_url, %node_type, %title, %teaser, %body. Not all variables will be available in all contexts.'),
+  );
+  return $form;
+}
+
+function system_send_email_action_validate($form, $form_state) {
+  $form_values = $form_state['values'];
+  // Validate the configuration form.
+  if (!valid_email_address($form_values['recipient']) && $form_values['recipient'] != t('%author')) {
+    form_set_error('recipient', t('Please enter a valid email address or %author.', array('%author' => theme('placeholder', '%author'))));
+   }
+}
+
+function system_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;
+}
+
+function system_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 the following variables: %site_name, %username, %node_url, %node_type, %title, %teaser, %body.'),
+      );
+      return $form;
+}
+
+function system_message_action_submit($form, $form_state) {
+  return array('message' => $form_state['values']['message']);
+}
+
+/**
+ * Implementation of a configurable Drupal action.
+ * Sends a configurable message to the current user's screen.
+ */
+function system_message_action(&$object, $context = array()) {
+  switch($context['hook']) {
+    case 'nodeapi':
+      $node = &$object;
+      break;
+    // The comment hook gives us the nid of the node.
+    case 'comment':
+      $node = node_load($object['nid']);
+  }
+  global $user;
+  $variables = array(
+    '%site_name' => variable_get('site_name', 'drupal'),
+    '%username' => $user->name ? $user->name : variable_get('anonymous', t('Anonymous')),
+   );
+
+  if (isset($node) && is_object($node)) {
+    $variables += array(
+      '%uid' => $node->uid,
+      '%node_url' => url('node/' . $node->nid, NULL, NULL, TRUE),
+      '%node_type' => check_plain($node->type),
+      '%title' => filter_xss($node->title),
+      '%teaser' => filter_xss($node->teaser),
+      '%body' => filter_xss($node->body),
+    );
+  }
+  $context['message'] = strtr($context['message'], $variables);
+  drupal_set_message($context['message']);
+}
\ No newline at end of file
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.796
diff -u -u -p -r1.796 user.module
--- modules/user/user.module	11 Jun 2007 15:27:52 -0000	1.796
+++ modules/user/user.module	12 Jun 2007 20:06:57 -0000
@@ -3185,3 +3185,60 @@ function _user_password_dynamic_validati
     $complete = TRUE;
   }
 }
+
+/**
+ * Implementation of hook_action_info().
+ */
+function user_action_info() {
+  return array(
+    'user_block_user_action' => array(
+      'description' => t('Block current user'),
+      'type' => 'user',
+      'configurable' => FALSE,
+      'hooks' => array(
+        'user' => array('view', 'insert', 'update', 'delete', 'login', 'logout'),
+        'nodeapi' => array('presave', 'delete','insert','update','view'),
+        'comment' => array('view', 'insert', 'update', 'delete'),
+        ),
+      ),
+    'user_block_ip_action' => array(
+      'description' => t('Ban IP address of current user'),
+      'type' => 'user',
+      'configurable' => FALSE,
+      'hooks' => array(
+        'user' => array('view', 'insert', 'update', 'delete', 'login', 'logout'),
+        'nodeapi' => array('presave', 'delete','insert','update','view'),
+        'comment' => array('view', 'insert', 'update', 'delete'),
+      )
+    )
+  );
+}
+
+/**
+ * Implementation of a Drupal action.
+ * Blocks the current user.
+ */
+function user_block_user_action(&$object, $context = array()) {
+  if (isset($object->uid)) {
+    $uid = $object->uid;
+  }
+  elseif (isset($context['uid'])) {
+    $uid = $context['uid'];
+  }
+  else {
+    global $user;
+    $uid = $user->uid;
+  }
+  db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid);
+  sess_destroy_uid($uid);
+  watchdog('action', t('Blocked user %name.', array('%name' => check_plain($user->name))));
+}
+
+/**
+ * Implementation of a Drupal action.
+ * Adds an access rule that blocks the user's IP address.
+ */
+function user_block_ip_action() {
+  db_query("INSERT INTO {access} (mask, type, status) VALUES ('%s', '%s', %d)", $_SERVER['REMOTE_ADDR'], 'host', 0);
+  watchdog('action', t('Banned IP %ip', array('%ip' => $_SERVER['REMOTE_ADDR'])));
+}
\ No newline at end of file
