diff --git a/src/Context/ExecutionState.php b/src/Context/ExecutionState.php index 3600645e..fb3f4a09 100644 --- a/src/Context/ExecutionState.php +++ b/src/Context/ExecutionState.php @@ -23,6 +23,9 @@ class ExecutionState implements ExecutionStateInterface { /** * Globally keeps the ids of rules blocked due to recursion prevention. * + * Keyed by Rules config entity UUIDs, the values are the context values keyed + * by context name. + * * @var array * * @todo Implement recursion prevention from D7. @@ -173,6 +176,38 @@ class ExecutionState implements ExecutionStateInterface { } } + /** + * Checks if a rules configuration is blocked to avoid recursion. + * + * @param string $config_uuid + * The UUID of the Rules config entity to check. + * @param array $context_values + * The context values the rule will be executed with, keyed by context name. + * + * @return bool + * TRUE if the Rules config entity is blocked from being executed, FALSE + * otherwise. + */ + public static function isBlocked($config_uuid, array $context_values) { + if (isset(static::$blocked[$config_uuid]) && static::$blocked[$config_uuid] == $context_values) { + return TRUE; + } + return FALSE; + } + + /** + * Marks a Rules configuration entity as blocked to prevent recursion. + * + * @param string $config_uuid + * The UUID of the Rules config entity to block. + * @param array $context_values + * The list of context values the Rules config is invoked with, keyed by + * context name. + */ + public static function block($config_uuid, array $context_values) { + static::$blocked[$config_uuid] = $context_values; + } + /** * {@inheritdoc} */ diff --git a/src/EventSubscriber/GenericEventSubscriber.php b/src/EventSubscriber/GenericEventSubscriber.php index d82563c3..fc43d3e2 100644 --- a/src/EventSubscriber/GenericEventSubscriber.php +++ b/src/EventSubscriber/GenericEventSubscriber.php @@ -118,6 +118,7 @@ class GenericEventSubscriber implements EventSubscriberInterface { // Setup the execution state. $state = ExecutionState::create(); + $context_values = []; foreach ($event_definition['context_definitions'] as $context_name => $context_definition) { // If this is a GenericEvent, get the context for the rule from the event // arguments. @@ -135,6 +136,7 @@ class GenericEventSubscriber implements EventSubscriberInterface { $context_definition, $value ); + $context_values[$context_name] = $value; } $components = $this->componentRepository->getMultiple($triggered_events, 'rules_event'); @@ -144,7 +146,10 @@ class GenericEventSubscriber implements EventSubscriberInterface { 'element' => NULL, 'scope' => TRUE, ]); - $component->getExpression()->executeWithState($state); + if (!ExecutionState::isBlocked($component->uuid(), $context_values)) { + ExecutionState::block($component->uuid(), $context_values); + $component->getExpression()->executeWithState($state); + } $this->rulesDebugLogger->info('Finished reacting on event %label.', [ '%label' => $event_definition['label'], 'element' => NULL, diff --git a/tests/src/Functional/ConfigureAndExecuteTest.php b/tests/src/Functional/ConfigureAndExecuteTest.php index a6c268d9..bf325d14 100644 --- a/tests/src/Functional/ConfigureAndExecuteTest.php +++ b/tests/src/Functional/ConfigureAndExecuteTest.php @@ -261,4 +261,53 @@ class ConfigureAndExecuteTest extends RulesBrowserTestBase { $assert->elementExists('xpath', '//input[@id="edit-context-definitions-type-setting" and not(contains(@class, "rules-autocomplete"))]'); } + /** + * Tests that the recursion prevention works when a node is updated. + */ + public function testRecursionPrevention() { + $account = $this->drupalCreateUser([ + 'create article content', + 'edit own article content', + 'administer rules', + 'administer site configuration', + ]); + $this->drupalLogin($account); + + $this->drupalGet('admin/config/workflow/rules'); + + // Set up a rule that will change the title on update which will trigger + // another update. + $this->clickLink('Add reaction rule'); + + $this->fillField('Label', 'Test rule'); + $this->fillField('Machine-readable name', 'test_rule'); + $this->fillField('React on event', 'rules_entity_update:node'); + $this->pressButton('Save'); + + $this->clickLink('Add action'); + $this->fillField('Action', 'rules_data_set'); + $this->pressButton('Continue'); + + $this->fillField('context_defintions[data][setting]', 'node.title.value'); + $this->fillField('context_defintions[value][setting]', 'new title'); + $this->pressButton('Save'); + + // One more save to permanently store the rule. + $this->pressButton('Save'); + + // Add a node now. + $this->drupalGet('node/add/article'); + $this->fillField('Title', 'Test title'); + $this->pressButton('Save'); + + // Edit the node now which should trigger the rule. + $this->drupalGet('node/1/edit'); + $this->fillField('Title', 'Whatever'); + $this->pressButton('Save'); + + /** @var \Drupal\Tests\WebAssert $assert */ + $assert = $this->assertSession(); + $assert->pageTextContains('Article new title has been updated.'); + } + }