Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.66% covered (success)
89.66%
52 / 58
83.33% covered (success)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
RemoteApiClient
89.66% covered (success)
89.66%
52 / 58
83.33% covered (success)
83.33%
5 / 6
13.19
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
 getVersions
71.43% covered (warning)
71.43%
15 / 21
0.00% covered (danger)
0.00%
0 / 1
3.21
 isVerified
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 getVerificationIssues
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 setHttpClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRemoteHashes
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Remote API client handler class.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2018-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2018-03-27
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Administration;
21
22use Exception;
23use phpMyFAQ\Configuration;
24use phpMyFAQ\System;
25use stdClass;
26use Symfony\Component\HttpClient\HttpClient;
27use Symfony\Component\HttpFoundation\Response;
28use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
29use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
30use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
31use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
32use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
33use Symfony\Contracts\HttpClient\HttpClientInterface;
34
35/**
36 * Class RemoteApiClient
37 *
38 * @package phpMyFAQ
39 */
40class RemoteApiClient
41{
42    private string $apiUrl = 'https://api.phpmyfaq.de/';
43
44    private ?string $remoteHashes = null;
45
46    private HttpClientInterface $httpClient;
47
48    /**
49     * Api constructor.
50     */
51    public function __construct(
52        private readonly Configuration $configuration,
53        private readonly System $system,
54    ) {
55        $this->setHttpClient(HttpClient::create(['max_redirects' => 2, 'timeout' => 30]));
56    }
57
58    /**
59     * Returns the installed, the current available and the next version
60     * as an array.
61     *
62     * @return string[]
63     * @throws Exception|DecodingExceptionInterface|TransportExceptionInterface
64     */
65    public function getVersions(): array
66    {
67        $response = $this->httpClient->request('GET', $this->apiUrl . 'versions');
68
69        if ($response->getStatusCode() === Response::HTTP_OK) {
70            try {
71                $content = $response->toArray();
72                return [
73                    'installed' => $this->configuration->getVersion(),
74                    'stable' => (string) ($content['stable'] ?? ''),
75                    'development' => (string) ($content['development'] ?? ''),
76                    'nightly' => (string) ($content['nightly'] ?? ''),
77                ];
78            } catch (
79                ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $exception
80            ) {
81                throw new Exception(
82                    'phpMyFAQ Verification API is not available: ' . $exception->getMessage(),
83                    (int) $exception->getCode(),
84                    $exception,
85                );
86            }
87        }
88
89        return [
90            'installed' => $this->configuration->getVersion(),
91            'stable' => 'n/a',
92            'development' => 'n/a',
93            'nightly' => 'n/a',
94        ];
95    }
96
97    /**
98     * Returns true, if an installed version can be verified. Otherwise, false.
99     *
100     * @throws Exception|TransportExceptionInterface|\JsonException
101     */
102    public function isVerified(): bool
103    {
104        $response = $this->httpClient->request('GET', $this->apiUrl . 'verify/' . $this->configuration->getVersion());
105
106        try {
107            $this->remoteHashes = $response->getContent();
108            if (
109                json_decode(json: $this->remoteHashes, associative: null, depth: 512, flags: JSON_THROW_ON_ERROR)
110                instanceof stdClass
111            ) {
112                return is_array(json_decode(
113                    json: $this->remoteHashes,
114                    associative: true,
115                    depth: 512,
116                    flags: JSON_THROW_ON_ERROR,
117                ));
118            }
119        } catch (
120            ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $exception
121        ) {
122            throw new Exception(
123                'phpMyFAQ Verification API is not available: ' . $exception->getMessage(),
124                (int) $exception->getCode(),
125                $exception,
126            );
127        }
128
129        return false;
130    }
131
132    /**
133     * @return string[]
134     * @throws \JsonException
135     * @throws Exception
136     */
137    public function getVerificationIssues(): array
138    {
139        $localHashes = json_decode(
140            json: $this->system->createHashes(),
141            associative: true,
142            depth: 512,
143            flags: JSON_THROW_ON_ERROR,
144        );
145        $remoteHashes = json_decode(
146            json: (string) $this->remoteHashes,
147            associative: true,
148            depth: 512,
149            flags: JSON_THROW_ON_ERROR,
150        );
151
152        $issues = array_diff(is_array($localHashes) ? $localHashes : [], is_array($remoteHashes) ? $remoteHashes : []);
153
154        $verificationIssues = [];
155        foreach ($issues as $issueKey => $issueValue) {
156            $verificationIssues[$issueKey] = (string) $issueValue;
157        }
158
159        return $verificationIssues;
160    }
161
162    public function setHttpClient(HttpClientInterface $httpClient): void
163    {
164        $this->httpClient = $httpClient;
165    }
166
167    public function setRemoteHashes(?string $remoteHashes): RemoteApiClient
168    {
169        $this->remoteHashes = $remoteHashes;
170        return $this;
171    }
172}