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 ~= "&lt;";
304               break;
305             }
306 
307             case '>':
308             {
309               result ~= "&gt;";
310               break;
311             }
312 
313             case '"':
314             {
315               result ~= "&quot;";
316               break;
317             }
318 
319             case '\'':
320             {
321               result ~= "&#39";
322               break;
323             }
324 
325             case '&':
326             {
327               result ~= "&amp;";
328               break;
329             }
330 
331             case ' ':
332             {
333               result ~= "&nbsp;";
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 }