diff --git a/core/modules/simpletest/tests/actions.test b/core/modules/simpletest/tests/actions.test index d115662..afcb686 100644 --- a/core/modules/simpletest/tests/actions.test +++ b/core/modules/simpletest/tests/actions.test @@ -62,3 +62,66 @@ class ActionsConfigurationTestCase extends DrupalWebTestCase { $this->assertFalse($exists, t('Make sure the action is gone from the database after being deleted.')); } } + +/** + * Test actions executing in a potential loop, and make sure they abort properly. + */ +class ActionLoopTestCase extends DrupalWebTestCase { + protected $aid; + + public static function getInfo() { + return array( + 'name' => 'Actions executing in a potentially infinite loop', + 'description' => 'Tests actions executing in a loop, and makes sure they abort properly.', + 'group' => 'Actions', + ); + } + + function setUp() { + parent::setUp('dblog', 'actions_loop_test'); + } + + /** + * Set up a loop with 3 - 12 recursions, and see if it aborts properly. + */ + function testActionLoop() { + $user = $this->drupalCreateUser(array('administer actions')); + $this->drupalLogin($user); + + $info = actions_loop_test_action_info(); + $this->aid = actions_save('actions_loop_test_log', $info['actions_loop_test_log']['type'], array(), $info['actions_loop_test_log']['label']); + + // Delete any existing watchdog messages to clear the plethora of + // "Action added" messages from when Drupal was installed. + db_delete('watchdog')->execute(); + // To prevent this test from failing when xdebug is enabled, the maximum + // recursion level should be kept low enough to prevent the xdebug + // infinite recursion protection mechanism from aborting the request. + // See http://drupal.org/node/587634. + variable_set('actions_max_stack', mt_rand(3, 12)); + $this->triggerActions(); + } + + /** + * Create an infinite loop by causing a watchdog message to be set, + * which causes the actions to be triggered again, up to actions_max_stack + * times. + */ + protected function triggerActions() { + $this->drupalGet('', array('query' => array('trigger_actions_on_watchdog' => $this->aid))); + $expected = array(); + $expected[] = 'Triggering action loop'; + for ($i = 1; $i <= variable_get('actions_max_stack', 35); $i++) { + $expected[] = "Test log #$i"; + } + $expected[] = 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.'; + + $result = db_query("SELECT message FROM {watchdog} WHERE type = 'actions_loop_test' OR type = 'actions' ORDER BY wid"); + $loop_started = FALSE; + foreach ($result as $row) { + $expected_message = array_shift($expected); + $this->assertEqual($row->message, $expected_message, t('Expected message %expected, got %message.', array('%expected' => $expected_message, '%message' => $row->message))); + } + $this->assertTrue(empty($expected), t('All expected messages found.')); + } +} diff --git a/core/modules/simpletest/tests/actions_loop_test.info b/core/modules/simpletest/tests/actions_loop_test.info new file mode 100644 index 0000000..3507511 --- /dev/null +++ b/core/modules/simpletest/tests/actions_loop_test.info @@ -0,0 +1,6 @@ +name = Actions loop test +description = Support module for action loop testing. +package = Testing +version = VERSION +core = 8.x +hidden = TRUE diff --git a/core/modules/simpletest/tests/actions_loop_test.install b/core/modules/simpletest/tests/actions_loop_test.install new file mode 100644 index 0000000..b22fd85 --- /dev/null +++ b/core/modules/simpletest/tests/actions_loop_test.install @@ -0,0 +1,11 @@ +fields(array('weight' => 1)) + ->condition('name', 'actions_loop_test') + ->execute(); +} diff --git a/core/modules/simpletest/tests/actions_loop_test.module b/core/modules/simpletest/tests/actions_loop_test.module new file mode 100644 index 0000000..66516b6 --- /dev/null +++ b/core/modules/simpletest/tests/actions_loop_test.module @@ -0,0 +1,79 @@ + 'watchdog', + ); + // Fire the actions on the associated object ($log_entry) and the context + // variable. + $aids = (array) $_GET['trigger_actions_on_watchdog']; + actions_do($aids, $log_entry, $context); +} + +/** + * Implements hook_init(). + */ +function actions_loop_test_init() { + if (!empty($_GET['trigger_actions_on_watchdog'])) { + watchdog_skip_semaphore('actions_loop_test', 'Triggering action loop'); + } +} + +/** + * Implements hook_action_info(). + */ +function actions_loop_test_action_info() { + return array( + 'actions_loop_test_log' => array( + 'label' => t('Write a message to the log.'), + 'type' => 'system', + 'configurable' => FALSE, + 'triggers' => array('any'), + ), + ); +} + +/** + * Write a message to the log. + */ +function actions_loop_test_log() { + $count = &drupal_static(__FUNCTION__, 0); + $count++; + watchdog_skip_semaphore('actions_loop_test', "Test log #$count"); +} + +/** + * Replacement of the watchdog() function that eliminates the use of semaphores + * so that we can test the abortion of an action loop. + */ +function watchdog_skip_semaphore($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE, $link = NULL) { + global $user, $base_root; + + // Prepare the fields to be logged + $log_entry = array( + 'type' => $type, + 'message' => $message, + 'variables' => $variables, + 'severity' => $severity, + 'link' => $link, + 'user' => $user, + 'request_uri' => $base_root . request_uri(), + 'referer' => $_SERVER['HTTP_REFERER'], + 'ip' => ip_address(), + 'timestamp' => REQUEST_TIME, + ); + + // Call the logging hooks to log/process the message + foreach (module_implements('watchdog') as $module) { + module_invoke($module, 'watchdog', $log_entry); + } +} diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 21ad1fd..e577bb6 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -3061,7 +3061,7 @@ function system_actions_configure($form, &$form_state, $action = NULL) { '#title' => t('Label'), '#default_value' => $edit['actions_label'], '#maxlength' => '255', - '#description' => t('A unique label for this advanced action. This label will be displayed in the interface of modules that integrate with actions, such as Trigger module.'), + '#description' => t('A unique label for this advanced action. This label will be displayed in the interface of modules that integrate with actions.'), '#weight' => -10 ); $action_form = $function . '_form'; diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 27c79c7..360bfea 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -3384,9 +3384,7 @@ function hook_file_mimetype_mapping_alter(&$mapping) { * Declares information about actions. * * Any module can define actions, and then call actions_do() to make those - * actions happen in response to events. The trigger module provides a user - * interface for associating actions with module-defined triggers, and it makes - * sure the core triggers fire off actions when their events happen. + * actions happen in response to events. * * An action consists of two or three parts: * - an action definition (returned by this hook) @@ -3417,14 +3415,9 @@ function hook_file_mimetype_mapping_alter(&$mapping) { * declare support for any trigger by returning array('any') for this value. * - 'behavior': (optional) A machine-readable array of behaviors of this * action, used to signal additionally required actions that may need to be - * triggered. Currently recognized behaviors by Trigger module: - * - 'changes_property': If an action with this behavior is assigned to a - * trigger other than a "presave" hook, any save actions also assigned to - * this trigger are moved later in the list. If no save action is present, - * one will be added. - * Modules that are processing actions (like Trigger module) should take - * special care for the "presave" hook, in which case a dependent "save" - * action should NOT be invoked. + * triggered. Modules that are processing actions should take special care + * for the "presave" hook, in which case a dependent "save" action should + * NOT be invoked. * * @ingroup actions */ @@ -3470,8 +3463,6 @@ function hook_actions_delete($aid) { * * Called by actions_list() to allow modules to alter the return values from * implementations of hook_action_info(). - * - * @see trigger_example_action_info_alter() */ function hook_action_info_alter(&$actions) { $actions['node_unpublish_action']['label'] = t('Unpublish and remove from public view.');