Passed
Pull Request — master (#13)
by
unknown
03:49 queued 45s
created

HasEncryptedAttributes::originalIsEquivalent()   D

Complexity

Conditions 9
Paths 15

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 10.4768

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 15
nop 2
dl 0
loc 27
ccs 14
cts 19
cp 0.7368
crap 10.4768
rs 4.909
c 0
b 0
f 0
1
<?php
2
/**
3
 * src/Traits/HasEncryptedAttributes.php.
4
 *
5
 * @author      Austin Heap <[email protected]>
6
 * @version     v0.1.0
7
 */
8
declare(strict_types=1);
9
10
namespace AustinHeap\Database\Encryption\Traits;
11
12
use Log;
13
use Crypt;
14
use DatabaseEncryption;
0 ignored issues
show
Bug introduced by
The type DatabaseEncryption was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use Illuminate\Support\Arr;
16
use Illuminate\Contracts\Encryption\DecryptException;
17
use Illuminate\Contracts\Encryption\EncryptException;
18
19
/**
20
 * HasEncryptedAttributes.
21
 *
22
 * Automatically encrypt and decrypt Laravel 5.5+ Eloquent values
23
 *
24
 * ### Example
25
 *
26
 * <code>
27
 *   use AustinHeap\Database\Encryption\Traits\HasEncryptedAttributes;
28
 *
29
 *   class User extends Eloquent {
30
 *
31
 *       use HasEncryptedAttributes;
32
 *
33
 *       protected $encrypted = [
34
 *           'address_line_1', 'first_name', 'last_name', 'postcode'
35
 *       ];
36
 *   }
37
 * </code>
38
 *
39
 * ### Summary of Methods in Illuminate\Database\Eloquent\Model
40
 *
41
 * This surveys the major methods in the Laravel Model class as of
42
 * Laravel v5.5 and checks to see how those models set attributes
43
 * and hence how they are affected by this trait.
44
 *
45
 * - __construct -- calls fill()
46
 * - fill() -- calls setAttribute() which has been overridden.
47
 * - hydrate() -- TBD
48
 * - create() -- calls constructor and hence fill()
49
 * - firstOrCreate -- calls constructor
50
 * - firstOrNew -- calls constructor
51
 * - updateOrCreate -- calls fill()
52
 * - update() -- calls fill()
53
 * - toArray() -- calls attributesToArray()
54
 * - jsonSerialize() -- calls toArray()
55
 * - toJson() -- calls toArray()
56
 * - attributesToArray() -- has been over-ridden here.
57
 * - getAttribute -- calls getAttributeValue()
58
 * - getAttributeValue -- calls getAttributeFromArray()
59
 * - getAttributeFromArray -- calls getArrayableAttributes
60
 * - getArrayableAttributes -- has been over-ridden here.
61
 * - setAttribute -- has been over-ridden here.
62
 * - getAttributes -- has been over-ridden here.
63
 *
64
 * @see         Illuminate\Support\Facades\Crypt
65
 * @see         Illuminate\Contracts\Encryption\Encrypter
66
 * @see         Illuminate\Encryption\Encrypter
67
 * @link        http://laravel.com/docs/5.5/eloquent
68
 * @link        https://github.com/austinheap/laravel-database-encryption
69
 * @link        https://packagist.org/packages/austinheap/laravel-database-encryption
70
 * @link        https://austinheap.github.io/laravel-database-encryption/classes/AustinHeap.Database.Encryption.EncryptionServiceProvider.html
71
 */
72
trait HasEncryptedAttributes
73
{
74
    /**
75
     * Private copy of last Encryption exception to occur.
76
     *
77
     * @var null|EncryptException|DecryptException
78
     */
79
    private $lastEncryptionException = null;
80
81
    /**
82
     * Get the last encryption-related exception to occur, if any.
83
     *
84
     * @return null|EncryptException|DecryptException
85
     */
86 2
    public function getLastEncryptionException()
87
    {
88 2
        return $this->lastEncryptionException;
89
    }
90
91
    /**
92
     * Set the last encryption-related exception to occur, if any.
93
     *
94
     * @param null|EncryptException|DecryptException $exception
95
     * @param null|string                            $function
96
     *
97
     * @return self
98
     */
99 2
    protected function setLastEncryptionException($exception, ?string $function = null): self
100
    {
101 2
        Log::debug('Ignored exception "'.get_class($exception).'" in function "'.(is_null($function) ? '(unknown)' : $function).'": '.$exception->getMessage());
0 ignored issues
show
Bug introduced by
The method getMessage() does not exist on null. ( Ignorable by Annotation )

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

101
        Log::debug('Ignored exception "'.get_class($exception).'" in function "'.(is_null($function) ? '(unknown)' : $function).'": '.$exception->/** @scrutinizer ignore-call */ getMessage());

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...
102
103 2
        $this->lastEncryptionException = $exception;
104
105 2
        return $this;
106
    }
107
108
    /**
109
     * Get the configuration setting for the prefix used to determine if a string is encrypted.
110
     *
111
     * @return string
112
     */
113 11
    protected function getEncryptionPrefix(): string
114
    {
115 11
        return DatabaseEncryption::getHeaderPrefix();
116
    }
117
118
    /**
119
     * Determine whether an attribute should be encrypted.
120
     *
121
     * @param string $key
122
     *
123
     * @return bool
124
     */
125 11
    protected function shouldEncrypt($key): bool
126
    {
127 11
        $encrypt = DatabaseEncryption::isEnabled() && isset($this->encrypted) ? $this->encrypted : [];
0 ignored issues
show
Bug Best Practice introduced by
The property encrypted does not exist on AustinHeap\Database\Encr...\HasEncryptedAttributes. Did you maybe forget to declare it?
Loading history...
128
129 11
        return in_array($key, $encrypt);
130
    }
131
132
    /**
133
     * Determine whether a string has already been encrypted.
134
     *
135
     * @param mixed $value
136
     *
137
     * @return bool
138
     */
139 11
    protected function isEncrypted($value): bool
140
    {
141 11
        return strpos((string) $value, $this->getEncryptionPrefix()) === 0;
142
    }
143
144
    /**
145
     * Return the encrypted value of an attribute's value.
146
     *
147
     * This has been exposed as a public method because it is of some
148
     * use when searching.
149
     *
150
     * @param string $value
151
     *
152
     * @return null|string
153
     */
154 9
    public function encryptedAttribute($value): ?string
155
    {
156 9
        return DatabaseEncryption::buildHeader($value).Crypt::encrypt($value);
157
    }
158
159
    /**
160
     * Return the decrypted value of an attribute's encrypted value.
161
     *
162
     * This has been exposed as a public method because it is of some
163
     * use when searching.
164
     *
165
     * @param string $value
166
     *
167
     * @return null|string
168
     */
169 7
    public function decryptedAttribute($value): ?string
170
    {
171 7
        $characters = DatabaseEncryption::getControlCharacters('header');
172 7
        $value = substr($value, strpos($value, $characters['stop']['string']));
173
174 7
        return Crypt::decrypt($value);
175
    }
176
177
    /**
178
     * Encrypt a stored attribute.
179
     *
180
     * @param string $key
181
     *
182
     * @return self
183
     */
184 12
    protected function doEncryptAttribute($key): self
185
    {
186 12
        if ($this->shouldEncrypt($key) && ! $this->isEncrypted($this->attributes[$key])) {
187
            try {
188 12
                $this->attributes[$key] = $this->encryptedAttribute($this->attributes[$key]);
0 ignored issues
show
Bug Best Practice introduced by
The property attributes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
189 1
            } catch (EncryptException $exception) {
190 1
                $this->setLastEncryptionException($exception, __FUNCTION__);
191
            }
192
        }
193
194 12
        return $this;
195
    }
196
197
    /**
198
     * Decrypt an attribute if required.
199
     *
200
     * @param string $key
201
     * @param mixed  $value
202
     *
203
     * @return mixed
204
     */
205 9
    protected function doDecryptAttribute($key, $value)
206
    {
207 9
        if ($this->shouldEncrypt($key) && $this->isEncrypted($value)) {
208
            try {
209 8
                return $this->decryptedAttribute($value);
210 1
            } catch (DecryptException $exception) {
211 1
                $this->setLastEncryptionException($exception, __FUNCTION__);
212
            }
213
        }
214
215 9
        return $value;
216
    }
217
218
    /**
219
     * Decrypt each attribute in the array as required.
220
     *
221
     * @param array $attributes
222
     *
223
     * @return array
224
     */
225 3
    public function doDecryptAttributes($attributes)
226
    {
227 3
        foreach ($attributes as $key => $value) {
228 3
            $attributes[$key] = $this->doDecryptAttribute($key, $value);
229
        }
230
231 3
        return $attributes;
232
    }
233
234
    //
235
    // Methods below here override methods within the base Laravel/Illuminate/Eloquent
236
    // model class and may need adjusting for later releases of Laravel.
237
    //
238
239
    /**
240
     * Decrypt encrypted data before it is processed by cast attribute.
241
     *
242
     * @param $key
243
     * @param $value
244
     *
245
     * @return mixed
246
     */
247 5
    protected function castAttribute($key, $value)
248
    {
249 5
        return parent::castAttribute($key, $this->doDecryptAttribute($key, $value));
250
    }
251
252
    /**
253
     * Get the model's original attribute values.
254
     *
255
     * @param  string|null  $key
256
     * @param  mixed  $default
257
     * @return mixed|array
258
     */
259 6
    public function getOriginal($key = null, $default = null)
260
    {
261 6
        return Arr::get($this->original, $key, $default);
0 ignored issues
show
Bug Best Practice introduced by
The property original does not exist on AustinHeap\Database\Encr...\HasEncryptedAttributes. Did you maybe forget to declare it?
Loading history...
262
    }
263
264
    /**
265
     * Determine if the given attribute is a date or date castable.
266
     *
267
     * @param  string  $key
268
     * @return bool
269
     */
270 9
    protected function isDateAttribute($key)
271
    {
272 9
        return in_array($key, $this->getDates()) ||
0 ignored issues
show
Bug introduced by
It seems like getDates() 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

272
        return in_array($key, $this->/** @scrutinizer ignore-call */ getDates()) ||
Loading history...
273 9
                                    $this->isDateCastable($key);
0 ignored issues
show
Bug introduced by
It seems like isDateCastable() 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

273
                                    $this->/** @scrutinizer ignore-call */ 
274
                                           isDateCastable($key);
Loading history...
274
    }
275
276
    /**
277
     * Convert a DateTime to a storable string.
278
     *
279
     * @param  \DateTime|int  $value
280
     * @return string
281
     */
282 9
    public function fromDateTime($value)
283
    {
284 9
        return empty($value) ? $value : $this->asDateTime($value)->format(
0 ignored issues
show
Bug introduced by
It seems like asDateTime() 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

284
        return empty($value) ? $value : $this->/** @scrutinizer ignore-call */ asDateTime($value)->format(
Loading history...
285 9
            $this->getDateFormat()
0 ignored issues
show
Bug introduced by
It seems like getDateFormat() 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

285
            $this->/** @scrutinizer ignore-call */ 
286
                   getDateFormat()
Loading history...
286
        );
287
    }
288
289
    /**
290
     * Determine whether an attribute should be cast to a native type.
291
     *
292
     * @param  string  $key
293
     * @param  array|string|null  $types
294
     * @return bool
295
     */
296 9
    public function hasCast($key, $types = null)
297
    {
298 9
        if (array_key_exists($key, $this->getCasts())) {
0 ignored issues
show
Bug introduced by
It seems like getCasts() 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

298
        if (array_key_exists($key, $this->/** @scrutinizer ignore-call */ getCasts())) {
Loading history...
299 9
            return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
0 ignored issues
show
Bug introduced by
It seems like getCastType() 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

299
            return $types ? in_array($this->/** @scrutinizer ignore-call */ getCastType($key), (array) $types, true) : true;
Loading history...
300
        }
301
302 9
        return false;
303
    }
304
305
    /**
306
     * Get the attributes that have been changed since last sync.
307
     *
308
     * @return array
309
     */
310 9
    public function getDirty()
311
    {
312 9
        $dirty = [];
313
314 9
        foreach ($this->attributes as $key => $value) {
315 9
            if (! $this->originalIsEquivalent($key, $value)) {
316 9
                $dirty[$key] = $value;
317
            }
318
        }
319
320 9
        return $dirty;
321
    }
322
323
    /**
324
     * Determine if the new and old values for a given key are equivalent.
325
     *
326
     * @param  string $key
327
     * @param  mixed  $current
328
     * @return bool
329
     */
330 9
    protected function originalIsEquivalent($key, $current)
331
    {
332 9
        if (! array_key_exists($key, $this->original)) {
0 ignored issues
show
Bug Best Practice introduced by
The property original does not exist on AustinHeap\Database\Encr...\HasEncryptedAttributes. Did you maybe forget to declare it?
Loading history...
333 9
            return false;
334
        }
335
336 6
        $original = $this->getOriginal($key);
337
338 6
        if ($this->shouldEncrypt($key)) {
339 6
            $current = $this->decryptedAttribute($current);
340 6
            $original = $this->decryptedAttribute($this->getOriginal($key));
341
        }
342
343 6
        if ($current === $original) {
344 6
            return true;
345 4
        } elseif (is_null($current)) {
346
            return false;
347 4
        } elseif ($this->isDateAttribute($key)) {
348
            return $this->fromDateTime($current) ===
0 ignored issues
show
Bug introduced by
It seems like $current can also be of type string; however, parameter $value of AustinHeap\Database\Encr...ributes::fromDateTime() does only seem to accept integer|DateTime, 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

348
            return $this->fromDateTime(/** @scrutinizer ignore-type */ $current) ===
Loading history...
349
                   $this->fromDateTime($original);
350 4
        } elseif ($this->hasCast($key)) {
351
            return $this->castAttribute($key, $current) ===
352
                   $this->castAttribute($key, $original);
353
        }
354
355 4
        return is_numeric($current) && is_numeric($original)
356 4
                && strcmp((string) $current, (string) $original) === 0;
357
    }
358
359
    /**
360
     * Set a given attribute on the model.
361
     *
362
     * @param string $key
363
     * @param mixed  $value
364
     *
365
     * @return void
366
     */
367 11
    public function setAttribute($key, $value)
368
    {
369 11
        parent::setAttribute($key, $value);
370
371 11
        $this->doEncryptAttribute($key);
372 11
    }
373
374
    /**
375
     * Get an attribute from the $attributes array.
376
     *
377
     * @param string $key
378
     *
379
     * @return mixed
380
     */
381 5
    protected function getAttributeFromArray($key)
382
    {
383 5
        return $this->doDecryptAttribute($key, parent::getAttributeFromArray($key));
384
    }
385
386
    /**
387
     * Get an attribute array of all arrayable attributes.
388
     *
389
     * @return array
390
     */
391 1
    protected function getArrayableAttributes()
392
    {
393 1
        return $this->doDecryptAttributes(parent::getArrayableAttributes());
394
    }
395
396
    /**
397
     * Get all of the current attributes on the model.
398
     *
399
     * @return array
400
     */
401 2
    public function getAttributes()
402
    {
403 2
        return $this->doDecryptAttributes(parent::getAttributes());
404
    }
405
}
406