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