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