1 /** 2 * Copyright © DiamondMVC 2018 3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module diamond.controllers.controller; 7 8 import diamond.core.apptype; 9 10 static if (isWeb) 11 { 12 import std..string : strip, format; 13 import std.traits : fullyQualifiedName, hasUDA, getUDAs; 14 import std.array : split, array; 15 16 public import diamond.http; 17 public import diamond.controllers.authentication; 18 public import diamond.authentication; 19 20 import diamond.controllers.action; 21 import diamond.controllers.status; 22 import diamond.controllers.basecontroller; 23 import diamond.controllers.attributes; 24 import diamond.core.collections; 25 import diamond.core..string : firstToLower; 26 import diamond.errors; 27 import diamond.controllers.rest; 28 import diamond.security; 29 30 package(diamond.controllers) 31 { 32 /// Specially mapped routes. 33 RoutePart[][string] _mappedRoutes; 34 /// Mapped controllers. 35 HashSet!string _mappedControllers; 36 } 37 38 /// The format used for default mappings. 39 enum defaultMappingFormat = q{ 40 static if (hasUDA!(%1$s.%2$s, HttpDefault)) 41 { 42 mapDefault(&controller.%2$s); 43 } 44 }; 45 46 /// The format used for mandatory formats. 47 enum mandatoryMappingFormat = q{ 48 static if (hasUDA!(%1$s.%2$s, HttpMandatory)) 49 { 50 mapMandatory(&controller.%2$s); 51 } 52 }; 53 54 /// The format for creating the http acton member. 55 enum actionNameFormat = "static HttpAction action_%2$s;\r\n"; 56 57 /// The format used for actions. 58 enum actionMappingFormat = q{ 59 static if (hasUDA!(%1$s.%2$s, HttpAction)) 60 { 61 static action_%2$s = getUDAs!(%1$s.%2$s, HttpAction)[0]; 62 63 if (action_%2$s.action && action_%2$s.action.strip().length) 64 { 65 auto routingData = _mappedRoutes.get("%2$s", null); 66 67 if (!routingData && !_mappedControllers[fullyQualifiedName!TController]) 68 { 69 routingData = parseRoute(action_%2$s.action); 70 71 if (routingData && routingData.length > 1) 72 { 73 _mappedRoutes["%2$s"] = routingData; 74 75 action_%2$s.action = routingData[0].identifier; 76 } 77 } 78 79 if (routingData && routingData.length) 80 { 81 if (routingData[0].identifier == "<>") 82 { 83 action_%2$s.action = null; 84 } 85 } 86 } 87 88 mapAction( 89 action_%2$s.method, 90 ( 91 action_%2$s.action && action_%2$s.action.strip().length ? 92 action_%2$s.action : "%2$s" 93 ).firstToLower(), 94 &controller.%2$s 95 ); 96 } 97 }; 98 99 /// The format used for disabled authentication. 100 enum disableAuthFormat = q{ 101 static if (hasUDA!(%1$s.%2$s, HttpDisableAuth)) 102 { 103 static if (hasUDA!(%1$s.%2$s, HttpDefault)) 104 { 105 _disabledAuth.add("/"); 106 } 107 else static if (hasUDA!(%1$s.%2$s, HttpAction)) 108 { 109 _disabledAuth.add( 110 ( 111 action_%2$s.action && action_%2$s.action.strip().length ? 112 action_%2$s.action : "%2$s" 113 ).firstToLower() 114 ); 115 } 116 } 117 }; 118 119 /// The format used for restricted connections. 120 enum restrictedFormat = q{ 121 static if (hasUDA!(%1$s.%2$s, HttpRestricted)) 122 { 123 static if (hasUDA!(%1$s.%2$s, HttpDefault)) 124 { 125 _restrictedActions.add("/"); 126 } 127 else static if (hasUDA!(%1$s.%2$s, HttpAction)) 128 { 129 _restrictedActions.add( 130 ( 131 action_%2$s.action && action_%2$s.action.strip().length ? 132 action_%2$s.action : "%2$s" 133 ).firstToLower() 134 ); 135 } 136 } 137 }; 138 } 139 140 // WebServer's will have a view associated with the controller, the view then contains information about the request etc. 141 static if (isWebServer) 142 { 143 public import diamond.views.view; 144 145 /// Wrapper around a controller. 146 class Controller(TView) : BaseController 147 { 148 private: 149 /// The view associatedi with the controller. 150 TView _view; 151 152 /// The authentication used for the controller. 153 IControllerAuth _auth; 154 155 /// Hash set of actions with disabled authentication. 156 HashSet!string _disabledAuth; 157 158 /// Hash set of actions with restrictions. 159 HashSet!string _restrictedActions; 160 161 /// The successor to this controller. 162 BaseController _successorController; 163 164 /// The version in which the successor controller can be used. 165 string _successorVersion; 166 167 protected: 168 /** 169 * Creates a new controller. 170 * Params: 171 * view = The view associated with the controller. 172 */ 173 this(this TController)(TView view) 174 { 175 super(); 176 177 _view = view; 178 179 mixin("import diamondapp : " ~ TController.stringof.split("!")[1][1 .. $-1] ~ ";"); 180 181 import controllers; 182 auto controller = cast(TController)this; 183 184 static if (hasUDA!(TController, HttpAuthentication)) 185 { 186 enum authenticationUDA = getUDAs!(TController, HttpAuthentication)[0]; 187 188 mixin("_auth = new " ~ authenticationUDA.authenticationClass ~ ";"); 189 _disabledAuth = new HashSet!string; 190 } 191 192 static if (hasUDA!(TController, HttpVersion)) 193 { 194 import std..string : indexOf; 195 196 enum versionUDA = getUDAs!(TController, HttpVersion)[0]; 197 198 mixin 199 ( 200 "_successorController = new " ~ 201 versionUDA.versionControllerClass[0 .. versionUDA.versionControllerClass.indexOf('(')] ~ 202 "!" ~ TView.stringof ~ "(_view);" 203 ); 204 _successorVersion = versionUDA.versionName; 205 } 206 207 _restrictedActions = new HashSet!string; 208 209 auto fullName = fullyQualifiedName!TController; 210 211 if (!_mappedControllers) 212 { 213 _mappedControllers = new HashSet!string; 214 } 215 216 foreach (member; __traits(derivedMembers, TController)) 217 { 218 static if (member != "__ctor") 219 { 220 mixin(defaultMappingFormat.format(TController.stringof, member)); 221 mixin(mandatoryMappingFormat.format(TController.stringof, member)); 222 mixin(actionMappingFormat.format(TController.stringof, member)); 223 mixin(disableAuthFormat.format(TController.stringof, member)); 224 mixin(restrictedFormat.format(TController.stringof, member)); 225 } 226 } 227 228 if (!_mappedControllers[fullName]) 229 { 230 _mappedControllers.add(fullName); 231 } 232 } 233 234 public: 235 final: 236 @property 237 { 238 /// Gets the view. 239 TView view() { return _view; } 240 } 241 242 /** 243 * Generates a json response. 244 * Params: 245 * jsonObject = The object to serialize as json. 246 * Returns: 247 * A status of Status.end 248 */ 249 Status json(T)(T jsonObject) 250 { 251 import vibe.d : serializeToJsonString; 252 return jsonString(jsonObject.serializeToJsonString()); 253 } 254 255 /** 256 * Generates a json response from a json string. 257 * Params: 258 * s = The json string. 259 * Returns: 260 * A status of Status.end 261 */ 262 Status jsonString(string s) 263 { 264 import diamond.core.webconfig; 265 foreach (headerKey,headerValue; webConfig.defaultHeaders.general) 266 { 267 _view.client.rawResponse.headers[headerKey] = headerValue; 268 } 269 270 _view.client.rawResponse.headers["Content-Type"] = "text/json; charset=UTF-8"; 271 _view.client.write(s); 272 273 return Status.end; 274 } 275 276 /** 277 * Redirects the response to a specific url. 278 * Params: 279 * url = The url to redirect to. 280 * status = The status of the redirection. (Default is HTTPStatus.Found) 281 * Returns: 282 * The status required for the redirection to work properly. (Status.end) 283 */ 284 Status redirectTo(string url, HttpStatus status = HttpStatus.found) 285 { 286 _view.client.redirect(url, status); 287 288 return Status.end; 289 } 290 291 /** 292 * Gets a value from the route's parameters by index. 293 * Params: 294 * index = The index. 295 * defaultValue = The default value. 296 * Returns: 297 * The value from the route's parameters if found, else the default value. 298 */ 299 T getByIndex(T)(size_t index, T defaultValue = T.init) 300 { 301 if (index < 0 || index >= _view.route.params.length) 302 { 303 return defaultValue; 304 } 305 306 return _view.route.getData!T(index); 307 } 308 309 /** 310 * Handles the view's current controller action. 311 * Returns: 312 * The status of the controller action. 313 */ 314 final override Status handle() 315 { 316 if (_successorController && _view.client.route.action == _successorVersion) 317 { 318 _view.client.route.passDataToAction(); 319 320 auto status = _successorController.handle(); 321 322 if (status != Status.notFound) 323 { 324 return status; 325 } 326 } 327 328 if (_view.isDefaultRoute) 329 { 330 if (_restrictedActions["/"]) 331 { 332 validateRestrictedIPs(view.client); 333 } 334 335 if (_auth && !_disabledAuth["/"]) 336 { 337 auto authStatus = _auth.isAuthenticated(view.client); 338 339 if (!authStatus || !authStatus.authenticated) 340 { 341 _auth.authenticationFailed(authStatus); 342 return Status.end; 343 } 344 } 345 346 if (_mandatoryAction) 347 { 348 auto mandatoryResult = _mandatoryAction(); 349 350 if (mandatoryResult != Status.success) 351 { 352 return mandatoryResult; 353 } 354 } 355 356 if (_defaultAction) 357 { 358 return _defaultAction(); 359 } 360 361 return Status.success; 362 } 363 364 ActionEntry methodEntries = _actions.get(_view.httpMethod, null); 365 366 if (!methodEntries) 367 { 368 return Status.notFound; 369 } 370 371 auto action = methodEntries.get(_view.route.action, null); 372 373 if (!action) 374 { 375 return Status.notFound; 376 } 377 378 if (_restrictedActions[_view.route.action]) 379 { 380 validateRestrictedIPs(view.client); 381 } 382 383 if (_auth && !_disabledAuth[_view.route.action]) 384 { 385 auto authStatus = _auth.isAuthenticated(view.client); 386 387 if (!authStatus || !authStatus.authenticated) 388 { 389 _auth.authenticationFailed(authStatus); 390 return Status.end; 391 } 392 } 393 394 if (_mandatoryAction) 395 { 396 auto mandatoryResult = _mandatoryAction(); 397 398 if (mandatoryResult != Status.success) 399 { 400 return mandatoryResult; 401 } 402 } 403 404 auto routeData = _mappedRoutes.get(_view.route.action, null); 405 406 if (routeData) 407 { 408 validateRoute(routeData, view.route.params); 409 } 410 411 return action(); 412 } 413 414 import diamond.extensions; 415 mixin ExtensionEmit!(ExtensionType.controllerExtension, q{ 416 mixin {{extensionEntry}}.extensions; 417 }); 418 } 419 } 420 // A webapi will not have a view associated with it, thus all information such as the request etc. is available within the controller 421 else static if (isWebApi) 422 { 423 /// Wrapper around a controller. 424 class Controller : BaseController 425 { 426 private: 427 /// The client. 428 HttpClient _client; 429 430 /// The authentication used for the controller. 431 IControllerAuth _auth; 432 433 /// Hash set of actions with disabled authentication. 434 HashSet!string _disabledAuth; 435 436 /// Hash set of actions with restrictions. 437 HashSet!string _restrictedActions; 438 439 /// The successor to this controller. 440 BaseController _successorController; 441 442 /// The version in which the successor controller can be used. 443 string _successorVersion; 444 445 protected: 446 /** 447 * Creates a new controller. 448 * Params: 449 * client = The client of the controller. 450 * controller = The controller itself 451 */ 452 this(this TController)(HttpClient client) 453 { 454 super(); 455 456 _client = client; 457 458 import controllers; 459 auto controller = cast(TController)this; 460 461 static if (hasUDA!(TController, HttpAuthentication)) 462 { 463 enum authenticationUDA = getUDAs!(TController, HttpAuthentication)[0]; 464 465 mixin("_auth = new " ~ authenticationUDA.authenticationClass ~ ";"); 466 _disabledAuth = new HashSet!string; 467 } 468 469 static if (hasUDA!(TController, HttpVersion)) 470 { 471 enum versionUDA = getUDAs!(TController, HttpVersion)[0]; 472 473 mixin("_successorController = new " ~ versionUDA.versionControllerClass ~ "(_client);"); 474 _successorVersion = versionUDA.versionName; 475 } 476 477 _restrictedActions = new HashSet!string; 478 479 auto fullName = fullyQualifiedName!TController; 480 481 if (!_mappedControllers) 482 { 483 _mappedControllers = new HashSet!string; 484 } 485 486 foreach (member; __traits(derivedMembers, TController)) 487 { 488 static if (member != "__ctor") 489 { 490 mixin(defaultMappingFormat.format(TController.stringof, member)); 491 mixin(mandatoryMappingFormat.format(TController.stringof, member)); 492 mixin(actionMappingFormat.format(TController.stringof, member)); 493 mixin(disableAuthFormat.format(TController.stringof, member)); 494 mixin(restrictedFormat.format(TController.stringof, member)); 495 } 496 } 497 498 if (!_mappedControllers[fullName]) 499 { 500 _mappedControllers.add(fullName); 501 } 502 } 503 504 public: 505 final: 506 @property 507 { 508 /// Gets the client. 509 auto client() { return _client; } 510 /// Gets the http method. 511 auto httpMethod() { return _client.method; } 512 /// Gets the route. 513 auto route() { return client.route; } 514 } 515 516 /** 517 * Generates a json response. 518 * Params: 519 * jsonObject = The object to serialize as json. 520 * Returns: 521 * A status of Status.end 522 */ 523 Status json(T)(T jsonObject) 524 { 525 import vibe.d : serializeToJsonString; 526 527 return jsonString(jsonObject.serializeToJsonString()); 528 } 529 530 /** 531 * Generates a json response from a json string. 532 * Params: 533 * s = The json string. 534 * Returns: 535 * A status of Status.end 536 */ 537 Status jsonString(string s) 538 { 539 import diamond.core.webconfig; 540 foreach (headerKey,headerValue; webConfig.defaultHeaders.general) 541 { 542 _client.rawResponse.headers[headerKey] = headerValue; 543 } 544 545 _client.rawResponse.headers["Content-Type"] = "text/json; charset=UTF-8"; 546 _client.write(s); 547 548 return Status.end; 549 } 550 551 /** 552 * Redirects the response to a specific url. 553 * Params: 554 * url = The url to redirect to. 555 * status = The status of the redirection. (Default is HTTPStatus.Found) 556 * Returns: 557 * The status required for the redirection to work properly. (Status.end) 558 */ 559 Status redirectTo(string url, HttpStatus status = HttpStatus.found) 560 { 561 _client.redirect(url, status); 562 563 return Status.end; 564 } 565 566 /** 567 * Gets a value from the route's parameters by index. 568 * Params: 569 * index = The index. 570 * defaultValue = The default value. 571 * Returns: 572 * The value from the route's parameters if found, else the default value. 573 */ 574 T getByIndex(T)(size_t index, T defaultValue = T.init) 575 { 576 if (index < 0 || index >= route.params.length) 577 { 578 return defaultValue; 579 } 580 581 return route.getData!T(index); 582 } 583 584 /** 585 * Handles the current controller action. 586 * Returns: 587 * The status of the controller action. 588 */ 589 final override Status handle() 590 { 591 if (_successorController && route.action == _successorVersion) 592 { 593 route.passDataToAction(); 594 595 auto status = _successorController.handle(); 596 597 if (status != Status.notFound) 598 { 599 return status; 600 } 601 } 602 603 if (!route.action) 604 { 605 if (_restrictedActions["/"]) 606 { 607 validateRestrictedIPs(_client); 608 } 609 610 if (_auth && !_disabledAuth["/"]) 611 { 612 auto authStatus = _auth.isAuthenticated(_client); 613 614 if (!authStatus || !authStatus.authenticated) 615 { 616 _auth.authenticationFailed(authStatus); 617 return Status.end; 618 } 619 } 620 621 if (_mandatoryAction) 622 { 623 auto mandatoryResult = _mandatoryAction(); 624 625 if (mandatoryResult != Status.success) 626 { 627 return mandatoryResult; 628 } 629 } 630 631 if (_defaultAction) 632 { 633 return _defaultAction(); 634 } 635 636 return Status.notFound; 637 } 638 639 ActionEntry methodEntries = _actions.get(httpMethod, null); 640 641 if (!methodEntries) 642 { 643 return Status.notFound; 644 } 645 646 auto action = methodEntries.get(route.action, null); 647 648 if (!action) 649 { 650 return Status.notFound; 651 } 652 653 if (_restrictedActions[route.action]) 654 { 655 validateRestrictedIPs(client); 656 } 657 658 if (_auth && !_disabledAuth[route.action]) 659 { 660 auto authStatus = _auth.isAuthenticated(client); 661 662 if (!authStatus || !authStatus.authenticated) 663 { 664 _auth.authenticationFailed(authStatus); 665 return Status.end; 666 } 667 } 668 669 if (_mandatoryAction) 670 { 671 auto mandatoryResult = _mandatoryAction(); 672 673 if (mandatoryResult != Status.success) 674 { 675 return mandatoryResult; 676 } 677 } 678 679 auto routeData = _mappedRoutes.get(route.action, null); 680 681 if (routeData) 682 { 683 validateRoute(routeData, route.params); 684 } 685 686 return action(); 687 } 688 689 import diamond.extensions; 690 mixin ExtensionEmit!(ExtensionType.controllerExtension, q{ 691 mixin {{extensionEntry}}.extensions; 692 }); 693 } 694 695 /// Mixin template for generating the controllers. 696 mixin template GenerateControllers(string[] controllerInitializers) 697 { 698 import std.traits : hasUDA, getUDAs; 699 import controllers; 700 701 /// Format for generating the routes for controllers. 702 private enum generateFormat = q{ 703 static if (hasUDA!(%sController, HttpRoutes)) 704 { 705 static const controller_%s = getUDAs!(%sController, HttpRoutes)[0]; 706 707 foreach (controllerRoute; controller_%s.routes) 708 { 709 import diamond.core..string : firstToLower; 710 711 controllerCollection[controllerRoute.firstToLower()] = new GenerateControllerAction((client) 712 { 713 return new %sController(client); 714 }); 715 } 716 } 717 else 718 { 719 controllerCollection["%s"] = new GenerateControllerAction((client) 720 { 721 return new %sController(client); 722 }); 723 } 724 }; 725 726 /// Generates the controller collection. 727 string generateControllerCollection() 728 { 729 auto controllerCollectionResult = ""; 730 731 foreach (controller; controllerInitializers) 732 { 733 import std..string : format; 734 controllerCollectionResult ~= 735 format 736 ( 737 generateFormat, 738 controller, controller, controller, 739 controller, controller, 740 controller.firstToLower(), 741 controller 742 ); 743 } 744 745 return controllerCollectionResult; 746 } 747 748 /// The controller collection. 749 GenerateControllerAction[string] controllerCollection; 750 751 /// Gets a controller by its name 752 GenerateControllerAction getControllerAction(string name) 753 { 754 if (!controllerCollection || !controllerCollection.length) 755 { 756 mixin(generateControllerCollection); 757 } 758 759 return controllerCollection.get(name, null); 760 } 761 } 762 }