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 }