Issues (19)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Builder.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace TomHart\Restful;
4
5
use DebugBar\DataCollector\MessagesCollector;
6
use DebugBar\DebugBar;
7
use DebugBar\DebugBarException;
8
use GuzzleHttp\Client;
9
use Illuminate\Contracts\Container\BindingResolutionException;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Query\Grammars\Grammar;
12
use Illuminate\Support\Collection;
13
use InvalidArgumentException;
14
use Psr\Http\Message\MessageInterface;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\HttpFoundation\Response;
17
use Symfony\Component\Routing\Exception\RouteNotFoundException;
18
use TomHart\Restful\Concerns\Restful;
19
use TomHart\Restful\Concerns\Transformer;
20
use TomHart\Restful\Routing\Route;
21
22
use function GuzzleHttp\json_decode as guzzle_json_decode;
23
24
class Builder
25
{
26
    /**
27
     * @var Restful
28
     */
29
    protected $model;
30
31
    /**
32
     * @var Client
33
     */
34
    protected $client;
35
36
    /**
37
     * @var Grammar
38
     */
39
    protected $grammar;
40
41
    /**
42
     * @var mixed[]
43
     */
44
    protected $wheres = [];
45
46
    /**
47
     * @var string[]
48
     */
49
    private $order;
50
51
    /**
52
     * @var int
53
     */
54
    private $limit;
55
56
    /**
57
     * @var int[]
58
     */
59
    private $goodStatusCodes = [
60
        Response::HTTP_OK,
61
        Response::HTTP_CREATED,
62
        Response::HTTP_ACCEPTED
63
    ];
64
    /**
65
     * @var LoggerInterface
66
     */
67
    private $logger;
68
69
    /**
70
     * Builder constructor.
71
     * @param Client $client
72
     * @param LoggerInterface $logger
73
     * @param Restful $model
74
     */
75
    public function __construct(Client $client, LoggerInterface $logger, Restful $model)
76
    {
77
        $this->model = $model;
78
        $this->client = $client;
79
        $this->logger = $logger;
80
    }
81
82
    /**
83
     * What model are we querying?
84
     * @param string $class
85
     * @return Builder
86
     */
87
    public static function model(string $class)
88
    {
89
        $class = new $class();
90
        return app(static::class, ['model' => $class]);
91
    }
92
93
    /**
94
     * Returns a new instance of the model
95
     * @return Model
96
     */
97
    private function newModelInstance(): Model
98
    {
99
        return new $this->model();
100
    }
101
102
    /**
103
     * Get the index route for the model via the options route.
104
     * @param string $route
105
     * @param null $id
106
     * @return Route
107
     * @throws Exceptions\UndefinedIndexException
108
     * @throws RouteNotFoundException
109
     */
110
    private function getModelRoute(string $route, $id = null): Route
111
    {
112
        $optionRoute = $this->model->getOptionsRoute();
113
        $links = $this->getResponse($optionRoute, ['id' => $id]);
114
        if (!$links) {
115
            throw new RouteNotFoundException('Cannot get options from route');
116
        }
117
118
        $links = $this->parseJsonFromResponse($links);
119
        if (!isset($links[$route])) {
120
            throw new RouteNotFoundException("Cannot find link to $route route");
121
        }
122
123
        return Route::fromArray($links[$route]);
124
    }
125
126
    /**
127
     * Get the JSON from the given URL.
128
     * @param Route $route
129
     * @param array $queryString
130
     * @param array $postData
131
     * @return MessageInterface
132
     * @throws Exceptions\UndefinedIndexException
133
     */
134
    private function getResponse(Route $route, array $queryString = [], array $postData = []): ?MessageInterface
135
    {
136
        if ($domain = config('restful.api_domain')) {
137
            $url = $domain . $route->getHrefs('relative', $queryString);
138
        } else {
139
            $url = $route->getHrefs('absolute', $queryString);
140
        }
141
142
        if ($this->shouldLog()) {
143
            $this->logger->info(sprintf('REST-CALL: %s to %s', $route->getMethod(), $url));
144
145
            try {
146
                /** @var DebugBar $debugBar */
147
                $debugBar = app('debugbar');
148
149
                /** @var MessagesCollector $collector */
150
                $collector = $debugBar->getCollector('restful_calls');
151
                $collector->addMessage(sprintf('REST-CALL: %s to %s', $route->getMethod(), $url));
152
            } catch (DebugBarException|BindingResolutionException $e) {
153
            }
154
        }
155
156
        return $this->client->request(
157
            $route->getMethod(),
158
            $url,
0 ignored issues
show
It seems like $url defined by $route->getHrefs('absolute', $queryString) on line 139 can also be of type array<integer,string>; however, GuzzleHttp\Client::request() does only seem to accept string|object<Psr\Http\Message\UriInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
159
            [
160
                'headers' => config('restful.headers'),
161
                'form_params' => $postData
162
            ]
163
        );
164
    }
165
166
    /**
167
     * Extract JSON from a Response.
168
     * @param MessageInterface $response
169
     * @return mixed
170
     * @throws Exceptions\UndefinedIndexException
171
     */
172
    private function parseJsonFromResponse(MessageInterface $response)
173
    {
174
        $body = $response->getBody()->getContents();
175
        if (!$body) {
176
            return null;
177
        }
178
179
        $json = guzzle_json_decode($body, true);
180
181
        // If the response is paginated, and there is a next page, get that and add the results.
182
        if ($this->isPaginatedResponse($json) &&
183
            isset($json['next_page_url'], $json['current_page'], $json['last_page']) &&
184
            $json['current_page'] < $json['last_page']
185
        ) {
186
            $nextPageResponse = $this->getResponse(Route::fromUrl($json['next_page_url'], 'GET'));
187
            if ($nextPageResponse) {
188
                $nextPageJson = $this->parseJsonFromResponse($nextPageResponse);
189
190
                $json['data'] = array_merge($json['data'], $nextPageJson['data']);
191
192
                return $json;
193
            }
194
        }
195
196
        return $json;
197
    }
198
199
    /**
200
     * Is the JSON responses a paginatable one?
201
     * @param mixed[] $json
202
     * @return bool
203
     */
204
    private function isPaginatedResponse(array $json): bool
205
    {
206
        return empty(array_diff(config('restful.pagination_json_keys'), array_keys($json)));
207
    }
208
209
    /**
210
     * Extract data from the JSON
211
     * @param $json
212
     * @return array
213
     * @throws InvalidArgumentException
214
     */
215
    private function extractDataFromJson($json): array
216
    {
217
        if (!isset($json['data'])) {
218
            throw new InvalidArgumentException('$json doesn\'t contain a data key');
219
        }
220
221
        return $json['data'];
222
    }
223
224
    /**
225
     * Add a where clause.
226
     * @param string $column
227
     * @param $value
228
     * @return Builder
229
     */
230
    public function where(string $column, $value): self
231
    {
232
        $this->wheres[] = [
233
            'column' => $column,
234
            'type' => 'Basic',
235
            'value' => $value
236
        ];
237
238
        return $this;
239
    }
240
241
    /**
242
     * Add a whereIn clause.
243
     * @param string $column
244
     * @param mixed[] $values
245
     * @return Builder
246
     */
247
    public function whereIn(string $column, array $values): self
248
    {
249
        $this->wheres[] = [
250
            'column' => $column,
251
            'type' => 'In',
252
            'values' => $values
253
        ];
254
255
        return $this;
256
    }
257
258
    /**
259
     * Return all the wheres
260
     * @return mixed[]
261
     */
262
    public function getWheres(): array
263
    {
264
        return $this->wheres;
265
    }
266
267
    /**
268
     * Set an order
269
     * @param string $column
270
     * @param string $direction
271
     * @return $this\
272
     */
273
    public function order(string $column, string $direction): self
274
    {
275
        $this->order = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('column' => $colum...rection' => $direction) of type array<string,string,{"co...,"direction":"string"}> is incompatible with the declared type array<integer,string> of property $order.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
276
            'column' => $column,
277
            'direction' => $direction
278
        ];
279
280
        return $this;
281
    }
282
283
    /**
284
     * Get the order
285
     * @return array|null
286
     */
287
    public function getOrder(): ?array
288
    {
289
        return $this->order;
290
    }
291
292
    /**
293
     * Specify a limit
294
     * @param int $limit
295
     * @return $this
296
     */
297
    public function limit(int $limit): self
298
    {
299
        $this->limit = $limit;
300
301
        return $this;
302
    }
303
304
    /**
305
     * Get the limit
306
     * @return int|null
307
     */
308
    public function getLimit(): ?int
309
    {
310
        return $this->limit;
311
    }
312
313
    /**
314
     * Call the API, and return the models.
315
     * @return Collection
316
     * @throws Exceptions\UndefinedIndexException
317
     */
318
    public function get(): Collection
319
    {
320
        $options = $this->getModelRoute('index');
321
322
        /** @var Transformer $transformer */
323
        $transformer = app(Transformer::class);
324
325
        $queryString = $transformer->buildQueryString($this);
326
327
        $response = $this->getResponse($options, $queryString);
328
        if (!$response) {
329
            return collect();
330
        }
331
332
        $json = $this->parseJsonFromResponse($response);
333
        if (!$json) {
334
            return collect();
335
        }
336
337
        $data = $this->extractDataFromJson($json);
338
339
        $instance = $this->newModelInstance();
340
341
        return $instance->newCollection(
342
            array_map(
343
                function ($item) use ($instance) {
344
                    return $instance->newFromBuilder($item);
345
                },
346
                $data
347
            )
348
        );
349
    }
350
351
    /**
352
     * @param string $route
353
     * @param mixed[] $data
354
     * @param mixed $id
355
     * @return bool
356
     * @throws Exceptions\UndefinedIndexException
357
     */
358
    private function _call(string $route, array $data, $id = null): bool
359
    {
360
        $route = $this->getModelRoute($route, $id);
361
        $response = $this->getResponse($route, [], $data);
362
        if (!$response) {
363
            return false;
364
        }
365
366
        return in_array($response->getStatusCode(), $this->goodStatusCodes, true);
367
    }
368
369
    /**
370
     * @param mixed[] $data
371
     * @return bool
372
     * @throws Exceptions\UndefinedIndexException
373
     */
374
    public function insert(array $data): bool
375
    {
376
        return $this->_call('store', $data);
377
    }
378
379
    /**
380
     * @param mixed $id
381
     * @param mixed[] $data
382
     * @return bool
383
     * @throws Exceptions\UndefinedIndexException
384
     */
385
    public function update($id, array $data): bool
386
    {
387
        return $this->_call('update', $data, $id);
388
    }
389
390
    /**
391
     * @param mixed $id
392
     * @return bool
393
     * @throws Exceptions\UndefinedIndexException
394
     */
395
    public function delete($id): bool
396
    {
397
        return $this->_call('destroy', [], $id);
398
    }
399
400
    /**
401
     * Should we log requests?
402
     * @return bool
403
     */
404
    private function shouldLog(): bool
405
    {
406
        return !!config('restful.logging', false);
407
    }
408
}
409