Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions ProcessMaker/Models/ProcessRequestToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface;
use ProcessMaker\Nayra\Managers\WorkflowManagerDefault;
use ProcessMaker\Nayra\Storage\BpmnDocument;
use ProcessMaker\Managers\DataManager;
use ProcessMaker\Models\MustacheExpressionEvaluator;
use ProcessMaker\Notifications\ActivityActivatedNotification;
use ProcessMaker\Notifications\TaskReassignmentNotification;
use ProcessMaker\Query\Expression;
Expand Down Expand Up @@ -1440,6 +1442,49 @@ public function reassign($toUserId, User $requestingUser, $comments = '')
}
}

/**
* Build context for Mustache (end event external URL). Same as scripts/screens: _user, _request, process data, APP_URL.
*/
private function getElementDestinationMustacheContext(): array
{
try {
$context = (new DataManager())->getData($this);
} catch (Throwable $e) {
$request = $this->processRequest;
$context = array_merge($request->data ?? [], $request ? (new DataManager())->updateRequestMagicVariable([], $request) : []);
$user = $this->user ?? \Illuminate\Support\Facades\Auth::user();
if ($user) {
$userData = $user->attributesToArray();
unset($userData['remember_token']);
$context['_user'] = $userData;
}
}

$context['APP_URL'] = config('app.url');

// Normalize to plain arrays/scalars so Mustache resolves all keys (common PHP idiom)
$normalized = json_decode(json_encode($context), true);

return is_array($normalized) ? $normalized : [];
}

/**
* Resolve Mustache in end event external URL. FEEL is not supported here; use Mustache only.
* Context: APP_URL, _request, _user, process variables (same as getElementDestinationMustacheContext).
*
* Example (Mustache):
* {{APP_URL}}/admin/users/{{_request.id}}/edit -> https://example.com/admin/users/123/edit
* {{APP_URL}}/webentry/{{_request.id}} -> https://example.com/webentry/123
* {{APP_URL}}/path/{{my_process_var}} -> uses process variable my_process_var
*/
private function resolveElementDestinationUrl(string $url): string
{
$url = html_entity_decode($url, ENT_QUOTES | ENT_HTML401, 'UTF-8');
$context = $this->getElementDestinationMustacheContext();

return (new MustacheExpressionEvaluator())->render($url, $context);
}

/**
* Determines the destination based on the type of element destination property
*
Expand Down Expand Up @@ -1481,6 +1526,9 @@ private function getElementDestination($elementDestinationType, $elementDestinat
$elementDestination = $elementDestinationProp['value']['url'] ?? null;
}
}
if ($elementDestinationType === 'externalURL' && is_string($elementDestination) && $elementDestination !== '') {
$elementDestination = $this->resolveElementDestinationUrl($elementDestination);
}
break;
case 'taskList':
$elementDestination = route('tasks.index');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

namespace Tests\Unit\ProcessMaker\Models;

use ProcessMaker\Models\Process;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
use ProcessMaker\Models\User;
use ReflectionClass;
use Tests\TestCase;

class ProcessRequestTokenElementDestinationTest extends TestCase
{
/**
* Invoke a private method on an object.
*/
private function invokePrivateMethod(object $object, string $methodName, array $args = []): mixed
{
$reflection = new ReflectionClass($object);
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);

return $method->invokeArgs($object, $args);
}

/**
* Test getElementDestinationMustacheContext returns context with APP_URL, _request, _user and process data.
*/
public function testGetElementDestinationMustacheContextReturnsExpectedKeys(): void
{
$user = User::factory()->create(['status' => 'ACTIVE']);
$process = Process::factory()->create();
$request = ProcessRequest::factory()->create([
'process_id' => $process->id,
'user_id' => $user->id,
'data' => ['processVar' => 'value123'],
]);

$token = ProcessRequestToken::factory()->create([
'process_id' => $process->id,
'process_request_id' => $request->id,
'user_id' => $user->id,
'element_id' => 'end_1',
'element_type' => 'end_event',
]);

$context = $this->invokePrivateMethod($token, 'getElementDestinationMustacheContext');

$this->assertIsArray($context);
$this->assertArrayHasKey('APP_URL', $context);
$this->assertSame(config('app.url'), $context['APP_URL']);
$this->assertArrayHasKey('_request', $context);
$this->assertIsArray($context['_request']);
$this->assertArrayHasKey('id', $context['_request']);
$this->assertSame((string) $request->id, (string) $context['_request']['id']);
$this->assertArrayHasKey('case_number', $context['_request']);
$this->assertArrayHasKey('_user', $context);
$this->assertIsArray($context['_user']);
$this->assertArrayHasKey('id', $context['_user']);
$this->assertArrayHasKey('processVar', $context);
$this->assertSame('value123', $context['processVar']);
}

/**
* Test resolveElementDestinationUrl resolves Mustache placeholders APP_URL and _request.id.
*/
public function testResolveElementDestinationUrlResolvesMustache(): void
{
$user = User::factory()->create(['status' => 'ACTIVE']);
$process = Process::factory()->create();
$request = ProcessRequest::factory()->create([
'process_id' => $process->id,
'user_id' => $user->id,
'data' => [],
]);

$token = ProcessRequestToken::factory()->create([
'process_id' => $process->id,
'process_request_id' => $request->id,
'user_id' => $user->id,
'element_id' => 'end_1',
'element_type' => 'end_event',
]);

$urlTemplate = '{{APP_URL}}/path/{{_request.id}}';
$resolved = $this->invokePrivateMethod($token, 'resolveElementDestinationUrl', [$urlTemplate]);

$expectedUrl = config('app.url') . '/path/' . $request->id;
$this->assertSame($expectedUrl, $resolved);
}

/**
* Test resolveElementDestinationUrl resolves process variable in URL.
*/
public function testResolveElementDestinationUrlResolvesProcessVariable(): void
{
$user = User::factory()->create(['status' => 'ACTIVE']);
$process = Process::factory()->create();
$request = ProcessRequest::factory()->create([
'process_id' => $process->id,
'user_id' => $user->id,
'data' => ['segment' => 'admin'],
]);

$token = ProcessRequestToken::factory()->create([
'process_id' => $process->id,
'process_request_id' => $request->id,
'user_id' => $user->id,
'element_id' => 'end_1',
'element_type' => 'end_event',
]);

$urlTemplate = '{{APP_URL}}/{{segment}}/users';
$resolved = $this->invokePrivateMethod($token, 'resolveElementDestinationUrl', [$urlTemplate]);

$this->assertSame(config('app.url') . '/admin/users', $resolved);
}

/**
* Test resolveElementDestinationUrl decodes HTML entities in template.
*/
public function testResolveElementDestinationUrlDecodesHtmlEntities(): void
{
$user = User::factory()->create(['status' => 'ACTIVE']);
$process = Process::factory()->create();
$request = ProcessRequest::factory()->create([
'process_id' => $process->id,
'user_id' => $user->id,
'data' => [],
]);

$token = ProcessRequestToken::factory()->create([
'process_id' => $process->id,
'process_request_id' => $request->id,
'user_id' => $user->id,
'element_id' => 'end_1',
'element_type' => 'end_event',
]);

$urlWithEntities = '&#104;&#116;&#116;&#112;&#115;&#58;//example.com/{{_request.id}}';
$resolved = $this->invokePrivateMethod($token, 'resolveElementDestinationUrl', [$urlWithEntities]);

$this->assertStringContainsString((string) $request->id, $resolved);
$this->assertStringContainsString('https://example.com/', $resolved);
}
}