Passed
Push — 1.11.x ( 573d31...0f96ea )
by Yannick
20:58 queued 07:13
created

RequestService::commandRequest()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 27
c 1
b 0
f 1
dl 0
loc 43
rs 9.1768
cc 5
nc 5
nop 1
1
<?php
2
3
namespace Onlyoffice\DocsIntegrationSdk\Service\Request;
4
5
/**
6
 *
7
 * (c) Copyright Ascensio System SIA 2024
8
 *
9
 * Licensed under the Apache License, Version 2.0 (the "License");
10
 * you may not use this file except in compliance with the License.
11
 * You may obtain a copy of the License at
12
 *
13
 *     http://www.apache.org/licenses/LICENSE-2.0
14
 *
15
 * Unless required by applicable law or agreed to in writing, software
16
 * distributed under the License is distributed on an "AS IS" BASIS,
17
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
 * See the License for the specific language governing permissions and
19
 * limitations under the License.
20
 *
21
 */
22
use Onlyoffice\DocsIntegrationSdk\Manager\Document\DocumentManager;
23
use Onlyoffice\DocsIntegrationSdk\Manager\Settings\SettingsManager;
24
use Onlyoffice\DocsIntegrationSdk\Manager\Security\JwtManager;
25
use Onlyoffice\DocsIntegrationSdk\Models\ConvertRequest;
26
use Onlyoffice\DocsIntegrationSdk\Service\Request\RequestServiceInterface;
27
use Onlyoffice\DocsIntegrationSdk\Service\Request\HttpClientInterface;
28
use Onlyoffice\DocsIntegrationSdk\Util\CommandResponseError;
29
use Onlyoffice\DocsIntegrationSdk\Util\CommonError;
30
use Onlyoffice\DocsIntegrationSdk\Util\ConvertResponseError;
31
32
/**
33
 * Default Document service.
34
 *
35
 * @package Onlyoffice\DocsIntegrationSdk\Service\Request
36
 */
37
38
abstract class RequestService implements RequestServiceInterface
39
{
40
41
    /**
42
     * Minimum supported version of editors
43
     *
44
     * @var float
45
     */
46
    private const MIN_EDITORS_VERSION = 6.0;
47
48
    protected SettingsManager $settingsManager;
49
    protected JwtManager $jwtManager;
50
51
    abstract public function getFileUrlForConvert();
52
53
    public function __construct(
54
        SettingsManager $settingsManager,
55
        HttpClientInterface $httpClient,
56
        JwtManager $jwtManager
57
    ) {
58
        $this->settingsManager = $settingsManager;
59
        $this->jwtManager = $jwtManager;
60
        $this->httpClient = $httpClient;
61
    }
62
63
    /**
64
    * Request to Document Server
65
    *
66
    * @param string $url - request address
67
    * @param array $method - request method
68
    * @param array $opts - request options
69
    *
70
    * @return string
71
    */
72
    public function request($url, $method = "GET", $opts = [])
73
    {
74
        if ($this->settingsManager->isIgnoreSSL()) {
75
            $opts["verify"] = false;
76
        }
77
78
        if (!array_key_exists("timeout", $opts)) {
79
            $opts["timeout"] = 60;
80
        }
81
82
        $this->httpClient->request($url, $method, $opts);
83
        if ($this->httpClient->getStatusCode() === 200) {
84
            return $this->httpClient->getBody();
85
        }
86
87
        return "";
88
    }
89
90
    /**
91
     * Generate an error code table of convertion
92
     *
93
     * @param int $errorCode - Error code
94
     *
95
     * @throws Exception
96
     */
97
    public function processConvServResponceError($errorCode)
98
    {
99
        $errorMessage = '';
100
101
        switch ($errorCode) {
102
            case ConvertResponseError::UNKNOWN:
103
                $errorMessage = ConvertResponseError::message(ConvertResponseError::UNKNOWN);
104
                break;
105
            case ConvertResponseError::TIMEOUT:
106
                $errorMessage = ConvertResponseError::message(ConvertResponseError::TIMEOUT);
107
                break;
108
            case ConvertResponseError::CONVERSION:
109
                $errorMessage = ConvertResponseError::message(ConvertResponseError::CONVERSION);
110
                break;
111
            case ConvertResponseError::DOWNLOADING:
112
                $errorMessage = ConvertResponseError::message(ConvertResponseError::DOWNLOADING);
113
                break;
114
            case ConvertResponseError::PASSWORD:
115
                $errorMessage = ConvertResponseError::message(ConvertResponseError::PASSWORD);
116
                break;
117
            case ConvertResponseError::DATABASE:
118
                $errorMessage = ConvertResponseError::message(ConvertResponseError::DATABASE);
119
                break;
120
            case ConvertResponseError::INPUT:
121
                $errorMessage = ConvertResponseError::message(ConvertResponseError::INPUT);
122
                break;
123
            case ConvertResponseError::TOKEN:
124
                $errorMessage = ConvertResponseError::message(ConvertResponseError::TOKEN);
125
                break;
126
            default:
127
                $errorMessage = "ErrorCode = " . $errorCode;
128
                break;
129
        }
130
131
        throw new \Exception($errorMessage);
132
    }
133
134
    /**
135
     * Generate an error code table of command
136
     *
137
     * @param string $errorCode - Error code
138
     *
139
     * @throws Exception
140
     */
141
    public function processCommandServResponceError($errorCode)
142
    {
143
        $errorMessage = "";
144
145
        switch ($errorCode) {
146
            case CommandResponseError::NO:
147
                return;
148
            case CommandResponseError::KEY:
149
                $errorMessage = CommandResponseError::message(CommandResponseError::KEY);
150
                break;
151
            case CommandResponseError::CALLBACK_URL:
152
                $errorMessage = CommandResponseError::message(CommandResponseError::CALLBACK_URL);
153
                break;
154
            case CommandResponseError::INTERNAL_SERVER:
155
                $errorMessage = CommandResponseError::message(CommandResponseError::INTERNAL_SERVER);
156
                break;
157
            case CommandResponseError::FORCE_SAVE:
158
                $errorMessage = CommandResponseError::message(CommandResponseError::FORCE_SAVE);
159
                break;
160
            case CommandResponseError::COMMAND:
161
                $errorMessage = CommandResponseError::message(CommandResponseError::COMMAND);
162
                break;
163
            case CommandResponseError::TOKEN:
164
                $errorMessage = CommandResponseError::message(CommandResponseError::TOKEN);
165
                break;
166
            default:
167
                $errorMessage = "ErrorCode = " . $errorCode;
168
                break;
169
        }
170
171
        throw new \Exception($errorMessage);
172
    }
173
174
    /**
175
     * Request health status
176
     *
177
     * @throws Exception
178
     *
179
     * @return bool
180
     */
181
    public function healthcheckRequest() : bool
182
    {
183
        $healthcheckUrl = $this->settingsManager->getDocumentServerHealthcheckUrl();
184
        if (empty($healthcheckUrl)) {
185
            throw new \Exception(CommonError::message(CommonError::NO_HEALTHCHECK_ENDPOINT));
186
        }
187
188
        $response = $this->request($healthcheckUrl);
189
        return $response === "true";
190
    }
191
192
    /**
193
     * Request for conversion to a service
194
     *
195
     * @param string $documentUri - Uri for the document to convert
196
     * @param string $fromExtension - Document extension
197
     * @param string $toExtension - Extension to which to convert
198
     * @param string $documentRevisionId - Key for caching on service
199
     * @param bool - $isAsync - Perform conversions asynchronously
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
200
     * @param string $region - Region
201
     *
202
     * @throws Exception
203
     *
204
     * @return array
205
     */
206
    public function sendRequestToConvertService(
207
        $documentUri,
208
        $fromExtension,
209
        $toExtension,
210
        $documentRevisionId,
211
        $isAsync,
212
        $region = null
213
    ) {
214
        $urlToConverter = $this->settingsManager->getConvertServiceUrl(true);
215
        if (empty($urlToConverter)) {
216
            throw new \Exception(CommonError::message(CommonError::NO_CONVERT_SERVICE_ENDPOINT));
217
        }
218
219
        if (empty($documentRevisionId)) {
220
            $documentRevisionId = $documentUri;
221
        }
222
        $documentRevisionId = DocumentManager::generateRevisionId($documentRevisionId);
223
224
        if (empty($fromExtension)) {
225
            $fromExtension = pathinfo($documentUri)["extension"];
226
        } else {
227
            $fromExtension = trim($fromExtension, ".");
228
        }
229
230
        $data = new ConvertRequest;
231
        $data->setAsync($isAsync);
232
        $data->setUrl($documentUri);
233
        $data->setOutputtype(trim($toExtension, "."));
234
        $data->setFiletype($fromExtension);
235
        $data->setTitle($documentRevisionId . "." . $fromExtension);
236
        $data->setKey($documentRevisionId);
237
238
        if (!is_null($region)) {
239
            $data->setRegion($region);
240
        }
241
242
        $opts = [
243
            "timeout" => "120",
244
            "headers" => [
245
                'Content-type' => 'application/json'
246
            ],
247
            "body" => json_encode($data)
248
        ];
249
250
        if ($this->jwtManager->isJwtEnabled()) {
251
            $params = [
252
                "payload" => json_decode(json_encode($data), true)
253
            ];
254
            $token = $this->jwtManager->jwtEncode($params);
255
            $jwtHeader = $this->settingsManager->getJwtHeader();
256
            $jwtPrefix = $this->settingsManager->getJwtPrefix();
257
258
            if (empty($jwtHeader)) {
259
                throw new \Exception(CommonError::message(CommonError::NO_JWT_HEADER));
260
            } elseif (empty($jwtPrefix)) {
261
                throw new \Exception(CommonError::message(CommonError::NO_JWT_PREFIX));
262
            }
263
264
            $opts["headers"][$jwtHeader] = (string)$jwtPrefix . $token;
265
            $token = $this->jwtManager->jwtEncode(json_decode(json_encode($data), true));
266
            $data->setToken($token);
267
            $opts["body"] = json_encode($data);
268
        }
269
270
271
        $responseXmlData = $this->request($urlToConverter, "POST", $opts);
272
        libxml_use_internal_errors(true);
273
274
        if (!function_exists("simplexml_load_file")) {
275
             throw new \Exception(CommonError::message(CommonError::READ_XML));
276
        }
277
278
        $responseData = simplexml_load_string($responseXmlData);
279
        
280
        if (!$responseData) {
0 ignored issues
show
introduced by
$responseData is of type SimpleXMLElement, thus it always evaluated to true.
Loading history...
281
            $exc = CommonError::message(CommonError::BAD_RESPONSE_XML);
282
            foreach (libxml_get_errors() as $error) {
283
                $exc = $exc . PHP_EOL . $error->message;
284
            }
285
            throw new \Exception($exc);
286
        }
287
288
        return $responseData;
289
    }
290
291
    /**
292
     * The method is to convert the file to the required format and return the result url
293
     *
294
     * @param string $documentUri - Uri for the document to convert
295
     * @param string $fromExtension - Document extension
296
     * @param string $toExtension - Extension to which to convert
297
     * @param string $documentRevisionId - Key for caching on service
298
     * @param string $region - Region
299
     *
300
     * @return string
301
     */
302
    public function getConvertedUri($documentUri, $fromExtension, $toExtension, $documentRevisionId, $region = null)
303
    {
304
        $responseFromConvertService = $this->sendRequestToConvertService(
305
            $documentUri,
306
            $fromExtension,
307
            $toExtension,
308
            $documentRevisionId,
309
            false,
310
            $region
311
        );
312
        // phpcs:ignore
313
        $errorElement = $responseFromConvertService->Error;
314
        if ($errorElement->count() > 0) {
315
            $this->processConvServResponceError($errorElement);
316
        }
317
318
        // phpcs:ignore
319
        $isEndConvert = $responseFromConvertService->EndConvert;
320
321
        if ($isEndConvert !== null && strtolower($isEndConvert) === "true") {
322
            // phpcs:ignore
323
            return is_string($responseFromConvertService->FileUrl) ? $responseFromConvertService->FileUrl : $responseFromConvertService->FileUrl->__toString();
324
        }
325
326
        return "";
327
    }
328
329
    /**
330
     * Send command
331
     *
332
     * @param string $method - type of command
333
     *
334
     * @return array
335
     */
336
    public function commandRequest($method)
337
    {
338
        $urlCommand = $this->settingsManager->getCommandServiceUrl(true);
339
        if (empty($urlCommand)) {
340
            throw new \Exception(CommonError::message(CommonError::NO_COMMAND_ENDPOINT));
341
        }
342
343
        $data = [
344
            "c" => $method
345
        ];
346
        $opts = [
347
            "headers" => [
348
                "Content-type" => "application/json"
349
            ],
350
            "body" => json_encode($data)
351
        ];
352
353
        if ($this->jwtManager->isJwtEnabled()) {
354
            $params = [
355
                "payload" => $data
356
            ];
357
            $token = $this->jwtManager->jwtEncode($params);
358
            $jwtHeader = $this->settingsManager->getJwtHeader();
359
            $jwtPrefix = $this->settingsManager->getJwtPrefix();
360
361
            if (empty($jwtHeader)) {
362
                throw new \Exception(CommonError::message(CommonError::NO_JWT_HEADER));
363
            } elseif (empty($jwtPrefix)) {
364
                throw new \Exception(CommonError::message(CommonError::NO_JWT_PREFIX));
365
            }
366
367
            $opts["headers"][$jwtHeader] = $jwtPrefix . $token;
368
            $token = $this->jwtManager->jwtEncode($data);
369
            $data["token"] = $token;
370
            $opts["body"] = json_encode($data);
371
        }
372
373
        $response = $this->request($urlCommand, "post", $opts);
374
375
        $data = json_decode($response);
376
        $this->processCommandServResponceError($data->error);
377
378
        return $data;
379
    }
380
381
    /**
382
     * Checking document service location
383
     *
384
     * @return array
385
     */
386
    public function checkDocServiceUrl()
387
    {
388
        $version = null;
389
        $documentServerUrl = $this->settingsManager->getDocumentServerUrl();
390
        if (empty($documentServerUrl)) {
391
            throw new \Exception(CommonError::message(CommonError::NO_DOCUMENT_SERVER_URL));
392
        }
393
394
        try {
395
            if ((isset($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == "on" || $_SERVER["HTTPS"] == 1)
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $_SERVER['...i', $documentServerUrl), Probably Intended Meaning: IssetNode && $_SERVER['H...', $documentServerUrl))
Loading history...
396
            || isset($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] == "https")
397
            && preg_match('/^http:\/\//i', $documentServerUrl)) {
398
                throw new \Exception(CommonError::message(CommonError::MIXED_CONTENT));
399
            }
400
        } catch (\Exception $e) {
401
            return [$e->getMessage(), $version];
402
        }
403
404
        try {
405
            $healthcheckResponse = $this->healthcheckRequest();
406
407
            if (!$healthcheckResponse) {
408
                throw new \Exception(CommonError::message(CommonError::BAD_HEALTHCHECK_STATUS));
409
            }
410
        } catch (\Exception $e) {
411
            return [$e->getMessage(), $version];
412
        }
413
414
        try {
415
            $commandResponse = $this->commandRequest('version');
416
417
            if (empty($commandResponse)) {
418
                throw new \Exception(CommonError::message(CommonError::BAD_HEALTHCHECK_STATUS));
419
            }
420
421
            $version = $commandResponse->version;
422
            $versionF = floatval($version);
423
424
            if ($versionF > 0.0 && $versionF <= self::MIN_EDITORS_VERSION) {
425
                throw new \Exception(CommonError::message(CommonError::NOT_SUPPORTED_VERSION));
426
            }
427
        } catch (\Exception $e) {
428
            return [$e->getMessage(), $version];
429
        }
430
431
        try {
432
            $fileUrl = $this->getFileUrlForConvert();
433
434
            if (!empty($fileUrl)) {
435
                if (!empty($this->settingsManager->getStorageUrl())) {
436
                    $fileUrl = str_replace(
437
                        $this->settingsManager->getServerUrl(),
438
                        $this->settingsManager->getStorageUrl(),
439
                        $fileUrl
440
                    );
441
                }
442
                $convertedFileUri = $this->getConvertedUri($fileUrl, "docx", "docx", "check_" . rand());
443
            }
444
        } catch (\Exception $e) {
445
            return [$e->getMessage(), $version];
446
        }
447
448
        try {
449
            $this->request($convertedFileUri);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $convertedFileUri does not seem to be defined for all execution paths leading up to this point.
Loading history...
450
        } catch (\Exception $e) {
451
            return [$e->getMessage(), $version];
452
        }
453
454
        return ["", $version];
455
    }
456
}
457