Passed
Push — master ( f25e71...663fcb )
by Sebastian
03:51
created

Request_Param::isList()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * File containing the {@link Request_Param} class.
4
 * @package Application Utils
5
 * @subpackage Request
6
 * @see Request_Param
7
 */
8
9
namespace AppUtils;
10
11
/**
12
 * Class used for handling a single request parameter - implements
13
 * validation and filtering. It is possible to filter values or
14
 * validate them, or both. Filtering is done before validation.
15
 *
16
 * Usage: use any of the setXX() methods to set the validation
17
 * type to use (only one validation type may be used per parameter),
18
 * and any of the addXXFilter() methods to specific filters to use.
19
 * Filters can be stacked, and are applied in the order that they
20
 * are added.
21
 *
22
 * @package Application Utils
23
 * @subpackage Request
24
 * @author Sebastian Mordziol <[email protected]>
25
 */
26
class Request_Param
27
{
28
    const ERROR_UNKNOWN_VALIDATION_TYPE = 16301;
29
    
30
    const ERROR_NOT_A_VALID_CALLBACK = 16302;
31
    
32
    const ERROR_INVALID_FILTER_TYPE = 16303;
33
    
34
    /**
35
     * @var Request
36
     */
37
    protected $request;
38
39
    protected $validationType = 'none';
40
41
    protected $paramName;
42
43
    protected $validationParams;
44
45
    protected $filters = array();
46
47
    protected static $validationTypes;
48
49
    protected static $filterTypes;
50
51
    const VALIDATION_TYPE_NONE = 'none';
52
53
    const VALIDATION_TYPE_NUMERIC = 'numeric';
54
55
    const VALIDATION_TYPE_INTEGER = 'integer';
56
57
    const VALIDATION_TYPE_REGEX = 'regex';
58
59
    const VALIDATION_TYPE_ALPHA = 'alpha';
60
61
    const VALIDATION_TYPE_ALNUM = 'alnum';
62
    
63
    const VALIDATION_TYPE_ENUM = 'enum';
64
65
    const VALIDATION_TYPE_ARRAY = 'array';
66
    
67
    const VALIDATION_TYPE_CALLBACK = 'callback';
68
69
    const VALIDATION_TYPE_URL = 'url';
70
    
71
    const VALIDATION_TYPE_VALUESLIST = 'valueslist';
72
    
73
    const VALIDATION_TYPE_JSON = 'json';
74
    
75
    const FILTER_TYPE_CALLBACK = 'callback';
76
    
77
    const FILTER_TYPE_CLASS = 'class';
78
    
79
    /**
80
     * Constructor for the specified parameter name. Note that this
81
     * is instantiated automatically by the request class itself. You
82
     * should never have a situation where you instantiate this manually.
83
     *
84
     * @param Request $request
85
     * @param string $paramName
86
     */
87
    public function __construct(Request $request, $paramName)
88
    {
89
        $this->request = $request;
90
        $this->paramName = $paramName;
91
92
        // initialize the validation types list that is used to
93
        // make sure that the specified validation types are valid
94
        // at runtime in case a developer used the wrong one.
95
        if (!isset(self::$validationTypes)) {
96
            self::$validationTypes = array(
97
                self::VALIDATION_TYPE_NONE,
98
                self::VALIDATION_TYPE_ALPHA,
99
                self::VALIDATION_TYPE_ALNUM,
100
                self::VALIDATION_TYPE_ENUM,
101
                self::VALIDATION_TYPE_INTEGER,
102
                self::VALIDATION_TYPE_NUMERIC,
103
                self::VALIDATION_TYPE_REGEX,
104
                self::VALIDATION_TYPE_ARRAY,
105
                self::VALIDATION_TYPE_CALLBACK,
106
                self::VALIDATION_TYPE_URL,
107
                self::VALIDATION_TYPE_VALUESLIST,
108
                self::VALIDATION_TYPE_JSON
109
            );
110
            self::$filterTypes = array(
111
                self::FILTER_TYPE_CALLBACK,
112
                self::FILTER_TYPE_CLASS
113
            );
114
        }
115
    }
116
    
117
   /**
118
    * Adds a callback as a validation method. The callback gets the
119
    * value to validate as first parameter, and any additional 
120
    * parameters passed here get appended to that.
121
    * 
122
    * The callback must return boolean true or false depending on
123
    * whether the value is valid.
124
    * 
125
    * @param callable $callback
126
    * @param array $args
127
    * @return Request_Param
128
    */
129
    public function setCallback($callback, array $args=array()) : Request_Param
130
    {
131
        if(!is_callable($callback)) {
132
            throw new Request_Exception(
133
                'Not a valid callback',
134
                'The specified callback is not a valid callable entity.',
135
                self::ERROR_NOT_A_VALID_CALLBACK
136
            );
137
        }
138
        
139
        return $this->setValidation(
140
            self::VALIDATION_TYPE_CALLBACK, 
141
            array(
142
                'callback' => $callback,
143
                'arguments' => $args
144
            )
145
        );
146
    }
147
    
148
    /**
149
     * Validates a request parameter: called automatically for all
150
     * registered parameters by the request class. If no specific
151
     * parameter type has been selected, the value will simply be
152
     * passed through.
153
     *
154
     * @param mixed $value
155
     * @return mixed
156
     */
157
    public function validate($value)
158
    {
159
        // first off, apply filtering
160
        $value = $this->filter($value);
161
        
162
        if($this->valueType === self::VALUE_TYPE_LIST)
163
        {
164
            if(!is_array($value))
0 ignored issues
show
introduced by
The condition is_array($value) is always false.
Loading history...
165
            {
166
                $value = explode(',', $value);
167
            }
168
            
169
            $keep = array();
170
            
171
            foreach($value as $subval)
172
            {
173
                $subval = $this->filter($subval);
174
                
175
                $subval = $this->applyValidations($subval, true);
176
177
                if($subval !== null) {
178
                    $keep[] = $subval;
179
                }
180
            }
181
            
182
            return $keep;
183
        }
184
        
185
        $value = $this->filter($value);
186
        
187
        $value = $this->applyValidations($value);
188
        
189
        return $value;
190
    }
191
    
192
   /**
193
    * Runs the value through all validations that were added.
194
    * 
195
    * @param mixed $value
196
    * @return mixed
197
    */
198
    protected function applyValidations($value, bool $subval=false)
199
    {
200
        // go through all enqueued validations in turn, each time
201
        // replacing the value with the adjusted, validated value.
202
        foreach($this->validations as $validateDef)
203
        {
204
            $value = $this->validateType($value, $validateDef['type'], $validateDef['params'], $subval);
205
        }
206
        
207
        return $value;
208
    }
209
    
210
   /**
211
    * Validates the specified value using the validation type. Returns
212
    * the validated value. 
213
    * 
214
    * @param mixed $value
215
    * @param string $type
216
    * @param array $params
217
    * @param bool $subval Whether this is a subvalue in a list
218
    * @throws Request_Exception
219
    * @return mixed
220
    */
221
    protected function validateType($value, string $type, array $params, bool $subval)
222
    {
223
        $class = '\AppUtils\Request_Param_Validator_'.ucfirst($type);
224
        
225
        if(!class_exists($class))
226
        {
227
            throw new Request_Exception(
228
                'Unknown validation type.',
229
                sprintf(
230
                    'Cannot validate using type [%s], the target class [%s] does not exist.',
231
                    $type,
232
                    $class
233
                ),
234
                self::ERROR_UNKNOWN_VALIDATION_TYPE
235
            );
236
        }
237
        
238
        $validator = new $class($this, $subval);
239
        $validator->setOptions($params);
240
        
241
        $value = $validator->validate($value);
242
        
243
        return $value;
244
    }
245
    
246
    /**
247
     * Sets the parameter value as numeric, meaning it will be validated
248
     * using PHP's is_numeric method.
249
     *
250
     * @return Request_Param
251
     */
252
    public function setNumeric()
253
    {
254
        return $this->setValidation(self::VALIDATION_TYPE_NUMERIC);
255
    }
256
257
    /**
258
     * Sets the parameter value as integer, it will be validated using a
259
     * regex to match only integer values.
260
     *
261
     * @return Request_Param
262
     */
263
    public function setInteger()
264
    {
265
        return $this->setValidation(self::VALIDATION_TYPE_INTEGER);
266
    }
267
    
268
    /**
269
     * Sets a regex to bu used for validation parameter values.
270
     * @param string $regex
271
     * @return Request_Param
272
     */
273
    public function setRegex($regex)
274
    {
275
        return $this->setValidation(self::VALIDATION_TYPE_REGEX, array('regex' => $regex));
276
    }
277
    
278
    public function setURL()
279
    {
280
        return $this->setValidation(self::VALIDATION_TYPE_URL);
281
    }
282
    
283
    const VALUE_TYPE_STRING = 'string';
284
    
285
    const VALUE_TYPE_LIST = 'ids_list';
286
    
287
    protected $valueType = self::VALUE_TYPE_STRING;
288
289
   /**
290
    * Sets the variable to contain a comma-separated list of integer IDs.
291
    * Example: <code>145,248,4556</code>. A single ID is also allowed, e.g.
292
    * <code>145</code>.
293
    * 
294
    * @return Request_Param
295
    */
296
    public function setIDList()
297
    {
298
        $this->valueType = self::VALUE_TYPE_LIST;
299
        $this->addFilterTrim();
300
        $this->setInteger();
301
        
302
        return $this;
303
    }
304
    
305
   /**
306
    * Sets the variable to be an alias, as defined by the
307
    * {@link RegexHelper::REGEX_ALIAS} regular expression.
308
    * 
309
    * @return Request_Param
310
    * @see RegexHelper::REGEX_ALIAS
311
    */
312
    public function setAlias()
313
    {
314
        return $this->setRegex(RegexHelper::REGEX_ALIAS);
315
    }
316
317
    /**
318
     * Sets the variable to be a name or title, as defined by the
319
     * {@link RegexHelper::REGEX_NAME_OR_TITLE} regular expression.
320
     *
321
     * @return Request_Param
322
     * @see RegexHelper::REGEX_NAME_OR_TITLE
323
     */
324
    public function setNameOrTitle()
325
    {
326
        return $this->setRegex(RegexHelper::REGEX_NAME_OR_TITLE);
327
    }
328
    
329
    /**
330
     * Sets the variable to be a name or title, as defined by the
331
     * {@link RegexHelper::REGEX_LABEL} regular expression.
332
     *
333
     * @return Request_Param
334
     * @see RegexHelper::REGEX_LABEL
335
     */
336
    public function setLabel()
337
    {
338
        return $this->setRegex(RegexHelper::REGEX_LABEL);
339
    }
340
341
    /**
342
     * Sets the parameter value as a string containing only lowercase
343
     * and/or uppercase letters.
344
     *
345
     * @return Request_Param
346
     */
347
    public function setAlpha()
348
    {
349
        return $this->setValidation(self::VALIDATION_TYPE_ALPHA);
350
    }
351
    
352
   /**
353
    * Sets the parameter value as a string containing lowercase
354
    * and/or uppercase letters, as well as numbers.
355
    * 
356
    * @return Request_Param
357
    */
358
    public function setAlnum()
359
    {
360
        return $this->setValidation(self::VALIDATION_TYPE_ALNUM);   
361
    }
362
363
    /**
364
     * Validates that the parameter value is one of the specified values.
365
     * 
366
     * Note: specify possible values as parameters to this function.
367
     * If you do not specify any values, the validation will always
368
     * fail.
369
     *
370
     * It is also possible to specifiy an array of possible values
371
     * as the first parameter.
372
     *
373
     * @return Request_Param
374
     */
375
    public function setEnum()
376
    {
377
        $args = func_get_args(); // cannot be used as function parameter in some PHP versions
378
        
379
        if(is_array($args[0])) 
380
        {
381
            $args = $args[0];
382
        }
383
384
        return $this->setValidation(
385
            self::VALIDATION_TYPE_ENUM, 
386
            array('values' => $args)
387
        );
388
    }
389
    
390
   /**
391
    * Only available for array values: the parameter must be
392
    * an array value, and the array may only contain values 
393
    * specified in the values array.
394
    * 
395
    * Submitted values that are not in the allowed list of
396
    * values are stripped from the value.
397
    *  
398
    * @param array $values List of allowed values
399
    * @return \AppUtils\Request_Param
400
    */
401
    public function setValuesList(array $values)
402
    {
403
        $this->setArray();
404
        
405
        return $this->setValidation(
406
            self::VALIDATION_TYPE_VALUESLIST, 
407
            array(
408
                'values' => $values
409
            )
410
        );
411
    }
412
    
413
   /**
414
    * Whether the parameter is a list of values.
415
    * 
416
    * @return bool
417
    */
418
    public function isList() : bool
419
    {
420
        return $this->valueType === self::VALUE_TYPE_LIST;
421
    }
422
    
423
    public function setArray()
424
    {
425
        return $this->setValidation(self::VALIDATION_TYPE_ARRAY);
426
    }
427
    
428
   /**
429
    * Specifies that a JSON-encoded string is expected.
430
    * 
431
    * NOTE: Numbers or quoted strings are technically valid
432
    * JSON, but are not accepted, because it is assumed
433
    * at least an array or object are expected.
434
    * 
435
    * @return \AppUtils\Request_Param
436
    */
437
    public function setJSON() : Request_Param
438
    {
439
        return $this->setValidation(self::VALIDATION_TYPE_JSON, array('arrays' => true));
440
    }
441
    
442
   /**
443
    * Like {@link Request_Param::setJSON()}, but accepts
444
    * only JSON objects. Arrays will not be accepted.
445
    * 
446
    * @return \AppUtils\Request_Param
447
    */
448
    public function setJSONObject() : Request_Param
449
    {
450
        return $this->setValidation(self::VALIDATION_TYPE_JSON, array('arrays' => false));
451
    }
452
    
453
   /**
454
    * The parameter is a string boolean representation. This means
455
    * it can be any of the following: "yes", "true", "no", "false".
456
    * The value is automatically converted to a boolean when retrieving
457
    * the parameter.
458
    * 
459
    * @return Request_Param
460
    */
461
    public function setBoolean() : Request_Param
462
    {
463
        return $this->addClassFilter('Boolean');
464
    }
465
    
466
   /**
467
    * Validates the request parameter as an MD5 string,
468
    * so that only values resembling md5 values are accepted.
469
    * 
470
    * NOTE: This can only guarantee the format, not whether
471
    * it is an actual valid hash of something.
472
    * 
473
    * @return \AppUtils\Request_Param
474
    */
475
    public function setMD5() : Request_Param
476
    {
477
        return $this->setRegex(RegexHelper::REGEX_MD5);
478
    }
479
480
    protected $validations = array();
481
    
482
    /**
483
     * Sets the validation type to use. See the VALIDATION_TYPE_XX class
484
     * constants for a list of types to use, or use any of the setXX methods
485
     * directly as shorthand.
486
     *
487
     * @param string $type
488
     * @param array $params
489
     * @return Request_Param
490
     * @throws Request_Exception
491
     * 
492
     * @see Request_Param::ERROR_UNKNOWN_VALIDATION_TYPE
493
     */
494
    public function setValidation(string $type, array $params = array()) : Request_Param
495
    {
496
        if (!in_array($type, self::$validationTypes)) {
497
            throw new Request_Exception(
498
                'Invalid validation type',
499
                sprintf(
500
                    'Tried setting the validation type to "%1$s". Possible validation types are: %2$s. Use the class constants VALIDATION_TYPE_XXX to set the desired validation type to avoid errors like this.',
501
                    $type,
502
                    implode(', ', self::$validationTypes)
503
                ),
504
                self::ERROR_UNKNOWN_VALIDATION_TYPE
505
            );
506
        }
507
508
        $this->validations[] = array(
509
            'type' => $type,
510
            'params' => $params
511
        );
512
513
        return $this;
514
    }
515
    
516
   /**
517
    * Retrieves the value of the request parameter,
518
    * applying all filters (if any) and validation
519
    * (if any).
520
    * 
521
    * @param mixed $default
522
    * @return mixed
523
    */
524
    public function get($default=null)
525
    {
526
        $value = $this->request->getParam($this->paramName);
527
        if($value !== null && $value !== '') {
528
            return $value;
529
        }
530
531
        return $this->validate($default);
532
    }
533
534
    /**
535
     * Filters the specified value by going through all available
536
     * filters, if any. If none have been set, the value is simply
537
     * passed through.
538
     *
539
     * @param mixed $value
540
     * @return string
541
     */
542
    protected function filter($value)
543
    {
544
        $total = count($this->filters);
545
        for ($i = 0; $i < $total; $i++) {
546
            $method = 'applyFilter_' . $this->filters[$i]['type'];
547
            $value = $this->$method($value, $this->filters[$i]['params']);
548
        }
549
550
        return $value;
551
    }
552
    
553
    protected function applyFilter_class($value, array $config)
554
    {
555
        $class = '\AppUtils\Request_Param_Filter_'.$config['name'];
556
        
557
        $filter = new $class($this);
558
        $filter->setOptions($config['params']);
559
        
560
        return $filter->filter($value);
561
    }
562
563
    /**
564
     * Applies the callback filter.
565
     * @param mixed $value
566
     * @param array $callbackDef
567
     * @return mixed
568
     */
569
    protected function applyFilter_callback($value, $callbackDef)
570
    {
571
        $params = $callbackDef['params'];
572
        array_unshift($params, $value);
573
574
        return call_user_func_array($callbackDef['callback'], $params);
575
    }
576
577
    /**
578
     * Adds a filter to apply to the parameter value before validation.
579
     * See the FILTER_XX class constants for available types, or use any
580
     * of the addXXFilter methods as shorthand.
581
     *
582
     * @param string $type
583
     * @param mixed $params
584
     * @return Request_Param
585
     * @throws Request_Exception
586
     * 
587
     * @see Request_Param::ERROR_INVALID_FILTER_TYPE
588
     */
589
    public function addFilter($type, $params = null) : Request_Param
590
    {
591
        if (!in_array($type, self::$filterTypes)) {
592
            throw new Request_Exception(
593
                'Invalid filter type',
594
                sprintf(
595
                    'Tried setting the filter type to "%1$s". Possible validation types are: %2$s. Use the class constants FILTER_XXX to set the desired validation type to avoid errors like this.',
596
                    $type,
597
                    implode(', ', self::$filterTypes)
598
                ),
599
                self::ERROR_INVALID_FILTER_TYPE
600
            );
601
        }
602
603
        $this->filters[] = array(
604
            'type' => $type,
605
            'params' => $params
606
        );
607
608
        return $this;
609
    }
610
    
611
   /**
612
    * Adds a filter that trims whitespace from the request
613
    * parameter using the PHP <code>trim</code> function.
614
    * 
615
    * @return \AppUtils\Request_Param
616
    */
617
    public function addFilterTrim() : Request_Param
618
    {
619
        // to guarantee we only work with strings
620
        $this->addStringFilter();
621
        
622
        return $this->addCallbackFilter('trim');
623
    }
624
625
   /**
626
    * Converts the value to a string, even if it is not
627
    * a string value. Complex types like arrays and objects
628
    * are converted to an empty string.
629
    * 
630
    * @return \AppUtils\Request_Param
631
    */
632
    public function addStringFilter() : Request_Param
633
    {
634
        return $this->addClassFilter('String');
635
    }
636
637
    /**
638
     * Adds a filter using the specified callback. Can be any
639
     * type of callback, for example:
640
     *
641
     * // use the trim() function on the value
642
     * addCallbackFilter('trim');
643
     *
644
     * // use an object's method
645
     * addCallbackFilter(array($object, 'methodName'));
646
     *
647
     * // specify additional callback function parameters using an array (first one is always the value)
648
     * addCallbackFilter('strip_tags', array('<b><a><ul>'));
649
     *
650
     * @param mixed $callback
651
     * @param array $params
652
     * @return Request_Param
653
     */
654
    public function addCallbackFilter($callback, $params = array()) : Request_Param
655
    {
656
        return $this->addFilter(
657
            self::FILTER_TYPE_CALLBACK,
658
            array(
659
                'callback' => $callback,
660
                'params' => $params
661
            )
662
        );
663
    }
664
665
    /**
666
     * Adds a strip tags filter to the stack using PHP's strip_tags
667
     * function. Specify allowed tags like you would for the function
668
     * like this: "<b><a><ul>", or leave it empty for none.
669
     *
670
     * @param string $allowedTags
671
     * @return \AppUtils\Request_Param
672
     */
673
    public function addStripTagsFilter($allowedTags = '') : Request_Param
674
    {
675
        // to ensure we work only with string values.
676
        $this->addStringFilter();
677
        
678
        return $this->addCallbackFilter('strip_tags', array($allowedTags));
679
    }
680
    
681
   /**
682
    * Adds a filter that strips all whitespace from the
683
    * request parameter, from spaces to tabs and newlines.
684
    * 
685
    * @return \AppUtils\Request_Param
686
    */
687
    public function addStripWhitespaceFilter() : Request_Param
688
    {
689
        // to ensure we only work with strings.
690
        $this->addStringFilter();
691
        
692
        return $this->addClassFilter('StripWhitespace');
693
    }   
694
    
695
   /**
696
    * Adds a filter that transforms comma separated values
697
    * into an array of values.
698
    * 
699
    * @param bool $trimEntries Trim whitespace from each entry?
700
    * @param bool $stripEmptyEntries Remove empty entries from the array?
701
    * @return \AppUtils\Request_Param
702
    */
703
    public function addCommaSeparatedFilter(bool $trimEntries=true, bool $stripEmptyEntries=true) : Request_Param
704
    {
705
        $this->setArray();
706
        
707
        return $this->addClassFilter(
708
            'CommaSeparated', 
709
            array(
710
                'trimEntries' => $trimEntries,
711
                'stripEmptyEntries' => $stripEmptyEntries
712
            )
713
        );
714
    }
715
    
716
    protected function addClassFilter(string $name, array $params=array()) : Request_Param
717
    {
718
        return $this->addFilter(
719
            self::FILTER_TYPE_CLASS,
720
            array(
721
                'name' => $name,
722
                'params' => $params
723
            )
724
        );
725
    }
726
    
727
   /**
728
    * Adds a filter that encodes all HTML special characters
729
    * using the PHP <code>htmlspecialchars</code> function.
730
    * 
731
    * @return \AppUtils\Request_Param
732
    */
733
    public function addHTMLSpecialcharsFilter() : Request_Param
734
    {
735
        return $this->addCallbackFilter('htmlspecialchars', array(ENT_QUOTES, 'UTF-8'));
736
    }
737
738
    public function getName() : string
739
    {
740
        return $this->paramName;
741
    }
742
    
743
    protected $required = false;
744
    
745
   /**
746
    * Marks this request parameter as required. To use this feature,
747
    * you have to call the request's {@link Request::validate()}
748
    * method.
749
    * 
750
    * @return Request_Param
751
    * @see Request::validate()
752
    */
753
    public function makeRequired() : Request_Param
754
    {
755
        $this->required = true;
756
        return $this;
757
    }
758
    
759
    public function isRequired() : bool
760
    {
761
        return $this->required;
762
    }
763
}
764