Passed
Push — master ( 4a79ed...7ae6f1 )
by Sebastian
02:30
created

URLInfo::offsetSet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 2
b 0
f 0
nc 2
nop 2
dl 0
loc 4
rs 10
1
<?php
2
/**
3
 * File containing the {@see AppUtils\URLInfo} class.
4
 * 
5
 * @package Application Utils
6
 * @subpackage URLInfo
7
 * @see AppUtils\URLInfo
8
 */
9
10
declare(strict_types=1);
11
12
namespace AppUtils;
13
14
/**
15
 * Replacement for PHP's native `parse_url` function, which
16
 * handles some common pitfalls and issues that are hard to 
17
 * follow, as well as adding a number of utility methods.
18
 * 
19
 * @package Application Utils
20
 * @subpackage URLInfo
21
 * @author Sebastian Mordziol <[email protected]>
22
 */
23
class URLInfo implements \ArrayAccess
24
{
25
    const ERROR_MISSING_SCHEME = 42101;
26
    
27
    const ERROR_INVALID_SCHEME = 42102;
28
29
    const ERROR_MISSING_HOST = 42103;
30
    
31
    const ERROR_CANNOT_FIND_CSS_FOLDER = 42104;
32
    
33
    const ERROR_UNKNOWN_TYPE_FOR_LABEL = 42105;
34
    
35
    const TYPE_EMAIL = 'email';
36
    const TYPE_FRAGMENT = 'fragment';
37
    const TYPE_PHONE = 'phone';
38
    const TYPE_URL = 'url';
39
    
40
   /**
41
    * The original URL that was passed to the constructor.
42
    * @var string
43
    */
44
    protected $rawURL;
45
46
   /**
47
    * @var array
48
    */
49
    protected $info;
50
    
51
   /**
52
    * @var string[]
53
    */
54
    protected $excludedParams = array();
55
    
56
   /**
57
    * @var bool
58
    * @see URLInfo::setParamExclusion()
59
    */
60
    protected $paramExclusion = false;
61
    
62
   /**
63
    * @var array
64
    * @see URLInfo::getTypeLabel()
65
    */
66
    protected static $typeLabels;
67
    
68
   /**
69
    * @var bool
70
    */
71
    protected $highlightExcluded = false;
72
    
73
   /**
74
    * @var array
75
    */
76
    protected $infoKeys = array(
77
        'scheme',
78
        'host',
79
        'port',
80
        'user',
81
        'pass',
82
        'path',
83
        'query',
84
        'fragment'
85
    );
86
    
87
   /**
88
    * @var string
89
    */
90
    protected $url;
91
    
92
   /**
93
    * @var URLInfo_Parser
94
    */
95
    protected $parser;
96
    
97
    public function __construct(string $url)
98
    {
99
        $this->rawURL = $url;
100
        $this->url = self::filterURL($url);
101
        
102
        $this->parser = new URLInfo_Parser($this->url);
103
        $this->info = $this->parser->getInfo();
104
    }
105
    
106
   /**
107
    * Filters an URL: removes control characters and the
108
    * like to have a clean URL to work with.
109
    * 
110
    * @param string $url
111
    * @return string
112
    */
113
    public static function filterURL(string $url)
114
    {
115
        return URLInfo_Filter::filter($url);
116
    }
117
    
118
    /**
119
     * Checks if it is an https link.
120
     * @return boolean
121
     */
122
    public function isSecure() : bool
123
    {
124
        if(isset($this->info['scheme']) && $this->info['scheme'] === 'https') {
125
            return true;
126
        }
127
        
128
        return false;
129
    }
130
    
131
    public function isAnchor() : bool
132
    {
133
        return $this->info['type'] === self::TYPE_FRAGMENT;
134
    }
135
    
136
    public function isEmail() : bool
137
    {
138
        return $this->info['type'] === self::TYPE_EMAIL;
139
    }
140
    
141
    public function isPhoneNumber() : bool
142
    {
143
        return $this->info['type'] === self::TYPE_PHONE;
144
    }
145
    
146
   /**
147
    * Whether the URL is a regular URL, not one of the 
148
    * other types like a phone number or email address.
149
    * 
150
    * @return bool
151
    */
152
    public function isURL() : bool
153
    {
154
        $host = $this->getHost();
155
        return !empty($host);
156
    }
157
    
158
    public function isValid() : bool
159
    {
160
        return $this->parser->isValid();
161
    }
162
    
163
   /**
164
    * Retrieves the host name, or an empty string if none is present.
165
    * 
166
    * @return string
167
    */
168
    public function getHost() : string
169
    {
170
        return $this->getInfoKey('host');
171
    }
172
    
173
   /**
174
    * Retrieves the path, or an empty string if none is present.
175
    * @return string
176
    */
177
    public function getPath() : string
178
    {
179
        return $this->getInfoKey('path');
180
    }
181
    
182
    public function getFragment() : string
183
    {
184
        return $this->getInfoKey('fragment');
185
    }
186
    
187
    public function getScheme() : string
188
    {
189
        return $this->getInfoKey('scheme');
190
    }
191
    
192
   /**
193
    * Retrieves the port specified in the URL, or -1 if none is preseent.
194
    * @return int
195
    */
196
    public function getPort() : int
197
    {
198
        $port = $this->getInfoKey('port');
199
        if(!empty($port)) {
200
            return (int)$port;
201
        }
202
        
203
        return -1;
204
    }
205
    
206
   /**
207
    * Retrieves the raw query string, or an empty string if none is present.
208
    * 
209
    * @return string
210
    * 
211
    * @see URLInfo::getParams()
212
    */
213
    public function getQuery() : string
214
    {
215
        return $this->getInfoKey('query');
216
    }
217
    
218
    public function getUsername() : string
219
    {
220
        return $this->getInfoKey('user');
221
    }
222
    
223
    public function getPassword() : string
224
    {
225
        return $this->getInfoKey('pass');
226
    }
227
    
228
   /**
229
    * Whether the URL contains a port number.
230
    * @return bool
231
    */
232
    public function hasPort() : bool
233
    {
234
        return $this->getPort() !== -1;
235
    }
236
    
237
   /**
238
    * Alias for the hasParams() method.
239
    * @return bool
240
    * @see URLInfo::hasParams()
241
    */
242
    public function hasQuery() : bool
243
    {
244
        return $this->hasParams();
245
    }
246
    
247
    public function hasHost() : bool
248
    {
249
        return $this->getHost() !== ''; 
250
    }
251
    
252
    public function hasPath() : bool
253
    {
254
        return $this->getPath() !== '';
255
    }
256
    
257
    public function hasFragment() : bool
258
    {
259
        return $this->getFragment() !== '';
260
    }
261
    
262
    public function hasUsername() : bool
263
    {
264
        return $this->getUsername() !== '';
265
    }
266
    
267
    public function hasPassword() : bool
268
    {
269
        return $this->getPassword() !== '';
270
    }
271
    
272
    public function hasScheme() : bool
273
    {
274
        return $this->getScheme() !== '';
275
    }
276
    
277
    protected function getInfoKey(string $name) : string
278
    {
279
        if(isset($this->info[$name])) {
280
            return (string)$this->info[$name];
281
        }
282
        
283
        return '';
284
    }
285
    
286
    public function getNormalized() : string
287
    {
288
        if(!$this->isValid()) {
289
            return '';
290
        }
291
        
292
        if($this->isAnchor())
293
        {
294
            return '#'.$this->getFragment();
295
        }
296
        else if($this->isPhoneNumber())
297
        {
298
            return 'tel://'.$this->getHost();
299
        }
300
        else if($this->isEmail())
301
        {
302
            return 'mailto:'.$this->getPath();
303
        }
304
        
305
        $normalized = $this->info['scheme'].'://'.$this->info['host'];
306
        if(isset($this->info['path'])) {
307
            $normalized .= $this->info['path'];
308
        }
309
        
310
        $params = $this->getParams();
311
        if(!empty($params)) {
312
            $normalized .= '?'.http_build_query($params);
313
        }
314
        
315
        if(isset($this->info['fragment'])) {
316
            $normalized .= '#'.$this->info['fragment'];
317
        }
318
        
319
        return $normalized;
320
    }
321
    
322
   /**
323
    * Creates a hash of the URL, which can be used for comparisons.
324
    * Since any parameters in the URL's query are sorted alphabetically,
325
    * the same links with a different parameter order will have the 
326
    * same hash.
327
    * 
328
    * @return string
329
    */
330
    public function getHash()
331
    {
332
        return \AppUtils\ConvertHelper::string2shortHash($this->getNormalized());
333
    }
334
335
   /**
336
    * Highlights the URL using HTML tags with specific highlighting
337
    * class names.
338
    * 
339
    * @return string Will return an empty string if the URL is not valid.
340
    */
341
    public function getHighlighted() : string
342
    {
343
        if(!$this->isValid()) {
344
            return '';
345
        }
346
        
347
        $highlighter = new URLInfo_Highlighter($this);
348
        
349
        return $highlighter->highlight();
350
    }
351
    
352
    public function getErrorMessage() : string
353
    {
354
        return $this->parser->getErrorMessage();
355
    }
356
    
357
    public function getErrorCode() : int
358
    {
359
        return $this->parser->getErrorCode();
360
    }
361
    
362
    public function hasParams() : bool
363
    {
364
        $params = $this->getParams();
365
        return !empty($params);
366
    }
367
    
368
    public function countParams() : int
369
    {
370
        $params = $this->getParams();
371
        return count($params);
372
    }
373
    
374
   /**
375
    * Retrieves all parameters specified in the url,
376
    * if any, as an associative array. 
377
    * 
378
    * NOTE: Ignores parameters that have been added
379
    * to the excluded parameters list.
380
    *
381
    * @return array
382
    */
383
    public function getParams() : array
384
    {
385
        if(!$this->paramExclusion || empty($this->excludedParams)) {
386
            return $this->info['params'];
387
        }
388
        
389
        $keep = array();
390
        foreach($this->info['params'] as $name => $value) {
391
            if(!isset($this->excludedParams[$name])) {
392
                $keep[$name] = $value;
393
            }
394
        }
395
        
396
        return $keep;
397
    }
398
    
399
   /**
400
    * Retrieves the names of all parameters present in the URL, if any.
401
    * @return string[]
402
    */
403
    public function getParamNames() : array
404
    {
405
        $params = $this->getParams();
406
        return array_keys($params);
407
    }
408
    
409
   /**
410
    * Retrieves a specific parameter value from the URL.
411
    * 
412
    * @param string $name
413
    * @return string The parameter value, or an empty string if it does not exist.
414
    */
415
    public function getParam(string $name) : string
416
    {
417
        if(isset($this->info['params'][$name])) {
418
            return $this->info['params'][$name];
419
        }
420
        
421
        return '';
422
    }
423
    
424
   /**
425
    * Excludes an URL parameter entirely if present:
426
    * the parser will act as if the parameter was not
427
    * even present in the source URL, effectively
428
    * stripping it.
429
    *
430
    * @param string $name
431
    * @param string $reason A human readable explanation why this is excluded - used when highlighting links.
432
    * @return URLInfo
433
    */
434
    public function excludeParam(string $name, string $reason) : URLInfo
435
    {
436
        if(!isset($this->excludedParams[$name]))
437
        {
438
            $this->excludedParams[$name] = $reason;
439
            $this->setParamExclusion();
440
        }
441
        
442
        return $this;
443
    }
444
445
    /**
446
     * Retrieves a string identifier of the type of URL that was detected.
447
     *
448
     * @return string
449
     *
450
     * @see URLInfo::TYPE_EMAIL
451
     * @see URLInfo::TYPE_FRAGMENT
452
     * @see URLInfo::TYPE_PHONE
453
     * @see URLInfo::TYPE_URL
454
     */
455
    public function getType() : string
456
    {
457
        return $this->info['type'];
458
    }
459
    
460
    public function getTypeLabel() : string
461
    {
462
        if(!isset(self::$typeLabels))
463
        {
464
            self::$typeLabels = array(
465
                self::TYPE_EMAIL => t('Email'),
466
                self::TYPE_FRAGMENT => t('Jump mark'),
467
                self::TYPE_PHONE => t('Phone number'),
468
                self::TYPE_URL => t('URL'),
469
            );
470
        }
471
        
472
        $type = $this->getType();
473
        
474
        if(!isset(self::$typeLabels[$type]))
475
        {
476
            throw new BaseException(
477
                sprintf('Unknown URL type label for type [%s].', $type),
478
                null,
479
                self::ERROR_UNKNOWN_TYPE_FOR_LABEL
480
            );
481
        }
482
        
483
        return self::$typeLabels[$this->getType()];
484
    }
485
486
   /**
487
    * Whether excluded parameters should be highlighted in
488
    * a different color in the URL when using the
489
    * {@link URLInfo::getHighlighted()} method.
490
    *
491
    * @param bool $highlight
492
    * @return URLInfo
493
    */
494
    public function setHighlightExcluded(bool $highlight=true) : URLInfo
495
    {
496
        $this->highlightExcluded = $highlight;
497
        return $this;
498
    }
499
    
500
   /**
501
    * Returns an array with all relevant URL information.
502
    * 
503
    * @return array
504
    */
505
    public function toArray() : array
506
    {
507
        return array(
508
            'hasParams' => $this->hasParams(),
509
            'params' => $this->getParams(),
510
            'type' => $this->getType(),
511
            'typeLabel' => $this->getTypeLabel(),
512
            'normalized' => $this->getNormalized(),
513
            'highlighted' => $this->getHighlighted(),
514
            'hash' => $this->getHash(),
515
            'host' => $this->getHost(),
516
            'isValid' => $this->isValid(),
517
            'isURL' => $this->isURL(),
518
            'isEmail' => $this->isEmail(),
519
            'isAnchor' => $this->isAnchor(),
520
            'isPhoneNumber' => $this->isPhoneNumber(),
521
            'errorMessage' => $this->getErrorMessage(),
522
            'errorCode' => $this->getErrorCode(),
523
            'excludedParams' => array_keys($this->excludedParams)
524
        );
525
    }
526
    
527
    /**
528
     * Enable or disable parameter exclusion: if any parameters
529
     * to exclude have been added, this allows switching between
530
     * both modes. When enabled, methods like getNormalized or
531
     * getHighlighted will exclude any parameters to exclude. When
532
     * disabled, it will act as usual.
533
     *
534
     * This allows adding parameters to exclude, but still have
535
     * access to the original URLs.
536
     *
537
     * @param bool $enabled
538
     * @return URLInfo
539
     * @see URLInfo::isParamExclusionEnabled()
540
     * @see URLInfo::setHighlightExcluded()
541
     */
542
    public function setParamExclusion(bool $enabled=true) : URLInfo
543
    {
544
        $this->paramExclusion = $enabled;
545
        return $this;
546
    }
547
    
548
   /**
549
    * Whether the parameter exclusion mode is enabled:
550
    * In this case, if any parameters have been added to the
551
    * exclusion list, all relevant methods will exclude these.
552
    *
553
    * @return bool
554
    */
555
    public function isParamExclusionEnabled() : bool
556
    {
557
        return $this->paramExclusion;
558
    }
559
    
560
   /**
561
    * Checks whether the link contains any parameters that
562
    * are on the list of excluded parameters.
563
    *
564
    * @return bool
565
    */
566
    public function containsExcludedParams() : bool
567
    {
568
        if(empty($this->excludedParams)) {
569
            return false;
570
        }
571
        
572
        $names = array_keys($this->info['params']);
573
        foreach($names as $name) {
574
            if(isset($this->excludedParams[$name])) {
575
                return true;
576
            }
577
        }
578
        
579
        return false;
580
    }
581
    
582
    public function hasParam(string $name) : bool
583
    {
584
        $names = $this->getParamNames();
585
        return in_array($name, $names);
586
    }
587
588
    public function offsetSet($offset, $value) 
589
    {
590
        if(in_array($offset, $this->infoKeys)) {
591
            $this->info[$offset] = $value;
592
        }
593
    }
594
    
595
    public function offsetExists($offset) 
596
    {
597
        return isset($this->info[$offset]);
598
    }
599
    
600
    public function offsetUnset($offset) 
601
    {
602
        unset($this->info[$offset]);
603
    }
604
    
605
    public function offsetGet($offset) 
606
    {
607
        if($offset === 'port') {
608
            return $this->getPort();
609
        }
610
        
611
        if(in_array($offset, $this->infoKeys)) {
612
            return $this->getInfoKey($offset);
613
        }
614
        
615
        return '';
616
    }
617
    
618
    public static function getHighlightCSS() : string
619
    {
620
        $cssFolder = realpath(__DIR__.'/../css');
621
        
622
        if($cssFolder === false) {
623
            throw new BaseException(
624
                'Cannot find package CSS folder.',
625
                null,
626
                self::ERROR_CANNOT_FIND_CSS_FOLDER
627
            );
628
        }
629
        
630
        return FileHelper::readContents($cssFolder.'/urlinfo-highlight.css');
631
    }
632
    
633
    public function getExcludedParams() : array
634
    {
635
        return $this->excludedParams;
636
    }
637
    
638
    public function isHighlightExcludeEnabled() : bool
639
    {
640
        return $this->highlightExcluded;
641
    }
642
}
643