Skip to content
6 changes: 4 additions & 2 deletions ProcessMaker/Jobs/BpmnAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
use Illuminate\Support\Facades\Log;
use ProcessMaker\BpmnEngine;
use ProcessMaker\Exception\HttpABTestingException;
use ProcessMaker\Listeners\HandleRedirectListener;
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestLock;
use ProcessMaker\Services\RedirectToEventService;
use Throwable;

abstract class BpmnAction implements ShouldQueue
Expand Down Expand Up @@ -60,6 +60,7 @@ abstract class BpmnAction implements ShouldQueue
public function handle()
{
$response = null;
$redirectToEventService = app(RedirectToEventService::class);
try {
extract($this->loadContext());
$this->engine = $engine;
Expand All @@ -74,7 +75,7 @@ public function handle()
// (e.g. completed, assigned, process completed, etc)
// excluding system process (non_persistent_process)
if ($this->processId !== 'non_persistent_process') {
HandleRedirectListener::sendRedirectToEvent();
$redirectToEventService->sendRedirectToEvent();
}
} catch (HttpABTestingException $exception) {
Log::error($exception->getMessage());
Expand All @@ -87,6 +88,7 @@ public function handle()
$request->logError($exception, $element);
}
} finally {
$redirectToEventService->reset();
$this->unlock();
}

Expand Down
44 changes: 7 additions & 37 deletions ProcessMaker/Listeners/HandleRedirectListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,19 @@

namespace ProcessMaker\Listeners;

use ProcessMaker\Events\RedirectToEvent;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Services\RedirectToEventService;

class HandleRedirectListener
{
private static $processRequest = null;

protected static $redirectionMethod = '';

private static $redirectionParams = [];

protected function setRedirectTo(ProcessRequest $processRequest, string $method, ...$params): void
{
self::$processRequest = $processRequest;
self::$redirectionMethod = $method;
self::$redirectionParams = $params;
public function __construct(
private ?RedirectToEventService $redirectToEventService = null
) {
}

/**
* Reset the static state for Octane compatibility.
* This prevents data leaks between requests in long-running workers.
*/
public static function reset(): void
{
self::$processRequest = null;
self::$redirectionMethod = '';
self::$redirectionParams = [];
}

public static function sendRedirectToEvent()
protected function setRedirectTo(ProcessRequest $processRequest, string $method, ...$params): void
{
$method = self::$redirectionMethod;
$params = self::$redirectionParams;
$processRequest = self::$processRequest;

// Only get active tokens if there is a valid process request
if ($processRequest !== null) {
$params['activeTokens'] = ProcessRequest::getActiveTokens($processRequest);
$event = new RedirectToEvent($processRequest, $method, $params);
event($event);

// Clean params to prevent sending the same redirect multiple times
self::reset();
}
$this->redirectToEventService ??= app(RedirectToEventService::class);
$this->redirectToEventService->setRedirectTo($processRequest, $method, ...$params);
}
}
9 changes: 7 additions & 2 deletions ProcessMaker/Octane/ResetRequestState.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@

namespace ProcessMaker\Octane;

use ProcessMaker\Listeners\HandleRedirectListener;
use ProcessMaker\Providers\ProcessMakerServiceProvider;
use ProcessMaker\Services\RedirectToEventService;

final class ResetRequestState
{
public function __construct(
private readonly RedirectToEventService $redirectToEventService
) {
}

public function handle(): void
{
ProcessMakerServiceProvider::beginRequestTiming();
HandleRedirectListener::reset();
$this->redirectToEventService->reset();
}
}
3 changes: 3 additions & 0 deletions ProcessMaker/Providers/ProcessMakerServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
use ProcessMaker\Providers\PermissionServiceProvider;
use ProcessMaker\Repositories\SettingsConfigRepository;
use ProcessMaker\Services\ConditionalRedirectService;
use ProcessMaker\Services\RedirectToEventService;
use RuntimeException;
use Spatie\Multitenancy\Events\MadeTenantCurrentEvent;
use Spatie\Multitenancy\Events\TenantNotFoundForRequestEvent;
Expand Down Expand Up @@ -249,6 +250,8 @@ public function register(): void

$this->app->instance('tenant-resolved', false);

$this->app->scoped(RedirectToEventService::class);

/**
* Conditional Redirect Service
* This service is used to evaluate the conditional redirect property of a process request token.
Expand Down
75 changes: 75 additions & 0 deletions ProcessMaker/Services/RedirectToEventService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace ProcessMaker\Services;

use ProcessMaker\Events\RedirectToEvent;
use ProcessMaker\Models\ProcessRequest;

/**
* Collects and dispatches the redirect selected during a workflow operation.
*
* The service is container-scoped so pending redirect data is isolated to the
* current HTTP request or queue job. When several workflow events request a
* redirect, the most recently recorded redirect replaces the previous one.
*/
class RedirectToEventService
{
private ?ProcessRequest $processRequest = null;

private string $redirectionMethod = '';

private array $redirectionParams = [];

/**
* Record the redirect to dispatch when the current workflow operation ends.
*
* Calling this method again before dispatch replaces all previously pending
* redirect state, preserving the existing "last redirect wins" behavior.
*
* @param ProcessRequest $processRequest Request whose channels receive the redirect
* @param string $method Client-side redirect method to invoke
* @param mixed ...$params Ordered arguments passed to the redirect event
*/
public function setRedirectTo(ProcessRequest $processRequest, string $method, ...$params): void
{
$this->processRequest = $processRequest;
$this->redirectionMethod = $method;
$this->redirectionParams = $params;
}

/**
* Dispatch the pending redirect, including the request's active token IDs.
*
* This method is a no-op when no redirect is pending. Pending state is
* consumed before querying tokens or dispatching the event so an exception
* cannot cause stale request data to be retried or leaked into later work.
*
* @throws \Throwable If active-token retrieval or event dispatch fails
*/
public function sendRedirectToEvent(): void
{
if ($this->processRequest === null) {
return;
}

$processRequest = $this->processRequest;
$method = $this->redirectionMethod;
$params = $this->redirectionParams;

// Consume the pending redirect before doing work that may throw.
$this->reset();

$params['activeTokens'] = ProcessRequest::getActiveTokens($processRequest);
event(new RedirectToEvent($processRequest, $method, $params));
}

/**
* Discard all pending redirect state without dispatching an event.
*/
public function reset(): void
{
$this->processRequest = null;
$this->redirectionMethod = '';
$this->redirectionParams = [];
}
}
25 changes: 25 additions & 0 deletions tests/unit/ProcessMaker/Jobs/BpmnActionRedirectCleanupTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Tests\Unit\ProcessMaker\Jobs;

use Mockery;
use ProcessMaker\Jobs\BpmnAction;
use ProcessMaker\Services\RedirectToEventService;
use Tests\TestCase;

class BpmnActionRedirectCleanupTest extends TestCase
{
public function test_redirect_state_is_reset_when_bpmn_context_loading_fails(): void
{
$redirectToEventService = Mockery::mock(RedirectToEventService::class);
$redirectToEventService->shouldReceive('sendRedirectToEvent')->never();
$redirectToEventService->shouldReceive('reset')->once();
app()->instance(RedirectToEventService::class, $redirectToEventService);

$job = new class extends BpmnAction {
protected $definitionsId = -1;
};

$this->assertNull($job->handle());
}
}
Loading
Loading