Completed
Pull Request — master (#436)
by
unknown
12:03
created

Auditable::getAuditDisabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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
     * Audit disabled.
47
     *
48
     * @var string
49
     */
50
    protected $auditDisabled;
51
52
    /**
53
     * Is auditing disabled?
54
     *
55
     * @var bool
56
     */
57
    public static $auditingDisabled = false;
58
59
    /**
60
     * Auditable boot logic.
61
     *
62
     * @return void
63
     */
64 261
    public static function bootAuditable()
65
    {
66 261
        if (static::isAuditingEnabled()) {
67 258
            static::observe(new AuditableObserver());
68
        }
69 261
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 36
    public function audits(): MorphMany
75
    {
76 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

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

106
            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

106
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->/** @scrutinizer ignore-call */ getUpdatedAtColumn());
Loading history...
107
108 156
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
109 144
                $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

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

171
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
172 12
            if ($this->isAttributeAuditable($attribute)) {
173 12
                $old[$attribute] = array_get($this->original, $attribute);
174 12
                $new[$attribute] = array_get($this->attributes, $attribute);
175
            }
176
        }
177
178
        return [
179 15
            $old,
180 15
            $new,
181
        ];
182
    }
183
184
    /**
185
     * Get the old/new attributes of a deleted event.
186
     *
187
     * @return array
188
     */
189 12
    protected function getDeletedEventAttributes(): array
190
    {
191 12
        $old = [];
192
193 12
        foreach ($this->attributes as $attribute => $value) {
194 12
            if ($this->isAttributeAuditable($attribute)) {
195 12
                $old[$attribute] = $value;
196
            }
197
        }
198
199
        return [
200 12
            $old,
201
            [],
202
        ];
203
    }
204
205
    /**
206
     * Get the old/new attributes of a restored event.
207
     *
208
     * @return array
209
     */
210 6
    protected function getRestoredEventAttributes(): array
211
    {
212
        // A restored event is just a deleted event in reverse
213 6
        return array_reverse($this->getDeletedEventAttributes());
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219 189
    public function readyForAuditing(): bool
220
    {
221 189
        if (static::$auditingDisabled || $this->getAuditDisabled()) {
222 3
            return false;
223
        }
224
225 189
        return $this->isEventAuditable($this->auditEvent);
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 189
    public function getAuditDisabled(): bool
232
    {
233 189
        return $this->auditDisabled ?? Config::get('audit.disabled', false);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->auditDisab...audit.disabled', false) could return the type string which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
234
    }
235
236
    /**
237
     * Redact attribute value.
238
     *
239
     * @param string $attribute
240
     * @param mixed  $value
241
     *
242
     * @throws AuditingException
243
     *
244
     * @return mixed
245
     */
246 6
    protected function redactAttributeValue(string $attribute, $value)
247
    {
248 6
        $auditRedactors = $this->getAuditRedactors();
249
250 6
        if (!array_key_exists($attribute, $auditRedactors)) {
251 3
            return $value;
252
        }
253
254 6
        $auditRedactor = $auditRedactors[$attribute];
255
256 6
        if (is_subclass_of($auditRedactor, AuditRedactor::class)) {
257 3
            return call_user_func([$auditRedactor, 'redact'], $value);
258
        }
259
260 3
        throw new AuditingException('Invalid AuditRedactor implementation');
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266 171
    public function toAudit(): array
267
    {
268 171
        if (!$this->readyForAuditing()) {
269 3
            throw new AuditingException('A valid audit event has not been set');
270
        }
271
272 168
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
273
274 168
        if (!method_exists($this, $attributeGetter)) {
275 12
            throw new AuditingException(sprintf(
276 12
                'Unable to handle "%s" event, %s() method missing',
277 12
                $this->auditEvent,
278 12
                $attributeGetter
279
            ));
280
        }
281
282 156
        $this->resolveAuditExclusions();
283
284 156
        list($old, $new) = $this->$attributeGetter();
285
286 156
        if ($this->getAuditRedactors() && Config::get('audit.redact', false)) {
287 6
            foreach ($old as $attribute => $value) {
288 3
                $old[$attribute] = $this->redactAttributeValue($attribute, $value);
289
            }
290
291 6
            foreach ($new as $attribute => $value) {
292 6
                $new[$attribute] = $this->redactAttributeValue($attribute, $value);
293
            }
294
        }
295
296 153
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
297
298 153
        $tags = implode(',', $this->generateTags());
299
300 153
        $user = $this->resolveUser();
301
302 150
        return $this->transformAudit([
303 150
            'old_values'         => $old,
304 150
            'new_values'         => $new,
305 150
            'event'              => $this->auditEvent,
306 150
            '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

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

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

613
        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

613
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
614 3
            throw new AuditableTransitionException(sprintf(
615 3
                'Incompatibility between [%s:%s] and [%s:%s]',
616 3
                $this->getMorphClass(),
617 3
                $this->getKey(),
618 3
                get_class($audit),
619 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

619
                $audit->/** @scrutinizer ignore-call */ 
620
                        getKey()
Loading history...
620 3
            ), array_keys($incompatibilities));
621
        }
622
623 12
        $key = $old ? 'old' : 'new';
624
625 12
        foreach ($modified as $attribute => $value) {
626 9
            if (array_key_exists($key, $value)) {
627 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

627
                $this->/** @scrutinizer ignore-call */ 
628
                       setAttribute($attribute, $value[$key]);
Loading history...
628
            }
629
        }
630
631 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...
632
    }
633
}
634