diff --git a/ProcessMaker/Managers/TaskSchedulerManager.php b/ProcessMaker/Managers/TaskSchedulerManager.php index cd93db4e6d..10e26e4fd4 100644 --- a/ProcessMaker/Managers/TaskSchedulerManager.php +++ b/ProcessMaker/Managers/TaskSchedulerManager.php @@ -16,10 +16,12 @@ use PDOException; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Jobs\StartEventConditional; +use ProcessMaker\Models\EnvironmentVariable; use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestLock; use ProcessMaker\Models\ScheduledTask; +use ProcessMaker\Models\Setting; use ProcessMaker\Models\TimerExpression; use ProcessMaker\Nayra\Bpmn\Models\BoundaryEvent; use ProcessMaker\Nayra\Bpmn\Models\DatePeriod; @@ -398,6 +400,16 @@ public function executeTimerStartEvent(ScheduledTask $task, $config) if (!$definitions->findElementById($config->element_id)) { return; } + + if ($this->shouldSkipHandleRepliesTimerStart($process)) { + Log::info('Skipping Actions By Email Handle Replies timer event because ABE inbound mail configuration is not adequate', [ + 'process_id' => $process->id, + 'process_name' => $process->name, + ]); + + return; + } + $event = $definitions->getEvent($config->element_id); $data = []; @@ -405,6 +417,194 @@ public function executeTimerStartEvent(ScheduledTask $task, $config) $processRequest = WorkflowManager::triggerStartEvent($process, $event, $data); } + /** + * Determine if the timer should be skipped for the Actions By Email handle replies process. + * + * @param Process $process + * @return bool + */ + private function shouldSkipHandleRepliesTimerStart(Process $process): bool + { + if (!$this->isHandleRepliesProcess($process)) { + return false; + } + + return !$this->hasAdequateAbeInboundConfiguration(); + } + + /** + * Whether Actions By Email has enough configuration to poll inbound mail (IMAP or OAuth). + * + * This is a heuristic in core: the connector may add more keys; we avoid starting the + * Handle Replies timer when the mailbox clearly is not set up (FOUR-30587). + * + * @return bool + */ + private function hasAdequateAbeInboundConfiguration(): bool + { + if (!Setting::readyToUseSettingsDatabase()) { + return false; + } + + $authMethodIndex = (int) ($this->getAbeInboundSettingValue(['abe_imap_auth_method']) ?? 0); + $username = $this->getAbeInboundSettingValue(['abe_imap_username', 'email_connector_mail_username']); + + if (!$this->hasValue($username)) { + return false; + } + + if ($authMethodIndex === 1) { + return $this->hasEnvironmentVariables([ + 'ABE_GMAIL_API_CLIENT_ID', + 'ABE_GMAIL_API_SECRET', + 'ABE_GMAIL_API_ACCESS_TOKEN', + 'ABE_GMAIL_API_REFRESH_TOKEN', + ]); + } + + if ($authMethodIndex === 2) { + return $this->hasEnvironmentVariables([ + 'ABE_OFFICE_365_CLIENT_ID', + 'ABE_OFFICE_365_TENANT_ID', + 'ABE_OFFICE_365_SECRET', + 'ABE_OFFICE_365_ACCESS_TOKEN', + 'ABE_OFFICE_365_REFRESH_TOKEN', + 'ABE_OFFICE_365_ACCESS_TOKEN_EXPIRE_DATE', + ]); + } + + return $this->hasStandardAbeInboundConfiguration(); + } + + /** + * IMAP configuration for standard authentication mode. + */ + private function hasStandardAbeInboundConfiguration(): bool + { + $password = $this->getAbeInboundSettingValue(['abe_imap_password', 'email_connector_mail_password']); + if (!$this->hasValue($password)) { + return false; + } + + $inboxUri = $this->getAbeInboundSettingValue(['abe_imap_inbox_uri']); + if ($this->hasValue($inboxUri)) { + return true; + } + + $server = $this->getAbeInboundSettingValue(['abe_imap_server', 'email_connector_mail_host']); + $port = $this->getAbeInboundSettingValue(['abe_imap_port', 'email_connector_mail_port']); + + return $this->hasValue($server) && $this->hasValue($port); + } + + /** + * Reads the first non-empty value from supported Actions By Email / mail settings keys. + * + * @param array $keys + * @return string|null + */ + private function getAbeInboundSettingValue(array $keys): ?string + { + $settings = Setting::query() + ->whereIn('key', $keys) + ->get() + ->keyBy('key'); + + if ($settings->isEmpty()) { + return null; + } + + foreach ($keys as $key) { + $setting = $settings->get($key); + if (!$setting) { + continue; + } + + $value = $this->extractSettingValue($setting->config); + if ($this->hasValue($value)) { + return (string) $value; + } + } + + return null; + } + + /** + * Normalize setting values from different possible setting formats. + * + * @param mixed $value + * @return mixed + */ + private function extractSettingValue($value) + { + if (is_object($value)) { + $value = (array) $value; + } + + if (is_array($value)) { + if (array_key_exists('value', $value)) { + return $value['value']; + } + + return $value; + } + + return $value; + } + + /** + * Check if a setting value is present and not empty. + * + * @param mixed $value + * @return bool + */ + private function hasValue($value): bool + { + if (is_array($value)) { + foreach ($value as $item) { + if ($this->hasValue($item)) { + return true; + } + } + + return false; + } + + return is_scalar($value) && trim((string) $value) !== ''; + } + + /** + * Identify the handle replies process by name. + * + * @param Process $process + * @return bool + */ + private function isHandleRepliesProcess(Process $process): bool + { + if ((string) $process->package_key === 'package-actions-by-email/handle-replies') { + return true; + } + + $name = (string) $process->name; + + return stripos($name, 'actions by email') !== false && stripos($name, 'handle replies') !== false; + } + + private function hasEnvironmentVariables(array $names): bool + { + $values = EnvironmentVariable::query() + ->whereIn('name', $names) + ->pluck('value', 'name'); + + foreach ($names as $name) { + if (!isset($values[$name]) || trim((string) $values[$name]) === '') { + return false; + } + } + + return true; + } + /** * Execute a timer start event * diff --git a/tests/Feature/Api/TimerStartEventTest.php b/tests/Feature/Api/TimerStartEventTest.php index 88b483e7be..27635609ab 100644 --- a/tests/Feature/Api/TimerStartEventTest.php +++ b/tests/Feature/Api/TimerStartEventTest.php @@ -228,4 +228,25 @@ public function testScheduleMustNotStartTimerEventWhenProcessInactive() $task->type = 'TIMER_START_EVENT'; $manager->executeTimerStartEvent($task, json_decode($task->configuration)); } + + public function testScheduleMustNotStartHandleRepliesTimerWhenAbeInboundConfigInadequate() + { + // triggerStartEvent must not run when ABE inbound mail settings are missing/inadequate + WorkflowManager::shouldReceive('triggerStartEvent') + ->never() + ->with(\Mockery::any(), \Mockery::any(), \Mockery::any()); + + $data = []; + $data['name'] = 'Actions By Email - Handle Replies'; + $data['bpmn'] = Process::getProcessTemplate('TimerStartEvent.bpmn'); + + $process = Process::factory()->create($data); + + $manager = new TaskSchedulerManager(); + $task = new ScheduledTask(); + $task->process_id = $process->id; + $task->configuration = '{"type":"TimeCycle","interval":"R4\/2019-02-13T13:08:00Z\/PT1M", "element_id" : "_9"}'; + $task->type = 'TIMER_START_EVENT'; + $manager->executeTimerStartEvent($task, json_decode($task->configuration)); + } } diff --git a/tests/unit/ProcessMaker/Managers/TaskSchedulerManagerTest.php b/tests/unit/ProcessMaker/Managers/TaskSchedulerManagerTest.php index 7937ed4211..3e3d1c5adc 100644 --- a/tests/unit/ProcessMaker/Managers/TaskSchedulerManagerTest.php +++ b/tests/unit/ProcessMaker/Managers/TaskSchedulerManagerTest.php @@ -4,6 +4,10 @@ use Carbon\Carbon; use DateTime; +use ProcessMaker\Models\EnvironmentVariable; +use ProcessMaker\Models\Process; +use ProcessMaker\Models\Setting; +use ReflectionMethod; use Tests\TestCase; class TaskSchedulerManagerTest extends TestCase @@ -74,4 +78,64 @@ public function testTruncateDates() $rounded = $this->manager->truncateDateTime($date); $this->assertEquals('00:01:00', $rounded->format('H:i:s')); } + + public function testHasAdequateAbeInboundConfigurationForStandardAuth() + { + Setting::updateOrCreate(['key' => 'abe_imap_auth_method'], ['config' => '0']); + Setting::updateOrCreate(['key' => 'abe_imap_username'], ['config' => 'abe@test.com']); + Setting::updateOrCreate(['key' => 'abe_imap_password'], ['config' => '123Test']); + Setting::updateOrCreate(['key' => 'abe_imap_server'], ['config' => 'imap.example.com']); + Setting::updateOrCreate(['key' => 'abe_imap_port'], ['config' => '993']); + + $this->assertTrue((bool) $this->invokePrivateMethod('hasAdequateAbeInboundConfiguration')); + } + + public function testHasAdequateAbeInboundConfigurationForGoogleOauth() + { + Setting::updateOrCreate(['key' => 'abe_imap_auth_method'], ['config' => '1']); + Setting::updateOrCreate(['key' => 'abe_imap_username'], ['config' => 'abe@test.com']); + + foreach ([ + 'ABE_GMAIL_API_CLIENT_ID', + 'ABE_GMAIL_API_SECRET', + 'ABE_GMAIL_API_ACCESS_TOKEN', + 'ABE_GMAIL_API_REFRESH_TOKEN', + ] as $name) { + EnvironmentVariable::factory()->create([ + 'name' => $name, + 'value' => 'value-' . strtolower($name), + ]); + } + + $this->assertTrue((bool) $this->invokePrivateMethod('hasAdequateAbeInboundConfiguration')); + } + + public function testHasAdequateAbeInboundConfigurationReturnsFalseWhenUsernameIsMissing() + { + Setting::updateOrCreate(['key' => 'abe_imap_auth_method'], ['config' => '0']); + Setting::updateOrCreate(['key' => 'abe_imap_username'], ['config' => '']); + Setting::updateOrCreate(['key' => 'abe_imap_password'], ['config' => '123Test']); + Setting::updateOrCreate(['key' => 'abe_imap_server'], ['config' => 'imap.example.com']); + Setting::updateOrCreate(['key' => 'abe_imap_port'], ['config' => '993']); + + $this->assertFalse((bool) $this->invokePrivateMethod('hasAdequateAbeInboundConfiguration')); + } + + public function testIsHandleRepliesProcessUsesPackageKey() + { + $process = new Process([ + 'package_key' => 'package-actions-by-email/handle-replies', + 'name' => 'Anything', + ]); + + $this->assertTrue((bool) $this->invokePrivateMethod('isHandleRepliesProcess', [$process])); + } + + private function invokePrivateMethod(string $method, array $args = []) + { + $reflection = new ReflectionMethod($this->manager, $method); + $reflection->setAccessible(true); + + return $reflection->invokeArgs($this->manager, $args); + } }