Passed
Push — master ( 8e6273...6164f4 )
by Quetzy
04:59
created

Auditable::redactAttributeValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 2
crap 3
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 249
    public static function bootAuditable()
58
    {
59 249
        if (static::isAuditingEnabled()) {
60 246
            static::observe(new AuditableObserver());
61
        }
62 249
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 36
    public function audits(): MorphMany
68
    {
69 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

69
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
70 36
            Config::get('audit.implementation', Models\Audit::class),
71 36
            'auditable'
72
        );
73
    }
74
75
    /**
76
     * Resolve the Auditable attributes to exclude from the Audit.
77
     *
78
     * @return void
79
     */
80 141
    protected function resolveAuditExclusions()
81
    {
82 141
        $this->excludedAttributes = $this->getAuditExclude();
83
84
        // When in strict mode, hidden and non visible attributes are excluded
85 141
        if ($this->getAuditStrict()) {
86
            // Hidden attributes
87 3
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
88
89
            // Non visible attributes
90 3
            if ($this->visible) {
91 3
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
92
93 3
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
94
            }
95
        }
96
97
        // Exclude Timestamps
98 141
        if (!$this->getAuditTimestamps()) {
99 141
            array_push($this->excludedAttributes, static::CREATED_AT, static::UPDATED_AT);
0 ignored issues
show
Bug introduced by
The constant OwenIt\Auditing\Auditable::CREATED_AT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The constant OwenIt\Auditing\Auditable::UPDATED_AT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
100
101 141
            if (in_array(SoftDeletes::class, class_uses_recursive($this))) {
102 129
                $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
        // Valid attributes are all those that made it out of the exclusion array
107 141
        $attributes = array_except($this->attributes, $this->excludedAttributes);
108
109 141
        foreach ($attributes as $attribute => $value) {
110
            // Apart from null, non scalar values will be excluded
111 129
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
112 129
                $this->excludedAttributes[] = $attribute;
113
            }
114
        }
115 141
    }
116
117
    /**
118
     * Get the old/new attributes of a retrieved event.
119
     *
120
     * @return array
121
     */
122 6
    protected function getRetrievedEventAttributes(): array
123
    {
124
        // This is a read event with no attribute changes,
125
        // only metadata will be stored in the Audit
126
127
        return [
128 6
            [],
129
            [],
130
        ];
131
    }
132
133
    /**
134
     * Get the old/new attributes of a created event.
135
     *
136
     * @return array
137
     */
138 120
    protected function getCreatedEventAttributes(): array
139
    {
140 120
        $new = [];
141
142 120
        foreach ($this->attributes as $attribute => $value) {
143 108
            if ($this->isAttributeAuditable($attribute)) {
144 108
                $new[$attribute] = $value;
145
            }
146
        }
147
148
        return [
149 120
            [],
150 120
            $new,
151
        ];
152
    }
153
154
    /**
155
     * Get the old/new attributes of an updated event.
156
     *
157
     * @return array
158
     */
159 15
    protected function getUpdatedEventAttributes(): array
160
    {
161 15
        $old = [];
162 15
        $new = [];
163
164 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

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

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

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

595
        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

595
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
596 3
            throw new AuditableTransitionException(sprintf(
597 3
                'Incompatibility between [%s:%s] and [%s:%s]',
598 3
                $this->getMorphClass(),
599 3
                $this->getKey(),
600 3
                get_class($audit),
601 3
                $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 3
            ), array_keys($incompatibilities));
603
        }
604
605 12
        $key = $old ? 'old' : 'new';
606
607 12
        foreach ($modified as $attribute => $value) {
608 9
            if (array_key_exists($key, $value)) {
609 9
                $this->setAttribute($attribute, $value[$key]);
0 ignored issues
show
Bug introduced by
It seems like setAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

609
                $this->/** @scrutinizer ignore-call */ 
610
                       setAttribute($attribute, $value[$key]);
Loading history...
610
            }
611
        }
612
613 12
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type OwenIt\Auditing\Auditable which is incompatible with the type-hinted return OwenIt\Auditing\Contracts\Auditable.
Loading history...
614
    }
615
}
616