Completed
Pull Request — master (#437)
by Quetzy
10:38
created

Auditable::redactAttributeValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

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.7666
c 0
b 0
f 0
cc 3
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\AttributeEncoder;
22
use OwenIt\Auditing\Contracts\AttributeRedactor;
23
use OwenIt\Auditing\Contracts\IpAddressResolver;
24
use OwenIt\Auditing\Contracts\UrlResolver;
25
use OwenIt\Auditing\Contracts\UserAgentResolver;
26
use OwenIt\Auditing\Contracts\UserResolver;
27
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
28
use OwenIt\Auditing\Exceptions\AuditingException;
29
30
trait Auditable
31
{
32
    /**
33
     * Auditable attributes excluded from the Audit.
34
     *
35
     * @var array
36
     */
37
    protected $excludedAttributes = [];
38
39
    /**
40
     * Audit event name.
41
     *
42
     * @var string
43
     */
44
    protected $auditEvent;
45
46
    /**
47
     * Is auditing disabled?
48
     *
49
     * @var bool
50
     */
51
    public static $auditingDisabled = false;
52
53
    /**
54
     * Auditable boot logic.
55
     *
56
     * @return void
57
     */
58 264
    public static function bootAuditable()
59
    {
60 264
        if (static::isAuditingEnabled()) {
61 261
            static::observe(new AuditableObserver());
62
        }
63 264
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 36
    public function audits(): MorphMany
69
    {
70 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

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

100
            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

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

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

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

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

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

602
        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

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

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

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