Passed
Push — master ( 42367a...08368b )
by Sebastian
02:40
created

Request_Param::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1
c 2
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
        // go through all enqueued validations in turn, each time
163
        // replacing the value with the adjusted, validated value.
164
        foreach($this->validations as $validateDef) 
165
        {
166
            $value = $this->validateType($value, $validateDef['type'], $validateDef['params']);
167
        }
168
169
        return $value;
170
    }
171
    
172
   /**
173
    * Validates the specified value using the validation type. Returns
174
    * the validated value. 
175
    * 
176
    * @param mixed $value
177
    * @param string $type
178
    * @param array $params
179
    * @throws Request_Exception
180
    * @return mixed
181
    */
182
    protected function validateType($value, string $type, array $params)
183
    {
184
        $class = '\AppUtils\Request_Param_Validator_'.ucfirst($type);
185
        
186
        if(!class_exists($class))
187
        {
188
            throw new Request_Exception(
189
                'Unknown validation type.',
190
                sprintf(
191
                    'Cannot validate using type [%s], the target class [%s] does not exist.',
192
                    $type,
193
                    $class
194
                ),
195
                self::ERROR_UNKNOWN_VALIDATION_TYPE
196
            );
197
        }
198
        
199
        $validator = new $class($this);
200
        $validator->setOptions($params);
201
        
202
        if($this->valueType === self::VALUE_TYPE_ID_LIST)
203
        {
204
            $value = $this->validateType_idList($value, $validator);
205
        }
206
        else
207
        {
208
            $value = $validator->validate($value);
209
        }
210
        
211
        return $value;
212
    }
213
    
214
    protected function validateType_idList($value, Request_Param_Validator $validator) : array
215
    {
216
        if(!is_array($value))
217
        {
218
            $value = explode(',', $value);
219
        }
220
        
221
        $keep = array();
222
        
223
        foreach($value as $subval)
224
        {
225
            $subval = trim($subval);
226
            $subval = $validator->validate($subval);
227
            
228
            if($subval !== null) {
229
                $keep[] = intval($subval);
230
            }
231
        }
232
         
233
        return $keep;
234
    }
235
    
236
    /**
237
     * Sets the parameter value as numeric, meaning it will be validated
238
     * using PHP's is_numeric method.
239
     *
240
     * @return Request_Param
241
     */
242
    public function setNumeric()
243
    {
244
        return $this->setValidation(self::VALIDATION_TYPE_NUMERIC);
245
    }
246
247
    /**
248
     * Sets the parameter value as integer, it will be validated using a
249
     * regex to match only integer values.
250
     *
251
     * @return Request_Param
252
     */
253
    public function setInteger()
254
    {
255
        return $this->setValidation(self::VALIDATION_TYPE_INTEGER);
256
    }
257
    
258
    /**
259
     * Sets a regex to bu used for validation parameter values.
260
     * @param string $regex
261
     * @return Request_Param
262
     */
263
    public function setRegex($regex)
264
    {
265
        return $this->setValidation(self::VALIDATION_TYPE_REGEX, array('regex' => $regex));
266
    }
267
    
268
    public function setURL()
269
    {
270
        return $this->setValidation(self::VALIDATION_TYPE_URL);
271
    }
272
    
273
    const VALUE_TYPE_STRING = 'string';
274
    
275
    const VALUE_TYPE_ID_LIST = 'ids_list';
276
    
277
    protected $valueType = self::VALUE_TYPE_STRING;
278
279
   /**
280
    * Sets the variable to contain a comma-separated list of integer IDs.
281
    * Example: <code>145,248,4556</code>. A single ID is also allowed, e.g.
282
    * <code>145</code>.
283
    * 
284
    * @return Request_Param
285
    */
286
    public function setIDList()
287
    {
288
        $this->valueType = self::VALUE_TYPE_ID_LIST;
289
        $this->setInteger();
290
        
291
        return $this;
292
    }
293
    
294
   /**
295
    * Sets the variable to be an alias, as defined by the
296
    * {@link RegexHelper::REGEX_ALIAS} regular expression.
297
    * 
298
    * @return Request_Param
299
    * @see RegexHelper::REGEX_ALIAS
300
    */
301
    public function setAlias()
302
    {
303
        return $this->setRegex(RegexHelper::REGEX_ALIAS);
304
    }
305
306
    /**
307
     * Sets the variable to be a name or title, as defined by the
308
     * {@link RegexHelper::REGEX_NAME_OR_TITLE} regular expression.
309
     *
310
     * @return Request_Param
311
     * @see RegexHelper::REGEX_NAME_OR_TITLE
312
     */
313
    public function setNameOrTitle()
314
    {
315
        return $this->setRegex(RegexHelper::REGEX_NAME_OR_TITLE);
316
    }
317
    
318
    /**
319
     * Sets the variable to be a name or title, as defined by the
320
     * {@link RegexHelper::REGEX_LABEL} regular expression.
321
     *
322
     * @return Request_Param
323
     * @see RegexHelper::REGEX_LABEL
324
     */
325
    public function setLabel()
326
    {
327
        return $this->setRegex(RegexHelper::REGEX_LABEL);
328
    }
329
330
    /**
331
     * Sets the parameter value as a string containing only lowercase
332
     * and/or uppercase letters.
333
     *
334
     * @return Request_Param
335
     */
336
    public function setAlpha()
337
    {
338
        return $this->setValidation(self::VALIDATION_TYPE_ALPHA);
339
    }
340
    
341
   /**
342
    * Sets the parameter value as a string containing lowercase
343
    * and/or uppercase letters, as well as numbers.
344
    * 
345
    * @return Request_Param
346
    */
347
    public function setAlnum()
348
    {
349
        return $this->setValidation(self::VALIDATION_TYPE_ALNUM);   
350
    }
351
352
    /**
353
     * Validates that the parameter value is one of the specified values.
354
     * 
355
     * Note: specify possible values as parameters to this function.
356
     * If you do not specify any values, the validation will always
357
     * fail.
358
     *
359
     * It is also possible to specifiy an array of possible values
360
     * as the first parameter.
361
     *
362
     * @return Request_Param
363
     */
364
    public function setEnum()
365
    {
366
        $args = func_get_args(); // cannot be used as function parameter in some PHP versions
367
        
368
        if(is_array($args[0])) 
369
        {
370
            $args = $args[0];
371
        }
372
373
        return $this->setValidation(
374
            self::VALIDATION_TYPE_ENUM, 
375
            array('values' => $args)
376
        );
377
    }
378
    
379
   /**
380
    * Only available for array values: the parameter must be
381
    * an array value, and the array may only contain values 
382
    * specified in the values array.
383
    * 
384
    * Submitted values that are not in the allowed list of
385
    * values are stripped from the value.
386
    *  
387
    * @param array $values List of allowed values
388
    * @return \AppUtils\Request_Param
389
    */
390
    public function setValuesList(array $values)
391
    {
392
        $this->setArray();
393
        
394
        return $this->setValidation(
395
            self::VALIDATION_TYPE_VALUESLIST, 
396
            array(
397
                'values' => $values
398
            )
399
        );
400
    }
401
    
402
    public function setArray()
403
    {
404
        return $this->setValidation(self::VALIDATION_TYPE_ARRAY);
405
    }
406
    
407
   /**
408
    * Specifies that a JSON-encoded string is expected.
409
    * 
410
    * NOTE: Numbers or quoted strings are technically valid
411
    * JSON, but are not accepted, because it is assumed
412
    * at least an array or object are expected.
413
    * 
414
    * @return \AppUtils\Request_Param
415
    */
416
    public function setJSON() : Request_Param
417
    {
418
        return $this->setValidation(self::VALIDATION_TYPE_JSON, array('arrays' => true));
419
    }
420
    
421
   /**
422
    * Like {@link Request_Param::setJSON()}, but accepts
423
    * only JSON objects. Arrays will not be accepted.
424
    * 
425
    * @return \AppUtils\Request_Param
426
    */
427
    public function setJSONObject() : Request_Param
428
    {
429
        return $this->setValidation(self::VALIDATION_TYPE_JSON, array('arrays' => false));
430
    }
431
    
432
   /**
433
    * The parameter is a string boolean representation. This means
434
    * it can be any of the following: "yes", "true", "no", "false".
435
    * The value is automatically converted to a boolean when retrieving
436
    * the parameter.
437
    * 
438
    * @return Request_Param
439
    */
440
    public function setBoolean() : Request_Param
441
    {
442
        return $this->addClassFilter('Boolean');
443
    }
444
    
445
   /**
446
    * Validates the request parameter as an MD5 string,
447
    * so that only values resembling md5 values are accepted.
448
    * 
449
    * NOTE: This can only guarantee the format, not whether
450
    * it is an actual valid hash of something.
451
    * 
452
    * @return \AppUtils\Request_Param
453
    */
454
    public function setMD5() : Request_Param
455
    {
456
        return $this->setRegex(RegexHelper::REGEX_MD5);
457
    }
458
459
    protected $validations = array();
460
    
461
    /**
462
     * Sets the validation type to use. See the VALIDATION_TYPE_XX class
463
     * constants for a list of types to use, or use any of the setXX methods
464
     * directly as shorthand.
465
     *
466
     * @param string $type
467
     * @param array $params
468
     * @return Request_Param
469
     * @throws Request_Exception
470
     * 
471
     * @see Request_Param::ERROR_UNKNOWN_VALIDATION_TYPE
472
     */
473
    public function setValidation(string $type, array $params = array()) : Request_Param
474
    {
475
        if (!in_array($type, self::$validationTypes)) {
476
            throw new Request_Exception(
477
                'Invalid validation type',
478
                sprintf(
479
                    '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.',
480
                    $type,
481
                    implode(', ', self::$validationTypes)
482
                ),
483
                self::ERROR_UNKNOWN_VALIDATION_TYPE
484
            );
485
        }
486
487
        $this->validations[] = array(
488
            'type' => $type,
489
            'params' => $params
490
        );
491
492
        return $this;
493
    }
494
    
495
   /**
496
    * Retrieves the value of the request parameter,
497
    * applying all filters (if any) and validation
498
    * (if any).
499
    * 
500
    * @param mixed $default
501
    * @return mixed
502
    */
503
    public function get($default=null)
504
    {
505
        $value = $this->request->getParam($this->paramName);
506
        if($value !== null && $value !== '') {
507
            return $value;
508
        }
509
510
        return $this->validate($default);
511
    }
512
513
    /**
514
     * Filters the specified value by going through all available
515
     * filters, if any. If none have been set, the value is simply
516
     * passed through.
517
     *
518
     * @param mixed $value
519
     * @return string
520
     */
521
    protected function filter($value)
522
    {
523
        $total = count($this->filters);
524
        for ($i = 0; $i < $total; $i++) {
525
            $method = 'applyFilter_' . $this->filters[$i]['type'];
526
            $value = $this->$method($value, $this->filters[$i]['params']);
527
        }
528
529
        return $value;
530
    }
531
    
532
    protected function applyFilter_class($value, array $config)
533
    {
534
        $class = '\AppUtils\Request_Param_Filter_'.$config['name'];
535
        
536
        $filter = new $class($this);
537
        $filter->setOptions($config['params']);
538
        
539
        return $filter->filter($value);
540
    }
541
542
    /**
543
     * Applies the callback filter.
544
     * @param mixed $value
545
     * @param array $callbackDef
546
     * @return mixed
547
     */
548
    protected function applyFilter_callback($value, $callbackDef)
549
    {
550
        $params = $callbackDef['params'];
551
        array_unshift($params, $value);
552
553
        return call_user_func_array($callbackDef['callback'], $params);
554
    }
555
556
    /**
557
     * Adds a filter to apply to the parameter value before validation.
558
     * See the FILTER_XX class constants for available types, or use any
559
     * of the addXXFilter methods as shorthand.
560
     *
561
     * @param string $type
562
     * @param mixed $params
563
     * @return Request_Param
564
     * @throws Request_Exception
565
     * 
566
     * @see Request_Param::ERROR_INVALID_FILTER_TYPE
567
     */
568
    public function addFilter($type, $params = null) : Request_Param
569
    {
570
        if (!in_array($type, self::$filterTypes)) {
571
            throw new Request_Exception(
572
                'Invalid filter type',
573
                sprintf(
574
                    '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.',
575
                    $type,
576
                    implode(', ', self::$filterTypes)
577
                ),
578
                self::ERROR_INVALID_FILTER_TYPE
579
            );
580
        }
581
582
        $this->filters[] = array(
583
            'type' => $type,
584
            'params' => $params
585
        );
586
587
        return $this;
588
    }
589
    
590
   /**
591
    * Adds a filter that trims whitespace from the request
592
    * parameter using the PHP <code>trim</code> function.
593
    * 
594
    * @return \AppUtils\Request_Param
595
    */
596
    public function addFilterTrim() : Request_Param
597
    {
598
        // to guarantee we only work with strings
599
        $this->addStringFilter();
600
        
601
        return $this->addCallbackFilter('trim');
602
    }
603
604
   /**
605
    * Converts the value to a string, even if it is not
606
    * a string value. Complex types like arrays and objects
607
    * are converted to an empty string.
608
    * 
609
    * @return \AppUtils\Request_Param
610
    */
611
    public function addStringFilter() : Request_Param
612
    {
613
        return $this->addClassFilter('String');
614
    }
615
616
    /**
617
     * Adds a filter using the specified callback. Can be any
618
     * type of callback, for example:
619
     *
620
     * // use the trim() function on the value
621
     * addCallbackFilter('trim');
622
     *
623
     * // use an object's method
624
     * addCallbackFilter(array($object, 'methodName'));
625
     *
626
     * // specify additional callback function parameters using an array (first one is always the value)
627
     * addCallbackFilter('strip_tags', array('<b><a><ul>'));
628
     *
629
     * @param mixed $callback
630
     * @param array $params
631
     * @return Request_Param
632
     */
633
    public function addCallbackFilter($callback, $params = array()) : Request_Param
634
    {
635
        return $this->addFilter(
636
            self::FILTER_TYPE_CALLBACK,
637
            array(
638
                'callback' => $callback,
639
                'params' => $params
640
            )
641
        );
642
    }
643
644
    /**
645
     * Adds a strip tags filter to the stack using PHP's strip_tags
646
     * function. Specify allowed tags like you would for the function
647
     * like this: "<b><a><ul>", or leave it empty for none.
648
     *
649
     * @param string $allowedTags
650
     * @return \AppUtils\Request_Param
651
     */
652
    public function addStripTagsFilter($allowedTags = '') : Request_Param
653
    {
654
        // to ensure we work only with string values.
655
        $this->addStringFilter();
656
        
657
        return $this->addCallbackFilter('strip_tags', array($allowedTags));
658
    }
659
    
660
   /**
661
    * Adds a filter that strips all whitespace from the
662
    * request parameter, from spaces to tabs and newlines.
663
    * 
664
    * @return \AppUtils\Request_Param
665
    */
666
    public function addStripWhitespaceFilter() : Request_Param
667
    {
668
        // to ensure we only work with strings.
669
        $this->addStringFilter();
670
        
671
        return $this->addClassFilter('StripWhitespace');
672
    }   
673
    
674
   /**
675
    * Adds a filter that transforms comma separated values
676
    * into an array of values.
677
    * 
678
    * @param bool $trimEntries Trim whitespace from each entry?
679
    * @param bool $stripEmptyEntries Remove empty entries from the array?
680
    * @return \AppUtils\Request_Param
681
    */
682
    public function addCommaSeparatedFilter(bool $trimEntries=true, bool $stripEmptyEntries=true) : Request_Param
683
    {
684
        $this->setArray();
685
        
686
        return $this->addClassFilter(
687
            'CommaSeparated', 
688
            array(
689
                'trimEntries' => $trimEntries,
690
                'stripEmptyEntries' => $stripEmptyEntries
691
            )
692
        );
693
    }
694
    
695
    protected function addClassFilter(string $name, array $params=array()) : Request_Param
696
    {
697
        return $this->addFilter(
698
            self::FILTER_TYPE_CLASS,
699
            array(
700
                'name' => $name,
701
                'params' => $params
702
            )
703
        );
704
    }
705
    
706
   /**
707
    * Adds a filter that encodes all HTML special characters
708
    * using the PHP <code>htmlspecialchars</code> function.
709
    * 
710
    * @return \AppUtils\Request_Param
711
    */
712
    public function addHTMLSpecialcharsFilter() : Request_Param
713
    {
714
        return $this->addCallbackFilter('htmlspecialchars', array(ENT_QUOTES, 'UTF-8'));
715
    }
716
717
    public function getName() : string
718
    {
719
        return $this->paramName;
720
    }
721
    
722
    protected $required = false;
723
    
724
   /**
725
    * Marks this request parameter as required. To use this feature,
726
    * you have to call the request's {@link Request::validate()}
727
    * method.
728
    * 
729
    * @return Request_Param
730
    * @see Request::validate()
731
    */
732
    public function makeRequired() : Request_Param
733
    {
734
        $this->required = true;
735
        return $this;
736
    }
737
    
738
    public function isRequired() : bool
739
    {
740
        return $this->required;
741
    }
742
}
743