diff --git a/ProcessMaker/Http/Controllers/Api/UserController.php b/ProcessMaker/Http/Controllers/Api/UserController.php index d08e3374db..d7ad0274e4 100644 --- a/ProcessMaker/Http/Controllers/Api/UserController.php +++ b/ProcessMaker/Http/Controllers/Api/UserController.php @@ -255,11 +255,12 @@ public function getUsersTaskCount(Request $request) $processRequestToken = ProcessRequestToken::findOrFail($request->input('assignable_for_task_id')); if (config('app.reassign_restrict_to_assignable_users')) { $include_ids = $processRequestToken->process->getAssignableUsersByAssignmentType($processRequestToken); - $assignmentRule = $processRequestToken->getAssignmentRule(); - if ($assignmentRule === 'rule_expression' && $request->has('form_data')) { + $bpmnAssignment = $processRequestToken->getBpmnDefinition()->getBpmnElementInstance() + ->getProperty('assignment', null); + if ($bpmnAssignment === 'rule_expression' && $request->has('form_data')) { $include_ids = $processRequestToken->getAssigneesFromExpression($request->input('form_data')); } - if ($assignmentRule === 'process_variable' && $request->has('form_data')) { + if ($bpmnAssignment === 'process_variable' && $request->has('form_data')) { $include_ids = $processRequestToken->getUsersFromProcessVariable($request->input('form_data')); } } diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php index b65c1b0a06..383be630db 100644 --- a/ProcessMaker/Models/ProcessRequestToken.php +++ b/ProcessMaker/Models/ProcessRequestToken.php @@ -1074,21 +1074,7 @@ public function getAssignees(array $assignments, array $variables): array $language = new ExpressionLanguage(); foreach ($assignments as $assignment) { - $isTrue = false; - - if (!empty($assignment['expression'])) { - try { - $isTrue = $language->evaluate($assignment['expression'], $variables); - } catch (Throwable $e) { - $isTrue = false; - } - } - - if ($isTrue) { - $result[] = $assignment['assignee']; - } - - if (isset($assignment['default']) && $assignment['default'] === true) { + if ($this->isAssignmentRuleMatch($assignment, $variables, $language)) { $result[] = $assignment['assignee']; } } @@ -1099,26 +1085,61 @@ public function getAssignees(array $assignments, array $variables): array /** * Get the assignees from the expression * - * @param string $form_data + * @param string|array $form_data * @return array */ - public function getAssigneesFromExpression(string $form_data): array + public function getAssigneesFromExpression(string|array $form_data): array { - $formData = json_decode($form_data, true); + $formData = is_array($form_data) ? $form_data : json_decode($form_data, true); $activity = $this->getBpmnDefinition()->getBpmnElementInstance(); $assignmentRules = $activity->getProperty('assignmentRules', null); - $assignments = json_decode($assignmentRules, true); + $assignments = json_decode($assignmentRules, true) ?? []; - $include_ids = $this->getAssignees($assignments, $formData); + $userIds = []; + $language = new ExpressionLanguage(); + foreach ($assignments as $assignment) { + if (!$this->isAssignmentRuleMatch($assignment, $formData, $language)) { + continue; + } - // we add the manager to the list of assignees - $manager_id = $this->process->manager_id; - if ($manager_id) { - $include_ids[] = $manager_id; + if (($assignment['type'] ?? 'user') === 'group') { + $groupUsers = []; + $this->process->getConsolidatedUsers($assignment['assignee'], $groupUsers); + foreach ($groupUsers as $userId) { + if (!empty($userId) && is_numeric($userId)) { + $userIds[$userId] = $userId; + } + } + } else { + $userIds[$assignment['assignee']] = $assignment['assignee']; + } } - return $include_ids; + foreach ((array) ($this->process->manager_id ?? []) as $managerId) { + if (!empty($managerId) && is_numeric($managerId)) { + $userIds[$managerId] = $managerId; + } + } + + return array_values($userIds); + } + + private function isAssignmentRuleMatch(array $assignment, array $variables, ExpressionLanguage $language): bool + { + if (isset($assignment['default']) && $assignment['default'] === true) { + return true; + } + + if (empty($assignment['expression'])) { + return false; + } + + try { + return $language->evaluate($assignment['expression'], $variables); + } catch (Throwable $e) { + return false; + } } /** diff --git a/tests/Feature/Api/UsersTest.php b/tests/Feature/Api/UsersTest.php index c1dacd9e18..c84983f6db 100644 --- a/tests/Feature/Api/UsersTest.php +++ b/tests/Feature/Api/UsersTest.php @@ -1007,6 +1007,100 @@ public function testPostUsersTaskCount() $result->assertStatus(200); } + public function testPostUsersTaskCountWithRuleExpressionAssignment() + { + config(['app.reassign_restrict_to_assignable_users' => true]); + + $admin = $this->user; + $assignableUser = User::factory()->create(['status' => 'ACTIVE']); + $otherUser = User::factory()->create(['status' => 'ACTIVE']); + + $rules = [ + ['type' => 'user', 'assignee' => $assignableUser->id, 'expression' => 'TestVar < 10'], + ['type' => 'user', 'assignee' => $otherUser->id, 'expression' => 'TestVar > 10'], + ]; + + $bpmn = file_get_contents(__DIR__ . '/processes/AssignmentByProcessVariable.bpmn'); + $bpmn = str_replace('[ASSIGNMENT]', 'rule_expression', $bpmn); + $bpmn = str_replace('[ASSIGNED_USERS]', '', $bpmn); + $bpmn = str_replace('[ASSIGNED_GROUPS]', '', $bpmn); + $bpmn = str_replace('[IS_SELF_SERVICE]', 'false', $bpmn); + $bpmn = str_replace('[ASSIGNMENT_RULES]', htmlspecialchars(json_encode($rules)), $bpmn); + + $process = Process::factory()->create([ + 'user_id' => $admin->id, + 'manager_id' => $admin->id, + 'bpmn' => $bpmn, + ]); + + $request = ProcessRequest::factory()->create([ + 'process_id' => $process->id, + 'user_id' => $admin->id, + ]); + + $task = ProcessRequestToken::factory()->create([ + 'process_id' => $process->id, + 'process_request_id' => $request->id, + 'element_id' => 'task1_node', + 'user_id' => $assignableUser->id, + 'status' => 'ACTIVE', + ]); + + $result = $this->apiCall('POST', route('api.users.users_task_count_post'), [ + 'assignable_for_task_id' => $task->id, + 'form_data' => ['TestVar' => 5], + ]); + + $result->assertStatus(200); + $userIds = array_column($result->json()['data'], 'id'); + $this->assertContains($assignableUser->id, $userIds); + $this->assertNotContains($otherUser->id, $userIds); + } + + public function testPostUsersTaskCountWithRuleExpressionGroupAssignment() + { + config(['app.reassign_restrict_to_assignable_users' => true]); + + $groupUser = User::factory()->create(['status' => 'ACTIVE']); + $group = Group::factory()->create(); + GroupMember::factory()->create([ + 'group_id' => $group->id, + 'member_id' => $groupUser->id, + 'member_type' => User::class, + ]); + + $rules = [ + ['type' => 'group', 'assignee' => $group->id, 'expression' => 'TestVar<10'], + ]; + + $bpmn = file_get_contents(__DIR__ . '/processes/AssignmentByProcessVariable.bpmn'); + $bpmn = str_replace('[ASSIGNMENT]', 'rule_expression', $bpmn); + $bpmn = str_replace('[ASSIGNED_USERS]', '', $bpmn); + $bpmn = str_replace('[ASSIGNED_GROUPS]', '', $bpmn); + $bpmn = str_replace('[IS_SELF_SERVICE]', 'false', $bpmn); + $bpmn = str_replace('[ASSIGNMENT_RULES]', htmlspecialchars(json_encode($rules)), $bpmn); + + $process = Process::factory()->create(['bpmn' => $bpmn]); + + $route = route('api.process_events.trigger', [$process->id, 'event' => 'start_node']); + $response = $this->apiCall('POST', $route, ['TestVar' => 5]); + $requestId = $response->json()['id']; + + $task = ProcessRequestToken::where([ + 'process_request_id' => $requestId, + 'status' => 'ACTIVE', + ])->firstOrFail(); + + $result = $this->apiCall('POST', route('api.users.users_task_count_post'), [ + 'assignable_for_task_id' => $task->id, + 'form_data' => ['TestVar' => 5], + ]); + + $result->assertStatus(200); + $userIds = array_column($result->json()['data'], 'id'); + $this->assertContains($groupUser->id, $userIds); + } + /** * Test save and get filters per user saved in cache */ diff --git a/tests/Model/ProcessRequestTokenTest.php b/tests/Model/ProcessRequestTokenTest.php index ddc7a3193d..30e9eb0290 100644 --- a/tests/Model/ProcessRequestTokenTest.php +++ b/tests/Model/ProcessRequestTokenTest.php @@ -424,4 +424,148 @@ public function testGetUsersFromProcessVariableFiltersInvalidIds() $this->assertNotContains(0, $result); $this->assertNotContains(-1, $result); } + + public function testGetAssigneesFromExpressionAcceptsArrayFormData() + { + $assignableUser = User::factory()->create(['status' => 'ACTIVE']); + + $process = Process::factory()->create(); + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); + + $rules = [ + ['type' => 'user', 'assignee' => $assignableUser->id, 'default' => true], + ]; + + $activity = $this->createMock(\ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface::class); + $activity->method('getProperty') + ->willReturnCallback(function ($key, $default) use ($rules) { + if ($key === 'assignmentRules') { + return json_encode($rules); + } + + return $default; + }); + + $bpmnDefinition = $this->createMock(\ProcessMaker\Nayra\Storage\BpmnElement::class); + $bpmnDefinition->method('getBpmnElementInstance') + ->willReturn($activity); + + $token = $this->getMockBuilder(ProcessRequestToken::class) + ->onlyMethods(['getBpmnDefinition']) + ->getMock(); + + $token->process_id = $process->id; + $token->process_request_id = $request->id; + $token->process = $process; + + $token->expects($this->atLeastOnce()) + ->method('getBpmnDefinition') + ->willReturn($bpmnDefinition); + + $formData = ['TestVar' => 5]; + + $result = $token->getAssigneesFromExpression($formData); + + $this->assertContains($assignableUser->id, $result); + + $resultFromString = $token->getAssigneesFromExpression(json_encode($formData)); + $this->assertEquals($result, $resultFromString); + } + + public function testGetAssigneesFromExpressionDoesNotExpandUnmatchedGroupWithSameAssigneeId() + { + $assignableUser = User::factory()->create(['status' => 'ACTIVE']); + $sharedAssigneeId = $assignableUser->id; + $rules = [ + ['type' => 'user', 'assignee' => $sharedAssigneeId, 'expression' => 'TestVar<10'], + ['type' => 'group', 'assignee' => $sharedAssigneeId, 'expression' => 'TestVar>10'], + ]; + + $activity = $this->createMock(\ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface::class); + $activity->method('getProperty') + ->willReturnCallback(function ($key, $default) use ($rules) { + if ($key === 'assignmentRules') { + return json_encode($rules); + } + + return $default; + }); + + $bpmnDefinition = $this->createMock(\ProcessMaker\Nayra\Storage\BpmnElement::class); + $bpmnDefinition->method('getBpmnElementInstance') + ->willReturn($activity); + + $process = $this->createMock(Process::class); + $process->expects($this->never())->method('getConsolidatedUsers'); + + $request = ProcessRequest::factory()->create(); + + $token = $this->getMockBuilder(ProcessRequestToken::class) + ->onlyMethods(['getBpmnDefinition']) + ->getMock(); + + $token->process_id = $request->process_id; + $token->process_request_id = $request->id; + $token->process = $process; + + $token->expects($this->atLeastOnce()) + ->method('getBpmnDefinition') + ->willReturn($bpmnDefinition); + + $result = $token->getAssigneesFromExpression(['TestVar' => 5]); + + $this->assertEquals([$sharedAssigneeId], $result); + } + + public function testGetAssigneesFromExpressionPreservesGroupMembersWhenManagerIdCollides() + { + $groupUsers = User::factory()->count(3)->create(['status' => 'ACTIVE']); + $group = Group::factory()->create(); + foreach ($groupUsers as $groupUser) { + GroupMember::factory()->create([ + 'group_id' => $group->id, + 'member_id' => $groupUser->id, + 'member_type' => User::class, + ]); + } + + $process = Process::factory()->create(['manager_id' => $groupUsers[1]->id]); + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); + + $rules = [ + ['type' => 'group', 'assignee' => $group->id, 'expression' => 'TestVar<10'], + ]; + + $activity = $this->createMock(\ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface::class); + $activity->method('getProperty') + ->willReturnCallback(function ($key, $default) use ($rules) { + if ($key === 'assignmentRules') { + return json_encode($rules); + } + + return $default; + }); + + $bpmnDefinition = $this->createMock(\ProcessMaker\Nayra\Storage\BpmnElement::class); + $bpmnDefinition->method('getBpmnElementInstance') + ->willReturn($activity); + + $token = $this->getMockBuilder(ProcessRequestToken::class) + ->onlyMethods(['getBpmnDefinition']) + ->getMock(); + + $token->process_id = $process->id; + $token->process_request_id = $request->id; + $token->process = $process; + + $token->expects($this->atLeastOnce()) + ->method('getBpmnDefinition') + ->willReturn($bpmnDefinition); + + $result = $token->getAssigneesFromExpression(['TestVar' => 5]); + + foreach ($groupUsers as $groupUser) { + $this->assertContains($groupUser->id, $result); + } + } }