URLInfo::hasUsername()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
use AppUtils\URLInfo\URIConnectionTester;
15
use AppUtils\URLInfo\URIFilter;
16
use AppUtils\URLInfo\URINormalizer;
17
use AppUtils\URLInfo\URIParser;
18
use AppUtils\URLInfo\URLException;
19
use ArrayAccess;
20
21
/**
22
 * Replacement for PHP's native `parse_url` function, which
23
 * handles some common pitfalls and issues that are hard to 
24
 * follow, as well as adding a number of utility methods.
25
 * 
26
 * @package Application Utils
27
 * @subpackage URLInfo
28
 * @author Sebastian Mordziol <[email protected]>
29
 * @implements ArrayAccess<string,mixed>
30
 */
31
class URLInfo implements ArrayAccess
32
{
33
    public const ERROR_MISSING_SCHEME = 42101;
34
    public const ERROR_INVALID_SCHEME = 42102;
35
    public const ERROR_MISSING_HOST = 42103;
36
    public const ERROR_CANNOT_FIND_CSS_FOLDER = 42104;
37
    public const ERROR_UNKNOWN_TYPE_FOR_LABEL = 42105;
38
    public const ERROR_CURL_INIT_FAILED = 42106;
39
    public const ERROR_UNKNOWN_TYPE = 42107;
40
    
41
    public const TYPE_EMAIL = 'email';
42
    public const TYPE_FRAGMENT = 'fragment';
43
    public const TYPE_PHONE = 'phone';
44
    public const TYPE_URL = 'url';
45
    public const TYPE_NONE = 'none';
46
47
   /**
48
    * The original URL that was passed to the constructor.
49
    * @var string
50
    */
51
    protected string $rawURL;
52
53
   /**
54
    * @var array<string,mixed>
55
    */
56
    protected array $info;
57
    
58
   /**
59
    * @var string[]
60
    */
61
    protected array $excludedParams = array();
62
    
63
   /**
64
    * @var bool
65
    * @see URLInfo::setParamExclusion()
66
    */
67
    protected bool $paramExclusion = false;
68
    
69
   /**
70
    * @var array<string,string>|NULL
71
    * @see URLInfo::getTypeLabel()
72
    */
73
    protected static ?array $typeLabels = null;
74
    
75
   /**
76
    * @var bool
77
    */
78
    protected bool $highlightExcluded = false;
79
    
80
   /**
81
    * @var string[]
82
    */
83
    protected array $infoKeys = array(
84
        'scheme',
85
        'host',
86
        'port',
87
        'user',
88
        'pass',
89
        'path',
90
        'query',
91
        'fragment'
92
    );
93
    
94
   /**
95
    * @var string
96
    */
97
    protected string $url;
98
    
99
   /**
100
    * @var URIParser
101
    */
102
    protected URIParser $parser;
103
    
104
   /**
105
    * @var URINormalizer|NULL
106
    */
107
    protected ?URINormalizer $normalizer = null;
108
    
109
   /**
110
    * @var bool
111
    */
112
    protected bool $encodeUTFChars = false;
113
    
114
    public function __construct(string $url)
115
    {
116
        $this->rawURL = $url;
117
        $this->url = self::filterURL($url);
118
        
119
        $this->parse();
120
    }
121
    
122
    protected function parse() : void
123
    {
124
        $this->parser = new URIParser($this->url, $this->encodeUTFChars);
125
        $this->info = $this->parser->getInfo();
126
    }
127
128
    public function getParser(): URIParser
129
    {
130
        return $this->parser;
131
    }
132
133
   /**
134
    * Whether to URL encode any non-encoded UTF8 characters in the URL.
135
    * Default is to leave them as-is for better readability, since 
136
    * browsers handle this well.
137
    * 
138
    * @param bool $enabled
139
    * @return URLInfo
140
    */
141
    public function setUTFEncoding(bool $enabled=true) : URLInfo
142
    {
143
        if($this->encodeUTFChars !== $enabled)
144
        {
145
            $this->encodeUTFChars = $enabled;
146
            $this->parse(); // reparse the URL to apply the changes
147
        }
148
        
149
        return $this;
150
    }
151
    
152
    public function isUTFEncodingEnabled() : bool
153
    {
154
        return $this->encodeUTFChars;
155
    }
156
    
157
   /**
158
    * Filters a URL: removes control characters and the
159
    * like to have a clean URL to work with.
160
    * 
161
    * @param string $url
162
    * @return string
163
    */
164
    public static function filterURL(string $url) : string
165
    {
166
        return URIFilter::filter($url);
167
    }
168
    
169
    /**
170
     * Checks if it is a https link.
171
     * @return boolean
172
     */
173
    public function isSecure() : bool
174
    {
175
        return $this->getScheme() === 'https';
176
    }
177
    
178
    public function isAnchor() : bool
179
    {
180
        return $this->info['type'] === self::TYPE_FRAGMENT;
181
    }
182
    
183
    public function isEmail() : bool
184
    {
185
        return $this->info['type'] === self::TYPE_EMAIL;
186
    }
187
    
188
    public function isPhoneNumber() : bool
189
    {
190
        return $this->info['type'] === self::TYPE_PHONE;
191
    }
192
    
193
   /**
194
    * Whether the URL is a regular URL, not one of the 
195
    * other types like a phone number or email address.
196
    * 
197
    * @return bool
198
    */
199
    public function isURL() : bool
200
    {
201
        $host = $this->getHost();
202
        return !empty($host);
203
    }
204
    
205
    public function isValid() : bool
206
    {
207
        return $this->parser->isValid();
208
    }
209
    
210
   /**
211
    * Retrieves the host name, or an empty string if none is present.
212
    * 
213
    * @return string
214
    */
215
    public function getHost() : string
216
    {
217
        return $this->getInfoKey('host');
218
    }
219
    
220
   /**
221
    * Retrieves the path, or an empty string if none is present.
222
    * @return string
223
    */
224
    public function getPath() : string
225
    {
226
        return $this->getInfoKey('path');
227
    }
228
    
229
    public function getFragment() : string
230
    {
231
        return $this->getInfoKey('fragment');
232
    }
233
    
234
    public function getScheme() : string
235
    {
236
        return $this->getInfoKey('scheme');
237
    }
238
    
239
   /**
240
    * Retrieves the port specified in the URL, or -1 if none is present.
241
    * @return int
242
    */
243
    public function getPort() : int
244
    {
245
        $port = $this->getInfoKey('port');
246
        
247
        if(!empty($port)) {
248
            return (int)$port;
249
        }
250
        
251
        return -1;
252
    }
253
    
254
   /**
255
    * Retrieves the raw query string, or an empty string if none is present.
256
    * 
257
    * @return string
258
    * 
259
    * @see URLInfo::getParams()
260
    */
261
    public function getQuery() : string
262
    {
263
        return $this->getInfoKey('query');
264
    }
265
    
266
    public function getUsername() : string
267
    {
268
        return $this->getInfoKey('user');
269
    }
270
    
271
    public function getPassword() : string
272
    {
273
        return $this->getInfoKey('pass');
274
    }
275
    
276
   /**
277
    * Whether the URL contains a port number.
278
    * @return bool
279
    */
280
    public function hasPort() : bool
281
    {
282
        return $this->getPort() !== -1;
283
    }
284
    
285
   /**
286
    * Alias for the hasParams() method.
287
    * @return bool
288
    * @see URLInfo::hasParams()
289
    */
290
    public function hasQuery() : bool
291
    {
292
        return $this->hasParams();
293
    }
294
    
295
    public function hasHost() : bool
296
    {
297
        return $this->getHost() !== ''; 
298
    }
299
    
300
    public function hasPath() : bool
301
    {
302
        return $this->getPath() !== '';
303
    }
304
    
305
    public function hasFragment() : bool
306
    {
307
        return $this->getFragment() !== '';
308
    }
309
    
310
    public function hasUsername() : bool
311
    {
312
        return $this->getUsername() !== '';
313
    }
314
    
315
    public function hasPassword() : bool
316
    {
317
        return $this->getPassword() !== '';
318
    }
319
    
320
    public function hasScheme() : bool
321
    {
322
        return $this->getScheme() !== '';
323
    }
324
    
325
    protected function getInfoKey(string $name) : string
326
    {
327
        if(isset($this->info[$name])) {
328
            return (string)$this->info[$name];
329
        }
330
        
331
        return '';
332
    }
333
334
   /**
335
    * Retrieves a normalized URL: this ensures that all parameters
336
    * in the URL are always in the same order.
337
    * 
338
    * @return string
339
    */
340
    public function getNormalized() : string
341
    {
342
        return $this->normalize();
343
    }
344
    
345
   /**
346
    * Like getNormalized(), but if a username and password are present
347
    * in the URL, returns the URL without them.
348
    * 
349
    * @return string
350
    */
351
    public function getNormalizedWithoutAuth() : string
352
    {
353
        return $this->normalize(false);
354
    }
355
    
356
    protected function normalize(bool $auth=true) : string
357
    {
358
        if(!$this->isValid()) {
359
            return '';
360
        }
361
        
362
        if(!isset($this->normalizer)) {
363
            $this->normalizer = new URINormalizer($this);
364
        }
365
        
366
        $this->normalizer->enableAuth($auth);
0 ignored issues
show
Bug introduced by
The method enableAuth() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

366
        $this->normalizer->/** @scrutinizer ignore-call */ 
367
                           enableAuth($auth);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
367
        
368
        return $this->normalizer->normalize();
369
    }
370
    
371
   /**
372
    * Creates a hash of the URL, which can be used for comparisons.
373
    * Since any parameters in the URL's query are sorted alphabetically,
374
    * the same links with a different parameter order will have the 
375
    * same hash.
376
    * 
377
    * @return string
378
    */
379
    public function getHash() : string
380
    {
381
        return ConvertHelper::string2shortHash($this->getNormalized());
382
    }
383
384
   /**
385
    * Highlights the URL using HTML tags with specific highlighting
386
    * class names.
387
    * 
388
    * @return string Will return an empty string if the URL is not valid.
389
    */
390
    public function getHighlighted() : string
391
    {
392
        if(!$this->isValid()) {
393
            return '';
394
        }
395
        
396
        return (new URIHighlighter($this))->highlight();
397
    }
398
    
399
    public function getErrorMessage() : string
400
    {
401
        return $this->parser->getErrorMessage();
402
    }
403
    
404
    public function getErrorCode() : int
405
    {
406
        return $this->parser->getErrorCode();
407
    }
408
    
409
    public function hasParams() : bool
410
    {
411
        $params = $this->getParams();
412
        return !empty($params);
413
    }
414
    
415
    public function countParams() : int
416
    {
417
        $params = $this->getParams();
418
        return count($params);
419
    }
420
    
421
   /**
422
    * Retrieves all parameters specified in the url,
423
    * if any, as an associative array. 
424
    * 
425
    * NOTE: Ignores parameters that have been added
426
    * to the excluded parameters list.
427
    *
428
    * @return array<string,string>
429
    */
430
    public function getParams() : array
431
    {
432
        if(!$this->paramExclusion || empty($this->excludedParams)) {
433
            return $this->info['params'];
434
        }
435
        
436
        $keep = array();
437
        foreach($this->info['params'] as $name => $value) 
438
        {
439
            if(!isset($this->excludedParams[$name])) {
440
                $keep[$name] = $value;
441
            }
442
        }
443
        
444
        return $keep;
445
    }
446
    
447
   /**
448
    * Retrieves the names of all parameters present in the URL, if any.
449
    * @return string[]
450
    */
451
    public function getParamNames() : array
452
    {
453
        $params = $this->getParams();
454
        return array_keys($params);
455
    }
456
    
457
   /**
458
    * Retrieves a specific parameter value from the URL.
459
    * 
460
    * @param string $name
461
    * @return string The parameter value, or an empty string if it does not exist.
462
    */
463
    public function getParam(string $name) : string
464
    {
465
        return $this->info['params'][$name] ?? '';
466
    }
467
    
468
   /**
469
    * Excludes a URL parameter entirely if present:
470
    * the parser will act as if the parameter was not
471
    * even present in the source URL, effectively
472
    * stripping it.
473
    *
474
    * @param string $name
475
    * @param string $reason A human-readable explanation why this is excluded - used when highlighting links.
476
    * @return URLInfo
477
    */
478
    public function excludeParam(string $name, string $reason='') : URLInfo
479
    {
480
        if(!isset($this->excludedParams[$name]))
481
        {
482
            $this->excludedParams[$name] = $reason;
483
            $this->setParamExclusion();
484
        }
485
        
486
        return $this;
487
    }
488
489
    /**
490
     * Retrieves a string identifier of the type of URL that was detected.
491
     *
492
     * @return string
493
     *
494
     * @see URLInfo::TYPE_EMAIL
495
     * @see URLInfo::TYPE_FRAGMENT
496
     * @see URLInfo::TYPE_PHONE
497
     * @see URLInfo::TYPE_URL
498
     * @see URLInfo::TYPE_NONE
499
     */
500
    public function getType() : string
501
    {
502
        return $this->info['type'];
503
    }
504
    
505
    public function getTypeLabel() : string
506
    {
507
        if(!isset(self::$typeLabels))
508
        {
509
            self::$typeLabels = array(
510
                self::TYPE_EMAIL => t('Email'),
511
                self::TYPE_FRAGMENT => t('Jump mark'),
512
                self::TYPE_PHONE => t('Phone number'),
513
                self::TYPE_URL => t('URL'),
514
                self::TYPE_NONE => t('Unknown')
515
            );
516
        }
517
        
518
        $type = $this->getType();
519
        
520
        if(!isset(self::$typeLabels[$type]))
521
        {
522
            throw new URLException(
523
                sprintf('Unknown URL type label for type [%s].', $type),
524
                null,
525
                self::ERROR_UNKNOWN_TYPE_FOR_LABEL
526
            );
527
        }
528
        
529
        return self::$typeLabels[$this->getType()];
530
    }
531
532
   /**
533
    * Whether excluded parameters should be highlighted in
534
    * a different color in the URL when using the
535
    * {@link URLInfo::getHighlighted()} method.
536
    *
537
    * @param bool $highlight
538
    * @return URLInfo
539
    */
540
    public function setHighlightExcluded(bool $highlight=true) : URLInfo
541
    {
542
        $this->highlightExcluded = $highlight;
543
        return $this;
544
    }
545
546
    /**
547
     * Returns an array with all relevant URL information.
548
     *
549
     * @return array<string,mixed>
550
     * @throws URLException
551
     */
552
    public function toArray() : array
553
    {
554
        return array(
555
            'hasParams' => $this->hasParams(),
556
            'params' => $this->getParams(),
557
            'type' => $this->getType(),
558
            'typeLabel' => $this->getTypeLabel(),
559
            'normalized' => $this->getNormalized(),
560
            'highlighted' => $this->getHighlighted(),
561
            'hash' => $this->getHash(),
562
            'host' => $this->getHost(),
563
            'isValid' => $this->isValid(),
564
            'isURL' => $this->isURL(),
565
            'isEmail' => $this->isEmail(),
566
            'isAnchor' => $this->isAnchor(),
567
            'isPhoneNumber' => $this->isPhoneNumber(),
568
            'errorMessage' => $this->getErrorMessage(),
569
            'errorCode' => $this->getErrorCode(),
570
            'excludedParams' => array_keys($this->excludedParams)
571
        );
572
    }
573
    
574
    /**
575
     * Enable or disable parameter exclusion: if any parameters
576
     * to exclude have been added, this allows switching between
577
     * both modes. When enabled, methods like getNormalized or
578
     * getHighlighted will exclude any parameters to exclude. When
579
     * disabled, it will act as usual.
580
     *
581
     * This allows adding parameters to exclude, but still have
582
     * access to the original URLs.
583
     *
584
     * @param bool $enabled
585
     * @return URLInfo
586
     * @see URLInfo::isParamExclusionEnabled()
587
     * @see URLInfo::setHighlightExcluded()
588
     */
589
    public function setParamExclusion(bool $enabled=true) : URLInfo
590
    {
591
        $this->paramExclusion = $enabled;
592
        return $this;
593
    }
594
    
595
   /**
596
    * Whether the parameter exclusion mode is enabled:
597
    * In this case, if any parameters have been added to the
598
    * exclusion list, all relevant methods will exclude these.
599
    *
600
    * @return bool
601
    */
602
    public function isParamExclusionEnabled() : bool
603
    {
604
        return $this->paramExclusion;
605
    }
606
    
607
   /**
608
    * Checks whether the link contains any parameters that
609
    * are on the list of excluded parameters.
610
    *
611
    * @return bool
612
    */
613
    public function containsExcludedParams() : bool
614
    {
615
        if(empty($this->excludedParams)) {
616
            return false;
617
        }
618
        
619
        $names = array_keys($this->info['params']);
620
        foreach($names as $name) {
621
            if(isset($this->excludedParams[$name])) {
622
                return true;
623
            }
624
        }
625
        
626
        return false;
627
    }
628
    
629
    public function hasParam(string $name) : bool
630
    {
631
        $names = $this->getParamNames();
632
        return in_array($name, $names);
633
    }
634
635
    public function offsetSet($offset, $value)  : void
636
    {
637
        if(in_array($offset, $this->infoKeys, true)) {
638
            $this->info[$offset] = $value;
639
        }
640
    }
641
    
642
    public function offsetExists($offset) : bool
643
    {
644
        return isset($this->info[$offset]);
645
    }
646
    
647
    public function offsetUnset($offset) : void
648
    {
649
        unset($this->info[$offset]);
650
    }
651
    
652
    public function offsetGet($offset)
653
    {
654
        if($offset === 'port') {
655
            return $this->getPort();
656
        }
657
        
658
        if(in_array($offset, $this->infoKeys, true)) {
659
            return $this->getInfoKey($offset);
660
        }
661
        
662
        return '';
663
    }
664
    
665
    public static function getHighlightCSS() : string
666
    {
667
        return URIHighlighter::getHighlightCSS();
668
    }
669
670
    /**
671
     * @return string[]
672
     */
673
    public function getExcludedParams() : array
674
    {
675
        return $this->excludedParams;
676
    }
677
    
678
    public function isHighlightExcludeEnabled() : bool
679
    {
680
        return $this->highlightExcluded;
681
    }
682
683
    /**
684
     * Checks if the URL exists, i.e. can be connected to. Will return
685
     * true if the returned HTTP status code is `200` or `302`.
686
     *
687
     * NOTE: If the target URL requires HTTP authentication, the username
688
     * and password should be integrated into the URL.
689
     *
690
     * @param bool $verifySSL
691
     * @return bool
692
     */
693
    public function tryConnect(bool $verifySSL=true) : bool
694
    {
695
        return $this->createConnectionTester()
696
            ->setVerifySSL($verifySSL)
697
            ->canConnect();
698
    }
699
    
700
   /**
701
    * Creates the connection tester instance that is used
702
    * to check if a URL can be connected to, and which is
703
    * used in the {@see URLInfo::tryConnect()} method. It
704
    * allows more settings to be used.
705
    * 
706
    * @return URIConnectionTester
707
    */
708
    public function createConnectionTester() : URIConnectionTester
709
    {
710
        return new URIConnectionTester($this);
711
    }
712
    
713
   /**
714
    * Adds/overwrites a URL parameter.
715
    *  
716
    * @param string $name
717
    * @param string $val
718
    * @return URLInfo
719
    */
720
    public function setParam(string $name, string $val) : URLInfo
721
    {
722
        $this->info['params'][$name] = $val;
723
        
724
        return $this;
725
    }
726
    
727
   /**
728
    * Removes a URL parameter. Has no effect if the
729
    * parameter is not present to begin with.
730
    * 
731
    * @param string $param
732
    * @return URLInfo
733
    */
734
    public function removeParam(string $param) : URLInfo
735
    {
736
        if(isset($this->info['params'][$param]))
737
        {
738
            unset($this->info['params'][$param]);
739
        }
740
        
741
        return $this;
742
    }
743
744
    public function hasIPAddress() : bool
745
    {
746
        return isset($this->info['ip']);
747
    }
748
}
749