Passed
Push — master ( 50c0f4...5cee22 )
by Thomas
02:13
created

EncryptedDBField   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 297
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 46
eloc 99
c 1
b 0
f 0
dl 0
loc 297
rs 8.72

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getEncryptedField() 0 8 2
A setValueField() 0 3 1
A getSearchValue() 0 14 5
A addToQuery() 0 5 1
A fetchRecord() 0 7 2
A writeToManipulation() 0 16 2
A getSearchParams() 0 8 2
A getValueField() 0 3 1
A getBlindIndexField() 0 3 1
A setBlindIndexField() 0 3 1
A scaffoldFormField() 0 4 1
A __toString() 0 3 1
A Nice() 0 3 1
A scalarValueOnly() 0 3 1
F setValue() 0 64 19
A saveInto() 0 24 3
A isChanged() 0 3 1
A exists() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like EncryptedDBField often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EncryptedDBField, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use Exception;
6
use SilverStripe\ORM\DataObject;
7
use SilverStripe\Forms\TextField;
8
use ParagonIE\CipherSweet\BlindIndex;
9
use SilverStripe\ORM\Queries\SQLSelect;
10
use ParagonIE\CipherSweet\EncryptedField;
11
use SilverStripe\ORM\FieldType\DBComposite;
12
use ParagonIE\CipherSweet\Exception\InvalidCiphertextException;
13
14
/**
15
 * Value will be set on parent record through built in getField
16
 * mechanisms for composite fields
17
 */
18
class EncryptedDBField extends DBComposite
19
{
20
    /**
21
     * @param array
22
     */
23
    private static $composite_db = array(
0 ignored issues
show
introduced by
The private property $composite_db is not used, and could be removed.
Loading history...
24
        "Value" => "Varchar(191)",
25
        "BlindIndex" => 'Varchar(32)',
26
    );
27
28
    /**
29
     * @return string
30
     */
31
    public function getValueField()
32
    {
33
        return $this->getField('Value');
34
    }
35
36
    /**
37
     * @return $this
38
     */
39
    public function setValueField($value, $markChanged = true)
40
    {
41
        return $this->setField('Value', $value, $markChanged);
42
    }
43
44
    /**
45
     * @return string
46
     */
47
    public function getBlindIndexField()
48
    {
49
        return $this->getField('BlindIndex');
50
    }
51
52
    /**
53
     * @return $this
54
     */
55
    public function setBlindIndexField($value, $markChanged = true)
56
    {
57
        return $this->setField('BlindIndex', $value, $markChanged);
58
    }
59
60
    /**
61
     * @param CipherSweet $engine
0 ignored issues
show
Bug introduced by
The type LeKoala\Encrypt\CipherSweet was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
62
     * @return EncryptedField
63
     */
64
    public function getEncryptedField($engine = null)
65
    {
66
        if ($engine === null) {
67
            $engine = EncryptHelper::getCipherSweet();
68
        }
69
        $encryptedField = (new EncryptedField($engine, $this->tableName, $this->name))
70
            ->addBlindIndex(new BlindIndex($this->name . "BlindIndex", [], 32));
71
        return $encryptedField;
72
    }
73
74
    /**
75
     * This is not called anymore, we rely on saveInto for now
76
     * @link https://github.com/silverstripe/silverstripe-framework/issues/8800
77
     * @param array $manipulation
78
     * @return void
79
     */
80
    public function writeToManipulation(&$manipulation)
81
    {
82
        $encryptedField = $this->getEncryptedField();
83
84
        if ($this->value) {
85
            $dataForStorage = $encryptedField->prepareForStorage($this->value);
86
            $encryptedValue = $this->prepValueForDB($dataForStorage[0]);
87
            $blindIndexes = $dataForStorage[1];
88
        } else {
89
            $encryptedValue = null;
90
            $blindIndexes = [];
91
        }
92
93
94
        $manipulation['fields'][$this->name . 'Value'] = $encryptedValue;
95
        $manipulation['fields'][$this->name . 'BlindIndex'] = $blindIndexes[$this->name . "BlindIndex"] ?? null;
96
    }
97
98
    /**
99
     * @param SQLSelect $query
100
     */
101
    public function addToQuery(&$query)
102
    {
103
        parent::addToQuery($query);
104
        $query->selectField(sprintf('"%sValue"', $this->name));
105
        $query->selectField(sprintf('"%sBlindIndex"', $this->name));
106
    }
107
108
    /**
109
     * Return the blind index value to search in the database
110
     *
111
     * @param string $val The unencrypted value
112
     * @param string $index The blind index. Defaults to full index
113
     * @return string
114
     */
115
    public function getSearchValue($val, $index = 'BlindIndex')
116
    {
117
        if (!$this->tableName && $this->record) {
118
            $this->tableName = DataObject::getSchema()->tableName(get_class($this->record));
0 ignored issues
show
Bug introduced by
It seems like $this->record can also be of type array; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

118
            $this->tableName = DataObject::getSchema()->tableName(get_class(/** @scrutinizer ignore-type */ $this->record));
Loading history...
119
        }
120
        if (!$this->tableName) {
121
            throw new Exception("Table name not set for search value. Please set a dataobject.");
122
        }
123
        if (!$this->name) {
124
            throw new Exception("Name not set for search value");
125
        }
126
        $field = $this->getEncryptedField();
127
        $index = $field->getBlindIndex($val, $this->name . $index);
128
        return $index;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $index also could return the type array<string,string> which is incompatible with the documented return type string.
Loading history...
129
    }
130
131
    /**
132
     * Return a ready to use array params for a where clause
133
     *
134
     * @param string $val The unencrypted value
135
     * @param string $index The blind index. Defaults to full index
136
     * @return array
137
     */
138
    public function getSearchParams($val, $index = null)
139
    {
140
        if (!$index) {
141
            $index = 'BlindIndex';
142
        }
143
        $searchValue = $this->getSearchValue($val, $index);
144
        $blindIndexField = $this->name . $index;
145
        return array($blindIndexField . ' = ?' => $searchValue);
146
    }
147
148
    /**
149
     * @param string $val The unencrypted value
150
     * @param string $index The blind index. Defaults to full index
151
     * @return DataObject
152
     */
153
    public function fetchRecord($val, $index = null)
154
    {
155
        if (!$this->record) {
156
            throw new Exception("No record set for this field");
157
        }
158
        $class = get_class($this->record);
0 ignored issues
show
Bug introduced by
It seems like $this->record can also be of type array; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

158
        $class = get_class(/** @scrutinizer ignore-type */ $this->record);
Loading history...
159
        return $class::get()->where($this->getSearchParams($val, $index))->first();
160
    }
161
162
    public function setValue($value, $record = null, $markChanged = true)
163
    {
164
        if ($markChanged) {
165
            $this->isChanged = true;
166
        }
167
168
        // When given a dataobject, bind this field to that
169
        if ($record instanceof DataObject) {
170
            $this->bindTo($record);
171
            $record = null;
172
        }
173
174
        // Convert an object to an array
175
        if ($record && $record instanceof DataObject) {
176
            $record = $record->getQueriedDatabaseFields();
177
        }
178
179
        // Set the table name if it was not set earlier
180
        if (!$this->tableName && $record) {
181
            $this->tableName = DataObject::getSchema()->tableName(get_class($record));
182
            if (!$this->tableName) {
183
                throw new Exception("Could not get table name from record from " . gettype($record));
184
            }
185
        }
186
187
        $encryptedField = $this->getEncryptedField();
188
        // Value will store the decrypted value
189
        if ($value instanceof EncryptedDBField) {
190
            $this->value = $value->getValue();
191
        } elseif ($record && isset($record[$this->name . 'Value'])) {
192
            // In that case, the value come from the database and might be encrypted
193
            if ($record[$this->name . 'Value']) {
194
                $encryptedValue = $record[$this->name . 'Value'];
195
                try {
196
                    if (EncryptHelper::isEncrypted($encryptedValue)) {
197
                        $this->value = $encryptedField->decryptValue($encryptedValue);
198
                    } else {
199
                        // Value wasn't crypted in the db
200
                        $this->value = $encryptedValue;
201
                    }
202
                } catch (InvalidCiphertextException $ex) {
203
                    // rotate backend ?
204
                    // $this->value = $newEncryptedField->decryptValue($encryptedValue);
205
                } catch (Exception $ex) {
206
                    // We cannot decrypt
207
                    $this->value = $this->nullValue();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->value is correct as $this->nullValue() targeting SilverStripe\ORM\FieldType\DBField::nullValue() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
208
                }
209
            } else {
210
                $this->value = $this->nullValue();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->value is correct as $this->nullValue() targeting SilverStripe\ORM\FieldType\DBField::nullValue() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
211
            }
212
        } elseif (is_array($value)) {
213
            if (array_key_exists('Value', $value)) {
214
                $this->value = $value;
215
            }
216
        } elseif (is_string($value) || !$value) {
217
            $this->value = $value;
218
        } else {
219
            throw new Exception("Unexcepted value of type " . gettype($value));
220
        }
221
222
        // Forward changes since writeToManipulation are not called
223
        // $this->setValueField($value, $markChanged);
224
225
        return $this;
226
    }
227
228
    /**
229
     * @return string
230
     */
231
    public function Nice($options = array())
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

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

231
    public function Nice(/** @scrutinizer ignore-unused */ $options = array())

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
232
    {
233
        return $this->getValue();
234
    }
235
236
    /**
237
     * @return boolean
238
     */
239
    public function exists()
240
    {
241
        return strlen($this->value) > 0;
242
    }
243
244
    /**
245
     * This is called by getChangedFields() to check if a field is changed
246
     *
247
     * @return boolean
248
     */
249
    public function isChanged()
250
    {
251
        return $this->isChanged;
252
    }
253
254
    /**
255
     * If we pass a DBField to the setField method, it will
256
     * trigger this method
257
     *
258
     * We save encrypted value on sub fields. They will be collected
259
     * by write() operation by prepareManipulationTable
260
     *
261
     * Currently prepareManipulationTable ignores composite fields
262
     * so we rely on the sub field mechanisms
263
     *
264
     * @param DataObject $dataObject
265
     * @return void
266
     */
267
    public function saveInto($dataObject)
268
    {
269
        $encryptedField = $this->getEncryptedField();
270
271
        if ($this->value) {
272
            $dataForStorage = $encryptedField->prepareForStorage($this->value);
273
            $encryptedValue = $this->prepValueForDB($dataForStorage[0]);
274
            $blindIndexes = $dataForStorage[1];
275
        } else {
276
            $encryptedValue = null;
277
            $blindIndexes = [];
278
        }
279
280
        // This cause infinite loops
281
        // $dataObject->setField($this->getName(), $this->value);
282
283
        // Encrypt value
284
        $key = $this->getName() . 'Value';
285
        $dataObject->setField($key, $encryptedValue);
286
287
        // Build blind index
288
        $key = $this->getName() . 'BlindIndex';
289
        if (isset($blindIndexes[$key])) {
290
            $dataObject->setField($key, $blindIndexes[$key]);
291
        }
292
    }
293
294
    /**
295
     * @param string $title Optional. Localized title of the generated instance
296
     * @return FormField
0 ignored issues
show
Bug introduced by
The type LeKoala\Encrypt\FormField was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
297
     */
298
    public function scaffoldFormField($title = null, $params = null)
299
    {
300
        $field = TextField::create($this->name);
301
        return $field;
302
    }
303
304
    /**
305
     * Returns the string value
306
     */
307
    public function __toString()
308
    {
309
        return (string) $this->getValue();
310
    }
311
312
    public function scalarValueOnly()
313
    {
314
        return false;
315
    }
316
}
317