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 }