Skip to content

Commit 087c5a3

Browse files
Merge branch 'VITE_INBOX' of github.com:ProcessMaker/processmaker into FOUR-32412
2 parents d63a35a + 1b9bc4b commit 087c5a3

45 files changed

Lines changed: 3471 additions & 495 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ SCRIPT_MICROSERVICE_PUSHER_HOST=
6666
SCRIPT_MICROSERVICE_PUSHER_PORT=
6767

6868
# FrankenPHP workers ignore CLI php.ini. Use Caddyfile + --caddyfile=Caddyfile.
69-
# FRANKENPHP_MEMORY_LIMIT=3072M
69+
# OCTANE_MEMORY_LIMIT=512M
70+
# OCTANE_MAX_EXECUTION_TIME=90
71+
# OCTANE_POST_MAX_SIZE=200M
72+
# OCTANE_UPLOAD_MAX_FILESIZE=200M
73+
# OCTANE_MAX_INPUT_VARS=9000
74+
# OCTANE_MAX_INPUT_TIME=90
7075

7176
KEYCLOAK_CLIENT_ID=
7277
KEYCLOAK_CLIENT_SECRET=

‎.github/workflows/deploy-pm4.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ on:
66
- cron: '30 2 * * *' # run daily
77
workflow_dispatch:
88
workflow_call:
9+
10+
# Cancel superseded PR runs (including nested deploy-pm4). Do not cancel a close/delete.
11+
concurrency:
12+
group: deploy-pm4-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}
13+
cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }}
14+
915
jobs:
1016
run:
1117
name: Run PM4-workflow

‎Caddyfile‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
{$CADDY_GLOBAL_OPTIONS}
33

44
frankenphp {
5-
# FrankenPHP workers ignore CLI php.ini / PHPRC. Set memory here.
6-
php_ini memory_limit {$FRANKENPHP_MEMORY_LIMIT:3072M}
5+
# FrankenPHP workers ignore CLI php.ini / PHPRC. Set PHP limits here.
6+
php_ini memory_limit {$OCTANE_MEMORY_LIMIT:3072M}
7+
php_ini max_execution_time {$OCTANE_MAX_EXECUTION_TIME:90}
8+
php_ini post_max_size {$OCTANE_POST_MAX_SIZE:200M}
9+
php_ini upload_max_filesize {$OCTANE_UPLOAD_MAX_FILESIZE:200M}
10+
php_ini max_input_vars {$OCTANE_MAX_INPUT_VARS:9000}
11+
php_ini max_input_time {$OCTANE_MAX_INPUT_TIME:90}
712

813
worker {
914
file "{$APP_PUBLIC_PATH}/frankenphp-worker.php"

‎ProcessMaker/BpmnEngine.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ public function setInlineTaskExecutionEnabled(bool $enabled): void
189189

190190
public function scheduleInlineJob(array $job): void
191191
{
192+
// This is a single pending-job slot; replacing it would silently skip a task.
193+
if ($this->nextInlineJob !== null) {
194+
throw new \LogicException('An inline BPMN job is already scheduled.');
195+
}
196+
192197
$this->nextInlineJob = $job;
193198
}
194199

‎ProcessMaker/Console/Commands/TransitionExecutors.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ public function handle(): int
8080
$this->info("Transitioning executor {$executors[$index]->uuid} ({$executors[$index]->language}) to the microservice...");
8181

8282
try {
83+
// Check if the executor type is null, and if so, set it to Custom and save it
84+
if ($executors[$index]->type === null) {
85+
$executors[$index]->type = ScriptExecutorType::Custom;
86+
$executors[$index]->save();
87+
}
88+
8389
$response = $this->scriptMicroserviceService->updateCustomExecutor($executors[$index]);
8490
Log::debug('Response', ['response' => $response]);
8591
$status = strtolower((string)($response['status'] ?? ''));

‎ProcessMaker/Http/Controllers/Api/TaskController.php‎

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,21 @@
3434
use ProcessMaker\Models\UserResourceView;
3535
use ProcessMaker\Notifications\TaskReassignmentNotification;
3636
use ProcessMaker\Query\SyntaxError;
37+
use ProcessMaker\Repositories\ProcessExecutionRawRepository;
3738
use ProcessMaker\SanitizeHelper;
3839
use ProcessMaker\Traits\TaskControllerIndexMethods;
3940

4041
class TaskController extends Controller
4142
{
4243
use TaskControllerIndexMethods;
4344

45+
private ?ProcessExecutionRawRepository $processExecutionRaw = null;
46+
47+
private function processExecutionRaw(): ProcessExecutionRawRepository
48+
{
49+
return $this->processExecutionRaw ??= app(ProcessExecutionRawRepository::class);
50+
}
51+
4452
/**
4553
* A whitelist of attributes that should not be
4654
* sanitized by our SanitizeInput middleware.
@@ -354,28 +362,48 @@ public function show(ProcessRequestToken $task)
354362
*/
355363
public function update(Request $request, ProcessRequestToken $task)
356364
{
365+
if (!$task->relationLoaded('process')) {
366+
$task->setRelation('process', $this->processExecutionRaw()->getProcessForAuthorizeRaw($task->process_id));
367+
}
357368
$this->authorize('update', $task);
358369
if ($request->input('status') === 'COMPLETED') {
359370
if ($task->status === 'CLOSED') {
360371
return abort(422, __('Task already closed'));
361372
}
362373
// Skip ConvertEmptyStringsToNull and TrimStrings middlewares
363374
$data = json_optimize_decode($request->getContent(), true);
364-
$data = SanitizeHelper::sanitizeData($data['data'], null, $task->processRequest->do_not_sanitize ?? []);
375+
$instance = $this->processExecutionRaw()->getProcessRequestForCompleteRaw($task->process_request_id);
376+
$data = SanitizeHelper::sanitizeData($data['data'], null, $instance->do_not_sanitize ?? []);
377+
365378
//Call the manager to trigger the start event
366-
$process = $task->process;
367-
$instance = $task->processRequest;
368-
TaskDraft::moveDraftFiles($task);
379+
$process = $this->processExecutionRaw()->getProcessForCompleteRaw($task->process_id);
380+
$instance->setRelation('process', $process);
381+
if ($processVersion = $this->processExecutionRaw()->getProcessVersionForCompleteRaw($instance->process_version_id)) {
382+
$instance->setRelation('processVersion', $processVersion);
383+
}
384+
$task->setRelation('processRequest', $instance);
385+
$task->setRelation('process', $process);
386+
387+
if ($this->processExecutionRaw()->taskHasDraftRaw($task->id)) {
388+
TaskDraft::moveDraftFiles($task);
389+
}
390+
369391
WorkflowManager::completeTask($process, $instance, $task, $data);
370392

371-
return new Resource($task->refresh());
393+
$responseInstance = $this->processExecutionRaw()->getProcessRequestForResponseRaw($task->process_request_id);
394+
$responseInstance->setRelation('process', $process);
395+
$taskRefreshed = $this->processExecutionRaw()->refreshTaskRaw($task, $process, $responseInstance);
396+
397+
return new Resource($taskRefreshed);
372398
} elseif (!empty($request->input('user_id'))) {
399+
$process = $this->processExecutionRaw()->getProcessForReassignRaw($task->process_id);
400+
$task->setRelation('process', $process);
401+
373402
$userToAssign = $request->input('user_id');
374403
$comments = $request->input('comments');
375404
$task->reassign($userToAssign, $request->user(), $comments);
376405

377-
$taskRefreshed = $task->refresh();
378-
406+
$taskRefreshed = $this->processExecutionRaw()->refreshTaskRaw($task, $process, $task->processRequest);
379407
CaseUpdate::dispatchSync($task->processRequest, $taskRefreshed);
380408

381409
return new Resource($taskRefreshed);

‎ProcessMaker/Http/Controllers/Api/UserController.php‎

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,48 @@ class UserController extends Controller
3535
public $doNotSanitize = [
3636
'username', // has alpha_dash rule
3737
'password',
38+
'firstname', // validated as plain text by User::rules()
39+
'lastname', // validated as plain text by User::rules()
40+
'title', // validated as plain text by User::rules()
41+
];
42+
43+
/**
44+
* Fields accepted when a non-administrative user updates their own profile.
45+
*
46+
* @var array<string>
47+
*/
48+
private const SELF_SERVICE_UPDATE_FIELDS = [
49+
'username',
50+
'password',
51+
'firstname',
52+
'lastname',
53+
'title',
54+
'email',
55+
'address',
56+
'city',
57+
'state',
58+
'postal',
59+
'country',
60+
'phone',
61+
'fax',
62+
'cell',
63+
'timezone',
64+
'datetime_format',
65+
'status',
66+
'avatar',
67+
'preferences_2fa',
68+
'connected_accounts',
69+
'meta',
70+
'valpassword',
71+
];
72+
73+
/**
74+
* Metadata fields accepted during a self-service profile update.
75+
*
76+
* @var array<string>
77+
*/
78+
private const SELF_SERVICE_META_FIELDS = [
79+
'disableRecommendations',
3880
];
3981

4082
/**
@@ -452,12 +494,22 @@ public function getPinnnedControls(User $user)
452494
*/
453495
public function update(User $user, Request $request)
454496
{
455-
if (!Auth::user()->can('edit', $user)) {
497+
$authenticatedUser = Auth::user();
498+
if (!$authenticatedUser->can('edit', $user)) {
456499
throw new AuthorizationException(__('Not authorized to update this user.'));
457500
}
458501

459-
$request->validate(User::rules($user));
460502
$fields = $request->json()->all();
503+
$isSelfServiceUpdate = $this->authorizeSelfServiceUpdate($authenticatedUser, $user, $fields);
504+
$rules = User::rules($user);
505+
if ($isSelfServiceUpdate) {
506+
$rules['meta'] = ['sometimes', 'array'];
507+
$rules['meta.disableRecommendations'] = ['sometimes', 'boolean'];
508+
}
509+
$request->validate($rules);
510+
if ($isSelfServiceUpdate) {
511+
$fields = $this->normalizeSelfServiceMeta($user, $fields);
512+
}
461513
if (isset($fields['password'])) {
462514
$fields['password'] = Hash::make($fields['password']);
463515
$fields['password_changed_at'] = Carbon::now()->toDateTimeString();
@@ -466,6 +518,7 @@ public function update(User $user, Request $request)
466518
session()->forget('login-error');
467519
}
468520
$original = $user->getOriginal();
521+
$isLdapUser = $user->meta?->authenticationType === 'ldap';
469522
$user->fill($fields);
470523
if (array_key_exists('cell', $fields)) {
471524
$response = $this->validateCellPhoneNumber($user, $fields['cell']);
@@ -474,28 +527,24 @@ public function update(User $user, Request $request)
474527
}
475528
}
476529
if ($fields['email'] !== $original['email']) {
530+
$ssoUser = $isLdapUser;
477531
if (class_exists(SsoUser::class)) {
478532
// Check if the user is an SSO user (including SAML)
479-
$ssoUser = SsoUser::where('user_id', $user->id)->exists();
480-
481-
// Check if the user is an LDAP user
482-
if (isset($user->meta?->authenticationType) && $user->meta->authenticationType === 'ldap') {
483-
$ssoUser = true;
484-
}
485-
if ($ssoUser) {
486-
return response([
487-
'message' => __(
488-
"The email can't be edited. This action is only available for SSO-synced users."
489-
),
490-
'errors' => [
491-
'email' => [
492-
__(
493-
"The email can't be edited. This action is only available for SSO-synced users."
494-
),
495-
],
533+
$ssoUser = $ssoUser || SsoUser::where('user_id', $user->id)->exists();
534+
}
535+
if ($ssoUser) {
536+
return response([
537+
'message' => __(
538+
"The email can't be edited. This action is only available for SSO-synced users."
539+
),
540+
'errors' => [
541+
'email' => [
542+
__(
543+
"The email can't be edited. This action is only available for SSO-synced users."
544+
),
496545
],
497-
], 422);
498-
}
546+
],
547+
], 422);
499548
}
500549
if (!isset($fields['valpassword'])) {
501550
return response([
@@ -564,6 +613,59 @@ public function update(User $user, Request $request)
564613
return response([], 204);
565614
}
566615

616+
/**
617+
* Authorize and constrain self-service profile updates.
618+
*/
619+
private function authorizeSelfServiceUpdate(User $authenticatedUser, User $targetUser, array $fields): bool
620+
{
621+
$isSelfServiceUpdate = $authenticatedUser->id === $targetUser->id
622+
&& !$authenticatedUser->is_administrator
623+
&& !$authenticatedUser->hasPermission('edit-users');
624+
625+
if (!$isSelfServiceUpdate) {
626+
return false;
627+
}
628+
629+
if (!$authenticatedUser->hasPermission('edit-personal-profile')) {
630+
throw new AuthorizationException(__('Not authorized to update this user.'));
631+
}
632+
633+
$disallowedFields = array_diff(array_keys($fields), self::SELF_SERVICE_UPDATE_FIELDS);
634+
if ($disallowedFields !== []) {
635+
throw new AuthorizationException(__('Not authorized to update one or more user fields.'));
636+
}
637+
638+
if (isset($fields['meta']) && is_array($fields['meta'])) {
639+
$disallowedMetaFields = array_diff(array_keys($fields['meta']), self::SELF_SERVICE_META_FIELDS);
640+
if ($disallowedMetaFields !== []) {
641+
throw new AuthorizationException(__('Not authorized to update one or more user fields.'));
642+
}
643+
}
644+
645+
return true;
646+
}
647+
648+
/**
649+
* Merge self-service metadata into the persisted server-managed values.
650+
*/
651+
private function normalizeSelfServiceMeta(User $user, array $fields): array
652+
{
653+
$meta = (array) $user->meta;
654+
if (
655+
array_key_exists('meta', $fields)
656+
&& array_key_exists('disableRecommendations', $fields['meta'])
657+
) {
658+
if ($fields['meta']['disableRecommendations']) {
659+
$meta['disableRecommendations'] = true;
660+
} else {
661+
unset($meta['disableRecommendations']);
662+
}
663+
}
664+
$fields['meta'] = $meta ?: null;
665+
666+
return $fields;
667+
}
668+
567669
/**
568670
* Validate the phone number for SMS two-factor authentication.
569671
*

‎ProcessMaker/Http/Middleware/ValidateEditUserAndPasswordPermission.php‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ public function handle(Request $request, Closure $next)
1515
{
1616
$user = $request->route('user');
1717
$fields = $request->json()->all();
18-
if (($fields['username'] !== $user->getAttribute('username') || in_array('password', $fields)) &&
19-
!Auth::user()->hasPermission('edit-user-and-password') && !Auth::user()->is_administrator) {
18+
$usernameChanged = array_key_exists('username', $fields)
19+
&& $fields['username'] !== $user->getAttribute('username');
20+
$passwordChanged = array_key_exists('password', $fields);
21+
if (($usernameChanged || $passwordChanged) &&
22+
!Auth::user()->hasPermission('edit-user-and-password') && !Auth::user()->is_administrator) {
2023
throw new AuthorizationException(__('Not authorized to update the username and password.'));
2124
}
2225

‎ProcessMaker/Jobs/BpmnAction.php‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,8 @@ private function findInstanceWithRetry($instanceId, bool $lightweight = false)
338338
}
339339

340340
/**
341-
* Load only lock and revision metadata when validating the fast path.
341+
* Load ProcessRequest for BPMN actions. In lightweight mode, only lock and
342+
* revision metadata are loaded when validating the fast path.
342343
*/
343344
private function findInstance($instanceId, bool $lightweight): ProcessRequest
344345
{
@@ -349,6 +350,12 @@ private function findInstance($instanceId, bool $lightweight): ProcessRequest
349350
'process_collaboration_id',
350351
'execution_revision',
351352
]);
353+
} else {
354+
$query->with([
355+
'process',
356+
'processVersion',
357+
'collaboration',
358+
]);
352359
}
353360

354361
return $query->findOrFail($instanceId);

0 commit comments

Comments
 (0)