Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.12% covered (warning)
80.12%
133 / 166
59.32% covered (warning)
59.32%
35 / 59
CRAP
0.00% covered (danger)
0.00%
0 / 1
Form
80.12% covered (warning)
80.12%
133 / 166
59.32% covered (warning)
59.32%
35 / 59
186.35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 expect
89.29% covered (warning)
89.29%
25 / 28
0.00% covered (danger)
0.00%
0 / 1
8.08
 expectBool
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 requireBool
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectBoolArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireBoolArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectTrue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 requireTrue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectTrueArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireTrueArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expectEmail
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireEmail
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectEmailArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireEmailArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expectFloat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireFloat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectFloatArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireFloatArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectInt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireInt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectIntArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireIntArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectIp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireIp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectIpArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireIpArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expectRegex
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 requireRegex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectRegexArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireRegexArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expectUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectUrlArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireUrlArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expectString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectStringArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireStringArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectAnything
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireAnything
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expectAnythingArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireAnythingArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expectInArray
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 requireInArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 expectInArrayArray
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 requireInArrayArray
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 expectDateTime
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 requireDateTime
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 validate
91.89% covered (success)
91.89%
34 / 37
0.00% covered (danger)
0.00%
0 / 1
21.24
 customValidationHook
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 getValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasErrors
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 urlEncode
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 qsMerge
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 qsRemove
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 required
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 wantArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @section LICENSE
4 * This file is part of Wikimedia Slim application library
5 *
6 * Wikimedia Slim application library is free software: you can
7 * redistribute it and/or modify it under the terms of the GNU General Public
8 * License as published by the Free Software Foundation, either version 3 of
9 * the License, or (at your option) any later version.
10 *
11 * Wikimedia Slim application library is distributed in the hope that it
12 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with Wikimedia Grants Review application.  If not, see
18 * <http://www.gnu.org/licenses/>.
19 *
20 * @file
21 * @copyright Â© 2015 Bryan Davis, Wikimedia Foundation and contributors.
22 */
23
24namespace Wikimedia\Slimapp;
25
26use DateTime;
27use Exception;
28use InvalidArgumentException;
29use Psr\Log\LoggerInterface;
30use Psr\Log\NullLogger;
31use const FILTER_CALLBACK;
32use const FILTER_REQUIRE_ARRAY;
33use const FILTER_UNSAFE_RAW;
34use const FILTER_VALIDATE_BOOLEAN;
35use const FILTER_VALIDATE_EMAIL;
36use const FILTER_VALIDATE_FLOAT;
37use const FILTER_VALIDATE_INT;
38use const FILTER_VALIDATE_IP;
39use const FILTER_VALIDATE_REGEXP;
40use const FILTER_VALIDATE_URL;
41
42/**
43 * Collect and validate user input.
44 *
45 * Wraps PHP's built-in filter_var_array function to collect GET or POST input
46 * as a collection of sanitized data.
47 *
48 * @author Bryan Davis <bd808@wikimedia.org>
49 * @copyright Â© 2015 Bryan Davis, Wikimedia Foundation and contributors.
50 */
51class Form {
52
53    /**
54     * @var LoggerInterface
55     */
56    protected $logger;
57
58    /**
59     * Input parameters to expect.
60     * @var array
61     */
62    protected $params = [];
63
64    /**
65     * Values received after filtering.
66     * @var array
67     */
68    protected $values = [];
69
70    /**
71     * Fields with errors.
72     * @var array
73     */
74    protected $errors = [];
75
76    /**
77     * @param LoggerInterface $logger Log channel
78     */
79    public function __construct( $logger = null ) {
80        $this->logger = $logger ?: new NullLogger();
81    }
82
83    /**
84     * Add an input expectation.
85     *
86     * Allowed options:
87     * - default: Default value for missing input
88     * - flags: Flags to pass to filter_var_array
89     * - required: Is this input required?
90     * - validate: Callable to perform additional validation
91     * - callback: Callable for \FILTER_CALLBACK validation
92     *
93     * @param string $name Parameter to expect
94     * @param int $filter Validation filter(s) to apply
95     * @param array $options Validation options
96     * @return Form Self, for message chaining
97     */
98    public function expect( $name, $filter, $options = null ) {
99        $options = ( is_array( $options ) ) ? $options : [];
100        $flags = null;
101        $required = false;
102        $validate = null;
103
104        if ( isset( $options['flags'] ) ) {
105            $flags = $options['flags'];
106            unset( $options['flags'] );
107        }
108
109        if ( isset( $options['required'] ) ) {
110            $required = $options['required'];
111            unset( $options['required'] );
112        }
113
114        if ( isset( $options['validate'] ) ) {
115            $validate = $options['validate'];
116            unset( $options['validate'] );
117        }
118
119        if ( $filter === FILTER_CALLBACK ) {
120            if ( !isset( $options['callback'] ) ||
121                !is_callable( $options['callback'] )
122            ) {
123                throw new InvalidArgumentException(
124                    'FILTER_CALLBACK requires a valid callback.'
125                );
126            }
127            $options = $options['callback'];
128        }
129
130        $this->params[$name] = [
131            'filter'   => $filter,
132            'flags'    => $flags,
133            'options'  => $options,
134            'required' => $required,
135            'validate' => $validate,
136        ];
137
138        return $this;
139    }
140
141    /**
142     * @param string $name Parameter to expect
143     * @param array $options Additional options
144     * @return Form Self, for message chaining
145     */
146    public function expectBool( $name, $options = null ) {
147        $options = ( is_array( $options ) ) ? $options : [];
148        if ( !isset( $options['default'] ) ) {
149            $options['default'] = false;
150        }
151        return $this->expect( $name, FILTER_VALIDATE_BOOLEAN, $options );
152    }
153
154    /**
155     * @param string $name Parameter to require
156     * @param array $options Additional options
157     * @return Form Self, for message chaining
158     */
159    public function requireBool( $name, $options = null ) {
160        return $this->expectBool( $name, self::required( $options ) );
161    }
162
163    /**
164     * @param string $name Parameter to expect
165     * @param array $options Additional options
166     * @return Form Self, for message chaining
167     */
168    public function expectBoolArray( $name, $options = null ) {
169        return $this->expectBool( $name, self::wantArray( $options ) );
170    }
171
172    /**
173     * @param string $name Parameter to require
174     * @param array $options Additional options
175     * @return Form Self, for message chaining
176     */
177    public function requireBoolArray( $name, $options = null ) {
178        return $this->requireBool( $name, self::wantArray( $options ) );
179    }
180
181    /**
182     * @param string $name Parameter to expect
183     * @param array $options Additional options
184     * @return Form Self, for message chaining
185     */
186    public function expectTrue( $name, $options = null ) {
187        $options = ( is_array( $options ) ) ? $options : [];
188        $options['validate'] = static function ( $v ) {
189            return (bool)$v;
190        };
191        return $this->expectBool( $name, $options );
192    }
193
194    /**
195     * @param string $name Parameter to require
196     * @param array $options Additional options
197     * @return Form Self, for message chaining
198     */
199    public function requireTrue( $name, $options = null ) {
200        return $this->expectTrue( $name, self::required( $options ) );
201    }
202
203    /**
204     * @param string $name Parameter to expect
205     * @param array $options Additional options
206     * @return Form Self, for message chaining
207     */
208    public function expectTrueArray( $name, $options = null ) {
209        return $this->expectTrue( $name, self::wantArray( $options ) );
210    }
211
212    /**
213     * @param string $name Parameter to require
214     * @param array $options Additional options
215     * @return Form Self, for message chaining
216     */
217    public function requireTrueArray( $name, $options = null ) {
218        return $this->requireTrue( $name, self::wantArray( $options ) );
219    }
220
221    /**
222     * @param string $name Parameter to expect
223     * @param array $options Additional options
224     * @return Form Self, for message chaining
225     */
226    public function expectEmail( $name, $options = null ) {
227        return $this->expect( $name, FILTER_VALIDATE_EMAIL, $options );
228    }
229
230    /**
231     * @param string $name Parameter to require
232     * @param array $options Additional options
233     * @return Form Self, for message chaining
234     */
235    public function requireEmail( $name, $options = null ) {
236        return $this->expectEmail( $name, self::required( $options ) );
237    }
238
239    /**
240     * @param string $name Parameter to expect
241     * @param array $options Additional options
242     * @return Form Self, for message chaining
243     */
244    public function expectEmailArray( $name, $options = null ) {
245        return $this->expectEmail( $name, self::wantArray( $options ) );
246    }
247
248    /**
249     * @param string $name Parameter to require
250     * @param array $options Additional options
251     * @return Form Self, for message chaining
252     */
253    public function requireEmailArray( $name, $options = null ) {
254        return $this->requireEmail( $name, self::wantArray( $options ) );
255    }
256
257    /**
258     * @param string $name Parameter to expect
259     * @param array $options Additional options
260     * @return Form Self, for message chaining
261     */
262    public function expectFloat( $name, $options = null ) {
263        return $this->expect( $name, FILTER_VALIDATE_FLOAT, $options );
264    }
265
266    /**
267     * @param string $name Parameter to require
268     * @param array $options Additional options
269     * @return Form Self, for message chaining
270     */
271    public function requireFloat( $name, $options = null ) {
272        return $this->expectFloat( $name, self::required( $options ) );
273    }
274
275    /**
276     * @param string $name Parameter to expect
277     * @param array $options Additional options
278     * @return Form Self, for message chaining
279     */
280    public function expectFloatArray( $name, $options = null ) {
281        return $this->expectFloat( $name, self::wantArray( $options ) );
282    }
283
284    /**
285     * @param string $name Parameter to require
286     * @param array $options Additional options
287     * @return Form Self, for message chaining
288     */
289    public function requireFloatArray( $name, $options = null ) {
290        return $this->requireFloat( $name, self::wantArray( $options ) );
291    }
292
293    /**
294     * @param string $name Parameter to expect
295     * @param array $options Additional options
296     * @return Form Self, for message chaining
297     */
298    public function expectInt( $name, $options = null ) {
299        return $this->expect( $name, FILTER_VALIDATE_INT, $options );
300    }
301
302    /**
303     * @param string $name Parameter to require
304     * @param array $options Additional options
305     * @return Form Self, for message chaining
306     */
307    public function requireInt( $name, $options = null ) {
308        return $this->expectInt( $name, self::required( $options ) );
309    }
310
311    /**
312     * @param string $name Parameter to expect
313     * @param array $options Additional options
314     * @return Form Self, for message chaining
315     */
316    public function expectIntArray( $name, $options = null ) {
317        return $this->expectInt( $name, self::wantArray( $options ) );
318    }
319
320    /**
321     * @param string $name Parameter to require
322     * @param array $options Additional options
323     * @return Form Self, for message chaining
324     */
325    public function requireIntArray( $name, $options = null ) {
326        return $this->requireInt( $name, self::wantArray( $options ) );
327    }
328
329    /**
330     * @param string $name Parameter to expect
331     * @param array $options Additional options
332     * @return Form Self, for message chaining
333     */
334    public function expectIp( $name, $options = null ) {
335        return $this->expect( $name, FILTER_VALIDATE_IP, $options );
336    }
337
338    /**
339     * @param string $name Parameter to require
340     * @param array $options Additional options
341     * @return Form Self, for message chaining
342     */
343    public function requireIp( $name, $options = null ) {
344        return $this->expectIp( $name, self::required( $options ) );
345    }
346
347    /**
348     * @param string $name Parameter to expect
349     * @param array $options Additional options
350     * @return Form Self, for message chaining
351     */
352    public function expectIpArray( $name, $options = null ) {
353        return $this->expectIp( $name, self::wantArray( $options ) );
354    }
355
356    /**
357     * @param string $name Parameter to require
358     * @param array $options Additional options
359     * @return Form Self, for message chaining
360     */
361    public function requireIpArray( $name, $options = null ) {
362        return $this->requireIp( $name, self::wantArray( $options ) );
363    }
364
365    /**
366     * @param string $name Parameter to expect
367     * @param string $re Regular expression
368     * @param array $options Additional options
369     * @return Form Self, for message chaining
370     */
371    public function expectRegex( $name, $re, $options = null ) {
372        $options = ( is_array( $options ) ) ? $options : [];
373        $options['regexp'] = $re;
374        return $this->expect( $name, FILTER_VALIDATE_REGEXP, $options );
375    }
376
377    /**
378     * @param string $name Parameter to require
379     * @param string $re Regular expression
380     * @param array $options Additional options
381     * @return Form Self, for message chaining
382     */
383    public function requireRegex( $name, $re, $options = null ) {
384        return $this->expectRegex( $name, $re, self::required( $options ) );
385    }
386
387    /**
388     * @param string $name Parameter to expect
389     * @param string $re Regular expression
390     * @param array $options Additional options
391     * @return Form Self, for message chaining
392     */
393    public function expectRegexArray( $name, $re, $options = null ) {
394        return $this->expectRegex( $name, $re, self::wantArray( $options ) );
395    }
396
397    /**
398     * @param string $name Parameter to require
399     * @param string $re Regular expression
400     * @param array $options Additional options
401     * @return Form Self, for message chaining
402     */
403    public function requireRegexArray( $name, $re, $options = null ) {
404        return $this->requireRegex( $name, $re, self::wantArray( $options ) );
405    }
406
407    /**
408     * @param string $name Parameter to expect
409     * @param array $options Additional options
410     * @return Form Self, for message chaining
411     */
412    public function expectUrl( $name, $options = null ) {
413        return $this->expect( $name, FILTER_VALIDATE_URL, $options );
414    }
415
416    /**
417     * @param string $name Parameter to require
418     * @param array $options Additional options
419     * @return Form Self, for message chaining
420     */
421    public function requireUrl( $name, $options = null ) {
422        return $this->expectUrl( $name, self::required( $options ) );
423    }
424
425    /**
426     * @param string $name Parameter to expect
427     * @param array $options Additional options
428     * @return Form Self, for message chaining
429     */
430    public function expectUrlArray( $name, $options = null ) {
431        return $this->expectUrl( $name, self::wantArray( $options ) );
432    }
433
434    /**
435     * @param string $name Parameter to require
436     * @param array $options Additional options
437     * @return Form Self, for message chaining
438     */
439    public function requireUrlArray( $name, $options = null ) {
440        return $this->requireUrl( $name, self::wantArray( $options ) );
441    }
442
443    /**
444     * @param string $name Parameter to expect
445     * @param array $options Additional options
446     * @return Form Self, for message chaining
447     */
448    public function expectString( $name, $options = null ) {
449        return $this->expectRegex( $name, '/^.+$/s', $options );
450    }
451
452    /**
453     * @param string $name Parameter to require
454     * @param array $options Additional options
455     * @return Form Self, for message chaining
456     */
457    public function requireString( $name, $options = null ) {
458        return $this->expectString( $name, self::required( $options ) );
459    }
460
461    /**
462     * @param string $name Parameter to expect
463     * @param array $options Additional options
464     * @return Form Self, for message chaining
465     */
466    public function expectStringArray( $name, $options = null ) {
467        return $this->expectString( $name, self::wantArray( $options ) );
468    }
469
470    /**
471     * @param string $name Parameter to require
472     * @param array $options Additional options
473     * @return Form Self, for message chaining
474     */
475    public function requireStringArray( $name, $options = null ) {
476        return $this->requireString( $name, self::wantArray( $options ) );
477    }
478
479    /**
480     * @param string $name Parameter to expect
481     * @param array $options Additional options
482     * @return Form Self, for message chaining
483     */
484    public function expectAnything( $name, $options = null ) {
485        return $this->expect( $name, FILTER_UNSAFE_RAW, $options );
486    }
487
488    /**
489     * @param string $name Parameter to require
490     * @param array $options Additional options
491     * @return Form Self, for message chaining
492     */
493    public function requireAnything( $name, $options = null ) {
494        return $this->expectAnything( $name, self::required( $options ) );
495    }
496
497    /**
498     * @param string $name Parameter to expect
499     * @param array $options Additional options
500     * @return Form Self, for message chaining
501     */
502    public function expectAnythingArray( $name, $options = null ) {
503        return $this->expectAnything( $name, self::wantArray( $options ) );
504    }
505
506    /**
507     * @param string $name Parameter to require
508     * @param array $options Additional options
509     * @return Form Self, for message chaining
510     */
511    public function requireAnythingArray( $name, $options = null ) {
512        return $this->requireAnything( $name, self::wantArray( $options ) );
513    }
514
515    /**
516     * @param string $name Parameter to expect
517     * @param array $valids Valid values
518     * @param array $options Additional options
519     * @return Form Self, for message chaining
520     */
521    public function expectInArray( $name, $valids, $options = null ) {
522        $options = ( is_array( $options ) ) ? $options : [];
523        $required = $options['required'] ?? false;
524        $options['validate'] = static function ( $val ) use ( $valids, $required ) {
525            return ( !$required && empty( $val ) ) || in_array( $val, $valids );
526        };
527        return $this->expectAnything( $name, $options );
528    }
529
530    /**
531     * @param string $name Parameter to require
532     * @param array $valids Valid values
533     * @param array $options Additional options
534     * @return Form Self, for message chaining
535     */
536    public function requireInArray( $name, $valids, $options = null ) {
537        return $this->expectInArray( $name, $valids,
538            self::required( $options )
539        );
540    }
541
542    /**
543     * @param string $name Parameter to expect
544     * @param array $valids Valid values
545     * @param array $options Additional options
546     * @return Form Self, for message chaining
547     */
548    public function expectInArrayArray( $name, $valids, $options = null ) {
549        return $this->expectInArray(
550            $name, $valids, self::wantArray( $options )
551        );
552    }
553
554    /**
555     * @param string $name Parameter to require
556     * @param array $valids Valid values
557     * @param array $options Additional options
558     * @return Form Self, for message chaining
559     */
560    public function requireInArrayArray( $name, $valids, $options = null ) {
561        return $this->requireInArray(
562            $name, $valids, self::wantArray( $options )
563        );
564    }
565
566    /**
567     * Add an input expectation for a DateTime object.
568     *
569     * @param string $name Parameter to expect
570     * @param string $format Expected date/time format
571     * @param array $options Additional options
572     * @return Form Self, for message chaining
573     * @see DateTime::createFromFormat
574     */
575    public function expectDateTime( $name, $format, $options = null ) {
576        $options = ( is_array( $options ) ) ? $options : [];
577        $options['callback'] = static function ( $value ) use ( $format ) {
578            try {
579                $date = DateTime::createFromFormat( $format, $value );
580                $formatErrors = DateTime::getLastErrors();
581                if ( $formatErrors['error_count'] == 0 &&
582                    $formatErrors['warning_count'] == 0
583                ) {
584                    return $date;
585                }
586            } catch ( Exception $ignored ) {
587                // no-op
588            }
589            return false;
590        };
591        return $this->expect( $name, FILTER_CALLBACK, $options );
592    }
593
594    /**
595     * @param string $name Parameter to require
596     * @param string $format Expected date/time format
597     * @param array $options Additional options
598     * @return Form Self, for message chaining
599     */
600    public function requireDateTime( $name, $format, $options = null ) {
601        return $this->expectDateTime( $name, $format,
602            self::required( $options )
603        );
604    }
605
606    /**
607     * Validate the provided input data using this form's expectations.
608     *
609     * @param array $vars Input to validate (default $_POST)
610     * @return bool True if input is valid, false otherwise
611     */
612    public function validate( $vars = null ) {
613        $vars = $vars ?: $_POST;
614        $this->values = [];
615        $this->errors = [];
616        $arrayInvalids = [];
617
618        $cleaned = filter_var_array( $vars, $this->params );
619
620        foreach ( $this->params as $name => $opt ) {
621            $clean = isset( $vars[$name] ) ? $cleaned[$name] : null;
622
623            if ( $clean === false &&
624                $opt['filter'] !== FILTER_VALIDATE_BOOLEAN
625            ) {
626                $this->values[$name] = null;
627
628            } elseif ( is_array( $clean ) &&
629                ( $opt['flags'] & FILTER_REQUIRE_ARRAY ) &&
630                $opt['filter'] !== FILTER_VALIDATE_BOOLEAN
631            ) {
632                // Strip invalid value markers from input array
633                $this->values[$name] = [];
634                foreach ( $clean as $key => $value ) {
635                    if ( $opt['filter'] !== FILTER_VALIDATE_BOOLEAN &&
636                        $value !== false
637                    ) {
638                        $this->values[$name][$key] = $value;
639
640                    } elseif ( $opt['filter'] === FILTER_VALIDATE_BOOLEAN &&
641                        $value !== null
642                    ) {
643                        $this->values[$name][$key] = $value;
644
645                    } else {
646                        // Keep track of invalid keys in case input was
647                        // required
648                        if ( !isset( $arrayInvalids[$name] ) ) {
649                            $arrayInvalids[$name] = [];
650                        }
651                        $arrayInvalids[$name][] = "{$name}[{$key}]";
652                    }
653                }
654
655            } else {
656                $this->values[$name] = $clean;
657            }
658
659            if ( $opt['required'] && $this->values[$name] === null ) {
660                $this->errors[] = $name;
661
662            } elseif ( $opt['required'] && isset( $arrayInvalids[$name] ) ) {
663                $this->errors = array_merge(
664                    $this->errors, $arrayInvalids[$name]
665                );
666
667            } elseif ( is_callable( $opt['validate'] ) &&
668                call_user_func( $opt['validate'], $this->values[$name] ) === false
669            ) {
670                $this->errors[] = $name;
671                $this->values[$name] = null;
672            }
673        }
674
675        $this->customValidationHook();
676
677        return count( $this->errors ) === 0;
678    }
679
680    /**
681     * Stub method that can be extended by subclasses to add additional
682     * validation logic.
683     */
684    protected function customValidationHook() {
685    }
686
687    /**
688     * @param string $name Parameter name
689     * @return mixed Parameter value
690     */
691    public function get( $name ) {
692        if ( isset( $this->values[$name] ) ) {
693            return $this->values[$name];
694
695        } elseif ( isset( $this->params[$name]['options']['default'] ) ) {
696            return $this->params[$name]['options']['default'];
697
698        } else {
699            return null;
700        }
701    }
702
703    /**
704     * @return array Form values
705     */
706    public function getValues() {
707        return $this->values;
708    }
709
710    /**
711     * @return array Form errors
712     */
713    public function getErrors() {
714        return $this->errors;
715    }
716
717    /**
718     * @return bool True if form has errors, false otherwise.
719     */
720    public function hasErrors() {
721        return count( $this->errors ) !== 0;
722    }
723
724    /**
725     * Make a URL-encoded string from a key=>value array
726     * @param array $parms Parameter array
727     * @return string URL-encoded message body
728     */
729    public static function urlEncode( $parms ) {
730        $payload = [];
731
732        foreach ( $parms as $key => $value ) {
733            if ( is_array( $value ) ) {
734                foreach ( $value as $item ) {
735                    $payload[] = urlencode( $key ) . '=' . urlencode( $item );
736                }
737            } else {
738                $payload[] = urlencode( $key ) . '=' . urlencode( $value );
739            }
740        }
741
742        return implode( '&', $payload );
743    }
744
745    /**
746     * Merge parameters into current query string.
747     * @param array $params Parameter array
748     * @return string URL-encoded message body
749     */
750    public static function qsMerge( $params = [] ) {
751        return self::urlEncode( array_merge( $_GET, $params ) );
752    }
753
754    /**
755     * Remove parameters from current query string.
756     * @param array $params Parameters to remove
757     * @return string URL-encoded message body
758     */
759    public static function qsRemove( $params = [] ) {
760        return self::urlEncode( array_diff_key( $_GET, array_flip( $params ) ) );
761    }
762
763    /**
764     * Ensure that the given options collection contains a 'required' key.
765     *
766     * @param array $options
767     * @return array
768     */
769    protected static function required( $options ) {
770        return array_merge( [ 'required' => true ], (array)$options );
771    }
772
773    /**
774     * Ensure that the given options collection contains a 'flags' key that
775     * requires the input to be an array.
776     *
777     * @param array $options
778     * @return array
779     */
780    protected static function wantArray( $options ) {
781        $options = array_merge( [ 'flags' => 0 ], (array)$options );
782        $options['flags'] |= FILTER_REQUIRE_ARRAY;
783        return $options;
784    }
785}