Completed
Push — master ( f2cd13...82e8ab )
by Mihail
03:03
created

ModelValidator::getInput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace Ffcms\Core\Traits;
4
5
6
use Ffcms\Core\App;
7
use Ffcms\Core\Exception\SyntaxException;
8
use Ffcms\Core\Filter\Native;
9
use Ffcms\Core\Helper\Type\Obj;
10
use Ffcms\Core\Helper\Type\Str;
11
use Dflydev\DotAccessData\Data as DotData;
12
13
/**
14
 * Class ModelValidator. Extended realisation of model field validation
15
 * @package Ffcms\Core\Traits
16
 */
17
trait ModelValidator
18
{
19
    protected $_badAttr;
20
    protected $_sendMethod = 'POST';
21
22
    protected $_formName;
23
24
    public $_tokenRequired = false;
25
    protected $_tokenOk = true;
26
27
    /**
28
     * Initialize validator. Set csrf protection token from request data if available.
29
     * @param bool $csrf
30
     */
31
    public function initialize($csrf = false)
32
    {
33
        $this->_tokenRequired = $csrf;
34
        if ($csrf === true) {
35
            // get current token value from session
36
            $currentToken = App::$Session->get('_csrf_token', false);
37
            // set new token value to session
38
            $newToken = Str::randomLatinNumeric(mt_rand(32, 64));
39
            App::$Session->set('_csrf_token', $newToken);
40
            // if request is submited for this model - try to validate input data
41
            if ($this->send()) {
42
                // token is wrong - update bool state
43
                if ($currentToken !== $this->getRequest('_csrf_token', 'all')) {
44
                    $this->_tokenOk = false;
45
                }
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 || !Obj::isArray($rules)) {
62
            return true;
63
        }
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
73
            // check if target field defined as array and make recursive validation
74
            if (Obj::isArray($rule[0])) {
75
                $validate_foreach = true;
76
                foreach ($rule[0] as $field_name) {
77
                    // end false condition
78
                    if (!$this->validateRecursive($field_name, $rule[1], $rule[2], $rule[3], $rule[4])) {
79
                        $validate_foreach = false;
80
                    }
81
                }
82
                // assign total
83
                $validate = $validate_foreach;
84
            } else {
85
                $validate = $this->validateRecursive($rule[0], $rule[1], $rule[2], $rule[3], $rule[4]);
86
            }
87
88
            // do not change condition on "true" check's (end-false-condition)
89
            if ($validate === false) {
90
                $success = false;
91
            }
92
        }
93
94
        return $success;
95
    }
96
97
    /**
98
     * Try to recursive validate field by defined rules and set result to model properties if validation is successful passed
99
     * @param string|array $field_name
100
     * @param string $filter_name
101
     * @param mixed $filter_argv
102
     * @param bool $html
103
     * @param bool $secure
104
     * @return bool
105
     * @throws SyntaxException
106
     */
107
    public function validateRecursive($field_name, $filter_name, $filter_argv, $html = false, $secure = false)
108
    {
109
        // check if we got it from form defined request method
110
        if (App::$Request->getMethod() !== $this->_sendMethod) {
111
            return false;
112
        }
113
114
        // get field value from user input data
115
        $field_value = $this->getFieldValue($field_name, $html, $secure);
0 ignored issues
show
Bug introduced by
It seems like $field_name defined by parameter $field_name on line 107 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...
116
117
        $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...
118
        // maybe no filter required?
119
        if ($filter_name === 'used') {
120
            $check = true;
121
        } elseif (Str::contains('::', $filter_name)) { // sounds like a callback class::method::method
122
            // string to array via delimiter ::
123
            $callbackArray = explode('::', $filter_name);
124
            // first item is a class name
125
            $class = array_shift($callbackArray);
126
            // last item its a function
127
            $method = array_pop($callbackArray);
128
            // left any items? maybe post-static callbacks?
129
            if (count($callbackArray) > 0) {
130
                foreach ($callbackArray as $obj) {
131
                    if (Str::startsWith('$', $obj) && property_exists($class, ltrim($obj, '$'))) { // sounds like a variable
132
                        $obj = ltrim($obj, '$'); // trim variable symbol '$'
133
                        $class = $class::$$obj; // make magic :)
134
                    } elseif (method_exists($class, $obj)) { // maybe its a function?
135
                        $class = $class::$obj; // call function
136
                    } else {
137
                        throw new SyntaxException('Filter callback execution failed: ' . $filter_name);
138
                    }
139
140
                }
141
            }
142
143
            // check is endpoint method exist
144
            if (method_exists($class, $method)) {
145
                $check = @$class::$method($field_value, $filter_argv);
146
            } else {
147
                throw new SyntaxException('Filter callback execution failed: ' . $filter_name);
148
            }
149
        } elseif (method_exists('Ffcms\Core\Filter\Native', $filter_name)) { // only full namespace\class path based :(
150
            if ($filter_argv != null) {
151
                $check = Native::$filter_name($field_value, $filter_argv);
152
            } else {
153
                $check = Native::$filter_name($field_value);
154
            }
155
        } else {
156
            throw new SyntaxException('Filter "' . $filter_name . '" is not exist');
157
        }
158
        if ($check !== true) { // switch only on fail check.
159
            $this->_badAttr[] = $field_name;
160
        } else {
161
            $field_set_name = $field_name;
162
            // prevent array-type setting
163
            if (Str::contains('.', $field_set_name)) {
164
                $field_set_name = strstr($field_set_name, '.', true);
165
            }
166
            if (property_exists($this, $field_set_name)) {
167
                if ($field_name !== $field_set_name) { // array-based property
168
                    $dot_path = trim(strstr($field_name, '.'), '.');
169
                    // prevent throws any exceptions for null and false objects
170
                    if (!Obj::isArray($this->{$field_set_name})) {
171
                        $this->{$field_set_name} = [];
172
                    }
173
                    // use dot-data provider to compile output array
174
                    $dotData = new DotData($this->{$field_set_name});
175
                    $dotData->set($dot_path, $field_value); // todo: check me!!! Here can be bug of fail parsing dots and passing path-value
176
                    // export data from dot-data lib to model property
177
                    $this->{$field_set_name} = $dotData->export();
178
                } else { // just single property
179
                    $this->{$field_name} = $field_value; // refresh model property's from post data
180
                }
181
            }
182
        }
183
        return $check;
184
    }
185
186
    /**
187
     * Get field value from input POST/GET/AJAX data with defined security level (html - safe html, !secure = fully unescaped)
188
     * @param string $field_name
189
     * @param bool $html
190
     * @param bool $secure
191
     * @return array|null|string
192
     * @throws \InvalidArgumentException
193
     */
194
    private function getFieldValue($field_name, $html = false, $secure = false)
195
    {
196
        // get type of input data (where we must look it up)
197
        $sources = [];
198
        $inputType = Str::lowerCase($this->_sendMethod);
199
        // check input data type. Maybe file or input (text)
200
        if (method_exists($this, 'sources')) {
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
        }
203
204
        if (Obj::isArray($sources) && array_key_exists($field_name, $sources)) {
205
            $inputType = Str::lowerCase($sources[$field_name]);
206
        }
207
208
        $field_value = $this->getRequest($field_name, $inputType);
209
        // apply html security filter for input data
210
        if ($inputType !== 'file') {
211
            if ($html !== true) {
212
                $field_value = App::$Security->strip_tags($field_value);
0 ignored issues
show
Bug introduced by
It seems like $field_value can also be of type null; however, Ffcms\Core\Helper\Security::strip_tags() does only seem to accept string|array, 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...
213
            } elseif($secure !== true) {
214
                $field_value = App::$Security->secureHtml($field_value);
0 ignored issues
show
Bug introduced by
It seems like $field_value defined by \Ffcms\Core\App::$Securi...ecureHtml($field_value) on line 214 can also be of type null; however, Ffcms\Core\Helper\Security::secureHtml() does only seem to accept string|array, 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...
215
            }
216
        }
217
218
        return $field_value;
219
    }
220
221
    /**
222
     * Get fail validation attributes as array if exist
223
     * @return null|array
224
     */
225
    public function getBadAttribute()
226
    {
227
        return $this->_badAttr;
228
    }
229
230
    /**
231
     * Set model send method type. Allowed: post, get
232
     * @param string $acceptMethod
233
     */
234
    final public function setSubmitMethod($acceptMethod)
235
    {
236
        $this->_sendMethod = Str::upperCase($acceptMethod);
237
    }
238
239
    /**
240
     * Get model submit method. Allowed: post, get
241
     * @return string
242
     */
243
    final public function getSubmitMethod()
244
    {
245
        return $this->_sendMethod;
246
    }
247
248
    /**
249
     * Check if model get POST-based request as submit of SEND data
250
     * @return bool
251
     * @throws \InvalidArgumentException
252
     */
253
    final public function send()
254
    {
255
        if (!Str::equalIgnoreCase($this->_sendMethod, App::$Request->getMethod())) {
256
            return false;
257
        }
258
259
        return $this->getRequest('submit', $this->_sendMethod) !== null;
260
    }
261
262
    /**
263
     * Form default name (used in field building)
264
     * @return string
265
     */
266
    public function getFormName()
267
    {
268
        if ($this->_formName === null) {
269
            $cname = get_class($this);
270
            $this->_formName = substr($cname, strrpos($cname, '\\') + 1);
271
        }
272
273
        return $this->_formName;
274
    }
275
276
    /**
277
     * @deprecated
278
     * Get input params GET/POST/PUT method
279
     * @param string $param
280
     * @return string|null
281
     */
282
    public function getInput($param)
283
    {
284
        return $this->getRequest($param, $this->_sendMethod);
285
    }
286
287
    /**
288
     * @deprecated
289
     * Get uploaded file from user via POST request
290
     * @param string $param
291
     * @return \Symfony\Component\HttpFoundation\File\UploadedFile|null
292
     */
293
    public function getFile($param)
294
    {
295
        return $this->getRequest($param, 'file');
296
    }
297
298
    /**
299
     * Get input param for current model form based on param name and request method
300
     * @param string $param
301
     * @param string|null $method
302
     * @return string|null|array
303
     * @throws \InvalidArgumentException
304
     */
305
    public function getRequest($param, $method = null)
306
    {
307
        // build param query for http foundation request
308
        $paramQuery = $this->getFormName();
309
        if (Str::contains('.', $param)) {
310
            foreach (explode('.', $param) as $item) {
311
                $paramQuery .= '[' . $item . ']';
312
            }
313
        } else {
314
            $paramQuery .= '[' . $param . ']';
315
        }
316
317
        if ($method === null) {
318
            $method = $this->_sendMethod;
319
        }
320
321
        // get request based on method and param query
322
        $method = Str::lowerCase($method);
323
        switch ($method) {
324
            case 'get':
325
                return App::$Request->query->get($paramQuery, null, true);
326
            case 'post':
327
                return App::$Request->request->get($paramQuery, null, true);
328
            case 'file':
329
                return App::$Request->files->get($paramQuery, null, true);
330
            default:
331
                return App::$Request->get($paramQuery, null, true);
332
333
        }
334
    }
335
}