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 }