Passed
Push — master ( 06c7d6...5383e7 )
by Thomas
02:04
created

EncryptedDBField   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 58
eloc 135
c 1
b 0
f 0
dl 0
loc 393
rs 4.5599

22 Methods

Rating   Name   Duplication   Size   Complexity  
A fetchDataList() 0 15 3
A getEncryptedField() 0 10 2
A setValueField() 0 3 1
A scaffoldFormField() 0 4 1
A getSearchValue() 0 17 6
A addToQuery() 0 5 1
A __toString() 0 3 1
A fetchRecord() 0 22 5
A writeToManipulation() 0 15 2
A getSearchParams() 0 8 2
A Nice() 0 3 1
A getOutputSize() 0 8 2
A getIndexSize() 0 9 3
A getValueField() 0 3 1
A scalarValueOnly() 0 3 1
C setValue() 0 54 17
A saveInto() 0 24 3
A isChanged() 0 3 1
A exists() 0 3 1
A getBlindIndexField() 0 3 1
A setBlindIndexField() 0 3 1
A getDomainSize() 0 8 2

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
13
/**
14
 * Value will be set on parent record through built in getField
15
 * mechanisms for composite fields
16
 */
17
class EncryptedDBField extends DBComposite
18
{
19
    use HasBaseEncryption;
20
21
    const LARGE_INDEX_SIZE = 32;
22
    const SMALL_INDEX_SIZE = 16;
23
    const VALUE_SUFFIX = "Value";
24
    const INDEX_SUFFIX = "BlindIndex";
25
26
    /**
27
     * @config
28
     * @var int
29
     */
30
    private static $output_size = 15;
0 ignored issues
show
introduced by
The private property $output_size is not used, and could be removed.
Loading history...
31
32
    /**
33
     * @config
34
     * @var int
35
     */
36
    private static $domain_size = 127;
0 ignored issues
show
introduced by
The private property $domain_size is not used, and could be removed.
Loading history...
37
38
    /**
39
     * @param array
40
     */
41
    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...
42
        "Value" => "Varchar(191)",
43
        "BlindIndex" => 'Varchar(32)',
44
    );
45
46
    /**
47
     * Output size is the number of bits (not bytes) of a blind index.
48
     * Eg: 4 for a 4 digits year
49
     * Note: the larger the output size, the smaller the index should be
50
     * @return int
51
     */
52
    public function getOutputSize()
53
    {
54
        if (array_key_exists('output_size', $this->options)) {
55
            $outputSize = $this->options['output_size'];
56
        } else {
57
            $outputSize = static::config()->get('output_size');
58
        }
59
        return $outputSize;
60
    }
61
62
    /**
63
     * Input domain is the set of all possible distinct inputs.
64
     * Eg : 4 digits have 10,000 possible values (10^4). The log (base 2) of 10,000 is 13.2877; you would want to always round up (so 14).
65
     * @return int
66
     */
67
    public function getDomainSize()
68
    {
69
        if (array_key_exists('domain_size', $this->options)) {
70
            $domainSize = $this->options['domain_size'];
71
        } else {
72
            $domainSize = static::config()->get('domain_size');
73
        }
74
        return $domainSize;
75
    }
76
77
    /**
78
     * @param int $default
79
     * @return int
80
     */
81
    public function getIndexSize($default = null)
82
    {
83
        if (array_key_exists('index_size', $this->options)) {
84
            return $this->options['index_size'];
85
        }
86
        if ($default) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $default of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
87
            return $default;
88
        }
89
        return self::LARGE_INDEX_SIZE;
90
    }
91
92
    /**
93
     * @return string
94
     */
95
    public function getValueField()
96
    {
97
        return $this->getField(self::VALUE_SUFFIX);
98
    }
99
100
    /**
101
     * @return $this
102
     */
103
    public function setValueField($value, $markChanged = true)
104
    {
105
        return $this->setField(self::VALUE_SUFFIX, $value, $markChanged);
106
    }
107
108
    /**
109
     * @return string
110
     */
111
    public function getBlindIndexField()
112
    {
113
        return $this->getField(self::INDEX_SUFFIX);
114
    }
115
116
    /**
117
     * @return $this
118
     */
119
    public function setBlindIndexField($value, $markChanged = true)
120
    {
121
        return $this->setField(self::INDEX_SUFFIX, $value, $markChanged);
122
    }
123
124
    /**
125
     * @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...
126
     * @return EncryptedField
127
     */
128
    public function getEncryptedField($engine = null)
129
    {
130
        if ($engine === null) {
131
            $engine = EncryptHelper::getCipherSweet();
132
        }
133
        $indexSize = $this->getIndexSize(self::LARGE_INDEX_SIZE);
134
        // fieldName needs to match exact db name for row rotator to work properly
135
        $encryptedField = (new EncryptedField($engine, $this->tableName, $this->name . self::VALUE_SUFFIX))
136
            ->addBlindIndex(new BlindIndex($this->name . self::INDEX_SUFFIX, [], $indexSize));
137
        return $encryptedField;
138
    }
139
140
    /**
141
     * This is not called anymore, we rely on saveInto for now
142
     * @link https://github.com/silverstripe/silverstripe-framework/issues/8800
143
     * @param array $manipulation
144
     * @return void
145
     */
146
    public function writeToManipulation(&$manipulation)
147
    {
148
        $encryptedField = $this->getEncryptedField();
149
150
        if ($this->value) {
151
            $dataForStorage = $encryptedField->prepareForStorage($this->value);
152
            $encryptedValue = $this->prepValueForDB($dataForStorage[0]);
153
            $blindIndexes = $dataForStorage[1];
154
        } else {
155
            $encryptedValue = null;
156
            $blindIndexes = [];
157
        }
158
159
        $manipulation['fields'][$this->name . self::VALUE_SUFFIX] = $encryptedValue;
160
        $manipulation['fields'][$this->name . self::INDEX_SUFFIX] = $blindIndexes[$this->name . self::INDEX_SUFFIX] ?? null;
161
    }
162
163
    /**
164
     * @param SQLSelect $query
165
     */
166
    public function addToQuery(&$query)
167
    {
168
        parent::addToQuery($query);
169
        $query->selectField(sprintf('"%s' . self::VALUE_SUFFIX . '"', $this->name));
170
        $query->selectField(sprintf('"%s' . self::INDEX_SUFFIX . '"', $this->name));
171
    }
172
173
    /**
174
     * Return the blind index value to search in the database
175
     *
176
     * @param string $val The unencrypted value
177
     * @param string $indexSuffix The blind index. Defaults to full index
178
     * @return string
179
     */
180
    public function getSearchValue($val, $indexSuffix = null)
181
    {
182
        if (!$this->tableName && $this->record) {
183
            $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

183
            $this->tableName = DataObject::getSchema()->tableName(get_class(/** @scrutinizer ignore-type */ $this->record));
Loading history...
184
        }
185
        if (!$this->tableName) {
186
            throw new Exception("Table name not set for search value. Please set a dataobject.");
187
        }
188
        if (!$this->name) {
189
            throw new Exception("Name not set for search value");
190
        }
191
        if ($indexSuffix === null) {
192
            $indexSuffix = self::INDEX_SUFFIX;
193
        }
194
        $field = $this->getEncryptedField();
195
        $index = $field->getBlindIndex($val, $this->name . $indexSuffix);
196
        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...
197
    }
198
199
    /**
200
     * Return a ready to use array params for a where clause
201
     *
202
     * @param string $val The unencrypted value
203
     * @param string $indexSuffix The blind index. Defaults to full index
204
     * @return array
205
     */
206
    public function getSearchParams($val, $indexSuffix = null)
207
    {
208
        if (!$indexSuffix) {
209
            $indexSuffix = self::INDEX_SUFFIX;
210
        }
211
        $searchValue = $this->getSearchValue($val, $indexSuffix);
212
        $blindIndexField = $this->name . $indexSuffix;
213
        return array($blindIndexField . ' = ?' => $searchValue);
214
    }
215
216
    /**
217
     * @param string $val The unencrypted value
218
     * @param string $indexSuffix The blind index. Defaults to full index
219
     * @return DataList
0 ignored issues
show
Bug introduced by
The type LeKoala\Encrypt\DataList 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...
220
     */
221
    public function fetchDataList($val, $indexSuffix = null)
222
    {
223
        if (!$this->record) {
224
            throw new Exception("No record set for this field");
225
        }
226
        if (!$indexSuffix) {
227
            $indexSuffix = self::INDEX_SUFFIX;
228
        }
229
        $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

229
        $class = get_class(/** @scrutinizer ignore-type */ $this->record);
Loading history...
230
231
        // A blind index can return false positives
232
        $params = $this->getSearchParams($val, $indexSuffix);
233
        $blindIndexes = $this->getEncryptedField()->getBlindIndexObjects();
0 ignored issues
show
Unused Code introduced by
The assignment to $blindIndexes is dead and can be removed.
Loading history...
234
        $list = $class::get()->where($params);
235
        return $list;
236
    }
237
238
    /**
239
     * @param string $val The unencrypted value
240
     * @param string $indexSuffix The blind index. Defaults to full index
241
     * @return DataObject
242
     */
243
    public function fetchRecord($val, $indexSuffix = null)
244
    {
245
        if (!$indexSuffix) {
246
            $indexSuffix = self::INDEX_SUFFIX;
247
        }
248
        $list = $this->fetchDataList($val, $indexSuffix);
249
        $blindIndexes = $this->getEncryptedField()->getBlindIndexObjects();
250
        $blindIndex = $blindIndexes[$this->name . $indexSuffix];
251
        $name = $this->name;
252
        /** @var DataObject $record  */
253
        foreach ($list as $record) {
254
            $obj = $record->dbObject($name);
255
            // Value might be transformed
256
            if ($blindIndex->getTransformed($obj->getValue()) == $val) {
257
                return $record;
258
            }
259
        }
260
        // throw exception if there where matches but none with the right value
261
        if ($list->count()) {
262
            throw new Exception($list->count() . " records were found but none matched the right value");
263
        }
264
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type SilverStripe\ORM\DataObject.
Loading history...
265
    }
266
267
    public function setValue($value, $record = null, $markChanged = true)
268
    {
269
        // Return early if we keep encrypted value in memory
270
        if (!EncryptHelper::getAutomaticDecryption()) {
271
            parent::setValue($value, $record, $markChanged);
272
            return $this;
273
        }
274
275
        if ($markChanged) {
276
            $this->isChanged = true;
277
        }
278
279
        // When given a dataobject, bind this field to that
280
        if ($record instanceof DataObject) {
281
            $this->bindTo($record);
282
        }
283
284
        // Convert an object to an array
285
        if ($record && $record instanceof DataObject) {
286
            $record = $record->getQueriedDatabaseFields();
287
            if (!$record) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
288
                throw new Exception("Could not convert record to array");
289
            }
290
        }
291
292
        // Set the table name if it was not set earlier
293
        if (!$this->tableName && $record) {
294
            $this->tableName = DataObject::getSchema()->tableName(get_class($record));
295
            if (!$this->tableName) {
296
                throw new Exception("Could not get table name from record from " . gettype($record));
297
            }
298
        }
299
300
        // Value will store the decrypted value
301
        if ($value instanceof EncryptedDBField) {
302
            $this->value = $value->getValue();
303
        } elseif ($record && isset($record[$this->name . self::VALUE_SUFFIX])) {
304
            // In that case, the value come from the database and might be encrypted
305
            $encryptedValue = $record[$this->name . self::VALUE_SUFFIX];
306
            $this->value = $this->decryptValue($encryptedValue);
307
        } elseif (is_array($value)) {
308
            if (array_key_exists(self::VALUE_SUFFIX, $value)) {
309
                $this->value = $value;
310
            }
311
        } elseif (is_string($value) || !$value) {
312
            $this->value = $value;
313
        } else {
314
            throw new Exception("Unexcepted value of type " . gettype($value));
315
        }
316
317
        // Forward changes since writeToManipulation are not called
318
        // $this->setValueField($value, $markChanged);
319
320
        return $this;
321
    }
322
323
    /**
324
     * @return string
325
     */
326
    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

326
    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...
327
    {
328
        return $this->getValue();
329
    }
330
331
    /**
332
     * @return boolean
333
     */
334
    public function exists()
335
    {
336
        return strlen($this->value) > 0;
337
    }
338
339
    /**
340
     * This is called by getChangedFields() to check if a field is changed
341
     *
342
     * @return boolean
343
     */
344
    public function isChanged()
345
    {
346
        return $this->isChanged;
347
    }
348
349
    /**
350
     * If we pass a DBField to the setField method, it will
351
     * trigger this method
352
     *
353
     * We save encrypted value on sub fields. They will be collected
354
     * by write() operation by prepareManipulationTable
355
     *
356
     * Currently prepareManipulationTable ignores composite fields
357
     * so we rely on the sub field mechanisms
358
     *
359
     * @param DataObject $dataObject
360
     * @return void
361
     */
362
    public function saveInto($dataObject)
363
    {
364
        $encryptedField = $this->getEncryptedField();
365
366
        if ($this->value) {
367
            $dataForStorage = $encryptedField->prepareForStorage($this->value);
368
            $encryptedValue = $this->prepValueForDB($dataForStorage[0]);
369
            $blindIndexes = $dataForStorage[1];
370
        } else {
371
            $encryptedValue = null;
372
            $blindIndexes = [];
373
        }
374
375
        // This cause infinite loops
376
        // $dataObject->setField($this->getName(), $this->value);
377
378
        // Encrypt value
379
        $key = $this->getName() . 'Value';
380
        $dataObject->setField($key, $encryptedValue);
381
382
        // Build blind index
383
        $key = $this->getName() . self::INDEX_SUFFIX;
384
        if (isset($blindIndexes[$key])) {
385
            $dataObject->setField($key, $blindIndexes[$key]);
386
        }
387
    }
388
389
    /**
390
     * @param string $title Optional. Localized title of the generated instance
391
     * @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...
392
     */
393
    public function scaffoldFormField($title = null, $params = null)
394
    {
395
        $field = TextField::create($this->name);
396
        return $field;
397
    }
398
399
    /**
400
     * Returns the string value
401
     */
402
    public function __toString()
403
    {
404
        return (string) $this->getValue();
405
    }
406
407
    public function scalarValueOnly()
408
    {
409
        return false;
410
    }
411
}
412