This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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 BadMethodCallException; |
||
6 | use Exception; |
||
7 | use Illuminate\Contracts\Pagination\LengthAwarePaginator; |
||
8 | use Illuminate\Contracts\Routing\ResponseFactory; |
||
9 | use Illuminate\Contracts\View\Factory; |
||
10 | use Illuminate\Database\Eloquent\Builder; |
||
0 ignored issues
–
show
|
|||
11 | use Illuminate\Database\Eloquent\Collection; |
||
12 | use Illuminate\Database\Eloquent\Model; |
||
13 | use Illuminate\Database\Eloquent\Relations\BelongsToMany; |
||
14 | use Illuminate\Database\Eloquent\Relations\HasMany; |
||
15 | use Illuminate\Database\Eloquent\Relations\HasOneOrMany; |
||
16 | use Illuminate\Database\Eloquent\Relations\MorphMany; |
||
17 | use Illuminate\Database\Eloquent\Relations\MorphOneOrMany; |
||
18 | use Illuminate\Database\Eloquent\Relations\MorphToMany; |
||
19 | use Illuminate\Database\Eloquent\Relations\Relation; |
||
20 | use Illuminate\Foundation\Auth\Access\AuthorizesRequests; |
||
21 | use Illuminate\Foundation\Validation\ValidatesRequests; |
||
22 | use Illuminate\Http\JsonResponse; |
||
23 | use Illuminate\Http\RedirectResponse; |
||
24 | use Illuminate\Http\Request; |
||
25 | use Illuminate\Http\Response; |
||
26 | use Illuminate\Routing\Controller as BaseController; |
||
27 | use Illuminate\Routing\Redirector; |
||
28 | use Illuminate\Routing\Route; |
||
29 | use Illuminate\Support\Str; |
||
30 | use Illuminate\View\View; |
||
31 | use InvalidArgumentException; |
||
32 | use Symfony\Component\HttpFoundation\Response as SymResponse; |
||
33 | use TomHart\Restful\Concerns\HasLinks; |
||
34 | |||
35 | abstract class AbstractRestfulController extends BaseController |
||
36 | { |
||
37 | use AuthorizesRequests; |
||
38 | use ValidatesRequests; |
||
39 | |||
40 | /** |
||
41 | * The views to render. |
||
42 | * @var string[] |
||
43 | */ |
||
44 | protected $views = []; |
||
45 | |||
46 | /** |
||
47 | * Return a list of matching models. |
||
48 | * @param Request $request |
||
49 | * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector |
||
50 | */ |
||
51 | public function index(Request $request) |
||
52 | { |
||
53 | $builder = $this->createModelQueryBuilder(); |
||
54 | |||
55 | $input = collect($request->input())->except(config('restful.query_string_keys.page'))->toArray(); |
||
56 | foreach ($input as $column => $value) { |
||
57 | $this->filterValue($builder, $column, $value); |
||
58 | } |
||
59 | |||
60 | $data = $builder->paginate( |
||
61 | $request->input('limit', null), |
||
62 | ['*'], |
||
63 | config('restful.query_string_keys.page') |
||
64 | ); |
||
65 | |||
66 | return $this->return($request, $data, 'index'); |
||
67 | } |
||
68 | |||
69 | /** |
||
70 | * Generate a new query builder for the model. |
||
71 | * @return Builder |
||
72 | */ |
||
73 | protected function createModelQueryBuilder(): Builder |
||
74 | { |
||
75 | return $this->newModelInstance()->newQuery(); |
||
76 | } |
||
77 | |||
78 | /** |
||
79 | * Creates a new model instance. |
||
80 | * @return Model |
||
81 | */ |
||
82 | protected function newModelInstance(): Model |
||
83 | { |
||
84 | $classFQDN = $this->getModelClass(); |
||
85 | |||
86 | return app($classFQDN); |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * What Model class to search for entities. |
||
91 | * @return string |
||
92 | */ |
||
93 | abstract protected function getModelClass(): string; |
||
94 | |||
95 | /** |
||
96 | * Apply causes to the builder. |
||
97 | * @param Builder $builder |
||
98 | * @param string $column |
||
99 | * @param mixed $value |
||
100 | */ |
||
101 | protected function filterValue(Builder $builder, string $column, $value): void |
||
102 | { |
||
103 | $builder->where($column, $value); |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * Build and return a response. |
||
108 | * @param Request $request |
||
109 | * @param mixed $data |
||
110 | * @param string $method |
||
111 | * @return JsonResponse|ResponseFactory|Response|RedirectResponse|Redirector |
||
112 | */ |
||
113 | protected function return(Request $request, $data, string $method) |
||
114 | { |
||
115 | $status = SymResponse::HTTP_OK; |
||
116 | switch ($method) { |
||
117 | case 'store': |
||
118 | $status = SymResponse::HTTP_CREATED; |
||
119 | break; |
||
120 | } |
||
121 | |||
122 | if ($request->wantsJson()) { |
||
123 | return app(ResponseFactory::class)->json($data, $status); |
||
124 | } |
||
125 | |||
126 | if (isset($this->views[$method]) && app(Factory::class)->exists($this->views[$method])) { |
||
127 | /** @var View $view */ |
||
128 | $view = view( |
||
129 | $this->views[$method], |
||
130 | [ |
||
131 | 'data' => $data |
||
132 | ] |
||
133 | ); |
||
134 | |||
135 | return response($view, $status); |
||
136 | } |
||
137 | |||
138 | switch ($method) { |
||
139 | case 'store': |
||
140 | case 'update': |
||
141 | // If it's store/update, and the user isn't asking for JSON, we want to |
||
142 | // try and redirect them to the related show record page. |
||
143 | if (($redirect = $this->redirectToShowRoute($request, $data))) { |
||
144 | return $redirect; |
||
145 | } |
||
146 | } |
||
147 | |||
148 | return app(ResponseFactory::class)->json($data, $status); |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * Redirects to the show route for the model if one exists. |
||
153 | * @param Request $request |
||
154 | * @param mixed $data |
||
155 | * @return RedirectResponse|Redirector|null |
||
156 | */ |
||
157 | protected function redirectToShowRoute(Request $request, $data) |
||
158 | { |
||
159 | if ($data instanceof HasLinks) { |
||
160 | $route = $data->getRouteName() . '.show'; |
||
161 | $key = $data->getRouteKey(); |
||
162 | |||
163 | return redirect( |
||
164 | route( |
||
165 | $route, |
||
166 | [ |
||
167 | $key => $data->id |
||
0 ignored issues
–
show
Accessing
id on the interface TomHart\Restful\Concerns\HasLinks suggest that you code against a concrete implementation. How about adding an instanceof check?
If you access a property on an interface, you most likely code against a concrete implementation of the interface. Available Fixes
![]() |
|||
168 | ] |
||
169 | ) |
||
170 | ); |
||
171 | } |
||
172 | |||
173 | /** @var Route|null $route */ |
||
174 | $route = $request->route(); |
||
175 | if (!$route) { |
||
176 | return null; |
||
177 | } |
||
178 | |||
179 | $name = $route->getName(); |
||
180 | if (!$name) { |
||
0 ignored issues
–
show
The expression
$name of type string|null is loosely compared to false ; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
181 | return null; |
||
182 | } |
||
183 | |||
184 | $exploded = explode('.', $name); |
||
185 | array_pop($exploded); |
||
186 | $topLevel = array_pop($exploded); |
||
187 | |||
188 | if (!$topLevel) { |
||
189 | return null; |
||
190 | } |
||
191 | |||
192 | $key = Str::singular(str_replace('-', '_', $topLevel)); |
||
193 | |||
194 | return redirect( |
||
195 | route( |
||
196 | "$topLevel.show", |
||
197 | [ |
||
198 | $key => $data->id |
||
199 | ] |
||
200 | ) |
||
201 | ); |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * Handles creating a model. The C of CRUD |
||
206 | * @param Request $request |
||
207 | * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector |
||
208 | */ |
||
209 | View Code Duplication | public function store(Request $request) |
|
210 | { |
||
211 | $model = $this->newModelInstance(); |
||
212 | |||
213 | foreach ((array)$request->input() as $column => $value) { |
||
214 | $model->$column = $value; |
||
215 | } |
||
216 | |||
217 | $this->saveModel($model); |
||
218 | |||
219 | return $this->return($request, $this->findModel($model->getAttribute('id')), 'store'); |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * Save a model, either from a store or an update. |
||
224 | * @param Model $model |
||
225 | * @return bool |
||
226 | */ |
||
227 | protected function saveModel(Model $model) |
||
228 | { |
||
229 | return $model->save(); |
||
230 | } |
||
231 | |||
232 | /** |
||
233 | * Finds the model instance. |
||
234 | * @param int|string $id |
||
235 | * @param Request|null $request |
||
236 | * @return Model |
||
237 | */ |
||
238 | protected function findModel($id, Request $request = null): Model |
||
239 | { |
||
240 | $builder = $this->createModelQueryBuilder(); |
||
241 | |||
242 | if ($request) { |
||
243 | $this->preloadRelationships($builder, $request); |
||
244 | } |
||
245 | |||
246 | |||
247 | $builder = $builder->findOrFail($id); |
||
248 | return $builder; |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * Preload any relationships required. |
||
253 | * @param Builder $builder |
||
254 | * @param Request $request |
||
255 | * @return void |
||
256 | */ |
||
257 | protected function preloadRelationships(Builder &$builder, Request $request): void |
||
258 | { |
||
259 | $headers = $request->headers; |
||
260 | if (!$headers) { |
||
261 | return; |
||
262 | } |
||
263 | |||
264 | $header = $headers->get('X-Load-Relationship'); |
||
265 | if (!$header || empty($header)) { |
||
266 | return; |
||
267 | } |
||
268 | |||
269 | $relationships = array_filter(explode(',', $header)); |
||
270 | |||
271 | $builder = $builder->with($relationships); |
||
272 | } |
||
273 | |||
274 | /** |
||
275 | * Shows a model. The R of CRUD. |
||
276 | * @param Request $request |
||
277 | * @param int $id |
||
278 | * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector |
||
279 | */ |
||
280 | public function show(Request $request, $id) |
||
281 | { |
||
282 | $model = $this->findModel($id, $request); |
||
283 | |||
284 | $model = $this->iterateThroughChildren($model, $request); |
||
285 | |||
286 | return $this->return($request, $model, 'show'); |
||
287 | } |
||
288 | |||
289 | /** |
||
290 | * Looks for an "extra" param in the route, and if it exists, looks for relationships |
||
291 | * based on that route. |
||
292 | * @param Model $model |
||
293 | * @param Request $request |
||
294 | * @return LengthAwarePaginator|Collection|Model|mixed |
||
295 | * @throws BadMethodCallException |
||
296 | */ |
||
297 | protected function iterateThroughChildren(Model $model, Request $request) |
||
298 | { |
||
299 | // If there's no route or extra param, just return. |
||
300 | if (!$request->route() || |
||
301 | !($request->route() instanceof Route) || |
||
302 | !$request->route()->parameter('extra')) { |
||
303 | return $model; |
||
304 | } |
||
305 | |||
306 | $parts = array_filter(explode('/', (string)$request->route()->parameter('extra'))); |
||
307 | |||
308 | // Loop through the parts. |
||
309 | foreach ($parts as $part) { |
||
310 | // Look for an array accessor, "children[5]" for example. |
||
311 | preg_match('/\[(\d+)]$/', $part, $matches); |
||
312 | $offset = false; |
||
313 | |||
314 | // If one was found, save the offset and remove it from $part. |
||
315 | if (!empty($matches[0])) { |
||
316 | $part = str_replace(array_shift($matches), '', $part); |
||
317 | $offset = array_shift($matches); |
||
318 | } |
||
319 | |||
320 | $model = $model->$part(); |
||
321 | |||
322 | // If it's a relationship, see if it's paginate-able. |
||
323 | if (stripos(get_class($model), 'Many') !== false) { |
||
324 | /** @var BelongsToMany|HasMany|HasOneOrMany|MorphMany|MorphOneOrMany|MorphToMany $model */ |
||
325 | $model = $model->paginate(); |
||
0 ignored issues
–
show
The method
paginate does only exist in Illuminate\Database\Eloq...Relations\BelongsToMany , but not in Illuminate\Database\Eloq...\Relations\HasOneOrMany .
It seems like the method you are trying to call exists only in some of the possible types. Let’s take a look at an example: class A
{
public function foo() { }
}
class B extends A
{
public function bar() { }
}
/**
* @param A|B $x
*/
function someFunction($x)
{
$x->foo(); // This call is fine as the method exists in A and B.
$x->bar(); // This method only exists in B and might cause an error.
}
Available Fixes
![]() |
|||
326 | } elseif ($model instanceof Relation) { |
||
327 | $model = $model->getResults(); |
||
328 | } |
||
329 | |||
330 | // If there is an offset, get it. |
||
331 | if ($offset !== false) { |
||
332 | $model = $model[$offset]; |
||
333 | } |
||
334 | } |
||
335 | |||
336 | return $model; |
||
337 | } |
||
338 | |||
339 | /** |
||
340 | * Update a record. The U of CRUD. |
||
341 | * @param Request $request |
||
342 | * @param int $id |
||
343 | * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector |
||
344 | */ |
||
345 | View Code Duplication | public function update(Request $request, $id) |
|
346 | { |
||
347 | $model = $this->findModel($id); |
||
348 | |||
349 | foreach ((array)$request->input() as $column => $value) { |
||
350 | $model->$column = $value; |
||
351 | } |
||
352 | |||
353 | $this->saveModel($model); |
||
354 | |||
355 | return $this->return($request, $model, 'update'); |
||
356 | } |
||
357 | |||
358 | /** |
||
359 | * Destroy a model. The D of CRUD. |
||
360 | * @param Request $request |
||
361 | * @param int $id |
||
362 | * @return ResponseFactory|Response |
||
363 | * @throws Exception |
||
364 | */ |
||
365 | public function destroy(Request $request, $id) |
||
366 | { |
||
367 | $model = $this->findModel($id); |
||
368 | |||
369 | $model->delete(); |
||
370 | |||
371 | return response(null, SymResponse::HTTP_NO_CONTENT); |
||
372 | } |
||
373 | |||
374 | /** |
||
375 | * Return the _links. The O of CRUD..... |
||
376 | * @param Request $request |
||
377 | * @return ResponseFactory|JsonResponse|RedirectResponse|Response|Redirector |
||
378 | */ |
||
379 | public function options(Request $request) |
||
380 | { |
||
381 | $class = $this->newModelInstance(); |
||
382 | |||
383 | if (!($class instanceof HasLinks)) { |
||
384 | throw new InvalidArgumentException('OPTIONS only works for models implementing HasLinks'); |
||
385 | } |
||
386 | |||
387 | foreach ($request->input() as $key => $value) { |
||
388 | $class->$key = $value; |
||
389 | } |
||
390 | |||
391 | return $this->return($request, $class->buildLinks(), 'options'); |
||
392 | } |
||
393 | } |
||
394 |
Let’s assume that you have a directory layout like this:
and let’s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: