Passed
Push — master ( 3da677...af34c7 )
by Sebastian
02:33
created

URLInfo::tryConnect()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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