Test Failed
Push — master ( 1f9a79...0fd1be )
by Sebastian
08:36
created

Request::sendHTMLAndExit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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