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 }