Completed
Pull Request — master (#487)
by
unknown
10:27
created

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

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

88
            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

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

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

153
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
154 12
            if ($this->isAttributeAuditable($attribute)) {
155 12
                $old[$attribute] = array_get($this->original, $attribute);
156 12
                $new[$attribute] = array_get($this->attributes, $attribute);
157
            }
158
        }
159
160
        return [
161 15
            $old,
162 15
            $new,
163
        ];
164
    }
165
166
    /**
167
     * Get the old/new attributes of a deleted event.
168
     *
169
     * @return array
170
     */
171 12
    protected function getDeletedEventAttributes(): array
172
    {
173 12
        $old = [];
174
175 12
        foreach ($this->attributes as $attribute => $value) {
176 12
            if ($this->isAttributeAuditable($attribute)) {
177 12
                $old[$attribute] = $value;
178
            }
179
        }
180
181
        return [
182 12
            $old,
183
            [],
184
        ];
185
    }
186
187
    /**
188
     * Get the old/new attributes of a restored event.
189
     *
190
     * @return array
191
     */
192 6
    protected function getRestoredEventAttributes(): array
193
    {
194
        // A restored event is just a deleted event in reverse
195 6
        return array_reverse($this->getDeletedEventAttributes());
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 177
    public function readyForAuditing(): bool
202
    {
203 177
        if (static::$auditingDisabled) {
204 3
            return false;
205
        }
206
207 177
        return $this->isEventAuditable($this->auditEvent);
208
    }
209
210
    /**
211
     * Modify attribute value.
212
     *
213
     * @param string $attribute
214
     * @param mixed  $value
215
     *
216
     * @throws AuditingException
217
     *
218
     * @return mixed
219
     */
220 6
    protected function modifyAttributeValue(string $attribute, $value)
221
    {
222 6
        $attributeModifiers = $this->getAttributeModifiers();
223
224 6
        if (!array_key_exists($attribute, $attributeModifiers)) {
225 3
            return $value;
226
        }
227
228 6
        $attributeModifier = $attributeModifiers[$attribute];
229
230 6
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
231 3
            return call_user_func([$attributeModifier, 'redact'], $value);
232
        }
233
234 6
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
235 3
            return call_user_func([$attributeModifier, 'encode'], $value);
236
        }
237
238 3
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244 159
    public function toAudit(): array
245
    {
246 159
        if (!$this->readyForAuditing()) {
247 3
            throw new AuditingException('A valid audit event has not been set');
248
        }
249
250 156
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
251
252 156
        if (!method_exists($this, $attributeGetter)) {
253 12
            throw new AuditingException(sprintf(
254 12
                'Unable to handle "%s" event, %s() method missing',
255 12
                $this->auditEvent,
256 12
                $attributeGetter
257
            ));
258
        }
259
260 144
        $this->resolveAuditExclusions();
261
262 144
        list($old, $new) = $this->$attributeGetter();
263
264 144
        if ($this->getAttributeModifiers()) {
265 6
            foreach ($old as $attribute => $value) {
266 3
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
267
            }
268
269 6
            foreach ($new as $attribute => $value) {
270 6
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
271
            }
272
        }
273
274 141
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
275
276 141
        $tags = implode(',', $this->generateTags());
277
278 141
        $user = $this->resolveUser();
279
280 138
        return $this->transformAudit([
281 138
            'old_values'         => $old,
282 138
            'new_values'         => $new,
283 138
            'event'              => $this->auditEvent,
284 138
            '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

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

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

590
        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

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

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

604
                $this->/** @scrutinizer ignore-call */ 
605
                       setAttribute($attribute, $value[$key]);
Loading history...
605
            }
606
        }
607
608
        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...
609
    }
610
}
611