Passed
Pull Request — master (#662)
by
unknown
09:57
created

Auditable::shouldCreateEmptyAudits()   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 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 188
    public static function bootAuditable()
48
    {
49 188
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
50 184
            static::observe(new AuditableObserver());
51
        }
52 188
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 26
    public function audits(): MorphMany
58
    {
59 26
        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 26
            Config::get('audit.implementation', Models\Audit::class),
61 26
            'auditable'
62
        );
63
    }
64
65
    /**
66
     * Resolve the Auditable attributes to exclude from the Audit.
67
     *
68
     * @return void
69
     */
70 116
    protected function resolveAuditExclusions()
71
    {
72 116
        $this->excludedAttributes = $this->getAuditExclude();
73
74
        // When in strict mode, hidden and non visible attributes are excluded
75 116
        if ($this->getAuditStrict()) {
76
            // Hidden attributes
77 2
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
78
79
            // Non visible attributes
80 2
            if ($this->visible) {
81 2
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
82
83 2
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
84
            }
85
        }
86
87
        // Exclude Timestamps
88 116
        if (!$this->getAuditTimestamps()) {
89 116
            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 116
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
92 108
                $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 116
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
98
99 116
        foreach ($attributes as $attribute => $value) {
100
            // Apart from null, non scalar values will be excluded
101 108
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
102 4
                $this->excludedAttributes[] = $attribute;
103
            }
104
        }
105 116
    }
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 102
    protected function getCreatedEventAttributes(): array
129
    {
130 102
        $new = [];
131
132 102
        foreach ($this->attributes as $attribute => $value) {
133 94
            if ($this->isAttributeAuditable($attribute)) {
134 94
                $new[$attribute] = $value;
135
            }
136
        }
137
138
        return [
139 102
            [],
140 102
            $new,
141
        ];
142
    }
143
144
    /**
145
     * Get the old/new attributes of an updated event.
146
     *
147
     * @return array
148
     */
149 16
    protected function getUpdatedEventAttributes(): array
150
    {
151 16
        $old = [];
152 16
        $new = [];
153
154 16
        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 14
            if ($this->isAttributeAuditable($attribute)) {
156 10
                $old[$attribute] = Arr::get($this->original, $attribute);
157 10
                $new[$attribute] = Arr::get($this->attributes, $attribute);
158
            }
159
        }
160
161
        return [
162 16
            $old,
163 16
            $new,
164
        ];
165
    }
166
167
    /**
168
     * Get the old/new attributes of a deleted event.
169
     *
170
     * @return array
171
     */
172 8
    protected function getDeletedEventAttributes(): array
173
    {
174 8
        $old = [];
175
176 8
        foreach ($this->attributes as $attribute => $value) {
177 8
            if ($this->isAttributeAuditable($attribute)) {
178 8
                $old[$attribute] = $value;
179
            }
180
        }
181
182
        return [
183 8
            $old,
184
            [],
185
        ];
186
    }
187
188
    /**
189
     * Get the old/new attributes of a restored event.
190
     *
191
     * @return array
192
     */
193 4
    protected function getRestoredEventAttributes(): array
194
    {
195
        // A restored event is just a deleted event in reverse
196 4
        return array_reverse($this->getDeletedEventAttributes());
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 138
    public function readyForAuditing(): bool
203
    {
204 138
        if (static::$auditingDisabled) {
205 2
            return false;
206
        }
207
208 138
        return $this->isEventAuditable($this->auditEvent);
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 94
    public function shouldCreateEmptyAudits(): bool
215
    {
216 94
        return true;
217
    }
218
219
    /**
220
     * Modify attribute value.
221
     *
222
     * @param string $attribute
223
     * @param mixed  $value
224
     *
225
     * @throws AuditingException
226
     *
227
     * @return mixed
228
     */
229 4
    protected function modifyAttributeValue(string $attribute, $value)
230
    {
231 4
        $attributeModifiers = $this->getAttributeModifiers();
232
233 4
        if (!array_key_exists($attribute, $attributeModifiers)) {
234 2
            return $value;
235
        }
236
237 4
        $attributeModifier = $attributeModifiers[$attribute];
238
239 4
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
240 2
            return call_user_func([$attributeModifier, 'redact'], $value);
241
        }
242
243 4
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
244 2
            return call_user_func([$attributeModifier, 'encode'], $value);
245
        }
246
247 2
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253 126
    public function toAudit(): array
254
    {
255 126
        if (!$this->readyForAuditing()) {
256 2
            throw new AuditingException('A valid audit event has not been set');
257
        }
258
259 124
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
260
261 124
        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

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

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

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

599
        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

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

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

613
                $this->/** @scrutinizer ignore-call */ 
614
                       setAttribute($attribute, $value[$key]);
Loading history...
614
            }
615
        }
616
617 10
        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...
618
    }
619
}
620