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