Passed
Push — main ( 63b0eb...e16704 )
by Felix
03:05
created

RestApiRequest   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 319
Duplicated Lines 0 %

Test Coverage

Coverage 46.9%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 104
c 2
b 0
f 1
dl 0
loc 319
ccs 53
cts 113
cp 0.469
rs 9.68
wmc 34

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getRequestData() 0 11 5
A convertDataToArray() 0 12 4
A postCall() 0 13 2
A getRestApiJsonFormat() 0 3 1
A overrideOriginalRestApiRequest() 0 31 6
A restoreOriginalRestApiRequest() 0 7 1
A executeRestApiRequest() 0 20 3
A __destruct() 0 2 1
A handleRequestByTypo3Cache() 0 14 2
A __construct() 0 4 1
A composeHeaders() 0 2 1
A storeOriginalRestApiRequest() 0 8 2
A handle() 0 30 5
1
<?php
2
3
namespace Aoe\Restler\System\RestApi;
4
5
/***************************************************************
6
 *  Copyright notice
7
 *
8
 *  (c) 2015 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 as Typo3Cache;
30
use Luracast\Restler\Restler;
31
use Luracast\Restler\RestException;
32
use Luracast\Restler\Defaults;
33
use Luracast\Restler\Format\JsonFormat;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
use Exception;
36
use stdClass;
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
     * @var array
58
     */
59
    private static $originalGetVars;
60
    /**
61
     * store data from $_POST in this property
62
     *
63
     * Attention:
64
     * This property must be static, because it can happen, that some REST-API-calls
65
     * will be called recursive, so we MUST store the 'really original' data
66
     *
67
     * @var array
68
     */
69
    private static $originalPostVars;
70
    /**
71
     * store data from $_SERVER in this property
72
     *
73
     * Attention:
74
     * This property must be static, because it can happen, that some REST-API-calls
75
     * will be called recursive, so we MUST store the 'really original' data
76
     *
77
     * @var array
78
     */
79
    private static $originalServerSettings;
80
81
    /**
82
     * @var array
83
     */
84
    private $restApiGetData;
85
    /**
86
     * @var array
87
     */
88
    private $restApiPostData;
89
    /**
90
     * This property defines the request-uri (without GET-params, e.g. '/api/products/320'), which should be called
91
     *
92
     * @var string
93
     */
94
    private $restApiRequestUri;
95
    /**
96
     * This property defines the request-method (e.g. 'GET', 'POST', 'PUT' or 'DELETE'), which should be used while calling the rest-api
97
     *
98
     * @var string
99
     */
100
    private $restApiRequestMethod;
101
    /**
102
     * @var RestApiRequestScope
103
     */
104
    private $restApiRequestScope;
105
    /**
106
     * @var Typo3Cache
107
     */
108
    private $typo3Cache;
109
110
111
112
    /***************************************************************************************************************************/
113
    /***************************************************************************************************************************/
114
    /* Block of methods, which MUST be overriden from parent-class (otherwise this class can not work) *************************/
115
    /***************************************************************************************************************************/
116
    /***************************************************************************************************************************/
117
    /**
118
     * Override parent method...because we don't want to call it!
119
     * The original method would set some properties (e.g. set this object into static properties of global classes)
120
     *
121
     * @param RestApiRequestScope $restApiRequestScope
122
     * @param Typo3Cache $typo3Cache
123
     */
124 6
    public function __construct(RestApiRequestScope $restApiRequestScope, Typo3Cache $typo3Cache)
125
    {
126 6
        $this->restApiRequestScope = $restApiRequestScope;
127 6
        $this->typo3Cache = $typo3Cache;
128 6
    }
129
130
    /**
131
     * Override parent method...because we don't want to call it (the original method would cache the determined routes)!
132
     */
133
    public function __destruct()
134
    {
135
    }
136
137
138
139
    /***************************************************************************************************************************/
140
    /***************************************************************************************************************************/
141
    /* Block of methods, which does NOT override logic from parent-class *******************************************************/
142
    /***************************************************************************************************************************/
143
    /***************************************************************************************************************************/
144
    /**
145
     * @param string $requestMethod
146
     * @param string $requestUri
147
     * @param array|stdClass $getData
148
     * @param array|stdClass $postData
149
     * @return mixed can be a primitive or array or object
150
     * @throws RestException
151
     */
152 6
    public function executeRestApiRequest($requestMethod, $requestUri, $getData = null, $postData = null)
153
    {
154 6
        $this->restApiRequestMethod = $requestMethod;
155 6
        $this->restApiRequestUri = $requestUri;
156 6
        $this->restApiGetData = $this->convertDataToArray($getData);
157 6
        $this->restApiPostData = $this->convertDataToArray($postData);
158
159 6
        $this->storeOriginalRestApiRequest();
160 6
        $this->overrideOriginalRestApiRequest();
161
162
        try {
163 6
            $result = $this->handle();
164 2
            $this->restoreOriginalRestApiRequest();
165 2
            return $result;
166 4
        } catch (RestException $e) {
167 2
            $this->restoreOriginalRestApiRequest();
168 2
            throw $e;
169 2
        } catch (Exception $e) {
170 2
            $this->restoreOriginalRestApiRequest();
171 2
            throw new RestException(500, $e->getMessage(), [], $e);
172
        }
173
    }
174
175
    /**
176
     * Override parent method...because we don't want to call it (the original method would send some headers to the client)!
177
     */
178
    public function composeHeaders(RestException $e = null)
179
    {
180
    }
181
182
    /**
183
     * Override parent method...because we must return the request-data of THIS REST-API request!
184
     * The original method would return the request-data of the ORIGINAL called REST-API request
185
     *
186
     * @param boolean $includeQueryParameters
187
     * @return array
188
     */
189
    public function getRequestData($includeQueryParameters = true)
190
    {
191
        $requestData = [];
192
        if ($this->restApiRequestMethod == 'PUT' || $this->restApiRequestMethod == 'PATCH' || $this->restApiRequestMethod == 'POST') {
193
            $requestData = array_merge($this->restApiPostData, [Defaults::$fullRequestDataName => $this->restApiPostData]);
194
        }
195
196
        if ($includeQueryParameters === true) {
197
            return $requestData + $this->restApiGetData;
198
        }
199
        return $requestData;
200
    }
201
202
    /**
203
     * Override parent method...because we must return the data of the REST-API request and we need NO exception-handling!
204
     *
205
     * @return mixed can be a primitive or array or object
206
     */
207
    public function handle()
208
    {
209
        // get information about the REST-request
210
        $this->get();
211
212
        if ($this->requestMethod === 'GET' && $this->typo3Cache->hasCacheEntry($this->url, $_GET)) {
213
            return $this->handleRequestByTypo3Cache();
214
        }
215
216
        // if no cache exist: restler should handle the request
217
        if (Defaults::$useVendorMIMEVersioning) {
218
            $this->responseFormat = $this->negotiateResponseFormat();
219
        }
220
        $this->route();
221
        $this->negotiate();
222
        $this->preAuthFilter();
223
        $this->authenticate();
224
        $this->postAuthFilter();
225
        $this->validate();
226
        $this->preCall();
227
        $this->call();
228
        $this->compose();
229
        $this->postCall();
230
231
        if ($this->responseFormat instanceof JsonFormat) {
232
            // return stdClass-object (instead of an array)
233
            return $this->getRestApiJsonFormat()
234
                ->decode($this->responseData);
235
        }
236
        return $this->responseFormat->decode($this->responseData);
237
    }
238
239
    /**
240
     * Return class, which can decode a JSON-string into a stdClass-object (instead of an array)
241
     *
242
     * @return RestApiJsonFormat
243
     */
244
    protected function getRestApiJsonFormat()
245
    {
246
        return GeneralUtility::makeInstance(RestApiJsonFormat::class);
247
    }
248
249
    /**
250
     * override postCall so that we can cache response via TYPO3-caching-framework - if it's possible
251
     */
252
    protected function postCall()
253
    {
254
        parent::postCall();
255
256
        if ($this->typo3Cache->isResponseCacheableByTypo3Cache($this->requestMethod, $this->apiMethodInfo->metadata)) {
257
            $this->typo3Cache->cacheResponseByTypo3Cache(
258
                $this->responseCode,
259
                $this->url,
260
                $_GET,
261
                $this->apiMethodInfo->metadata,
262
                $this->responseData,
263
                get_class($this->responseFormat),
264
                [] // we don't know which headers would be 'normally' send - because this is an internal REST-API-call
265
            );
266
        }
267
    }
268
269
    /**
270
     * @param array|stdClass $data
271
     * @return array
272
     * @throws RestException
273
     */
274 6
    private function convertDataToArray($data)
275
    {
276 6
        if ($data === null) {
277 3
            return [];
278
        }
279 3
        if (is_array($data)) {
280 3
            return $data;
281
        }
282
        if ($data instanceof stdClass) {
0 ignored issues
show
introduced by
$data is always a sub-type of stdClass.
Loading history...
283
            return json_decode(json_encode($data), true); // convert stdClass to array
284
        }
285
        throw new RestException(500, 'data must be type of null, array or stdClass');
286
    }
287
288
    /**
289
     * @return string
290
     */
291
    private function handleRequestByTypo3Cache()
292
    {
293
        $cacheEntry = $this->typo3Cache->getCacheEntry($this->url, $_GET);
294
        $this->responseCode = $cacheEntry['responseCode'];
295
        $this->responseData = $cacheEntry['responseData'];
296
        $this->responseFormat = new $cacheEntry['responseFormatClass']();
297
298
        // send data to client
299
        if ($this->responseFormat instanceof JsonFormat) {
300
            // return stdClass-object (instead of an array)
301
            return $this->getRestApiJsonFormat()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getRestApi...de($this->responseData) returns the type stdClass which is incompatible with the documented return type string.
Loading history...
302
                ->decode($this->responseData);
303
        }
304
        return $this->responseFormat->decode($this->responseData);
305
    }
306
307
    /**
308
     * Override (the stored) data of $_GET, $_POST and $_SERVER (which are used in several restler-PHP-classes) and the original
309
     * REST-API-Request-object, because this data/object 'defines' the REST-API-request, which we want to call
310
     */
311 6
    private function overrideOriginalRestApiRequest()
312
    {
313 6
        $_GET = $this->restApiGetData;
314 6
        $_POST = $this->restApiPostData;
315 6
        $_SERVER['REQUEST_METHOD'] = $this->restApiRequestMethod;
316 6
        $_SERVER['REQUEST_URI'] = $this->restApiRequestUri;
317
318 6
        if ($this->restApiRequestMethod !== 'POST' && $this->restApiRequestMethod !== 'PUT') {
319
            // content-type and content-length should only exist when request-method is
320
            // POST or PUT (because in this case there can be the request-data in the body)
321 6
            if (array_key_exists('CONTENT_TYPE', $_SERVER)) {
322
                unset($_SERVER['CONTENT_TYPE']);
323
            }
324 6
            if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) {
325
                unset($_SERVER['HTTP_CONTENT_TYPE']);
326
            }
327 6
            if (array_key_exists('CONTENT_LENGTH', $_SERVER)) {
328
                unset($_SERVER['CONTENT_LENGTH']);
329
            }
330
        }
331
332 6
        $this->restApiRequestScope->overrideOriginalRestApiRequest($this);
333 6
        $this->restApiRequestScope->removeRestApiAuthenticationObjects();
334
335
        /**
336
         * add all authentication-classes:
337
         *  - we must add all authentication-classes, because the authentication-classes are stored in this object
338
         *  - we don't must add all REST-API-classes, because the REST-API-classes are not stored in this object
339
         */
340 6
        $this->authClasses = $this->restApiRequestScope->getOriginalRestApiRequest()
341 6
            ->_authClasses ?? [];
342 6
    }
343
344
    /**
345
     * Restore (the overridden) data of $_GET, $_POST and $_SERVER and the original REST-API-request-object
346
     */
347 6
    private function restoreOriginalRestApiRequest()
348
    {
349 6
        $_GET = self::$originalGetVars;
350 6
        $_POST = self::$originalPostVars;
351 6
        $_SERVER = self::$originalServerSettings;
352 6
        $this->restApiRequestScope->restoreOriginalRestApiRequest();
353 6
        $this->restApiRequestScope->restoreOriginalRestApiAuthenticationObjects();
354 6
    }
355
356
    /**
357
     * Store (the original) data of $_GET, $_POST and $_SERVER and the original REST-API-request-object
358
     */
359 6
    private function storeOriginalRestApiRequest()
360
    {
361 6
        if (isset(self::$originalServerSettings) === false) {
362 1
            self::$originalGetVars = $_GET;
363 1
            self::$originalPostVars = $_POST;
364 1
            self::$originalServerSettings = $_SERVER;
365 1
            $this->restApiRequestScope->storeOriginalRestApiRequest();
366 1
            $this->restApiRequestScope->storeOriginalRestApiAuthenticationObjects();
367
        }
368 6
    }
369
}
370