Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.666
diff -u -F^f -r1.666 common.inc
--- includes/common.inc	28 Jun 2007 07:48:40 -0000	1.666
+++ includes/common.inc	29 Jun 2007 03:29:13 -0000
@@ -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
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.556
diff -u -F^f -r1.556 comment.module
--- modules/comment/comment.module	24 Jun 2007 10:09:52 -0000	1.556
+++ modules/comment/comment.module	29 Jun 2007 03:29:14 -0000
@@ -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
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.842
diff -u -F^f -r1.842 node.module
--- modules/node/node.module	28 Jun 2007 07:48:40 -0000	1.842
+++ modules/node/node.module	29 Jun 2007 03:29:14 -0000
@@ -616,6 +616,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;
@@ -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_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'),
+      ),
+    ),
+    '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'),
+      ),
+    ),
+  );
+}
+
+/**
+ * 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_action(&$node, $context = array()) {
+  $node->status = 0;
+  watchdog('action', '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', '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', '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(&$node, $context = array()) {
+  $node->promote = 1;
+  watchdog('action', '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(&$node, $context = array()) {
+  $node->promote = 1;
+  watchdog('action', '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) {
+  $node->uid = $context['owner_uid'];
+  $owner_name = db_result(db_query("SELECT name FROM {users} WHERE uid = %d", $context['owner_uid']));
+  watchdog('action', 'Changed owner of @type %title to uid %name.', array('@type' => node_get_types('type', $node), '%title' => $node->title, '%name' => $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}"));
+  $owner_name = '';
+  if (isset($context['owner_uid'])) {
+    $owner_name = db_result(db_query("SELECT name FROM {users} WHERE uid = %d", $context['owner_uid']));
+  }
+
+  // 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' => $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' => '7',
+      '#description' => $description,
+    );
+  }
+  return $form;
+}
+
+function node_assign_owner_action_validate($form, $form_state) {
+  $count = db_result(db_query("SELECT COUNT(*) FROM {users} WHERE name = '%s'", $form_state['values']['owner_name']));
+  if (intval($count) != 1) {
+    form_set_error('owner_name', t('Please enter a valid username.'));
+  }
+}
+
+function node_assign_owner_action_submit($form, $form_state) {
+  // Username can change, so we need to store the ID, not the username.
+  $uid = db_result(db_query("SELECT uid from {users} WHERE name = '%s'", $form_state['values']['owner_name']));
+  return array('owner_uid' => $uid);
+}
+
+/**
+ * Implementation of a configurable Drupal action.
+ * Saves a node.
+ */
+function node_save_action($node) {
+  node_save($node);
+  watchdog('action', 'Saved @type %title', array('@type' => node_get_types('type', $node), '%title' => $node->title));
+}
+
+function node_unpublish_by_keyword_action_form($context) {
+  $form['keywords'] = array(
+    '#title' => t('Keywords'),
+    '#type' => 'textarea',
+    '#description' => t('The node 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 node_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 node if it contains a certain string.
+ *
+ * @param $context
+ *   An array providing more information about the context of the call to this action.
+ * @param $comment
+ *   A node object.
+ */
+function node_unpublish_by_keyword_action($node, $context) {
+  foreach ($context['keywords'] as $keyword) {
+    if (strstr(node_view(drupal_clone($node)), $keyword) || strstr($node->title, $keyword)) {
+      $node->status = 0;
+      watchdog('action', 'Set @type %title to unpublished.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
+      break;
+    }
+  }
+}
\ No newline at end of file
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.499
diff -u -F^f -r1.499 system.module
--- modules/system/system.module	28 Jun 2007 00:48:26 -0000	1.499
+++ modules/system/system.module	29 Jun 2007 03:29:15 -0000
@@ -2818,3 +2818,316 @@ function system_batch_page() {
     print theme('page', $output, FALSE, FALSE);
   }
 }
+
+/**
+ * Implementation of hook_hook_info().
+ */
+function system_hook_info() {
+  return array(
+    'system' => array(
+      'cron' => array(
+        'run' => array(
+          'runs when' => t('When cron runs'),
+        ),
+      ),
+    ),
+  );
+}
+
+/**
+ * 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'),
+        'taxonomy' => array('insert', 'update', 'delete'),
+      ),
+    ),
+    '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'),
+        'taxonomy' => array('insert', 'update', 'delete'),
+      )
+    ),
+    'system_goto_action' => array(
+      'description' => t('Redirect to URL'),
+      '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'),
+      )
+    )
+  );
+}
+
+/**
+ * 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.
+ */
+function system_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'],
+    '#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' => '%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'] != '%author') {
+    // We want the literal %author placeholder to be emphasized in the error message.
+    form_set_error('recipient', t('Please enter a valid email address or %author.', array('%author' => '%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;
+}
+
+/**
+ * Implementation of a configurable Drupal action.
+ * Sends an email.
+ */
+function system_send_email_action($object, $context) {
+  global $user;
+  $variables['%site_name'] = variable_get('site_name', 'Drupal');
+
+  switch ($context['hook']) {
+    case 'nodeapi':
+      // Because this is not an action of type 'node' the node
+      // will not be passed as $object, but it will still be available
+      // in $context.
+      $node = $context['node'];
+      break;
+    // The comment hook provides nid, in $context.
+    case 'comment':
+      $comment = $context['comment'];
+      $node = node_load($comment->nid);
+    case 'user':
+      // Because this is not an action of type 'user' the user
+      // object is not passed as $object, but it will still be available
+      // in $context.
+      $account = $context['account'];
+      if (isset($context['node'])) {
+        $node = $context['node'];
+      }
+      elseif ($context['recipient'] == '%author') {
+        // If we don't have a node, we don't have a node author.
+        watchdog('error', 'Cannot use %author token in this context.');
+        return;
+      }
+      break;
+    case 'taxonomy':
+      $account = $user;
+      $vocabulary = taxonomy_vocabulary_load($object->vid);
+      $variables = array_merge($variables, array(
+        '%term_name' => $object->name,
+        '%term_description' => $object->description,
+        '%term_id' => $object->tid,
+        '%vocabulary_name' => $vocabulary->name,
+        '%vocabulary_description' => $vocabulary->description,
+        '%vocabulary_id' => $vocabulary->vid,
+        )
+      );
+      break;
+    default:
+      // We are being called directly.
+      $node = $object;
+  }
+
+  $from = variable_get('site_mail', ini_get('sendmail_from'));
+  $recipient = $context['recipient'];
+
+  if (isset($node)) {
+    if (!isset($account)) {
+      $account = user_load(array('uid' => $node->uid));
+    }
+    if ($recipient == '%author') {
+      $recipient = $account->mail;
+    }
+  }
+
+  $variables['%username'] = $account->name;
+
+  // Node-based variable translation is only available if we have a node.
+  if (isset($node) && is_object($node)) {
+    $variables = array_merge($variables, array(
+        '%uid' => $node->uid,
+        '%node_url' => url('node/'. $node->nid, array('absolute' => TRUE)),
+        '%node_type' => node_get_types('name', $node),
+        '%title' => $node->title,
+        '%teaser' => $node->teaser,
+        '%body' => $node->body
+      )
+    );
+  }
+
+  $subject = strtr($context['subject'], $variables);
+  $subject = str_replace(array("\r", "\n"), '', $subject);
+  $message = strtr($context['message'], $variables);
+  $body = drupal_html_to_text($message);
+
+  if (drupal_mail('action_send_email', $recipient, $subject, $body, $from )) {
+    watchdog('action', 'Sent email to %recipient', array('%recipient' => $recipient));
+  }
+  else {
+    watchdog('error', 'Unable to send email to %recipient', array('%recipient' => $recipient));
+  }
+}
+
+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. Not all variables will be available in all contexts.'),
+  );
+  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()) {
+  global $user;
+  $variables = array(
+    '%site_name' => variable_get('site_name', 'Drupal'),
+    '%username' => $user->name ? $user->name : variable_get('anonymous', t('Anonymous')),
+   );
+
+  // This action can be called in any context, but if placeholders
+  // are used a node object must be present to be the source
+  // of substituted text.
+  switch ($context['hook']) {
+    case 'nodeapi':
+      // Because this is not an action of type 'node' the node
+      // will not be passed as $object, but it will still be available
+      // in $context.
+      $node = $context['node'];
+      break;
+    // The comment hook also provides the node, in context.
+    case 'comment':
+      $comment = $context['comment'];
+      $node = node_load($comment->nid);
+      break;
+    case 'taxonomy':
+      $vocabulary = taxonomy_vocabulary_load($object->vid);
+      $variables = array_merge($variables, array(
+        '%term_name' => $object->name,
+        '%term_description' => $object->description,
+        '%term_id' => $object->tid,
+        '%vocabulary_name' => $vocabulary->name,
+        '%vocabulary_description' => $vocabulary->description,
+        '%vocabulary_id' => $vocabulary->vid,
+        )
+      );
+      break;
+    default:
+      // We are being called directly.
+      $node = $object;
+  }
+
+  if (isset($node) && is_object($node)) {
+    $variables = array_merge($variables, array(
+      '%uid' => $node->uid,
+      '%node_url' => url('node/'. $node->nid, array('absolute' => TRUE)),
+      '%node_type' => check_plain(node_get_types('name', $node)),
+      '%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']);
+}
+
+/**
+ * Implementation of a configurable Drupal action.
+ * Redirect user to a URL.
+ */
+function system_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 http://drupal.org.'),
+    '#default_value' => isset($context['url']) ? $context['url'] : '',
+    '#required' => TRUE,
+  );
+  return $form;
+}
+
+function system_goto_action_submit($form, $form_state) {
+  return array(
+    'url' => $form_state['values']['url']
+  );
+}
+
+function system_goto_action($object, $context) {
+  drupal_goto($context['url']);
+}
\ No newline at end of file
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.363
diff -u -F^f -r1.363 taxonomy.module
--- modules/taxonomy/taxonomy.module	22 Jun 2007 08:46:15 -0000	1.363
+++ modules/taxonomy/taxonomy.module	29 Jun 2007 03:29:16 -0000
@@ -826,7 +826,7 @@ function taxonomy_node_save($node, $term
     unset($terms['tags']);
 
     foreach ($typed_input as $vid => $vid_value) {
-      $typed_terms = taxonomy_explode_tags($vid_value);
+      $typed_terms = drupal_explode_tags($vid_value);
 
       $inserted = array();
       foreach ($typed_terms as $typed_term) {
@@ -1502,7 +1502,7 @@ function _taxonomy_get_tid_from_term($te
  */
 function taxonomy_autocomplete($vid, $string = '') {
   // The user enters a comma-separated list of tags. We only autocomplete the last tag.
-  $array = taxonomy_explode_tags($string);
+  $array = drupal_explode_tags($string);
 
   // Fetch last tag
   $last_string = trim(array_pop($array));
@@ -1526,30 +1526,6 @@ function taxonomy_autocomplete($vid, $st
 }
 
 /**
- * Explode a string of given tags into an array.
- */
-function taxonomy_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 a list of tags of a certain vocabulary into a string.
  */
 function taxonomy_implode_tags($tags, $vid = NULL) {
@@ -1568,3 +1544,24 @@ function taxonomy_implode_tags($tags, $v
   }
   return implode(', ', $typed_tags);
 }
+
+/**
+ * Implementation of hook_hook_info().
+ */
+function taxonomy_hook_info() {
+  return array(
+    'taxonomy' => array(
+      'taxonomy' => array(
+        'insert' => array(
+          'runs when' => t('When a new category has been created'),
+        ),
+        'update' => array(
+          'runs when' => t('When a category has been updated'),
+        ),
+        'delete' => array(
+          'runs when' => t('When a category has been deleted')
+        ),
+      ),
+    ),
+  );
+}
\ No newline at end of file
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.809
diff -u -F^f -r1.809 user.module
--- modules/user/user.module	28 Jun 2007 07:48:41 -0000	1.809
+++ modules/user/user.module	29 Jun 2007 03:29:16 -0000
@@ -3200,3 +3200,90 @@ function _user_password_dynamic_validati
     $complete = TRUE;
   }
 }
+
+
+
+/**
+ * Implementation of hook_hook_info().
+ */
+function user_hook_info() {
+  return array(
+    'user' => array(
+      'user' => array(
+        'insert' => array(
+          'runs when' => t('When a user account has been created'),
+        ),
+        'update' => array(
+          'runs when' => t('When a user account has been updated'),
+        ),
+        'delete' => array(
+          'runs when' => t('When a user account has been deleted')
+        ),
+        'login' => array(
+          'runs when' => t('When a user has logged in')
+        ),
+        'logout' => array(
+          'runs when' => t('When a user has logged out')
+        ),
+        'view' => array(
+          'runs when' => t("When a user's account information is displayed")
+        ),
+      ),
+    ),
+  );
+}
+
+/**
+ * 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(
+        '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(
+        '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', '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', 'Banned IP address %ip', array('%ip' => $_SERVER['REMOTE_ADDR']));
+}
\ No newline at end of file
