Passed
Pull Request — master (#32)
by Axel
06:37
created

HasEncryptedAttributes::getUnencryptedAttributes()   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 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * src/Traits/HasEncryptedAttributes.php.
4
 *
5
 * @author      Austin Heap <[email protected]>
6
 * @version     v0.2.0
7
 */
8
declare(strict_types=1);
9
10
namespace AustinHeap\Database\Encryption\Traits;
11
12
use Illuminate\Support\Facades\Log;
13
use Illuminate\Support\Facades\Crypt;
14
use Illuminate\Contracts\Encryption\DecryptException;
15
use Illuminate\Contracts\Encryption\EncryptException;
16
use AustinHeap\Database\Encryption\EncryptionFacade as DatabaseEncryption;
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
 * - getUnencryptedAttributes -- calls doDecryptAttributes()
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 16
    protected function getEncryptionPrefix(): string
113
    {
114 16
        return DatabaseEncryption::getHeaderPrefix();
0 ignored issues
show
Bug introduced by
The method getHeaderPrefix() does not exist on AustinHeap\Database\Encryption\EncryptionFacade. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

114
        return DatabaseEncryption::/** @scrutinizer ignore-call */ getHeaderPrefix();
Loading history...
115
    }
116
117
    /**
118
     * Determine whether an attribute should be encrypted.
119
     *
120
     * @param string $key
121
     *
122
     * @return bool
123
     */
124 16
    protected function shouldEncrypt($key): bool
125
    {
126 16
        $encrypt = DatabaseEncryption::isEnabled() && isset($this->encrypted) && is_array($this->encrypted) ? $this->encrypted : [];
0 ignored issues
show
Bug introduced by
The method isEnabled() does not exist on AustinHeap\Database\Encryption\EncryptionFacade. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

126
        $encrypt = DatabaseEncryption::/** @scrutinizer ignore-call */ isEnabled() && isset($this->encrypted) && is_array($this->encrypted) ? $this->encrypted : [];
Loading history...
127
128 16
        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 16
    protected function isEncrypted($value): bool
139
    {
140 16
        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 14
    public function encryptedAttribute($value): ?string
154
    {
155 14
        return DatabaseEncryption::buildHeader($value).Crypt::encrypt($value);
0 ignored issues
show
Bug introduced by
The method buildHeader() does not exist on AustinHeap\Database\Encryption\EncryptionFacade. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

155
        return DatabaseEncryption::/** @scrutinizer ignore-call */ buildHeader($value).Crypt::encrypt($value);
Loading history...
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|mixed
167
     * @throws \Throwable
168
     */
169 10
    public function decryptedAttribute($value)
170
    {
171 10
        $characters = DatabaseEncryption::getControlCharacters('header');
0 ignored issues
show
Bug introduced by
The method getControlCharacters() does not exist on AustinHeap\Database\Encryption\EncryptionFacade. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

171
        /** @scrutinizer ignore-call */ 
172
        $characters = DatabaseEncryption::getControlCharacters('header');
Loading history...
172
173 10
        throw_if(! array_key_exists('stop', $characters), DecryptException::class, 'Cannot decrypt model attribute not originally encrypted by this package!');
174
175 10
        $offset = strpos($value, $characters['stop']['string']);
176
177 10
        throw_if($offset === false, DecryptException::class, 'Cannot decrypt model attribute with no package header!');
178
179 10
        $value = substr($value, $offset);
180
181 10
        return Crypt::decrypt($value);
182
    }
183
184
    /**
185
     * Encrypt a stored attribute.
186
     *
187
     * @param string $key
188
     *
189
     * @return self
190
     */
191 17
    protected function doEncryptAttribute($key): self
192
    {
193 17
        if ($this->shouldEncrypt($key) && ! $this->isEncrypted($this->attributes[$key])) {
194
            try {
195 17
                $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...
196 1
            } catch (EncryptException $exception) {
197 1
                $this->setLastEncryptionException($exception, __FUNCTION__);
198
            }
199
        }
200
201 17
        return $this;
202
    }
203
204
    /**
205
     * Decrypt an attribute if required.
206
     *
207
     * @param string $key
208
     * @param mixed  $value
209
     *
210
     * @return mixed
211
     */
212 15
    protected function doDecryptAttribute($key, $value)
213
    {
214 15
        if ($this->shouldEncrypt($key) && $this->isEncrypted($value)) {
215
            try {
216 13
                return $this->decryptedAttribute($value);
217 1
            } catch (DecryptException $exception) {
218 1
                $this->setLastEncryptionException($exception, __FUNCTION__);
219
            }
220
        }
221
222 11
        return $value;
223
    }
224
225
    /**
226
     * Decrypt each attribute in the array as required.
227
     *
228
     * @param array $attributes
229
     *
230
     * @return array
231
     */
232 3
    public function doDecryptAttributes($attributes)
233
    {
234 3
        foreach ($attributes as $key => $value) {
235 3
            $attributes[$key] = $this->doDecryptAttribute($key, $value);
236
        }
237
238 3
        return $attributes;
239
    }
240
241
    //
242
    // Methods below here override methods within the base Laravel/Illuminate/Eloquent
243
    // model class and may need adjusting for later releases of Laravel.
244
    //
245
246
    /**
247
     * Decrypt encrypted data before it is processed by cast attribute.
248
     *
249
     * @param $key
250
     * @param $value
251
     *
252
     * @return mixed
253
     */
254 5
    protected function castAttribute($key, $value)
255
    {
256 5
        return parent::castAttribute($key, $this->doDecryptAttribute($key, $value));
257
    }
258
259
    /**
260
     * Get the attributes that have been changed since last sync.
261
     *
262
     * @return array
263
     */
264 14
    public function getDirty()
265
    {
266 14
        $dirty = [];
267
268 14
        foreach ($this->attributes as $key => $value) {
269 14
            if (! $this->originalIsEquivalent($key, $value)) {
0 ignored issues
show
Bug introduced by
It seems like originalIsEquivalent() 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

269
            if (! $this->/** @scrutinizer ignore-call */ originalIsEquivalent($key, $value)) {
Loading history...
270 14
                $dirty[$key] = $value;
271
            }
272
        }
273
274 14
        return $dirty;
275
    }
276
277
    /**
278
     * Set a given attribute on the model.
279
     *
280
     * @param string $key
281
     * @param mixed  $value
282
     *
283
     * @return void
284
     */
285 16
    public function setAttribute($key, $value)
286
    {
287 16
        parent::setAttribute($key, $value);
288
289 16
        $this->doEncryptAttribute($key);
290 16
    }
291
292
    /**
293
     * Get an attribute from the $attributes array.
294
     *
295
     * @param string $key
296
     *
297
     * @return mixed
298
     */
299 11
    protected function getAttributeFromArray($key)
300
    {
301 11
        return $this->doDecryptAttribute($key, parent::getAttributeFromArray($key));
302
    }
303
304
    /**
305
     * Get an attribute array of all arrayable attributes.
306
     *
307
     * @return array
308
     */
309 1
    protected function getArrayableAttributes()
310
    {
311 1
        return $this->doDecryptAttributes(parent::getArrayableAttributes());
312
    }
313
314
    /**
315
     * Get all of the current unencrypted attributes on the model.
316
     *
317
     * @return array
318
     */
319 2
    public function getUnencryptedAttributes()
320
    {
321 2
        return $this->doDecryptAttributes(parent::getAttributes());
322
    }
323
}
324