Request::buildRefreshURL()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 1
nc 1
nop 2
1
<?php
2
/**
3
 * File containing the {@link Request} class.
4
 * @package Application Utils
5
 * @subpackage Request
6
 * @see Request
7
 */
8
9
declare(strict_types=1);
10
11
namespace AppUtils;
12
13
use AppUtils\ConvertHelper\JSONConverter;
14
use AppUtils\ConvertHelper\JSONConverter\JSONConverterException;
15
use AppUtils\Request\RequestParam;
16
use JsonException;
17
use stdClass;
18
19
/**
20
 * Request management: wrapper around request variables with validation
21
 * capabilities and overall easier and more robust request variable handling.
22
 *
23
 * Usage:
24
 *
25
 * // get a parameter. If it does not exist, returns null.
26
 * $request->getParam('name');
27
 *
28
 * // get a parameter and specify the default value to return if it does not exist.
29
 * $request->getParam('name', 'Default value');
30
 *
31
 * // register a parameter to specify its validation: if the existing
32
 * // value does not match the type, it will be considered inexistent.
33
 * $request->registerParam('name')->setInteger();
34
 *
35
 * @package Application Utils
36
 * @subpackage Request
37
 * @author Sebastian Mordziol <[email protected]>
38
 */
39
class Request
40
{
41
    public const ERROR_MISSING_OR_INVALID_PARAMETER = 97001;
42
    public const ERROR_PARAM_NOT_REGISTERED = 97002;
43
    
44
    protected static ?Request $instance = null;
45
    protected string $baseURL = '';
46
47
    /**
48
     * Stores registered parameter objects.
49
     * @see registerParam()
50
     *@var RequestParam[]
51
     */
52
    protected array $knownParams = array();
53
54
    public function __construct()
55
    {
56
        self::$instance = $this;
57
        
58
        $this->init();
59
    }
60
    
61
   /**
62
    * Can be extended in a subclass, to avoid
63
    * redefining the constructor.
64
    *
65
    * @return void
66
    */
67
    protected function init() : void
68
    {
69
        
70
    }
71
    
72
    /**
73
     * @return Request
74
     */
75
    public static function getInstance() : self
76
    {
77
        return self::$instance ?? new Request();
78
    }
79
    
80
    /**
81
     * Retrieves the value of a request parameter. Note that these values
82
     * are NOT validated unless they have been specifically registered
83
     * using the {@link registerParam()} method.
84
     *
85
     * If the request parameter has not been set or its value is empty,
86
     * the specified default value is returned.
87
     *
88
     * @param string $name
89
     * @param mixed|NULL $default
90
     * @return mixed|NULL
91
     */
92
    public function getParam(string $name, $default = null)
93
    {
94
        $value = $_REQUEST[$name] ?? $default;
95
96
        if(isset($this->knownParams[$name])) {
97
            $value = $this->knownParams[$name]->validate($value);
98
        }
99
        
100
        return $value;
101
    }
102
103
    /**
104
     * @return array<mixed>
105
     */
106
    public function getParams() : array
107
    {
108
        return $_REQUEST;
109
    }
110
    
111
    /**
112
     * Builds a URL to refresh the current page: includes all currently
113
     * specified request variables, with the option to overwrite/specify
114
     * new ones via the params parameter.
115
     *
116
     * @param array<string,string|number> $params
117
     * @param string[] $exclude Names of parameters to exclude from the refresh URL.
118
     * @return string
119
     * 
120
     * @see Request::getRefreshParams()
121
     */
122
    public function buildRefreshURL(array $params = array(), array $exclude = array()) : string
123
    {
124
        $params = $this->getRefreshParams($params, $exclude);
125
        
126
        $dispatcher = $this->getDispatcher();
127
        
128
        return $this->buildURL($params, $dispatcher);
129
    }
130
    
131
   /**
132
    * Retrieves the name of the current dispatcher script / page.
133
    * This is made to be extended and implemented in a subclass.
134
    * 
135
    * @return string
136
    */
137
    public function getDispatcher() : string
138
    {
139
        return '';
140
    }
141
    
142
   /**
143
    * Filters and retrieves the current request variables 
144
    * to be used to build a URL to refresh the current page.
145
    * 
146
    * For further customization options, use the 
147
    * {@see Request::createRefreshParams()} method.
148
    * 
149
    * @param array<string,mixed> $params Key => value pairs of parameters to always include in the result.
150
    * @param string[] $exclude Names of parameters to exclude from the result.
151
    * @return array<string,mixed>
152
    * 
153
    * @see Request::createRefreshParams()
154
    */
155
    public function getRefreshParams(array $params = array(), array $exclude = array()) : array
156
    {
157
        return $this->createRefreshParams()
158
            ->overrideParams($params)
159
            ->excludeParamsByName($exclude)
160
            ->getParams();
161
    }
162
    
163
   /**
164
    * Creates an instance of the helper that can be used to
165
    * retrieve the request's parameters collection, with the
166
    * possibility to exclude and override some by rules.
167
    * 
168
    * @return Request_RefreshParams
169
    */
170
    public function createRefreshParams() : Request_RefreshParams
171
    {
172
        return new Request_RefreshParams();
173
    }
174
175
    /**
176
     * @return string[]
177
     */
178
    public function getExcludeParams() : array
179
    {
180
        return array();
181
    }
182
    
183
    /**
184
     * Builds an application URL using the specified parameters: returns
185
     * an absolute URL to the main dispatcher with the specified parameters.
186
     * Not specifying any parameters returns the absolute URL to the
187
     * application, without ending slash.
188
     *
189
     * @param array<string,mixed> $params
190
     * @param string $dispatcher Relative path to script to use for the URL. Append trailing slash if needed.
191
     * @return string
192
     */
193
    public function buildURL(array $params = array(), string $dispatcher='') : string
194
    {
195
        $url = rtrim($this->getBaseURL(), '/') . '/' . $dispatcher;
196
        
197
        // append any leftover parameters to the end of the URL
198
        if (!empty($params)) {
199
            $url .= '?' . http_build_query($params, '', '&amp;');
200
        }
201
        
202
        return $url;
203
    }
204
    
205
   /**
206
    * Retrieves the base URL of the application.
207
    * @return string
208
    */
209
    public function getBaseURL() : string
210
    {
211
        return $this->baseURL;
212
    }
213
    
214
    public function setBaseURL(string $url) : Request
215
    {
216
        $this->baseURL = $url;
217
        return $this;
218
    }
219
    
220
    /**
221
     * Registers a known parameter by name, allowing you to set validation
222
     * rules for the parameter. Returns the parameter object, so you can
223
     * configure it directly by chaining.
224
     *
225
     * @param string $name
226
     * @return RequestParam
227
     */
228
    public function registerParam(string $name) : RequestParam
229
    {
230
        if(!isset($this->knownParams[$name])) {
231
            $param = new RequestParam($this, $name);
232
            $this->knownParams[$name] = $param;
233
        }
234
        
235
        return $this->knownParams[$name];
236
    }
237
    
238
   /**
239
    * Retrieves a previously registered parameter instance.
240
    * 
241
    * @param string $name
242
    * @return RequestParam
243
    *@throws Request_Exception
244
    */
245
    public function getRegisteredParam(string $name) : RequestParam
246
    {
247
        if(isset($this->knownParams[$name])) {
248
            return $this->knownParams[$name];
249
        }
250
        
251
        throw new Request_Exception(
252
            'Unknown registered request parameter.',
253
            sprintf(
254
                'The request parameter [%s] has not been registered.',
255
                $name
256
            ),
257
            self::ERROR_PARAM_NOT_REGISTERED
258
        );
259
    }
260
    
261
   /**
262
    * Checks whether a parameter with the specified name 
263
    * has been registered.
264
    * 
265
    * @param string $name
266
    * @return bool
267
    */
268
    public function hasRegisteredParam(string $name) : bool
269
    {
270
        return isset($this->knownParams[$name]);
271
    }
272
    
273
   /**
274
    * Retrieves an indexed array with accept mime types
275
    * that the client sent, in the order of preference
276
    * the client specified.
277
    *
278
    * Example:
279
    *
280
    * array(
281
    *     'text/html',
282
    *     'application/xhtml+xml',
283
    *     'image/webp'
284
    *     ...
285
    * )
286
    * 
287
    * @return string[]
288
    * @see Request::parseAcceptHeaders()
289
    */
290
    public static function getAcceptHeaders() : array
291
    {
292
        return self::parseAcceptHeaders()->getMimeStrings();
293
    }
294
    
295
   /**
296
    * Returns an instance of the "accept" headers parser,
297
    * to access information on the browser's accepted
298
    * mime types.
299
    *  
300
    * @return Request_AcceptHeaders
301
    * @see Request::getAcceptHeaders()
302
    */
303
    public static function parseAcceptHeaders() : Request_AcceptHeaders
304
    {
305
        static $accept;
306
        
307
        if(!isset($accept)) {
308
            $accept = new Request_AcceptHeaders();
309
        }
310
        
311
        return $accept;
312
    }
313
    
314
    /**
315
     * Sets a request parameter. Does nothing more than setting/overwriting
316
     * a parameter value within the same request.
317
     *
318
     * @param string $name
319
     * @param mixed $value
320
     * @return Request
321
     */
322
    public function setParam(string $name, $value) : Request
323
    {
324
        $_REQUEST[$name] = $value;
325
        
326
        if(isset($this->knownParams[$name])) {
327
            unset($this->knownParams[$name]);
328
        }
329
        
330
        return $this;
331
    }
332
333
    /**
334
     * Checks whether the specified param exists in the current request.
335
     * Note: if the parameter exists, but is not valid according to the
336
     * parameter definition, it is assumed it does not exist.
337
     *
338
     * @param string $name
339
     * @return boolean
340
     */
341
    public function hasParam(string $name) : bool
342
    {
343
        return $this->getParam($name) !== null;
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
     * @param bool $default
390
     * @return bool
391
     * @throws ConvertHelper_Exception
392
     */
393
    public function getBool(string $name, bool $default=false) : bool
394
    {
395
        $value = $this->getParam($name, $default);
396
397
        if(ConvertHelper::isBoolean($value)) {
398
            return ConvertHelper::string2bool($value);
399
        }
400
        
401
        return false;
402
    }
403
    
404
    public function validate() : void
405
    {
406
        foreach($this->knownParams as $param) 
407
        {
408
            $name = $param->getName();
409
            
410
            if($param->isRequired() && !$this->hasParam($name)) 
411
            {
412
                throw new Request_Exception(
413
                    'Missing request parameter '.$name,
414
                    sprintf(
415
                        'The request parameter [%s] is required, and is either empty or invalid.',
416
                        $name
417
                    ),
418
                    self::ERROR_MISSING_OR_INVALID_PARAMETER
419
                );
420
            }
421
        }
422
    }
423
    
424
    /**
425
     * Retrieves a param, filtered to remove HTML tags and with
426
     * html special characters encoded to avoid XSS injections.
427
     *
428
     * @param string $name
429
     * @param mixed $default
430
     * @return mixed
431
     */
432
    public function getFilteredParam(string $name, $default=null)
433
    {
434
        $val = $this->getParam($name, $default);
435
436
        if(is_string($val))
437
        {
438
            return htmlspecialchars(trim(strip_tags($val)), ENT_QUOTES, 'UTF-8');
439
        }
440
441
        if(is_bool($val))
442
        {
443
            return ConvertHelper::boolStrict2string($val);
444
        }
445
446
        if(is_numeric($val))
447
        {
448
            return (string)$val;
449
        }
450
451
        if(is_null($val))
452
        {
453
            return '';
454
        }
455
456
        return $val;
457
    }
458
459
    /**
460
     * Treats the request parameter as a JSON string, and
461
     * if it exists and contains valid JSON, returns the
462
     * decoded JSON value as an array (default).
463
     *
464
     * @param string $name
465
     * @param bool $assoc
466
     * @return array<mixed>|object
467
     *
468
     * @see Request::getJSONObject()
469
     * @see Request::getJSONAssoc()
470
     */
471
    public function getJSON(string $name, bool $assoc=true)
472
    {
473
        $value = $this->getParam($name);
474
        
475
        if(!empty($value) && is_string($value)) 
476
        {
477
            $value = JSONConverter::json2varSilent($value, $assoc);
478
479
            if($assoc && is_array($value)) {
480
                return $value;
481
            }
482
            
483
            if(is_object($value)) {
484
                return $value;
485
            }
486
        }
487
        
488
        if($assoc) {
489
            return array();
490
        }
491
        
492
        return new stdClass();
493
    }
494
495
    /**
496
     * Like {@link Request::getJSON()}, but omitting the second
497
     * parameter. Use this for more readable code.
498
     *
499
     * @param string $name
500
     * @return array<mixed>
501
     */
502
    public function getJSONAssoc(string $name) : array
503
    {
504
        $result = $this->getJSON($name);
505
        if(is_array($result)) {
0 ignored issues
show
introduced by
The condition is_array($result) is always false.
Loading history...
506
            return $result;
507
        }
508
        
509
        return array();
510
    }
511
512
    /**
513
     * Like {@link Request::getJSON()}, but omitting the second
514
     * parameter. Use this for more readable code.
515
     *
516
     * @param string $name
517
     * @return object
518
     */
519
    public function getJSONObject(string $name) : object
520
    {
521
        $result = $this->getJSON($name, false);
522
        if(is_object($result)) {
523
            return $result;
524
        }
525
        
526
        return new stdClass();
527
    }
528
529
    /**
530
     * Sends a JSON response with the correct headers.
531
     *
532
     * @param array<mixed>|string $data
533
     * @throws JSONConverterException
534
     */
535
    public static function sendJSON($data) : void
536
    {
537
        $payload = $data;
538
539
        if(!is_string($payload)) {
540
            $payload = JSONConverter::var2json($payload);
541
        }
542
        
543
        header('Cache-Control: no-cache, must-revalidate');
544
        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
545
        header('Content-type: application/json');
546
        
547
        echo $payload;
548
    }
549
550
    /**
551
     * @param array<mixed>|string $data
552
     * @return never
553
     * @throws JSONConverterException
554
     */
555
    public static function sendJSONAndExit($data) : void
556
    {
557
        self::sendJSON($data);
558
        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...
559
    }
560
    
561
   /**
562
    * Sends HTML to the browser with the correct headers.
563
    * 
564
    * @param string $html
565
    */
566
    public static function sendHTML(string $html) : void
567
    {
568
        header('Cache-Control: no-cache, must-revalidate');
569
        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
570
        header('Content-type: text/html; charset=utf-8');
571
        
572
        echo $html;
573
    }
574
575
    /**
576
     * @param string $html
577
     * @return never
578
     */
579
    public static function sendHTMLAndExit(string $html) : void
580
    {
581
        self::sendHTML($html);
582
583
        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...
584
    }
585
    
586
   /**
587
    * Creates a new instance of the URL comparer, which can check 
588
    * whether the specified URLs match, regardless of the order in 
589
    * which the query parameters are, if any.
590
    * 
591
    * @param string $sourceURL
592
    * @param string $targetURL
593
    * @param string[] $limitParams Whether to limit the comparison to these specific parameter names (if present)
594
    * @return Request_URLComparer
595
    */
596
    public function createURLComparer(string $sourceURL, string $targetURL, array $limitParams=array()) : Request_URLComparer
597
    {
598
        $comparer = new Request_URLComparer($this, $sourceURL, $targetURL);
599
        $comparer->addLimitParams($limitParams);
600
        
601
        return $comparer;
602
    }
603
    
604
   /**
605
    * Retrieves the full URL that was used to access the current page.
606
    * @return string
607
    */
608
    public function getCurrentURL() : string
609
    {
610
        return $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
611
    }
612
}