1 | <?php |
||||||
2 | /** |
||||||
3 | * src/Traits/HasEncryptedAttributes.php. |
||||||
4 | * |
||||||
5 | * @author Austin Heap <[email protected]> |
||||||
6 | * @version v0.2.1 |
||||||
7 | */ |
||||||
8 | declare(strict_types=1); |
||||||
9 | |||||||
10 | namespace AustinHeap\Database\Encryption\Traits; |
||||||
11 | |||||||
12 | use AustinHeap\Database\Encryption\EncryptionFacade as DatabaseEncryption; |
||||||
13 | use Illuminate\Contracts\Encryption\DecryptException; |
||||||
14 | use Illuminate\Contracts\Encryption\EncryptException; |
||||||
15 | use Illuminate\Support\Facades\Crypt; |
||||||
16 | use Illuminate\Support\Facades\Log; |
||||||
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
|
|||||||
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 | 21 | protected function getEncryptionPrefix(): string |
|||||
113 | { |
||||||
114 | 21 | return DatabaseEncryption::getHeaderPrefix(); |
|||||
0 ignored issues
–
show
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
Loading history...
|
|||||||
115 | } |
||||||
116 | |||||||
117 | /** |
||||||
118 | * Determine whether an attribute should be encrypted. |
||||||
119 | * |
||||||
120 | * @param string $key |
||||||
121 | * |
||||||
122 | * @return bool |
||||||
123 | */ |
||||||
124 | 21 | protected function shouldEncrypt($key): bool |
|||||
125 | { |
||||||
126 | 21 | $encrypt = DatabaseEncryption::isEnabled() && isset($this->encrypted) && is_array($this->encrypted) ? $this->encrypted : []; |
|||||
0 ignored issues
–
show
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
Loading history...
|
|||||||
127 | |||||||
128 | 21 | return in_array($key, $encrypt, true); |
|||||
129 | } |
||||||
130 | |||||||
131 | /** |
||||||
132 | * Determine whether a model is ready for encryption. |
||||||
133 | * |
||||||
134 | * @return bool |
||||||
135 | */ |
||||||
136 | 21 | protected function isEncryptable(): bool |
|||||
137 | { |
||||||
138 | 21 | $exists = property_exists($this, 'exists'); |
|||||
139 | |||||||
140 | 21 | return $exists === false || ($exists === true && $this->exists === true); |
|||||
141 | } |
||||||
142 | |||||||
143 | /** |
||||||
144 | * Determine whether a string has already been encrypted. |
||||||
145 | * |
||||||
146 | * @param mixed $value |
||||||
147 | * |
||||||
148 | * @return bool |
||||||
149 | */ |
||||||
150 | 21 | protected function isEncrypted($value): bool |
|||||
151 | { |
||||||
152 | 21 | return strpos((string) $value, $this->getEncryptionPrefix()) === 0; |
|||||
153 | } |
||||||
154 | |||||||
155 | /** |
||||||
156 | * Return the encrypted value of an attribute's value. |
||||||
157 | * |
||||||
158 | * This has been exposed as a public method because it is of some |
||||||
159 | * use when searching. |
||||||
160 | * |
||||||
161 | * @param string $value |
||||||
162 | * |
||||||
163 | * @return null|string |
||||||
164 | */ |
||||||
165 | 18 | public function encryptedAttribute($value): ?string |
|||||
166 | { |
||||||
167 | 18 | return DatabaseEncryption::buildHeader($value).Crypt::encrypt($value); |
|||||
0 ignored issues
–
show
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
Loading history...
|
|||||||
168 | } |
||||||
169 | |||||||
170 | /** |
||||||
171 | * Return the decrypted value of an attribute's encrypted value. |
||||||
172 | * |
||||||
173 | * This has been exposed as a public method because it is of some |
||||||
174 | * use when searching. |
||||||
175 | * |
||||||
176 | * @param string $value |
||||||
177 | * |
||||||
178 | * @return null|mixed |
||||||
179 | * @throws \Throwable |
||||||
180 | */ |
||||||
181 | 11 | public function decryptedAttribute($value) |
|||||
182 | { |
||||||
183 | 11 | $characters = DatabaseEncryption::getControlCharacters('header'); |
|||||
0 ignored issues
–
show
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
Loading history...
|
|||||||
184 | |||||||
185 | 11 | throw_if(! array_key_exists('stop', $characters), DecryptException::class, 'Cannot decrypt model attribute not originally encrypted by this package!'); |
|||||
186 | |||||||
187 | 11 | $offset = strpos($value, $characters['stop']['string']); |
|||||
188 | |||||||
189 | 11 | throw_if($offset === false, DecryptException::class, 'Cannot decrypt model attribute with no package header!'); |
|||||
190 | |||||||
191 | 11 | $value = substr($value, $offset); |
|||||
192 | |||||||
193 | 11 | return Crypt::decrypt($value); |
|||||
194 | } |
||||||
195 | |||||||
196 | /** |
||||||
197 | * Encrypt a stored attribute. |
||||||
198 | * |
||||||
199 | * @param string $key |
||||||
200 | * |
||||||
201 | * @return self |
||||||
202 | */ |
||||||
203 | 22 | protected function doEncryptAttribute($key): self |
|||||
204 | { |
||||||
205 | 22 | if ($this->shouldEncrypt($key) && ! $this->isEncrypted($this->attributes[$key])) { |
|||||
206 | try { |
||||||
207 | 22 | $this->attributes[$key] = $this->encryptedAttribute($this->attributes[$key]); |
|||||
0 ignored issues
–
show
|
|||||||
208 | 1 | } catch (EncryptException $exception) { |
|||||
209 | 1 | $this->setLastEncryptionException($exception, __FUNCTION__); |
|||||
210 | } |
||||||
211 | } |
||||||
212 | |||||||
213 | 22 | return $this; |
|||||
214 | } |
||||||
215 | |||||||
216 | /** |
||||||
217 | * Decrypt an attribute if required. |
||||||
218 | * |
||||||
219 | * @param string $key |
||||||
220 | * @param mixed $value |
||||||
221 | * |
||||||
222 | * @return mixed |
||||||
223 | */ |
||||||
224 | 17 | protected function doDecryptAttribute($key, $value) |
|||||
225 | { |
||||||
226 | 17 | if ($this->shouldEncrypt($key) && $this->isEncrypted($value)) { |
|||||
227 | try { |
||||||
228 | 14 | return $this->decryptedAttribute($value); |
|||||
229 | 1 | } catch (DecryptException $exception) { |
|||||
230 | 1 | $this->setLastEncryptionException($exception, __FUNCTION__); |
|||||
231 | } |
||||||
232 | } |
||||||
233 | |||||||
234 | 13 | return $value; |
|||||
235 | } |
||||||
236 | |||||||
237 | /** |
||||||
238 | * Decrypt each attribute in the array as required. |
||||||
239 | * |
||||||
240 | * @param array $attributes |
||||||
241 | * |
||||||
242 | * @return array |
||||||
243 | */ |
||||||
244 | 3 | public function doDecryptAttributes($attributes) |
|||||
245 | { |
||||||
246 | 3 | foreach ($attributes as $key => $value) { |
|||||
247 | 3 | $attributes[$key] = $this->doDecryptAttribute($key, $value); |
|||||
248 | } |
||||||
249 | |||||||
250 | 3 | return $attributes; |
|||||
251 | } |
||||||
252 | |||||||
253 | // |
||||||
254 | // Methods below here override methods within the base Laravel/Illuminate/Eloquent |
||||||
255 | // model class and may need adjusting for later releases of Laravel. |
||||||
256 | // |
||||||
257 | |||||||
258 | /** |
||||||
259 | * Decrypt encrypted data before it is processed by cast attribute. |
||||||
260 | * |
||||||
261 | * @param $key |
||||||
262 | * @param $value |
||||||
263 | * |
||||||
264 | * @return mixed |
||||||
265 | */ |
||||||
266 | 7 | protected function castAttribute($key, $value) |
|||||
267 | { |
||||||
268 | 7 | return parent::castAttribute($key, $this->doDecryptAttribute($key, $value)); |
|||||
269 | } |
||||||
270 | |||||||
271 | /** |
||||||
272 | * Get the attributes that have been changed since last sync. |
||||||
273 | * |
||||||
274 | * @return array |
||||||
275 | */ |
||||||
276 | 17 | public function getDirty() |
|||||
277 | { |
||||||
278 | 17 | $dirty = []; |
|||||
279 | |||||||
280 | 17 | foreach ($this->attributes as $key => $value) { |
|||||
281 | 17 | if (! $this->originalIsEquivalent($key, $value)) { |
|||||
0 ignored issues
–
show
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
Loading history...
|
|||||||
282 | 17 | $dirty[$key] = $value; |
|||||
283 | } |
||||||
284 | } |
||||||
285 | |||||||
286 | 17 | return $dirty; |
|||||
287 | } |
||||||
288 | |||||||
289 | /** |
||||||
290 | * Set a given attribute on the model. |
||||||
291 | * |
||||||
292 | * @param string $key |
||||||
293 | * @param mixed $value |
||||||
294 | * |
||||||
295 | * @return void |
||||||
296 | */ |
||||||
297 | 21 | public function setAttribute($key, $value) |
|||||
298 | { |
||||||
299 | 21 | parent::setAttribute($key, $value); |
|||||
300 | |||||||
301 | 21 | $this->doEncryptAttribute($key); |
|||||
302 | 21 | } |
|||||
303 | |||||||
304 | /** |
||||||
305 | * Get an attribute from the $attributes array. |
||||||
306 | * |
||||||
307 | * @param string $key |
||||||
308 | * |
||||||
309 | * @return mixed |
||||||
310 | */ |
||||||
311 | 13 | protected function getAttributeFromArray($key) |
|||||
312 | { |
||||||
313 | 13 | return $this->doDecryptAttribute($key, parent::getAttributeFromArray($key)); |
|||||
314 | } |
||||||
315 | |||||||
316 | /** |
||||||
317 | * Get an attribute array of all arrayable attributes. |
||||||
318 | * |
||||||
319 | * @return array |
||||||
320 | */ |
||||||
321 | 1 | protected function getArrayableAttributes() |
|||||
322 | { |
||||||
323 | 1 | return $this->doDecryptAttributes(parent::getArrayableAttributes()); |
|||||
324 | } |
||||||
325 | |||||||
326 | /** |
||||||
327 | * Get all of the current attributes on the model. |
||||||
328 | * |
||||||
329 | * @return array |
||||||
330 | */ |
||||||
331 | 19 | public function getAttributes() |
|||||
332 | { |
||||||
333 | 19 | return $this->isEncryptable() ? $this->doDecryptAttributes(parent::getAttributes()) : parent::getAttributes(); |
|||||
334 | } |
||||||
335 | } |
||||||
336 |
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.