Completed
Push — master ( c1fd74...f54537 )
by Mihail
17:26
created

ModelValidator::getRequest()   C

Complexity

Conditions 8
Paths 24

Size

Total Lines 38
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 38
rs 5.3846
cc 8
eloc 27
nc 24
nop 2
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
            // check is endpoint method exist
146
            if (method_exists($class, $method)) {
147
                $check = @$class::$method($fieldValue, $filterArgs);
148
            } else {
149
                throw new SyntaxException('Filter callback execution failed: ' . $filterName);
150
            }
151
        } elseif (method_exists('Ffcms\Core\Helper\ModelFilters', $filterName)) { // only full namespace\class path based :(
152
            if ($filterArgs != null) {
153
                $check = ModelFilters::$filterName($fieldValue, $filterArgs);
154
            } else {
155
                $check = ModelFilters::$filterName($fieldValue);
156
            }
157
        } else {
158
            throw new SyntaxException('Filter "' . $filterName . '" is not exist');
159
        }
160
161
        // if one from all validation tests is fail - mark as incorrect attribute
162
        if ($check !== true) {
163
            $this->_badAttr[] = $propertyName;
164
            if (App::$Debug)
165
                App::$Debug->addMessage('Validation failed. Property: ' . $propertyName . ', filter: ' . $filterName, 'warning');
166
        } else {
167
            $field_set_name = $propertyName;
168
            // prevent array-type setting
169
            if (Str::contains('.', $field_set_name)) {
0 ignored issues
show
Bug introduced by
It seems like $field_set_name defined by $propertyName on line 167 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...
170
                $field_set_name = strstr($field_set_name, '.', true);
171
            }
172
            if (property_exists($this, $field_set_name)) {
173
                if ($propertyName !== $field_set_name) { // array-based property
174
                    $dot_path = trim(strstr($propertyName, '.'), '.');
175
                    // prevent throws any exceptions for null and false objects
176
                    if (!Any::isArray($this->{$field_set_name}))
177
                        $this->{$field_set_name} = [];
178
179
                    // use dot-data provider to compile output array
180
                    $dotData = new DotData($this->{$field_set_name});
181
                    $dotData->set($dot_path, $fieldValue); // todo: check me!!! Here can be bug of fail parsing dots and passing path-value
182
                    // export data from dot-data lib to model property
183
                    $this->{$field_set_name} = $dotData->export();
184
                } else { // just single property
185
                    $this->{$propertyName} = $fieldValue; // refresh model property's from post data
186
                }
187
            }
188
        }
189
        return $check;
190
    }
191
192
    /**
193
     * Get field value from input POST/GET/AJAX data with defined security level (html - safe html, !secure = fully unescaped)
194
     * @param string $propertyName
195
     * @return array|null|string
196
     * @throws \InvalidArgumentException
197
     */
198
    private function getFieldValue($propertyName)
199
    {
200
        // get type of input data (where we must look it up)
201
        $inputType = Str::lowerCase($this->_sendMethod);
202
        $filterType = 'text';
203
        // get declared field sources and types
204
        $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...
205
        $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...
206
        // validate sources for current field
207
        if (array_key_exists($propertyName, $sources))
208
            $inputType = Str::lowerCase($sources[$propertyName]);
209
210
211
        // check if field is array-nested element by dots and use first element as general
212
        $filterField = $propertyName;
213
        if (array_key_exists($filterField, $types))
214
            $filterType = Str::lowerCase($types[$filterField]);
215
216
        // get clear field value
217
        $propertyValue = $this->getRequest($propertyName, $inputType);
218
219
        // apply security filter for input data
220
        if ($inputType !== 'file') {
221
            if ($filterType === 'html') {
222
                $propertyValue = App::$Security->secureHtml($propertyValue);
223
            } elseif ($filterType !== '!secure') {
224
                $propertyValue = App::$Security->strip_tags($propertyValue);
225
            }
226
        }
227
228
        return $propertyValue;
229
    }
230
231
    /**
232
     * Get fail validation attributes as array if exist
233
     * @return null|array
234
     */
235
    public function getBadAttribute()
236
    {
237
        return $this->_badAttr;
238
    }
239
240
    /**
241
     * Set model send method type. Allowed: post, get
242
     * @param string $acceptMethod
243
     */
244
    final public function setSubmitMethod($acceptMethod)
245
    {
246
        $this->_sendMethod = Str::upperCase($acceptMethod);
247
    }
248
249
    /**
250
     * Get model submit method. Allowed: post, get
251
     * @return string
252
     */
253
    final public function getSubmitMethod()
254
    {
255
        return $this->_sendMethod;
256
    }
257
258
    /**
259
     * Check if model get POST-based request as submit of SEND data
260
     * @return bool
261
     * @throws \InvalidArgumentException
262
     */
263
    final public function send()
264
    {
265
        if (!Str::equalIgnoreCase($this->_sendMethod, App::$Request->getMethod())) {
266
            return false;
267
        }
268
269
        return $this->getRequest('submit', $this->_sendMethod) !== null;
270
    }
271
272
    /**
273
     * Form default name (used in field building)
274
     * @return string
275
     */
276
    public function getFormName()
277
    {
278
        if ($this->_formName === null) {
279
            $cname = get_class($this);
280
            $this->_formName = substr($cname, strrpos($cname, '\\') + 1);
281
        }
282
283
        return $this->_formName;
284
    }
285
286
    /**
287
     * @deprecated
288
     * Get input params GET/POST/PUT method
289
     * @param string $param
290
     * @return string|null
291
     */
292
    public function getInput($param)
293
    {
294
        return $this->getRequest($param, $this->_sendMethod);
295
    }
296
297
    /**
298
     * @deprecated
299
     * Get uploaded file from user via POST request
300
     * @param string $param
301
     * @return \Symfony\Component\HttpFoundation\File\UploadedFile|null
302
     */
303
    public function getFile($param)
304
    {
305
        return $this->getRequest($param, 'file');
306
    }
307
308
    /**
309
     * Get input value based on param path and request method
310
     * @param string $param
311
     * @param string|null $method
312
     * @return mixed
313
     */
314
    public function getRequest($param, $method = null)
315
    {
316
        if ($method === null)
317
            $method = $this->_sendMethod;
318
319
        $method = Str::lowerCase($method);
320
        // get root request as array or string
321
        switch ($method) {
322
            case 'get':
323
                $request = App::$Request->query->get($this->getFormName(), null);
324
                break;
325
            case 'post':
326
                $request = App::$Request->request->get($this->getFormName(), null);
327
                break;
328
            case 'file':
329
                $request = App::$Request->files->get($this->getFormName(), null);
330
                break;
331
            default:
332
                $request = App::$Request->get($this->getFormName(), null);
333
                break;
334
        }
335
336
        $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...
337
        // param is a dot-separated array type
338
        if (Str::contains('.', $param)) {
339
            $response = $request;
340
            foreach (explode('.', $param) as $path) {
341
                if (!array_key_exists($path, $response))
342
                    return null;
343
                // find deep array nesting offset
344
                $response = $response[$path];
345
            }
346
        } else {
347
            $response = $request[$param];
348
        }
349
350
        return $response;
351
    }
352
}