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