Passed
Pull Request — master (#673)
by
unknown
06:32
created

Auditable::resolveIpAddress()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 4
c 1
b 0
f 1
nc 2
nop 0
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 10
1
<?php
2
3
namespace OwenIt\Auditing;
4
5
use Illuminate\Database\Eloquent\Relations\MorphMany;
6
use Illuminate\Database\Eloquent\SoftDeletes;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Facades\App;
9
use Illuminate\Support\Facades\Config;
10
use OwenIt\Auditing\Contracts\AttributeEncoder;
11
use OwenIt\Auditing\Contracts\AttributeRedactor;
12
use OwenIt\Auditing\Contracts\Resolver;
13
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
14
use OwenIt\Auditing\Exceptions\AuditingException;
15
16
trait Auditable
17
{
18
    /**
19
     * Auditable attributes excluded from the Audit.
20
     *
21
     * @var array
22
     */
23
    protected $excludedAttributes = [];
24
25
    /**
26
     * Audit event name.
27
     *
28
     * @var string
29
     */
30
    protected $auditEvent;
31
32
    /**
33
     * Is auditing disabled?
34
     *
35
     * @var bool
36
     */
37
    public static $auditingDisabled = false;
38
39
    /**
40
     * Auditable boot logic.
41
     *
42
     * @return void
43
     */
44 186
    public static function bootAuditable()
45
    {
46 186
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
47 182
            static::observe(new AuditableObserver());
48
        }
49 186
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54 26
    public function audits(): MorphMany
55
    {
56 26
        return $this->morphMany(
0 ignored issues
show
Bug introduced by
It seems like morphMany() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

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

86
            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

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

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

151
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
152 10
            if ($this->isAttributeAuditable($attribute)) {
153 10
                $old[$attribute] = Arr::get($this->original, $attribute);
154 10
                $new[$attribute] = Arr::get($this->attributes, $attribute);
155
            }
156
        }
157
158
        return [
159 12
            $old,
160 12
            $new,
161
        ];
162
    }
163
164
    /**
165
     * Get the old/new attributes of a deleted event.
166
     *
167
     * @return array
168
     */
169 8
    protected function getDeletedEventAttributes(): array
170
    {
171 8
        $old = [];
172
173 8
        foreach ($this->attributes as $attribute => $value) {
174 8
            if ($this->isAttributeAuditable($attribute)) {
175 8
                $old[$attribute] = $value;
176
            }
177
        }
178
179
        return [
180 8
            $old,
181
            [],
182
        ];
183
    }
184
185
    /**
186
     * Get the old/new attributes of a restored event.
187
     *
188
     * @return array
189
     */
190 4
    protected function getRestoredEventAttributes(): array
191
    {
192
        // A restored event is just a deleted event in reverse
193 4
        return array_reverse($this->getDeletedEventAttributes());
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199 136
    public function readyForAuditing(): bool
200
    {
201 136
        if (static::$auditingDisabled) {
202 2
            return false;
203
        }
204
205 136
        return $this->isEventAuditable($this->auditEvent);
206
    }
207
208
    /**
209
     * Modify attribute value.
210
     *
211
     * @param string $attribute
212
     * @param mixed  $value
213
     *
214
     * @throws AuditingException
215
     *
216
     * @return mixed
217
     */
218 4
    protected function modifyAttributeValue(string $attribute, $value)
219
    {
220 4
        $attributeModifiers = $this->getAttributeModifiers();
221
222 4
        if (!array_key_exists($attribute, $attributeModifiers)) {
223 2
            return $value;
224
        }
225
226 4
        $attributeModifier = $attributeModifiers[$attribute];
227
228 4
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
229 2
            return call_user_func([$attributeModifier, 'redact'], $value);
230
        }
231
232 4
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
233 2
            return call_user_func([$attributeModifier, 'encode'], $value);
234
        }
235
236 2
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242 124
    public function toAudit(): array
243
    {
244 124
        if (!$this->readyForAuditing()) {
245 2
            throw new AuditingException('A valid audit event has not been set');
246
        }
247
248 122
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
249
250 122
        if (!method_exists($this, $attributeGetter)) {
0 ignored issues
show
Bug introduced by
It seems like $attributeGetter can also be of type null; however, parameter $method of method_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

250
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
251 8
            throw new AuditingException(sprintf(
252 8
                'Unable to handle "%s" event, %s() method missing',
253 8
                $this->auditEvent,
254
                $attributeGetter
255
            ));
256
        }
257
258 114
        $this->resolveAuditExclusions();
259
260 114
        list($old, $new) = $this->$attributeGetter();
261
262 114
        if ($this->getAttributeModifiers()) {
263 4
            foreach ($old as $attribute => $value) {
264 2
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
265
            }
266
267 4
            foreach ($new as $attribute => $value) {
268 4
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
269
            }
270
        }
271
272 112
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
273
274 112
        $tags = implode(',', $this->generateTags());
275
276 112
        $user = $this->resolveUser();
277
278 56
        $auditData = [
279 112
            'old_values'           => $old,
280 112
            'new_values'           => $new,
281 112
            'event'                => $this->auditEvent,
282 112
            'auditable_id'         => $this->getKey(),
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

282
            'auditable_id'         => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
283 112
            'auditable_type'       => $this->getMorphClass(),
0 ignored issues
show
Bug introduced by
It seems like getMorphClass() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

283
            'auditable_type'       => $this->/** @scrutinizer ignore-call */ getMorphClass(),
Loading history...
284 112
            $morphPrefix . '_id'   => $user ? $user->getAuthIdentifier() : null,
285 112
            $morphPrefix . '_type' => $user ? $user->getMorphClass() : null,
286 112
            'tags'                 => empty($tags) ? null : $tags,
287
        ];
288
289
290 112
        foreach (Config::get('audit.resolvers') as $name => $class) {
291
292 112
            if (is_subclass_of($class, Resolver::class)) {
293 110
                $auditData[$name] = call_user_func_array([$class, 'resolve'], [&$this]);
294 110
                continue;
295
            }
296
297 8
            throw new AuditingException('Invalid Resolver implementation for ' . $name);
298
        }
299
300 104
        return $this->transformAudit($auditData);
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306 102
    public function transformAudit(array $data): array
307
    {
308 102
        return $data;
309
    }
310
311
    /**
312
     * Resolve the User.
313
     *
314
     * @throws AuditingException
315
     *
316
     * @return mixed|null
317
     */
318 112
    protected function resolveUser()
319
    {
320 112
        $userResolver = Config::get('audit.user.resolver');
321
322 112
        if (is_subclass_of($userResolver, Resolver::class)) {
323 112
            return call_user_func_array([$userResolver, 'resolve'], [&$this]);
324
        }
325
326
        throw new AuditingException('Invalid UserResolver implementation');
327
    }
328
329
    /**
330
     * Determine if an attribute is eligible for auditing.
331
     *
332
     * @param string $attribute
333
     *
334
     * @return bool
335
     */
336 104
    protected function isAttributeAuditable(string $attribute): bool
337
    {
338
        // The attribute should not be audited
339 104
        if (in_array($attribute, $this->excludedAttributes, true)) {
340 96
            return false;
341
        }
342
343
        // The attribute is auditable when explicitly
344
        // listed or when the include array is empty
345 104
        $include = $this->getAuditInclude();
346
347 104
        return empty($include) || in_array($attribute, $include, true);
348
    }
349
350
    /**
351
     * Determine whether an event is auditable.
352
     *
353
     * @param string $event
354
     *
355
     * @return bool
356
     */
357 138
    protected function isEventAuditable($event): bool
358
    {
359 138
        return is_string($this->resolveAttributeGetter($event));
360
    }
361
362
    /**
363
     * Attribute getter method resolver.
364
     *
365
     * @param string $event
366
     *
367
     * @return string|null
368
     */
369 138
    protected function resolveAttributeGetter($event)
370
    {
371 138
        foreach ($this->getAuditEvents() as $key => $value) {
372 138
            $auditableEvent = is_int($key) ? $value : $key;
373
374 138
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
375
376 138
            if (preg_match($auditableEventRegex, $event)) {
377 134
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
378
            }
379
        }
380 52
    }
381
382
    /**
383
     * {@inheritdoc}
384
     */
385 138
    public function setAuditEvent(string $event): Contracts\Auditable
386
    {
387 138
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
388
389 138
        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...
390
    }
391
392
    /**
393
     * {@inheritdoc}
394
     */
395 4
    public function getAuditEvent()
396
    {
397 4
        return $this->auditEvent;
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403 144
    public function getAuditEvents(): array
404
    {
405 144
        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...
406 144
            'created',
407
            'updated',
408
            'deleted',
409
            'restored',
410
        ]);
411
    }
412
413
    /**
414
     * Disable Auditing.
415
     *
416
     * @return void
417
     */
418 2
    public static function disableAuditing()
419
    {
420 2
        static::$auditingDisabled = true;
421 2
    }
422
423
    /**
424
     * Enable Auditing.
425
     *
426
     * @return void
427
     */
428 2
    public static function enableAuditing()
429
    {
430 2
        static::$auditingDisabled = false;
431 2
    }
432
433
    /**
434
     * Determine whether auditing is enabled.
435
     *
436
     * @return bool
437
     */
438 190
    public static function isAuditingEnabled(): bool
439
    {
440 190
        if (App::runningInConsole()) {
441 186
            return Config::get('audit.console', false);
442
        }
443
444 4
        return Config::get('audit.enabled', true);
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     */
450 108
    public function getAuditInclude(): array
451
    {
452 108
        return $this->auditInclude ?? [];
453
    }
454
455
    /**
456
     * {@inheritdoc}
457
     */
458 118
    public function getAuditExclude(): array
459
    {
460 118
        return $this->auditExclude ?? [];
461
    }
462
463
    /**
464
     * {@inheritdoc}
465
     */
466 120
    public function getAuditStrict(): bool
467
    {
468 120
        return $this->auditStrict ?? Config::get('audit.strict', false);
469
    }
470
471
    /**
472
     * {@inheritdoc}
473
     */
474 120
    public function getAuditTimestamps(): bool
475
    {
476 120
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
477
    }
478
479
    /**
480
     * {@inheritdoc}
481
     */
482 106
    public function getAuditDriver()
483
    {
484 106
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
485
    }
486
487
    /**
488
     * {@inheritdoc}
489
     */
490 100
    public function getAuditThreshold(): int
491
    {
492 100
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
493
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498 114
    public function getAttributeModifiers(): array
499
    {
500 114
        return $this->attributeModifiers ?? [];
501
    }
502
503
    /**
504
     * {@inheritdoc}
505
     */
506 114
    public function generateTags(): array
507
    {
508 114
        return [];
509
    }
510
511
    /**
512
     * {@inheritdoc}
513
     */
514 22
    public function transitionTo(Contracts\Audit $audit, bool $old = false): Contracts\Auditable
515
    {
516
        // The Audit must be for an Auditable model of this type
517 22
        if ($this->getMorphClass() !== $audit->auditable_type) {
0 ignored issues
show
Bug introduced by
Accessing auditable_type on the interface OwenIt\Auditing\Contracts\Audit suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
518 4
            throw new AuditableTransitionException(sprintf(
519 4
                'Expected Auditable type %s, got %s instead',
520 4
                $this->getMorphClass(),
521 4
                $audit->auditable_type
522
            ));
523
        }
524
525
        // The Audit must be for this specific Auditable model
526 18
        if ($this->getKey() !== $audit->auditable_id) {
0 ignored issues
show
Bug introduced by
Accessing auditable_id on the interface OwenIt\Auditing\Contracts\Audit suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
527 4
            throw new AuditableTransitionException(sprintf(
528 4
                'Expected Auditable id %s, got %s instead',
529 4
                $this->getKey(),
530 4
                $audit->auditable_id
531
            ));
532
        }
533
534
        // Redacted data should not be used when transitioning states
535 14
        foreach ($this->getAttributeModifiers() as $attribute => $modifier) {
536 2
            if (is_subclass_of($modifier, AttributeRedactor::class)) {
537 2
                throw new AuditableTransitionException('Cannot transition states when an AttributeRedactor is set');
538
            }
539
        }
540
541
        // The attribute compatibility between the Audit and the Auditable model must be met
542 12
        $modified = $audit->getModified();
543
544 12
        if ($incompatibilities = array_diff_key($modified, $this->getAttributes())) {
0 ignored issues
show
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

544
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
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

544
        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...
545 2
            throw new AuditableTransitionException(sprintf(
546 2
                'Incompatibility between [%s:%s] and [%s:%s]',
547 2
                $this->getMorphClass(),
548 2
                $this->getKey(),
549 2
                get_class($audit),
550 2
                $audit->getKey()
0 ignored issues
show
Bug introduced by
The method getKey() does not exist on OwenIt\Auditing\Contracts\Audit. Since it exists in all sub-types, consider adding an abstract or default implementation to OwenIt\Auditing\Contracts\Audit. ( Ignorable by Annotation )

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

550
                $audit->/** @scrutinizer ignore-call */ 
551
                        getKey()
Loading history...
551 2
            ), array_keys($incompatibilities));
552
        }
553
554 10
        $key = $old ? 'old' : 'new';
555
556 10
        foreach ($modified as $attribute => $value) {
557 6
            if (array_key_exists($key, $value)) {
558 6
                $this->setAttribute($attribute, $value[$key]);
0 ignored issues
show
Bug introduced by
It seems like setAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

558
                $this->/** @scrutinizer ignore-call */ 
559
                       setAttribute($attribute, $value[$key]);
Loading history...
559
            }
560
        }
561
562 10
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type OwenIt\Auditing\Auditable which is incompatible with the type-hinted return OwenIt\Auditing\Contracts\Auditable.
Loading history...
563
    }
564
}
565