Passed
Push — master ( 2d6f8b...2b9668 )
by Sebastian
08:32 queued 11s
created

URLInfo   F

Complexity

Total Complexity 120

Size/Duplication

Total Lines 959
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 335
c 5
b 0
f 0
dl 0
loc 959
rs 2
wmc 120

55 Methods

Rating   Name   Duplication   Size   Complexity  
A getPassword() 0 3 1
A getErrorCode() 0 7 2
A getQuery() 0 3 1
A getPath() 0 3 1
D getHighlighted() 0 143 15
A setError() 0 5 1
A hasQuery() 0 3 1
A getHash() 0 3 1
A hasParams() 0 4 1
A isAnchor() 0 3 1
A hasPassword() 0 3 1
A getInfoKey() 0 7 2
A isValid() 0 3 1
A getParamNames() 0 4 1
A isPhoneNumber() 0 3 1
A getFragment() 0 3 1
A detectPhoneLink() 0 8 3
A isSecure() 0 7 3
B parse() 0 73 9
A hasScheme() 0 3 1
A hasHost() 0 3 1
A getScheme() 0 3 1
A isEmail() 0 3 1
A hasUsername() 0 3 1
A filterParsed() 0 15 5
A getUsername() 0 3 1
A detectEmail() 0 15 5
A countParams() 0 4 1
A getHost() 0 3 1
A detectFragmentLink() 0 8 3
A isURL() 0 4 1
A filterURL() 0 20 1
A __construct() 0 6 1
A getParams() 0 14 5
B getNormalized() 0 34 8
A hasPort() 0 3 1
A getPort() 0 8 2
A hasPath() 0 3 1
A getErrorMessage() 0 7 2
A hasFragment() 0 3 1
A getTypeLabel() 0 24 3
A toArray() 0 19 1
A offsetSet() 0 4 2
A setHighlightExcluded() 0 4 1
A offsetExists() 0 3 1
A setParamExclusion() 0 4 1
A getHighlightCSS() 0 13 2
A offsetGet() 0 11 3
A hasParam() 0 4 1
A containsExcludedParams() 0 14 4
A offsetUnset() 0 3 1
A excludeParam() 0 9 2
A getParam() 0 7 2
A isParamExclusionEnabled() 0 3 1
A getType() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like URLInfo often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use URLInfo, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * File containing the {@see AppUtils\URLInfo} class.
5
 * 
6
 * @package Application Utils
7
 * @subpackage URLInfo
8
 * @see AppUtils\URLInfo
9
 */
10
11
declare(strict_types=1);
12
13
namespace AppUtils;
14
15
/**
16
 * Replacement for PHP's native `parse_url` function, which
17
 * handles some common pitfalls and issues that are hard to 
18
 * follow, as well as adding a number of utility methods.
19
 * 
20
 * @package Application Utils
21
 * @subpackage URLInfo
22
 * @author Sebastian Mordziol <[email protected]>
23
 */
24
class URLInfo implements \ArrayAccess
25
{
26
    const ERROR_MISSING_SCHEME = 42101;
27
    
28
    const ERROR_INVALID_SCHEME = 42102;
29
30
    const ERROR_MISSING_HOST = 42103;
31
    
32
    const ERROR_CANNOT_FIND_CSS_FOLDER = 42104;
33
    
34
    const ERROR_UNKNOWN_TYPE_FOR_LABEL = 42105;
35
    
36
    const TYPE_EMAIL = 'email';
37
    const TYPE_FRAGMENT = 'fragment';
38
    const TYPE_PHONE = 'phone';
39
    const TYPE_URL = 'url';
40
    
41
   /**
42
    * The original URL that was passed to the constructor.
43
    * @var string
44
    */
45
    protected $rawURL;
46
47
   /**
48
    * @var array
49
    */
50
    protected $info;
51
    
52
   /**
53
    * @var bool
54
    */
55
    protected $isEmail = false;
56
    
57
   /**
58
    * @var bool
59
    */
60
    protected $isFragment = false;
61
    
62
   /**
63
    * @var bool
64
    */
65
    protected $isValid = true;
66
    
67
   /**
68
    * @var bool
69
    */
70
    protected $isPhone = false;
71
    
72
   /**
73
    * @var array
74
    */
75
    protected $knownSchemes = array(
76
        'ftp',
77
        'http',
78
        'https',
79
        'mailto',
80
        'tel',
81
        'data',
82
        'file'
83
    );
84
85
   /**
86
    * @var array
87
    */
88
    protected $error;
89
    
90
   /**
91
    * @var array
92
    */
93
    protected $params = array();
94
    
95
   /**
96
    * @var string[]
97
    */
98
    protected $excludedParams = array();
99
    
100
   /**
101
    * @var bool
102
    * @see URLInfo::setParamExclusion()
103
    */
104
    protected $paramExclusion = false;
105
    
106
   /**
107
    * @var array
108
    * @see URLInfo::getTypeLabel()
109
    */
110
    protected static $typeLabels;
111
    
112
   /**
113
    * @var bool
114
    */
115
    protected $highlightExcluded = false;
116
    
117
   /**
118
    * @var array
119
    */
120
    protected $infoKeys = array(
121
        'scheme',
122
        'host',
123
        'port',
124
        'user',
125
        'pass',
126
        'path',
127
        'query',
128
        'fragment'
129
    );
130
    
131
   /**
132
    * @var string
133
    */
134
    protected $url;
135
    
136
    public function __construct(string $url)
137
    {
138
        $this->rawURL = $url;
139
        $this->url = self::filterURL($url);
140
        
141
        $this->parse();
142
    }
143
    
144
    protected function parse()
145
    {
146
        // fix for parsing unicode characters in URLs:
147
        // this is dependent on the machine's locale,
148
        // so to ensure this works we temporarily change
149
        // it to the always available US UTF8 locale.
150
        $prev = setlocale(LC_CTYPE, 'en_US.UTF-8');
151
        
152
        $this->info = parse_url($this->url);
153
        
154
        // restore the previous locale
155
        setlocale(LC_CTYPE, $prev);
156
157
        $this->filterParsed();
158
        
159
        if($this->detectEmail()) {
160
            return;
161
        }
162
        
163
        if($this->detectFragmentLink()) {
164
            return;
165
        }
166
        
167
        if($this->detectPhoneLink()) {
168
            return;
169
        }
170
        
171
        if(!$this->isValid) {
172
            return;
173
        }
174
        
175
        // no scheme found: it may be an email address without the mailto:
176
        // It can't be a variable, since without the scheme it would already
177
        // have been recognized as a vaiable only link.
178
        if(!isset($this->info['scheme'])) {
179
            $this->setError(
180
                self::ERROR_MISSING_SCHEME,
181
                t('Cannot determine the link\'s scheme, e.g. %1$s.', 'http')
182
            );
183
            $this->isValid = false;
184
            return;
185
        }
186
        
187
        if(!in_array($this->info['scheme'], $this->knownSchemes)) {
188
            $this->setError(
189
                self::ERROR_INVALID_SCHEME,
190
                t('The scheme %1$s is not supported for links.', $this->info['scheme']) . ' ' .
191
                t('Valid schemes are: %1$s.', implode(', ', $this->knownSchemes))
192
            );
193
            $this->isValid = false;
194
            return;
195
        }
196
        
197
        // every link needs a host. This case can happen for ex, if
198
        // the link starts with a typo with only one slash, like:
199
        // "http:/hostname"
200
        if(!isset($this->info['host'])) {
201
            $this->setError(
202
                self::ERROR_MISSING_HOST,
203
                t('Cannot determine the link\'s host name.') . ' ' .
204
                t('This usually happens when there\'s a typo somewhere.')
205
            );
206
            $this->isValid = false;
207
            return;
208
        }
209
210
        if(!empty($this->info['query'])) 
211
        {
212
            $this->params = \AppUtils\ConvertHelper::parseQueryString($this->info['query']);
213
            ksort($this->params);
214
        }
215
        
216
        $this->isValid = true;
217
    }
218
    
219
   /**
220
    * Filters an URL: removes control characters and the
221
    * like to have a clean URL to work with.
222
    * 
223
    * @param string $url
224
    * @return string
225
    */
226
    public static function filterURL(string $url)
227
    {
228
        // fix ampersands if it comes from HTML
229
        $url = str_replace('&amp;', '&', $url);
230
        
231
        // we remove any control characters from the URL, since these
232
        // may be copied when copy+pasting from word or pdf documents
233
        // for example.
234
        $url = \AppUtils\ConvertHelper::stripControlCharacters($url);
235
        
236
        // fix the pesky unicode hyphen that looks like a regular hyphen,
237
        // but isn't and can cause all sorts of problems
238
        $url = str_replace('‐', '-', $url);
239
        
240
        // remove newlines and tabs
241
        $url = str_replace(array("\n", "\r", "\t"), '', $url);
242
        
243
        $url = trim($url);
244
        
245
        return $url;
246
    }
247
    
248
   /**
249
    * Goes through all information in the parse_url result
250
    * array, and attempts to fix any user errors in formatting
251
    * that can be recovered from, mostly regarding stray spaces.
252
    */
253
    protected function filterParsed()
254
    {
255
        foreach($this->info as $key => $val)
256
        {
257
            if(is_string($val)) {
258
                $this->info[$key] = trim($val);
259
            }
260
        }
261
        
262
        if(isset($this->info['host'])) {
263
            $this->info['host'] = str_replace(' ', '', $this->info['host']);
264
        }
265
        
266
        if(isset($this->info['path'])) {
267
            $this->info['path'] = str_replace(' ', '', $this->info['path']);
268
        }
269
    }
270
    
271
    protected function detectEmail()
272
    {
273
        if(isset($this->info['scheme']) && $this->info['scheme'] == 'mailto') {
274
            $this->isEmail = true;
275
            return true;
276
        }
277
        
278
        if(isset($this->info['path']) && preg_match(\AppUtils\RegexHelper::REGEX_EMAIL, $this->info['path'])) 
279
        {
280
            $this->info['scheme'] = 'email';
281
            $this->isEmail = true;
282
            return true;
283
        }
284
        
285
        return false;
286
    }
287
    
288
    protected function detectFragmentLink()
289
    {
290
        if(isset($this->info['fragment']) && !isset($this->info['scheme'])) {
291
            $this->isFragment = true;
292
            return true;
293
        }
294
        
295
        return false;
296
    }
297
    
298
    protected function detectPhoneLink()
299
    {
300
        if(isset($this->info['scheme']) && $this->info['scheme'] == 'tel') {
301
            $this->isPhone = true;
302
            return true;
303
        }
304
        
305
        return false;
306
    }
307
    
308
    /**
309
     * Checks if it is an https link.
310
     * @return boolean
311
     */
312
    public function isSecure()
313
    {
314
        if(isset($this->info['scheme']) && $this->info['scheme']=='https') {
315
            return true;
316
        }
317
        
318
        return false;
319
    }
320
    
321
    public function isAnchor() : bool
322
    {
323
        return $this->isFragment;
324
    }
325
    
326
    public function isEmail() : bool
327
    {
328
        return $this->isEmail;
329
    }
330
    
331
    public function isPhoneNumber() : bool
332
    {
333
        return $this->isPhone;
334
    }
335
    
336
   /**
337
    * Whether the URL is a regular URL, not one of the 
338
    * other types like a phone number or email address.
339
    * 
340
    * @return bool
341
    */
342
    public function isURL() : bool
343
    {
344
        $host = $this->getHost();
345
        return !empty($host);
346
    }
347
    
348
    public function isValid()
349
    {
350
        return $this->isValid;
351
    }
352
    
353
   /**
354
    * Retrieves the host name, or an empty string if none is present.
355
    * 
356
    * @return string
357
    */
358
    public function getHost() : string
359
    {
360
        return $this->getInfoKey('host');
361
    }
362
    
363
   /**
364
    * Retrieves the path, or an empty string if none is present.
365
    * @return string
366
    */
367
    public function getPath() : string
368
    {
369
        return $this->getInfoKey('path');
370
    }
371
    
372
    public function getFragment() : string
373
    {
374
        return $this->getInfoKey('fragment');
375
    }
376
    
377
    public function getScheme() : string
378
    {
379
        return $this->getInfoKey('scheme');
380
    }
381
    
382
   /**
383
    * Retrieves the port specified in the URL, or -1 if none is preseent.
384
    * @return int
385
    */
386
    public function getPort() : int
387
    {
388
        $port = $this->getInfoKey('port');
389
        if(!empty($port)) {
390
            return (int)$port;
391
        }
392
        
393
        return -1;
394
    }
395
    
396
   /**
397
    * Retrieves the raw query string, or an empty string if none is present.
398
    * 
399
    * @return string
400
    * 
401
    * @see URLInfo::getParams()
402
    */
403
    public function getQuery() : string
404
    {
405
        return $this->getInfoKey('query');
406
    }
407
    
408
    public function getUsername() : string
409
    {
410
        return $this->getInfoKey('user');
411
    }
412
    
413
    public function getPassword() : string
414
    {
415
        return $this->getInfoKey('pass');
416
    }
417
    
418
   /**
419
    * Whether the URL contains a port number.
420
    * @return bool
421
    */
422
    public function hasPort() : bool
423
    {
424
        return $this->getPort() !== -1;
425
    }
426
    
427
   /**
428
    * Alias for the hasParams() method.
429
    * @return bool
430
    * @see URLInfo::hasParams()
431
    */
432
    public function hasQuery() : bool
433
    {
434
        return $this->hasParams();
435
    }
436
    
437
    public function hasHost() : bool
438
    {
439
        return $this->getHost() !== ''; 
440
    }
441
    
442
    public function hasPath() : bool
443
    {
444
        return $this->getPath() !== '';
445
    }
446
    
447
    public function hasFragment() : bool
448
    {
449
        return $this->getFragment() !== '';
450
    }
451
    
452
    public function hasUsername() : bool
453
    {
454
        return $this->getUsername() !== '';
455
    }
456
    
457
    public function hasPassword() : bool
458
    {
459
        return $this->getPassword() !== '';
460
    }
461
    
462
    public function hasScheme() : bool
463
    {
464
        return $this->getScheme() !== '';
465
    }
466
    
467
    protected function getInfoKey(string $name) : string
468
    {
469
        if(isset($this->info[$name])) {
470
            return (string)$this->info[$name];
471
        }
472
        
473
        return '';
474
    }
475
    
476
    public function getNormalized() : string
477
    {
478
        if(!$this->isValid) {
479
            return '';
480
        }
481
        
482
        if($this->isFragment === true)
483
        {
484
            return '#'.$this->getFragment();
485
        }
486
        else if($this->isPhone === true)
487
        {
488
            return 'tel://'.$this->getHost();
489
        }
490
        else if($this->isEmail === true)
491
        {
492
            return 'mailto:'.$this->getPath();
493
        }
494
        
495
        $normalized = $this->info['scheme'].'://'.$this->info['host'];
496
        if(isset($this->info['path'])) {
497
            $normalized .= $this->info['path'];
498
        }
499
        
500
        $params = $this->getParams();
501
        if(!empty($params)) {
502
            $normalized .= '?'.http_build_query($params);
503
        }
504
        
505
        if(isset($this->info['fragment'])) {
506
            $normalized .= '#'.$this->info['fragment'];
507
        }
508
        
509
        return $normalized;
510
    }
511
    
512
   /**
513
    * Creates a hash of the URL, which can be used for comparisons.
514
    * Since any parameters in the URL's query are sorted alphabetically,
515
    * the same links with a different parameter order will have the 
516
    * same hash.
517
    * 
518
    * @return string
519
    */
520
    public function getHash()
521
    {
522
        return \AppUtils\ConvertHelper::string2shortHash($this->getNormalized());
523
    }
524
525
   /**
526
    * Highlights the URL using HTML tags with specific highlighting
527
    * class names.
528
    * 
529
    * @return string Will return an empty string if the URL is not valid.
530
    */
531
    public function getHighlighted() : string
532
    {
533
        if(!$this->isValid) {
534
            return '';
535
        }
536
        
537
        if($this->isEmail) {
538
            return sprintf(
539
                '<span class="link-scheme scheme-mailto">mailto:</span>'.
540
                '<span class="link-host">%s</span>',
541
                $this->info['path']
542
            );
543
        }
544
        
545
        if($this->isFragment) {
546
            return sprintf(
547
                '<span class="link-fragment-sign">#</span>'.
548
                '<span class="link-fragment-value">%s</span>',
549
                $this->info['fragment']
550
            );
551
        }
552
        
553
        $result = '';
554
        
555
        if($this->hasScheme())
556
        {
557
            $result = sprintf(
558
                '<span class="link-scheme scheme-%1$s">'.
559
                    '%1$s:'.
560
                '</span>',
561
                $this->getScheme()
562
            );
563
        }
564
565
        $result .= '<span class="link-component double-slashes">//</span>';
566
        
567
        if($this->hasUsername())
568
        {
569
            $result .= sprintf(
570
                '<span class="link-credentials">%s</span>'.
571
                '<span class="link-component credentials-separator">:</span>'.
572
                '<span class="link-credentials">%s</span>'.
573
                '<span class="link-component credentials-at">@</span>',
574
                $this->getUsername(),
575
                $this->getPassword()
576
            );
577
        }
578
        
579
        if($this->hasHost()) 
580
        {
581
            $result .=
582
            sprintf(
583
                '<span class="link-host">%s</span><wbr>',
584
                $this->getHost()
585
            );
586
        }
587
        
588
        if($this->hasPort()) 
589
        {
590
            $result .= sprintf(
591
                '<span class="link-component port-separator">:</span>'.
592
                '<span class="link-port">%s</span>',
593
                $this->getPort()
594
            );
595
        }
596
        
597
        if($this->hasPath()) 
598
        {
599
            $path = str_replace(array(';', '='), array(';<wbr>', '=<wbr>'), $this->getPath());
600
            $tokens = explode('/', $path);
601
            $path = implode('<span class="link-component path-separator">/</span><wbr>', $tokens);
602
            $result .= sprintf(
603
                '<span class="link-path">%s</span><wbr>',
604
                $path
605
            );
606
        }
607
        
608
        if(!empty($this->params))
609
        {
610
            $tokens = array();
611
            
612
            foreach($this->params as $param => $value)
613
            {
614
                $parts = sprintf(
615
                    '<span class="link-param-name">%s</span>'.
616
                    '<span class="link-component param-equals">=</span>'.
617
                    '<span class="link-param-value">%s</span>'.
618
                    '<wbr>',
619
                    $param,
620
                    str_replace(
621
                        array(':', '.', '-', '_'),
622
                        array(':<wbr>', '.<wbr>', '-<wbr>', '_<wbr>'),
623
                        $value
624
                    )
625
                );
626
                
627
                $tag = '';
628
                
629
                // is parameter exclusion enabled, and is this an excluded parameter?
630
                if($this->paramExclusion && isset($this->excludedParams[$param]))
631
                {
632
                    // display the excluded parameter, but highlight it
633
                    if($this->highlightExcluded)
634
                    {
635
                        $tooltip = $this->excludedParams[$param];
636
                        
637
                        $tag = sprintf(
638
                            '<span class="link-param excluded-param" title="%s" data-toggle="tooltip">%s</span>',
639
                            $tooltip,
640
                            $parts
641
                        );
642
                    }
643
                    else
644
                    {
645
                        continue;
646
                    }
647
                }
648
                else
649
                {
650
                    $tag = sprintf(
651
                        '<span class="link-param">%s</span>',
652
                        $parts
653
                    );
654
                }
655
                
656
                $tokens[] = $tag;
657
            }
658
            
659
            $result .=
660
            '<span class="link-component query-sign">?</span>'.implode('<span class="link-component param-separator">&amp;</span>', $tokens);
661
        }
662
        
663
        if(isset($this->info['fragment'])) {
664
            $result .= sprintf(
665
                '<span class="link-fragment-sign">#</span>'.
666
                '<span class="link-fragment">%s</span>',
667
                $this->info['fragment']
668
            );
669
        }
670
        
671
        $result = '<span class="link">'.$result.'</span>';
672
        
673
        return $result;
674
    }
675
    
676
    protected function setError(int $code, string $message)
677
    {
678
        $this->error = array(
679
            'code' => $code,
680
            'message' => $message
681
        );
682
    }
683
    
684
    public function getErrorMessage() : string
685
    {
686
        if(isset($this->error)) {
687
            return $this->error['message'];
688
        }
689
        
690
        return '';
691
    }
692
    
693
    public function getErrorCode() : int
694
    {
695
        if(isset($this->error)) {
696
            return $this->error['code'];
697
        }
698
        
699
        return -1;
700
    }
701
    
702
    public function hasParams() : bool
703
    {
704
        $params = $this->getParams();
705
        return !empty($params);
706
    }
707
    
708
    public function countParams() : int
709
    {
710
        $params = $this->getParams();
711
        return count($params);
712
    }
713
    
714
   /**
715
    * Retrieves all parameters specified in the url,
716
    * if any, as an associative array. 
717
    * 
718
    * NOTE: Ignores parameters that have been added
719
    * to the excluded parameters list.
720
    *
721
    * @return array
722
    */
723
    public function getParams() : array
724
    {
725
        if(!$this->paramExclusion || empty($this->excludedParams)) {
726
            return $this->params;
727
        }
728
        
729
        $keep = array();
730
        foreach($this->params as $name => $value) {
731
            if(!isset($this->excludedParams[$name])) {
732
                $keep[$name] = $value;
733
            }
734
        }
735
        
736
        return $keep;
737
    }
738
    
739
   /**
740
    * Retrieves the names of all parameters present in the URL, if any.
741
    * @return string[]
742
    */
743
    public function getParamNames() : array
744
    {
745
        $params = $this->getParams();
746
        return array_keys($params);
747
    }
748
    
749
   /**
750
    * Retrieves a specific parameter value from the URL.
751
    * 
752
    * @param string $name
753
    * @return string The parameter value, or an empty string if it does not exist.
754
    */
755
    public function getParam(string $name) : string
756
    {
757
        if(isset($this->params[$name])) {
758
            return $this->params[$name];
759
        }
760
        
761
        return '';
762
    }
763
    
764
   /**
765
    * Excludes an URL parameter entirely if present:
766
    * the parser will act as if the parameter was not
767
    * even present in the source URL, effectively
768
    * stripping it.
769
    *
770
    * @param string $name
771
    * @param string $reason A human readable explanation why this is excluded - used when highlighting links.
772
    * @return URLInfo
773
    */
774
    public function excludeParam(string $name, string $reason) : URLInfo
775
    {
776
        if(!isset($this->excludedParams[$name]))
777
        {
778
            $this->excludedParams[$name] = $reason;
779
            $this->setParamExclusion();
780
        }
781
        
782
        return $this;
783
    }
784
785
    /**
786
     * Retrieves a string identifier of the type of URL that was detected.
787
     *
788
     * @return string
789
     *
790
     * @see URLInfo::TYPE_EMAIL
791
     * @see URLInfo::TYPE_FRAGMENT
792
     * @see URLInfo::TYPE_PHONE
793
     * @see URLInfo::TYPE_URL
794
     */
795
    public function getType() : string
796
    {
797
        if($this->isEmail) {
798
            return self::TYPE_EMAIL;
799
        }
800
        
801
        if($this->isFragment) {
802
            return self::TYPE_FRAGMENT;
803
        }
804
        
805
        if($this->isPhone) {
806
            return self::TYPE_PHONE;
807
        }
808
        
809
        return self::TYPE_URL;
810
    }
811
    
812
    public function getTypeLabel() : string
813
    {
814
        if(!isset(self::$typeLabels))
815
        {
816
            self::$typeLabels = array(
817
                self::TYPE_EMAIL => t('Email'),
818
                self::TYPE_FRAGMENT => t('Jump mark'),
819
                self::TYPE_PHONE => t('Phone number'),
820
                self::TYPE_URL => t('URL'),
821
            );
822
        }
823
        
824
        $type = $this->getType();
825
        
826
        if(!isset(self::$typeLabels[$type]))
827
        {
828
            throw new BaseException(
829
                sprintf('Unknown URL type label for type [%s].', $type),
830
                null,
831
                self::ERROR_UNKNOWN_TYPE_FOR_LABEL
832
            );
833
        }
834
        
835
        return self::$typeLabels[$this->getType()];
836
    }
837
838
   /**
839
    * Whether excluded parameters should be highlighted in
840
    * a different color in the URL when using the
841
    * {@link URLInfo::getHighlighted()} method.
842
    *
843
    * @param bool $highlight
844
    * @return URLInfo
845
    */
846
    public function setHighlightExcluded(bool $highlight=true) : URLInfo
847
    {
848
        $this->highlightExcluded = $highlight;
849
        return $this;
850
    }
851
    
852
   /**
853
    * Returns an array with all relevant URL information.
854
    * 
855
    * @return array
856
    */
857
    public function toArray() : array
858
    {
859
        return array(
860
            'hasParams' => $this->hasParams(),
861
            'params' => $this->getParams(),
862
            'type' => $this->getType(),
863
            'typeLabel' => $this->getTypeLabel(),
864
            'normalized' => $this->getNormalized(),
865
            'highlighted' => $this->getHighlighted(),
866
            'hash' => $this->getHash(),
867
            'host' => $this->getHost(),
868
            'isValid' => $this->isValid(),
869
            'isURL' => $this->isURL(),
870
            'isEmail' => $this->isEmail(),
871
            'isAnchor' => $this->isAnchor(),
872
            'isPhoneNumber' => $this->isPhoneNumber(),
873
            'errorMessage' => $this->getErrorMessage(),
874
            'errorCode' => $this->getErrorCode(),
875
            'excludedParams' => array_keys($this->excludedParams)
876
        );
877
    }
878
    
879
    /**
880
     * Enable or disable parameter exclusion: if any parameters
881
     * to exclude have been added, this allows switching between
882
     * both modes. When enabled, methods like getNormalized or
883
     * getHighlighted will exclude any parameters to exclude. When
884
     * disabled, it will act as usual.
885
     *
886
     * This allows adding parameters to exclude, but still have
887
     * access to the original URLs.
888
     *
889
     * @param bool $enabled
890
     * @return URLInfo
891
     * @see URLInfo::isParamExclusionEnabled()
892
     * @see URLInfo::setHighlightExcluded()
893
     */
894
    public function setParamExclusion(bool $enabled=true) : URLInfo
895
    {
896
        $this->paramExclusion = $enabled;
897
        return $this;
898
    }
899
    
900
   /**
901
    * Whether the parameter exclusion mode is enabled:
902
    * In this case, if any parameters have been added to the
903
    * exclusion list, all relevant methods will exclude these.
904
    *
905
    * @return bool
906
    */
907
    public function isParamExclusionEnabled() : bool
908
    {
909
        return $this->paramExclusion;
910
    }
911
    
912
   /**
913
    * Checks whether the link contains any parameters that
914
    * are on the list of excluded parameters.
915
    *
916
    * @return bool
917
    */
918
    public function containsExcludedParams() : bool
919
    {
920
        if(empty($this->excludedParams)) {
921
            return false;
922
        }
923
        
924
        $names = array_keys($this->params);
925
        foreach($names as $name) {
926
            if(isset($this->excludedParams[$name])) {
927
                return true;
928
            }
929
        }
930
        
931
        return false;
932
    }
933
    
934
    public function hasParam(string $name) : bool
935
    {
936
        $names = $this->getParamNames();
937
        return in_array($name, $names);
938
    }
939
940
    public function offsetSet($offset, $value) 
941
    {
942
        if(in_array($offset, $this->infoKeys)) {
943
            $this->info[$offset] = $value;
944
        }
945
    }
946
    
947
    public function offsetExists($offset) 
948
    {
949
        return isset($this->info[$offset]);
950
    }
951
    
952
    public function offsetUnset($offset) 
953
    {
954
        unset($this->info[$offset]);
955
    }
956
    
957
    public function offsetGet($offset) 
958
    {
959
        if($offset === 'port') {
960
            return $this->getPort();
961
        }
962
        
963
        if(in_array($offset, $this->infoKeys)) {
964
            return $this->getInfoKey($offset);
965
        }
966
        
967
        return '';
968
    }
969
    
970
    public static function getHighlightCSS() : string
971
    {
972
        $cssFolder = realpath(__DIR__.'/../css');
973
        
974
        if($cssFolder === false) {
975
            throw new BaseException(
976
                'Cannot find package CSS folder.',
977
                null,
978
                self::ERROR_CANNOT_FIND_CSS_FOLDER
979
            );
980
        }
981
        
982
        return FileHelper::readContents($cssFolder.'/urlinfo-highlight.css');
983
    }
984
}
985