Issues (84)

src/EncryptedDBJson.php (1 issue)

1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use SilverStripe\ORM\DataObject;
6
use SilverStripe\Forms\HiddenField;
7
use ParagonIE\CipherSweet\CipherSweet;
8
use ParagonIE\CipherSweet\JsonFieldMap;
9
use ParagonIE\CipherSweet\EncryptedJsonField;
10
use SilverStripe\Forms\FormField;
11
use SilverStripe\Model\ModelData;
12
13
/**
14
 * A simple extension over EncryptedDBText that supports json
15
 * as a datastructure
16
 * The data is stored in a text field
17
 *
18
 * If you want to access array stuff, you need to use
19
 * $model->dbObject('myField')->toArray() or any other method
20
 *
21
 * This field is a great way to store serialized encrypted data
22
 */
23
class EncryptedDBJson extends EncryptedDBText
24
{
25
    /**
26
     * @return ?string
27
     */
28
    public function getJsonMap()
29
    {
30
        if (array_key_exists('map', $this->options)) {
31
            return $this->options['map'];
32
        }
33
        return null;
34
    }
35
36
    /**
37
     * We cannot search on json fields
38
     *
39
     * @param string $title
40
     * @return HiddenField
41
     */
42
    public function scaffoldSearchField(?string $title = null): ?FormField
43
    {
44
        return HiddenField::create($this->getName());
45
    }
46
47
    /**
48
     * Json data is not easily displayed
49
     *
50
     * @param string $title
51
     * @param array<mixed> $params
52
     * @return HiddenField
53
     */
54
    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
55
    {
56
        return HiddenField::create($this->getName());
57
    }
58
59
    /**
60
     * @return mixed
61
     */
62
    public function decode()
63
    {
64
        if (!$this->value) {
65
            return false;
66
        }
67
        return json_decode($this->value);
68
    }
69
70
    /**
71
     * @return array<string,mixed>
72
     */
73
    public function decodeArray()
74
    {
75
        if (!$this->value) {
76
            return [];
77
        }
78
        return json_decode($this->value, true);
79
    }
80
81
    /**
82
     * @return array<string,mixed>
83
     */
84
    public function toArray()
85
    {
86
        return $this->decodeArray();
87
    }
88
89
    /**
90
     * @return string
91
     */
92
    public function pretty()
93
    {
94
        if (!$this->value) {
95
            return '';
96
        }
97
        $decoded = json_decode($this->value);
98
        if (!$decoded) {
99
            return json_last_error_msg();
100
        }
101
        $result = json_encode($decoded, JSON_PRETTY_PRINT);
102
        if (!$result) {
103
            return json_last_error_msg();
104
        }
105
        return $result;
106
    }
107
108
    public function saveInto(ModelData $model): void
109
    {
110
        if ($this->value && is_array($this->value)) {
111
            $this->value = json_encode($this->value);
112
        }
113
        parent::saveInto($model);
114
    }
115
116
    /**
117
     * Add a value
118
     *
119
     * @link https://stackoverflow.com/questions/7851590/array-set-value-using-dot-notation
120
     * @param string|array<string> $key
121
     * @param string $value
122
     * @return $this
123
     */
124
    public function addValue($key, $value)
125
    {
126
        $currentValue = $this->decodeArray();
127
128
        if (!is_array($key)) {
129
            $key = [$key];
130
        }
131
        $arr = &$currentValue;
132
        foreach ($key as $idx) {
133
            if (!isset($arr[$idx])) {
134
                $arr[$idx] = [];
135
            }
136
            $arr = &$arr[$idx];
137
        }
138
        $arr = $value;
139
        return $this->setValue($currentValue);
140
    }
141
142
    /**
143
     * Internally, the value is always a json string
144
     *
145
     * @param mixed $value
146
     * @param DataObject $record
147
     * @param boolean $markChanged
148
     * @return $this
149
     */
150
    public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
151
    {
152
        $this->setEncryptionAad($record);
153
154
        // Not supported, we need decrypted values for methods to work properly
155
        // Return early if we keep encrypted value in memory
156
        // if (!EncryptHelper::getAutomaticDecryption()) {
157
        //     $this->value = $value;
158
        //     return $this;
159
        // }
160
161
        // Decrypt first if needed
162
        if ($this->getJsonMap() && $value && is_string($value)) {
163
            if (EncryptHelper::isJsonEncrypted($value)) {
164
                $aad = $this->encryptionAad;
165
                $value = json_encode($this->getEncryptedJsonField()->decryptJson($value, $aad));
166
            }
167
        }
168
        // Internally, we use a string
169
        if (is_array($value)) {
170
            $value = json_encode($value);
171
        }
172
173
        return parent::setValue($value, $record, $markChanged);
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::setValue(... $record, $markChanged) returns the type LeKoala\Encrypt\EncryptedDBText which includes types incompatible with the type-hinted return LeKoala\Encrypt\EncryptedDBJson.
Loading history...
174
    }
175
176
    public function prepValueForDB(mixed $value): array|string|null
177
    {
178
        // We need an array to encrypt
179
        if ($this->getJsonMap() && $value && is_string($value)) {
180
            $value = $this->toArray();
181
        }
182
        if (is_array($value)) {
183
            if ($this->getJsonMap()) {
184
                $aad = $this->encryptionAad;
185
                $value = $this->getEncryptedJsonField()->encryptJson($value, $aad);
186
                return $value; // return early
187
            } else {
188
                $value = json_encode($value);
189
                if ($value === false) {
190
                    $value = '';
191
                }
192
            }
193
        }
194
        return parent::prepValueForDB($value);
195
    }
196
197
    /**
198
     * We return false because we can accept array and convert it to string
199
     */
200
    public function scalarValueOnly(): bool
201
    {
202
        return false;
203
    }
204
205
    /**
206
     * @param CipherSweet $engine
207
     * @param JsonFieldMap $map
208
     * @return EncryptedJsonField
209
     */
210
    public function getEncryptedJsonField($engine = null, $map = null)
211
    {
212
        if ($engine === null) {
213
            $engine = EncryptHelper::getCipherSweet();
214
        }
215
        if ($map === null) {
216
            $mapString = $this->getJsonMap();
217
            $map = JsonFieldMap::fromString($mapString);
218
        }
219
        $encryptedField = EncryptedJsonField::create($engine, $map, $this->tableName, $this->name);
220
        $encryptedField->setStrictMode(false);
221
        return $encryptedField;
222
    }
223
}
224