Completed
Push — master ( c9b7ea...c1fd74 )
by Mihail
02:38
created

ModelValidator   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
wmc 54
c 0
b 0
f 0
lcom 1
cbo 10
dl 0
loc 332
rs 7.0642

12 Methods

Rating   Name   Duplication   Size   Complexity  
A initialize() 0 20 4
D runValidate() 0 42 10
C validateRecursive() 0 78 17
B getFieldValue() 0 32 6
A getBadAttribute() 0 4 1
A setSubmitMethod() 0 4 1
A getSubmitMethod() 0 4 1
A send() 0 8 2
A getFormName() 0 9 2
A getInput() 0 4 1
A getFile() 0 4 1
C getRequest() 0 38 8

How to fix   Complexity   

Complex Class

Complex classes like ModelValidator 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 ModelValidator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Ffcms\Core\Traits;
4
5
6
use Dflydev\DotAccessData\Data as DotData;
7
use Ffcms\Core\App;
8
use Ffcms\Core\Exception\SyntaxException;
9
use Ffcms\Core\Helper\ModelFilters;
10
use Ffcms\Core\Helper\Type\Any;
11
use Ffcms\Core\Helper\Type\Obj;
12
use Ffcms\Core\Helper\Type\Str;
13
14
/**
15
 * Class ModelValidator. Extended realisation of model field validation
16
 * @package Ffcms\Core\Traits
17
 */
18
trait ModelValidator
19
{
20
    protected $_badAttr;
21
    protected $_sendMethod = 'POST';
22
23
    protected $_formName;
24
25
    public $_tokenRequired = false;
26
    protected $_tokenOk = true;
27
28
    /**
29
     * Initialize validator. Set csrf protection token from request data if available.
30
     * @param bool $csrf
31
     */
32
    public function initialize($csrf = false)
33
    {
34
        $this->_tokenRequired = $csrf;
35
        if ($csrf === true) {
36
            // get current token value from session
37
            $currentToken = App::$Session->get('_csrf_token', false);
38
            // set new token value to session
39
            $newToken = Str::randomLatinNumeric(mt_rand(32, 64));
40
            App::$Session->set('_csrf_token', $newToken);
41
            // if request is submited for this model - try to validate input data
42
            if ($this->send()) {
43
                // token is wrong - update bool state
44
                if ($currentToken !== $this->getRequest('_csrf_token', 'all')) {
45
                    $this->_tokenOk = false;
46
                }
47
            }
48
            // set token data to display
49
            $this->_csrf_token = $newToken;
0 ignored issues
show
Bug introduced by
The property _csrf_token does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
50
        }
51
    }
52
53
    /**
54
     * Start validation for defined fields in rules() model method.
55
     * @param array|null $rules
56
     * @return bool
57
     * @throws SyntaxException
58
     */
59
    public function runValidate(array $rules = null)
60
    {
61
        // skip validation on empty rules
62
        if ($rules === null)
63
            return true;
64
65
        $success = true;
66
        // list each rule as single one
67
        foreach ($rules as $rule) {
68
            // 0 = field (property) name, 1 = filter name, 2 = filter value
69
            if ($rule[0] === null || $rule[1] === null)
70
                continue;
71
72
            $propertyName = $rule[0];
73
            $validationRule = $rule[1];
74
            $validationValue = null;
75
            if (isset($rule[2]))
76
                $validationValue = $rule[2];
77
78
            // check if target field defined as array and make recursive validation
79
            if (Any::isArray($propertyName)) {
80
                $cumulateValidation = true;
81
                foreach ($propertyName as $attrNme) {
82
                    // end false condition
83
                    if (!$this->validateRecursive($attrNme, $validationRule, $validationValue)) {
84
                        $cumulateValidation = false;
85
                    }
86
                }
87
                // assign total
88
                $validate = $cumulateValidation;
89
            } else {
90
                $validate = $this->validateRecursive($propertyName, $validationRule, $validationValue);
91
            }
92
93
            // do not change condition on "true" check's (end-false-condition)
94
            if ($validate === false) {
95
                $success = false;
96
            }
97
        }
98
99
        return $success;
100
    }
101
102
    /**
103
     * Try to recursive validate field by defined rules and set result to model properties if validation is successful passed
104
     * @param string|array $propertyName
105
     * @param string $filterName
106
     * @param mixed $filterArgs
107
     * @return bool
108
     * @throws SyntaxException
109
     */
110
    public function validateRecursive($propertyName, $filterName, $filterArgs = null)
111
    {
112
        // check if we got it from form defined request method
113
        if (App::$Request->getMethod() !== $this->_sendMethod) {
114
            return false;
115
        }
116
117
        // get field value from user input data
118
        $fieldValue = $this->getFieldValue($propertyName);
0 ignored issues
show
Bug introduced by
It seems like $propertyName defined by parameter $propertyName on line 110 can also be of type array; however, Ffcms\Core\Traits\ModelValidator::getFieldValue() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
119
120
        $check = false;
0 ignored issues
show
Unused Code introduced by
$check is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
121
        // maybe no filter required?
122
        if ($filterName === 'used') {
123
            $check = true;
124
        } elseif (Str::contains('::', $filterName)) { // sounds like a callback class::method::method
125
            // string to array via delimiter ::
126
            $callbackArray = explode('::', $filterName);
127
            // first item is a class name
128
            $class = array_shift($callbackArray);
129
            // last item its a function
130
            $method = array_pop($callbackArray);
131
            // left any items? maybe post-static callbacks?
132
            if (count($callbackArray) > 0) {
133
                foreach ($callbackArray as $obj) {
134
                    if (Str::startsWith('$', $obj) && property_exists($class, ltrim($obj, '$'))) { // sounds like a variable
135
                        $obj = ltrim($obj, '$'); // trim variable symbol '$'
136
                        $class = $class::${$obj}; // make magic :)
137
                    } elseif (method_exists($class, $obj)) { // maybe its a function?
138
                        $class = $class::$obj; // call function
139
                    } else {
140
                        throw new SyntaxException('Filter callback execution failed: ' . $filterName);
141
                    }
142
143
                }
144
            }
145
146
            // check is endpoint method exist
147
            if (method_exists($class, $method)) {
148
                $check = @$class::$method($fieldValue, $filterArgs);
149
            } else {
150
                throw new SyntaxException('Filter callback execution failed: ' . $filterName);
151
            }
152
        } elseif (method_exists('Ffcms\Core\Helper\ModelFilters', $filterName)) { // only full namespace\class path based :(
153
            if ($filterArgs != null) {
154
                $check = ModelFilters::$filterName($fieldValue, $filterArgs);
155
            } else {
156
                $check = ModelFilters::$filterName($fieldValue);
157
            }
158
        } else {
159
            throw new SyntaxException('Filter "' . $filterName . '" is not exist');
160
        }
161
        if ($check !== true) { // if one from all validation tests is fail - mark as incorrect attribute
162
            $this->_badAttr[] = $propertyName;
163
        } else {
164
            $field_set_name = $propertyName;
165
            // prevent array-type setting
166
            if (Str::contains('.', $field_set_name)) {
0 ignored issues
show
Bug introduced by
It seems like $field_set_name defined by $propertyName on line 164 can also be of type array; however, Ffcms\Core\Helper\Type\Str::contains() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
167
                $field_set_name = strstr($field_set_name, '.', true);
168
            }
169
            if (property_exists($this, $field_set_name)) {
170
                if ($propertyName !== $field_set_name) { // array-based property
171
                    $dot_path = trim(strstr($propertyName, '.'), '.');
172
                    // prevent throws any exceptions for null and false objects
173
                    if (!Any::isArray($this->{$field_set_name}))
174
                        $this->{$field_set_name} = [];
175
176
                    // use dot-data provider to compile output array
177
                    $dotData = new DotData($this->{$field_set_name});
178
                    $dotData->set($dot_path, $fieldValue); // todo: check me!!! Here can be bug of fail parsing dots and passing path-value
179
                    // export data from dot-data lib to model property
180
                    $this->{$field_set_name} = $dotData->export();
181
                } else { // just single property
182
                    $this->{$propertyName} = $fieldValue; // refresh model property's from post data
183
                }
184
            }
185
        }
186
        return $check;
187
    }
188
189
    /**
190
     * Get field value from input POST/GET/AJAX data with defined security level (html - safe html, !secure = fully unescaped)
191
     * @param string $propertyName
192
     * @return array|null|string
193
     * @throws \InvalidArgumentException
194
     */
195
    private function getFieldValue($propertyName)
196
    {
197
        // get type of input data (where we must look it up)
198
        $inputType = Str::lowerCase($this->_sendMethod);
199
        $filterType = 'text';
200
        // get declared field sources and types
201
        $sources = $this->sources();
0 ignored issues
show
Bug introduced by
It seems like sources() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
202
        $types = $this->types();
0 ignored issues
show
Bug introduced by
It seems like types() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
203
        // validate sources for current field
204
        if (array_key_exists($propertyName, $sources))
205
            $inputType = Str::lowerCase($sources[$propertyName]);
206
207
208
        // check if field is array-nested element by dots and use first element as general
209
        $filterField = $propertyName;
210
        if (array_key_exists($filterField, $types))
211
            $filterType = Str::lowerCase($types[$filterField]);
212
213
        // get clear field value
214
        $propertyValue = $this->getRequest($propertyName, $inputType);
215
216
        // apply security filter for input data
217
        if ($inputType !== 'file') {
218
            if ($filterType === 'html') {
219
                $propertyValue = App::$Security->secureHtml($propertyValue);
220
            } elseif ($filterType !== '!secure') {
221
                $propertyValue = App::$Security->strip_tags($propertyValue);
222
            }
223
        }
224
225
        return $propertyValue;
226
    }
227
228
    /**
229
     * Get fail validation attributes as array if exist
230
     * @return null|array
231
     */
232
    public function getBadAttribute()
233
    {
234
        return $this->_badAttr;
235
    }
236
237
    /**
238
     * Set model send method type. Allowed: post, get
239
     * @param string $acceptMethod
240
     */
241
    final public function setSubmitMethod($acceptMethod)
242
    {
243
        $this->_sendMethod = Str::upperCase($acceptMethod);
244
    }
245
246
    /**
247
     * Get model submit method. Allowed: post, get
248
     * @return string
249
     */
250
    final public function getSubmitMethod()
251
    {
252
        return $this->_sendMethod;
253
    }
254
255
    /**
256
     * Check if model get POST-based request as submit of SEND data
257
     * @return bool
258
     * @throws \InvalidArgumentException
259
     */
260
    final public function send()
261
    {
262
        if (!Str::equalIgnoreCase($this->_sendMethod, App::$Request->getMethod())) {
263
            return false;
264
        }
265
266
        return $this->getRequest('submit', $this->_sendMethod) !== null;
267
    }
268
269
    /**
270
     * Form default name (used in field building)
271
     * @return string
272
     */
273
    public function getFormName()
274
    {
275
        if ($this->_formName === null) {
276
            $cname = get_class($this);
277
            $this->_formName = substr($cname, strrpos($cname, '\\') + 1);
278
        }
279
280
        return $this->_formName;
281
    }
282
283
    /**
284
     * @deprecated
285
     * Get input params GET/POST/PUT method
286
     * @param string $param
287
     * @return string|null
288
     */
289
    public function getInput($param)
290
    {
291
        return $this->getRequest($param, $this->_sendMethod);
292
    }
293
294
    /**
295
     * @deprecated
296
     * Get uploaded file from user via POST request
297
     * @param string $param
298
     * @return \Symfony\Component\HttpFoundation\File\UploadedFile|null
299
     */
300
    public function getFile($param)
301
    {
302
        return $this->getRequest($param, 'file');
303
    }
304
305
    /**
306
     * Get input value based on param path and request method
307
     * @param string $param
308
     * @param string|null $method
309
     * @return mixed
310
     */
311
    public function getRequest($param, $method = null)
312
    {
313
        if ($method === null)
314
            $method = $this->_sendMethod;
315
316
        $method = Str::lowerCase($method);
317
        // get root request as array or string
318
        switch ($method) {
319
            case 'get':
320
                $request = App::$Request->query->get($this->getFormName(), null);
321
                break;
322
            case 'post':
323
                $request = App::$Request->request->get($this->getFormName(), null);
324
                break;
325
            case 'file':
326
                $request = App::$Request->files->get($this->getFormName(), null);
327
                break;
328
            default:
329
                $request = App::$Request->get($this->getFormName(), null);
330
                break;
331
        }
332
333
        $response = null;
0 ignored issues
show
Unused Code introduced by
$response is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
334
        // param is a dot-separated array type
335
        if (Str::contains('.', $param)) {
336
            $response = $request;
337
            foreach (explode('.', $param) as $path) {
338
                if (!array_key_exists($path, $response))
339
                    return null;
340
                // find deep array nesting offset
341
                $response = $response[$path];
342
            }
343
        } else {
344
            $response = $request[$param];
345
        }
346
347
        return $response;
348
    }
349
}