diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index d96db0cceb3..5433a5ff236 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -1101,7 +1101,8 @@ class DrupalWebTestCase extends DrupalTestCase { // Make a request to the logout page, and redirect to the user page, the // idea being if you were properly logged out you should be seeing a login // screen. - $this->drupalGet('user/logout', array('query' => array('destination' => 'user'))); + $this->drupalGet('user/logout'); + $this->drupalGet('user'); $pass = $this->assertField('name', t('Username field found.'), t('Logout')); $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout')); @@ -2964,7 +2965,9 @@ class DrupalWebTestCase extends DrupalTestCase { } /** - * Assert that the most recently sent e-mail message has a field with the given value. + * Asserts that the most recently sent e-mail message has the given value. + * + * The field in $name must have the content described in $value. * * @param $name * Name of field or message property to assert. Examples: subject, body, id, ... @@ -2972,6 +2975,7 @@ class DrupalWebTestCase extends DrupalTestCase { * Value of the field to assert. * @param $message * Message to display. + * * @return * TRUE on pass, FALSE on fail. */ @@ -2982,13 +2986,76 @@ class DrupalWebTestCase extends DrupalTestCase { } /** - * Log verbose message in a text file. + * Asserts that the most recently sent e-mail message has the string in it. + * + * @param $field_name + * Name of field or message property to assert: subject, body, id, ... + * @param $string + * String to search for. + * @param $email_depth + * Number of emails to search for string, starting with most recent. + * + * @return + * TRUE on pass, FALSE on fail. + */ + protected function assertMailString($field_name, $string, $email_depth) { + $mails = $this->drupalGetMails(); + $string_found = FALSE; + for ($i = sizeof($mails) -1; $i >= sizeof($mails) - $email_depth && $i >= 0; $i--) { + $mail = $mails[$i]; + // Normalize whitespace, as we don't know what the mail system might have + // done. Any run of whitespace becomes a single space. + $normalized_mail = preg_replace('/\s+/', ' ', $mail[$field_name]); + $normalized_string = preg_replace('/\s+/', ' ', $string); + $string_found = (FALSE !== strpos($normalized_mail, $normalized_string)); + if ($string_found) { + break; + } + } + return $this->assertTrue($string_found, t('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $string))); + } + + /** + * Asserts that the most recently sent e-mail message has the pattern in it. + * + * @param $field_name + * Name of field or message property to assert: subject, body, id, ... + * @param $regex + * Pattern to search for. + * + * @return + * TRUE on pass, FALSE on fail. + */ + protected function assertMailPattern($field_name, $regex, $message) { + $mails = $this->drupalGetMails(); + $mail = end($mails); + $regex_found = preg_match("/$regex/", $mail[$field_name]); + return $this->assertTrue($regex_found, t('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $regex))); + } + + /** + * Outputs to verbose the most recent $count emails sent. + * + * @param $count + * Optional number of emails to output. + */ + protected function verboseEmail($count = 1) { + $mails = $this->drupalGetMails(); + for ($i = sizeof($mails) -1; $i >= sizeof($mails) - $count && $i >= 0; $i--) { + $mail = $mails[$i]; + $this->verbose(t('Email:') . '
' . print_r($mail, TRUE) . '
'); + } + } + + /** + * Logs verbose message in a text file. * * The a link to the vebose message will be placed in the test results via * as a passing assertion with the text '[verbose message]'. * * @param $message * The verbose message to be stored. + * * @see simpletest_verbose() */ protected function verbose($message) { @@ -3001,7 +3068,7 @@ class DrupalWebTestCase extends DrupalTestCase { } /** - * Log verbose message in a text file. + * Logs verbose message in a text file. * * If verbose mode is enabled then page requests will be dumped to a file and * presented on the test result screen. The messages will be placed in a file @@ -3013,8 +3080,10 @@ class DrupalWebTestCase extends DrupalTestCase { * The original file directory, before it was changed for testing purposes. * @param $test_class * The active test case class. + * * @return * The ID of the message to be placed in related assertion messages. + * * @see DrupalTestCase->originalFileDirectory * @see DrupalWebTestCase->verbose() */ diff --git a/modules/system/system.module b/modules/system/system.module index 7ebd8004bc7..4f61584aeb9 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -2861,7 +2861,7 @@ function system_action_info() { 'type' => 'user', 'label' => t('Ban IP address of current user'), 'configurable' => FALSE, - 'triggers' => array(), + 'triggers' => array('any'), ), 'system_goto_action' => array( 'type' => 'system', diff --git a/modules/trigger/tests/trigger_test.module b/modules/trigger/tests/trigger_test.module index 9f0eeadd3e3..06428725186 100644 --- a/modules/trigger/tests/trigger_test.module +++ b/modules/trigger/tests/trigger_test.module @@ -35,7 +35,6 @@ function trigger_test_action_info() { 'comment_insert', 'comment_update', 'comment_delete', - 'user_presave', 'user_insert', 'user_update', 'user_delete', diff --git a/modules/trigger/trigger.module b/modules/trigger/trigger.module index 93ed2485c21..91c36c65df3 100644 --- a/modules/trigger/trigger.module +++ b/modules/trigger/trigger.module @@ -145,9 +145,6 @@ function trigger_trigger_info() { ), ), 'user' => array( - 'user_presave' => array( - 'label' => t('When either creating a new user account or updating an existing'), - ), 'user_insert' => array( 'label' => t('After creating a new user account'), ), @@ -488,17 +485,10 @@ function trigger_user_login(&$edit, $account, $category) { * Implements hook_user_logout(). */ function trigger_user_logout($account) { - $edit = NULL; + $edit = array(); _trigger_user('user_logout', $edit, $account); } -/** - * Implements hook_user_presave(). - */ -function trigger_user_presave(&$edit, $account, $category) { - _trigger_user('user_presave', $edit, $account, $category); -} - /** * Implements hook_user_insert(). */ @@ -528,7 +518,8 @@ function trigger_user_cancel($edit, $account, $method) { * Implements hook_user_delete(). */ function trigger_user_delete($account) { - _trigger_user('user_delete', $edit, $account, $method); + $edit = array(); + _trigger_user('user_delete', $edit, $account, NULL); } /** @@ -557,7 +548,7 @@ function _trigger_user($hook, &$edit, $account, $category = NULL) { if (!isset($objects[$type])) { $objects[$type] = _trigger_normalize_user_context($type, $account); } - $context['account'] = $account; + $context['user'] = $account; actions_do($aid, $objects[$type], $context); } else { diff --git a/modules/trigger/trigger.test b/modules/trigger/trigger.test index fd4e2be42c9..461686993c9 100644 --- a/modules/trigger/trigger.test +++ b/modules/trigger/trigger.test @@ -2,7 +2,7 @@ // $Id$ /** - * Class with common helper methods. + * Provides common helper methods. */ class TriggerWebTestCase extends DrupalWebTestCase { @@ -14,6 +14,7 @@ class TriggerWebTestCase extends DrupalWebTestCase { * @param $edit * The $edit array for the form to be used to configure. * Example members would be 'actions_label' (always), 'message', etc. + * * @return * the aid (action id) of the configured action, or FALSE if none. */ @@ -30,7 +31,7 @@ class TriggerWebTestCase extends DrupalWebTestCase { } /** - * Test node triggers. + * Provides tests for node triggers. */ class TriggerContentTestCase extends TriggerWebTestCase { var $_cleanup_roles = array(); @@ -49,7 +50,9 @@ class TriggerContentTestCase extends TriggerWebTestCase { } /** - * Various tests, all in one function to assure they happen in the right order. + * Tests several content-oriented trigger issues. + * + * These are in one function to assure they happen in the right order. */ function testActionsContent() { global $user; @@ -98,8 +101,10 @@ class TriggerContentTestCase extends TriggerWebTestCase { } /** - * Test that node actions are fired for each node individually if acting on - * multiple nodes. + * Tests multiple node actions. + * + * Verifies that node actions are fired for each node individually, if acting + * on multiple nodes. */ function testActionContentMultiple() { // Assign an action to the node save/update trigger. @@ -127,10 +132,13 @@ class TriggerContentTestCase extends TriggerWebTestCase { } /** - * Helper function for testActionsContent(): returns some info about each of the content actions. + * Returns some info about each of the content actions. + * + * This is helper function for testActionsContent(). * * @param $action * The name of the action to return info about. + * * @return * An associative array of info about the action. */ @@ -172,7 +180,7 @@ class TriggerContentTestCase extends TriggerWebTestCase { } /** - * Test cron trigger. + * Tests cron trigger. */ class TriggerCronTestCase extends TriggerWebTestCase { public static function getInfo() { @@ -188,7 +196,7 @@ class TriggerCronTestCase extends TriggerWebTestCase { } /** - * Test assigning multiple actions to the cron trigger. + * Tests assigning multiple actions to the cron trigger. * * This test ensures that both simple and multiple complex actions * succeed properly. This is done in the cron trigger test because @@ -239,7 +247,296 @@ class TriggerCronTestCase extends TriggerWebTestCase { } /** - * Test other triggers. + * Provides a base class with trigger assignments and test comparisons. + */ +class TriggerActionTestCase extends TriggerWebTestCase { + + function setUp() { + parent::setUp('trigger'); + } + + /** + * Creates a message with tokens. + * + * @param $trigger + * + * @return + * A message with embedded tokens. + */ + function generateMessageWithTokens($trigger) { + // Note that subject is limited to 254 characters in action configuration. + $message = t('Action was triggered by trigger @trigger user:name=[user:name] user:uid=[user:uid] user:mail=[user:mail] user:url=[user:url] user:edit-url=[user:edit-url] user:created=[user:created]', + array('@trigger' => $trigger)); + return trim($message); + } + + /** + * Generates a comparison message to match the pre-token-replaced message. + * + * @param $trigger + * Trigger, like 'user_login'. + * @param $account + * Associated user account. + * + * @return + * The token-replaced equivalent message. This does not use token + * functionality. + * + * @see generateMessageWithTokens() + */ + function generateTokenExpandedComparison($trigger, $account) { + // Note that user:last-login was omitted because it changes and can't + // be properly verified. + $message = t('Action was triggered by trigger @trigger user:name=@username user:uid=@uid user:mail=@mail user:url=@user_url user:edit-url=@user_edit_url user:created=@user_created', + array( + '@trigger' => $trigger, + '@username' => $account->name, + '@uid' => !empty($account->uid) ? $account->uid : t('not yet assigned'), + '@mail' => $account->mail, + '@user_url' => !empty($account->uid) ? url("user/$account->uid", array('absolute' => TRUE)) : t('not yet assigned'), + '@user_edit_url' => !empty($account->uid) ? url("user/$account->uid/edit", array('absolute' => TRUE)) : t('not yet assigned'), + '@user_created' => isset($account->created) ? format_date($account->created, 'medium') : t('not yet created'), + ) + ); + return trim($message); + } + + + /** + * Assigns a simple (non-configurable) action to a trigger. + * + * @param $trigger + * The trigger to assign to, like 'user_login'. + * @param $action + * The simple action to be assigned, like 'comment_insert'. + */ + function assignSimpleAction($trigger, $action) { + $form_name = "trigger_{$trigger}_assign_form"; + $form_html_id = strtr($form_name, '_', '-'); + $edit = array('aid' => drupal_hash_base64($action)); + $trigger_type = preg_replace('/_.*/', '', $trigger); + $this->drupalPost("admin/structure/trigger/$trigger_type", $edit, t('Assign'), array(), array(), $form_html_id); + $actions = trigger_get_assigned_actions($trigger); + $this->assertTrue(!empty($actions[$action]), t('Simple action @action assigned to trigger @trigger', array('@action' => $action, '@trigger' => $trigger))); + } + + /** + * Assigns a system message action to the passed-in trigger. + * + * @param $trigger + * For example, 'user_login' + */ + function assignSystemMessageAction($trigger) { + $form_name = "trigger_{$trigger}_assign_form"; + $form_html_id = strtr($form_name, '_', '-'); + // Assign a configurable action 'System message' to the passed trigger. + $action_edit = array( + 'actions_label' => $trigger . "_system_message_action_" . $this->randomName(16), + 'message' => $this->generateMessageWithTokens($trigger), + ); + + // Configure an advanced action that we can assign. + $aid = $this->configureAdvancedAction('system_message_action', $action_edit); + + $edit = array('aid' => drupal_hash_base64($aid)); + $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), $form_html_id); + } + + + /** + * Assigns a system_send_email_action to the passed-in trigger. + * + * @param $trigger + * For example, 'user_login' + */ + function assignSystemEmailAction($trigger) { + $form_name = "trigger_{$trigger}_assign_form"; + $form_html_id = strtr($form_name, '_', '-'); + + $message = $this->generateMessageWithTokens($trigger); + // Assign a configurable action 'System message' to the passed trigger. + $action_edit = array( + // 'actions_label' => $trigger . "_system_send_message_action_" . $this->randomName(16), + 'actions_label' => $trigger . "_system_send_email_action", + 'recipient' => '[user:mail]', + 'subject' => $message, + 'message' => $message, + ); + + // Configure an advanced action that we can assign. + $aid = $this->configureAdvancedAction('system_send_email_action', $action_edit); + + $edit = array('aid' => drupal_hash_base64($aid)); + $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), $form_html_id); + } + + /** + * Asserts correct token replacement in both system message and email. + * + * @param $trigger + * A trigger like 'user_login'. + * @param $account + * The user account which triggered the action. + * @param $email_depth + * Number of emails to scan, starting with most recent. + */ + function assertSystemMessageAndEmailTokenReplacement($trigger, $account, $email_depth = 1) { + $this->assertSystemMessageTokenReplacement($trigger, $account); + $this->assertSystemEmailTokenReplacement($trigger, $account, $email_depth); + } + + /** + * Asserts correct token replacement for the given trigger and account. + * + * @param $trigger + * A trigger like 'user_login'. + * @param $account + * The user account which triggered the action. + */ + function assertSystemMessageTokenReplacement($trigger, $account) { + $expected = $this->generateTokenExpandedComparison($trigger, $account); + $this->assertText($expected, + t('Expected system message to contain token-replaced text "@expected" found in configured system message action', array('@expected' => $expected )) ); + } + + + /** + * Asserts correct token replacement for the given trigger and account. + * + * @param $trigger + * A trigger like 'user_login'. + * @param $account + * The user account which triggered the action. + * @param $email_depth + * Number of emails to scan, starting with most recent. + */ + function assertSystemEmailTokenReplacement($trigger, $account, $email_depth = 1) { + $this->verboseEmail($email_depth); + $expected = $this->generateTokenExpandedComparison($trigger, $account); + $this->assertMailString('subject', $expected, $email_depth); + $this->assertMailString('body', $expected, $email_depth); + $this->assertMail('to', $account->mail, t('Mail sent to correct destination')); + } +} + +/** + * Tests token substitution in trigger actions. + * + * This tests nearly every permutation of user triggers with system actions + * and checks the token replacement. + */ +class TriggerUserTokenTestCase extends TriggerActionTestCase { + public static function getInfo() { + return array( + 'name' => 'Test user triggers', + 'description' => 'Test user triggers and system actions with token replacement.', + 'group' => 'Trigger', + ); + } + + + /** + * Tests a variety of token replacements in actions. + */ + function testUserTriggerTokenReplacement() { + $test_user = $this->drupalCreateUser(array('administer actions', 'administer users', 'change own username', 'access user profiles')); + $this->drupalLogin($test_user); + + $triggers = array('user_login', 'user_insert', 'user_update', 'user_delete', 'user_logout', 'user_view'); + foreach ($triggers as $trigger) { + $this->assignSystemMessageAction($trigger); + $this->assignSystemEmailAction($trigger); + } + + $this->drupalLogout(); + $this->assertSystemEmailTokenReplacement('user_logout', $test_user); + + $this->drupalLogin($test_user); + $this->assertSystemMessageAndEmailTokenReplacement('user_login', $test_user, 2); + $this->assertSystemMessageAndEmailTokenReplacement('user_view', $test_user, 2); + + $this->drupalPost("user/{$test_user->uid}/edit", array('name' => $test_user->name . '_changed'), t('Save')); + $test_user->name .= '_changed'; // Since we just changed it. + $this->assertSystemMessageAndEmailTokenReplacement('user_update', $test_user, 2); + + $this->drupalGet('user'); + $this->assertSystemMessageAndEmailTokenReplacement('user_view', $test_user); + + $new_user = $this->drupalCreateUser(array('administer actions', 'administer users', 'cancel account', 'access administration pages')); + $this->assertSystemEmailTokenReplacement('user_insert', $new_user); + + $this->drupalLogin($new_user); + $user_to_delete = $this->drupalCreateUser(array('access content')); + variable_set('user_cancel_method', 'user_cancel_delete'); + + $this->drupalPost("user/{$user_to_delete->uid}/cancel", array(), t('Cancel account')); + $this->assertSystemMessageAndEmailTokenReplacement('user_delete', $user_to_delete); + } + + +} + +/** + * Tests token substitution in trigger actions. + * + * This tests nearly every permutation of user triggers with system actions + * and checks the token replacement. + */ +class TriggerUserActionTestCase extends TriggerActionTestCase { + public static function getInfo() { + return array( + 'name' => 'Test user actions', + 'description' => 'Test user actions.', + 'group' => 'Trigger', + ); + } + + /** + * Tests user action assignment and execution. + */ + function testUserActionAssignmentExecution() { + $test_user = $this->drupalCreateUser(array('administer actions', 'create article content', 'access comments', 'administer comments', 'post comments without approval', 'edit own comments')); + $this->drupalLogin($test_user); + + $triggers = array('comment_presave', 'comment_insert', 'comment_update'); + // system_block_ip_action is difficult to test without ruining the test. + $actions = array('user_block_user_action'); + foreach ($triggers as $trigger) { + foreach ($actions as $action) { + $this->assignSimpleAction($trigger, $action); + } + } + + $node = $this->drupalCreateNode(array('type' => 'article')); + $this->drupalPost("node/{$node->nid}", array('comment_body[und][0][value]' => t("my comment"), 'subject' => t("my comment subject")), t('Save')); + // Posting a comment should have blocked this user. + $account = user_load($test_user->uid, TRUE); + $this->assertTrue($account->status == 0, t('Account is blocked')); + $comment_author_uid = $account->uid; + // Now rehabilitate the comment author so it can be be blocked again when + // the comment is updated. + user_save($account, array('status' => TRUE)); + + $test_user = $this->drupalCreateUser(array('administer actions', 'create article content', 'access comments', 'administer comments', 'post comments without approval', 'edit own comments')); + $this->drupalLogin($test_user); + + // Our original comment will have been comment 1. + $this->drupalPost("comment/1/edit", array('comment_body[und][0][value]' => t("my comment, updated"), 'subject' => t("my comment subject")), t('Save')); + $comment_author_account = user_load($comment_author_uid, TRUE); + $this->assertTrue($comment_author_account->status == 0, t('Comment author account (uid=@uid) is blocked after update to comment', array('@uid' => $comment_author_uid))); + + // Verify that the comment was updated. + $test_user = $this->drupalCreateUser(array('administer actions', 'create article content', 'access comments', 'administer comments', 'post comments without approval', 'edit own comments')); + $this->drupalLogin($test_user); + + $this->drupalGet("node/$node->nid"); + $this->assertText(t("my comment, updated")); + $this->verboseEmail(); + } +} + +/** + * Tests other triggers. */ class TriggerOtherTestCase extends TriggerWebTestCase { var $_cleanup_roles = array(); @@ -258,7 +555,7 @@ class TriggerOtherTestCase extends TriggerWebTestCase { } /** - * Test triggering on user create and user login. + * Tests triggering on user create and user login. */ function testActionsUser() { // Assign an action to the create user trigger. @@ -315,7 +612,7 @@ class TriggerOtherTestCase extends TriggerWebTestCase { } /** - * Test triggering on comment save. + * Tests triggering on comment save. */ function testActionsComment() { // Assign an action to the comment save trigger. @@ -344,7 +641,7 @@ class TriggerOtherTestCase extends TriggerWebTestCase { } /** - * Test triggering on taxonomy new term. + * Tests triggering on taxonomy new term. */ function testActionsTaxonomy() { // Assign an action to the taxonomy term save trigger. @@ -382,7 +679,7 @@ class TriggerOtherTestCase extends TriggerWebTestCase { } /** - * Test that orphaned actions are properly handled. + * Tests that orphaned actions are properly handled. */ class TriggerOrphanedActionsTestCase extends DrupalWebTestCase { @@ -399,7 +696,7 @@ class TriggerOrphanedActionsTestCase extends DrupalWebTestCase { } /** - * Test logic around orphaned actions. + * Tests logic around orphaned actions. */ function testActionsOrphaned() { $action = 'trigger_test_generic_any_action'; diff --git a/modules/user/user.module b/modules/user/user.module index bac1b767cd0..0fa22cb09f0 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -3373,7 +3373,7 @@ function user_action_info() { 'label' => t('Block current user'), 'type' => 'user', 'configurable' => FALSE, - 'triggers' => array(), + 'triggers' => array('any'), ), ); } @@ -3384,22 +3384,25 @@ function user_action_info() { * @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 { - global $user; - $uid = $user->uid; + $uid = $GLOBALS['user']->uid; } db_update('users') ->fields(array('status' => 0)) ->condition('uid', $uid) ->execute(); drupal_session_destroy_uid($uid); - watchdog('action', 'Blocked user %name.', array('%name' => $user->name)); + $account = user_load($uid); + watchdog('action', 'Blocked user %name.', array('%name' => $account->name)); } /** diff --git a/modules/user/user.tokens.inc b/modules/user/user.tokens.inc index 70d4914d529..0c5f88b840b 100644 --- a/modules/user/user.tokens.inc +++ b/modules/user/user.tokens.inc @@ -81,7 +81,8 @@ function user_tokens($type, $tokens, array $data = array(), array $options = arr switch ($name) { // Basic user account information. case 'uid': - $replacements[$original] = $account->uid; + // In the case of hook user_presave uid is not set yet. + $replacements[$original] = !empty($account->uid) ? $account->uid : t('not yet assigned'); break; case 'name': @@ -94,20 +95,21 @@ function user_tokens($type, $tokens, array $data = array(), array $options = arr break; case 'url': - $replacements[$original] = url("user/$account->uid", $url_options); + $replacements[$original] = !empty($account->uid) ? url("user/$account->uid", $url_options) : t('not yet assigned'); break; case 'edit-url': - $replacements[$original] = url("user/$account->uid/edit", $url_options); + $replacements[$original] = !empty($account->uid) ? url("user/$account->uid/edit", $url_options) : t('not yet assigned'); break; // These tokens are default variations on the chained tokens handled below. case 'last-login': - $replacements[$original] = format_date($account->login, 'medium', '', NULL, $language_code); + $replacements[$original] = !empty($account->login) ? format_date($account->login, 'medium', '', NULL, $language_code) : t('never'); break; case 'created': - $replacements[$original] = format_date($account->created, 'medium', '', NULL, $language_code); + // In the case of user_presave the created date may not yet be set. + $replacements[$original] = !empty($account->created) ? format_date($account->created, 'medium', '', NULL, $language_code) : t('not yet created'); break; } }