Completed
Push — master ( cac22e...48735e )
by Mihail
02:18
created

ModelValidator::getRequest()   D

Complexity

Conditions 9
Paths 24

Size

Total Lines 38
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 38
rs 4.909
c 0
b 0
f 0
cc 9
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'))
45
                    $this->_tokenOk = false;
46
            }
47
            // set token data to display
48
            $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...
49
        }
50
    }
51
52
    /**
53
     * Start validation for defined fields in rules() model method.
54
     * @param array|null $rules
55
     * @return bool
56
     * @throws SyntaxException
57
     */
58
    public function runValidate(array $rules = null)
59
    {
60
        // skip validation on empty rules
61
        if ($rules === null)
62
            return true;
63
64
        $success = true;
65
        // list each rule as single one
66
        foreach ($rules as $rule) {
67
            // 0 = field (property) name, 1 = filter name, 2 = filter value
68
            if ($rule[0] === null || $rule[1] === null)
69
                continue;
70
71
            $propertyName = $rule[0];
72
            $validationRule = $rule[1];
73
            $validationValue = null;
74
            if (isset($rule[2]))
75
                $validationValue = $rule[2];
76
77
            // check if target field defined as array and make recursive validation
78
            if (Any::isArray($propertyName)) {
79
                $cumulateValidation = true;
80
                foreach ($propertyName as $attrNme) {
81
                    // end false condition
82
                    if (!$this->validateRecursive($attrNme, $validationRule, $validationValue)) {
83
                        $cumulateValidation = false;
84
                    }
85
                }
86
                // assign total
87
                $validate = $cumulateValidation;
88
            } else {
89
                $validate = $this->validateRecursive($propertyName, $validationRule, $validationValue);
90
            }
91
92
            // do not change condition on "true" check's (end-false-condition)
93
            if ($validate === false) {
94
                $success = false;
95
            }
96
        }
97
98
        return $success;
99
    }
100
101
    /**
102
     * Try to recursive validate field by defined rules and set result to model properties if validation is successful passed
103
     * @param string|array $propertyName
104
     * @param string $filterName
105
     * @param mixed $filterArgs
106
     * @return bool
107
     * @throws SyntaxException
108
     */
109
    public function validateRecursive($propertyName, $filterName, $filterArgs = null)
110
    {
111
        // check if we got it from form defined request method
112
        if (App::$Request->getMethod() !== $this->_sendMethod) {
113
            return false;
114
        }
115
116
        // get field value from user input data
117
        $fieldValue = $this->getFieldValue($propertyName);
0 ignored issues
show
Bug introduced by
It seems like $propertyName defined by parameter $propertyName on line 109 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...
118
119
        $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...
120
        // maybe no filter required?
121
        if ($filterName === 'used') {
122
            $check = true;
123
        } elseif (Str::contains('::', $filterName)) { // sounds like a callback class::method::method
124
            // string to array via delimiter ::
125
            $callbackArray = explode('::', $filterName);
126
            // first item is a class name
127
            $class = array_shift($callbackArray);
128
            // last item its a function
129
            $method = array_pop($callbackArray);
130
            // left any items? maybe post-static callbacks?
131
            if (count($callbackArray) > 0) {
132
                foreach ($callbackArray as $obj) {
133
                    if (Str::startsWith('$', $obj) && property_exists($class, ltrim($obj, '$'))) { // sounds like a variable
134
                        $obj = ltrim($obj, '$'); // trim variable symbol '$'
135
                        $class = $class::${$obj}; // make magic :)
136
                    } elseif (method_exists($class, $obj)) { // maybe its a function?
137
                        $class = $class::$obj; // call function
138
                    } else {
139
                        throw new SyntaxException('Filter callback execution failed: ' . $filterName);
140
                    }
141
                }
142
            }
143
144
            // check is endpoint method exist
145
            if (method_exists($class, $method)) {
146
                $check = @$class::$method($fieldValue, $filterArgs);
147
            } else {
148
                throw new SyntaxException('Filter callback execution failed: ' . $filterName);
149
            }
150
        } elseif (method_exists('Ffcms\Core\Helper\ModelFilters', $filterName)) { // only full namespace\class path based :(
151
            if ($filterArgs != null) {
152
                $check = ModelFilters::$filterName($fieldValue, $filterArgs);
153
            } else {
154
                $check = ModelFilters::$filterName($fieldValue);
155
            }
156
        } else {
157
            throw new SyntaxException('Filter "' . $filterName . '" is not exist');
158
        }
159
160
        // if one from all validation tests is fail - mark as incorrect attribute
161
        if ($check !== true) {
162
            $this->_badAttr[] = $propertyName;
163
            if (App::$Debug)
164
                App::$Debug->addMessage('Validation failed. Property: ' . $propertyName . ', filter: ' . $filterName, 'warning');
165
        } else {
166
            $field_set_name = $propertyName;
167
            // prevent array-type setting
168
            if (Str::contains('.', $field_set_name)) {
0 ignored issues
show
Bug introduced by
It seems like $field_set_name defined by $propertyName on line 166 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...
169
                $field_set_name = strstr($field_set_name, '.', true);
170
            }
171
            if (property_exists($this, $field_set_name)) {
172
                if ($propertyName !== $field_set_name) { // array-based property
173
                    $dot_path = trim(strstr($propertyName, '.'), '.');
174
                    // prevent throws any exceptions for null and false objects
175
                    if (!Any::isArray($this->{$field_set_name}))
176
                        $this->{$field_set_name} = [];
177
178
                    // use dot-data provider to compile output array
179
                    $dotData = new DotData($this->{$field_set_name});
180
                    $dotData->set($dot_path, $fieldValue); // todo: check me!!! Here can be bug of fail parsing dots and passing path-value
181
                    // export data from dot-data lib to model property
182
                    $this->{$field_set_name} = $dotData->export();
183
                } else { // just single property
184
                    $this->{$propertyName} = $fieldValue; // refresh model property's from post data
185
                }
186
            }
187
        }
188
        return $check;
189
    }
190
191
    /**
192
     * Get field value from input POST/GET/AJAX data with defined security level (html - safe html, !secure = fully unescaped)
193
     * @param string $propertyName
194
     * @return array|null|string
195
     * @throws \InvalidArgumentException
196
     */
197
    private function getFieldValue($propertyName)
198
    {
199
        // get type of input data (where we must look it up)
200
        $inputType = Str::lowerCase($this->_sendMethod);
201
        $filterType = 'text';
202
        // get declared field sources and types
203
        $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...
204
        $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...
205
        // validate sources for current field
206
        if (array_key_exists($propertyName, $sources))
207
            $inputType = Str::lowerCase($sources[$propertyName]);
208
209
210
        // check if field is array-nested element by dots and use first element as general
211
        $filterField = $propertyName;
212
        if (array_key_exists($filterField, $types))
213
            $filterType = Str::lowerCase($types[$filterField]);
214
215
        // get clear field value
216
        $propertyValue = $this->getRequest($propertyName, $inputType);
217
218
        // apply security filter for input data
219
        if ($inputType !== 'file') {
220
            if ($filterType === 'html') {
221
                $propertyValue = App::$Security->secureHtml($propertyValue);
222
            } elseif ($filterType !== '!secure') {
223
                $propertyValue = App::$Security->strip_tags($propertyValue);
224
            }
225
        }
226
227
        return $propertyValue;
228
    }
229
230
    /**
231
     * Get fail validation attributes as array if exist
232
     * @return null|array
233
     */
234
    public function getBadAttribute()
235
    {
236
        return $this->_badAttr;
237
    }
238
239
    /**
240
     * Set model send method type. Allowed: post, get
241
     * @param string $acceptMethod
242
     */
243
    final public function setSubmitMethod($acceptMethod)
244
    {
245
        $this->_sendMethod = Str::upperCase($acceptMethod);
246
    }
247
248
    /**
249
     * Get model submit method. Allowed: post, get
250
     * @return string
251
     */
252
    final public function getSubmitMethod()
253
    {
254
        return $this->_sendMethod;
255
    }
256
257
    /**
258
     * Check if model get POST-based request as submit of SEND data
259
     * @return bool
260
     * @throws \InvalidArgumentException
261
     */
262
    final public function send()
263
    {
264
        if (!Str::equalIgnoreCase($this->_sendMethod, App::$Request->getMethod())) {
265
            return false;
266
        }
267
268
        return $this->getRequest('submit', $this->_sendMethod) !== null;
269
    }
270
271
    /**
272
     * Form default name (used in field building)
273
     * @return string
274
     */
275
    public function getFormName()
276
    {
277
        if ($this->_formName === null) {
278
            $cname = get_class($this);
279
            $this->_formName = substr($cname, strrpos($cname, '\\') + 1);
280
        }
281
282
        return $this->_formName;
283
    }
284
285
    /**
286
     * @deprecated
287
     * Get input params GET/POST/PUT method
288
     * @param string $param
289
     * @return string|null
290
     */
291
    public function getInput($param)
292
    {
293
        return $this->getRequest($param, $this->_sendMethod);
294
    }
295
296
    /**
297
     * @deprecated
298
     * Get uploaded file from user via POST request
299
     * @param string $param
300
     * @return \Symfony\Component\HttpFoundation\File\UploadedFile|null
301
     */
302
    public function getFile($param)
303
    {
304
        return $this->getRequest($param, 'file');
305
    }
306
307
    /**
308
     * Get input value based on param path and request method
309
     * @param string $param
310
     * @param string|null $method
311
     * @return mixed
312
     */
313
    public function getRequest($param, $method = null)
314
    {
315
        if ($method === null)
316
            $method = $this->_sendMethod;
317
318
        $method = Str::lowerCase($method);
319
        // get root request as array or string
320
        switch ($method) {
321
            case 'get':
322
                $request = App::$Request->query->get($this->getFormName(), null);
323
                break;
324
            case 'post':
325
                $request = App::$Request->request->get($this->getFormName(), null);
326
                break;
327
            case 'file':
328
                $request = App::$Request->files->get($this->getFormName(), null);
329
                break;
330
            default:
331
                $request = App::$Request->get($this->getFormName(), null);
332
                break;
333
        }
334
335
        $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...
336
        // param is a dot-separated array type
337
        if (Str::contains('.', $param)) {
338
            $response = $request;
339
            foreach (explode('.', $param) as $path) {
340
                if ($response !== null && !array_key_exists($path, $response))
341
                    return null;
342
                // find deep array nesting offset
343
                $response = $response[$path];
344
            }
345
        } else {
346
            $response = $request[$param];
347
        }
348
349
        return $response;
350
    }
351
}