Test Failed
Push — master ( 56550f...b49bd0 )
by Ismayil
03:13
created

Parser::isValidUrl()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6.73

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 8
cts 11
cp 0.7272
rs 8.7624
c 0
b 0
f 0
cc 6
eloc 12
nc 8
nop 1
crap 6.73
1
<?php
2
3
namespace hypeJunction;
4
5
use DOMDocument;
6
use Exception;
7
use GuzzleHttp\ClientInterface;
8
use GuzzleHttp\Psr7\Response;
9
10
/**
11
 * Parses HTTP resource into a serialable array of metatags
12
 */
13
class Parser {
14
15
	/**
16
	 * @var ClientInterface
17
	 */
18
	private $client;
19
20
	/**
21
	 * @var array
22
	 */
23
	private static $cache;
24
25
	/**
26
	 * Constructor
27
	 * @param ClientInterface $client HTTP Client
28
	 */
29
	public function __construct(ClientInterface $client) {
30
		$this->client = $client;
31
	}
32
33
	/**
34
	 * Parses a URL into a an array of metatags
35
	 *
36
	 * @param string $url URL to parse
37
	 * @return array
38
	 */
39 1
	public function parse($url = '') {
40
41 1
		$data = $this->getImageData($url);
42 1
		if (!$data) {
43 1
			$data = $this->getOEmbedData($url);
44 1
		}
45 1
		if (!$data) {
46 1
			$data = $this->getDOMData($url);
47 1
			if (is_array($data) && !empty($data['oembed_url'])) {
48 1
				foreach ($data['oembed_url'] as $oembed_url) {
49 1
					$oembed_data = $this->parse($oembed_url);
50 1
					if (!empty($oembed_data) && is_array($oembed_data)) {
51
						$oembed_data['oembed_url'] = $oembed_data['url'];
52
						unset($oembed_data['url']);
53
						$data = array_merge($data, $oembed_data);
54
					}
55 1
				}
56 1
			}
57 1
		}
58
59 1
		if (!is_array($data)) {
60 1
			$data = array();
61 1
		}
62
63 1
		if (empty($data['thumbnail_url']) && !empty($data['thumbnails'])) {
64 1
			$data['thumbnail_url'] = $data['thumbnails'][0];
65 1
		}
66
67 1
		return $data;
68
	}
69
70
	/**
71
	 * Parses image metatags
72
	 *
73
	 * @param string $url URL of the image
74
	 * @return array|false
75
	 */
76 1
	public function getImageData($url = '') {
77 1
		if (!$this->isImage($url)) {
78 1
			return false;
79
		}
80
81
		return array(
82 1
			'type' => 'photo',
83 1
			'url' => $url,
84 1
			'thumbnails' => array($url),
85 1
		);
86
	}
87
88
	/**
89
	 * Parses OEmbed data
90
	 *
91
	 * @param  string $url URL of the image
92
	 * @return array|false
93
	 */
94 2
	public function getOEmbedData($url = '') {
95
96 2
		if (!$this->isJSON($url) && !$this->isXML($url)) {
97 1
			return false;
98
		}
99
100
		$meta = array(
101 2
			'url' => $url,
102 2
		);
103
104 2
		$content = $this->read($url);
105 2
		if (!$content) {
106
			return $meta;
107
		}
108
109 2
		$data = new \stdClass();
110 2
		if ($this->isJSON($url)) {
111 1
			$data = json_decode($content);
112 2
		} else if ($this->isXML($url)) {
113 1
			$data = simplexml_load_string($content);
114 1
		}
115
116
		$props = array(
117 2
			'type',
118 2
			'version',
119 2
			'title',
120 2
			'author_name',
121 2
			'author_url',
122 2
			'provider_name',
123 2
			'provider_url',
124 2
			'cache_age',
125 2
			'thumbnail_url',
126 2
			'thumbnail_width',
127 2
			'thumbnail_height',
128 2
			'width',
129 2
			'height',
130 2
			'html',
131 2
		);
132 2
		foreach ($props as $key) {
133 2
			if (!empty($data->$key)) {
134 2
				$meta[$key] = (string) $data->$key;
135 2
			}
136 2
		}
137 2
		return $meta;
138
	}
139
140
	/**
141
	 * Parses metatags from DOM
142
	 *
143
	 * @param  string $url URL
144
	 * @return array|false
145
	 */
146 1
	public function getDOMData($url = '') {
147
148 1
		if (!$this->isHTML($url)) {
149 1
			return false;
150
		}
151
152 1
		$doc = $this->getDOM($url);
153 1
		if (!$doc) {
154
			return false;
155
		}
156
157
		$defaults = array(
158 1
			'url' => $url,
159 1
		);
160
161 1
		$link_tags = $this->parseLinkTags($doc);
162 1
		$meta_tags = $this->parseMetaTags($doc);
163 1
		$img_tags = $this->parseImgTags($doc);
164
165 1
		$meta = array_merge_recursive($defaults, $link_tags, $meta_tags, $img_tags);
166
167 1
		if (empty($meta['title'])) {
168
			$meta['title'] = $this->parseTitle($doc);
169
		}
170
171
172 1
		return $meta;
173
	}
174
175
	/**
176
	 * Check if URL exists and is reachable by making an HTTP request to retrieve header information
177
	 *
178
	 * @param string $url URL of the resource
179
	 * @return boolean
180
	 */
181 1
	public function exists($url = '') {
182 1
		$response = $this->request($url);
183 1
		if ($response instanceof Response) {
184 1
			return $response->getStatusCode() == 200;
185
		}
186
		return false;
187
	}
188
189
	/**
190
	 * Validate URL
191
	 * 
192
	 * @param string $url URL to validate
193
	 * @return bool
194
	 */
195 1
	public function isValidUrl($url = '') {
196 1
		// based on http://php.net/manual/en/function.filter-var.php#104160
197
		// adapted by @mrclay in https://github.com/mrclay/Elgg-leaf/blob/62bf31c0ccdaab549a7e585a4412443e09821db3/engine/lib/output.php
198
		$res = filter_var($url, FILTER_VALIDATE_URL);
199 1
		if ($res) {
200
			return $res;
201 1
		}
202 1
		// Check if it has unicode chars.
203
		$l = elgg_strlen($url);
204
		if (strlen($url) == $l) {
205
			return $res;
206 1
		}
207 1
		// Replace wide chars by “X”.
208
		$s = '';
209 1
		for ($i = 0; $i < $l; ++$i) {
210
			$ch = elgg_substr($url, $i, 1);
211
			$s .= (strlen($ch) > 1) ? 'X' : $ch;
212
		}
213
		// Re-check now.
214
		return filter_var($s, FILTER_VALIDATE_URL) ? $url : false;
0 ignored issues
show
Bug Compatibility introduced by
The expression filter_var($s, FILTER_VA...TE_URL) ? $url : false; of type string|false adds the type string to the return on line 214 which is incompatible with the return type documented by hypeJunction\Parser::isValidUrl of type boolean.
Loading history...
215
	}
216
217
	/**
218 1
	 * Returns head of the resource
219 1
	 *
220 1
	 * @param string $url URL of the resource
221 1
	 * @return Response|false
222
	 */
223
	public function request($url = '') {
224 1
		$url = str_replace(' ', '%20', $url);
225 1
		if (!$this->isValidUrl($url)) {
226 1
			return false;
227
		}
228
		if (!isset(self::$cache[$url])) {
229
			try {
230
				$response = $this->client->request('GET', $url);
231
			} catch (Exception $e) {
232
				$response = false;
233
				error_log("Parser Error for HEAD request ($url): {$e->getMessage()}");
234
			}
235 1
			self::$cache[$url] = $response;
236 1
		}
237 1
238
		return self::$cache[$url];
239
	}
240
241
	/**
242
	 * Get contents of the page
243
	 *
244
	 * @param string $url URL of the resource
245
	 * @return string
246 1
	 */
247 1
	public function read($url = '') {
248 1
		$body = '';
249
		if (!$this->exists($url)) {
250
			return $body;
251
		}
252
253
		$response = $this->request($url);
254
		$body = (string) $response->getBody();
255
		return $body;
256
	}
257 1
258 1
	/**
259 1
	 * Checks if resource is an html page
260
	 *
261
	 * @param string $url URL of the resource
262
	 * @return boolean
263
	 */
264
	public function isHTML($url = '') {
265
		$mime = $this->getContentType($url);
266
		return strpos($mime, 'text/html') !== false;
267
	}
268 1
269 1
	/**
270 1
	 * Checks if resource is JSON
271 1
	 *
272 1
	 * @param string $url URL of the resource
273
	 * @return boolean
274
	 */
275 1
	public function isJSON($url = '') {
276
		$mime = $this->getContentType($url);
277
		return strpos($mime, 'json') !== false;
278
	}
279
280
	/**
281
	 * Checks if resource is XML
282
	 *
283
	 * @param string $url URL of the resource
284 1
	 * @return boolean
285 1
	 */
286 1
	public function isXML($url = '') {
287 1
		$mime = $this->getContentType($url);
288 1
		return strpos($mime, 'xml') !== false;
289 1
	}
290 1
291
	/**
292 1
	 * Checks if resource is an image
293 1
	 *
294
	 * @param string $url URL of the resource
295
	 * @return boolean
296
	 */
297
	public function isImage($url = '') {
298
		$mime = $this->getContentType($url);
299
		if ($mime) {
300
			list($simple, ) = explode('/', $mime);
301
			return ($simple == 'image');
302 1
		}
303 1
304 1
		return false;
305
	}
306 1
307
	/**
308
	 * Get mime type of the URL content
309
	 *
310
	 * @param string $url URL of the resource
311
	 * @return string
312
	 */
313
	public function getContentType($url = '') {
314
		$response = $this->request($url);
315 1
		if ($response instanceof Response) {
316 1
			$header = $response->getHeader('Content-Type');
317 1
			if (is_array($header) && !empty($header)) {
318 1
				$parts = explode(';', $header[0]);
319
				return trim($parts[0]);
320 1
			}
321 1
		}
322 1
		return '';
323 1
	}
324
325
	/**
326 1
	 * Returns HTML contents of the page
327 1
	 *
328 1
	 * @param string $url URL of the resource
329 1
	 * @return string
330
	 */
331
	public function getHTML($url = '') {
332
		if (!$this->isHTML($url)) {
333
			return '';
334
		}
335
		return $this->read($url);
336
	}
337
338 1
	/**
339 1
	 * Returns HTML contents of the page as a DOMDocument
340 1
	 *
341 1
	 * @param string $url URL of the resource
342
	 * @return DOMDocument|false
343
	 */
344
	public function getDOM($url = '') {
345
		$html = $this->getHTML($url);
346
		if (empty($html)) {
347
			return false;
348
		}
349
		$doc = new DOMDocument();
350 1
		if (is_callable('mb_convert_encoding')) {
351
			$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
352 1
		} else {
353
			$doc->loadHTML($html);
354 1
		}
355 1
		if (!$doc->documentURI) {
356 1
			$doc->documentURI = $url;
357 1
		}
358
		return $doc;
359
	}
360
361 1
	/**
362 1
	 * Parses document title
363 1
	 *
364
	 * @param DOMDocument $doc Document
365 1
	 * @return string
366 1
	 */
367 1
	public function parseTitle(DOMDocument $doc) {
368
		$node = $doc->getElementsByTagName('title');
369 1
		$title = $node->item(0)->nodeValue;
370 1
		return ($title) ?: '';
371 1
	}
372 1
373 1
	/**
374 1
	 * Parses <link> tags
375
	 *
376 1
	 * @param DOMDocument $doc Document
377 1
	 * @return array
378 1
	 */
379 1
	public function parseLinkTags(DOMDocument $doc) {
380
381 1
		$meta = array();
382
383 1
		$nodes = $doc->getElementsByTagName('link');
384
		foreach ($nodes as $node) {
385
			$rel = $node->getAttribute('rel');
386
			$href = $node->getAttribute('href');
387
388
			switch ($rel) {
389
390
				case 'icon' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
391
					$image_url = $this->getAbsoluteURL($doc, $href);
392 1
					if ($this->isImage($image_url)) {
0 ignored issues
show
Security Bug introduced by
It seems like $image_url defined by $this->getAbsoluteURL($doc, $href) on line 391 can also be of type false; however, hypeJunction\Parser::isImage() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
393
						$meta['icons'][] = $image_url;
394 1
					}
395
					break;
396 1
397 1
				case 'canonical' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
398 1
					$meta['canonical'] = $this->getAbsoluteURL($doc, $href);
399 1
					break;
400 1
401 1
				case 'alternate' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
402 1
					$type = $node->getAttribute('type');
403 1
					if (in_array($type, array(
404 1
								'application/json+oembed',
405
								'text/json+oembed',
406
								'application/xml+oembed',
407 1
								'text/xml+oembed'
408
							))) {
409 1
						$meta['oembed_url'][] = $this->getAbsoluteURL($doc, $href);
410 1
					}
411 1
					break;
412 1
			}
413 1
		}
414 1
415 1
		return $meta;
416 1
	}
417
418
	/**
419
	 * Parses <meta> tags
420
	 *
421 1
	 * @param DOMDocument $doc Document
422 1
	 * @return array
423 1
	 */
424 1
	public function parseMetaTags(DOMDocument $doc) {
425 1
426 1
		$meta = array();
427 1
428
		$nodes = $doc->getElementsByTagName('meta');
429 1
		if (!empty($nodes)) {
430 1
			foreach ($nodes as $node) {
431 1
				$name = $node->getAttribute('name');
432 1
				if (!$name) {
433 1
					$name = $node->getAttribute('property');
434
				}
435 1
				if (!$name) {
436 1
					continue;
437 1
				}
438 1
439 1
				$name = strtolower($name);
440 1
441 1
				$content = $node->getAttribute('content');
442
				if (isset($meta['metatags'][$name])) {
443 1
					if (!is_array($meta['metatags'][$name])) {
444 1
						$meta['metatags'][$name] = array($meta['metatags'][$name]);
445 1
					}
446 1
					$meta['metatags'][$name][] = $content;
447 1
				} else {
448 1
					$meta['metatags'][$name] = $content;
449 1
				}
450
451 1
				switch ($name) {
452 1
453 1
					case 'title' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
454 1
					case 'og:title' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
455 1
					case 'twitter:title' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
456 1
						if (empty($meta['title'])) {
457
							$meta['title'] = $content;
458 1
						}
459 1
						break;
460 1
461 1
					case 'og:type' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
462
						if (empty($meta['type'])) {
463 1
							$meta['type'] = $content;
464 1
						}
465
						break;
466 1
467
					case 'description' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
468
					case 'og:description' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
469
					case 'twitter:description' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
470
						if (empty($meta['description'])) {
471
							$meta['description'] = $content;
472
						}
473
						break;
474
475 1
					case 'keywords' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
476
						if (is_string($content)) {
477 1
							$content = explode(',', $content);
478
							$content = array_map('trim', $content);
479 1
						}
480 1
						$meta['tags'] = $content;
481 1
						break;
482 1
483 1
					case 'og:site_name' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
484
					case 'twitter:site' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
485 1
						if (empty($meta['provider_name'])) {
486
							$meta['provider_name'] = $content;
487
						}
488
						break;
489
490
					case 'og:image' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
491
					case 'twitter:image' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
492
						$image_url = $this->getAbsoluteURL($doc, $content);
493
						if ($this->isImage($image_url)) {
0 ignored issues
show
Security Bug introduced by
It seems like $image_url defined by $this->getAbsoluteURL($doc, $content) on line 492 can also be of type false; however, hypeJunction\Parser::isImage() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
494
							$meta['thumbnails'][] = $image_url;
495 1
						}
496
						break;
497 1
				}
498
			}
499
		}
500
501
		return $meta;
502
	}
503 1
504 1
	/**
505
	 * Parses <img> tags
506
	 *
507 1
	 * @param DOMDocument $doc Document
508
	 * @return array
509
	 */
510 1
	public function parseImgTags(DOMDocument $doc) {
511 1
512 1
		$meta = array();
513 1
514
		$nodes = $doc->getElementsByTagName('img');
515
		foreach ($nodes as $node) {
516
			$src = $node->getAttribute('src');
517 1
			$image_url = $this->getAbsoluteURL($doc, $src);
518
			if ($this->isImage($image_url)) {
0 ignored issues
show
Security Bug introduced by
It seems like $image_url defined by $this->getAbsoluteURL($doc, $src) on line 517 can also be of type false; however, hypeJunction\Parser::isImage() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
519
				$meta['thumbnails'][] = $image_url;
520
			}
521
		}
522
523
		return $meta;
524
	}
525
526
	/**
527
	 * Normalizes relative URLs
528
	 *
529
	 * @param DOMDocument $doc  Document
530
	 * @param string      $href URL to normalize
531
	 * @return string|false
532
	 */
533
	public function getAbsoluteURL(DOMDocument $doc, $href = '') {
534
535
		if (preg_match("/^data:/i", $href)) {
536
			// data URIs can not be resolved
537
			return false;
538
		}
539
540
		// Check if $url is absolute
541
		if (parse_url($href, PHP_URL_HOST)) {
542
			return $href;
543
		}
544
545
		$uri = trim($doc->documentURI ?: '', '/');
546
547
		// Check if $url is relative to root
548
		if (substr($href, 0, 1) === "/") {
549
			$scheme = parse_url($uri, PHP_URL_SCHEME);
550
			$host = parse_url($uri, PHP_URL_HOST);
551
			return "$scheme://$host$href";
552
		}
553
554
		// $url is relative to page
555
		$uri = pathinfo($uri, PATHINFO_DIRNAME);
556
		return "$uri/$href";
557
	}
558
559
}
560