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 }