Skip to content

Commit d288caa

Browse files
authored
Merge pull request #8728 from ProcessMaker/feature/FOUR-29250
FOUR-29250 End Event – External URL with Mustache Support
2 parents 36cc698 + 2cbf37b commit d288caa

3 files changed

Lines changed: 403 additions & 0 deletions

File tree

ProcessMaker/Models/ProcessRequestToken.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use ProcessMaker\Events\ActivityAssigned;
1818
use ProcessMaker\Events\ActivityReassignment;
1919
use ProcessMaker\Facades\WorkflowUserManager;
20+
use ProcessMaker\Managers\DataManager;
21+
use ProcessMaker\Models\MustacheExpressionEvaluator;
2022
use ProcessMaker\Nayra\Bpmn\TokenTrait;
2123
use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
2224
use ProcessMaker\Nayra\Contracts\Bpmn\FlowElementInterface;
@@ -1440,6 +1442,59 @@ public function reassign($toUserId, User $requestingUser, $comments = '')
14401442
}
14411443
}
14421444

1445+
/**
1446+
* Build context for Mustache (end event external URL). Same as scripts/screens: _user, _request, process data, APP_URL.
1447+
*/
1448+
private function getElementDestinationMustacheContext(): array
1449+
{
1450+
try {
1451+
$context = (new DataManager())->getData($this);
1452+
} catch (Throwable $e) {
1453+
Log::warning('Failed to load Mustache context via DataManager, falling back to request data', [
1454+
'token_id' => $this->id,
1455+
'error' => $e->getMessage(),
1456+
]);
1457+
$request = $this->processRequest;
1458+
$context = array_merge($request->data ?? [], $request ? (new DataManager())->updateRequestMagicVariable([], $request) : []);
1459+
$user = $this->user ?? auth()->user();
1460+
if ($user) {
1461+
$userData = $user->attributesToArray();
1462+
unset($userData['remember_token']);
1463+
$context['_user'] = $userData;
1464+
}
1465+
}
1466+
1467+
$context['APP_URL'] = config('app.url');
1468+
1469+
// Never expose remember_token to Mustache (defense in depth; DataManager/fallback may already strip it)
1470+
if (isset($context['_user']) && is_array($context['_user'])) {
1471+
unset($context['_user']['remember_token']);
1472+
}
1473+
1474+
// Normalize to plain arrays/scalars so Mustache resolves all keys (common PHP idiom)
1475+
$json = json_encode($context, JSON_THROW_ON_ERROR);
1476+
$normalized = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
1477+
1478+
return is_array($normalized) ? $normalized : [];
1479+
}
1480+
1481+
/**
1482+
* Resolve Mustache in end event external URL. FEEL is not supported here; use Mustache only.
1483+
* Context: APP_URL, _request, _user, process variables (same as getElementDestinationMustacheContext).
1484+
*
1485+
* Example (Mustache):
1486+
* {{APP_URL}}/admin/users/{{_request.id}}/edit -> https://example.com/admin/users/123/edit
1487+
* {{APP_URL}}/webentry/{{_request.id}} -> https://example.com/webentry/123
1488+
* {{APP_URL}}/path/{{my_process_var}} -> uses process variable my_process_var
1489+
*/
1490+
private function resolveElementDestinationUrl(string $url): string
1491+
{
1492+
$url = html_entity_decode($url, ENT_QUOTES | ENT_HTML401, 'UTF-8');
1493+
$context = $this->getElementDestinationMustacheContext();
1494+
1495+
return (new MustacheExpressionEvaluator())->render($url, $context);
1496+
}
1497+
14431498
/**
14441499
* Determines the destination based on the type of element destination property
14451500
*
@@ -1481,6 +1536,11 @@ private function getElementDestination($elementDestinationType, $elementDestinat
14811536
$elementDestination = $elementDestinationProp['value']['url'] ?? null;
14821537
}
14831538
}
1539+
if (is_string($elementDestination) && $elementDestination !== '') {
1540+
if ($elementDestinationType === 'externalURL' || $elementDestinationType === 'customDashboard') {
1541+
$elementDestination = $this->resolveElementDestinationUrl($elementDestination);
1542+
}
1543+
}
14841544
break;
14851545
case 'taskList':
14861546
$elementDestination = route('tasks.index');

ProcessMaker/Services/ConditionalRedirectService.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ public function __construct()
103103
public function resolve(array $conditionalRedirect, array $data): ?array
104104
{
105105
$this->errors = [];
106+
$data = $this->normalizeDataForFeel($data);
107+
106108
foreach ($conditionalRedirect as $item) {
107109
if (!isset($item['condition'])) {
108110
throw new InvalidArgumentException('Condition is required');
@@ -125,6 +127,29 @@ public function resolve(array $conditionalRedirect, array $data): ?array
125127
return null;
126128
}
127129

130+
/**
131+
* Normalize data so FEEL comparisons like form_input_1==0 work when form sends string "0".
132+
* Converts numeric strings to int/float so that the first matching condition is the intended one.
133+
*
134+
* @param array $data
135+
* @return array
136+
*/
137+
private function normalizeDataForFeel(array $data): array
138+
{
139+
$normalized = [];
140+
foreach ($data as $key => $value) {
141+
if (is_array($value)) {
142+
$normalized[$key] = $this->normalizeDataForFeel($value);
143+
} elseif (is_string($value) && is_numeric($value)) {
144+
$normalized[$key] = str_contains($value, '.') ? (float) $value : (int) $value;
145+
} else {
146+
$normalized[$key] = $value;
147+
}
148+
}
149+
150+
return $normalized;
151+
}
152+
128153
/**
129154
* Process conditional redirects for a specific process request token
130155
*

0 commit comments

Comments
 (0)