Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.22% covered (success)
97.22%
35 / 36
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
TwofactorGatewayService
97.22% covered (success)
97.22%
35 / 36
85.71% covered (warning)
85.71%
6 / 7
15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ensureAvailable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isGatewayComplete
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 send
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 callIntegrationMethod
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 resolveIntegrationService
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service;
10
11use OCA\Libresign\Exception\LibresignException;
12use OCP\App\IAppManager;
13use Psr\Container\ContainerInterface;
14use Psr\Container\NotFoundExceptionInterface;
15use Psr\Log\LoggerInterface;
16
17final class TwofactorGatewayService {
18    private const APP_ID = 'twofactor_gateway';
19    private const INTEGRATION_SERVICE_ID = 'OCA\\TwoFactorGateway\\Service\\GatewayDirectIntegrationService';
20    /**
21     * Semantic error code of the "gateway app not enabled" error: the closest
22     * HTTP status with one extra digit (503 -> 5030), so it is not mistaken
23     * for an HTTP status code.
24     */
25    public const int CODE_NOT_ENABLED = 5030;
26
27    public function __construct(
28        private ContainerInterface $container,
29        private IAppManager $appManager,
30        private LoggerInterface $logger,
31    ) {
32    }
33
34    public function isEnabled(): bool {
35        return $this->appManager->isEnabledForAnyone(self::APP_ID);
36    }
37
38    /**
39     * @throws LibresignException
40     */
41    public function ensureAvailable(string $gatewayName): void {
42        $this->callIntegrationMethod('ensureAvailable', [$gatewayName]);
43    }
44
45    public function isGatewayComplete(string $gatewayName): bool {
46        if (!$this->isEnabled()) {
47            return false;
48        }
49
50        try {
51            $isComplete = $this->callIntegrationMethod('isGatewayComplete', [$gatewayName]);
52        } catch (\Exception) {
53            // Gateway names and exception messages can contain caller-controlled data.
54            $this->logger->warning('Unable to determine twofactor gateway completeness.', [
55                'reason' => 'integration_error',
56            ]);
57            return false;
58        }
59
60        if (!is_bool($isComplete)) {
61            $this->logger->warning('Twofactor gateway integration service returned an invalid completeness flag.', [
62                'reason' => 'invalid_response',
63            ]);
64            return false;
65        }
66
67        return $isComplete;
68    }
69
70    /**
71     * @throws LibresignException
72     */
73    public function send(string $gatewayName, string $identifier, string $message, array $extra = []): void {
74        $arguments = [$gatewayName, $identifier, $message];
75        if ($extra !== []) {
76            $arguments[] = $extra;
77        }
78        $this->callIntegrationMethod('send', $arguments);
79    }
80
81    /**
82     * @throws LibresignException
83     */
84    private function callIntegrationMethod(string $method, array $arguments = []): mixed {
85        $integrationService = $this->resolveIntegrationService();
86        if (!is_callable([$integrationService, $method])) {
87            throw new \UnexpectedValueException(sprintf(
88                'Twofactor gateway integration service does not expose %s().',
89                $method,
90            ));
91        }
92
93        return call_user_func([$integrationService, $method], ...$arguments);
94    }
95
96    /**
97     * @throws LibresignException
98     */
99    private function resolveIntegrationService(): object {
100        if (!$this->isEnabled()) {
101            throw new LibresignException('App Two-Factor Gateway is not enabled.', self::CODE_NOT_ENABLED);
102        }
103
104        try {
105            $integrationService = $this->container->get(self::INTEGRATION_SERVICE_ID);
106        } catch (NotFoundExceptionInterface $exception) {
107            throw new LibresignException('App Two-Factor Gateway is not installed.', 0, $exception);
108        }
109
110        if (!is_object($integrationService)) {
111            throw new \UnexpectedValueException('Twofactor gateway integration service is not an object.');
112        }
113
114        return $integrationService;
115    }
116}