RestApiRequest::convertDataToArray()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.8437

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 4
nop 1
dl 0
loc 15
ccs 5
cts 8
cp 0.625
crap 4.8437
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Aoe\Restler\System\RestApi;
4
5
/***************************************************************
6
 *  Copyright notice
7
 *
8
 *  (c) 2024 AOE GmbH <[email protected]>
9
 *
10
 *  All rights reserved
11
 *
12
 *  This script is part of the TYPO3 project. The TYPO3 project is
13
 *  free software; you can redistribute it and/or modify
14
 *  it under the terms of the GNU General Public License as published by
15
 *  the Free Software Foundation; either version 3 of the License, or
16
 *  (at your option) any later version.
17
 *
18
 *  The GNU General Public License can be found at
19
 *  http://www.gnu.org/copyleft/gpl.html.
20
 *
21
 *  This script is distributed in the hope that it will be useful,
22
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 *  GNU General Public License for more details.
25
 *
26
 *  This copyright notice MUST APPEAR in all copies of the script!
27
 ***************************************************************/
28
29
use Aoe\Restler\System\TYPO3\Cache;
30
use Exception;
31
use Luracast\Restler\Defaults;
32
use Luracast\Restler\Format\JsonFormat;
33
use Luracast\Restler\RestException;
34
use Luracast\Restler\Restler;
35
use stdClass;
36
use TYPO3\CMS\Core\Utility\GeneralUtility;
37
38
/**
39
 * This class represents a single REST-API-request, which can be called from PHP.
40
 * For each REST-API request, we need a new object of this class, because restler stores some data in this object
41
 *
42
 * The logic/idea behind this class is:
43
 * 1. We override the data of $_GET, $_POST and $_SERVER and we override the REST-API-request-object (aka Restler-object)
44
 *    ...because this data/object 'defines' the REST-API-request, which we want to call
45
 * 2. We call/start restler to handle the REST-API-request
46
 * 3. We return the result of the 'called' REST-API-request - instead of sending the result to the client
47
 */
48
class RestApiRequest extends Restler
49
{
50
    /**
51
     * store data from $_GET in this property
52
     *
53
     * Attention:
54
     * This property must be static, because it can happen, that some REST-API-calls
55
     * will be called recursive, so we MUST store the 'really original' data
56
     */
57
    private static array $originalGetVars;
58
59
    /**
60
     * store data from $_POST in this property
61
     *
62
     * Attention:
63
     * This property must be static, because it can happen, that some REST-API-calls
64
     * will be called recursive, so we MUST store the 'really original' data
65
     */
66
    private static array $originalPostVars;
67
68
    /**
69
     * store data from $_SERVER in this property
70
     *
71
     * Attention:
72
     * This property must be static, because it can happen, that some REST-API-calls
73
     * will be called recursive, so we MUST store the 'really original' data
74
     */
75
    private static array $originalServerSettings;
76
77
    private array $restApiGetData;
78
79
    private array $restApiPostData;
80
81
    /**
82
     * This property defines the request-uri (without GET-params, e.g. '/api/products/320'), which should be called
83
     */
84
    private string $restApiRequestUri;
85
86
    /**
87
     * This property defines the request-method (e.g. 'GET', 'POST', 'PUT' or 'DELETE'), which should be used while calling the rest-api
88
     */
89
    private string $restApiRequestMethod;
90
91
    /***************************************************************************************************************************/
92
    /***************************************************************************************************************************/
93
    /* Block of methods, which MUST be overridden from parent-class (otherwise this class can not work) *************************/
94
    /***************************************************************************************************************************/
95
    /***************************************************************************************************************************/
96
    /**
97
     * Override parent method...because we don't want to call it!
98
     * The original method would set some properties (e.g. set this object into static properties of global classes)
99
     */
100
    public function __construct(
101 6
        private readonly RestApiRequestScope $restApiRequestScope,
102
        private readonly Cache $typo3Cache
103 6
    ) {
104 6
    }
105 6
106
    /**
107
     * Override parent method...because we don't want to call it (the original method would cache the determined routes)!
108
     */
109
    public function __destruct()
110
    {
111
    }
112
113
    /***************************************************************************************************************************/
114
    /***************************************************************************************************************************/
115
    /* Block of methods, which does NOT override logic from parent-class *******************************************************/
116
    /***************************************************************************************************************************/
117
    /***************************************************************************************************************************/
118
    /**
119
     * @param array|stdClass $getData
120
     * @param array|stdClass $postData
121
     * @return mixed can be a primitive or array or object
122
     */
123
    public function executeRestApiRequest(string $requestMethod, string $requestUri, $getData = null, $postData = null)
124
    {
125
        $this->restApiRequestMethod = $requestMethod;
126
        $this->restApiRequestUri = $requestUri;
127 6
        $this->restApiGetData = $this->convertDataToArray($getData);
128
        $this->restApiPostData = $this->convertDataToArray($postData);
129 6
130 6
        $this->storeOriginalRestApiRequest();
131 6
        $this->overrideOriginalRestApiRequest();
132 6
133
        try {
134 6
            $result = $this->handle();
135 6
            $this->restoreOriginalRestApiRequest();
136
            return $result;
137
        } catch (RestException $restException) {
138 6
            $this->restoreOriginalRestApiRequest();
139 2
            throw $restException;
140 2
        } catch (Exception $exception) {
141 4
            $this->restoreOriginalRestApiRequest();
142 2
            throw new RestException('500', $exception->getMessage(), [], $exception);
143 2
        }
144 2
    }
145 2
146 2
    /**
147
     * Override parent method...because we don't want to call it (the original method would send some headers to the client)!
148
     */
149
    public function composeHeaders(RestException $e = null)
150
    {
151
    }
152
153
    /**
154
     * Override parent method...because we must return the request-data of THIS REST-API request!
155
     * The original method would return the request-data of the ORIGINAL called REST-API request
156
     *
157
     * @param boolean $includeQueryParameters
158
     */
159
    public function getRequestData($includeQueryParameters = true): array
160
    {
161
        $requestData = [];
162
        if ($this->restApiRequestMethod === 'PUT' || $this->restApiRequestMethod === 'PATCH' || $this->restApiRequestMethod === 'POST') {
163
            $requestData = array_merge($this->restApiPostData, [Defaults::$fullRequestDataName => $this->restApiPostData]);
164
        }
165
166
        if ($includeQueryParameters) {
167
            return $requestData + $this->restApiGetData;
168
        }
169
170
        return $requestData;
171
    }
172
173
    /**
174
     * Override parent method...because we must return the data of the REST-API request and we need NO exception-handling!
175
     *
176
     * @return mixed can be a primitive or array or object
177
     */
178
    public function handle()
179
    {
180
        // get information about the REST-request
181
        $this->get();
182
183
        if ($this->requestMethod === 'GET' && $this->typo3Cache->hasCacheEntry($this->url, $_GET)) {
184
            return $this->handleRequestByTypo3Cache();
185
        }
186
187
        // if no cache exist: restler should handle the request
188
        if (Defaults::$useVendorMIMEVersioning) {
189
            $this->responseFormat = $this->negotiateResponseFormat();
190
        }
191
192
        $this->route();
193
        $this->negotiate();
194
        $this->preAuthFilter();
195
        $this->authenticate();
196
        $this->postAuthFilter();
197
        $this->validate();
198
        $this->preCall();
199
        $this->call();
200
        $this->compose();
201
        $this->postCall();
202
203
        if ($this->responseFormat instanceof JsonFormat) {
204
            // return stdClass-object (instead of an array)
205
            return $this->getRestApiJsonFormat()
206
                ->decode($this->responseData);
207
        }
208
209
        return $this->responseFormat->decode($this->responseData);
210
    }
211
212
    /**
213
     * Return class, which can decode a JSON-string into a stdClass-object (instead of an array)
214
     */
215
    protected function getRestApiJsonFormat(): RestApiJsonFormat
216
    {
217
        return GeneralUtility::makeInstance(RestApiJsonFormat::class);
218
    }
219
220
    /**
221
     * override postCall so that we can cache response via TYPO3-caching-framework - if it's possible
222
     */
223
    protected function postCall()
224
    {
225
        parent::postCall();
226
227
        if ($this->typo3Cache->isResponseCacheableByTypo3Cache($this->requestMethod, $this->apiMethodInfo->metadata)) {
228
            $this->typo3Cache->cacheResponseByTypo3Cache(
229
                $this->responseCode,
0 ignored issues
show
Bug introduced by
It seems like $this->responseCode can also be of type null; however, parameter $responseCode of Aoe\Restler\System\TYPO3...eResponseByTypo3Cache() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
                /** @scrutinizer ignore-type */ $this->responseCode,
Loading history...
230
                $this->url,
231
                $_GET,
232
                $this->apiMethodInfo->metadata,
233
                $this->responseData,
234
                $this->responseFormat::class,
235
                [] // we don't know which headers would be 'normally' send - because this is an internal REST-API-call
236
            );
237
        }
238
    }
239
240
    /**
241
     * @param array|stdClass $data
242
     * @return array
243
     */
244
    private function convertDataToArray($data)
245
    {
246 6
        if (!$data) {
247
            return [];
248 6
        }
249 6
250
        if (is_array($data)) {
251 3
            return $data;
252 3
        }
253
254
        if ($data instanceof stdClass) {
0 ignored issues
show
introduced by
$data is always a sub-type of stdClass.
Loading history...
255
            return json_decode(json_encode($data), true); // convert stdClass to array
256
        }
257
258
        return [];
259
    }
260
261
    /**
262
     * @return mixed
263
     */
264
    private function handleRequestByTypo3Cache()
265
    {
266
        $cacheEntry = $this->typo3Cache->getCacheEntry($this->url, $_GET);
267
        $this->responseCode = $cacheEntry['responseCode'];
268
        $this->responseData = $cacheEntry['responseData'];
269
        $this->responseFormat = new $cacheEntry['responseFormatClass']();
270
271
        // send data to client
272
        if ($this->responseFormat instanceof JsonFormat) {
273
            // return stdClass-object (instead of an array)
274
            return $this->getRestApiJsonFormat()
275
                ->decode($this->responseData);
276
        }
277
278
        return $this->responseFormat->decode($this->responseData);
279
    }
280
281
    /**
282
     * Override (the stored) data of $_GET, $_POST and $_SERVER (which are used in several restler-PHP-classes) and the original
283 6
     * REST-API-Request-object, because this data/object 'defines' the REST-API-request, which we want to call
284
     */
285 6
    private function overrideOriginalRestApiRequest(): void
286 6
    {
287 6
        $_GET = $this->restApiGetData;
288 6
        $_POST = $this->restApiPostData;
289
        $_SERVER['REQUEST_METHOD'] = $this->restApiRequestMethod;
290 6
        $_SERVER['REQUEST_URI'] = $this->restApiRequestUri;
291
292
        if ($this->restApiRequestMethod !== 'POST' && $this->restApiRequestMethod !== 'PUT') {
293 6
            // content-type and content-length should only exist when request-method is
294
            // POST or PUT (because in this case there can be the request-data in the body)
295
            if (array_key_exists('CONTENT_TYPE', $_SERVER)) {
296 6
                unset($_SERVER['CONTENT_TYPE']);
297
            }
298
299 6
            if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) {
300
                unset($_SERVER['HTTP_CONTENT_TYPE']);
301
            }
302
303
            if (array_key_exists('CONTENT_LENGTH', $_SERVER)) {
304 6
                unset($_SERVER['CONTENT_LENGTH']);
305 6
            }
306
        }
307
308
        $this->restApiRequestScope->overrideOriginalRestApiRequest($this);
309
        $this->restApiRequestScope->removeRestApiAuthenticationObjects();
310
311
        /**
312 6
         * add all authentication-classes:
313 6
         *  - we must add all authentication-classes, because the authentication-classes are stored in this object
314 6
         *  - we don't must add all REST-API-classes, because the REST-API-classes are not stored in this object
315
         */
316
        $this->authClasses = $this->restApiRequestScope->getOriginalRestApiRequest()
317
            ->_authClasses ?? [];
318
    }
319 6
320
    /**
321 6
     * Restore (the overridden) data of $_GET, $_POST and $_SERVER and the original REST-API-request-object
322 6
     */
323 6
    private function restoreOriginalRestApiRequest(): void
324 6
    {
325 6
        $_GET = self::$originalGetVars;
326 6
        $_POST = self::$originalPostVars;
327
        $_SERVER = self::$originalServerSettings;
328
        $this->restApiRequestScope->restoreOriginalRestApiRequest();
329
        $this->restApiRequestScope->restoreOriginalRestApiAuthenticationObjects();
330
    }
331 6
332
    /**
333 6
     * Store (the original) data of $_GET, $_POST and $_SERVER and the original REST-API-request-object
334 1
     */
335 1
    private function storeOriginalRestApiRequest(): void
336 1
    {
337 1
        if (!isset(self::$originalServerSettings)) {
338 1
            self::$originalGetVars = $_GET;
339
            self::$originalPostVars = $_POST;
340 6
            self::$originalServerSettings = $_SERVER;
341
            $this->restApiRequestScope->storeOriginalRestApiRequest();
342
            $this->restApiRequestScope->storeOriginalRestApiAuthenticationObjects();
343
        }
344
    }
345
}
346