Completed
Pull Request — master (#420)
by
unknown
08:25
created

Auditable::resolveUserAgent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 0
crap 2
1
<?php
2
/**
3
 * This file is part of the Laravel Auditing package.
4
 *
5
 * @author     Antério Vieira <[email protected]>
6
 * @author     Quetzy Garcia  <[email protected]>
7
 * @author     Raphael França <[email protected]>
8
 * @copyright  2015-2018
9
 *
10
 * For the full copyright and license information,
11
 * please view the LICENSE.md file that was distributed
12
 * with this source code.
13
 */
14
15
namespace OwenIt\Auditing;
16
17
use Illuminate\Database\Eloquent\Relations\MorphMany;
18
use Illuminate\Database\Eloquent\SoftDeletes;
19
use Illuminate\Support\Facades\App;
20
use Illuminate\Support\Facades\Config;
21
use OwenIt\Auditing\Contracts\AuditRedactor;
22
use OwenIt\Auditing\Contracts\IpAddressResolver;
23
use OwenIt\Auditing\Contracts\UrlResolver;
24
use OwenIt\Auditing\Contracts\UserAgentResolver;
25
use OwenIt\Auditing\Contracts\UserClassResolver;
26
use OwenIt\Auditing\Contracts\UserIdResolver;
27
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
28
use OwenIt\Auditing\Exceptions\AuditingException;
29
30
trait Auditable
31
{
32
    /**
33
     * Auditable attributes excluded from the Audit.
34
     *
35
     * @var array
36
     */
37
    protected $excludedAttributes = [];
38
39
    /**
40
     * Audit event name.
41
     *
42
     * @var string
43
     */
44
    protected $auditEvent;
45
46
    /**
47
     * Is auditing disabled?
48
     *
49
     * @var bool
50
     */
51
    public static $auditingDisabled = false;
52
53
    /**
54
     * Auditable boot logic.
55
     *
56
     * @return void
57
     */
58 429
    public static function bootAuditable()
59
    {
60 429
        if (static::isAuditingEnabled()) {
61 426
            static::observe(new AuditableObserver());
62
        }
63 429
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 66
    public function audits(): MorphMany
69
    {
70 66
        return $this->morphMany(
0 ignored issues
show
Bug introduced by
It seems like morphMany() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

70
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
71 66
            Config::get('audit.implementation', Models\Audit::class),
72 66
            'auditable'
73
        );
74
    }
75
76
    /**
77
     * Resolve the Auditable attributes to exclude from the Audit.
78
     *
79
     * @return void
80
     */
81 240
    protected function resolveAuditExclusions()
82
    {
83 240
        $this->excludedAttributes = $this->getAuditExclude();
84
85
        // When in strict mode, hidden and non visible attributes are excluded
86 240
        if ($this->getAuditStrict()) {
87
            // Hidden attributes
88 6
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
89
90
            // Non visible attributes
91 6
            if ($this->visible) {
92 6
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
93
94 6
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
95
            }
96
        }
97
98
        // Exclude Timestamps
99 240
        if (!$this->getAuditTimestamps()) {
100 240
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->getUpdatedAtColumn());
0 ignored issues
show
Bug introduced by
It seems like getUpdatedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

100
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->/** @scrutinizer ignore-call */ getUpdatedAtColumn());
Loading history...
Bug introduced by
It seems like getCreatedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

100
            array_push($this->excludedAttributes, $this->/** @scrutinizer ignore-call */ getCreatedAtColumn(), $this->getUpdatedAtColumn());
Loading history...
101
102 240
            if (in_array(SoftDeletes::class, class_uses_recursive($this))) {
103 228
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
0 ignored issues
show
Bug introduced by
It seems like getDeletedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

103
                /** @scrutinizer ignore-call */ 
104
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
Loading history...
104
            }
105
        }
106
107
        // Valid attributes are all those that made it out of the exclusion array
108 240
        $attributes = array_except($this->attributes, $this->excludedAttributes);
109
110 240
        foreach ($attributes as $attribute => $value) {
111
            // Apart from null, non scalar values will be excluded
112 204
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
113 204
                $this->excludedAttributes[] = $attribute;
114
            }
115
        }
116 240
    }
117
118
    /**
119
     * Get the old/new attributes of a retrieved event.
120
     *
121
     * @return array
122
     */
123 6
    protected function getRetrievedEventAttributes(): array
124
    {
125
        // This is a read event with no attribute changes,
126
        // only metadata will be stored in the Audit
127
128
        return [
129 6
            [],
130
            [],
131
        ];
132
    }
133
134
    /**
135
     * Get the old/new attributes of a created event.
136
     *
137
     * @return array
138
     */
139 216
    protected function getCreatedEventAttributes(): array
140
    {
141 216
        $new = [];
142
143 216
        foreach ($this->attributes as $attribute => $value) {
144 180
            if ($this->isAttributeAuditable($attribute)) {
145 180
                $new[$attribute] = $value;
146
            }
147
        }
148
149
        return [
150 216
            [],
151 216
            $new,
152
        ];
153
    }
154
155
    /**
156
     * Get the old/new attributes of an updated event.
157
     *
158
     * @return array
159
     */
160 18
    protected function getUpdatedEventAttributes(): array
161
    {
162 18
        $old = [];
163 18
        $new = [];
164
165 18
        foreach ($this->getDirty() as $attribute => $value) {
0 ignored issues
show
Bug introduced by
It seems like getDirty() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

165
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
166 15
            if ($this->isAttributeAuditable($attribute)) {
167 15
                $old[$attribute] = array_get($this->original, $attribute);
168 15
                $new[$attribute] = array_get($this->attributes, $attribute);
169
            }
170
        }
171
172
        return [
173 18
            $old,
174 18
            $new,
175
        ];
176
    }
177
178
    /**
179
     * Get the old/new attributes of a deleted event.
180
     *
181
     * @return array
182
     */
183 12
    protected function getDeletedEventAttributes(): array
184
    {
185 12
        $old = [];
186
187 12
        foreach ($this->attributes as $attribute => $value) {
188 12
            if ($this->isAttributeAuditable($attribute)) {
189 12
                $old[$attribute] = $value;
190
            }
191
        }
192
193
        return [
194 12
            $old,
195
            [],
196
        ];
197
    }
198
199
    /**
200
     * Get the old/new attributes of a restored event.
201
     *
202
     * @return array
203
     */
204 6
    protected function getRestoredEventAttributes(): array
205
    {
206
        // A restored event is just a deleted event in reverse
207 6
        return array_reverse($this->getDeletedEventAttributes());
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 285
    public function readyForAuditing(): bool
214
    {
215 285
        if (static::$auditingDisabled) {
216 3
            return false;
217
        }
218
219 285
        return $this->isEventAuditable($this->auditEvent);
220
    }
221
222
    /**
223
     * Redact attribute value.
224
     *
225
     * @param string $attribute
226
     * @param mixed  $value
227
     *
228
     * @throws AuditingException
229
     *
230
     * @return mixed
231
     */
232 12
    protected function redactAttributeValue(string $attribute, $value)
233
    {
234 12
        $auditRedactors = $this->getAuditRedactors();
235
236 12
        if (!array_key_exists($attribute, $auditRedactors)) {
237 6
            return $value;
238
        }
239
240 12
        $auditRedactor = $auditRedactors[$attribute];
241
242 12
        if (is_subclass_of($auditRedactor, AuditRedactor::class)) {
243 6
            return call_user_func([$auditRedactor, 'redact'], $value);
244
        }
245
246 6
        throw new AuditingException('Invalid AuditRedactor implementation');
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 258
    public function toAudit(): array
253
    {
254 258
        if (!$this->readyForAuditing()) {
255 6
            throw new AuditingException('A valid audit event has not been set');
256
        }
257
258 252
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
259
260 252
        if (!method_exists($this, $attributeGetter)) {
261 12
            throw new AuditingException(sprintf(
262 12
                'Unable to handle "%s" event, %s() method missing',
263 12
                $this->auditEvent,
264 12
                $attributeGetter
265
            ));
266
        }
267
268 240
        $this->resolveAuditExclusions();
269
270 240
        list($old, $new) = $this->$attributeGetter();
271
272 240
        if ($this->getAuditRedactors() && Config::get('audit.redact', false)) {
273 12
            foreach ($old as $attribute => $value) {
274 6
                $old[$attribute] = $this->redactAttributeValue($attribute, $value);
275
            }
276
277 12
            foreach ($new as $attribute => $value) {
278 12
                $new[$attribute] = $this->redactAttributeValue($attribute, $value);
279
            }
280
        }
281
282 234
        $userForeignKey = Config::get('audit.user.foreign_key', 'user_id');
283
284 234
        $tags = implode(',', $this->generateTags());
285
286
        $data = [
287 234
            'old_values'     => $old,
288 234
            'new_values'     => $new,
289 234
            'event'          => $this->auditEvent,
290 234
            'auditable_id'   => $this->getKey(),
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

290
            'auditable_id'   => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
291 234
            'auditable_type' => $this->getMorphClass(),
0 ignored issues
show
Bug introduced by
It seems like getMorphClass() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

291
            'auditable_type' => $this->/** @scrutinizer ignore-call */ getMorphClass(),
Loading history...
292 234
            $userForeignKey  => $this->resolveUserId(),
293 222
            'url'            => $this->resolveUrl(),
294 216
            'ip_address'     => $this->resolveIpAddress(),
295 210
            'user_agent'     => $this->resolveUserAgent(),
296 204
            'tags'           => empty($tags) ? null : $tags,
297
        ];
298
299 204
        if (Config::get('audit.morphable', true)) {
300 72
            $data['user_type'] = $this->resolveUserClass();
301
        }
302
303 198
        return $this->transformAudit($data);
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309 192
    public function transformAudit(array $data): array
310
    {
311 192
        return $data;
312
    }
313
314
    /**
315
     * Resolve the User id.
316
     *
317
     * @throws AuditingException
318
     *
319
     * @return mixed|null
320
     */
321 234
    protected function resolveUserId()
322
    {
323 234
        $userResolver = Config::get('audit.resolver.user_id');
324
325 234
        if (is_subclass_of($userResolver, UserIdResolver::class)) {
326 228
            return call_user_func([$userResolver, 'resolve']);
327
        }
328
329 6
        throw new AuditingException('Invalid UserIdResolver implementation');
330
    }
331
332
    /**
333
     * Resolve the User class.
334
     *
335
     * @throws AuditingException
336
     *
337
     * @return mixed|null
338
     */
339 72
    protected function resolveUserClass()
340
    {
341 72
        $userResolver = Config::get('audit.resolver.user_class');
342
343 72
        if (is_subclass_of($userResolver, UserClassResolver::class)) {
344 66
            return call_user_func([$userResolver, 'resolve']);
345
        }
346
347 6
        throw new AuditingException('Invalid UserClassResolver implementation');
348
    }
349
350
    /**
351
     * Resolve the URL.
352
     *
353
     * @throws AuditingException
354
     *
355
     * @return string
356
     */
357 222
    protected function resolveUrl(): string
358
    {
359 222
        $urlResolver = Config::get('audit.resolver.url');
360
361 222
        if (is_subclass_of($urlResolver, UrlResolver::class)) {
362 216
            return call_user_func([$urlResolver, 'resolve']);
363
        }
364
365 6
        throw new AuditingException('Invalid UrlResolver implementation');
366
    }
367
368
    /**
369
     * Resolve the IP Address.
370
     *
371
     * @throws AuditingException
372
     *
373
     * @return string
374
     */
375 216
    protected function resolveIpAddress(): string
376
    {
377 216
        $ipAddressResolver = Config::get('audit.resolver.ip_address');
378
379 216
        if (is_subclass_of($ipAddressResolver, IpAddressResolver::class)) {
380 210
            return call_user_func([$ipAddressResolver, 'resolve']);
381
        }
382
383 6
        throw new AuditingException('Invalid IpAddressResolver implementation');
384
    }
385
386
    /**
387
     * Resolve the User Agent.
388
     *
389
     * @throws AuditingException
390
     *
391
     * @return string|null
392
     */
393 210
    protected function resolveUserAgent()
394
    {
395 210
        $userAgentResolver = Config::get('audit.resolver.user_agent');
396
397 210
        if (is_subclass_of($userAgentResolver, UserAgentResolver::class)) {
398 204
            return call_user_func([$userAgentResolver, 'resolve']);
399
        }
400
401 6
        throw new AuditingException('Invalid UserAgentResolver implementation');
402
    }
403
404
    /**
405
     * Determine if an attribute is eligible for auditing.
406
     *
407
     * @param string $attribute
408
     *
409
     * @return bool
410
     */
411 201
    protected function isAttributeAuditable(string $attribute): bool
412
    {
413
        // The attribute should not be audited
414 201
        if (in_array($attribute, $this->excludedAttributes, true)) {
415 186
            return false;
416
        }
417
418
        // The attribute is auditable when explicitly
419
        // listed or when the include array is empty
420 201
        $include = $this->getAuditInclude();
421
422 201
        return empty($include) || in_array($attribute, $include, true);
423
    }
424
425
    /**
426
     * Determine whether an event is auditable.
427
     *
428
     * @param string $event
429
     *
430
     * @return bool
431
     */
432 291
    protected function isEventAuditable($event): bool
433
    {
434 291
        return is_string($this->resolveAttributeGetter($event));
435
    }
436
437
    /**
438
     * Attribute getter method resolver.
439
     *
440
     * @param string $event
441
     *
442
     * @return string|null
443
     */
444 291
    protected function resolveAttributeGetter($event)
445
    {
446 291
        foreach ($this->getAuditEvents() as $key => $value) {
447 291
            $auditableEvent = is_int($key) ? $value : $key;
448
449 291
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
450
451 291
            if (preg_match($auditableEventRegex, $event)) {
452 291
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
453
            }
454
        }
455 102
    }
456
457
    /**
458
     * {@inheritdoc}
459
     */
460 291
    public function setAuditEvent(string $event): Contracts\Auditable
461
    {
462 291
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
463
464 291
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type OwenIt\Auditing\Auditable which is incompatible with the type-hinted return OwenIt\Auditing\Contracts\Auditable.
Loading history...
465
    }
466
467
    /**
468
     * {@inheritdoc}
469
     */
470 12
    public function getAuditEvent()
471
    {
472 12
        return $this->auditEvent;
473
    }
474
475
    /**
476
     * {@inheritdoc}
477
     */
478 309
    public function getAuditEvents(): array
479
    {
480 309
        return $this->auditEvents ?? Config::get('audit.events', [
0 ignored issues
show
Bug introduced by
The property auditEvents does not exist on OwenIt\Auditing\Auditable. Did you mean auditEvent?
Loading history...
481 309
            'created',
482
            'updated',
483
            'deleted',
484
            'restored',
485
        ]);
486
    }
487
488
    /**
489
     * Disable Auditing.
490
     *
491
     * @return void
492
     */
493 3
    public static function disableAuditing()
494
    {
495 3
        static::$auditingDisabled = true;
496 3
    }
497
498
    /**
499
     * Enable Auditing.
500
     *
501
     * @return void
502
     */
503 3
    public static function enableAuditing()
504
    {
505 3
        static::$auditingDisabled = false;
506 3
    }
507
508
    /**
509
     * Determine whether auditing is enabled.
510
     *
511
     * @return bool
512
     */
513 447
    public static function isAuditingEnabled(): bool
514
    {
515 447
        if (App::runningInConsole()) {
516 438
            return Config::get('audit.console', false);
517
        }
518
519 9
        return true;
520
    }
521
522
    /**
523
     * {@inheritdoc}
524
     */
525 213
    public function getAuditInclude(): array
526
    {
527 213
        return $this->auditInclude ?? [];
528
    }
529
530
    /**
531
     * {@inheritdoc}
532
     */
533 252
    public function getAuditExclude(): array
534
    {
535 252
        return $this->auditExclude ?? [];
536
    }
537
538
    /**
539
     * {@inheritdoc}
540
     */
541 258
    public function getAuditStrict(): bool
542
    {
543 258
        return $this->auditStrict ?? Config::get('audit.strict', false);
544
    }
545
546
    /**
547
     * {@inheritdoc}
548
     */
549 258
    public function getAuditTimestamps(): bool
550
    {
551 258
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
552
    }
553
554
    /**
555
     * {@inheritdoc}
556
     */
557 201
    public function getAuditDriver()
558
    {
559 201
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
560
    }
561
562
    /**
563
     * {@inheritdoc}
564
     */
565 192
    public function getAuditThreshold(): int
566
    {
567 192
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
568
    }
569
570
    /**
571
     * {@inheritdoc}
572
     */
573 240
    public function getAuditRedactors(): array
574
    {
575 240
        return $this->auditRedactors ?? [];
576
    }
577
578
    /**
579
     * {@inheritdoc}
580
     */
581 240
    public function generateTags(): array
582
    {
583 240
        return [];
584
    }
585
586
    /**
587
     * {@inheritdoc}
588
     */
589 42
    public function transitionTo(Contracts\Audit $audit, bool $old = false): Contracts\Auditable
590
    {
591
        // The Audit must be for an Auditable model of this type
592 42
        if ($this->getMorphClass() !== $audit->auditable_type) {
0 ignored issues
show
Bug introduced by
Accessing auditable_type on the interface OwenIt\Auditing\Contracts\Audit suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
593 12
            throw new AuditableTransitionException(sprintf(
594 12
                'Expected Auditable type %s, got %s instead',
595 12
                $this->getMorphClass(),
596 12
                $audit->auditable_type
597
            ));
598
        }
599
600
        // The Audit must be for this specific Auditable model
601 30
        if ($this->getKey() !== $audit->auditable_id) {
0 ignored issues
show
Bug introduced by
Accessing auditable_id on the interface OwenIt\Auditing\Contracts\Audit suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
602 6
            throw new AuditableTransitionException(sprintf(
603 6
                'Expected Auditable id %s, got %s instead',
604 6
                $this->getKey(),
605 6
                $audit->auditable_id
606
            ));
607
        }
608
609
        // Redacted data should not be used when transitioning states
610 24
        if ($auditRedactors = $this->getAuditRedactors()) {
611 6
            throw new AuditableTransitionException(
612 6
                'Cannot transition states when Audit redactors are set',
613 6
                $auditRedactors
614
            );
615
        }
616
617
        // The attribute compatibility between the Audit and the Auditable model must be met
618 18
        $modified = $audit->getModified();
619
620 18
        if ($incompatibilities = array_diff_key($modified, $this->getAttributes())) {
0 ignored issues
show
Bug introduced by
It seems like $modified can also be of type string; however, parameter $array1 of array_diff_key() does only seem to accept array, 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

620
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
Bug introduced by
It seems like getAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

620
        if ($incompatibilities = array_diff_key($modified, $this->/** @scrutinizer ignore-call */ getAttributes())) {
Loading history...
621 6
            throw new AuditableTransitionException(sprintf(
622 6
                'Incompatibility between [%s:%s] and [%s:%s]',
623 6
                $this->getMorphClass(),
624 6
                $this->getKey(),
625 6
                get_class($audit),
626 6
                $audit->getKey()
0 ignored issues
show
Bug introduced by
The method getKey() does not exist on OwenIt\Auditing\Contracts\Audit. Since it exists in all sub-types, consider adding an abstract or default implementation to OwenIt\Auditing\Contracts\Audit. ( Ignorable by Annotation )

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

626
                $audit->/** @scrutinizer ignore-call */ 
627
                        getKey()
Loading history...
627 6
            ), array_keys($incompatibilities));
628
        }
629
630 12
        $key = $old ? 'old' : 'new';
631
632 12
        foreach ($modified as $attribute => $value) {
633 9
            if (array_key_exists($key, $value)) {
634 9
                $this->setAttribute($attribute, $value[$key]);
0 ignored issues
show
Bug introduced by
It seems like setAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

634
                $this->/** @scrutinizer ignore-call */ 
635
                       setAttribute($attribute, $value[$key]);
Loading history...
635
            }
636
        }
637
638 12
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type OwenIt\Auditing\Auditable which is incompatible with the type-hinted return OwenIt\Auditing\Contracts\Auditable.
Loading history...
639
    }
640
}
641