Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.72% covered (success)
98.72%
77 / 78
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Encoded
98.72% covered (success)
98.72%
77 / 78
90.00% covered (success)
90.00%
9 / 10
66
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
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
21
 decodeValue
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
16
 verify
100.00% covered (success)
100.00%
1 / 1
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
 __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 (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Db\Record;
15
16/**
17 * Encoded record class
18 *
19 * @category   Pop
20 * @package    Pop\Db
21 * @author     Nick Sagona, III <dev@nolainteractive.com>
22 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
23 * @license    http://www.popphp.org/license     New BSD License
24 * @version    6.5.0
25 */
26class Encoded extends \Pop\Db\Record
27{
28
29    /**
30     * JSON-encoded fields
31     * @var array
32     */
33    protected array $jsonFields = [];
34
35    /**
36     * PHP-serialized fields
37     * @var array
38     */
39    protected array $phpFields = [];
40
41    /**
42     * Base64-encoded fields
43     * @var array
44     */
45    protected array $base64Fields = [];
46
47    /**
48     * Password-hashed fields
49     * @var array
50     */
51    protected array $hashFields = [];
52
53    /**
54     * Encrypted fields
55     * @var array
56     */
57    protected array $encryptedFields = [];
58
59    /**
60     * Hash algorithm
61     * @var string
62     */
63    protected string $hashAlgorithm = PASSWORD_BCRYPT;
64
65    /**
66     * Hash options
67     * @var array
68     */
69    protected array $hashOptions = ['cost' => 10];
70
71    /**
72     * Cipher method
73     * @var ?string
74     */
75    protected ?string $cipherMethod = null;
76
77    /**
78     * Encrypted field key
79     * @var ?string
80     */
81    protected ?string $key = null;
82
83    /**
84     * Encrypted field IV (base64-encoded)
85     * @var ?string
86     */
87    protected ?string $iv = null;
88
89    /**
90     * Set all the table column values at once
91     *
92     * @param  mixed  $columns
93     * @throws Exception
94     * @return Encoded
95     */
96    public function setColumns(mixed $columns = null): Encoded
97    {
98        if ($columns !== null) {
99            if (is_array($columns) || ($columns instanceof \ArrayObject)) {
100                $columns = $this->encode($columns);
101            } else if ($columns instanceof AbstractRecord) {
102                $columns = $this->encode($columns->toArray());
103            } else {
104                throw new Exception('The parameter passed must be either an array, an array object or null.');
105            }
106
107            parent::setColumns($columns);
108        }
109
110        return $this;
111    }
112
113    /**
114     * Get column values as array
115     *
116     * @throws Exception
117     * @return array
118     */
119    public function toArray(): array
120    {
121        $result = parent::toArray();
122
123        foreach ($result as $key => $value) {
124            if (($this->isEncodedColumn($key)) && ($value !== null)) {
125                $result[$key] = $this->decodeValue($key, $value);
126            }
127        }
128
129        return $result;
130    }
131
132    /**
133     * Encode value
134     *
135     * @param  string $key
136     * @param  mixed  $value
137     * @throws Exception
138     * @return string
139     */
140    public function encodeValue(string $key, mixed $value): string
141    {
142        if (in_array($key, $this->jsonFields)) {
143            if (!((is_string($value) && (json_decode($value) !== false)) && (json_last_error() == JSON_ERROR_NONE))) {
144                $value = json_encode($value);
145            }
146        } else if (in_array($key, $this->phpFields)) {
147            if (!(is_string($value) && (@unserialize($value) !== false))) {
148                $value = serialize($value);
149            }
150        } else if (in_array($key, $this->base64Fields)) {
151            if (!(is_string($value) && (base64_encode(base64_decode($value)) === $value))) {
152                $value = base64_encode($value);
153            }
154        } else if (in_array($key, $this->hashFields)) {
155            $info = password_get_info($value);
156            if (((int)$info['algo'] == 0) || (strtolower($info['algoName']) == 'unknown')) {
157                $value = password_hash($value, $this->hashAlgorithm, $this->hashOptions);
158            }
159        } else if (in_array($key, $this->encryptedFields)) {
160            if (empty($this->cipherMethod) || empty($this->key) || empty($this->iv)) {
161                throw new Exception('Error: The encryption properties have not been set for this class.');
162            }
163            $decodedValue = $this->decodeValue($key, $value);
164            if (!(is_string($value) && ($decodedValue !== false) && ($decodedValue != $value))) {
165                $value = base64_encode(
166                    openssl_encrypt($value, $this->cipherMethod, $this->key, OPENSSL_RAW_DATA, base64_decode($this->iv))
167                );
168            }
169        }
170
171        return $value;
172    }
173
174    /**
175     * Decode value
176     *
177     * @param  string $key
178     * @param  string  $value
179     * @throws Exception
180     * @return mixed
181     */
182    public function decodeValue(string $key, string $value): mixed
183    {
184        if (in_array($key, $this->jsonFields)) {
185            if ($value !== null) {
186                $jsonValue = @json_decode($value, true);
187                if (json_last_error() === JSON_ERROR_NONE) {
188                    $value = $jsonValue;
189                }
190            }
191        } else if (in_array($key, $this->phpFields)) {
192            if ($value !== null) {
193                $phpValue = @unserialize($value);
194                if ($phpValue !== false) {
195                    $value = $phpValue;
196                }
197            }
198        } else if (in_array($key, $this->base64Fields)) {
199            if ($value !== null) {
200                $base64Value = @base64_decode($value, true);
201                if ($base64Value !== false) {
202                    $value = $base64Value;
203                }
204            }
205        } else if (in_array($key, $this->encryptedFields)) {
206            if (empty($this->cipherMethod) || empty($this->key) || empty($this->iv)) {
207                throw new Exception('Error: The encryption properties have not been set for this class.');
208            }
209            if ($value !== null) {
210                $base64Value = @base64_decode($value, true);
211                if ($base64Value !== false) {
212                    $value = openssl_decrypt(
213                        base64_decode($value), $this->cipherMethod, $this->key, OPENSSL_RAW_DATA, base64_decode($this->iv)
214                    );
215                }
216            }
217        }
218
219        return $value;
220    }
221
222    /**
223     * Verify value against hash
224     *
225     * @param  string $key
226     * @param  string $value
227     * @return bool
228     */
229    public function verify(string $key, string $value): bool
230    {
231        return password_verify($value, $this->{$key});
232    }
233
234    /**
235     * Scrub the column values and encode them
236     *
237     * @param  array $columns
238     * @throws Exception
239     * @return array
240     */
241    public function encode(array $columns): array
242    {
243        foreach ($columns as $key => $value) {
244            if (($value !== null) && ($this->isEncodedColumn($key))) {
245                $columns[$key] = $this->encodeValue($key, $value);
246            }
247        }
248
249        return $columns;
250    }
251
252    /**
253     * Scrub the column values and decode them
254     *
255     * @param  array $columns
256     * @throws Exception
257     * @return array
258     */
259    public function decode(array $columns): array
260    {
261        foreach ($columns as $key => $value) {
262            if (($this->isEncodedColumn($key)) && ($value !== null)) {
263                $columns[$key] = $this->decodeValue($key, $value);
264            }
265        }
266
267        return $columns;
268    }
269
270    /**
271     * Determine if column is an encoded column
272     *
273     * @param  string $key
274     * @return bool
275     */
276    public function isEncodedColumn(string $key): bool
277    {
278        return (in_array($key, $this->jsonFields) || in_array($key, $this->phpFields) ||
279            in_array($key, $this->base64Fields) || in_array($key, $this->hashFields) || in_array($key, $this->encryptedFields));
280    }
281
282    /**
283     * Magic method to set the property to the value of $this->rowGateway[$name]
284     *
285     * @param  string $name
286     * @param  mixed  $value
287     * @throws Exception
288     * @return void
289     */
290    public function __set(string $name, mixed $value): void
291    {
292        if (($value !== null) && ($this->isEncodedColumn($name))) {
293            $value = $this->encodeValue($name, $value);
294        }
295        parent::__set($name, $value);
296    }
297
298    /**
299     * Magic method to return the value of $this->rowGateway[$name]
300     *
301     * @param  string $name
302     * @throws Exception
303     * @return mixed
304     */
305    public function __get(string $name): mixed
306    {
307        $value = parent::__get($name);
308
309        if (($this->isEncodedColumn($name)) && ($value !== null)) {
310            $value = $this->decodeValue($name, $value);
311        }
312
313        return $value;
314    }
315
316}