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 }