diff --git a/ProcessMaker/Http/Controllers/TaskController.php b/ProcessMaker/Http/Controllers/TaskController.php index 213d9374ab..2672a3abf6 100755 --- a/ProcessMaker/Http/Controllers/TaskController.php +++ b/ProcessMaker/Http/Controllers/TaskController.php @@ -27,6 +27,7 @@ use ProcessMaker\Models\TaskDraft; use ProcessMaker\Models\UserResourceView; use ProcessMaker\Nayra\Contracts\Bpmn\ScriptTaskInterface; +use ProcessMaker\Services\SmartExtractConfiguration; use ProcessMaker\Traits\HasControllerAddons; use ProcessMaker\Traits\SearchAutocompleteTrait; use ProcessMaker\Traits\TaskControllerIndexMethods; @@ -43,6 +44,10 @@ class TaskController extends Controller 'overdue' => 'Due', ]; + public function __construct(private readonly SmartExtractConfiguration $smartExtractConfiguration) + { + } + public function index() { $routerPath = Request::route('router'); @@ -190,25 +195,7 @@ public function edit(ProcessRequestToken $task, string $preview = '') 'datetime_format', ]); $userConfiguration = (new UserConfigurationController())->index(); - $hitlEnabled = config('smart-extract.hitl_enabled', false) && $isSmartExtractTask; - - // Build the iframe source - $iframeSrc = null; - if ($hitlEnabled) { - $dashboardUrl = config('smart-extract.dashboard_url'); - $requestData = $task->processRequest->data ?? []; - - $documentToken = $requestData['documentToken'] ?? null; - $fileId = $requestData['fileId'] ?? null; - - if ($documentToken && $fileId && !empty($dashboardUrl)) { - $queryParams = http_build_query([ - 'documentToken' => $documentToken, - 'fileId' => $fileId, - ]); - $iframeSrc = $dashboardUrl . '?' . $queryParams; - } - } + [$hitlEnabled, $iframeSrc] = $this->smartExtractHitlConfiguration($task, $isSmartExtractTask); return view('tasks.edit', [ 'task' => $task, @@ -231,6 +218,30 @@ public function edit(ProcessRequestToken $task, string $preview = '') } } + private function smartExtractHitlConfiguration( + ProcessRequestToken $task, + bool $isSmartExtractTask + ): array { + $hitlEnabled = $this->smartExtractConfiguration->hitlEnabled() && $isSmartExtractTask; + if (!$hitlEnabled) { + return [false, null]; + } + + $dashboardUrl = $this->smartExtractConfiguration->dashboardUrl(); + $requestData = $task->processRequest->data ?? []; + $documentToken = $requestData['documentToken'] ?? null; + $fileId = $requestData['fileId'] ?? null; + + if (!$documentToken || !$fileId || empty($dashboardUrl)) { + return [true, null]; + } + + return [true, $dashboardUrl . '?' . http_build_query([ + 'documentToken' => $documentToken, + 'fileId' => $fileId, + ])]; + } + public function quickFillEdit(ProcessRequestToken $task) { $screenVersion = $task->getScreenVersion(); diff --git a/ProcessMaker/Http/Resources/Task.php b/ProcessMaker/Http/Resources/Task.php index fe19ce85b5..7bf2eae8ff 100644 --- a/ProcessMaker/Http/Resources/Task.php +++ b/ProcessMaker/Http/Resources/Task.php @@ -11,6 +11,7 @@ use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; use ProcessMaker\Models\User; +use ProcessMaker\Services\SmartExtractConfiguration; use ProcessMaker\Traits\TaskResourceIncludes; class Task extends ApiResource @@ -118,7 +119,7 @@ private function addAssignableUsers(&$array, $include) private function mergeHitlCaseNumber(array &$array): void { - if (!config('smart-extract.hitl_enabled')) { + if (!app(SmartExtractConfiguration::class)->hitlEnabled()) { return; } diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php index c19ded9d5f..762fd6cd0b 100644 --- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php +++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php @@ -53,6 +53,7 @@ use ProcessMaker\Providers\PermissionServiceProvider; use ProcessMaker\Repositories\SettingsConfigRepository; use ProcessMaker\Services\ConditionalRedirectService; +use ProcessMaker\Services\SmartExtractConfiguration; use RuntimeException; use Spatie\Multitenancy\Events\MadeTenantCurrentEvent; use Spatie\Multitenancy\Events\TenantNotFoundForRequestEvent; @@ -122,6 +123,8 @@ public function register(): void // Register our permission services $this->app->register(PermissionServiceProvider::class); + $this->app->scoped(SmartExtractConfiguration::class); + $this->app->singleton(Managers\PackageManager::class, function () { return new Managers\PackageManager(); }); diff --git a/ProcessMaker/ScriptRunners/Base.php b/ProcessMaker/ScriptRunners/Base.php index d261245795..f24d4e2a0f 100644 --- a/ProcessMaker/ScriptRunners/Base.php +++ b/ProcessMaker/ScriptRunners/Base.php @@ -188,11 +188,9 @@ private function getEnvironmentVariables($useEscape = true) // Add the url to the host if ($useEscape) { $variablesParameter[] = 'HOST_URL=' . escapeshellarg(config('app.docker_host_url')); - $variablesParameter[] = 'SMART_EXTRACT_API_HOST=' . escapeshellarg(config('smart-extract.api_host')); $variablesParameter[] = 'SMART_EXTRACT_REQUEST_TIMEOUT=' . escapeshellarg((string) config('smart-extract.request_timeout')); } else { $variablesParameter[] = 'HOST_URL=' . config('app.docker_host_url'); - $variablesParameter[] = 'SMART_EXTRACT_API_HOST=' . config('smart-extract.api_host'); $variablesParameter[] = 'SMART_EXTRACT_REQUEST_TIMEOUT=' . config('smart-extract.request_timeout'); } diff --git a/ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php b/ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php index 7e243c104e..235eaecace 100644 --- a/ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php +++ b/ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php @@ -136,7 +136,6 @@ private function getEnvironmentVariables(User $user) $variablesParameter['API_HOST'] = config('app.docker_host_url') . '/api/1.0'; $variablesParameter['APP_URL'] = config('app.docker_host_url'); $variablesParameter['API_SSL_VERIFY'] = (config('app.api_ssl_verify') ? '1' : '0'); - $variablesParameter['SMART_EXTRACT_API_HOST'] = config('smart-extract.api_host'); $variablesParameter['SMART_EXTRACT_REQUEST_TIMEOUT'] = config('smart-extract.request_timeout'); } diff --git a/ProcessMaker/Services/SmartExtractConfiguration.php b/ProcessMaker/Services/SmartExtractConfiguration.php new file mode 100644 index 0000000000..5ddf03bb98 --- /dev/null +++ b/ProcessMaker/Services/SmartExtractConfiguration.php @@ -0,0 +1,81 @@ +stringValue(self::API_HOST); + } + + public function clientId(): ?string + { + return $this->stringValue(self::CLIENT_ID); + } + + public function clientSecret(): ?string + { + return $this->stringValue(self::CLIENT_SECRET); + } + + public function dashboardUrl(): ?string + { + return $this->stringValue(self::DASHBOARD_URL); + } + + public function hitlEnabled(): bool + { + $value = $this->stringValue(self::HITL_ENABLED); + + if ($value === null) { + return false; + } + + return filter_var(trim($value), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false; + } + + private function stringValue(string $name): ?string + { + $value = $this->values()[$name] ?? null; + + return is_string($value) && trim($value) !== '' ? $value : null; + } + + private function values(): array + { + if ($this->values !== null) { + return $this->values; + } + + $this->values = EnvironmentVariable::query() + ->whereIn('name', [ + self::API_HOST, + self::CLIENT_ID, + self::CLIENT_SECRET, + self::DASHBOARD_URL, + self::HITL_ENABLED, + ]) + ->get() + ->mapWithKeys(fn (EnvironmentVariable $variable) => [ + $variable->name => $variable->value, + ]) + ->all(); + + return $this->values; + } +} diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index dcfa97bea0..fd76a3065d 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -16,6 +16,7 @@ use ProcessMaker\Models\User; use ProcessMaker\Package\SavedSearch\Models\SavedSearch; use ProcessMaker\Query\SyntaxError; +use ProcessMaker\Services\SmartExtractConfiguration; trait TaskControllerIndexMethods { @@ -161,7 +162,7 @@ private function excludeNonVisibleTasks($query, $request) { $nonSystem = filter_var($request->input('non_system'), FILTER_VALIDATE_BOOLEAN); $allTasks = filter_var($request->input('all_tasks'), FILTER_VALIDATE_BOOLEAN); - $hitlEnabled = filter_var(config('smart-extract.hitl_enabled'), FILTER_VALIDATE_BOOLEAN); + $hitlEnabled = app(SmartExtractConfiguration::class)->hitlEnabled(); $includeScreen = filter_var($request->input('includeScreen'), FILTER_VALIDATE_BOOLEAN); $query->when(!$allTasks, function ($query) use ($includeScreen) { $query->where(function ($query) use ($includeScreen) { diff --git a/tests/unit/ProcessMaker/Http/Controllers/TaskControllerSmartExtractTest.php b/tests/unit/ProcessMaker/Http/Controllers/TaskControllerSmartExtractTest.php new file mode 100644 index 0000000000..6c10b4d719 --- /dev/null +++ b/tests/unit/ProcessMaker/Http/Controllers/TaskControllerSmartExtractTest.php @@ -0,0 +1,64 @@ +create([ + 'name' => SmartExtractConfiguration::HITL_ENABLED, + 'value' => 'true', + ]); + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::DASHBOARD_URL, + 'value' => 'https://dashboard.example.com/edit.html', + ]); + + $processRequest = new ProcessRequest([ + 'data' => [ + 'documentToken' => 'document-token', + 'fileId' => 'file-123', + ], + ]); + $task = new ProcessRequestToken(); + $task->setRelation('processRequest', $processRequest); + + $controller = new TaskController(app(SmartExtractConfiguration::class)); + $method = new ReflectionMethod(TaskController::class, 'smartExtractHitlConfiguration'); + $method->setAccessible(true); + + [$enabled, $iframeUrl] = $method->invoke($controller, $task, true); + + $this->assertTrue($enabled); + $this->assertSame( + 'https://dashboard.example.com/edit.html?documentToken=document-token&fileId=file-123', + $iframeUrl + ); + } + + public function test_hitl_configuration_fails_closed_when_disabled(): void + { + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::HITL_ENABLED, + 'value' => 'false', + ]); + + $task = new ProcessRequestToken(); + $task->setRelation('processRequest', new ProcessRequest(['data' => []])); + + $controller = new TaskController(app(SmartExtractConfiguration::class)); + $method = new ReflectionMethod(TaskController::class, 'smartExtractHitlConfiguration'); + $method->setAccessible(true); + + $this->assertSame([false, null], $method->invoke($controller, $task, true)); + } +} diff --git a/tests/unit/ProcessMaker/ScriptRunners/SmartExtractEnvironmentVariablesTest.php b/tests/unit/ProcessMaker/ScriptRunners/SmartExtractEnvironmentVariablesTest.php new file mode 100644 index 0000000000..2d361a19e4 --- /dev/null +++ b/tests/unit/ProcessMaker/ScriptRunners/SmartExtractEnvironmentVariablesTest.php @@ -0,0 +1,68 @@ +createApiHost(); + $executor = ScriptExecutor::factory()->create(['language' => 'php']); + $runner = new class ($executor) extends Base { + public function config($code, array $dockerConfig) + { + return $dockerConfig; + } + }; + + $method = new ReflectionMethod(Base::class, 'getEnvironmentVariables'); + $method->setAccessible(true); + $variables = $method->invoke($runner, false); + + $apiHosts = array_values(array_filter( + $variables, + fn (string $variable) => str_starts_with($variable, SmartExtractConfiguration::API_HOST . '=') + )); + + $this->assertSame([ + SmartExtractConfiguration::API_HOST . '=https://database.example.com', + ], $apiHosts); + } + + public function test_microservice_runner_preserves_database_api_host(): void + { + $this->createApiHost(); + $script = Script::factory()->create(['language' => 'php']); + $user = User::factory()->create(); + Cache::put('script-runner-' . $user->id, 'access-token'); + + $runner = new ScriptMicroserviceRunner($script); + $method = new ReflectionMethod(ScriptMicroserviceRunner::class, 'getEnvironmentVariables'); + $method->setAccessible(true); + $variables = $method->invoke($runner, $user); + + $this->assertSame( + 'https://database.example.com', + $variables[SmartExtractConfiguration::API_HOST] + ); + } + + private function createApiHost(): void + { + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::API_HOST, + 'value' => 'https://database.example.com', + ]); + } +} diff --git a/tests/unit/ProcessMaker/Services/SmartExtractConfigurationTest.php b/tests/unit/ProcessMaker/Services/SmartExtractConfigurationTest.php new file mode 100644 index 0000000000..bcebe58d0a --- /dev/null +++ b/tests/unit/ProcessMaker/Services/SmartExtractConfigurationTest.php @@ -0,0 +1,90 @@ +createConfigurationVariables(); + + DB::flushQueryLog(); + DB::enableQueryLog(); + + $configuration = new SmartExtractConfiguration(); + + $this->assertSame('https://extract.example.com', $configuration->apiHost()); + $this->assertSame('client-id', $configuration->clientId()); + $this->assertSame('client-secret', $configuration->clientSecret()); + $this->assertSame('https://dashboard.example.com/edit.html', $configuration->dashboardUrl()); + $this->assertTrue($configuration->hitlEnabled()); + + $queries = collect(DB::getQueryLog()) + ->filter(fn (array $query) => str_contains($query['query'], 'environment_variables')); + + $this->assertCount(1, $queries); + } + + public function test_missing_empty_and_invalid_values_fail_closed(): void + { + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::HITL_ENABLED, + 'value' => 'not-a-boolean', + ]); + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::DASHBOARD_URL, + 'value' => ' ', + ]); + + $configuration = new SmartExtractConfiguration(); + + $this->assertFalse($configuration->hitlEnabled()); + $this->assertNull($configuration->apiHost()); + $this->assertNull($configuration->clientId()); + $this->assertNull($configuration->clientSecret()); + $this->assertNull($configuration->dashboardUrl()); + } + + public function test_scoped_configuration_refreshes_on_the_next_lifecycle(): void + { + $apiHost = EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::API_HOST, + 'value' => 'https://first.example.com', + ]); + + $currentLifecycle = app(SmartExtractConfiguration::class); + $this->assertSame('https://first.example.com', $currentLifecycle->apiHost()); + + $apiHost->value = 'https://second.example.com'; + $apiHost->save(); + + $this->assertSame('https://first.example.com', $currentLifecycle->apiHost()); + + app()->forgetScopedInstances(); + $nextLifecycle = app(SmartExtractConfiguration::class); + + $this->assertNotSame($currentLifecycle, $nextLifecycle); + $this->assertSame('https://second.example.com', $nextLifecycle->apiHost()); + } + + private function createConfigurationVariables(): void + { + foreach ([ + SmartExtractConfiguration::API_HOST => 'https://extract.example.com', + SmartExtractConfiguration::CLIENT_ID => 'client-id', + SmartExtractConfiguration::CLIENT_SECRET => 'client-secret', + SmartExtractConfiguration::DASHBOARD_URL => 'https://dashboard.example.com/edit.html', + SmartExtractConfiguration::HITL_ENABLED => 'true', + ] as $name => $value) { + EnvironmentVariable::factory()->create([ + 'name' => $name, + 'value' => $value, + ]); + } + } +}