Passed
Pull Request — master (#13)
by
unknown
03:29
created

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

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

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

293
        } elseif ($this->/** @scrutinizer ignore-call */ isDateAttribute($key)) {

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...
294
            return $this->fromDateTime($current) ===
0 ignored issues
show
Bug introduced by
It seems like fromDateTime() 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

294
            return $this->/** @scrutinizer ignore-call */ fromDateTime($current) ===
Loading history...
295
                   $this->fromDateTime($original);
296 4
        } elseif ($this->hasCast($key)) {
0 ignored issues
show
Bug introduced by
It seems like hasCast() 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
        } elseif ($this->/** @scrutinizer ignore-call */ hasCast($key)) {
Loading history...
297
            return $this->castAttribute($key, $current) ===
298
                   $this->castAttribute($key, $original);
299
        }
300
301 4
        return is_numeric($current) && is_numeric($original)
302 4
                && strcmp((string) $current, (string) $original) === 0;
303
    }
304
305
    /**
306
     * Set a given attribute on the model.
307
     *
308
     * @param string $key
309
     * @param mixed  $value
310
     *
311
     * @return void
312
     */
313 11
    public function setAttribute($key, $value)
314
    {
315 11
        parent::setAttribute($key, $value);
316
317 11
        $this->doEncryptAttribute($key);
318 11
    }
319
320
    /**
321
     * Get an attribute from the $attributes array.
322
     *
323
     * @param string $key
324
     *
325
     * @return mixed
326
     */
327 5
    protected function getAttributeFromArray($key)
328
    {
329 5
        return $this->doDecryptAttribute($key, parent::getAttributeFromArray($key));
330
    }
331
332
    /**
333
     * Get an attribute array of all arrayable attributes.
334
     *
335
     * @return array
336
     */
337 1
    protected function getArrayableAttributes()
338
    {
339 1
        return $this->doDecryptAttributes(parent::getArrayableAttributes());
340
    }
341
342
    /**
343
     * Get all of the current attributes on the model.
344
     *
345
     * @return array
346
     */
347 2
    public function getAttributes()
348
    {
349 2
        return $this->doDecryptAttributes(parent::getAttributes());
350
    }
351
}
352