Skip to content

Commit ed92e9b

Browse files
committed
Merge branch 'develop' into bugfix/FOUR-25401
2 parents 2d0da95 + 01ad702 commit ed92e9b

25 files changed

Lines changed: 778 additions & 1374 deletions

File tree

ProcessMaker/Contracts/ConditionalRedirectServiceInterface.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

ProcessMaker/Events/ProcessUpdated.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ class ProcessUpdated implements ShouldBroadcastNow
2626

2727
public $activeTokens;
2828

29-
public $elementDestination;
30-
3129
/**
3230
* Create a new event instance.
3331
*
@@ -43,7 +41,6 @@ public function __construct(ProcessRequest $processRequest, $event, TokenInterfa
4341
if ($token) {
4442
$this->tokenId = $token->getId();
4543
$this->elementType = $token->element_type;
46-
$this->elementDestination = $token->elementDestination;
4744
}
4845
}
4946

ProcessMaker/Http/Controllers/Api/UserController.php

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,55 @@ public function index(Request $request)
183183
* ),
184184
* ),
185185
* )
186+
*
187+
* @OA\Post(
188+
* path="/users_task_count",
189+
* summary="Returns all users and their total tasks (POST version for large form_data)",
190+
* operationId="postUsersTaskCount",
191+
* tags={"Users"},
192+
* @OA\RequestBody(
193+
* description="Request body for filtering users",
194+
* @OA\JsonContent(
195+
* @OA\Property(
196+
* property="filter",
197+
* type="string",
198+
* description="Filter results by string. Searches First Name, Last Name, Email, or Username."
199+
* ),
200+
* @OA\Property(
201+
* property="include_ids",
202+
* type="string",
203+
* description="Comma separated list of user IDs to include in the response. Eg. 1,2,3"
204+
* ),
205+
* @OA\Property(
206+
* property="assignable_for_task_id",
207+
* type="integer",
208+
* description="Task ID to get assignable users for"
209+
* ),
210+
* @OA\Property(
211+
* property="form_data",
212+
* type="object",
213+
* description="Form data used to evaluate rule expressions for task assignment"
214+
* ),
215+
* ),
216+
* ),
217+
* @OA\Response(
218+
* response=200,
219+
* description="List of users with task counts",
220+
* @OA\JsonContent(
221+
* type="object",
222+
* @OA\Property(
223+
* property="data",
224+
* type="array",
225+
* @OA\Items(ref="#/components/schemas/users"),
226+
* ),
227+
* @OA\Property(
228+
* property="meta",
229+
* type="object",
230+
* ref="#/components/schemas/metadata",
231+
* ),
232+
* ),
233+
* ),
234+
* )
186235
*/
187236
public function getUsersTaskCount(Request $request)
188237
{
@@ -204,10 +253,10 @@ public function getUsersTaskCount(Request $request)
204253
$include_ids = explode(',', $include_ids_string);
205254
} elseif ($request->has('assignable_for_task_id')) {
206255
$processRequestToken = ProcessRequestToken::findOrFail($request->input('assignable_for_task_id'));
207-
$assignmentRule = $processRequestToken->getAssignmentRule();
208256
if (config('app.reassign_restrict_to_assignable_users')) {
209257
$include_ids = $processRequestToken->process->getAssignableUsersByAssignmentType($processRequestToken);
210258
}
259+
$assignmentRule = $processRequestToken->getAssignmentRule();
211260
if ($assignmentRule === 'rule_expression' && $request->has('form_data')) {
212261
$include_ids = $processRequestToken->getAssigneesFromExpression($request->input('form_data'));
213262
}
@@ -223,7 +272,8 @@ public function getUsersTaskCount(Request $request)
223272
->withCount('activeTasks')
224273
->orderBy(
225274
$request->input('order_by', 'username'),
226-
$request->input('order_direction', 'ASC'))
275+
$request->input('order_direction', 'ASC')
276+
)
227277
->paginate(50);
228278

229279
return new ApiCollection($response);
@@ -359,8 +409,8 @@ public function getPinnnedControls(User $user)
359409
$meta = $user->meta ? (array) $user->meta : [];
360410

361411
return array_key_exists('pinnedControls', $meta)
362-
? $meta['pinnedControls']
363-
: [];
412+
? $meta['pinnedControls']
413+
: [];
364414
}
365415

366416
/**
@@ -774,10 +824,12 @@ private function uploadAvatar(User $user, Request $request)
774824
// Validate image content
775825
if ($type === 'svg') {
776826
// For SVG files, validate against XSS
777-
if (preg_match('/<script/i', $data) ||
827+
if (
828+
preg_match('/<script/i', $data) ||
778829
preg_match('/on\w+\s*=/i', $data) ||
779830
preg_match('/javascript:/i', $data) ||
780-
preg_match('/data:/i', $data)) {
831+
preg_match('/data:/i', $data)
832+
) {
781833
throw new \Exception('SVG contains potentially malicious content');
782834
}
783835
} else {
@@ -853,7 +905,7 @@ public function restore(Request $request)
853905
// Otherwise, search trashed users
854906
// for the user to restore
855907
$user = User::onlyTrashed()->where($input, $request->input($input))
856-
->first();
908+
->first();
857909
}
858910

859911
if ($user instanceof User) {

ProcessMaker/ImportExport/Exporters/ProcessExporter.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use ProcessMaker\Models\Media;
1313
use ProcessMaker\Models\Process;
1414
use ProcessMaker\Models\ProcessCategory;
15+
use ProcessMaker\Models\ProcessLaunchpad;
1516
use ProcessMaker\Models\Screen;
1617
use ProcessMaker\Models\Script;
1718
use ProcessMaker\Models\User;
@@ -547,6 +548,9 @@ public function exportProcessLaunchpad(): void
547548
public function importProcessLaunchpad(): void
548549
{
549550
foreach ($this->getDependents('process_launchpad') as $launchpad) {
551+
if (ProcessLaunchpad::where('process_id', $this->model->id)->exists()) {
552+
continue;
553+
}
550554
$launchpad->model->setAttribute('process_id', $this->model->id);
551555
$launchpad->model->saveOrFail();
552556
}

ProcessMaker/Jobs/RefreshArtisanCaches.php

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
namespace ProcessMaker\Jobs;
44

55
use Illuminate\Bus\Queueable;
6+
use Illuminate\Console\Application;
67
use Illuminate\Contracts\Queue\ShouldBeUnique;
78
use Illuminate\Contracts\Queue\ShouldQueue;
89
use Illuminate\Foundation\Bus\Dispatchable;
910
use Illuminate\Queue\InteractsWithQueue;
1011
use Illuminate\Queue\Middleware\WithoutOverlapping;
1112
use Illuminate\Support\Facades\Artisan;
13+
use Illuminate\Support\Facades\Process;
1214

1315
class RefreshArtisanCaches implements ShouldQueue
1416
{
@@ -21,26 +23,7 @@ class RefreshArtisanCaches implements ShouldQueue
2123
*/
2224
public function handle()
2325
{
24-
// Skip in testing environment because this reconnects the database
25-
// meaning we loose transactions, and sets the console output verbosity
26-
// to quiet so we loose expectsOutput assertions.
27-
if (app()->environment('testing')) {
28-
return;
29-
}
30-
31-
$options = [
32-
'--no-interaction' => true,
33-
'--quiet' => true,
34-
];
35-
36-
if (app()->configurationIsCached()) {
37-
Artisan::call('config:cache', $options);
38-
} else {
39-
Artisan::call('queue:restart', $options);
40-
41-
// We call this manually here since this job is dispatched
42-
// automatically when the config *is* cached
43-
RestartMessageConsumers::dispatchSync();
44-
}
26+
// Do not rebuild the cache and restart the queue any more.
27+
// This is no longer needed.
4528
}
4629
}

ProcessMaker/Models/CallActivity.php

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,10 @@ protected function completeSubprocess(TokenInterface $token, ExecutionInstanceIn
9797
$store = $closedInstance->getDataStore();
9898
$allData = $store->getData();
9999

100-
// Determine which data should be merged back from the subprocess.
101-
$updatedKeys = method_exists($store, 'getUpdated')
102-
? $store->getUpdated()
103-
: null;
104-
105-
if ($updatedKeys === null) {
106-
// Legacy behavior or no tracking available: copy all data.
107-
$data = $allData;
108-
} elseif ($updatedKeys === []) {
109-
// Nothing was updated in the subprocess: do not merge anything.
110-
$data = [];
111-
} else {
112-
// Merge only the explicitly updated keys.
113-
$updatedKeys = array_values((array) $updatedKeys);
114-
$data = array_intersect_key($allData, array_flip($updatedKeys));
115-
}
100+
$data = $this->resolveUpdatedData($store, $allData);
101+
$parentData = $token->getInstance()->getDataStore()->getData();
102+
$data = $this->mergeNewKeys($data, $allData, $parentData);
103+
$data = $this->mergeChangedKeys($data, $allData, $parentData);
116104

117105
$dataManager = new DataManager();
118106
$dataManager->updateData($token, $data);
@@ -125,6 +113,62 @@ protected function completeSubprocess(TokenInterface $token, ExecutionInstanceIn
125113
return $this;
126114
}
127115

116+
/**
117+
* Decide which data from the subprocess should be merged based on updated keys.
118+
*/
119+
protected function resolveUpdatedData($store, array $allData): array
120+
{
121+
$updatedKeys = method_exists($store, 'getUpdated')
122+
? $store->getUpdated()
123+
: null;
124+
125+
if ($updatedKeys === null) {
126+
return $allData;
127+
}
128+
129+
if ($updatedKeys === []) {
130+
return [];
131+
}
132+
133+
$updatedKeys = array_values((array) $updatedKeys);
134+
135+
return array_intersect_key($allData, array_flip($updatedKeys));
136+
}
137+
138+
/**
139+
* Merge keys that exist only in the subprocess data.
140+
*/
141+
protected function mergeNewKeys(array $data, array $allData, array $parentData): array
142+
{
143+
$newKeys = array_diff(array_keys($allData), array_keys($parentData));
144+
if (empty($newKeys)) {
145+
return $data;
146+
}
147+
148+
return $data + array_intersect_key($allData, array_flip($newKeys));
149+
}
150+
151+
/**
152+
* Merge keys that changed in the subprocess but may not have been tracked.
153+
*/
154+
protected function mergeChangedKeys(array $data, array $allData, array $parentData): array
155+
{
156+
$changedKeys = [];
157+
foreach ($allData as $key => $value) {
158+
if (array_key_exists($key, $parentData) && $parentData[$key] !== $value) {
159+
$changedKeys[] = $key;
160+
}
161+
}
162+
163+
if (empty($changedKeys)) {
164+
return $data;
165+
}
166+
167+
$pendingKeys = array_diff($changedKeys, array_keys($data));
168+
169+
return $data + array_intersect_key($allData, array_flip($pendingKeys));
170+
}
171+
128172
/**
129173
* Catch a subprocess error
130174
*

ProcessMaker/Models/Process.php

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,29 +1053,65 @@ public function getAssignableUsers($processTaskUuid)
10531053
* - previous_task_assignee: would assign it to the Process Manager.
10541054
* - requester: would assign it to the Process Manager.
10551055
* - process_manager: would assign it to the same Process Manager.
1056-
*
1056+
*
10571057
* @param ProcessRequestToken $processRequestToken
1058-
* @return array
1058+
* @return array Array of user IDs, always returns a flat array (never nested)
10591059
*/
10601060
public function getAssignableUsersByAssignmentType(ProcessRequestToken $processRequestToken): array
10611061
{
1062-
$users = [];
1063-
switch ($processRequestToken->getAssignmentRule()) {
1064-
case 'user_group':
1065-
case 'process_variable':
1066-
case 'rule_expression':
1062+
$assignmentRule = $processRequestToken->getAssignmentRule();
1063+
$managerIds = $processRequestToken->process->manager_id ?? [];
1064+
1065+
// Rules that only return process managers
1066+
$managerOnlyRules = ['previous_task_assignee', 'requester', 'process_manager'];
1067+
if (in_array($assignmentRule, $managerOnlyRules, true)) {
1068+
return $this->normalizeUserIds($managerIds);
1069+
}
1070+
1071+
// Rules that return assignable users plus process managers
1072+
$groupBasedRules = ['user_group', 'process_variable', 'rule_expression'];
1073+
if (in_array($assignmentRule, $groupBasedRules, true)) {
1074+
$users = [];
1075+
1076+
// Get assignable users from task assignments
1077+
if (!empty($processRequestToken->element_id)) {
10671078
$users = $this->getAssignableUsers($processRequestToken->element_id);
1068-
$users[] = $processRequestToken->process->properties["manager_id"];
1069-
break;
1070-
case 'previous_task_assignee':
1071-
case 'requester':
1072-
$users[] = $processRequestToken->process->properties["manager_id"];
1073-
break;
1074-
case 'process_manager':
1075-
$users[] = $processRequestToken->process->properties["manager_id"];
1076-
break;
1079+
}
1080+
1081+
// Merge with manager IDs
1082+
if (!empty($managerIds)) {
1083+
$users = array_merge($users, $managerIds);
1084+
}
1085+
1086+
return $this->normalizeUserIds($users);
10771087
}
1078-
return $users;
1088+
1089+
// Default: return empty array for unknown rules
1090+
return [];
1091+
}
1092+
1093+
/**
1094+
* Normalize user IDs to ensure a flat array of unique numeric values.
1095+
*
1096+
* @param array|null $userIds
1097+
* @return array
1098+
*/
1099+
private function normalizeUserIds($userIds): array
1100+
{
1101+
if (empty($userIds)) {
1102+
return [];
1103+
}
1104+
1105+
// Flatten nested arrays and filter out invalid values
1106+
$normalized = array_filter(
1107+
array_values(Arr::flatten($userIds)),
1108+
function ($id) {
1109+
return !empty($id) && is_numeric($id);
1110+
}
1111+
);
1112+
1113+
// Remove duplicates and return
1114+
return array_values(array_unique($normalized));
10791115
}
10801116

10811117
/**

0 commit comments

Comments
 (0)