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.views.view; 7 8 import diamond.core.apptype; 9 10 static if (!isWebApi) 11 { 12 import std..string : format, strip; 13 import std.array : join, replace, split, array; 14 import std.conv : to; 15 import std.algorithm : filter; 16 17 import diamond.errors.checks; 18 19 static if (isWebServer) 20 { 21 import diamond.http; 22 } 23 24 /** 25 * Template to get the type name of a view. 26 * Params: 27 * name = The name of the view. 28 */ 29 template ViewTypeName(string name) 30 { 31 mixin("alias ViewTypeName = view_" ~ name ~ ";"); 32 } 33 34 /// The abstract wrapper for views. 35 abstract class View 36 { 37 private: 38 static if (isWebServer) 39 { 40 /// The client. 41 HttpClient _client; 42 43 /// Boolean determnining whether the view can be cached or not. 44 bool _cached; 45 } 46 47 /// The name of the view. 48 string _name; 49 50 /// The place holders. 51 string[string] _placeHolders; 52 53 /// The result. 54 string _result; 55 56 /// The layout view. 57 string _layoutName; 58 59 /// Boolean determining whether the page rendering is delayed. 60 bool _delayRender; 61 62 /// The view that's currently rendering the layout view. 63 View _renderView; 64 65 /// Boolean determining whether the view generation is raw or if it should call controllers etc. 66 bool _rawGenerate; 67 68 protected: 69 static if (isWebServer) 70 { 71 /** 72 * Creates a new view. 73 * Params: 74 * client = The client. 75 * name = The name of the view. 76 */ 77 this(HttpClient client, string name) 78 { 79 _client = enforceInput(client, "Cannot create a view without an associated client."); 80 _name = name; 81 82 _placeHolders["doctype"] = "<!DOCTYPE html>"; 83 _placeHolders["defaultRoute"] = _client.route.name; 84 85 import diamond.extensions; 86 mixin ExtensionEmit!(ExtensionType.viewCtorExtension, q{ 87 mixin {{extensionEntry}}.extension; 88 }); 89 90 onViewCtor(); 91 } 92 } 93 else 94 { 95 /** 96 * Creates a new view. 97 * Params: 98 * name = The name of the view. 99 */ 100 this(string name) 101 { 102 _name = name; 103 } 104 } 105 106 static if (isWebServer) 107 { 108 protected: 109 @property 110 { 111 /// Sets a boolean determining whether the view can be cached or not. 112 void cached(bool canBeCached) 113 { 114 _cached = canBeCached; 115 } 116 } 117 } 118 119 public: 120 @property 121 { 122 static if (isWebServer) 123 { 124 /// Gets a boolean determining whether the view can be cached or not. 125 bool cached() { return _cached; } 126 127 /// Gets the client. 128 HttpClient client() { return _client; } 129 130 /// Gets the method. 131 HttpMethod httpMethod() { return _client.method; } 132 133 /// Gets the route. 134 Route route() { return _client.route; } 135 136 /// Gets a boolean determining whether the route is the default route or not. 137 bool isDefaultRoute() 138 { 139 return !route.action || !route.action.length; 140 } 141 142 static if (isTesting) 143 { 144 import diamond.unittesting; 145 146 /// Gets a boolean determnining whether the request is a test or not. 147 bool testing() { return !testsPassed; } 148 } 149 } 150 151 /// Gets the name. 152 string name() { return _name; } 153 154 /// Sets the name. 155 void name(string name) 156 { 157 _name = name; 158 } 159 160 /// Gets the layout name. 161 string layoutName() { return _layoutName; } 162 163 /// Sets the layout name. 164 void layoutName(string newLayoutName) 165 { 166 _layoutName = newLayoutName; 167 } 168 169 /// Gets a boolean determining whether the rendering is delayed. 170 bool delayRender() { return _delayRender; } 171 172 /// Sets a boolean determining whether the rendering is delayed. 173 void delayRender(bool isDelayed) 174 { 175 _delayRender = isDelayed; 176 } 177 178 /// Gets the view that's currently rendering the layout view. 179 View renderView() { return _renderView; } 180 181 /// Sets a new render view. 182 void renderView(View newRenderView) 183 { 184 _renderView = newRenderView; 185 186 copyViewData(); 187 } 188 189 /// Gets a boolean determining whether the view generation is raw or if it should call controllers etc. 190 bool rawGenerate() { return _rawGenerate; } 191 192 /// Sets a boolean determining whether the view generation is raw or if it should call controllers etc. 193 void rawGenerate(bool isRawGenerate) 194 { 195 _rawGenerate= isRawGenerate; 196 } 197 } 198 199 protected void copyViewData() 200 { 201 static if (isWebServer) 202 { 203 _client = _renderView._client; 204 _cached = _renderView._cached; 205 } 206 207 _placeHolders = _renderView._placeHolders; 208 } 209 210 final 211 { 212 /// Clears the view result. 213 void clearView() 214 { 215 _result = ""; 216 } 217 218 /** 219 * Adds a place holder to the view. 220 * Params: 221 * key = The key of the place holder. 222 * value = The value of the place holder. 223 */ 224 void addPlaceHolder(string key, string value) 225 { 226 _placeHolders[key] = value; 227 } 228 229 /** 230 * Gets a place holder of the view. 231 * Params: 232 * key = The key of the place holder. 233 * Returns: 234 * Returns the place holder. 235 */ 236 string getPlaceHolder(string key) 237 { 238 return _placeHolders.get(key, null); 239 } 240 241 /** 242 * Prepares the view with its layout, placeholders etc. 243 * Returns: 244 * The resulting string after the view has been rendered with its layout. 245 */ 246 string prepare() 247 { 248 string result = _delayRender ? "" : cast(string)_result.dup; 249 250 if (_layoutName && _layoutName.strip().length) 251 { 252 auto layoutView = view(_layoutName); 253 254 if (layoutView) 255 { 256 layoutView.name = name; 257 layoutView._renderView = this; 258 259 layoutView.addPlaceHolder("view", result); 260 261 foreach (key,value; _placeHolders) 262 { 263 layoutView.addPlaceHolder(key, value); 264 } 265 266 result = layoutView.generate(); 267 } 268 } 269 270 return result; 271 } 272 273 /** 274 * Appends data to the view's result. 275 * This will append data to the current position. 276 * Generally this is not necessary, because of the template attributes such as @= 277 * Params: 278 * data = The data to append. 279 */ 280 void append(T)(T data) 281 { 282 _result ~= to!string(data); 283 } 284 285 /** 286 * Appends html escaped data to the view's result. 287 * This will append data to the current position. 288 * Generally this is not necessary, because of the template attributes such as @(), @$= etc. 289 * Params: 290 * data = The data to escape. 291 */ 292 void escape(T)(T data) 293 { 294 auto toEscape = to!string(data); 295 string result = ""; 296 297 foreach (c; toEscape) 298 { 299 switch (c) 300 { 301 case '<': 302 { 303 result ~= "<"; 304 break; 305 } 306 307 case '>': 308 { 309 result ~= ">"; 310 break; 311 } 312 313 case '"': 314 { 315 result ~= """; 316 break; 317 } 318 319 case '\'': 320 { 321 result ~= "'"; 322 break; 323 } 324 325 case '&': 326 { 327 result ~= "&"; 328 break; 329 } 330 331 case ' ': 332 { 333 result ~= " "; 334 break; 335 } 336 337 default: 338 { 339 if (c < ' ') 340 { 341 result ~= format("&#%d;", c); 342 } 343 else 344 { 345 result ~= to!string(c); 346 } 347 } 348 } 349 } 350 351 append(result); 352 } 353 354 /** 355 * Gets th current view as a specific view. 356 * Params: 357 * name = The name of the view to get the view as. 358 * Returns: 359 * The view converted to the specific view. 360 */ 361 auto asView(string name)() 362 { 363 mixin("import diamondapp : getView, view_" ~ name ~ ";"); 364 365 static if (isWebServer) 366 { 367 mixin("return cast(view_" ~ name ~ ")this;"); 368 } 369 else 370 { 371 mixin("return cast(view_" ~ name ~ ")this;"); 372 } 373 } 374 375 /** 376 * Retrieves a raw view by name. 377 * This wraps around getView. 378 * Params: 379 * name = The name of the view to retrieve. 380 * checkRoute = Boolean determining whether the name should be checked upon default routes. (Value doesn't matter if it isn't a webserver.) 381 * Returns: 382 * The view. 383 */ 384 auto viewRaw(string name)(bool checkRoute = false) { 385 mixin("import diamondapp : getView, view_" ~ name ~ ";"); 386 387 static if (isWebServer) 388 { 389 mixin("return cast(view_" ~ name ~ ")getView(_client, new Route(name), checkRoute);"); 390 } 391 else 392 { 393 mixin("return cast(view_" ~ name ~ ")getView(name);"); 394 } 395 } 396 397 /** 398 * Retrieves a view by name. 399 * This wraps around getView. 400 * Params: 401 * name = The name of the view to retrieve. 402 * checkRoute = Boolean determining whether the name should be checked upon default routes. (Value doesn't matter if it isn't a webserver.) 403 * Returns: 404 * The view. 405 */ 406 auto view(string name, bool checkRoute = false) 407 { 408 import diamondapp : getView; 409 410 static if (isWebServer) 411 { 412 return getView(_client, new Route(name), checkRoute); 413 } 414 else 415 { 416 return getView(name); 417 } 418 } 419 420 /** 421 * Retrieves the generated result of a view. 422 * This should generally only be used to render partial views into another view. 423 * Params: 424 * name = The name of the view to generate the result of. 425 * sectionName = The name of the setion to retrieve the generated result of. 426 * Returns: 427 * A string qeuivalent to the generated result. 428 */ 429 string retrieve(string name, string sectionName = "") 430 { 431 return view(name).generate(sectionName); 432 } 433 434 /** 435 * Will render another view into this one. 436 * Params: 437 * name = The name of the view to render. 438 * sectionName = The name of the section to render. 439 */ 440 void render(string name, string sectionName = "") 441 { 442 append(retrieve(name, sectionName)); 443 } 444 445 /** 446 * Retrieves the generated result of a view. 447 * This should generally only be used to render partial views into another view. 448 * Params: 449 * name = The name of the view to generate the result of. 450 * sectionName = The name of the section to retrieve the result of. 451 * Returns: 452 * A string qeuivalent to the generated result. 453 */ 454 string retrieve(string name)(string sectionName = "") 455 { 456 return viewRaw!name.generate(sectionName); 457 } 458 459 /** 460 * Will render another view into this one. 461 * Params: 462 * name = The name of the view to render. 463 * sectionName = The name of the section to render. 464 */ 465 void render(string name)(string sectionName = "") 466 { 467 append(retrieve!name(sectionName)); 468 } 469 470 /** 471 * Retrieves the generated result of a view. 472 * This should generally only be used to render partial views into another view. 473 * Params: 474 * name = The name of the view to generate the result of. 475 * Returns: 476 * A string qeuivalent to the generated result. 477 */ 478 string retrieveModel(string name, TModel)(TModel model, string sectionName = "") 479 { 480 return viewRaw!name.generateModel(model, sectionName); 481 } 482 483 /** 484 * Will render another view into this one. 485 * Params: 486 * name = The name of the view to render. 487 */ 488 void renderModel(string name, TModel)(TModel model, string sectionName = "") 489 { 490 append(retrieveModel!(name, TModel)(model, sectionName)); 491 } 492 493 static if (isWebServer) 494 { 495 /** 496 * Gets a route that fits an action for the current route. 497 * Params: 498 * actionName = The name of the action to get. 499 * Returns: 500 * A new constructed route with the given action name. 501 */ 502 string action(string actionName) 503 { 504 enforce(actionName, "No action given."); 505 506 return "/" ~ route.name ~ "/" ~ actionName; 507 } 508 509 /** 510 * Gets a route that fits an action and parameters for the current route. 511 * Params: 512 * actionName = The name of the action to get. 513 * params = The data parameters to give the route. 514 * Returns: 515 * A new constructed route with the given action name and parameters. 516 */ 517 string actionParams(string actionName, string[] params) 518 { 519 enforce(actionName, "No action given."); 520 enforce(params, "No parameters given."); 521 522 return action(actionName) ~ "/" ~ params.join("/"); 523 } 524 525 /** 526 * Inserts a javascript file in a script tag. 527 * Params: 528 * file = The javascript file. 529 */ 530 void script(string file) 531 { 532 append("<script src=\"%s\"></script>".format(file)); 533 } 534 535 /** 536 * Inserts an asynchronous javascript file in a script tag. 537 * Params: 538 * file = The javascript file. 539 */ 540 void asyncScript(string file) 541 { 542 append("<script src=\"%s\" async></script>".format(file)); 543 } 544 545 /** 546 * Inserts a javascript file in a script tag after the page has loaded. 547 * Params: 548 * file = The javascript file. 549 */ 550 void deferScript(string file) 551 { 552 append("<script src=\"%s\" defer></script>".format(file)); 553 } 554 555 import CSRF = diamond.security.csrf; 556 557 /// Clears the current csrf token. This is recommended before generating CSRF token fields. 558 void clearCSRFToken() 559 { 560 CSRF.clearCSRFToken(_client); 561 } 562 563 /** 564 * Appends a hidden-field with a generated token that can be used for csrf protection. 565 * Params: 566 * name = A custom name for the field. 567 * appendName = Boolean determining if the custom name should be appened to the default name "formToken". 568 */ 569 void appendCSRFTokenField(string name = null, bool appendName = false) 570 { 571 bool hasName = name && name.strip().length; 572 573 if (!hasName) 574 { 575 name = "formToken"; 576 } 577 else if (appendName && hasName) 578 { 579 name = "formToken_" ~ name; 580 } 581 582 auto csrfToken = CSRF.generateCSRFToken(_client); 583 584 append 585 ( 586 `<input type="hidden" value="%s" name="%s" id="%s">` 587 .format(csrfToken, name, name) 588 ); 589 } 590 591 enum FlashMessageType 592 { 593 always, 594 showOnce, 595 showOnceGuest, 596 custom 597 } 598 599 void flashMessage(string identifier, string message, FlashMessageType type, size_t displayTime = 0) 600 { 601 enforce(identifier && identifier.length, "No identifier specified."); 602 603 auto sessionValueName = "__D_FLASHMSG_" ~ _name ~ identifier; 604 605 switch (type) 606 { 607 case FlashMessageType.showOnce: 608 { 609 if (_client.session.hasValue(sessionValueName)) 610 { 611 return; 612 } 613 614 _client.session.setValue(sessionValueName, true); 615 break; 616 } 617 618 case FlashMessageType.showOnceGuest: 619 { 620 import diamond.authentication.roles : defaultRole; 621 622 if (_client.role && _client.role != defaultRole) 623 { 624 return; 625 } 626 627 if (_client.session.hasValue(sessionValueName)) 628 { 629 return; 630 } 631 632 _client.session.setValue(sessionValueName, true); 633 break; 634 } 635 636 default: break; 637 } 638 639 append(` 640 <div id="%s"> 641 %s 642 </div> 643 `.format(identifier, message)); 644 645 if (displayTime > 0 && type != FlashMessageType.custom) 646 { 647 append(` 648 <script type="text/javascript"> 649 window.addEventListener('load', function() { 650 var flashMessage = document.getElementById('%s'); 651 652 if (flashMessage && flashMessage.parentNode) { 653 setTimeout(function() { 654 flashMessage.parentNode.removeChild(flashMessage); 655 }, %d); 656 } 657 }, false); 658 </script> 659 `.format(identifier, displayTime)); 660 } 661 } 662 } 663 } 664 665 /** 666 * Generates the result of the view. 667 * This is override by each view implementation. 668 * Returns: 669 * A string equivalent to the generated result. 670 */ 671 string generate(string sectionName = "") 672 { 673 return prepare(); 674 } 675 676 import diamond.extensions; 677 mixin ExtensionEmit!(ExtensionType.viewExtension, q{ 678 mixin {{extensionEntry}}.extensions; 679 }); 680 } 681 }