1<?php
2
3/*
4
5A spiritual port of Python's urlparse.urljoin() function to PHP. Why this isn't in the standard library is anyone's guess.
6
7Author: fluffy, http://beesbuzz.biz/
8Latest version at: https://github.com/plaidfluff/php-urljoin
9
10 */
11
12function urljoin($base, $rel) {
13	if (!$base) {
14		return $rel;
15	}
16
17	if (!$rel) {
18		return $base;
19	}
20
21	$uses_relative = array('', 'ftp', 'http', 'gopher', 'nntp', 'imap',
22		'wais', 'file', 'https', 'shttp', 'mms',
23		'prospero', 'rtsp', 'rtspu', 'sftp',
24		'svn', 'svn+ssh', 'ws', 'wss');
25
26	$pbase = parse_url($base);
27	$prel = parse_url($rel);
28
29	if ($prel === false || preg_match('/^[a-z0-9\-.]*[^a-z0-9\-.:][a-z0-9\-.]*:/i', $rel)) {
30		/*
31			Either parse_url couldn't parse this, or the original URL
32			fragment had an invalid scheme character before the first :,
33			which can confuse parse_url
34		*/
35		$prel = array('path' => $rel);
36	}
37
38	if (array_key_exists('path', $pbase) && $pbase['path'] === '/') {
39		unset($pbase['path']);
40	}
41
42	if (isset($prel['scheme'])) {
43		if ($prel['scheme'] != $pbase['scheme'] || in_array($prel['scheme'], $uses_relative) == false) {
44			return $rel;
45		}
46	}
47
48	$merged = array_merge($pbase, $prel);
49
50	// Handle relative paths:
51	//   'path/to/file.ext'
52	// './path/to/file.ext'
53	if (array_key_exists('path', $prel) && substr($prel['path'], 0, 1) != '/') {
54
55		// Normalize: './path/to/file.ext' => 'path/to/file.ext'
56		if (substr($prel['path'], 0, 2) === './') {
57			$prel['path'] = substr($prel['path'], 2);
58		}
59
60		if (array_key_exists('path', $pbase)) {
61			$dir = preg_replace('@/[^/]*$@', '', $pbase['path']);
62			$merged['path'] = $dir . '/' . $prel['path'];
63		} else {
64			$merged['path'] = '/' . $prel['path'];
65		}
66
67	}
68
69	if(array_key_exists('path', $merged)) {
70		// Get the path components, and remove the initial empty one
71		$pathParts = explode('/', $merged['path']);
72		array_shift($pathParts);
73
74		$path = [];
75		$prevPart = '';
76		foreach ($pathParts as $part) {
77			if ($part == '..' && count($path) > 0) {
78				// Cancel out the parent directory (if there's a parent to cancel)
79				$parent = array_pop($path);
80				// But if it was also a parent directory, leave it in
81				if ($parent == '..') {
82					array_push($path, $parent);
83					array_push($path, $part);
84				}
85			} else if ($prevPart != '' || ($part != '.' && $part != '')) {
86				// Don't include empty or current-directory components
87				if ($part == '.') {
88					$part = '';
89				}
90				array_push($path, $part);
91			}
92			$prevPart = $part;
93		}
94		$merged['path'] = '/' . implode('/', $path);
95	}
96
97	$ret = '';
98	if (isset($merged['scheme'])) {
99		$ret .= $merged['scheme'] . ':';
100	}
101
102	if (isset($merged['scheme']) || isset($merged['host'])) {
103		$ret .= '//';
104	}
105
106	if (isset($prel['host'])) {
107		$hostSource = $prel;
108	} else {
109		$hostSource = $pbase;
110	}
111
112	// username, password, and port are associated with the hostname, not merged
113	if (isset($hostSource['host'])) {
114		if (isset($hostSource['user'])) {
115			$ret .= $hostSource['user'];
116			if (isset($hostSource['pass'])) {
117				$ret .= ':' . $hostSource['pass'];
118			}
119			$ret .= '@';
120		}
121		$ret .= $hostSource['host'];
122		if (isset($hostSource['port'])) {
123			$ret .= ':' . $hostSource['port'];
124		}
125	}
126
127	if (isset($merged['path'])) {
128		$ret .= $merged['path'];
129	}
130
131	if (isset($prel['query'])) {
132		$ret .= '?' . $prel['query'];
133	}
134
135	if (isset($prel['fragment'])) {
136		$ret .= '#' . $prel['fragment'];
137	}
138
139	return $ret;
140}
141