$this->installed(), 'available' => function_exists('proc_open') && $this->php() !== null && $this->composer() !== null && is_writable(__DIR__), 'request_timeout_ms' => (RuntimeConfig::get('composer_timeout_seconds') + 30) * 1000]; } public function install(bool $force = false): array { if (!$force && $this->installed()) { return ['installed' => true]; } $php = $this->php(); $composer = $this->composer(); if (!function_exists('proc_open') || $php === null || $composer === null || !is_writable(__DIR__)) { throw new InformationException('Web installation is unavailable. Run composer install from the Mailgateway directory using SSH.'); } if (!is_file(__DIR__ . '/composer.lock')) { throw new InformationException('The module composer.lock is missing. Restore it before installing dependencies.'); } $command = [$php, $composer, 'install', '--no-dev', '--prefer-dist', '--no-interaction', '--no-progress', '--no-scripts', '--no-plugins', '--optimize-autoloader']; $result = $this->run($command); if ($result['timeout']) { throw new InformationException('Dependency installation exceeded its configured timeout. Run composer install from the Mailgateway directory using SSH.'); } if ($result['exit'] !== 0 || !$this->installed()) { throw new InformationException('Composer installation failed. Run composer install from the Mailgateway directory using SSH to see the detailed error.'); } return ['installed' => true]; } private function installed(): bool { return is_file(__DIR__ . '/vendor/autoload.php') && is_file(__DIR__ . '/vendor/webklex/php-imap/src/ClientManager.php'); } private function php(): ?string { foreach (RuntimeConfig::get('php_cli_paths') as $path) { if (is_file($path) && is_executable($path)) { return $path; } } return null; } private function composer(): ?string { foreach (RuntimeConfig::get('composer_paths') as $path) { if (is_file($path) && is_readable($path)) { return $path; } } return null; } protected function run(array $command): array { // PHP may terminate a web request before Composer completes otherwise. @set_time_limit(RuntimeConfig::get('composer_timeout_seconds') + 15); $process = proc_open($command, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes, __DIR__); if (!is_resource($process)) { throw new InformationException('Composer could not be started. Run composer install using SSH.'); } fclose($pipes[0]); stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); $deadline = microtime(true) + RuntimeConfig::get('composer_timeout_seconds'); $exit = -1; $timeout = false; try { while (true) { // Drain both pipes to avoid deadlock. Never expose Composer output: // repository authentication and environment details can appear there. stream_get_contents($pipes[1]); stream_get_contents($pipes[2]); $status = proc_get_status($process); if (!$status['running']) { $exit = $status['exitcode']; break; } if (microtime(true) >= $deadline) { $timeout = true; proc_terminate($process, 9); break; } usleep(100000); } } finally { fclose($pipes[1]); fclose($pipes[2]); $closed = proc_close($process); if ($exit === -1) { $exit = $closed; } } return ['exit' => $exit, 'timeout' => $timeout]; } }