Issues (10)

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/Models/Discussion.php (6 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
declare(strict_types=1);
4
5
namespace Arcanedev\LaravelMessenger\Models;
6
7
use Arcanedev\LaravelMessenger\Contracts\Discussion as DiscussionContract;
8
use Arcanedev\LaravelMessenger\Contracts\Message as MessageContract;
9
use Arcanedev\LaravelMessenger\Contracts\Participation as ParticipationContract;
10
use Illuminate\Database\Eloquent\Builder;
11
use Illuminate\Database\Eloquent\Model as EloquentModel;
12
use Illuminate\Support\Carbon;
13
use Illuminate\Support\Collection;
14
15
/**
16
 * Class     Discussion
17
 *
18
 * @package  Arcanedev\LaravelMessenger\Models
19
 * @author   ARCANEDEV <[email protected]>
20
 *
21
 * @property  int                         id
22
 * @property  string                      subject
23
 * @property  \Illuminate\Support\Carbon  created_at
24
 * @property  \Illuminate\Support\Carbon  updated_at
25
 * @property  \Illuminate\Support\Carbon  deleted_at
26
 *
27
 * @property  \Illuminate\Database\Eloquent\Model         creator
28
 * @property  \Illuminate\Database\Eloquent\Collection    messages
29
 * @property  \Illuminate\Database\Eloquent\Collection    participations
30
 * @property  \Arcanedev\LaravelMessenger\Models\Message  latest_message
31
 *
32
 * @method static  \Illuminate\Database\Eloquent\Builder|static  subject(string $subject, bool $strict)
33
 * @method static  \Illuminate\Database\Eloquent\Builder|static  between(\Illuminate\Support\Collection|array $participables)
34
 * @method static  \Illuminate\Database\Eloquent\Builder|static  forUser(\Illuminate\Database\Eloquent\Model|mixed $participable)
35
 * @method static  \Illuminate\Database\Eloquent\Builder|static  forUserWithNewMessages(\Illuminate\Database\Eloquent\Model|mixed $participable)
36
 */
37
class Discussion extends Model implements DiscussionContract
38
{
39
    /* -----------------------------------------------------------------
40
     |  Properties
41
     | -----------------------------------------------------------------
42
     */
43
44
    /**
45
     * The attributes that can be set with Mass Assignment.
46
     *
47
     * @var array
48
     */
49
    protected $fillable = ['subject'];
50
51
    /**
52
     * The attributes that should be mutated to dates.
53
     *
54
     * @var array
55
     */
56
    protected $dates = ['deleted_at'];
57
58
    /**
59
     * The attributes that should be cast to native types.
60
     *
61
     * @var array
62
     */
63
    protected $casts = [
64
        'id' => 'integer',
65
    ];
66
67
    /* -----------------------------------------------------------------
68
     |  Constructor
69
     | -----------------------------------------------------------------
70
     */
71
72
    /**
73
     * Create a new Eloquent model instance.
74
     *
75
     * @param  array  $attributes
76
     */
77 240
    public function __construct(array $attributes = [])
78
    {
79 240
        $this->setTable(
80 240
            config('messenger.discussions.table', 'discussions')
81
        );
82
83 240
        parent::__construct($attributes);
84 240
    }
85
86
    /* -----------------------------------------------------------------
87
     |  Relationships
88
     | -----------------------------------------------------------------
89
     */
90
91
    /**
92
     * Participants relationship.
93
     *
94
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
95
     */
96 168
    public function participations()
97
    {
98 168
        return $this->hasMany(
99 168
            config('messenger.participations.model', Participation::class)
100
        );
101
    }
102
103
    /**
104
     * Messages relationship.
105
     *
106
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
107
     */
108 66
    public function messages()
109
    {
110 66
        return $this->hasMany(
111 66
            config('messenger.messages.model', Message::class)
112
        );
113
    }
114
115
    /**
116
     * Get the participable that created the first message.
117
     *
118
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
119
     */
120 6
    public function creator()
121
    {
122 6
        return $this->messages()->oldest()->first()->author();
123
    }
124
125
    /* -----------------------------------------------------------------
126
     |  Scopes
127
     | -----------------------------------------------------------------
128
     */
129
130
    /**
131
     * Scope discussions that the participable is associated with.
132
     *
133
     * @param  \Illuminate\Database\Eloquent\Builder  $query
134
     * @param  \Illuminate\Database\Eloquent\Model    $participable
135
     *
136
     * @return \Illuminate\Database\Eloquent\Builder|static
137
     */
138 12
    public function scopeForUser(Builder $query, EloquentModel $participable)
139
    {
140 12
        $table = $this->getParticipationsTable();
141 12
        $morph = config('messenger.users.morph', 'participable');
142
143
        return $query
144 12
            ->join($table, $this->getQualifiedKeyName(), '=', "{$table}.discussion_id")
145 12
            ->where("{$table}.{$morph}_type", '=', $participable->getMorphClass())
146 12
            ->where("{$table}.{$morph}_id", '=', $participable->getKey())
147 12
            ->whereNull("{$table}.deleted_at")
148 12
            ->select("{$this->getTable()}.*");
149
    }
150
151
    /**
152
     * Scope discussions with new messages that the participable is associated with.
153
     *
154
     * @param  \Illuminate\Database\Eloquent\Builder  $query
155
     * @param  \Illuminate\Database\Eloquent\Model    $participable
156
     *
157
     * @return \Illuminate\Database\Eloquent\Builder|static
158
     */
159 6
    public function scopeForUserWithNewMessages(Builder $query, EloquentModel $participable)
160
    {
161 6
        $prefix         = $this->getConnection()->getTablePrefix();
162 6
        $participations = $this->getParticipationsTable();
163 6
        $discussions    = $this->getTable();
164
165 6
        return $this->scopeForUser($query, $participable)
0 ignored issues
show
The method where does only exist in Illuminate\Database\Eloquent\Builder, but not in Arcanedev\LaravelMessenger\Models\Discussion.

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

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
166
                    ->where(function (Builder $query) use ($participations, $discussions, $prefix) {
167 6
                        $expression = $this->getConnection()->raw("{$prefix}{$participations}.last_read");
168
169 6
                        $query->where("{$discussions}.updated_at", '>', $expression)
170 6
                              ->orWhereNull("{$participations}.last_read");
171 6
                    });
172
    }
173
174
    /**
175
     * Scope discussions between given participables.
176
     *
177
     * @param  \Illuminate\Database\Eloquent\Builder  $query
178
     * @param  \Illuminate\Support\Collection|array   $participables
179
     *
180
     * @return \Illuminate\Database\Eloquent\Builder|static
181
     */
182 6
    public function scopeBetween(Builder $query, $participables)
183
    {
184
        return $query->whereHas($this->getParticipationsTable(), function (Builder $query) use ($participables) {
0 ignored issues
show
Bug Compatibility introduced by
The expression $query->whereHas($this->...t($participables)); }); of type Illuminate\Database\Eloq...ns\QueriesRelationships adds the type Illuminate\Database\Eloq...ns\QueriesRelationships to the return on line 184 which is incompatible with the return type declared by the interface Arcanedev\LaravelMesseng...iscussion::scopeBetween of type Illuminate\Database\Eloquent\Builder.
Loading history...
185 6
            $morph = config('messenger.users.morph', 'participable');
186 6
            $index = 0;
187
188 6
            foreach ($participables as $participable) {
189
                /** @var  \Illuminate\Database\Eloquent\Model  $participable */
190
                $clause = [
191 6
                    ["{$morph}_type", '=', $participable->getMorphClass()],
192 6
                    ["{$morph}_id", '=', $participable->getKey()],
193
                ];
194
195 6
                $query->where($clause, null, null, $index === 0 ? 'and' : 'or');
196
197 6
                $index++;
198
            }
199
200 6
            $query->groupBy('discussion_id')
201 6
                  ->havingRaw('COUNT(discussion_id)='.count($participables));
202 6
        });
203
    }
204
205
    /**
206
     * Scope the query by the subject.
207
     *
208
     * @param  \Illuminate\Database\Eloquent\Builder  $query
209
     * @param  string                                 $subject
210
     * @param  bool                                   $strict
211
     *
212
     * @return \Illuminate\Database\Eloquent\Builder|static
213
     */
214 12
    public function scopeSubject(Builder $query, $subject, $strict = false)
215
    {
216 12
        return $query->where('subject', 'like', $strict ? $subject : "%{$subject}%");
217
    }
218
219
    /* -----------------------------------------------------------------
220
     |  Getters & Setters
221
     | -----------------------------------------------------------------
222
     */
223
224
    /**
225
     * Get the latest_message attribute.
226
     *
227
     * @return \Arcanedev\LaravelMessenger\Models\Message
228
     */
229 6
    public function getLatestMessageAttribute()
230
    {
231 6
        return $this->messages->sortByDesc('created_at')->first();
232
    }
233
234
    /**
235
     * Get the participations table name.
236
     *
237
     * @return string
238
     */
239 18
    protected function getParticipationsTable()
240
    {
241 18
        return config('messenger.participations.table', 'participations');
242
    }
243
244
    /* -----------------------------------------------------------------
245
     |  Main Methods
246
     | -----------------------------------------------------------------
247
     */
248
249
    /**
250
     * Returns all of the latest discussions by `updated_at` date.
251
     *
252
     * @return \Illuminate\Database\Eloquent\Collection
253
     */
254 6
    public static function getLatest()
255
    {
256 6
        return self::query()->latest('updated_at')->get();
0 ignored issues
show
Bug Compatibility introduced by
The expression self::query()->latest('updated_at')->get(); of type Illuminate\Database\Eloq...base\Eloquent\Builder[] adds the type Illuminate\Database\Eloquent\Builder[] to the return on line 256 which is incompatible with the return type declared by the interface Arcanedev\LaravelMesseng...s\Discussion::getLatest of type Illuminate\Database\Eloquent\Collection.
Loading history...
257
    }
258
259
    /**
260
     * Returns all discussions by subject.
261
     *
262
     * @param  string  $subject
263
     * @param  bool    $strict
264
     *
265
     * @return \Illuminate\Database\Eloquent\Collection
266
     */
267 12
    public static function getBySubject($subject, $strict = false)
268
    {
269 12
        return self::subject($subject, $strict)->get();
0 ignored issues
show
The method get does only exist in Illuminate\Database\Eloquent\Builder, but not in Arcanedev\LaravelMessenger\Models\Discussion.

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

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
270
    }
271
272
    /**
273
     * Returns an array of participables that are associated with the discussion.
274
     *
275
     * @return \Illuminate\Database\Eloquent\Collection
276
     */
277 12
    public function getParticipables()
278
    {
279 12
        return $this->participations()
280 12
            ->withTrashed()
281 12
            ->get()
282
            ->transform(function (ParticipationContract $participant) {
283 12
                return $participant->participable;
0 ignored issues
show
Accessing participable on the interface Arcanedev\LaravelMessenger\Contracts\Participation 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...
284 12
            })
285
            ->unique(function (EloquentModel $participable) {
286 12
                return $participable->getMorphClass().'-'.$participable->getKey();
287 12
            });
288
    }
289
290
    /**
291
     * Add a participable to discussion.
292
     *
293
     * @param  \Illuminate\Database\Eloquent\Model  $participable
294
     *
295
     * @return \Arcanedev\LaravelMessenger\Models\Participation|mixed
296
     */
297 84
    public function addParticipant(EloquentModel $participable)
298
    {
299 84
        $morph = config('messenger.users.morph', 'participable');
300
301 84
        return $this->participations()->firstOrCreate([
302 84
            "{$morph}_id"   => $participable->getKey(),
303 84
            "{$morph}_type" => $participable->getMorphClass(),
304 84
            'discussion_id' => $this->id,
305
        ]);
306
    }
307
308
    /**
309
     * Add many participables to discussion.
310
     *
311
     * @param  \Illuminate\Support\Collection|array  $participables
312
     *
313
     * @return \Illuminate\Database\Eloquent\Collection
314
     */
315 48
    public function addParticipants($participables)
316
    {
317 48
        foreach ($participables as $participable) {
318 48
            $this->addParticipant($participable);
319
        }
320
321 48
        return $this->participations;
322
    }
323
324
    /**
325
     * Remove a participable from discussion.
326
     *
327
     * @param  \Illuminate\Database\Eloquent\Model  $participable
328
     * @param  bool                                 $reload
329
     *
330
     * @return int
331
     */
332 12
    public function removeParticipant(EloquentModel $participable, $reload = true)
333
    {
334 12
        return $this->removeParticipants([$participable], $reload);
335
    }
336
337
    /**
338
     * Remove many participables from discussion.
339
     *
340
     * @param  \Illuminate\Support\Collection|array  $participables
341
     * @param  bool   $reload
342
     *
343
     * @return int
344
     */
345 36
    public function removeParticipants($participables, $reload = true)
346
    {
347 36
        $morph   = config('messenger.users.morph', 'participable');
348 36
        $deleted = 0;
349
350 36
        foreach ($participables as $participable) {
351
            /** @var  \Illuminate\Database\Eloquent\Model  $participable */
352 36
            $deleted += $this->participations()
353 36
                ->where("{$morph}_type", '=', $participable->getMorphClass())
354 36
                ->where("{$morph}_id", '=', $participable->getKey())
355 36
                ->where('discussion_id', '=', $this->id)
356 36
                ->delete();
357
        }
358
359 36
        if ($reload)
360 18
            $this->load(['participations']);
361
362 36
        return $deleted;
363
    }
364
365
    /**
366
     * Mark a discussion as read for a participable.
367
     *
368
     * @param  \Illuminate\Database\Eloquent\Model  $participable
369
     *
370
     * @return bool|int
371
     */
372 30
    public function markAsRead(EloquentModel $participable)
373
    {
374 30
        if ($participant = $this->getParticipationByParticipable($participable)) {
375 24
            return $participant->update(['last_read' => Carbon::now()]);
376
        }
377
378 6
        return false;
379
    }
380
381
    /**
382
     * See if the current thread is unread by the participable.
383
     *
384
     * @param  \Illuminate\Database\Eloquent\Model  $participable
385
     *
386
     * @return bool
387
     */
388 12
    public function isUnread(EloquentModel $participable)
389
    {
390 12
        return ($participant = $this->getParticipationByParticipable($participable))
391 12
            ? $participant->last_read < $this->updated_at
392 12
            : false;
393
    }
394
395
    /**
396
     * Finds the participant record from a participable model.
397
     *
398
     * @param  \Illuminate\Database\Eloquent\Model  $participable
399
     *
400
     * @return \Arcanedev\LaravelMessenger\Models\Participation|mixed
401
     */
402 42
    public function getParticipationByParticipable(EloquentModel $participable)
403
    {
404 42
        $morph = config('messenger.users.morph', 'participable');
405
406 42
        return $this->participations()
407 42
            ->where("{$morph}_type", '=', $participable->getMorphClass())
408 42
            ->where("{$morph}_id", '=', $participable->getKey())
409 42
            ->first();
410
    }
411
412
    /**
413
     * Get the trashed participations.
414
     *
415
     * @return \Illuminate\Database\Eloquent\Collection
416
     */
417 12
    public function getTrashedParticipations()
418
    {
419 12
        return $this->participations()->onlyTrashed()->get();
420
    }
421
422
    /**
423
     * Restores all participations within a discussion.
424
     *
425
     * @param  bool  $reload
426
     *
427
     * @return int
428
     */
429 6
    public function restoreAllParticipations($reload = true)
430
    {
431 6
        $restored = $this->getTrashedParticipations()
432
            ->filter(function (ParticipationContract $participant) {
433 6
                return $participant->restore();
434 6
            })
435 6
            ->count();
436
437 6
        if ($reload)
438 6
            $this->load(['participations']);
439
440 6
        return $restored;
441
    }
442
443
    /**
444
     * Generates a participant information as a string.
445
     *
446
     * @param  \Closure|null  $callback
447
     * @param  string         $glue
448
     *
449
     * @return string
450
     */
451 12
    public function participationsString($callback = null, $glue = ', ')
452
    {
453
        /** @var \Illuminate\Database\Eloquent\Collection $participations */
454 12
        $participations = $this->participations->load(['participable']);
455
456 12
        if (is_null($callback)) {
457
            // By default: the participant name
458
            $callback = function (ParticipationContract $participant) {
459 6
                return $participant->stringInfo();
460 6
            };
461
        }
462
463 12
        return $participations->map($callback)->implode($glue);
464
    }
465
466
    /**
467
     * Checks to see if a participable is a current participant of the discussion.
468
     *
469
     * @param  \Illuminate\Database\Eloquent\Model  $participable
470
     *
471
     * @return bool
472
     */
473 12
    public function hasParticipation(EloquentModel $participable)
474
    {
475 12
        $morph = config('messenger.users.morph', 'participable');
476
477 12
        return $this->participations()
478 12
            ->where("{$morph}_id", '=', $participable->getKey())
479 12
            ->where("{$morph}_type", '=', $participable->getMorphClass())
480 12
            ->count() > 0;
481
    }
482
483
    /**
484
     * Get the unread messages in discussion for a specific participable.
485
     *
486
     * @param  \Illuminate\Database\Eloquent\Model  $participable
487
     *
488
     * @return \Illuminate\Support\Collection
489
     */
490 12
    public function getUnreadMessages(EloquentModel $participable)
491
    {
492 12
        $participation = $this->getParticipationByParticipable($participable);
493
494 12
        if (is_null($participation))
495 6
            return new Collection;
496
497 12
        return is_null($participation->last_read)
498 12
            ? $this->messages->toBase()
499
            : $this->messages->filter(function (MessageContract $message) use ($participation) {
500 12
                return $message->updated_at->gt($participation->last_read);
0 ignored issues
show
Accessing updated_at on the interface Arcanedev\LaravelMessenger\Contracts\Message 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...
501 12
            })->toBase();
502
    }
503
}
504