1 /**
2  * ae.net.ietf.url
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.net.ietf.url;
15 
16 import std.exception;
17 import std..string;
18 
19 import ae.utils.array;
20 
21 string applyRelativeURL(string base, string rel)
22 {
23 	{
24 		auto p = rel.indexOf("://");
25 		if (p >= 0 && rel.indexOf("/") > p)
26 			return rel;
27 	}
28 
29 	base = base.split("?")[0];
30 	base = base[0..base.lastIndexOf('/')+1];
31 	while (true)
32 	{
33 		if (rel.startsWith("../"))
34 		{
35 			rel = rel[3..$];
36 			base = base[0..base[0..$-1].lastIndexOf('/')+1];
37 			enforce(base.length, "Bad relative URL");
38 		}
39 		else
40 		if (rel.startsWith("/"))
41 			return base.split("/").slice(0, 3).join("/") ~ rel;
42 		else
43 			return base ~ rel;
44 	}
45 }
46 
47 unittest
48 {
49 	assert(applyRelativeURL("http://example.com/", "index.html") == "http://example.com/index.html");
50 	assert(applyRelativeURL("http://example.com/index.html", "page.html") == "http://example.com/page.html");
51 	assert(applyRelativeURL("http://example.com/dir/index.html", "page.html") == "http://example.com/dir/page.html");
52 	assert(applyRelativeURL("http://example.com/dir/index.html", "/page.html") == "http://example.com/page.html");
53 	assert(applyRelativeURL("http://example.com/dir/index.html", "../page.html") == "http://example.com/page.html");
54 	assert(applyRelativeURL("http://example.com/script.php?path=a/b/c", "page.html") == "http://example.com/page.html");
55 	assert(applyRelativeURL("http://example.com/index.html", "http://example.org/page.html") == "http://example.org/page.html");
56 	assert(applyRelativeURL("http://example.com/http://archived.website", "/http://archived.website/2") == "http://example.com/http://archived.website/2");
57 }
58 
59 string fileNameFromURL(string url)
60 {
61 	return url.split("?")[0].split("/")[$-1];
62 }
63 
64 unittest
65 {
66 	assert(fileNameFromURL("http://example.com/index.html") == "index.html");
67 	assert(fileNameFromURL("http://example.com/dir/index.html") == "index.html");
68 	assert(fileNameFromURL("http://example.com/script.php?path=a/b/c") == "script.php");
69 }
70 
71 // ***************************************************************************
72 
73 /// Encode an URL part using a custom function to decide
74 /// characters to encode.
75 template UrlEncoder(alias isCharAllowed, char escape = '%')
76 {
77 	bool[256] genCharAllowed()
78 	{
79 		bool[256] result;
80 		foreach (c; 0..256)
81 			result[c] = isCharAllowed(cast(char)c);
82 		return result;
83 	}
84 
85 	immutable bool[256] charAllowed = genCharAllowed();
86 
87 	struct UrlEncoder(Sink)
88 	{
89 		Sink sink;
90 
91 		void put(in char[] s)
92 		{
93 			foreach (c; s)
94 				if (charAllowed[c])
95 					sink.put(c);
96 				else
97 				{
98 					sink.put(escape);
99 					sink.put(hexDigits[cast(ubyte)c >> 4]);
100 					sink.put(hexDigits[cast(ubyte)c & 15]);
101 				}
102 		}
103 	}
104 }
105 
106 import ae.utils.textout : countCopy;
107 
108 string encodeUrlPart(alias isCharAllowed, char escape = '%')(string s) pure
109 {
110 	alias UrlPartEncoder = UrlEncoder!(isCharAllowed, escape);
111 
112 	static struct Encoder
113 	{
114 		string s;
115 
116 		void opCall(Sink)(Sink sink)
117 		{
118 			auto encoder = UrlPartEncoder!Sink(sink);
119 			encoder.put(s);
120 		}
121 	}
122 
123 	Encoder encoder = {s};
124 	return countCopy!char(encoder);
125 }
126 
127 import std.ascii;
128 
129 alias encodeUrlParameter = encodeUrlPart!(c => isAlphaNum(c) || c=='-' || c=='_');
130 
131 unittest
132 {
133 	assert(encodeUrlParameter("abc?123") == "abc%3F123");
134 }
135 
136 import ae.utils.aa : MultiAA;
137 
138 alias UrlParameters = MultiAA!(string, string);
139 
140 string encodeUrlParameters(UrlParameters dic)
141 {
142 	string[] segs;
143 	foreach (name, value; dic)
144 		segs ~= encodeUrlParameter(name) ~ '=' ~ encodeUrlParameter(value);
145 	return join(segs, "&");
146 }
147 
148 string encodeUrlParameters(string[string] dic) { return encodeUrlParameters(UrlParameters(dic)); }
149 
150 import ae.utils.text;
151 
152 string decodeUrlParameter(bool plusToSpace=true, char escape = '%')(string encoded)
153 {
154 	string s;
155 	for (auto i=0; i<encoded.length; i++)
156 		if (encoded[i] == escape && i+3 <= encoded.length)
157 		{
158 			s ~= cast(char)fromHex!ubyte(encoded[i+1..i+3]);
159 			i += 2;
160 		}
161 		else
162 		if (plusToSpace && encoded[i] == '+')
163 			s ~= ' ';
164 		else
165 			s ~= encoded[i];
166 	return s;
167 }
168 
169 UrlParameters decodeUrlParameters(string qs)
170 {
171 	UrlParameters dic;
172 	if (!qs.length)
173 		return dic;
174 	string[] segs = split(qs, "&");
175 	foreach (pair; segs)
176 	{
177 		auto p = pair.indexOf('=');
178 		if (p < 0)
179 			dic.add(decodeUrlParameter(pair), null);
180 		else
181 			dic.add(decodeUrlParameter(pair[0..p]), decodeUrlParameter(pair[p+1..$]));
182 	}
183 	return dic;
184 }
185 
186 unittest
187 {
188 	assert(decodeUrlParameters("").length == 0);
189 }