Passed
Push — master ( 8efc25...9adcd8 )
by Sebastian
02:27
created

Request::sortAcceptHeaders()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 9
c 2
b 0
f 0
nc 3
nop 2
dl 0
loc 16
rs 9.9666
1
<?php
2
/**
3
 * File containing the {@link Request} class.
4
 * @package Application Utils
5
 * @subpackage Request
6
 * @see Request
7
 */
8
9
namespace AppUtils;
10
11
/**
12
 * Request management: wrapper around request variables with validation
13
 * capabilities and overall easier and more robust request variable handling.
14
 *
15
 * Usage:
16
 *
17
 * // get a parameter. If it does not exist, returns null.
18
 * $request->getParam('name');
19
 *
20
 * // get a parameter and specifiy the default value to return if it does not exist.
21
 * $request->getParam('name', 'Default value');
22
 *
23
 * // register a parameter to specify its validation: if the existing
24
 * // value does not match the type, it will be considered inexistent.
25
 * $request->registerParam('name')->setInteger();
26
 *
27
 * @package Application Utils
28
 * @subpackage Request
29
 * @author Sebastian Mordziol <[email protected]>
30
 */
31
class Request
32
{
33
    const ERROR_MISSING_OR_INVALID_PARAMETER = 97001;
34
    
35
    const ERROR_PARAM_NOT_REGISTERED = 97002;
36
    
37
    /**
38
     * @var Request
39
     */
40
    protected static $instance;
41
    
42
   /**
43
    * @var string
44
    */
45
    protected $baseURL = '';
46
    
47
    public function __construct()
48
    {
49
        self::$instance = $this;
50
        
51
        $this->init();
52
    }
53
    
54
   /**
55
    * Can be extended in a subclass, to avoid
56
    * redefining the constructor.
57
    */
58
    protected function init()
59
    {
60
        
61
    }
62
    
63
    /**
64
     * @return Request
65
     */
66
    public static function getInstance()
67
    {
68
        return self::$instance;
69
    }
70
    
71
    /**
72
     * Stores registered parameter objects.
73
     * @var Request_Param[]
74
     * @see registerParam()
75
     */
76
    protected $knownParams = array();
77
    
78
    /**
79
     * Retrieves the value of a request parameter. Note that these values
80
     * are NOT validated unless they have been specifically registered
81
     * using the {@link registerParam()} method.
82
     *
83
     * If the request parameter has not been set or its value is empty,
84
     * the specified default value is returned.
85
     *
86
     * @param string $name
87
     * @param mixed $default
88
     * @return mixed
89
     */
90
    public function getParam($name, $default = null)
91
    {
92
        $value = $default;
93
        if(isset($_REQUEST[$name])) {
94
            $value = $_REQUEST[$name];
95
        }
96
        
97
        if(isset($this->knownParams[$name])) {
98
            $value = $this->knownParams[$name]->validate($value);
99
        }
100
        
101
        return $value;
102
    }
103
    
104
    public function getParams()
105
    {
106
        return $_REQUEST;
107
    }
108
    
109
    /**
110
     * Builds an URL to refresh the current page: includes all currently
111
     * specified request variables, with the option to overwrite/specify
112
     * new ones via the params parameter.
113
     *
114
     * @param array $params
115
     * @param string[] $exclude Names of parameters to exclude from the refresh URL.
116
     * @return string
117
     */
118
    public function buildRefreshURL($params = array(), $exclude = array())
119
    {
120
        $params = $this->getRefreshParams($params, $exclude);
121
        
122
        $dispatcher = $this->getDispatcher();
123
        
124
        return $this->buildURL($params, $dispatcher);
125
    }
126
    
127
   /**
128
    * Retrieves the name of the current dispatcher script / page.
129
    * This is made to be extended and implemented in a subclass.
130
    * 
131
    * @return string
132
    */
133
    public function getDispatcher() : string
134
    {
135
        return '';
136
    }
137
    
138
    public function getRefreshParams(array $params = array(), array $exclude = array())
139
    {
140
        $vars = $_REQUEST;
141
142
        $exclude[] = session_name();
143
        
144
        $exclude = array_merge($exclude, $this->getExcludeParams());
145
        
146
        foreach($exclude as $name) 
147
        {
148
            if(isset($vars[$name])) 
149
            {
150
                unset($vars[$name]);
151
            }
152
        }
153
        
154
        $names = array_keys($vars);
155
        
156
        // remove the HTML_QuickForm2 form variable if present, to 
157
        // avoid redirect loops when using the refresh URL in
158
        // a page in which a form has been submitted.
159
        foreach($names as $name) 
160
        {
161
            if(strstr($name, '_qf__')) 
162
            {
163
                unset($vars[$name]);
164
                break;
165
            }
166
        }
167
        
168
        // to allow specifiying even excluded parameters
169
        $params = array_merge($vars, $params);
170
        
171
        return $params;
172
    }
173
    
174
    public function getExcludeParams()
175
    {
176
        return array();
177
    }
178
    
179
    /**
180
     * Builds an application URL using the specified parameters: returns
181
     * an absolute URL to the main dispatcher with the specified parameters.
182
     * Not specifiying any parameters returns the absolute URL to the
183
     * application, without ending slash.
184
     *
185
     * @param array $params
186
     * @param string $dispatcher Relative path to script to use for the URL. Append trailing slash if needed.
187
     * @return string
188
     */
189
    public function buildURL($params = array(), string $dispatcher='')
190
    {
191
        $url = rtrim($this->getBaseURL(), '/') . '/' . $dispatcher;
192
        
193
        // append any leftover parameters to the end of the URL
194
        if (!empty($params)) {
195
            $url .= '?' . http_build_query($params, null, '&amp;');
196
        }
197
        
198
        return $url;
199
    }
200
    
201
   /**
202
    * Retrieves the base URL of the application.
203
    * @return string
204
    */
205
    public function getBaseURL() : string
206
    {
207
        return $this->baseURL;
208
    }
209
    
210
    public function setBaseURL(string $url) : Request
211
    {
212
        $this->baseURL = $url;
213
        return $this;
214
    }
215
    
216
    /**
217
     * Registers a known parameter by name, allowing you to set validation
218
     * rules for the parameter. Returns the parameter object, so you can
219
     * configure it directly by chaining.
220
     *
221
     * @param string $name
222
     * @return Request_Param
223
     */
224
    public function registerParam($name)
225
    {
226
        if(!isset($this->knownParams[$name])) {
227
            $param = new Request_Param($this, $name);
228
            $this->knownParams[$name] = $param;
229
        }
230
        
231
        return $this->knownParams[$name];
232
    }
233
    
234
   /**
235
    * Retrieves a previously registered parameter instance.
236
    * 
237
    * @param string $name
238
    * @throws Request_Exception
239
    * @return Request_Param
240
    */
241
    public function getRegisteredParam(string $name) : Request_Param
242
    {
243
        if(isset($this->knownParams[$name])) {
244
            return $this->knownParams[$name];
245
        }
246
        
247
        throw new Request_Exception(
248
            'Unknown registered request parameter.',
249
            sprintf(
250
                'The request parameter [%s] has not been registered.',
251
                $name
252
            ),
253
            self::ERROR_PARAM_NOT_REGISTERED
254
        );
255
    }
256
    
257
   /**
258
    * Checks whether a parameter with the specified name 
259
    * has been registered.
260
    * 
261
    * @param string $name
262
    * @return bool
263
    */
264
    public function hasRegisteredParam(string $name) : bool
265
    {
266
        return isset($this->knownParams[$name]);
267
    }
268
    
269
   /**
270
    * Retrieves an indexed array with accept mime types
271
    * that the client sent, in the order of preference
272
    * the client specified.
273
    *
274
    * Example:
275
    *
276
    * array(
277
    *     'text/html',
278
    *     'application/xhtml+xml',
279
    *     'image/webp'
280
    *     ...
281
    * )
282
    * 
283
    * @return array
284
    * @see Request::parseAcceptHeaders()
285
    */
286
    public static function getAcceptHeaders() : array
287
    {
288
        return self::parseAcceptHeaders()->getMimeStrings();
289
    }
290
    
291
   /**
292
    * Returns an instance of the accept headers parser,
293
    * to access information on the browser's accepted
294
    * mime types.
295
    *  
296
    * @return Request_AcceptHeaders
297
    * @see Request::getAcceptHeaders()
298
    */
299
    public static function parseAcceptHeaders() : Request_AcceptHeaders
300
    {
301
        static $accept;
302
        
303
        if(!isset($accept)) {
304
            $accept = new Request_AcceptHeaders();
305
        }
306
        
307
        return $accept;
308
    }
309
    
310
    /**
311
     * Sets a request parameter. Does nothing more than setting/overwriting
312
     * a parameter value within the same request.
313
     *
314
     * @param string $name
315
     * @param string $value
316
     * @return Request
317
     */
318
    public function setParam(string $name, $value) : Request
319
    {
320
        $_REQUEST[$name] = $value;
321
        
322
        if(isset($this->knownParams[$name])) {
323
            unset($this->knownParams[$name]);
324
        }
325
        
326
        return $this;
327
    }
328
    
329
    /**
330
     * Checks whether the specified param exists in the current request.
331
     * Note: if the parameter exists, but is not valid according to the
332
     * parameter definition, it is assumed it does not exist.
333
     *
334
     * @return boolean
335
     */
336
    public function hasParam(string $name) : bool
337
    {
338
        $value = $this->getParam($name);
339
        if ($value !== null) {
340
            return true;
341
        }
342
        
343
        return false;
344
    }
345
    
346
   /**
347
    * Removes a single parameter from the request.
348
    * If the parameter has been registered, also
349
    * removes the registration info.
350
    * 
351
    * @param string $name
352
    * @return Request
353
    */
354
    public function removeParam(string $name) : Request
355
    {
356
        if(isset($_REQUEST[$name])) {
357
            unset($_REQUEST[$name]);
358
        }
359
        
360
        if(isset($this->knownParams[$name])) {
361
            unset($this->knownParams[$name]);
362
        }
363
        
364
        return $this;
365
    }
366
    
367
   /**
368
    * Removes several parameters from the request.
369
    * 
370
    * @param string[] $names
371
    * @return Request
372
    */
373
    public function removeParams(array $names) : Request
374
    {
375
        foreach($names as $name) {
376
            $this->removeParam($name);
377
        }
378
        
379
        return $this;
380
    }
381
    
382
    /**
383
     * Treats the request parameter as a boolean parameter
384
     * and returns its value as a boolean. If it does not exist
385
     * or does not have a value convertable to a boolean,
386
     * returns false.
387
     *
388
     * @param string $name
389
     * @return bool
390
     */
391
    public function getBool($name, $default=false)
392
    {
393
        $value = $this->getParam($name, $default);
394
        if(ConvertHelper::isBoolean($value)) {
395
            return ConvertHelper::string2bool($value);
396
        }
397
        
398
        return false;
399
    }
400
    
401
    public function validate()
402
    {
403
        foreach($this->knownParams as $param) {
404
            $name = $param->getName();
405
            if($param->isRequired() && !$this->hasParam($name)) {
406
                throw new Request_Exception(
407
                    'Missing request parameter '.$name,
408
                    sprintf(
409
                        'The request parameter [%s] is required, and is either empty or invalid according to the specified format [%s].',
410
                        $name,
411
                        $param->getValidationType()
412
                    ),
413
                    self::ERROR_MISSING_OR_INVALID_PARAMETER
414
                );
415
            }
416
        }
417
    }
418
    
419
    /**
420
     * Retrieves a param, filtered to remove HTML tags and with
421
     * html special characters encoded to avoid XSS injections.
422
     *
423
     * @param string $name
424
     * @param mixed $default
425
     * @return string
426
     */
427
    public function getFilteredParam($name, $default=null)
428
    {
429
        $val = $this->getParam($name, $default);
430
        if(is_string($val)) {
431
            $val = htmlspecialchars(trim(strip_tags($val)), ENT_QUOTES, 'UTF-8');
432
        }
433
        
434
        return $val;
435
    }
436
    
437
   /**
438
    * Treats the request parameter as a JSON string, and
439
    * if it exists and contains valid JSON, returns the
440
    * decoded JSON value as an array (default).
441
    *
442
    * @param string $name
443
    * @param bool $assoc
444
    * @return array|object
445
    * 
446
    * @see Request::getJSONAssoc()
447
    * @see Request::getJSONObject()
448
    */
449
    public function getJSON(string $name, bool $assoc=true)
450
    {
451
        $value = $this->getParam($name);
452
        
453
        if(!empty($value) && is_string($value)) 
454
        {
455
            $data = json_decode($value, $assoc);
456
            
457
            if($assoc && is_array($data)) {
458
                return $data;
459
            }
460
            
461
            if(is_object($data)) {
462
                return $data;
463
            }
464
        }
465
        
466
        if($assoc) {
467
            return array();
468
        }
469
        
470
        return new \stdClass();
471
    }
472
    
473
   /**
474
    * Like {@link Request::getJSON()}, but omitting the second
475
    * parameter. Use this for more readable code.
476
    * 
477
    * @param string $name
478
    * @return array
479
    */
480
    public function getJSONAssoc(string $name) : array
481
    {
482
        $result = $this->getJSON($name);
483
        if(is_array($result)) {
484
            return $result;
485
        }
486
        
487
        return array();
488
    }
489
    
490
   /**
491
    * Like {@link Request::getJSON()}, but omitting the second
492
    * parameter. Use this for more readable code.
493
    *
494
    * @param string $name
495
    * @return object
496
    */
497
    public function getJSONObject(string $name) : object
498
    {
499
        $result = $this->getJSON($name, false);
500
        if(is_object($result)) {
501
            return $result;
502
        }
503
        
504
        return new \stdClass();
505
    }
506
    
507
   /**
508
    * Sends a JSON response with the correct headers.
509
    *
510
    * @param array|string $data
511
    * @param bool $exit Whether to exit the script afterwards.
512
    */
513
    public static function sendJSON($data, bool $exit=true)
514
    {
515
        $payload = $data;
516
        if(!is_string($payload)) {
517
            $payload = json_encode($payload);
518
        }
519
        
520
        header('Cache-Control: no-cache, must-revalidate');
521
        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
522
        header('Content-type: application/json');
523
        
524
        echo $payload;
525
        
526
        if($exit) 
527
        {
528
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
529
        }
530
    }
531
    
532
   /**
533
    * Sends HTML to the browser with the correct headers.
534
    * 
535
    * @param string $html
536
    * @param bool $exit Whether to exit the script afterwards.
537
    */
538
    public static function sendHTML(string $html, bool $exit=true)
539
    {
540
        header('Cache-Control: no-cache, must-revalidate');
541
        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
542
        header('Content-type: text/html; charset=utf-8');
543
        
544
        echo $html;
545
        
546
        if($exit)
547
        {
548
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
549
        }
550
    }
551
    
552
   /**
553
    * Creates a new instance of the URL comparer, which can check 
554
    * whether the specified URLs match, regardless of the order in 
555
    * which the query parameters are, if any.
556
    * 
557
    * @param string $sourceURL
558
    * @param string $targetURL
559
    * @param array $limitParams Whether to limit the comparison to these specific parameter names (if present)
560
    * @return Request_URLComparer
561
    */
562
    public function createURLComparer(string $sourceURL, string $targetURL, array $limitParams=array()) : Request_URLComparer
563
    {
564
        $comparer = new Request_URLComparer($this, $sourceURL, $targetURL);
565
        $comparer->addLimitParams($limitParams);
566
        
567
        return $comparer;
568
    }
569
    
570
   /**
571
    * Retrieves the full URL that was used to access the current page.
572
    * @return string
573
    */
574
    public function getCurrentURL() : string
575
    {
576
        return $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
577
    }
578
}