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