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