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 }