Completed
Pull Request — master (#439)
by
unknown
09:25 queued 01:29
created

Auditable::resolveAttributeGetter()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
ccs 0
cts 6
cp 0
rs 9.6111
c 0
b 0
f 0
cc 5
nc 7
nop 1
crap 30
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\UserResolver;
26
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
27
use OwenIt\Auditing\Exceptions\AuditingException;
28
29
trait Auditable
30
{
31
    /**
32
     * Auditable attributes excluded from the Audit.
33
     *
34
     * @var array
35
     */
36
    protected $excludedAttributes = [];
37
38
    /**
39
     * Audit event name.
40
     *
41
     * @var string
42
     */
43
    protected $auditEvent;
44
45
    /**
46
     * Is auditing disabled?
47
     *
48
     * @var bool
49
     */
50
    public static $auditingDisabled = false;
51
52
    /**
53
     * Auditable boot logic.
54
     *
55
     * @return void
56
     */
57 3
    public static function bootAuditable()
58
    {
59 3
        if (static::isAuditingEnabled()) {
60
            static::observe(new AuditableObserver());
61
        }
62 3
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function audits(): MorphMany
68
    {
69
        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

69
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
70
            Config::get('audit.implementation', Models\Audit::class),
71
            'auditable'
72
        );
73
    }
74
75
    /**
76
     * Resolve the Auditable attributes to exclude from the Audit.
77
     *
78
     * @return void
79
     */
80
    protected function resolveAuditExclusions()
81
    {
82
        $this->excludedAttributes = $this->getAuditExclude();
83
84
        // When in strict mode, hidden and non visible attributes are excluded
85
        if ($this->getAuditStrict()) {
86
            // Hidden attributes
87
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
88
89
            // Non visible attributes
90
            if ($this->visible) {
91
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
92
93
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
94
            }
95
        }
96
97
        // Exclude Timestamps
98
        if (!$this->getAuditTimestamps()) {
99
            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

99
            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

99
            array_push($this->excludedAttributes, $this->/** @scrutinizer ignore-call */ getCreatedAtColumn(), $this->getUpdatedAtColumn());
Loading history...
100
101
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
102
                $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

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

166
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
167
            if ($this->isAttributeAuditable($attribute)) {
168
                $old[$attribute] = array_get($this->original, $attribute);
169
                $new[$attribute] = array_get($this->attributes, $attribute);
170
            }
171
        }
172
173
        return [
174
            $old,
175
            $new,
176
        ];
177
    }
178
179
    /**
180
     * Get the old/new attributes of a deleted event.
181
     *
182
     * @return array
183
     */
184
    protected function getDeletedEventAttributes(): array
185
    {
186
        $old = [];
187
188
        foreach ($this->attributes as $attribute => $value) {
189
            if ($this->isAttributeAuditable($attribute)) {
190
                $old[$attribute] = $value;
191
            }
192
        }
193
194
        return [
195
            $old,
196
            [],
197
        ];
198
    }
199
200
    /**
201
     * Get the old/new attributes of a restored event.
202
     *
203
     * @return array
204
     */
205
    protected function getRestoredEventAttributes(): array
206
    {
207
        // A restored event is just a deleted event in reverse
208
        return array_reverse($this->getDeletedEventAttributes());
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214
    public function readyForAuditing(): bool
215
    {
216
        if (static::$auditingDisabled) {
217
            return false;
218
        }
219
220
        return $this->isEventAuditable($this->auditEvent);
221
    }
222
223
    /**
224
     * Redact attribute value.
225
     *
226
     * @param string $attribute
227
     * @param mixed  $value
228
     *
229
     * @throws AuditingException
230
     *
231
     * @return mixed
232
     */
233
    protected function redactAttributeValue(string $attribute, $value)
234
    {
235
        $auditRedactors = $this->getAuditRedactors();
236
237
        if (!array_key_exists($attribute, $auditRedactors)) {
238
            return $value;
239
        }
240
241
        $auditRedactor = $auditRedactors[$attribute];
242
243
        if (is_subclass_of($auditRedactor, AuditRedactor::class)) {
244
            return call_user_func([$auditRedactor, 'redact'], $value);
245
        }
246
247
        throw new AuditingException('Invalid AuditRedactor implementation');
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253
    public function toAudit(): array
254
    {
255
        if (!$this->readyForAuditing()) {
256
            throw new AuditingException('A valid audit event has not been set');
257
        }
258
259
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
260
261
        if (!method_exists($this, $attributeGetter)) {
262
            throw new AuditingException(sprintf(
263
                'Unable to handle "%s" event, %s() method missing',
264
                $this->auditEvent,
265
                $attributeGetter
266
            ));
267
        }
268
269
        $this->resolveAuditExclusions();
270
271
        list($old, $new) = $this->$attributeGetter();
272
273
        if ($this->getAuditRedactors() && Config::get('audit.redact', false)) {
274
            foreach ($old as $attribute => $value) {
275
                $old[$attribute] = $this->redactAttributeValue($attribute, $value);
276
            }
277
278
            foreach ($new as $attribute => $value) {
279
                $new[$attribute] = $this->redactAttributeValue($attribute, $value);
280
            }
281
        }
282
283
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
284
285
        $tags = implode(',', $this->generateTags());
286
287
        $user = $this->resolveUser();
288
289
        return $this->transformAudit([
290
            'old_values'         => $old,
291
            'new_values'         => $new,
292
            'event'              => $this->auditEvent,
293
            '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
            '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
            $morphPrefix.'_id'   => $user ? $user->getAuthIdentifier() : null,
296
            $morphPrefix.'_type' => $user ? $user->getMorphClass() : null,
297
            'url'                => $this->resolveUrl(),
298
            'ip_address'         => $this->resolveIpAddress(),
299
            'user_agent'         => $this->resolveUserAgent(),
300
            'tags'               => empty($tags) ? null : $tags,
301
        ]);
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307
    public function transformAudit(array $data): array
308
    {
309
        return $data;
310
    }
311
312
    /**
313
     * Resolve the User.
314
     *
315
     * @throws AuditingException
316
     *
317
     * @return mixed|null
318
     */
319
    protected function resolveUser()
320
    {
321
        $userResolver = Config::get('audit.resolver.user');
322
323
        if (is_subclass_of($userResolver, UserResolver::class)) {
324
            return call_user_func([$userResolver, 'resolve']);
325
        }
326
327
        throw new AuditingException('Invalid UserResolver implementation');
328
    }
329
330
    /**
331
     * Resolve the URL.
332
     *
333
     * @throws AuditingException
334
     *
335
     * @return string
336
     */
337
    protected function resolveUrl(): string
338
    {
339
        $urlResolver = Config::get('audit.resolver.url');
340
341
        if (is_subclass_of($urlResolver, UrlResolver::class)) {
342
            return call_user_func([$urlResolver, 'resolve']);
343
        }
344
345
        throw new AuditingException('Invalid UrlResolver implementation');
346
    }
347
348
    /**
349
     * Resolve the IP Address.
350
     *
351
     * @throws AuditingException
352
     *
353
     * @return string
354
     */
355
    protected function resolveIpAddress(): string
356
    {
357
        $ipAddressResolver = Config::get('audit.resolver.ip_address');
358
359
        if (is_subclass_of($ipAddressResolver, IpAddressResolver::class)) {
360
            return call_user_func([$ipAddressResolver, 'resolve']);
361
        }
362
363
        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
    protected function resolveUserAgent()
374
    {
375
        $userAgentResolver = Config::get('audit.resolver.user_agent');
376
377
        if (is_subclass_of($userAgentResolver, UserAgentResolver::class)) {
378
            return call_user_func([$userAgentResolver, 'resolve']);
379
        }
380
381
        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
    protected function isAttributeAuditable(string $attribute): bool
392
    {
393
        // The attribute should not be audited
394
        if (in_array($attribute, $this->excludedAttributes, true)) {
395
            return false;
396
        }
397
398
        // The attribute is auditable when explicitly
399
        // listed or when the include array is empty
400
        $include = $this->getAuditInclude();
401
402
        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
    protected function isEventAuditable($event): bool
413
    {
414
        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
    protected function resolveAttributeGetter($event)
425
    {
426
        foreach ($this->getAuditEvents() as $key => $value) {
427
            $auditableEvent = is_int($key) ? $value : $key;
428
429
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
430
431
            if (preg_match($auditableEventRegex, $event)) {
432
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
433
            }
434
        }
435
    }
436
437
    /**
438
     * {@inheritdoc}
439
     */
440
    public function setAuditEvent(string $event): Contracts\Auditable
441
    {
442
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
443
444
        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
    public function getAuditEvent()
451
    {
452
        return $this->auditEvent;
453
    }
454
455
    /**
456
     * {@inheritdoc}
457
     */
458
    public function getAuditEvents(): array
459
    {
460
        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
            'created',
462
            'updated',
463
            'deleted',
464
            'restored',
465
        ]);
466
    }
467
468
    /**
469
     * Disable Auditing.
470
     *
471
     * @return void
472
     */
473
    public static function disableAuditing()
474
    {
475
        static::$auditingDisabled = true;
476
    }
477
478
    /**
479
     * Enable Auditing.
480
     *
481
     * @return void
482
     */
483
    public static function enableAuditing()
484
    {
485
        static::$auditingDisabled = false;
486
    }
487
488
    /**
489
     * Determine whether auditing is enabled.
490
     *
491
     * @return bool
492
     */
493 3
    public static function isAuditingEnabled(): bool
494
    {
495 3
        if (App::runningInConsole()) {
496 3
            return Config::get('audit.console', false);
497
        }
498
499
        return true;
500
    }
501
502
    /**
503
     * {@inheritdoc}
504
     */
505
    public function getAuditInclude(): array
506
    {
507
        return $this->auditInclude ?? [];
508
    }
509
510
    /**
511
     * {@inheritdoc}
512
     */
513
    public function getAuditExclude(): array
514
    {
515
        return $this->auditExclude ?? [];
516
    }
517
518
    /**
519
     * {@inheritdoc}
520
     */
521
    public function getAuditStrict(): bool
522
    {
523
        return $this->auditStrict ?? Config::get('audit.strict', false);
524
    }
525
526
    /**
527
     * {@inheritdoc}
528
     */
529
    public function getAuditTimestamps(): bool
530
    {
531
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
532
    }
533
534
    /**
535
     * {@inheritdoc}
536
     */
537
    public function getAuditDriver()
538
    {
539
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
540
    }
541
542
    /**
543
     * {@inheritdoc}
544
     */
545
    public function getAuditThreshold(): int
546
    {
547
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
548
    }
549
550
    /**
551
     * {@inheritdoc}
552
     */
553
    public function getAuditRedactors(): array
554
    {
555
        return $this->auditRedactors ?? [];
556
    }
557
558
    /**
559
     * {@inheritdoc}
560
     */
561
    public function generateTags(): array
562
    {
563
        return [];
564
    }
565
566
    /**
567
     * {@inheritdoc}
568
     */
569
    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
        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
            throw new AuditableTransitionException(sprintf(
574
                'Expected Auditable type %s, got %s instead',
575
                $this->getMorphClass(),
576
                $audit->auditable_type
577
            ));
578
        }
579
580
        // The Audit must be for this specific Auditable model
581
        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
            throw new AuditableTransitionException(sprintf(
583
                'Expected Auditable id %s, got %s instead',
584
                $this->getKey(),
585
                $audit->auditable_id
586
            ));
587
        }
588
589
        // Redacted data should not be used when transitioning states
590
        if ($auditRedactors = $this->getAuditRedactors()) {
591
            throw new AuditableTransitionException(
592
                'Cannot transition states when Audit redactors are set',
593
                $auditRedactors
594
            );
595
        }
596
597
        // The attribute compatibility between the Audit and the Auditable model must be met
598
        $modified = $audit->getModified();
599
600
        if ($incompatibilities = array_diff_key($modified, $this->getAttributes())) {
0 ignored issues
show
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

600
        if ($incompatibilities = array_diff_key($modified, $this->/** @scrutinizer ignore-call */ getAttributes())) {
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

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

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

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