Issues (74)

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/HasMedia/HasMediaTrait.php (7 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 Spatie\MediaLibrary\HasMedia;
4
5
use DateTimeInterface;
6
use Illuminate\Database\Eloquent\SoftDeletes;
7
use Illuminate\Http\File;
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Collection;
10
use Illuminate\Support\Facades\Validator;
11
use Illuminate\Support\Str;
12
use Spatie\MediaLibrary\Conversion\Conversion;
13
use Spatie\MediaLibrary\Events\CollectionHasBeenCleared;
14
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\InvalidBase64Data;
15
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\MimeTypeNotAllowed;
16
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\UnreachableUrl;
17
use Spatie\MediaLibrary\Exceptions\MediaCannotBeDeleted;
18
use Spatie\MediaLibrary\Exceptions\MediaCannotBeUpdated;
19
use Spatie\MediaLibrary\FileAdder\FileAdder;
20
use Spatie\MediaLibrary\FileAdder\FileAdderFactory;
21
use Spatie\MediaLibrary\MediaCollection\MediaCollection;
22
use Spatie\MediaLibrary\MediaRepository;
23
use Spatie\MediaLibrary\Models\Media;
24
25
trait HasMediaTrait
26
{
27
    /** @var Conversion[] */
28
    public $mediaConversions = [];
29
30
    /** @var MediaCollection[] */
31
    public $mediaCollections = [];
32
33
    /** @var bool */
34
    protected $deletePreservingMedia = false;
35
36
    /** @var array */
37
    protected $unAttachedMediaLibraryItems = [];
38
39
    public static function bootHasMediaTrait()
40
    {
41
        static::deleting(function (HasMedia $entity) {
42
            if ($entity->shouldDeletePreservingMedia()) {
43
                return;
44
            }
45
46
            if (in_array(SoftDeletes::class, class_uses_recursive($entity))) {
47
                if (! $entity->forceDeleting) {
0 ignored issues
show
Accessing forceDeleting on the interface Spatie\MediaLibrary\HasMedia\HasMedia 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

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
48
                    return;
49
                }
50
            }
51
52
            $entity->media()->get()->each->delete();
53
        });
54
    }
55
56
    /**
57
     * Set the polymorphic relation.
58
     *
59
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
60
     */
61
    public function media()
62
    {
63
        return $this->morphMany(config('medialibrary.media_model'), 'model');
64
    }
65
66
    /**
67
     * Add a file to the medialibrary.
68
     *
69
     * @param string|\Symfony\Component\HttpFoundation\File\UploadedFile $file
70
     *
71
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
72
     */
73
    public function addMedia($file)
74
    {
75
        return app(FileAdderFactory::class)->create($this, $file);
76
    }
77
78
    /**
79
     * Add a file from the given disk.
80
     *
81
     * @param string $key
82
     * @param string $disk
83
     *
84
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
85
     */
86
    public function addMediaFromDisk(string $key, string $disk = null)
87
    {
88
        return app(FileAdderFactory::class)->createFromDisk($this, $key, $disk ?: config('filesystems.default'));
89
    }
90
91
    /**
92
     * Add a file from a request.
93
     *
94
     * @param string $key
95
     *
96
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
97
     */
98
    public function addMediaFromRequest(string $key)
99
    {
100
        return app(FileAdderFactory::class)->createFromRequest($this, $key);
101
    }
102
103
    /**
104
     * Add multiple files from a request by keys.
105
     *
106
     * @param string[] $keys
107
     *
108
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder[]
109
     */
110
    public function addMultipleMediaFromRequest(array $keys)
111
    {
112
        return app(FileAdderFactory::class)->createMultipleFromRequest($this, $keys);
113
    }
114
115
    /**
116
     * Add all files from a request.
117
     *
118
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder[]
119
     */
120
    public function addAllMediaFromRequest()
121
    {
122
        return app(FileAdderFactory::class)->createAllFromRequest($this);
123
    }
124
125
    /**
126
     * Add a remote file to the medialibrary.
127
     *
128
     * @param string $url
129
     * @param string|array ...$allowedMimeTypes
130
     *
131
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
132
     *
133
     * @throws \Spatie\MediaLibrary\Exceptions\FileCannotBeAdded
134
     */
135
    public function addMediaFromUrl(string $url, ...$allowedMimeTypes)
136
    {
137
        if (! $stream = @fopen($url, 'r')) {
138
            throw UnreachableUrl::create($url);
139
        }
140
141
        $temporaryFile = tempnam(sys_get_temp_dir(), 'media-library');
142
        file_put_contents($temporaryFile, $stream);
143
144
        $this->guardAgainstInvalidMimeType($temporaryFile, $allowedMimeTypes);
145
146
        $filename = basename(parse_url($url, PHP_URL_PATH));
147
        $filename = str_replace('%20', ' ', $filename);
148
149
        if ($filename === '') {
150
            $filename = 'file';
151
        }
152
153
        $mediaExtension = explode('/', mime_content_type($temporaryFile));
154
155
        if (! Str::contains($filename, '.')) {
156
            $filename = "{$filename}.{$mediaExtension[1]}";
157
        }
158
159
        return app(FileAdderFactory::class)
160
            ->create($this, $temporaryFile)
161
            ->usingName(pathinfo($filename, PATHINFO_FILENAME))
162
            ->usingFileName($filename);
163
    }
164
165
    /**
166
     * Add a base64 encoded file to the medialibrary.
167
     *
168
     * @param string $base64data
169
     * @param string|array ...$allowedMimeTypes
170
     *
171
     * @throws InvalidBase64Data
172
     * @throws \Spatie\MediaLibrary\Exceptions\FileCannotBeAdded
173
     *
174
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
175
     */
176
    public function addMediaFromBase64(string $base64data, ...$allowedMimeTypes): FileAdder
177
    {
178
        // strip out data uri scheme information (see RFC 2397)
179
        if (strpos($base64data, ';base64') !== false) {
180
            [$_, $base64data] = explode(';', $base64data);
0 ignored issues
show
The variable $_ does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
181
            [$_, $base64data] = explode(',', $base64data);
182
        }
183
184
        // strict mode filters for non-base64 alphabet characters
185
        if (base64_decode($base64data, true) === false) {
186
            throw InvalidBase64Data::create();
187
        }
188
189
        // decoding and then reencoding should not change the data
190
        if (base64_encode(base64_decode($base64data)) !== $base64data) {
191
            throw InvalidBase64Data::create();
192
        }
193
194
        $binaryData = base64_decode($base64data);
195
196
        // temporarily store the decoded data on the filesystem to be able to pass it to the fileAdder
197
        $tmpFile = tempnam(sys_get_temp_dir(), 'medialibrary');
198
        file_put_contents($tmpFile, $binaryData);
199
200
        $this->guardAgainstInvalidMimeType($tmpFile, $allowedMimeTypes);
201
202
        $file = app(FileAdderFactory::class)->create($this, $tmpFile);
203
204
        return $file;
205
    }
206
207
    /**
208
     * Copy a file to the medialibrary.
209
     *
210
     * @param string|\Symfony\Component\HttpFoundation\File\UploadedFile $file
211
     *
212
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
213
     */
214
    public function copyMedia($file)
215
    {
216
        return $this->addMedia($file)->preservingOriginal();
217
    }
218
219
    /*
220
     * Determine if there is media in the given collection.
221
     */
222
    public function hasMedia(string $collectionName = 'default'): bool
223
    {
224
        return count($this->getMedia($collectionName)) ? true : false;
225
    }
226
227
    /**
228
     * Get media collection by its collectionName.
229
     *
230
     * @param string $collectionName
231
     * @param array|callable $filters
232
     *
233
     * @return \Illuminate\Support\Collection
234
     */
235
    public function getMedia(string $collectionName = 'default', $filters = []): Collection
236
    {
237
        return app(MediaRepository::class)->getCollection($this, $collectionName, $filters);
238
    }
239
240
    public function getFirstMedia(string $collectionName = 'default', array $filters = []): ?Media
241
    {
242
        $media = $this->getMedia($collectionName, $filters);
243
244
        return $media->first();
245
    }
246
247
    /*
248
     * Get the url of the image for the given conversionName
249
     * for first media for the given collectionName.
250
     * If no profile is given, return the source's url.
251
     */
252
    public function getFirstMediaUrl(string $collectionName = 'default', string $conversionName = ''): string
253
    {
254
        $media = $this->getFirstMedia($collectionName);
255
256
        if (! $media) {
257
            return $this->getFallbackMediaUrl($collectionName) ?: '';
258
        }
259
260
        return $media->getUrl($conversionName);
261
    }
262
263
    /*
264
     * Get the url of the image for the given conversionName
265
     * for first media for the given collectionName.
266
     *
267
     * If no profile is given, return the source's url.
268
     */
269
    public function getFirstTemporaryUrl(DateTimeInterface $expiration, string $collectionName = 'default', string $conversionName = ''): string
270
    {
271
        $media = $this->getFirstMedia($collectionName);
272
273
        if (! $media) {
274
            return $this->getFallbackMediaUrl($collectionName) ?: '';
275
        }
276
277
        return $media->getTemporaryUrl($expiration, $conversionName);
278
    }
279
280
    public function getMediaCollection(string $collectionName = 'default'): ?MediaCollection
281
    {
282
        $this->registerMediaCollections();
283
284
        return collect($this->mediaCollections)
285
            ->first(function (MediaCollection $collection) use ($collectionName) {
286
                return $collection->name === $collectionName;
287
            });
288
    }
289
290
    public function getFallbackMediaUrl(string $collectionName = 'default'): string
291
    {
292
        return optional($this->getMediaCollection($collectionName))->fallbackUrl ?? '';
293
    }
294
295
    public function getFallbackMediaPath(string $collectionName = 'default'): string
296
    {
297
        return optional($this->getMediaCollection($collectionName))->fallbackPath ?? '';
298
    }
299
300
    /*
301
     * Get the url of the image for the given conversionName
302
     * for first media for the given collectionName.
303
     * If no profile is given, return the source's url.
304
     */
305
    public function getFirstMediaPath(string $collectionName = 'default', string $conversionName = ''): string
306
    {
307
        $media = $this->getFirstMedia($collectionName);
308
309
        if (! $media) {
310
            return $this->getFallbackMediaPath($collectionName) ?: '';
311
        }
312
313
        return $media->getPath($conversionName);
314
    }
315
316
    /**
317
     * Update a media collection by deleting and inserting again with new values.
318
     *
319
     * @param array $newMediaArray
320
     * @param string $collectionName
321
     *
322
     * @return \Illuminate\Support\Collection
323
     *
324
     * @throws \Spatie\MediaLibrary\Exceptions\MediaCannotBeUpdated
325
     */
326
    public function updateMedia(array $newMediaArray, string $collectionName = 'default'): Collection
327
    {
328
        $this->removeMediaItemsNotPresentInArray($newMediaArray, $collectionName);
329
330
        $mediaClass = config('medialibrary.media_model');
331
        $mediaInstance = new $mediaClass();
332
        $keyName = $mediaInstance->getKeyName();
333
334
        return collect($newMediaArray)
335
            ->map(function (array $newMediaItem) use ($collectionName, $mediaClass, $keyName) {
336
                static $orderColumn = 1;
337
338
                $currentMedia = $mediaClass::findOrFail($newMediaItem[$keyName]);
339
340
                if ($currentMedia->collection_name !== $collectionName) {
341
                    throw MediaCannotBeUpdated::doesNotBelongToCollection($collectionName, $currentMedia);
342
                }
343
344
                if (array_key_exists('name', $newMediaItem)) {
345
                    $currentMedia->name = $newMediaItem['name'];
346
                }
347
348
                if (array_key_exists('custom_properties', $newMediaItem)) {
349
                    $currentMedia->custom_properties = $newMediaItem['custom_properties'];
350
                }
351
352
                $currentMedia->order_column = $orderColumn++;
353
354
                $currentMedia->save();
355
356
                return $currentMedia;
357
            });
358
    }
359
360
    protected function removeMediaItemsNotPresentInArray(array $newMediaArray, string $collectionName = 'default')
361
    {
362
        $this->getMedia($collectionName)
363
            ->reject(function (Media $currentMediaItem) use ($newMediaArray) {
364
                return in_array($currentMediaItem->getKey(), array_column($newMediaArray, $currentMediaItem->getKeyName()));
365
            })
366
            ->each->delete();
367
    }
368
369
    /**
370
     * Remove all media in the given collection.
371
     *
372
     * @param string $collectionName
373
     *
374
     * @return $this
375
     */
376
    public function clearMediaCollection(string $collectionName = 'default'): self
377
    {
378
        $this->getMedia($collectionName)
379
            ->each->delete();
380
381
        event(new CollectionHasBeenCleared($this, $collectionName));
382
383
        if ($this->mediaIsPreloaded()) {
384
            unset($this->media);
385
        }
386
387
        return $this;
388
    }
389
390
    /**
391
     * Remove all media in the given collection except some.
392
     *
393
     * @param string $collectionName
394
     * @param \Spatie\MediaLibrary\Models\Media[]|\Illuminate\Support\Collection $excludedMedia
395
     *
396
     * @return $this
397
     */
398
    public function clearMediaCollectionExcept(string $collectionName = 'default', $excludedMedia = [])
399
    {
400
        if ($excludedMedia instanceof Media) {
401
            $excludedMedia = collect()->push($excludedMedia);
402
        }
403
404
        $excludedMedia = collect($excludedMedia);
405
406
        if ($excludedMedia->isEmpty()) {
407
            return $this->clearMediaCollection($collectionName);
408
        }
409
410
        $this->getMedia($collectionName)
411
            ->reject(function (Media $media) use ($excludedMedia) {
412
                return $excludedMedia->where($media->getKeyName(), $media->getKey())->count();
413
            })
414
            ->each->delete();
415
416
        if ($this->mediaIsPreloaded()) {
417
            unset($this->media);
418
        }
419
420
        if ($this->getMedia($collectionName)->isEmpty()) {
421
            event(new CollectionHasBeenCleared($this, $collectionName));
422
        }
423
424
        return $this;
425
    }
426
427
    /**
428
     * Delete the associated media with the given id.
429
     * You may also pass a media object.
430
     *
431
     * @param int|\Spatie\MediaLibrary\Models\Media $mediaId
432
     *
433
     * @throws \Spatie\MediaLibrary\Exceptions\MediaCannotBeDeleted
434
     */
435
    public function deleteMedia($mediaId)
436
    {
437
        if ($mediaId instanceof Media) {
438
            $mediaId = $mediaId->getKey();
439
        }
440
441
        $media = $this->media->find($mediaId);
442
443
        if (! $media) {
444
            throw MediaCannotBeDeleted::doesNotBelongToModel($mediaId, $this);
445
        }
446
447
        $media->delete();
448
    }
449
450
    /*
451
     * Add a conversion.
452
     */
453
    public function addMediaConversion(string $name): Conversion
454
    {
455
        $conversion = Conversion::create($name);
456
457
        $this->mediaConversions[] = $conversion;
458
459
        return $conversion;
460
    }
461
462
    public function addMediaCollection(string $name): MediaCollection
463
    {
464
        $mediaCollection = MediaCollection::create($name);
465
466
        $this->mediaCollections[] = $mediaCollection;
467
468
        return $mediaCollection;
469
    }
470
471
    /**
472
     * Delete the model, but preserve all the associated media.
473
     *
474
     * @return bool
475
     */
476
    public function deletePreservingMedia(): bool
477
    {
478
        $this->deletePreservingMedia = true;
479
480
        return $this->delete();
0 ignored issues
show
The method delete() does not exist on Spatie\MediaLibrary\HasMedia\HasMediaTrait. Did you maybe mean deleteMedia()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
481
    }
482
483
    /**
484
     * Determines if the media files should be preserved when the media object gets deleted.
485
     *
486
     * @return bool
487
     */
488
    public function shouldDeletePreservingMedia()
489
    {
490
        return $this->deletePreservingMedia ?? false;
491
    }
492
493
    protected function mediaIsPreloaded(): bool
494
    {
495
        return $this->relationLoaded('media');
0 ignored issues
show
It seems like relationLoaded() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
496
    }
497
498
    /**
499
     * Cache the media on the object.
500
     *
501
     * @param string $collectionName
502
     *
503
     * @return mixed
504
     */
505
    public function loadMedia(string $collectionName)
506
    {
507
        $collection = $this->exists
0 ignored issues
show
The property exists does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
508
            ? $this->media
509
            : collect($this->unAttachedMediaLibraryItems)->pluck('media');
510
511
        return $collection
512
            ->filter(function (Media $mediaItem) use ($collectionName) {
513
                if ($collectionName == '') {
514
                    return true;
515
                }
516
517
                return $mediaItem->collection_name === $collectionName;
518
            })
519
            ->sortBy('order_column')
520
            ->values();
521
    }
522
523
    public function prepareToAttachMedia(Media $media, FileAdder $fileAdder)
524
    {
525
        $this->unAttachedMediaLibraryItems[] = compact('media', 'fileAdder');
526
    }
527
528
    public function processUnattachedMedia(callable $callable)
529
    {
530
        foreach ($this->unAttachedMediaLibraryItems as $item) {
531
            $callable($item['media'], $item['fileAdder']);
532
        }
533
534
        $this->unAttachedMediaLibraryItems = [];
535
    }
536
537
    protected function guardAgainstInvalidMimeType(string $file, ...$allowedMimeTypes)
538
    {
539
        $allowedMimeTypes = Arr::flatten($allowedMimeTypes);
540
541
        if (empty($allowedMimeTypes)) {
542
            return;
543
        }
544
545
        $validation = Validator::make(
546
            ['file' => new File($file)],
547
            ['file' => 'mimetypes:'.implode(',', $allowedMimeTypes)]
548
        );
549
550
        if ($validation->fails()) {
551
            throw MimeTypeNotAllowed::create($file, $allowedMimeTypes);
552
        }
553
    }
554
555
    public function registerMediaConversions(Media $media = null)
0 ignored issues
show
The parameter $media is not used and could be removed.

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

Loading history...
556
    {
557
    }
558
559
    public function registerMediaCollections()
560
    {
561
    }
562
563
    public function registerAllMediaConversions(Media $media = null)
564
    {
565
        $this->registerMediaCollections();
566
567
        collect($this->mediaCollections)->each(function (MediaCollection $mediaCollection) use ($media) {
568
            $actualMediaConversions = $this->mediaConversions;
569
570
            $this->mediaConversions = [];
571
572
            ($mediaCollection->mediaConversionRegistrations)($media);
573
574
            $preparedMediaConversions = collect($this->mediaConversions)
575
                ->each(function (Conversion $conversion) use ($mediaCollection) {
576
                    $conversion->performOnCollections($mediaCollection->name);
577
                })
578
                ->values()
579
                ->toArray();
580
581
            $this->mediaConversions = array_merge($actualMediaConversions, $preparedMediaConversions);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge($actualMedia...eparedMediaConversions) of type array is incompatible with the declared type array<integer,object<Spa...Conversion\Conversion>> of property $mediaConversions.

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...
582
        });
583
584
        $this->registerMediaConversions($media);
585
    }
586
}
587