Completed
Push — master ( 2f8bc7...807794 )
by Mihail
02:57
created

ModelValidator::getBadAttribute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
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\Arr;
10
use Ffcms\Core\Helper\Type\Obj;
11
use Ffcms\Core\Helper\Type\Str;
12
use Dflydev\DotAccessData\Data as DotData;
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
    /**
26
     * Start validation for defined fields in rules() model method.
27
     * @param array|null $rules
28
     * @return bool
29
     * @throws SyntaxException
30
     */
31
    public function runValidate(array $rules = null)
32
    {
33
        // skip validation on empty rules
34
        if ($rules === null) {
35
            return true;
36
        }
37
38
        $success = true;
39
        // each
40
        foreach ($rules as $rule) {
41
            // 0 = field (property) name, 1 = filter name, 2 = filter value
42
            if ($rule[0] === null || $rule[1] === null) {
43
                continue;
44
            }
45
46
            if (Obj::isArray($rule[0])) {
47
                $validate_foreach = true;
48
                foreach ($rule[0] as $field_name) {
49
                    // end false condition
50
                    if (!$this->validateRecursive($field_name, $rule[1], $rule[2], $rule[3], $rule[4])) {
51
                        $validate_foreach = false;
52
                    }
53
                }
54
                // assign total
55
                $validate = $validate_foreach;
56
            } else {
57
                $validate = $this->validateRecursive($rule[0], $rule[1], $rule[2], $rule[3], $rule[4]);
58
            }
59
60
            // do not change condition on "true" check's (end-false-condition)
61
            if ($validate === false) {
62
                $success = false;
63
            }
64
        }
65
66
        return $success;
67
    }
68
69
    /**
70
     * Try to recursive validate field by defined rules and set result to model properties if validation is successful passed
71
     * @param string|array $field_name
72
     * @param string $filter_name
73
     * @param mixed $filter_argv
74
     * @param bool $html
75
     * @param bool $secure
76
     * @return bool
77
     * @throws SyntaxException
78
     */
79
    public function validateRecursive($field_name, $filter_name, $filter_argv, $html = false, $secure = false)
80
    {
81
        // check if we got it from form defined request method
82
        if (App::$Request->getMethod() !== $this->_sendMethod) {
83
            return false;
84
        }
85
86
        // get field value from user input data
87
        $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 79 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...
88
89
        $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...
90
        // maybe no filter required?
91
        if ($filter_name === 'used') {
92
            $check = true;
93
        } elseif (Str::contains('::', $filter_name)) { // sounds like a callback class::method::method
94
            // string to array via delimiter ::
95
            $callbackArray = explode('::', $filter_name);
96
            // first item is a class name
97
            $class = array_shift($callbackArray);
98
            // last item its a function
99
            $method = array_pop($callbackArray);
100
            // left any items? maybe post-static callbacks?
101
            if (count($callbackArray) > 0) {
102
                foreach ($callbackArray as $obj) {
103
                    if (Str::startsWith('$', $obj) && property_exists($class, ltrim($obj, '$'))) { // sounds like a variable
104
                        $obj = ltrim($obj, '$'); // trim variable symbol '$'
105
                        $class = $class::$$obj; // make magic :)
106
                    } elseif (method_exists($class, $obj)) { // maybe its a function?
107
                        $class = $class::$obj; // call function
108
                    } else {
109
                        throw new SyntaxException('Filter callback execution failed: ' . $filter_name);
110
                    }
111
112
                }
113
            }
114
115
            // check is endpoint method exist
116
            if (method_exists($class, $method)) {
117
                $check = @$class::$method($field_value, $filter_argv);
118
            } else {
119
                throw new SyntaxException('Filter callback execution failed: ' . $filter_name);
120
            }
121
        } elseif (method_exists('Ffcms\Core\Filter\Native', $filter_name)) { // only full namespace\class path based :(
122
            if ($filter_argv != null) {
123
                $check = Native::$filter_name($field_value, $filter_argv);
124
            } else {
125
                $check = Native::$filter_name($field_value);
126
            }
127
        } else {
128
            throw new SyntaxException('Filter "' . $filter_name . '" is not exist');
129
        }
130
        if ($check !== true) { // switch only on fail check.
131
            $this->_badAttr[] = $field_name;
132
        } else {
133
            $field_set_name = $field_name;
134
            // prevent array-type setting
135
            if (Str::contains('.', $field_set_name)) {
136
                $field_set_name = strstr($field_set_name, '.', true);
137
            }
138
            if (property_exists($this, $field_set_name)) {
139
                if ($field_name !== $field_set_name) { // array-based property
140
                    $dot_path = trim(strstr($field_name, '.'), '.');
141
                    // prevent throws any exceptions for null and false objects
142
                    if (!Obj::isArray($this->{$field_set_name})) {
143
                        $this->{$field_set_name} = [];
144
                    }
145
                    // use dot-data provider to compile output array
146
                    $dotData = new DotData($this->{$field_set_name});
147
                    $dotData->set($dot_path, $field_value); // todo: check me!!! big change the bug is there
148
                    // export data from dot-data lib to model property
149
                    $this->{$field_set_name} = $dotData->export();
150
                } else { // just single property
151
                    $this->{$field_name} = $field_value; // refresh model property's from post data
152
                }
153
            }
154
        }
155
        return $check;
156
    }
157
158
    /**
159
     * Get field value from input POST/GET/AJAX data with defined security level (html - safe html, !secure = fully unescaped)
160
     * @param string $field_name
161
     * @param bool $html
162
     * @param bool $secure
163
     * @return array|null|string
164
     * @throws \InvalidArgumentException
165
     */
166
    private function getFieldValue($field_name, $html = false, $secure = false)
167
    {
168
        // get type of input data (where we must look it up)
169
        $sources = [];
170
        $inputType = Str::lowerCase($this->_sendMethod);
171
        // check input data type. Maybe file or input (text)
172
        if (method_exists($this, 'sources')) {
173
            $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...
174
        }
175
176
        if (Obj::isArray($sources) && array_key_exists($field_name, $sources)) {
177
            $inputType = Str::lowerCase($sources[$field_name]);
178
        }
179
180
        $field_value = $this->getRequest($field_name, $inputType);
181
        // apply html security filter for input data
182
        if ($inputType !== 'file') {
183
            if ($html !== true) {
184
                $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...
185
            } elseif($secure !== true) {
186
                $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 186 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...
187
            }
188
        }
189
190
        return $field_value;
191
    }
192
193
    /**
194
     * Get fail validation attributes as array if exist
195
     * @return null|array
196
     */
197
    public function getBadAttribute()
198
    {
199
        return $this->_badAttr;
200
    }
201
202
    /**
203
     * Set model send method type. Allowed: post, get
204
     * @param string $acceptMethod
205
     */
206
    final public function setSubmitMethod($acceptMethod)
207
    {
208
        $this->_sendMethod = Str::upperCase($acceptMethod);
209
    }
210
211
    /**
212
     * Get model submit method. Allowed: post, get
213
     * @return string
214
     */
215
    final public function getSubmitMethod()
216
    {
217
        return $this->_sendMethod;
218
    }
219
220
    /**
221
     * Check if model get POST-based request as submit of SEND data
222
     * @return bool
223
     * @throws \InvalidArgumentException
224
     */
225
    final public function send()
226
    {
227
        if (App::$Request->getMethod() !== $this->_sendMethod) {
228
            return false;
229
        }
230
231
        return $this->getRequest('submit', $this->_sendMethod) !== null;
232
    }
233
234
    /**
235
     * Form default name (used in field building)
236
     * @return string
237
     */
238
    public function getFormName()
239
    {
240
        if ($this->_formName === null) {
241
            $cname = get_class($this);
242
            $this->_formName = substr($cname, strrpos($cname, '\\') + 1);
243
        }
244
245
        return $this->_formName;
246
    }
247
248
    /**
249
     * @deprecated
250
     * Get input params GET/POST/PUT method
251
     * @param string $param
252
     * @return string|null
253
     */
254
    public function getInput($param)
255
    {
256
        return $this->getRequest($param, $this->_sendMethod);
257
    }
258
259
    /**
260
     * @deprecated
261
     * Get uploaded file from user via POST request
262
     * @param string $param
263
     * @return \Symfony\Component\HttpFoundation\File\UploadedFile|null
264
     */
265
    public function getFile($param)
266
    {
267
        return $this->getRequest($param, 'file');
268
    }
269
270
    /**
271
     * Get input param for current model form based on param name and request method
272
     * @param string $param
273
     * @param string $method
274
     * @return string|null|array
275
     * @throws \InvalidArgumentException
276
     */
277
    public function getRequest($param, $method = 'get')
278
    {
279
        // build param query for http foundation request
280
        $paramQuery = $this->getFormName();
281
        if (Str::contains('.', $param)) {
282
            foreach (explode('.', $param) as $item) {
283
                $paramQuery .= '[' . $item . ']';
284
            }
285
        } else {
286
            $paramQuery .= '[' . $param . ']';
287
        }
288
289
        // get request based on method and param query
290
        $method = Str::lowerCase($method);
291
        switch ($method) {
292
            case 'get':
293
                return App::$Request->query->get($paramQuery, null, true);
294
            case 'post':
295
                return App::$Request->request->get($paramQuery, null, true);
296
            case 'file':
297
                return App::$Request->files->get($paramQuery, null, true);
298
            default:
299
                return App::$Request->get($paramQuery, null, true);
300
301
        }
302
    }
303
}