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._%+-]+(?:@|&#64;)[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], "&hellip;");
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("&quot;");
255 			break;
256 		case '\'':
257 			app.put("&#39;");
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("&lt;");
272 			break;
273 		case '>':
274 			app.put("&gt;");
275 			break;
276 		case '&':
277 			app.put("&amp;");
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 }