Test Failed
Pull Request — master (#680)
by Morten
07:23
created

Auditable::getAuditDriver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 2
b 1
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
namespace OwenIt\Auditing;
4
5
use Illuminate\Database\Eloquent\Relations\MorphMany;
6
use Illuminate\Database\Eloquent\SoftDeletes;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Facades\App;
9
use Illuminate\Support\Facades\Config;
10
use OwenIt\Auditing\Contracts\AttributeEncoder;
11
use OwenIt\Auditing\Contracts\AttributeRedactor;
12
use OwenIt\Auditing\Contracts\IpAddressResolver;
13
use OwenIt\Auditing\Contracts\UrlResolver;
14
use OwenIt\Auditing\Contracts\UserAgentResolver;
15
use OwenIt\Auditing\Contracts\UserResolver;
16
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
17
use OwenIt\Auditing\Exceptions\AuditingException;
18
19
trait Auditable
20
{
21
    /**
22
     * Auditable attributes excluded from the Audit.
23
     *
24
     * @var array
25
     */
26
    protected $excludedAttributes = [];
27
28
    /**
29
     * Audit event name.
30
     *
31
     * @var string
32
     */
33
    protected $auditEvent;
34
35
    /**
36
     * Is auditing disabled?
37
     *
38
     * @var bool
39
     */
40
    public static $auditingDisabled = false;
41
42
    /**
43
     * Auditable boot logic.
44
     *
45
     * @return void
46
     */
47 42
    public static function bootAuditable()
48
    {
49 42
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
50 40
            static::observe(new AuditableObserver());
51
        }
52 42
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 12
    public function audits(): MorphMany
58
    {
59 12
        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

59
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
60 12
            Config::get('audit.implementation', Models\Audit::class),
61 12
            'auditable'
62
        );
63
    }
64
65
    /**
66
     * Resolve the Auditable attributes to exclude from the Audit.
67
     *
68
     * @return void
69
     */
70 34
    protected function resolveAuditExclusions()
71
    {
72 34
        $this->excludedAttributes = $this->getAuditExclude();
73
74
        // When in strict mode, hidden and non visible attributes are excluded
75 34
        if ($this->getAuditStrict()) {
76
            // Hidden attributes
77
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
78
79
            // Non visible attributes
80
            if ($this->visible) {
81
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
82
83
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
84
            }
85
        }
86
87
        // Exclude Timestamps
88 34
        if (!$this->getAuditTimestamps()) {
89 34
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->getUpdatedAtColumn());
0 ignored issues
show
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

89
            array_push($this->excludedAttributes, $this->/** @scrutinizer ignore-call */ getCreatedAtColumn(), $this->getUpdatedAtColumn());
Loading history...
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

89
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->/** @scrutinizer ignore-call */ getUpdatedAtColumn());
Loading history...
90
91 34
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
92 26
                $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

92
                /** @scrutinizer ignore-call */ 
93
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
Loading history...
93
            }
94
        }
95
96
        // Valid attributes are all those that made it out of the exclusion array
97 34
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
98
99 34
        foreach ($attributes as $attribute => $value) {
100
            // Apart from null, non scalar values will be excluded
101 34
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
102
                $this->excludedAttributes[] = $attribute;
103
            }
104
        }
105 34
    }
106
107
    /**
108
     * Get the old/new attributes of a retrieved event.
109
     *
110
     * @return array
111
     */
112 4
    protected function getRetrievedEventAttributes(): array
113
    {
114
        // This is a read event with no attribute changes,
115
        // only metadata will be stored in the Audit
116
117
        return [
118 4
            [],
119
            [],
120
        ];
121
    }
122
123
    /**
124
     * Get the old/new attributes of a created event.
125
     *
126
     * @return array
127
     */
128 22
    protected function getCreatedEventAttributes(): array
129
    {
130 22
        $new = [];
131
132 22
        foreach ($this->attributes as $attribute => $value) {
133 22
            if ($this->isAttributeAuditable($attribute)) {
134 22
                $new[$attribute] = $value;
135
            }
136
        }
137
138
        return [
139 22
            [],
140 22
            $new,
141
        ];
142
    }
143
144
    /**
145
     * Get the old/new attributes of an updated event.
146
     *
147
     * @return array
148
     */
149 8
    protected function getUpdatedEventAttributes(): array
150
    {
151 8
        $old = [];
152 8
        $new = [];
153
154 8
        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

154
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
155 8
            if ($this->isAttributeAuditable($attribute)) {
156 8
                $old[$attribute] = Arr::get($this->original, $attribute);
157 8
                $new[$attribute] = Arr::get($this->attributes, $attribute);
158
            }
159
        }
160
161
        return [
162 8
            $old,
163 8
            $new,
164
        ];
165
    }
166
167
    /**
168
     * Get the old/new attributes of a deleted event.
169
     *
170
     * @return array
171
     */
172 4
    protected function getDeletedEventAttributes(): array
173
    {
174 4
        $old = [];
175
176 4
        foreach ($this->attributes as $attribute => $value) {
177 4
            if ($this->isAttributeAuditable($attribute)) {
178 4
                $old[$attribute] = $value;
179
            }
180
        }
181
182
        return [
183 4
            $old,
184
            [],
185
        ];
186
    }
187
188
    /**
189
     * Get the old/new attributes of a restored event.
190
     *
191
     * @return array
192
     */
193 2
    protected function getRestoredEventAttributes(): array
194
    {
195
        // A restored event is just a deleted event in reverse
196 2
        return array_reverse($this->getDeletedEventAttributes());
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 40
    public function readyForAuditing(): bool
203
    {
204 40
        if (static::$auditingDisabled) {
205 2
            return false;
206
        }
207
208 40
        return $this->isEventAuditable($this->auditEvent);
209
    }
210
211
    /**
212
     * Modify attribute value.
213
     *
214
     * @param string $attribute
215
     * @param mixed $value
216
     *
217
     * @return mixed
218
     * @throws AuditingException
219
     *
220
     */
221
    protected function modifyAttributeValue(string $attribute, $value)
222
    {
223
        $attributeModifiers = $this->getAttributeModifiers();
224
225
        if (!array_key_exists($attribute, $attributeModifiers)) {
226
            return $value;
227
        }
228
229
        $attributeModifier = $attributeModifiers[$attribute];
230
231
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
232
            return call_user_func([$attributeModifier, 'redact'], $value);
233
        }
234
235
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
236
            return call_user_func([$attributeModifier, 'encode'], $value);
237
        }
238
239
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 34
    public function toAudit(): array
246
    {
247 34
        if (!$this->readyForAuditing()) {
248
            throw new AuditingException('A valid audit event has not been set');
249
        }
250
251 34
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
252
253 34
        if (!method_exists($this, $attributeGetter)) {
0 ignored issues
show
Bug introduced by
It seems like $attributeGetter can also be of type null; however, parameter $method of method_exists() 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

253
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
254
            throw new AuditingException(sprintf(
255
                'Unable to handle "%s" event, %s() method missing',
256
                $this->auditEvent,
257
                $attributeGetter
258
            ));
259
        }
260
261 34
        $this->resolveAuditExclusions();
262
263 34
        list($old, $new) = $this->$attributeGetter();
264
265 34
        if ($this->getAttributeModifiers()) {
266
            foreach ($old as $attribute => $value) {
267
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
268
            }
269
270
            foreach ($new as $attribute => $value) {
271
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
272
            }
273
        }
274
275 34
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
276
277 34
        $tags = implode(',', $this->generateTags());
278
279 34
        $user = $this->resolveUser();
280
281 34
        return $this->transformAudit([
282 34
            'old_values'           => $old,
283 34
            'new_values'           => $new,
284 34
            'event'                => $this->auditEvent,
285 34
            '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

285
            'auditable_id'         => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
286 34
            '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

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

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

595
        if ($incompatibilities = array_diff_key($modified, $this->/** @scrutinizer ignore-call */ getAttributes())) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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

595
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
596
            throw new AuditableTransitionException(sprintf(
597
                'Incompatibility between [%s:%s] and [%s:%s]',
598
                $this->getMorphClass(),
599
                $this->getKey(),
600
                get_class($audit),
601
                $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

601
                $audit->/** @scrutinizer ignore-call */ 
602
                        getKey()
Loading history...
602
            ), array_keys($incompatibilities));
603
        }
604
605
        $key = $old ? 'old' : 'new';
606
607
        foreach ($modified as $attribute => $value) {
608
            if (array_key_exists($key, $value)) {
609
                $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

609
                $this->/** @scrutinizer ignore-call */ 
610
                       setAttribute($attribute, $value[$key]);
Loading history...
610
            }
611
        }
612
613
        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...
614
    }
615
}
616