1 module jax.filters; 2 3 4 import std.algorithm; 5 import std.array; 6 import std.format; 7 import std.string; 8 import std.regex; 9 import std.utf; 10 import std.traits; 11 12 13 private @property bool validURL(string url) { 14 import std.conv : to; 15 16 if (url.empty) { 17 return false; 18 } else if (url.ptr[0] != '/') { 19 auto idx = url.indexOf(':'); 20 if (idx <= 0) 21 return false; // no protocol 22 23 auto protocol = url[0..idx]; 24 url = url[idx+1..$]; 25 26 auto needsHost = false; 27 28 switch (protocol) { 29 case "http": 30 case "https": 31 case "ftp": 32 case "spdy": 33 case "sftp": 34 if (!url.startsWith("//")) 35 return false; // must start with protocol://... 36 37 needsHost = true; 38 url = url[2..$]; 39 goto default; 40 case "file": 41 if (!url.startsWith("//")) 42 return false; // must start with protocol://... 43 44 url = url[2..$]; 45 goto default; 46 default: 47 auto indexSlash = url.indexOf('/'); 48 if (indexSlash < 0) 49 indexSlash = url.length; 50 51 auto indexAt = url[0..indexSlash].indexOf('@'); 52 size_t indexHost = 0; 53 if (indexAt >= 0) { 54 indexHost = cast(size_t)indexAt + 1; 55 auto sep = url[0..indexAt].indexOf(':'); 56 auto username = (sep >= 0) ? url[0..sep] : url[0..indexAt]; 57 if (username.empty) 58 return false; // empty user name 59 } 60 61 auto host = url[indexHost..indexSlash]; 62 auto indexPort = host.indexOf(':'); 63 64 if (indexPort > 0) { 65 if (indexPort >= host.length-1) 66 return false; // empty port 67 try { 68 auto port = to!ushort(host[indexPort+1..$]); 69 } catch { 70 return false; 71 } 72 host = host[0..indexPort]; 73 } 74 75 if (host.empty && needsHost) 76 return false; // empty server name 77 78 url = url[indexSlash..$]; 79 } 80 } 81 82 return true; 83 } 84 85 86 private struct FixedAppender(AT : E[], size_t Size = 512, E) { 87 alias UE = Unqual!E; 88 89 private UE[Size] data_; 90 private size_t len_; 91 92 static if (!is(E == immutable)) { 93 void clear() { 94 len_ = 0; 95 } 96 } 97 98 void put(E x) { 99 data_[len_++] = x; 100 } 101 102 static if (is(UE == char)) { 103 void put(dchar x) { 104 if (x < 0x80) { 105 put(cast(char)x); 106 } else { 107 char[4] buf; 108 auto len = std.utf.encode(buf, x); 109 put(cast(AT)buf[0..len]); 110 } 111 } 112 } 113 114 static if (is(UE == wchar)) { 115 void put(dchar x) { 116 if (x < 0x80) { 117 put(cast(wchar)x); 118 } else { 119 wchar[3] buf; 120 auto len = std.utf.encode(buf, x); 121 put(cast(AT)buf[0..len]); 122 } 123 } 124 } 125 126 void put(AT arr) { 127 data_[len_..len_ + arr.length] = (cast(UE[])arr)[]; 128 len_ += arr.length; 129 } 130 131 @property AT data() { 132 return cast(AT)data_[0..len_]; 133 } 134 } 135 136 137 auto fixedAppender(AT : E[], size_t Size = 512, E)() { 138 return FixedAppender!(AT, Size, E)(); 139 } 140 141 142 string concat(Args...)(Args args) if (args.length > 0) { 143 static if (args.length > 1) { 144 auto length = 0; 145 auto precise = true; 146 147 foreach(arg; args) { 148 static if (isSomeString!(typeof(arg))) { 149 length += arg.length; 150 } else static if (isScalarType!(typeof(arg))) { 151 length += 24; 152 } else static if (isSomeChar!(typeof(arg))) { 153 length += 6; // max unicode code length 154 } else { 155 length += 16; 156 precise = false; 157 } 158 } 159 160 enum MaxStackAlloc = 1024; 161 162 if (precise && (length <= MaxStackAlloc)) { 163 auto app = fixedAppender!(string, MaxStackAlloc); 164 165 foreach(arg; args) { 166 static if (isSomeString!(typeof(arg)) || isSomeChar!(typeof(arg))) { 167 app.put(arg); 168 } else { 169 formattedWrite(&app, "%s", arg); 170 } 171 } 172 return app.data.idup; 173 } else { 174 auto app = appender!string; 175 app.reserve(length); 176 177 foreach(arg; args) { 178 static if (isSomeString!(typeof(arg)) || isSomeChar!(typeof(arg))) { 179 app.put(arg); 180 } else { 181 formattedWrite(&app, "%s", arg); 182 } 183 } 184 185 return app.data; 186 } 187 } else { 188 static if (isSomeString!(typeof(arg)) || isSomeChar!(typeof(arg))) { 189 return args[0]; 190 } else { 191 formattedWrite(&app, "%s", arg); 192 } 193 } 194 } 195 196 197 198 enum FormatHTMLOptions { 199 None = 0, 200 Escape = 1 << 0, 201 CreateLinks = 1 << 1, 202 Default = None, 203 } 204 205 206 private __gshared { 207 auto matchNewLine = ctRegex!(`\r\n|\n`, `g`); 208 auto matchLink = ctRegex!(`\b((?:[\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|(?:[^\s\."'!?()<>]|/)))\b`, `gi`); 209 auto matchEMail = ctRegex!(`(\b[a-z0-9._%+-]+(?:@|@)[a-z0-9.-]+\.[a-z]{2,4}\b)`, `gi`); 210 } 211 212 213 string formatHTML(FormatHTMLOptions Options = FormatHTMLOptions.Default)(string x) { 214 auto result = x; 215 216 static if (Options & FormatHTMLOptions.Escape) { 217 result = result.escapeHTML; 218 } 219 220 result = result.replaceAll(matchNewLine, "<br />"); 221 222 static if (Options & FormatHTMLOptions.CreateLinks) { 223 static string createLink(Captures!(string) m) { 224 auto url = m[0]; 225 if (url.indexOf("://") <= 0) 226 url = concat("http://", url); 227 228 auto urlShort = url; 229 230 if (url.validURL) { 231 if (url.length > 72) 232 urlShort = concat(urlShort[0..70], "…"); 233 return format(`<a href="%s" target="nofollow">%s</a>`, url, urlShort); 234 } else { 235 return m[0]; 236 } 237 } 238 239 result = result.replaceAll!(createLink)(matchLink); 240 result = result.replaceAll(matchEMail, "<a href=\"mailto:$1\">$1</a>"); 241 } 242 243 return result; 244 } 245 246 247 string escapeHTML(string x) { 248 auto app = appender!string; 249 app.reserve(8 + x.length + (x.length >> 1)); 250 251 foreach (ch; x.byDchar) { 252 switch (ch) { 253 case '"': 254 app.put("""); 255 break; 256 case '\'': 257 app.put("'"); 258 break; 259 case 'a': .. case 'z': 260 goto case; 261 case 'A': .. case 'Z': 262 goto case; 263 case '0': .. case '9': 264 goto case; 265 case ' ', '\t', '\n', '\r', '-', '_', '.', ':', ',', ';', 266 '#', '+', '*', '?', '=', '(', ')', '/', '!', 267 '%' , '{', '}', '[', ']', '$', '^', '~': 268 app.put(cast(char)ch); 269 break; 270 case '<': 271 app.put("<"); 272 break; 273 case '>': 274 app.put(">"); 275 break; 276 case '&': 277 app.put("&"); 278 break; 279 default: 280 formattedWrite(&app, "&#x%02X;", cast(uint)ch); 281 break; 282 } 283 } 284 return app.data; 285 } 286 287 288 string escapeJS(string x) { 289 auto app = appender!string; 290 app.reserve(x.length + (x.length >> 1)); 291 292 foreach (ch; x.byDchar) { 293 switch (ch) { 294 case '\\': 295 app.put(`\\`); 296 break; 297 case '\'': 298 app.put(`\'`); 299 break; 300 case '\"': 301 app.put(`\"`); 302 break; 303 case '\r': 304 break; 305 case '\n': 306 app.put(`\n`); 307 break; 308 default: 309 app.put(ch); 310 break; 311 } 312 } 313 return app.data; 314 } 315 316 317 string encodeURI(string x) { 318 auto app = appender!string; 319 app.reserve(8 + x.length + (x.length >> 1)); 320 321 encodeURI(app, x); 322 323 return app.data; 324 } 325 326 327 void encodeURI(Appender)(ref Appender app, string x, const(char)[]ignoreChars = null) { 328 foreach (i; 0..x.length) { 329 switch (x.ptr[i]) { 330 case 'A': .. case 'Z': 331 case 'a': .. case 'z': 332 case '0': .. case '9': 333 case '-': case '_': case '.': case '~': 334 app.put(x.ptr[i]); 335 break; 336 default: 337 if (ignoreChars.canFind(x.ptr[i])) { 338 app.put(x.ptr[i]); 339 } else { 340 formattedWrite(&app, "%%%02X", x.ptr[i]); 341 } 342 break; 343 } 344 } 345 } 346 347 348 string appendURIParam(string x, string param, string value) { 349 if (x.indexOf('?') == -1) 350 return concat(x, "?", param, "=", value); 351 return concat(x, "&", param, "=", value); 352 }