1 /**
2 	Implements a declarative framework for building web interfaces.
3 
4 	This module contains the sister funtionality to the $(D hb.web.rest)
5 	module. While the REST interface generator is meant for stateless
6 	machine-to-machine communication, this module aims at implementing
7 	user facing web services. Apart from that, both systems use the same
8 	declarative approach.
9 
10 	See $(D registerWebInterface) for an overview of how the system works.
11 
12 	Copyright: © 2013-2016 RejectedSoftware e.K.
13 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
14 	Authors: Sönke Ludwig
15 */
16 module hb.web.web;
17 
18 public import vibe.internal.meta.funcattr : PrivateAccessProxy, before, after;
19 public import hb.web.common;
20 public import hb.web.i18n;
21 public import hb.web.validation;
22 
23 import vibe.core.core;
24 import vibe.inet.url;
25 import vibe.http.common;
26 import vibe.http.router;
27 import vibe.http.server;
28 import vibe.http.websockets;
29 import hb.web.auth : AuthInfo, handleAuthentication, handleAuthorization, isAuthenticated;
30 
31 import std.encoding : sanitize;
32 
33 /*
34 	TODO:
35 		- conversion errors of path place holder parameters should result in 404
36 		- support format patterns for redirect()
37 		- add a way to specify response headers without explicit access to "res"
38 */
39 
40 
41 /**
42 	Registers a HTTP/web interface based on a class instance.
43 
44 	Each public method of the given class instance will be mapped to a HTTP
45 	route. Property methods are mapped to GET/PUT and all other methods are
46 	mapped according to their prefix verb. If the method has no known prefix,
47 	POST is used. The rest of the name is mapped to the path of the route
48 	according to the given `method_style`. Note that the prefix word must be
49 	all-lowercase and is delimited by either an upper case character, a
50 	non-alphabetic character, or the end of the string.
51 
52 	The following table lists the mappings from prefix verb to HTTP verb:
53 
54 	$(TABLE
55 		$(TR $(TH HTTP method) $(TH Recognized prefixes))
56 		$(TR $(TD GET)	  $(TD get, query, index))
57 		$(TR $(TD PUT)    $(TD set, put))
58 		$(TR $(TD POST)   $(TD add, create, post))
59 		$(TR $(TD DELETE) $(TD remove, erase, delete))
60 		$(TR $(TD PATCH)  $(TD update, patch))
61 	)
62 
63 	Method parameters will be sourced from either the query string
64 	or form data of the request, or, if the parameter name has an underscore
65 	prefixed, from the $(D vibe.http.server.HTTPServerRequest.params) map.
66 
67 	The latter can be used to inject custom data in various ways. Examples of
68 	this are placeholders specified in a `@path` annotation, values computed
69 	by a `@before` annotation, error information generated by the
70 	`@errorDisplay` annotation, or data injected manually in a HTTP method
71 	handler that processed the request prior to passing it to the generated
72 	web interface handler routes.
73 
74 	Methods that return a $(D class) or $(D interface) instance, instead of
75 	being mapped to a single HTTP route, will be mapped recursively by
76 	iterating the public routes of the returned instance. This way, complex
77 	path hierarchies can be mapped to class hierarchies.
78 
79 	Parameter_conversion_rules:
80 		For mapping method parameters without a prefixed underscore to
81 		query/form fields, the following rules are applied:
82 
83 		$(UL
84 			$(LI An array of values is mapped to
85 				$(D <parameter_name>_<index>), where $(D index)
86 				denotes the zero based index of the array entry. The length
87 				of the array is determined by searching for the first
88 				non-existent index in the set of form fields.)
89 			$(LI $(D Nullable!T) typed parameters, as well as parameters with
90 				default values, are optional parameters and are allowed to be
91 				missing in the set of form fields. All other parameter types
92 				require the corresponding field to be present and will result
93 				in a runtime error otherwise.)
94 			$(LI $(D struct) type parameters that don't define a $(D fromString)
95 				or a $(D fromStringValidate) method will be mapped to one
96 				form field per struct member with a scheme similar to how
97 				arrays are treated: $(D <parameter_name>_<member_name>))
98 			$(LI Boolean parameters will be set to $(D true) if a form field of
99 				the corresponding name is present and to $(D false) otherwise.
100 				This is compatible to how check boxes in HTML forms work.)
101 			$(LI All other types of parameters will be converted from a string
102 				by using the first available means of the following:
103 				a static $(D fromStringValidate) method, a static $(D fromString)
104 				method, using $(D std.conv.to!T).)
105 			$(LI Any of these rules can be applied recursively, so that it is
106 				possible to nest arrays and structs appropriately.)
107 		)
108 
109 	Special_parameters:
110 		$(UL
111 			$(LI A parameter named $(D __error) will be populated automatically
112 				with error information, when an $(D @errorDisplay) attribute
113 				is in use.)
114 			$(LI An $(D InputStream) typed parameter will receive the request
115 				body as an input stream. Note that this stream may be already
116 				emptied if the request was subject to certain body parsing
117 				options. See $(D vibe.http.server.HTTPServerOption).)
118 			$(LI Parameters of types $(D vibe.http.server.HTTPServerRequest),
119 				$(D vibe.http.server.HTTPServerResponse),
120 				$(D vibe.http.common.HTTPRequest) or
121 				$(D vibe.http.common.HTTPResponse) will receive the
122 				request/response objects of the invoking request.)
123 			$(LI If a parameter of the type `WebSocket` is found, the route
124 				is registered as a web socket endpoint. It will automatically
125 				upgrade the connection and pass the resulting WebSocket to
126 				the connection.)
127 		)
128 
129 
130 	Supported_attributes:
131 		The following attributes are supported for annotating methods of the
132 		registered class:
133 
134 		$(D @before), $(D @after), $(D @errorDisplay),
135 		$(D @hb.web.common.method), $(D @hb.web.common.path),
136 		$(D @hb.web.common.contentType)
137 
138 		The `@path` attribute can also be applied to the class itself, in which
139 		case it will be used as an additional prefix to the one in
140 		`WebInterfaceSettings.urlPrefix`.
141 
142 	Params:
143 		router = The HTTP router to register to
144 		instance = Class instance to use for the web interface mapping
145 		settings = Optional parameter to customize the mapping process
146 */
147 URLRouter registerWebInterface(C : Object, MethodStyle method_style = MethodStyle.lowerUnderscored)(URLRouter router, C instance, WebInterfaceSettings settings = null)
148 {
149 	import std.algorithm : endsWith;
150 	import std.traits;
151 	import vibe.internal.meta.uda : findFirstUDA;
152 
153 	if (!settings) settings = new WebInterfaceSettings;
154 
155 	string url_prefix = settings.urlPrefix;
156 	enum cls_path = findFirstUDA!(PathAttribute, C);
157 	static if (cls_path.found) {
158 		url_prefix = concatURL(url_prefix, cls_path.value, true);
159 	}
160 
161 	foreach (M; __traits(allMembers, C)) {
162 		/*static if (isInstanceOf!(SessionVar, __traits(getMember, instance, M))) {
163 			__traits(getMember, instance, M).m_getContext = toDelegate({ return s_requestContext; });
164 		}*/
165 		static if (!is(typeof(__traits(getMember, Object, M)))) { // exclude Object's default methods and field
166 			foreach (overload; MemberFunctionsTuple!(C, M)) {
167 				alias RT = ReturnType!overload;
168 				enum minfo = extractHTTPMethodAndName!(overload, true)();
169 				enum url = minfo.hadPathUDA ? minfo.url : adjustMethodStyle(minfo.url, method_style);
170 
171 				static if (findFirstUDA!(NoRouteAttribute, overload).found) {
172 					import vibe.core.log : logDebug;
173 					logDebug("Method %s.%s annotated with @noRoute - not generating a route entry.", C.stringof, M);
174 				} else static if (is(RT == class) || is(RT == interface)) {
175 					// nested API
176 					static assert(
177 						ParameterTypeTuple!overload.length == 0,
178 						"Instances may only be returned from parameter-less functions ("~M~")!"
179 					);
180 					auto subsettings = settings.dup;
181 					subsettings.urlPrefix = concatURL(url_prefix, url, true);
182 					registerWebInterface!RT(router, __traits(getMember, instance, M)(), subsettings);
183 				} else {
184 
185                     import std.meta : AliasSeq, ApplyLeft, Filter, templateAnd, templateNot;
186                     // adds underscored params to the route param parser
187                     static if (!minfo.hadPathUDA) {
188 	                    import vibe.internal.meta.funcattr : IsAttributedParameter;
189 	                    import std.range.primitives : empty;
190 	                    enum bool startsWithUnderscore(string T) = T[0] == '_';
191 	                    // TODO: make AttributedParameterMetadata map public
192 						alias hasAttributes = templateNot!(ApplyLeft!(IsAttributedParameter, overload));
193 	                    enum param_names = [Filter!(templateAnd!(startsWithUnderscore, hasAttributes), ParameterIdentifierTuple!overload)];
194 	                    // TODO: make this configurable
195                         static if (!param_names.empty) {
196                             string endurl;
197                             foreach (name; param_names) {
198                                 endurl ~= "/:" ~ name[1 .. $];
199                             }
200                             auto fullurl = concatURL(url_prefix, url.concatURL(endurl));
201                         } else {
202                             auto fullurl = concatURL(url_prefix, url);
203                         }
204 	                } else {
205 					    auto fullurl = concatURL(url_prefix, url);
206 	                }
207 				    import vibe.core.log;
208                     logDebug("[Web] registering: %s", fullurl);
209 					router.match(minfo.method, fullurl, (HTTPServerRequest req, HTTPServerResponse res) @trusted {
210 						handleRequest!(M, overload)(req, res, instance, settings);
211 					});
212 					if (settings.ignoreTrailingSlash && !fullurl.endsWith("*") && fullurl != "/") {
213 						auto m = fullurl.endsWith("/") ? fullurl[0 .. $-1] : fullurl ~ "/";
214 						router.match(minfo.method, m, delegate void (HTTPServerRequest req, HTTPServerResponse res) @safe {
215 							static if (minfo.method == HTTPMethod.GET) {
216 								URL redurl = req.fullURL;
217 								auto redpath = redurl.path;
218 								redpath.endsWithSlash = !redpath.endsWithSlash;
219 								redurl.path = redpath;
220 								res.redirect(redurl);
221 							} else {
222 								() @trusted { handleRequest!(M, overload)(req, res, instance, settings); } ();
223 							}
224 						});
225 					}
226 				}
227 			}
228 		}
229 	}
230 	return router;
231 }
232 
233 
234 /**
235 	Gives an overview of the basic features. For more advanced use, see the
236 	example in the "examples/web/" directory.
237 */
238 unittest {
239 	import vibe.http.router;
240 	import vibe.http.server;
241 	import hb.web.web;
242 
243 	class WebService {
244 		private {
245 			SessionVar!(string, "login_user") m_loginUser;
246 		}
247 
248 		@path("/")
249 		void getIndex(string _error = null)
250 		{
251 			//render!("index.dt", _error);
252 		}
253 
254 		// automatically mapped to: POST /login
255 		@errorDisplay!getIndex
256 		void postLogin(string username, string password)
257 		{
258 			enforceHTTP(username.length > 0, HTTPStatus.forbidden,
259 				"User name must not be empty.");
260 			enforceHTTP(password == "secret", HTTPStatus.forbidden,
261 				"Invalid password.");
262 			m_loginUser = username;
263 			redirect("/profile");
264 		}
265 
266 		// automatically mapped to: POST /logout
267 		void postLogout()
268 		{
269 			terminateSession();
270 			redirect("/");
271 		}
272 
273 		// automatically mapped to: GET /profile
274 		void getProfile()
275 		{
276 			enforceHTTP(m_loginUser.length > 0, HTTPStatus.forbidden,
277 				"Must be logged in to access the profile.");
278 			//render!("profile.dt")
279 		}
280 	}
281 
282 	void run()
283 	{
284 		auto router = new URLRouter;
285 		router.registerWebInterface(new WebService);
286 
287 		auto settings = new HTTPServerSettings;
288 		settings.port = 8080;
289 		listenHTTP(settings, router);
290 	}
291 }
292 
293 
294 /**
295 	Renders a Diet template file to the current HTTP response.
296 
297 	This function is equivalent to `vibe.http.server.render`, but implicitly
298 	writes the result to the response object of the currently processed
299 	request.
300 
301 	Note that this may only be called from a function/method
302 	registered using `registerWebInterface`.
303 
304 	In addition to the vanilla `render` function, this one also makes additional
305 	functionality available within the template:
306 
307 	$(UL
308 		$(LI The `req` variable that holds the current request object)
309 		$(LI If the `@translationContext` attribute us used, enables the
310 		     built-in i18n support of Diet templates)
311 	)
312 */
313 template render(string diet_file, ALIASES...) {
314 	void render(string MODULE = __MODULE__, string FUNCTION = __FUNCTION__)()
315 	{
316 		import hb.web.i18n;
317 		import vibe.internal.meta.uda : findFirstUDA;
318 		mixin("static import "~MODULE~";");
319 
320 		alias PARENT = typeof(__traits(parent, mixin(FUNCTION)).init);
321 		enum FUNCTRANS = findFirstUDA!(TranslationContextAttribute, mixin(FUNCTION));
322 		enum PARENTTRANS = findFirstUDA!(TranslationContextAttribute, PARENT);
323 		static if (FUNCTRANS.found) alias TranslateContext = FUNCTRANS.value.Context;
324 		else static if (PARENTTRANS.found) alias TranslateContext = PARENTTRANS.value.Context;
325 
326 		assert(s_requestContext.req !is null, "render() used outside of a web interface request!");
327 		auto req = s_requestContext.req;
328 
329 		struct TranslateCTX(string lang)
330 		{
331 			version (Have_diet_ng) {
332 				import diet.traits : dietTraits;
333 				@dietTraits static struct diet_translate__ {
334 					static string translate(string key, string context=null) { return tr!(TranslateContext, lang)(key, context); }
335 				}
336 			} else static string diet_translate__(string key,string context=null) { return tr!(TranslateContext, lang)(key, context); }
337 
338 			void render()
339 			{
340 				vibe.http.server.render!(diet_file, req, ALIASES, diet_translate__)(s_requestContext.res);
341 			}
342 		}
343 
344 		static if (is(TranslateContext) && TranslateContext.languages.length) {
345 			static if (TranslateContext.languages.length > 1) {
346 				switch (s_requestContext.language) {
347 					default: {
348 						TranslateCTX!(TranslateContext.languages[0]) renderctx;
349 						renderctx.render();
350 						return;
351 						}
352 					foreach (lang; TranslateContext.languages[1 .. $])
353 						case lang: {
354 							TranslateCTX!lang renderctx;
355 							renderctx.render();
356 							return;
357 							}
358 				}
359 			} else {
360 				TranslateCTX!(TranslateContext.languages[0]) renderctx;
361 				renderctx.render();
362 			}
363 		} else {
364 			vibe.http.server.render!(diet_file, req, ALIASES)(s_requestContext.res);
365 		}
366 	}
367 }
368 
369 
370 /**
371 	Redirects to the given URL.
372 
373 	The URL may either be a full URL, including the protocol and server
374 	portion, or it may be the local part of the URI (the path and an
375 	optional query string). Finally, it may also be a relative path that is
376 	combined with the path of the current request to yield an absolute
377 	path.
378 
379 	Note that this may only be called from a function/method
380 	registered using registerWebInterface.
381 */
382 void redirect(string url)
383 {
384 	import std.algorithm : canFind, endsWith, startsWith;
385 
386 	assert(s_requestContext.req !is null, "redirect() used outside of a web interface request!");
387 	alias ctx = s_requestContext;
388 	URL fullurl;
389 	if (url.startsWith("/")) {
390 		fullurl = ctx.req.fullURL;
391 		fullurl.localURI = url;
392 	} else if (url.canFind(":")) { // TODO: better URL recognition
393 		fullurl = URL(url);
394 	} else  if (ctx.req.fullURL.path.endsWithSlash) {
395 		fullurl = ctx.req.fullURL;
396 		fullurl.localURI = fullurl.path.toString() ~ url;
397 	} else {
398 		fullurl = ctx.req.fullURL.parentURL;
399 		assert(fullurl.localURI.endsWith("/"), "Parent URL not ending in a slash?!");
400 		fullurl.localURI = fullurl.localURI ~ url;
401 	}
402 	ctx.res.redirect(fullurl);
403 }
404 
405 /// sets a response header
406 void header(string name, string value)
407 {
408 	assert(s_requestContext.req !is null, "redirect() used outside of a web interface request!");
409 	alias ctx = s_requestContext;
410     ctx.res.headers[name] = value;
411 }
412 
413 /// sets the response status code
414 @property void status(int statusCode)
415 {
416 	assert(s_requestContext.req !is null, "redirect() used outside of a web interface request!");
417 	alias ctx = s_requestContext;
418 	ctx.res.statusCode = statusCode;
419 }
420 
421 /**
422 	Terminates the currently active session (if any).
423 
424 	Note that this may only be called from a function/method
425 	registered using registerWebInterface.
426 */
427 void terminateSession()
428 {
429 	alias ctx = s_requestContext;
430 	if (ctx.req.session) {
431 		ctx.res.terminateSession();
432 		ctx.req.session = Session.init;
433 	}
434 }
435 
436 
437 /**
438 	Translates text based on the language of the current web request.
439 
440 	The first overload performs a direct translation of the given translation
441 	key/text. The second overload can select from a set of plural forms
442 	based on the given integer value (msgid_plural).
443 
444 	Params:
445 		text = The translation key
446 		context = Optional context/namespace identifier (msgctxt)
447 		plural_text = Plural form of the translation key
448 		count = The quantity used to select the proper plural form of a translation
449 
450 	See_also: $(D hb.web.i18n.translationContext)
451 */
452 string trWeb(string text, string context = null)
453 {
454 	assert(s_requestContext.req !is null, "trWeb() used outside of a web interface request!");
455 	return s_requestContext.tr(text, context);
456 }
457 
458 /// ditto
459 string trWeb(string text, string plural_text, int count, string context = null) {
460 	assert(s_requestContext.req !is null, "trWeb() used outside of a web interface request!");
461 	return s_requestContext.tr_plural(text, plural_text, count, context);
462 }
463 
464 ///
465 unittest {
466 	struct TRC {
467 		import std.typetuple;
468 		alias languages = TypeTuple!("en_US", "de_DE", "fr_FR");
469 		//mixin translationModule!"test";
470 	}
471 
472 	@translationContext!TRC
473 	class WebService {
474 		void index(HTTPServerResponse res)
475 		{
476 			res.writeBody(trWeb("This text will be translated!"));
477 		}
478 	}
479 }
480 
481 
482 /**
483 	Methods marked with this attribute will not be treated as web endpoints.
484 
485 	This attribute enables the definition of public methods that do not take
486 	part in the interface genration process.
487 */
488 @property NoRouteAttribute noRoute()
489 {
490 	import hb.web.common : onlyAsUda;
491 	if (!__ctfe)
492 		assert(false, onlyAsUda!__FUNCTION__);
493 	return NoRouteAttribute.init;
494 }
495 
496 ///
497 unittest {
498 	interface IAPI {
499 		// Accessible as "GET /info"
500 		string getInfo();
501 
502 		// Not accessible over HTTP
503 		@noRoute
504 		int getFoo();
505 	}
506 }
507 
508 
509 /**
510 	Attribute to customize how errors/exceptions are displayed.
511 
512 	The first template parameter takes a function that maps an exception and an
513 	optional field name to a single error type. The result of this function
514 	will then be passed as the $(D _error) parameter to the method referenced
515 	by the second template parameter.
516 
517 	Supported types for the $(D _error) parameter are $(D bool), $(D string),
518 	$(D Exception), or a user defined $(D struct). The $(D field) member, if
519 	present, will be set to null if the exception was thrown after the field
520 	validation has finished.
521 */
522 @property errorDisplay(alias DISPLAY_METHOD)()
523 {
524 	return ErrorDisplayAttribute!DISPLAY_METHOD.init;
525 }
526 
527 /// Shows the basic error message display.
528 unittest {
529 	void getForm(string _error = null)
530 	{
531 		//render!("form.dt", _error);
532 	}
533 
534 	@errorDisplay!getForm
535 	void postForm(string name)
536 	{
537 		if (name.length == 0)
538 			throw new Exception("Name must not be empty");
539 		redirect("/");
540 	}
541 }
542 
543 /// Advanced error display including the offending form field.
544 unittest {
545 	struct FormError {
546 		// receives the original error message
547 		string error;
548 		// receives the name of the field that caused the error, if applicable
549 		string field;
550 	}
551 
552 	void getForm(FormError _error = FormError.init)
553 	{
554 		//render!("form.dt", _error);
555 	}
556 
557 	// throws an error if the submitted form value is not a valid integer
558 	@errorDisplay!getForm
559 	void postForm(int ingeter)
560 	{
561 		redirect("/");
562 	}
563 }
564 
565 /** Determines how nested D fields/array entries are mapped to form field names.
566 */
567 NestedNameStyleAttribute nestedNameStyle(NestedNameStyle style)
568 {
569 	import hb.web.common : onlyAsUda;
570 	if (!__ctfe) assert(false, onlyAsUda!__FUNCTION__);
571 	return NestedNameStyleAttribute(style);
572 }
573 
574 ///
575 unittest {
576 	struct Items {
577 		int[] entries;
578 	}
579 
580 	@nestedNameStyle(NestedNameStyle.d)
581 	class MyService {
582 		// expects fields in D native style:
583 		// "items.entries[0]", "items.entries[1]", ...
584 		void postItems(Items items)
585 		{
586 
587 		}
588 	}
589 }
590 
591 
592 /**
593 	Encapsulates settings used to customize the generated web interface.
594 */
595 class WebInterfaceSettings {
596 	string urlPrefix = "/";
597 	bool ignoreTrailingSlash = true;
598 
599 	@property WebInterfaceSettings dup() const {
600 		auto ret = new WebInterfaceSettings;
601 		ret.urlPrefix = this.urlPrefix;
602 		ret.ignoreTrailingSlash = this.ignoreTrailingSlash;
603 		return ret;
604 	}
605 }
606 
607 
608 /**
609 	Maps a web interface member variable to a session field.
610 
611 	Setting a SessionVar variable will implicitly start a session, if none
612 	has been started yet. The content of the variable will be stored in
613 	the session store and is automatically serialized and deserialized.
614 
615 	Note that variables of type SessionVar must only be used from within
616 	handler functions of a class that was registered using
617 	$(D registerWebInterface). Also note that two different session
618 	variables with the same $(D name) parameter will access the same
619 	underlying data.
620 */
621 struct SessionVar(T, string name) {
622 	private {
623 		T m_initValue;
624 	}
625 
626 	/** Initializes a session var with a constant value.
627 	*/
628 	this(T init_val) { m_initValue = init_val; }
629 	///
630 	unittest {
631 		class MyService {
632 			SessionVar!(int, "someInt") m_someInt = 42;
633 
634 			void index() {
635 				assert(m_someInt == 42);
636 			}
637 		}
638 	}
639 
640 	/** Accesses the current value of the session variable.
641 
642 		Any access will automatically start a new session and set the
643 		initializer value, if necessary.
644 	*/
645 	@property const(T) value()
646 	{
647 		assert(s_requestContext.req !is null, "SessionVar used outside of a web interface request!");
648 		alias ctx = s_requestContext;
649 		if (!ctx.req.session) ctx.req.session = ctx.res.startSession();
650 
651 		if (ctx.req.session.isKeySet(name))
652 			return ctx.req.session.get!T(name);
653 
654 		ctx.req.session.set!T(name, m_initValue);
655 		return m_initValue;
656 	}
657 	/// ditto
658 	@property void value(T new_value)
659 	{
660 		assert(s_requestContext.req !is null, "SessionVar used outside of a web interface request!");
661 		alias ctx = s_requestContext;
662 		if (!ctx.req.session) ctx.req.session = ctx.res.startSession();
663 		ctx.req.session.set(name, new_value);
664 	}
665 
666 	void opAssign(T new_value) { this.value = new_value; }
667 
668 	alias value this;
669 }
670 
671 private struct NoRouteAttribute {}
672 
673 private struct ErrorDisplayAttribute(alias DISPLAY_METHOD) {
674 	import std.traits : ParameterTypeTuple, ParameterIdentifierTuple;
675 
676 	alias displayMethod = DISPLAY_METHOD;
677 	enum displayMethodName = __traits(identifier, DISPLAY_METHOD);
678 
679 	private template GetErrorParamType(size_t idx) {
680 		static if (idx >= ParameterIdentifierTuple!DISPLAY_METHOD.length)
681 			static assert(false, "Error display method "~displayMethodName~" is missing the _error parameter.");
682 		else static if (ParameterIdentifierTuple!DISPLAY_METHOD[idx] == "_error")
683 			alias GetErrorParamType = ParameterTypeTuple!DISPLAY_METHOD[idx];
684 		else alias GetErrorParamType = GetErrorParamType!(idx+1);
685 	}
686 
687 	alias ErrorParamType = GetErrorParamType!0;
688 
689 	ErrorParamType getError(Exception ex, string field)
690 	{
691 		static if (is(ErrorParamType == bool)) return true;
692 		else static if (is(ErrorParamType == string)) return ex.msg;
693 		else static if (is(ErrorParamType == Exception)) return ex;
694 		else static if (is(typeof(ErrorParamType(ex, field)))) return ErrorParamType(ex, field);
695 		else static if (is(typeof(ErrorParamType(ex.msg, field)))) return ErrorParamType(ex.msg, field);
696 		else static if (is(typeof(ErrorParamType(ex.msg)))) return ErrorParamType(ex.msg);
697 		else static assert(false, "Error parameter type %s does not have the required constructor.");
698 	}
699 }
700 
701 private struct NestedNameStyleAttribute { NestedNameStyle value; }
702 
703 
704 private {
705 	TaskLocal!RequestContext s_requestContext;
706 }
707 
708 private struct RequestContext {
709 	HTTPServerRequest req;
710 	HTTPServerResponse res;
711 	string language;
712 	string function(string, string) @safe tr;
713 	string function(string, string, int, string) @safe tr_plural;
714 }
715 
716 private void handleRequest(string M, alias overload, C, ERROR...)(HTTPServerRequest req, HTTPServerResponse res, C instance, WebInterfaceSettings settings, ERROR error)
717 	if (ERROR.length <= 1)
718 {
719 	import std.algorithm : countUntil, startsWith;
720 	import std.traits;
721 	import std.typetuple : Filter, staticIndexOf;
722 	import vibe.core.stream;
723 	import vibe.data.json;
724 	import vibe.internal.meta.funcattr;
725 	import vibe.internal.meta.uda : findFirstUDA;
726 
727 	alias RET = ReturnType!overload;
728 	alias PARAMS = ParameterTypeTuple!overload;
729 	alias default_values = ParameterDefaultValueTuple!overload;
730 	alias AuthInfoType = AuthInfo!C;
731 	enum param_names = [ParameterIdentifierTuple!overload];
732 	enum erruda = findFirstUDA!(ErrorDisplayAttribute, overload);
733 
734 	static if (findFirstUDA!(NestedNameStyleAttribute, C).found)
735 		enum nested_style = findFirstUDA!(NestedNameStyleAttribute, C).value.value;
736 	else enum nested_style = NestedNameStyle.underscore;
737 
738 	s_requestContext = createRequestContext!overload(req, res);
739     enum hasAuth = isAuthenticated!(C, overload); // true for all routes that need authentication
740 
741 	static if (hasAuth) {
742 		auto auth_info = handleAuthentication!overload(instance, req, res);
743 		if (res.headerWritten) return;
744 	}
745 
746 	// collect all parameter values
747 	PARAMS params = void; // FIXME: in case of errors, destructors could be called on uninitialized variables!
748 	foreach (i, PT; PARAMS) {
749 		bool got_error = false;
750 		ParamError err;
751 		err.field = param_names[i];
752 		try {
753 			static if (hasAuth && is(PT == AuthInfoType)) {
754 				params[i] = auth_info;
755 			} else static if (IsAttributedParameter!(overload, param_names[i])) {
756 				params[i].setVoid(computeAttributedParameterCtx!(overload, param_names[i])(instance, req, res));
757 				if (res.headerWritten) return;
758 			}
759 			else static if (param_names[i] == "_error") {
760 				static if (ERROR.length == 1)
761 					params[i].setVoid(error[0]);
762 				else static if (!is(default_values[i] == void))
763 					params[i].setVoid(default_values[i]);
764 				else
765 					params[i] = typeof(params[i]).init;
766 			}
767 			else static if (is(PT == InputStream)) params[i] = req.bodyReader;
768 			else static if (is(PT == HTTPServerRequest) || is(PT == HTTPRequest)) params[i] = req;
769 			else static if (is(PT == HTTPServerResponse) || is(PT == HTTPResponse)) params[i] = res;
770 			else static if (is(PT == Json)) params[i] = req.json;
771 			else static if (is(PT == WebSocket)) {} // handled below
772 			else static if (param_names[i].startsWith("_")) {
773 				if (auto pv = param_names[i][1 .. $] in req.params) {
774 					got_error = !webConvTo(*pv, params[i], err);
775 					// treat errors in route parameters as non-match
776 					// FIXME: verify that the parameter is actually a route parameter!
777 					if (got_error) return;
778 				} else static if (!is(default_values[i] == void)) params[i].setVoid(default_values[i]);
779 				else static if (!isNullable!PT) enforceHTTP(false, HTTPStatus.badRequest, "Missing request parameter for "~param_names[i]);
780 			} else static if (is(PT == bool)) {
781 				params[i] = param_names[i] in req.form || param_names[i] in req.query;
782 			} else {
783 				enum has_default = !is(default_values[i] == void);
784 		        import vibe.core.log;
785 		        import std.algorithm.comparison : among;
786 		        // TODO: handle path parameters (and query, form, headers)
787 				logDebug("Trying to read %s", param_names[i]);
788 				ParamResult pres = void;
789 				// TODO: more intuitive distinguishment between json and form
790 				if (req.method.among(HTTPMethod.POST, HTTPMethod.PUT) && req.json.type != Json.Type.undefined && i == 0) {
791                     params[i].setVoid(req.json.deserializeJson!PT);
792                     pres = ParamResult.ok; // Other: error, skipped
793 				    logDebug("Read %s", param_names[i]);
794                 } else {
795                     pres = readFormParamRec(req, params[i], param_names[i], !has_default, nested_style, err);
796             	}
797                 static if (has_default) {
798                     if (pres == ParamResult.skipped)
799                         params[i].setVoid(default_values[i]);
800                 } else assert(pres != ParamResult.skipped);
801 
802                 if (pres == ParamResult.error) {
803                     got_error = true;
804                 }
805             }
806 		} catch (HTTPStatusException ex) {
807 			throw ex;
808 		} catch (Exception ex) {
809 			got_error = true;
810 			err.text = ex.msg;
811 			err.debugText = ex.toString().sanitize;
812 		}
813 
814 		if (got_error) {
815 			static if (erruda.found && ERROR.length == 0) {
816 				auto errnfo = erruda.value.getError(new Exception(err.text), err.field);
817 				handleRequest!(erruda.value.displayMethodName, erruda.value.displayMethod)(req, res, instance, settings, errnfo);
818 				return;
819 			} else {
820 				auto hex = new HTTPStatusException(HTTPStatus.badRequest, "Error handling field "~err.field~": "~err.text);
821 				hex.debugMessage = err.debugText;
822 				throw hex;
823 			}
824 		}
825 	}
826 
827 	// validate all confirmation parameters
828 	foreach (i, PT; PARAMS) {
829 		static if (isNullable!PT)
830 			alias ParamBaseType = typeof(PT.init.get());
831 		else alias ParamBaseType = PT;
832 
833 		static if (isInstanceOf!(Confirm, ParamBaseType)) {
834 			enum pidx = param_names.countUntil(PT.confirmedParameter);
835 			static assert(pidx >= 0, "Unknown confirmation parameter reference \""~PT.confirmedParameter~"\".");
836 			static assert(pidx != i, "Confirmation parameter \""~PT.confirmedParameter~"\" may not reference itself.");
837 
838 			bool matched;
839 			static if (isNullable!PT && isNullable!(PARAMS[pidx])) {
840 				matched = (params[pidx].isNull() && params[i].isNull()) ||
841 					(!params[pidx].isNull() && !params[i].isNull() && params[pidx] == params[i]);
842 			} else {
843 				static assert(!isNullable!PT && !isNullable!(PARAMS[pidx]),
844 					"Either both or none of the confirmation and original fields must be nullable.");
845 				matched = params[pidx] == params[i];
846 			}
847 
848 			if (!matched) {
849 				auto ex = new Exception("Comfirmation field mismatch.");
850 				static if (erruda.found && ERROR.length == 0) {
851 					auto err = erruda.value.getError(ex, param_names[i]);
852 					handleRequest!(erruda.value.displayMethodName, erruda.value.displayMethod)(req, res, instance, settings, err);
853 					return;
854 				} else {
855 					throw new HTTPStatusException(HTTPStatus.badRequest, ex.msg);
856 				}
857 			}
858 		}
859 	}
860 
861 	static if (hasAuth)
862 		handleAuthorization!(C, overload, params)(auth_info);
863 
864 	// execute the method and write the result
865 	try {
866 		import vibe.internal.meta.funcattr;
867 
868 		static if (staticIndexOf!(WebSocket, PARAMS) >= 0) {
869 			static assert(is(RET == void), "WebSocket handlers must return void.");
870 			handleWebSocket((scope ws) {
871 				foreach (i, PT; PARAMS)
872 					static if (is(PT == WebSocket))
873 						params[i] = ws;
874 
875 				__traits(getMember, instance, M)(params);
876 			}, req, res);
877 		} else static if (is(RET == void)) {
878 			__traits(getMember, instance, M)(params);
879 		} else {
880 			auto ret = __traits(getMember, instance, M)(params);
881 			ret = evaluateOutputModifiers!overload(ret, req, res);
882 
883 			static if (is(RET : Json)) {
884 				res.writeJsonBody(ret);
885 			} else static if (is(RET : InputStream) || is(RET : const ubyte[])) {
886 				enum type = findFirstUDA!(ContentTypeAttribute, overload);
887 				static if (type.found) {
888 					res.writeBody(ret, type.value);
889 				} else {
890 					res.writeBody(ret);
891 				}
892             } else static if (is(RET : string)) {
893 			    res.writeBody(ret);
894 			} else {
895 			    res.writeJsonBody(ret);
896 				//static assert(is(RET == void), M~": Only InputStream, Json and void are supported as return types for route methods.");
897 			}
898 		}
899 	} catch (Exception ex) {
900 		import vibe.core.log;
901 		logDebug("Web handler %s has thrown: %s", M, ex);
902 		static if (erruda.found && ERROR.length == 0) {
903 			auto err = erruda.value.getError(ex, null);
904 			handleRequest!(erruda.value.displayMethodName, erruda.value.displayMethod)(req, res, instance, settings, err);
905 		} else throw ex;
906 	}
907 }
908 
909 
910 private RequestContext createRequestContext(alias handler)(HTTPServerRequest req, HTTPServerResponse res)
911 @safe {
912 	RequestContext ret;
913 	ret.req = req;
914 	ret.res = res;
915 	ret.language = determineLanguage!handler(req);
916 
917 	import hb.web.i18n;
918 	import vibe.internal.meta.uda : findFirstUDA;
919 
920 	alias PARENT = typeof(__traits(parent, handler).init);
921 	enum FUNCTRANS = findFirstUDA!(TranslationContextAttribute, handler);
922 	enum PARENTTRANS = findFirstUDA!(TranslationContextAttribute, PARENT);
923 	static if (FUNCTRANS.found) alias TranslateContext = FUNCTRANS.value.Context;
924 	else static if (PARENTTRANS.found) alias TranslateContext = PARENTTRANS.value.Context;
925 
926 	static if (is(TranslateContext) && TranslateContext.languages.length) {
927 		static if (TranslateContext.languages.length > 1) {
928 			switch (ret.language) {
929 				default:
930 					ret.tr = &tr!(TranslateContext, TranslateContext.languages[0]);
931 					ret.tr_plural = &tr!(TranslateContext, TranslateContext.languages[0]);
932 					break;
933 				foreach (lang; TranslateContext.languages[1 .. $]) {
934 					case lang:
935 						ret.tr = &tr!(TranslateContext, lang);
936 						ret.tr_plural = &tr!(TranslateContext, lang);
937 						break;
938 				}
939 			}
940 		} else {
941 			ret.tr = &tr!(TranslateContext, TranslateContext.languages[0]);
942 			ret.tr_plural = &tr!(TranslateContext, TranslateContext.languages[0]);
943 		}
944 	} else {
945 		ret.tr = (t,c) => t;
946 		// Without more knowledge about the requested language, the best we can do is return the msgid as a hint
947 		// that either a po file is needed for the language, or that a translation entry does not exist for the msgid.
948 		ret.tr_plural = (txt,ptxt,cnt,ctx) => !ptxt.length || cnt == 1 ? txt : ptxt;
949 	}
950 
951 	return ret;
952 }