Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Pull Request — main (#4988)
by Pedro
40:53 queued 25:51
created

Uploader::multiple()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 5
rs 10
1
<?php
2
3
namespace Backpack\CRUD\app\Library\CrudPanel\Uploads\Uploaders;
4
5
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
6
use Backpack\CRUD\app\Library\CrudPanel\Uploads\Interfaces\UploaderInterface;
7
use Closure;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Http\UploadedFile;
10
use Illuminate\Support\Facades\Storage;
11
use Illuminate\Support\Str;
12
13
abstract class Uploader implements UploaderInterface
14
{
15
    /**
16
     * The name of the uploader AKA CrudField/Column name.
17
     *
18
     * @var string
19
     */
20
    public string $name;
21
22
    /**
23
     * Indicates the uploaded file should be deleted when entry is deleted.
24
     *
25
     * @var bool
26
     */
27
    public $deleteWhenEntryIsDeleted = true;
28
29
    /**
30
     * Indicates if this uploader instance is inside a repeatable container.
31
     *
32
     * @var bool
33
     */
34
    public $isRepeatable = false;
35
36
    /**
37
     * When inside a repeatable container, indicates the container name.
38
     *
39
     * @var string|null
40
     */
41
    public $repeatableContainerName = null;
42
43
    /**
44
     * Developer provided filename.
45
     *
46
     * @var null|string|Closure
47
     */
48
    public $fileName = null;
49
50
    /**
51
     * The disk where upload will be stored. By default `public`.
52
     *
53
     * @var string
54
     */
55
    public $disk = 'public';
56
57
    /**
58
     * Indicates if the upload handles multiple files.
59
     *
60
     * @var bool
61
     */
62
    public $isMultiple = false;
63
64
    /**
65
     * The path inside the disk to store the uploads.
66
     *
67
     * @var string
68
     */
69
    public $path = '';
70
71
    /**
72
     * Should the url to the object be a temporary one (eg: s3).
73
     *
74
     * @var bool
75
     */
76
    public $useTemporaryUrl = false;
77
78
    /**
79
     * When using temporary urls, defines the time that the url
80
     * should be available in minutes.
81
     *
82
     * By default 1 minute
83
     *
84
     * @var int
85
     */
86
    public $temporaryUrlExpirationTime = 1;
87
88
    /**
89
     * Indicates if the upload is relative to a relationship field/column.
90
     *
91
     * @var bool
92
     */
93
    public $isRelationship = false;
94
95
    public function __construct(array $crudObject, array $configuration)
96
    {
97
        $this->name = $crudObject['name'];
98
        $this->disk = $configuration['disk'] ?? $crudObject['disk'] ?? $this->disk;
99
        $this->useTemporaryUrl = $configuration['temporary'] ?? $this->useTemporaryUrl;
100
        $this->temporaryUrlExpirationTime = $configuration['expiration'] ?? $this->temporaryUrlExpirationTime;
101
        $this->path = $configuration['path'] ?? $crudObject['prefix'] ?? $this->path;
102
        $this->path = empty($this->path) ? $this->path : Str::of($this->path)->finish('/')->value;
103
        $this->fileName = $configuration['fileName'] ?? $this->fileName;
104
        $this->deleteWhenEntryIsDeleted = $configuration['whenDelete'] ?? $this->deleteWhenEntryIsDeleted;
105
    }
106
107
    /**
108
     * An abstract function that all uploaders must implement with a single file save process.
109
     *
110
     * @param  Model  $entry
111
     * @param  mixed  $values
112
     * @return mixed
113
     */
114
    abstract public function uploadFile(Model $entry, $values = null);
115
116
    /**
117
     * An function that all uploaders must implement if it also supports repeatable files.
118
     *
119
     * @param  Model  $entry
120
     * @param  mixed  $values
121
     * @return mixed
122
     */
123
    public function saveRepeatableFile(Model $entry, $values = null)
124
    {
125
    }
126
127
    public function getRepeatableContainerName()
128
    {
129
        return $this->repeatableContainerName;
130
    }
131
132
    /**
133
     * The function called in the saving event that starts the upload process.
134
     *
135
     * @param  Model  $entry
136
     * @return Model
137
     */
138
    public function processFileUpload(Model $entry)
139
    {
140
        if ($this->isRepeatable) {
141
            return $this->uploadRepeatableFile($entry);
142
        }
143
144
        $entry->{$this->name} = $this->uploadFile($entry);
145
146
        return $entry;
147
    }
148
149
    private function uploadRepeatableFile(Model $entry)
150
    {
151
        $values = collect(CRUD::getRequest()->get($this->repeatableContainerName));
0 ignored issues
show
Bug introduced by
The method getRequest() does not exist on Backpack\CRUD\app\Librar...udPanel\CrudPanelFacade. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

151
        $values = collect(CRUD::/** @scrutinizer ignore-call */ getRequest()->get($this->repeatableContainerName));
Loading history...
152
        $files = collect(CRUD::getRequest()->file($this->repeatableContainerName));
153
154
        $value = $this->mergeValuesRecursive($values, $files);
155
156
        if (! $this->isRelationship) {
157
            $entry->{$this->repeatableContainerName} = json_encode($this->processRepeatableUploads($entry, $value));
158
        } else {
159
            $modelCount = CRUD::get('uploaded_'.$this->repeatableContainerName.'_count');
0 ignored issues
show
Bug introduced by
The method get() does not exist on Backpack\CRUD\app\Librar...udPanel\CrudPanelFacade. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

159
            /** @scrutinizer ignore-call */ 
160
            $modelCount = CRUD::get('uploaded_'.$this->repeatableContainerName.'_count');
Loading history...
160
161
            $value = $value->slice($modelCount, 1)->toArray();
162
163
            foreach (app('UploadersRepository')->getRegisteredUploadersFor($this->repeatableContainerName) as $uploader) {
164
                if (array_key_exists($modelCount, $value) && isset($value[$modelCount][$uploader->getName()])) {
165
                    $entry->{$uploader->getName()} = $uploader->uploadFile($entry, $value[$modelCount][$uploader->getName()]);
166
                }
167
            }
168
        }
169
170
        return $entry;
171
    }
172
173
    private function processRepeatableUploads(Model $entry, $value)
174
    {
175
        foreach (app('UploadersRepository')->getRegisteredUploadersFor($this->repeatableContainerName) as $uploader) {
176
            $uploadedValues = $uploader->saveRepeatableFile($entry, $value->pluck($uploader->name)->toArray());
177
178
            $value = $value->map(function ($item, $key) use ($uploadedValues, $uploader) {
179
                $item[$uploader->getName()] = $uploadedValues[$key] ?? null;
180
181
                return $item;
182
            });
183
        }
184
185
        return $value;
186
    }
187
188
    /**
189
     * Return the uploader name.
190
     *
191
     * @return string
192
     */
193
    public function getName()
194
    {
195
        return $this->name;
196
    }
197
198
    /**
199
     * Return the uploader disk.
200
     *
201
     * @return string
202
     */
203
    public function getDisk()
204
    {
205
        return $this->disk;
206
    }
207
208
    /**
209
     * Return the uploader path.
210
     *
211
     * @return string
212
     */
213
    public function getPath()
214
    {
215
        return $this->path;
216
    }
217
218
    /**
219
     * Return the uploader temporary option.
220
     *
221
     * @return bool
222
     */
223
    public function getTemporary()
224
    {
225
        return $this->useTemporaryUrl;
226
    }
227
228
    /**
229
     * Return the uploader expiration time in minutes.
230
     *
231
     * @return int
232
     */
233
    public function getExpiration()
234
    {
235
        return $this->temporaryUrlExpirationTime;
236
    }
237
238
    /**
239
     * The function called in the retrieved event that handles the display of uploaded values.
240
     *
241
     * @param  Model  $entry
242
     * @return Model
243
     */
244
    public function retrieveUploadedFile(Model $entry)
245
    {
246
        if ($this->isRepeatable) {
247
            return $this->retrieveRepeatableFiles($entry);
248
        }
249
250
        return $this->retrieveFile($entry);
251
    }
252
253
    protected function retrieveFile($entry)
254
    {
255
        $value = $entry->{$this->name};
256
257
        if ($this->isMultiple && ! isset($entry->getCasts()[$this->name]) && is_string($value)) {
258
            $entry->{$this->name} = json_decode($value, true);
259
        } else {
260
            $entry->{$this->name} = Str::after($value, $this->path);
261
        }
262
263
        return $entry;
264
    }
265
266
    protected function retrieveRepeatableFiles($entry)
267
    {
268
        if ($this->isRelationship) {
269
            return $this->retrieveFile($entry);
270
        }
271
272
        return $entry;
273
    }
274
275
    /**
276
     * The function called in the deleting event to delete the uploaded files upon entry deletion.
277
     *
278
     * @param  Model  $entry
279
     * @return void
280
     */
281
    public function deleteUploadedFile(Model $entry)
282
    {
283
        if ($this->deleteWhenEntryIsDeleted) {
284
            if (! in_array(SoftDeletes::class, class_uses_recursive($entry), true)) {
0 ignored issues
show
Bug introduced by
The type Backpack\CRUD\app\Librar...s\Uploaders\SoftDeletes was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
285
                $this->performFileDeletion($entry);
286
            } else {
287
                if ($entry->forceDeleting === true) {
288
                    $this->performFileDeletion($entry);
289
                }
290
            }
291
        }
292
    }
293
294
    private function deleteRepeatableFiles($entry)
295
    {
296
        if ($this->isRelationship) {
297
            return $this->deleteFiles($entry);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->deleteFiles($entry) targeting Backpack\CRUD\app\Librar...Uploader::deleteFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
298
        }
299
300
        $repeatableValues = collect($entry->{$this->getName()});
301
        foreach (app('UploadersRepository')->getRegisteredUploadersFor($this->repeatableContainerName) as $upload) {
302
            if (! $upload->deleteWhenEntryIsDeleted) {
303
                continue;
304
            }
305
            $values = $repeatableValues->pluck($upload->getName())->toArray();
306
            foreach ($values as $value) {
307
                if (! $value) {
308
                    continue;
309
                }
310
                if (is_array($value)) {
311
                    foreach ($value as $subvalue) {
312
                        Storage::disk($upload->disk)->delete($upload->path.$subvalue);
313
                    }
314
315
                    continue;
316
                }
317
                Storage::disk($upload->disk)->delete($upload->path.$value);
318
            }
319
        }
320
    }
321
322
    private function deleteFiles($entry)
323
    {
324
        $values = $entry->{$this->name};
325
326
        if ($this->isMultiple) {
327
            if (! isset($entry->getCasts()[$this->name]) && is_string($values)) {
328
                $values = json_decode($values, true);
329
            }
330
        } else {
331
            $values = (array) Str::after($values, $this->path);
332
        }
333
334
        foreach ($values as $value) {
335
            Storage::disk($this->disk)->delete($this->path.$value);
336
        }
337
    }
338
339
    private function performFileDeletion($entry)
340
    {
341
        if ($this->isRelationship) {
342
            return $this->deleteFiles($entry);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->deleteFiles($entry) targeting Backpack\CRUD\app\Librar...Uploader::deleteFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
343
        }
344
345
        $this->deleteRepeatableFiles($entry);
346
    }
347
348
    /**
349
     * Build an uploader instance.
350
     *
351
     * @param  array  $crudObject
352
     * @param  array  $definition
353
     * @return self
354
     */
355
    public static function for(array $crudObject, array $definition)
356
    {
357
        return new static($crudObject, $definition);
358
    }
359
360
    /**
361
     * Set multiple attribute to true in the uploader.
362
     *
363
     * @return self
364
     */
365
    protected function multiple()
366
    {
367
        $this->isMultiple = true;
368
369
        return $this;
370
    }
371
372
    /**
373
     * Set relationship attribute in uploader.
374
     * When true, it also removes the repeatable in case the relationship is handled.
375
     *
376
     * @param  bool  $isRelationship
377
     * @return self
378
     */
379
    public function relationship(bool $isRelationship): self
380
    {
381
        $this->isRelationship = $isRelationship;
382
383
        return $this;
384
    }
385
386
    /**
387
     * Set the repeatable attribute to true in the uploader and the
388
     * corresponding container name.
389
     *
390
     * @param  string  $repeatableContainerName
391
     * @return self
392
     */
393
    public function repeats(string $repeatableContainerName): self
394
    {
395
        $this->isRepeatable = true;
396
397
        $this->repeatableContainerName = $repeatableContainerName;
398
399
        return $this;
400
    }
401
402
    /**
403
     * Repeatable items send _order_ parameter in the request.
404
     * This olds the information for uploads inside repeatable containers.
405
     *
406
     * @return array
407
     */
408
    protected function getFileOrderFromRequest()
409
    {
410
        $items = CRUD::getRequest()->input('_order_'.$this->repeatableContainerName) ?? [];
411
412
        array_walk($items, function (&$key, $value) {
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

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

412
        array_walk($items, function (&$key, /** @scrutinizer ignore-unused */ $value) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
413
            $requestValue = $key[$this->name] ?? null;
414
            $key = $this->isMultiple ? (is_string($requestValue) ? explode(',', $requestValue) : $requestValue) : $requestValue;
415
        });
416
417
        return $items;
418
    }
419
420
    /**
421
     * Return a new instance of the entry class for the uploader.
422
     *
423
     * @return Model
424
     */
425
    protected function modelInstance()
426
    {
427
        //return new $this->entryClass;
428
    }
429
430
    /**
431
     * Return the uploader stored values when in a repeatable container.
432
     *
433
     * @param  Model  $entry
434
     * @return array
435
     */
436
    protected function getPreviousRepeatableValues(Model $entry)
437
    {
438
        $previousValues = json_decode($entry->getOriginal($this->repeatableContainerName), true);
0 ignored issues
show
Bug introduced by
It seems like $entry->getOriginal($thi...epeatableContainerName) can also be of type array; however, parameter $json of json_decode() does only seem to accept string, 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

438
        $previousValues = json_decode(/** @scrutinizer ignore-type */ $entry->getOriginal($this->repeatableContainerName), true);
Loading history...
439
        if (! empty($previousValues)) {
440
            $previousValues = array_column($previousValues, $this->name);
441
        }
442
443
        return $previousValues ?? [];
444
    }
445
446
    /**
447
     * Return the file extension.
448
     *
449
     * @param  mixed  $file
450
     * @return string
451
     */
452
    protected function getExtensionFromFile($file)
453
    {
454
        return is_a($file, UploadedFile::class, true) ? $file->extension() : Str::after(mime_content_type($file), '/');
455
    }
456
457
    /**
458
     * Return the file name built by Backpack or by the developer in `fileName` configuration.
459
     *
460
     * @param  mixed  $file
461
     * @return string
462
     */
463
    protected function getFileName($file)
464
    {
465
        if (is_file($file)) {
466
            return Str::of($this->fileNameFrom($file) ?? Str::of($file->getClientOriginalName())->beforeLast('.')->slug()->append('-'.Str::random(4)));
467
        }
468
469
        return Str::of($this->fileNameFrom($file) ?? Str::random(40));
470
    }
471
472
    /**
473
     * Return the complete filename and extension.
474
     *
475
     * @param  mixed  $file
476
     * @return string
477
     */
478
    protected function getFileNameWithExtension($file)
479
    {
480
        if (is_file($file)) {
481
            return $this->getFileName($file).'.'.$this->getExtensionFromFile($file);
482
        }
483
484
        return Str::of($this->fileNameFrom($file) ?? Str::random(40)).'.'.$this->getExtensionFromFile($file);
485
    }
486
487
    /**
488
     * Allow developer to override the default Backpack fileName.
489
     *
490
     * @param  mixed  $file
491
     * @return string|null
492
     */
493
    private function fileNameFrom($file)
494
    {
495
        if (is_callable($this->fileName)) {
496
            return ($this->fileName)($file, $this);
497
        }
498
499
        return $this->fileName;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fileName also could return the type Closure which is incompatible with the documented return type null|string.
Loading history...
500
    }
501
502
    protected function mergeValuesRecursive($array1, $array2)
503
    {
504
        $merged = $array1;
505
        foreach ($array2 as $key => &$value) {
506
            if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
507
                $merged[$key] = $this->mergeValuesRecursive($merged[$key], $value);
508
            } else {
509
                $merged[$key] = $value;
510
            }
511
        }
512
513
        return $merged;
514
    }
515
516
    public function getIdentifier()
517
    {
518
        if ($this->isRepeatable) {
519
            return $this->repeatableContainerName.'_'.$this->name;
520
        }
521
522
        return $this->name;
523
    }
524
}
525