Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.32% covered (success)
90.32%
84 / 93
66.67% covered (warning)
66.67%
8 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Encoded
90.32% covered (success)
90.32%
84 / 93
66.67% covered (warning)
66.67%
8 / 12
82.37
0.00% covered (danger)
0.00%
0 / 1
 setColumns
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 toArray
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 encodeValue
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
23
 decodeValue
89.29% covered (success)
89.29%
25 / 28
0.00% covered (danger)
0.00%
0 / 1
18.40
 verify
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 encode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 decode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 isEncodedColumn
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
5
 loadEncryptionProperties
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
16.67
 getRawValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __set
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 __get
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Db\Record;
15
16use Pop\Crypt\Hashing\Hasher;
17use Pop\Crypt\Encryption\Encrypter;
18
19/**
20 * Encoded record class
21 *
22 * @category   Pop
23 * @package    Pop\Db
24 * @author     Nick Sagona, III <dev@noladev.com>
25 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    6.7.0
28 */
29class Encoded extends \Pop\Db\Record
30{
31
32    /**
33     * JSON-encoded fields
34     * @var array
35     */
36    protected array $jsonFields = [];
37
38    /**
39     * PHP-serialized fields
40     * @var array
41     */
42    protected array $phpFields = [];
43
44    /**
45     * Base64-encoded fields
46     * @var array
47     */
48    protected array $base64Fields = [];
49
50    /**
51     * Password-hashed fields
52     * @var array
53     */
54    protected array $hashFields = [];
55
56    /**
57     * Encrypted fields
58     * @var array
59     */
60    protected array $encryptedFields = [];
61
62    /**
63     * Hash algorithm
64     * @var string
65     */
66    protected string $hashAlgorithm = PASSWORD_BCRYPT;
67
68    /**
69     * Hash options
70     * @var array
71     */
72    protected array $hashOptions = [];
73
74    /**
75     * Encryption cipher method
76     * @var ?string
77     */
78    protected ?string $cipherMethod = null;
79
80    /**
81     * Encryption key
82     * @var ?string
83     */
84    protected ?string $key = null;
85
86    /**
87     * Encryption previous keys
88     * @var array
89     */
90    protected array $previousKeys = [];
91
92    /**
93     * Set all the table column values at once
94     *
95     * @param  mixed  $columns
96     * @throws Exception
97     * @return Encoded
98     */
99    public function setColumns(mixed $columns = null): Encoded
100    {
101        if ($columns !== null) {
102            if (is_array($columns) || ($columns instanceof \ArrayObject)) {
103                $columns = $this->encode($columns);
104            } else if ($columns instanceof AbstractRecord) {
105                $columns = $this->encode($columns->toArray());
106            } else {
107                throw new Exception('The parameter passed must be either an array, an array object or null.');
108            }
109
110            parent::setColumns($columns);
111        }
112
113        return $this;
114    }
115
116    /**
117     * Get column values as array
118     *
119     * @throws Exception
120     * @return array
121     */
122    public function toArray(): array
123    {
124        $result = parent::toArray();
125
126        foreach ($result as $key => $value) {
127            if (($this->isEncodedColumn($key)) && ($value !== null)) {
128                $result[$key] = $this->decodeValue($key, $value);
129            }
130        }
131
132        return $result;
133    }
134
135    /**
136     * Encode value
137     *
138     * @param  string $key
139     * @param  mixed  $value
140     * @throws Exception
141     * @return string
142     */
143    public function encodeValue(string $key, mixed $value): string
144    {
145        if (in_array($key, $this->jsonFields)) {
146            if (!((is_string($value) && (json_decode($value) !== false)) && (json_last_error() == JSON_ERROR_NONE))) {
147                $value = json_encode($value);
148            }
149        } else if (in_array($key, $this->phpFields)) {
150            if (!(is_string($value) && (@unserialize($value) !== false))) {
151                $value = serialize($value);
152            }
153        } else if (in_array($key, $this->base64Fields)) {
154            if (!(is_string($value) && (base64_encode(base64_decode($value)) === $value))) {
155                $value = base64_encode($value);
156            }
157        } else if (in_array($key, $this->hashFields)) {
158            $hasher = Hasher::create($this->hashAlgorithm, $this->hashOptions);
159            $info   = $hasher->getInfo($value);
160            if (((int)$info['algo'] == 0) || (strtolower($info['algoName']) == 'unknown')) {
161                $value = $hasher->make($value);
162            }
163        } else if (in_array($key, $this->encryptedFields)) {
164            // Attempt to load encryption properties from $_ENV
165            if (empty($this->cipherMethod) || empty($this->key)) {
166                $this->loadEncryptionProperties();
167            }
168            if (empty($this->cipherMethod) || empty($this->key)) {
169                throw new Exception('Error: The encryption properties have not been set.');
170            }
171
172            $encrypter = new Encrypter($this->key, $this->cipherMethod, false);
173
174            // Load any previous encryption keys
175            if (!empty($this->previousKeys)) {
176                $encrypter->setPreviousKeys($this->previousKeys, false);
177            }
178
179            $decodedValue = $this->decodeValue($key, $value);
180            if (!(is_string($value) && ($decodedValue !== false) && ($decodedValue != $value))) {
181                $value = $encrypter->encrypt($value);
182            }
183        }
184
185        return $value;
186    }
187
188    /**
189     * Decode value
190     *
191     * @param  string $key
192     * @param  string  $value
193     * @throws Exception
194     * @return mixed
195     */
196    public function decodeValue(string $key, string $value): mixed
197    {
198        if (in_array($key, $this->jsonFields)) {
199            if ($value !== null) {
200                $jsonValue = @json_decode($value, true);
201                if (json_last_error() === JSON_ERROR_NONE) {
202                    $value = $jsonValue;
203                }
204            }
205        } else if (in_array($key, $this->phpFields)) {
206            if ($value !== null) {
207                $phpValue = @unserialize($value);
208                if ($phpValue !== false) {
209                    $value = $phpValue;
210                }
211            }
212        } else if (in_array($key, $this->base64Fields)) {
213            if ($value !== null) {
214                $base64Value = @base64_decode($value, true);
215                if ($base64Value !== false) {
216                    $value = $base64Value;
217                }
218            }
219        } else if (in_array($key, $this->encryptedFields)) {
220            // Attempt to load encryption properties from $_ENV
221            if (empty($this->cipherMethod) || empty($this->key)) {
222                $this->loadEncryptionProperties();
223            }
224            if (empty($this->cipherMethod) || empty($this->key)) {
225                throw new Exception('Error: The encryption properties have not been set.');
226            }
227
228            $encrypter = new Encrypter($this->key, $this->cipherMethod, false);
229
230            // Load any previous encryption keys
231            if (!empty($this->previousKeys)) {
232                $encrypter->setPreviousKeys($this->previousKeys, false);
233            }
234
235            if ($value !== null) {
236                $base64Value = @base64_decode($value, true);
237                if ($base64Value !== false) {
238                    $value = $encrypter->decrypt($value);
239                }
240            }
241        }
242
243        return $value;
244    }
245
246    /**
247     * Verify value against hash
248     *
249     * @param  string $key
250     * @param  string $value
251     * @return bool
252     */
253    public function verify(string $key, string $value): bool
254    {
255        $hasher = Hasher::create($this->hashAlgorithm, $this->hashOptions);
256        return $hasher->verify($value, $this->{$key});
257    }
258
259    /**
260     * Scrub the column values and encode them
261     *
262     * @param  array $columns
263     * @throws Exception
264     * @return array
265     */
266    public function encode(array $columns): array
267    {
268        foreach ($columns as $key => $value) {
269            if (($value !== null) && ($this->isEncodedColumn($key))) {
270                $columns[$key] = $this->encodeValue($key, $value);
271            }
272        }
273
274        return $columns;
275    }
276
277    /**
278     * Scrub the column values and decode them
279     *
280     * @param  array $columns
281     * @throws Exception
282     * @return array
283     */
284    public function decode(array $columns): array
285    {
286        foreach ($columns as $key => $value) {
287            if (($this->isEncodedColumn($key)) && ($value !== null)) {
288                $columns[$key] = $this->decodeValue($key, $value);
289            }
290        }
291
292        return $columns;
293    }
294
295    /**
296     * Determine if column is an encoded column
297     *
298     * @param  string $key
299     * @return bool
300     */
301    public function isEncodedColumn(string $key): bool
302    {
303        return (in_array($key, $this->jsonFields) || in_array($key, $this->phpFields) ||
304            in_array($key, $this->base64Fields) || in_array($key, $this->hashFields) || in_array($key, $this->encryptedFields));
305    }
306
307    /**
308     * Attempt to load encryption properties from $_ENV vars
309     *
310     * @return void
311     */
312    public function loadEncryptionProperties(): void
313    {
314        if (empty($this->cipherMethod) && !empty($_ENV['APP_CIPHER_METHOD'])) {
315            $this->cipherMethod = trim($_ENV['APP_CIPHER_METHOD']);
316        }
317        if (empty($this->key) && !empty($_ENV['APP_KEY'])) {
318            $this->key = trim($_ENV['APP_KEY']);
319            if (!empty($_ENV['APP_PREVIOUS_KEYS'])) {
320                $this->previousKeys = array_map('trim', explode(',', $_ENV['APP_PREVIOUS_KEYS']));
321            }
322        }
323    }
324
325    /**
326     * Get raw un-encoded value
327     *
328     * @param  string $name
329     * @return mixed
330     */
331    public function getRawValue(string $name): mixed
332    {
333        return parent::__get($name);
334    }
335
336    /**
337     * Magic method to set the property to the value of $this->rowGateway[$name]
338     *
339     * @param  string $name
340     * @param  mixed  $value
341     * @throws Exception
342     * @return void
343     */
344    public function __set(string $name, mixed $value): void
345    {
346        if (($value !== null) && ($this->isEncodedColumn($name))) {
347            $value = $this->encodeValue($name, $value);
348        }
349        parent::__set($name, $value);
350    }
351
352    /**
353     * Magic method to return the value of $this->rowGateway[$name]
354     *
355     * @param  string $name
356     * @throws Exception
357     * @return mixed
358     */
359    public function __get(string $name): mixed
360    {
361        $value = parent::__get($name);
362
363        if (($this->isEncodedColumn($name)) && ($value !== null)) {
364            $value = $this->decodeValue($name, $value);
365        }
366
367        return $value;
368    }
369
370}