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