forked from synopse/mORMot2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmormot.rest.mvc.pas
2456 lines (2246 loc) · 92.4 KB
/
mormot.rest.mvc.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// MVC Web Server over mORMot's REST and Mustache
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.rest.mvc;
{
*****************************************************************************
Web Server using Model-View-Controller (MVC) pattern and Mustache
- Web Views Implementation using Mustache
- ViewModel/Controller Sessions using Cookies
- Web Renderer Returning Mustache Views or Json
- Application ViewModel/Controller using Interfaces
TODO: remove TMvcRunOnRestServer in favor of THttpServer.Router
*****************************************************************************
}
interface
{$I ..\mormot.defines.inc}
uses
sysutils,
classes,
variants,
contnrs,
mormot.core.base,
mormot.core.os,
mormot.core.buffers,
mormot.core.unicode,
mormot.core.text,
mormot.core.datetime,
mormot.core.variants,
mormot.core.data,
mormot.core.perf,
mormot.core.rtti,
mormot.crypt.core,
mormot.core.json,
mormot.core.search, // for FindTemplates()
mormot.crypt.secure,
mormot.core.log,
mormot.core.interfaces,
mormot.core.mustache,
mormot.orm.base,
mormot.orm.core,
mormot.orm.rest,
mormot.orm.server,
mormot.soa.core,
mormot.soa.server,
mormot.soa.codegen,
mormot.rest.core,
mormot.rest.server;
{ ************ Web Views Implementation using Mustache }
const
/// TDocVariantOptions for efficient MVC data context rendering
// - maps JSON_FAST_EXTENDED with field names interning
JSON_MVC =
[dvoReturnNullForUnknownProperty,
dvoValueCopiedByReference,
dvoSerializeAsExtendedJson,
dvoInternNames];
type
/// TMvcView.Flags rendering context
// - viewHasGenerationTimeTag is set if TMvcViewsAbstract.ViewGenerationTimeTag
// text appears in the template, for this time value not to affect the cache
TMvcViewFlags = set of (
viewHasGenerationTimeTag);
/// define a particular rendered View
// - is initialized by TMvcRendererFromViews.Renders(), then rendered by the
// TMvcViewsAbstract.Render() method
TMvcView = record
/// the low-level content of this View
Content: RawByteString;
/// the MIME content type of this View
ContentType: RawUtf8;
/// some additional rendering information about this View
Flags: TMvcViewFlags;
end;
/// an abstract class able to implement Views
TMvcViewsAbstract = class
protected
fFactory: TInterfaceFactory;
fLogClass: TSynLogClass;
fViewTemplateFolder, fViewStaticFolder: TFileName;
fFactoryErrorIndex: integer;
fViewFlags: TMvcViewFlags;
fViewGenerationTimeTag: RawUtf8;
procedure SetViewTemplateFolder(const aFolder: TFileName);
/// overriden implementations should return the rendered content
procedure Render(methodIndex: integer; const Context: variant;
var View: TMvcView); virtual; abstract;
/// return the static file contents - from fViewStaticFolder by default
// - called if cacheStatic has been defined
function GetStaticFile(const aFileName: TFileName): RawByteString; virtual;
public
/// initialize the class
constructor Create(aInterface: PRttiInfo; aLogClass: TSynLogClass);
/// read-only access to the associated factory for the implementation class
property Factory: TInterfaceFactory
read fFactory;
/// set or retrieve the local folder containing the Mustache views
// - if you change it, it will also change the ViewStaticFolder as '.static'
property ViewTemplateFolder: TFileName
read fViewTemplateFolder write SetViewTemplateFolder;
/// set or retrieve the .static local folder name
property ViewStaticFolder: TFileName
read fViewStaticFolder write fViewStaticFolder;
/// any occurrence of this tag in a rendered view will be converted
// into the rendering time in microseconds
// - equals '[[GENERATION_TIME_TAG]]' by default
property ViewGenerationTimeTag: RawUtf8
read fViewGenerationTimeTag write fViewGenerationTimeTag;
end;
/// general parameters defining the Mustache Views process
// - used as a separate value so that we would be able to store the
// settings in a file, e.g. encoded as a JSON object
TMvcViewsMustacheParameters = record
/// where the mustache template files are stored
// - if not set, will search in a 'Views' folder under the current executable
Folder: TFileName;
/// the file extensions to search in the given Folder, specified as CSV
// - if not set, will search for 'html,json,css'
CsvExtensions: TFileName;
/// defines if the view files should be checked for modification
// - any value would automatically update the rendering template, if the file
// changed after a given number of seconds - default is 5 seconds
// - setting 0 would be slightly faster, since content would never be checked
FileTimestampMonitorAfterSeconds: cardinal;
/// file extension (e.g. '.html') to be used to create void templates
// - default '' will create no void template file in the given Folder
ExtensionForNotExistingTemplate: TFileName;
/// set of block helpers to be registered to TSynMustache
// - default will use TSynMustache.HelpersGetStandardList definition
Helpers: TSynMustacheHelpers;
end;
/// define TMvcViewsMustache.RegisterExpressionHelpersForTables labels
THtmlTableStyleLabel = (
labelFalse,
labelTrue,
labelOff,
labelOn,
labelValue);
/// define TMvcViewsMustache.RegisterExpressionHelpersForTables CSS styling
TExpressionHtmlTableStyle = class
public
class procedure StartTable(WR: TTextWriter); virtual;
class procedure BeforeFieldName(WR: TTextWriter); virtual;
class procedure BeforeValue(WR: TTextWriter); virtual;
class procedure AddLabel(WR: TTextWriter; const text: string;
kind: THtmlTableStyleLabel); virtual;
class procedure AfterValue(WR: TTextWriter); virtual;
class procedure EndTable(WR: TTextWriter); virtual;
end;
/// to define TMvcViewsMustache.RegisterExpressionHelpersForTables CSS styling
TExpressionHtmlTableStyleClass = class of TExpressionHtmlTableStyle;
/// TMvcViewsMustache.RegisterExpressionHelpersForTables via Bootstrap CSS
TExpressionHtmlTableStyleBootstrap = class(TExpressionHtmlTableStyle)
public
class procedure StartTable(WR: TTextWriter); override;
class procedure AddLabel(WR: TTextWriter; const text: string;
kind: THtmlTableStyleLabel); override;
end;
/// a class able to implement Views using Mustache templates
TMvcViewsMustache = class(TMvcViewsAbstract)
protected
fViewTemplateFileTimestampMonitor: cardinal;
fViewPartials: TSynMustachePartials;
fViewHelpers: TSynMustacheHelpers;
fViews: array of record // follows fFactory.Methods[]
Mustache: TSynMustache;
Template: RawUtf8;
MethodName: TFileName;
SearchPattern: TFileName;
FileName: TFileName;
ShortFileName: TFileName;
FileExt: TFileName;
ContentType: RawUtf8;
Locker: IAutoLocker;
FileAgeLast: TUnixTime;
FileAgeCheckTick: Int64;
Flags: TMvcViewFlags;
end;
function GetRenderer(methodIndex: integer; var view: TMvcView): TSynMustache;
/// search for template files in ViewTemplateFolder
function FindTemplates(const Mask: TFileName): TFileNameDynArray; virtual;
/// return the template file contents
function GetTemplate(const aFileName: TFileName): RawUtf8; virtual;
/// return the template file date and time
function GetTemplateAge(const aFileName: TFileName): TUnixTime; virtual;
/// overriden implementations should return the rendered content
procedure Render(methodIndex: integer; const Context: variant;
var View: TMvcView); override;
// some helpers defined here to avoid mormot.crypt.core link
class procedure md5(const Value: variant; out Result: variant);
class procedure sha1(const Value: variant; out Result: variant);
class procedure sha256(const Value: variant; out Result: variant);
class procedure sha512(const Value: variant; out Result: variant);
public
/// create an instance of this ViewModel implementation class
// - define the associated REST instance, the interface definition and the
// local folder where the mustache template files are stored
// - will search and parse the matching views (and associated *.partial)
constructor Create(aInterface: PRttiInfo;
const aParameters: TMvcViewsMustacheParameters;
aLogClass: TSynLogClass = nil); reintroduce; overload; virtual;
/// create an instance of this ViewModel implementation class
// - this overloaded version will use default parameters (i.e. search for
// html+json+css in the "Views" sub-folder under the executable)
// - will search and parse the matching views (and associated *.partial),
// optionally creating void templates for any missing view
constructor Create(aInterface: PRttiInfo;
const aTemplatesFolder: TFileName = ''; aLogClass: TSynLogClass = nil;
const aExtensionForNotExistingTemplate: TFileName = ''); overload;
/// define the supplied Expression Helpers definition
// - returns self so that may be called in a fluent interface
function RegisterExpressionHelpers(const aNames: array of RawUtf8;
const aEvents: array of TSynMustacheHelperEvent): TMvcViewsMustache;
/// define Expression Helpers for some ORM tables
// - e.g. to read a TMyOrm from its ID value and put its fields
// in the current rendering data context, you can write:
// ! aView.RegisterExpressionHelpersForTables(aServer,[TMyOrm]);
// then use the following Mustache tag
// ! {{#TMyOrm MyRecordID}} ... {{/TMyOrm MyRecordID}}
// - use Bootstap CSS by default, but you can supply your aHtmlTableStyle
// - returns self so that may be called in a fluent interface
function RegisterExpressionHelpersForTables(aRest: TRest;
const aTables: array of TOrmClass;
aHtmlTableStyle: TExpressionHtmlTableStyleClass = nil): TMvcViewsMustache; overload;
/// define Expression Helpers for all ORM tables of the supplied model
// - e.g. to read a TMyOrm from its ID value and put its fields
// in the current rendering data context, you can write:
// ! aView.RegisterExpressionHelpersForTables(aServer);
// then use the following Mustache tag
// ! {{#TMyOrm MyRecordID}} ... {{/TMyOrm MyRecordID}}
// - returns self so that may be called in a fluent interface
function RegisterExpressionHelpersForTables(aRest: TRest;
aHtmlTableStyle: TExpressionHtmlTableStyleClass = nil): TMvcViewsMustache; overload;
/// define some Expression Helpers for hashing
// - i.e. md5, sha1 sha256 and sha512 hashing
// - would allow e.g. to compute a Gravatar URI via:
// ! <img src=http://www.gravatar.com/avatar/{{md5 email}}?s=200></img>
// - returns self so that may be called in a fluent interface
function RegisterExpressionHelpersForCrypto: TMvcViewsMustache;
/// finalize the instance
destructor Destroy; override;
end;
{ ************ ViewModel/Controller Sessions using Cookies }
type
TMvcApplication = class;
/// an abstract class able to implement ViewModel/Controller sessions
// - see TMvcSessionWithCookies to implement cookie-based sessions
// - this kind of ViewModel will implement client side storage of sessions,
// storing any (simple) record content on the browser client side
// - at login, a record containing session-related information (session ID,
// display and login name, preferences, rights...) can be computed only once
// on the server side from the Model, then stored on the client side (typically
// in a cookie): later on, session information can be retrieved by the server
// logic (via CheckAndRetrieve - note that any security attribute should be
// verified against the Model), then the renderer (CheckAndRetrieveInfo
// returning the record as TDocVariant in the data context "Session" field) -
// such a pattern is very efficient and allows good scaling
// - session are expected to be tied to the TMvcSessionAbstract instance
// lifetime, so are lost after server restart, unless they are persisted
// via LoadContext/SaveContext methods
TMvcSessionAbstract = class
protected
fApplication: TMvcApplication;
public
/// create an instance of this ViewModel implementation class
constructor Create(Owner: TMvcApplication); virtual;
/// will create a new session
// - setting an optional record data, and returning the internal session ID
// - you can supply a time period, after which the session will expire -
// default is 1 hour - note that overriden methods may not implement it
function Initialize(PRecordData: pointer = nil;
PRecordTypeInfo: PRttiInfo = nil;
SessionTimeOutMinutes: cardinal = 60): integer; virtual; abstract;
/// fast check if there is a session associated to the current context
function Exists: boolean; virtual; abstract;
/// retrieve the current session ID
// - can optionally retrieve the associated record Data parameter
// - Invalidate=true would force this cookie to be rejected in the future,
// and avoid cookies replay attacks e.g. from Finalize()
function CheckAndRetrieve(PRecordData: pointer = nil;
PRecordTypeInfo: PRttiInfo = nil; PExpires: PUnixTime = nil;
Invalidate: boolean = false): integer; virtual; abstract;
/// retrieve the session information as a JSON object
// - returned as a TDocVariant, including any associated record Data and
// optionally its session ID
// - will call CheckAndRetrieve() then RecordSaveJson() and _JsonFast()
// - to be called in overriden TMvcApplication.GetViewInfo method
// - warning: PSessionID^ should be a 32-bit "integer" variable, not a PtrInt
function CheckAndRetrieveInfo(PRecordDataTypeInfo: PRttiInfo;
PSessionID: PInteger = nil; Invalidate: boolean = false): variant; virtual;
/// clear the session
procedure Finalize(PRecordTypeInfo: PRttiInfo = nil); virtual; abstract;
/// return all session generation information as ready-to-be stored string
// - to be retrieved via LoadContext, e.g. after restart
function SaveContext: RawUtf8; virtual; abstract;
/// restore session generation information from SaveContext format
// - returns TRUE on success
function LoadContext(const Saved: RawUtf8): boolean; virtual; abstract;
/// access to the owner MVC Application
property Application: TMvcApplication
read fApplication;
end;
/// a class able to implement ViewModel/Controller sessions with cookies
// - this kind of ViewModel will implement cookie-based sessions, able to
// store any (simple) record content in the cookie, on the browser client side
// - those cookies have the same feature set than JWT, but with a lower
// payload (thanks to binary serialization), and cookie safety (not accessible
// from JavaScript): they are digitally signed (with HMAC-CRC32C and a
// temporary secret key), they include an unique session identifier (like
// "jti" claim), issue and expiration dates (like "iat" and "exp" claims),
// and they are encrypted with a temporary key - this secret keys is tied to
// the TMvcSessionWithCookies instance lifetime, so new cookies are generated
// after server restart, unless they are persisted via LoadContext/SaveContext
// - signature and encryption are weak, but very fast, to avoid DDOS attacks
TMvcSessionWithCookies = class(TMvcSessionAbstract)
protected
fContext: TBinaryCookieGenerator;
function GetCookieName: RawUtf8;
procedure SetCookieName(const Value: RawUtf8);
// overriden e.g. in TMvcSessionWithRestServer using ServiceContext threadvar
function GetCookie: RawUtf8; virtual; abstract;
procedure SetCookie(const cookie: RawUtf8); virtual; abstract;
public
/// create an instance of this ViewModel implementation class
constructor Create(Owner: TMvcApplication); override;
/// finalize this instance
destructor Destroy; override;
/// will initialize the session cookie
// - setting an optional record data, which will be stored Base64-encoded
// - will return the 32-bit internal session ID
// - you can supply a time period, after which the session will expire -
// default is 1 hour, and could go up to
function Initialize(PRecordData: pointer = nil;
PRecordTypeInfo: PRttiInfo = nil;
SessionTimeOutMinutes: cardinal = 60): integer; override;
/// fast check if there is a cookie session associated to the current context
function Exists: boolean; override;
/// retrieve the session ID from the current cookie
// - can optionally retrieve the record Data parameter stored in the cookie
// - Invalidate=true would force this cookie to be rejected in the future
// - will return the 32-bit internal session ID, or 0 if the cookie is invalid
function CheckAndRetrieve(PRecordData: pointer = nil;
PRecordTypeInfo: PRttiInfo = nil; PExpires: PUnixTime = nil;
Invalidate: boolean = false): integer; override;
/// clear the session
// - by deleting the cookie on the client side
procedure Finalize(PRecordTypeInfo: PRttiInfo = nil); override;
/// return all cookie generation information as base64 encoded text
// - to be retrieved via LoadContext
function SaveContext: RawUtf8; override;
/// restore cookie generation information from SaveContext text format
// - returns TRUE after checking the crc and unserializing the supplied data
// - WARNING: if the unerlying record type structure changed (i.e. any
// field is modified or added), restoration will lead to data corruption of
// low-level binary content, then trigger unexpected GPF: if you change the
// record type definition, do NOT use LoadContext - and reset all cookies
function LoadContext(const Saved: RawUtf8): boolean; override;
/// direct access to the low-level information used for cookies generation
// - use SaveContext and LoadContext methods to persist this information
// before server shutdown, so that the cookies can be re-used after restart
property Context: TBinaryCookieGenerator
read fContext write fContext;
/// you can customize the cookie name
// - default is 'mORMot', and cookie is restricted to Path=/RestRoot
property CookieName: RawUtf8
read GetCookieName write SetCookieName;
end;
/// implement a ViewModel/Controller sessions in a TRestServer instance
// - will use ServiceContext.Request threadvar to access the client cookies
TMvcSessionWithRestServer = class(TMvcSessionWithCookies)
protected
function GetCookie: RawUtf8; override;
procedure SetCookie(const cookie: RawUtf8); override;
end;
/// implement a single ViewModel/Controller in-memory session
// - this kind of session could be used in-process, e.g. for a VCL/FMX GUI
// - do NOT use it with multiple clients, e.g. from HTTP remote access
TMvcSessionSingle = class(TMvcSessionWithCookies)
protected
fSingleCookie: RawUtf8;
function GetCookie: RawUtf8; override;
procedure SetCookie(const cookie: RawUtf8); override;
end;
{ ************ Web Renderer Returning Mustache Views or Json }
/// record type to define commands e.g. to redirect to another URI
// - do NOT access those record property directly, but rather use
// TMvcApplication.GotoView/GotoError/GotoDefault methods, e.g.
// ! function TBlogApplication.Logout: TMvcAction;
// ! begin
// ! CurrentSession.Finalize;
// ! GotoDefault(result);
// ! end;
// - this record type should match exactly TServiceCustomAnswer layout,
// so that TServiceMethod.InternalExecute() would handle it directly
TMvcAction = record
/// the method name to be executed
RedirectToMethodName: RawUtf8;
/// may contain a JSON object which will be used to specify parameters
// to the specified method
RedirectToMethodParameters: RawUtf8;
/// which HTTP Status code should be returned
// - if RedirectMethodName is set, will return 307 HTTP_TEMPORARYREDIRECT
// by default, but you can set here the expected HTTP Status code, e.g.
// 201 HTTP_CREATED or 404 HTTP_NOTFOUND
ReturnedStatus: cardinal;
end;
/// abstract MVC rendering execution context
// - you shoud not execute this abstract class, but any of the inherited class
// - one instance inherited from this class would be allocated for each event
// - may return some data (when inheriting from TMvcRendererReturningData), or
// even simply display the value in a VCL/FMX GUI, without any output
TMvcRendererAbstract = class
protected
fApplication: TMvcApplication;
fMethodIndex: integer;
fInput, fRemoteIP, fRemoteUserAgent: RawUtf8;
fExecuteCached: TInterfaceMethodExecuteCachedDynArray;
procedure Renders(var outContext: variant; status: cardinal;
forcesError: boolean); virtual; abstract;
function Redirects(const action: TMvcAction): boolean; virtual;
procedure AddErrorContext(var context: variant; error: integer);
procedure CommandError(const ErrorName: RawUtf8; const ErrorValue: variant;
ErrorCode: integer); virtual;
public
/// initialize a rendering process for a given MVC Application/ViewModel
constructor Create(aApplication: TMvcApplication); reintroduce;
/// finalize this rendering context
destructor Destroy; override;
/// main execution method of the rendering process
// - Input should have been set with the incoming execution context
procedure ExecuteCommand(aMethodIndex: integer); virtual;
/// incoming execution context, to be processed via ExecuteCommand() method
// - should be specified as a raw JSON object
property Input: RawUtf8
read fInput write fInput;
/// access to the owner MVC Application
property Application: TMvcApplication
read fApplication;
end;
/// how TMvcRendererReturningData should cache its content
TMvcRendererCachePolicy = (
cacheNone,
cacheRootIgnoringSession,
cacheRootIfSession,
cacheRootIfNoSession,
cacheRootWithSession,
cacheWithParametersIgnoringSession,
cacheWithParametersIfSession,
cacheWithParametersIfNoSession);
TMvcRunWithViews = class;
/// abstract MVC rendering execution context, returning some content
// - the Output property would contain the content to be returned
// - can be used to return e.g. some rendered HTML or some raw JSON,
// or even some server-side generated report as PDF, using our mORMotReport.pas
TMvcRendererReturningData = class(TMvcRendererAbstract)
protected
fRun: TMvcRunWithViews;
fOutput: TServiceCustomAnswer;
fOutputFlags: TMvcViewFlags;
fCacheEnabled: boolean;
fCacheCurrent: (noCache, rootCache, inputCache);
fCacheCurrentSec: cardinal;
fCacheCurrentInputValueKey: RawUtf8;
function Redirects(const action: TMvcAction): boolean; override;
public
/// initialize a rendering process for a given MVC Application/ViewModel
// - you need to specify a MVC Views engine, e.g. TMvcViewsMustache instance
constructor Create(aRun: TMvcRunWithViews); reintroduce; virtual;
/// main execution method of the rendering process
// - this overriden method would handle proper caching as defined by
// TMvcRunWithViews.SetCache()
procedure ExecuteCommand(aMethodIndex: integer); override;
/// caller should retrieve this value after ExecuteCommand method execution
property Output: TServiceCustomAnswer
read fOutput;
end;
TMvcRendererReturningDataClass = class of TMvcRendererReturningData;
/// MVC rendering execution context, returning some rendered View content
// - will use an associated Views templates system, e.g. a Mustache renderer
TMvcRendererFromViews = class(TMvcRendererReturningData)
protected
// Renders() will fill Output using the corresponding View, to be sent back
procedure Renders(var outContext: variant; status: cardinal;
forcesError: boolean); override;
public
/// initialize a rendering process for a given MVC Application/ViewModel
// - this overriden constructor will ensure that cache is enabled
constructor Create(aRun: TMvcRunWithViews); override;
end;
/// MVC rendering execution context, returning some un-rendered JSON content
// - may be used e.g. for debugging purpose
// - for instance, TMvcRunOnRestServer will return such context with the
// supplied URI ends with '/json' (e.g. for any /root/method/json request)
TMvcRendererJson = class(TMvcRendererReturningData)
protected
// Renders() will fill Output with the outgoing JSON, to be sent back
procedure Renders(var outContext: variant; status: cardinal;
forcesError: boolean); override;
end;
/// abstract class used by TMvcApplication to run
// - a single TMvcApplication logic may handle several TMvcRun instances
TMvcRun = class
protected
fApplication: TMvcApplication;
public
/// link this runner class to a specified MVC application
// - will also reset the associated Application.Session instance
constructor Create(aApplication: TMvcApplication); reintroduce;
/// method called to flush the caching mechanism for all MVC commands
procedure NotifyContentChanged; virtual;
/// you may call this method to flush any caching mechanism for a MVC command
procedure NotifyContentChangedForMethod(
aMethodIndex: integer); overload; virtual;
/// you may call this method to flush any caching mechanism for a MVC command
procedure NotifyContentChangedForMethod(
const aMethodName: RawUtf8); overload;
/// read-write access to the associated MVC Application/ViewModel instance
property Application: TMvcApplication
read fApplication write fApplication;
end;
/// abstract class used by TMvcApplication to run TMvcViews-based process
// - this inherited class will host a MVC Views instance, and handle
// an optional simple in-memory cache
TMvcRunWithViews = class(TMvcRun)
protected
fViews: TMvcViewsAbstract;
fCacheLocker: IAutoLocker;
fCache: array of record
Policy: TMvcRendererCachePolicy;
TimeOutSeconds: cardinal;
RootValue: RawUtf8;
RootValueExpirationTime: cardinal;
InputValues: TSynNameValue;
end;
public
/// link this runner class to a specified MVC application
constructor Create(aApplication: TMvcApplication;
aViews: TMvcViewsAbstract = nil); reintroduce;
/// method called to flush the caching mechanism for a MVC command
procedure NotifyContentChangedForMethod(aMethodIndex: integer); override;
/// defines the caching policy for a given MVC command
// - a time expiration period (up to 5 minutes) can also be defined per
// MVC command - leaving default 0 will set to 5 minutes expiration delay
// - function calls can be chained to create some fluent definition interface
// like in TAnyBLogapplication.Create:
// ! fMainRunner := TMvcRunWithViews.Create(self).SetCache('default',cacheRoot);
function SetCache(const aMethodName: RawUtf8;
aPolicy: TMvcRendererCachePolicy;
aTimeOutSeconds: cardinal = 0): TMvcRunWithViews; virtual;
/// finalize this instance
destructor Destroy; override;
/// read-write access to the associated MVC Views instance
property Views: TMvcViewsAbstract
read fViews;
end;
/// the kinds of optional content which may be published
// - publishMvcInfo will define a /root/[aSubURI/]mvc-info HTML page,
// which is pretty convenient when working with views
// - publishStatic will define a /root/[aSubURI/].static sub-folder,
// ready to serve any file available in the Views\.static local folder,
// via an in-memory cache (if cacheStatic is also defined)
// - cacheStatic enables an in-memory cache of publishStatic files; if not set,
// TRestServerUriContext.ReturnFile is called to avoid buffering, which may
// be a better solution on http.sys or if NGINX's X-Accel-Redirect header is set
// - registerOrmTableAsExpressions will register Mustache Expression Helpers
// for every TOrm table of the Server data model
// - by default, TRestServer authentication would be by-passed for all
// MVC routes, unless bypassAuthentication option is undefined
// - allowJsonFormat will recognize ####/json URIs and return the Mustache
// data context as plain JSON without any HTML rendering
// - defaultErrorContext will include basic {{originalErrorContext}}
// information - could be disabled for verbose object debugging purposes
TMvcPublishOption = (
publishMvcInfo,
publishStatic,
cacheStatic,
registerOrmTableAsExpressions,
bypassAuthentication,
allowJsonFormat,
defaultErrorContext);
/// which kind of optional content should be publish
TMvcPublishOptions = set of TMvcPublishOption;
/// run TMvcApplication directly within a TRestServer method-based service
// - this is the easiest way to host and publish a MVC Application, optionally
// in conjunction with REST/AJAX client access
TMvcRunOnRestServer = class(TMvcRunWithViews)
protected
fRestServer: TRestServer;
fPublishOptions: TMvcPublishOptions;
fAllowedMethods: TUriMethods;
fMvcInfoCache: RawUtf8;
fStaticCache: TSynNameValue;
fStaticCacheControlMaxAge: integer;
/// callback used for the rendering on the TRestServer
procedure RunOnRestServerRoot(Ctxt: TRestServerUriContext);
procedure RunOnRestServerSub(Ctxt: TRestServerUriContext);
procedure InternalRunOnRestServer(Ctxt: TRestServerUriContext;
const MethodName: RawUtf8);
public
/// this constructor will publish some views to a TRestServer instance
// - the associated RestModel can match the supplied TRestServer, or be
// another instance (if the data model is not part of the publishing server)
// - all TMvcApplication methods would be registered to the TRestServer,
// as /root/methodName if aSubURI is '', or as /root/aSubURI/methodName
// - if aApplication has no Views instance associated, this constructor will
// initialize a Mustache renderer in its default folder, with '.html' void
// template generation
// - will also create a TMvcSessionWithRestServer for simple cookie sessions
// - aPublishOptions could be used to specify integration with the server
// - aAllowedMethods will render standard GET/POST by default
constructor Create(aApplication: TMvcApplication;
const aTemplatesFolder: TFileName = ''; aRestServer: TRestServer = nil;
const aSubURI: RawUtf8 = ''; aViews: TMvcViewsAbstract = nil;
aPublishOptions: TMvcPublishOptions=
[low(TMvcPublishOption) .. high(TMvcPublishOption)];
aAllowedMethods: TUriMethods = [mGET, mPOST]); reintroduce;
/// define some content for a static file
// - only used if cacheStatic has been defined
function AddStaticCache(const aFileName: TFileName;
const aFileContent: RawByteString): RawByteString;
/// current publishing options, as specified to the constructor
// - all options are included by default
property PublishOptions: TMvcPublishOptions
read fPublishOptions write fPublishOptions;
/// current HTTP methods recognized, as specified to the constructor
// - equals [mGET, mPOST] by default, as expected by HTML applications
// with standard www-form
property AllowedMethods: TUriMethods
read fAllowedMethods;
/// optional "Cache-Control: max-age=###" header seconds value for static content
property StaticCacheControlMaxAge: integer
read fStaticCacheControlMaxAge write fStaticCacheControlMaxAge;
end;
{ ************ Application ViewModel/Controller using Interfaces }
/// Exception class triggerred by mORMot MVC/MVVM applications internally
// - those error are internal fatal errors of the server side process
EMvcException = class(ESynException);
/// Exception class triggerred by mORMot MVC/MVVM applications externally
// - those error are external errors which should be notified to the client
// - can be used to change the default view, e.g. on application error
EMvcApplication = class(ESynException)
protected
fAction: TMvcAction;
public
/// same as calling TMvcApplication.GotoView()
// - HTTP_TEMPORARYREDIRECT will change the URI, but HTTP_SUCCESS won't
constructor CreateGotoView(const aMethod: RawUtf8;
const aParametersNameValuePairs: array of const;
aStatus: cardinal = HTTP_TEMPORARYREDIRECT);
/// same as calling TMvcApplication.GotoError()
constructor CreateGotoError(const aErrorMessage: string;
aErrorCode: integer = HTTP_BADREQUEST); overload;
/// same as calling TMvcApplication.GotoError()
constructor CreateGotoError(aHtmlErrorCode: integer); overload;
/// same as calling TMvcApplication.GotoDefault
// - HTTP_TEMPORARYREDIRECT will change the URI, but HTTP_SUCCESS won't
constructor CreateDefault(aStatus: cardinal = HTTP_TEMPORARYREDIRECT);
/// just a wrapper around raise CreateGotoView()
class procedure GotoView(const aMethod: RawUtf8;
const aParametersNameValuePairs: array of const;
aStatus: cardinal = HTTP_TEMPORARYREDIRECT);
/// just a wrapper around raise CreateGotoError()
class procedure GotoError(const aErrorMessage: string;
aErrorCode: integer = HTTP_BADREQUEST); overload;
/// just a wrapper around raise CreateGotoError()
class procedure GotoError(aHtmlErrorCode: integer); overload;
/// just a wrapper around raise CreateDefault()
class procedure Default(aStatus: cardinal = HTTP_TEMPORARYREDIRECT);
end;
/// defines the main and error pages for the ViewModel of one application
IMvcApplication = interface(IInvokable)
['{C48718BF-861B-448A-B593-8012DB51E15D}']
/// the default main page
// - whole data context is retrieved and returned as a TDocVariant
procedure Default(var Scope: variant);
/// the error page
// - in addition to the error message, a whole data context is retrieved
// and returned as a TDocVariant
procedure Error(var Msg: RawUtf8; var Scope: variant);
end;
/// event as called by TMvcApplication.OnBeforeRender/OnAfterRender
// - should return TRUE to continue the process, FALSE if Ctxt has been filled
// as expected and no further process should be done
TOnMvcRender = function(Ctxt: TRestServerUriContext; Method: PInterfaceMethod;
var Input: variant; Renderer: TMvcRendererReturningData): boolean of object;
/// event called by TMvcApplication.OnSessionCreate/OnSessionFinalized
TOnMvcSession = procedure(Sender: TMvcSessionAbstract; SessionID: integer;
const Info: variant) of object;
/// parent class to implement a MVC/MVVM application
// - you should inherit from this class, then implement an interface inheriting
// from IMvcApplication to define the various commands of the application
// - here the Model would be a TRest instance, Views will be defined by
// TMvcViewsAbstract (e.g. TMvcViewsMustache), and the ViewModel/Controller
// will be implemented with IMvcApplication methods of the inherited class
// - inherits from TInjectableObject, so that you could resolve dependencies
// via services or stubs, following the IoC pattern
TMvcApplication = class(TInjectableObject)
protected
fFactory: TInterfaceFactory;
fFactoryEntry: pointer;
fFactoryErrorIndex: integer;
fRenderOptions: set of (roDefaultErrorContext);
fSession: TMvcSessionAbstract;
fRestModel: TRest;
fRestServer: TRestServer;
fLocker: IAutoLocker;
// if any TMvcRun instance is store here, will be freed by Destroy
// but note that a single TMvcApplication logic may handle several TMvcRun
fMainRunner: TMvcRun;
fOnBeforeRender, fOnAfterRender: TOnMvcRender;
fOnSessionCreate, fOnSessionFinalized: TOnMvcSession;
procedure SetSession(Value: TMvcSessionAbstract);
/// to be called when the data model did change to force content re-creation
// - this default implementation will call fMainRunner.NotifyContentChanged
procedure FlushAnyCache; virtual;
/// generic IMvcApplication.Error method implementation
procedure Error(var Msg: RawUtf8; var Scope: variant); virtual;
/// every view will have this data context transmitted as "main":...
procedure GetViewInfo(MethodIndex: integer; out info: variant); virtual;
/// compute the data context e.g. for the /mvc-info URI
procedure GetMvcInfo(out info: variant); virtual;
/// wrappers to redirect to IMvcApplication standard methods
// - if status is HTTP_TEMPORARYREDIRECT, it will change the URI
// whereas HTTP_SUCCESS would just render the view for the current URI
class procedure GotoView(var Action: TMvcAction; const MethodName: RawUtf8;
const ParametersNameValuePairs: array of const;
Status: cardinal = HTTP_TEMPORARYREDIRECT);
class procedure GotoError(var Action: TMvcAction; const Msg: string;
ErrorCode: integer = HTTP_BADREQUEST); overload;
class procedure GotoError(var Action: TMvcAction;
ErrorCode: integer); overload;
class procedure GotoDefault(var Action: TMvcAction;
Status: cardinal = HTTP_TEMPORARYREDIRECT);
public
/// initialize the instance of the MVC/MVVM application
// - define the associated REST instance, and the interface definition for
// application commands
// - is not defined as constructor, since this TInjectableObject may
// expect injection using the CreateInjected() constructor
procedure Start(aRestModel: TRest; aInterface: PRttiInfo); virtual;
/// finalize the application
// - and release any associated CurrentSession, Views, and fMainRunner
destructor Destroy; override;
/// read-only access to the associated mORMot REST instance implementing the
// MVC data Model of the application
// - is a TRestServer instance e.g. for TMvcRunOnRestServer
property RestModel: TRest
read fRestModel;
/// read-only access to the associated factory for IMvcApplication interface
property Factory: TInterfaceFactory
read fFactory;
/// read-write access to the associated Session instance
property CurrentSession: TMvcSessionAbstract
read fSession write SetSession;
/// this event is called before a page is rendered
// - you can override the supplied Input TDocVariantData if needed
property OnBeforeRender: TOnMvcRender
read fOnBeforeRender write fOnBeforeRender;
/// this event is called after the page has been rendered
// - Renderer.Output.Content contains the result of Renderer.ExecuteCommand
property OnAfterRender: TOnMvcRender
read fOnAfterRender write fOnAfterRender;
/// this event is called when a session/cookie has been initiated
property OnSessionCreate: TOnMvcSession
read fOnSessionCreate write fOnSessionCreate;
/// this event is called when a session/cookie has been finalized
property OnSessionFinalized: TOnMvcSession
read fOnSessionFinalized write fOnSessionFinalized;
/// global mutex which may be used to protect ViewModel/Controller code
// - you may call Locker.ProtectMethod in any implementation method to
// ensure that no other thread would access the same data
// - for store some cache data among methods, you may consider defining a
// ILockedDocVariant private field, and use it to store values safely
// - note that regular RestModel CRUD operations are already thread safe, so
// it is not necessary to use this Locker with ORM or SOA methods
property Locker: IAutoLocker
read fLocker;
/// read-write access to the main associated TMvcRun instance
// - if any TMvcRun instance is stored here, will be freed by Destroy
// - but note that a single TMvcApplication logic may handle several TMvcRun
property MainRunner: TMvcRun
read fMainRunner;
end;
var
/// the pseudo-method name for the MVC information html page
MVCINFO_URI: RawUtf8 = 'mvc-info';
/// the pseudo-method name for any static content for Views
STATIC_URI: RawUtf8 = '.static';
implementation
const
MUSTACHE_METHODPARTIAL =
'{{<method}}{{verb}} {{methodName}}{{#hasInParams}}({{#args}}{{^dirResult}}' +
'{{dirName}} {{argName}}: {{typeDelphi}}{{commaArg}}{{/dirResult}}{{/args}})' +
'{{/hasInParams}}{{#args}}{{#dirResult}}: {{typeDelphi}}{{/dirResult}}' +
'{{/args}};{{/method}}';
MUSTACHE_VOIDVIEW = MUSTACHE_METHODPARTIAL +
'<<! void template created for the {{interfaceName}}.{{methodName}} View:'#13#10 +
' defined as'#13#10' {{>method}}'#13#10' with the following data context:'#13#10 +
' * Main: variant'#13#10'{{#args}}{{#dirOutput}} * {{argName}}:' +
' {{typePascal}}'#13#10'{{/dirOutput}}{{/args}}>>'#13#10;
MUSTACHE_MVCINFO = MUSTACHE_METHODPARTIAL +
'{{<url}}/{{root}}/{{methodName}}{{#hasInParams}}?' +
'{{#args}}{{#dirInput}}{{argName}}=</b>..[{{typePascal}}]..<b>' +
'{{#commaInSingle}}&{{/commaInSingle}}{{/dirInput}}{{/args}}{{/hasInParams}}{{/url}}' +
'{{<mustache}}<b>{{Main}}</b>: variant{{#args}}{{#dirOutput}}<br><b>{{' +
'{{argName}}}}</b>: {{typePascal}}{{/dirOutput}}{{/args}}{{/mustache}}' +
'<!DOCTYPE html><html><head><title>{{Name}} Information</title></head><body ' +
'style="font-family:Verdana;"><h1>{{Name}} mormot.rest.mvc Information</h1>' +
'<p><strong>Generated by a <i>mORMot</i> {{mORMot}} server</strong><br>' +
'<small>©Synopse Informatique - <a href=https://synopse.info>' +
'https://synopse.info</a></small></p><h2>Controller Definition</h2>' +
'<p>Registered interface is:</p><pre>'#13#10 +
' I{{name}} = interface(IInvokable)'#13#10'{{#methods}}'#13#10 +
' {{>method}}'#13#10'{{/methods}}'#13#10' end;'#13#10 +
'</pre><p>Use this page as reference when writing your <a href=' +
'https://mustache.github.io>Mustache</a> Views.</p>' +
'<h2>Available Commands</h2><p>You can access the following commands:</p>' +
'<ul>{{#methods}}<li><b>{{>url}}</b>{{/methods}}</ul><p>Any missing parameter ' +
'would be replaced by its default value.</p><h2>Available Views</h2>' +
'<p>The following views are defined, with expected data context:</p><ul>' +
'{{#methods}}{{^resultIsServiceCustomAnswer}}<li><b>{{>url}}</b><p>{{>mustache}}' +
'</p></li>{{/resultIsServiceCustomAnswer}}{{/methods}}</ul><p>' +
'Currently, all views are located in the <code>{{viewsFolder}}</code> folder.</p>';
MUSTACHE_DEFAULTERROR =
'<!DOCTYPE html><html><head><title>mormot.rest.mvc Error</title></head><body style=' +
'"font-family:Verdana;"><h1>mormot.rest.mvc Default Error Page</h1><p>A <code>' +
'{{exceptionName}}</code> exception did raise during {{className}} process ' +
'with the following message:</p><pre>{{exceptionMessage}}</pre><p>' +
'Triggered with the following context:</p><pre>{{originalErrorContext}}</pre>';
{ ************ Web Views Implementation using Mustache }
{ TMvcViewsAbstract }
constructor TMvcViewsAbstract.Create(aInterface: PRttiInfo;
aLogClass: TSynLogClass);
begin
inherited Create;
fFactory := TInterfaceFactory.Get(aInterface);
fFactoryErrorIndex := fFactory.FindMethodIndex('Error');
if aLogClass = nil then
fLogClass := TSynLog
else
fLogClass := aLogClass;
fViewGenerationTimeTag := '[[GENERATION_TIME_TAG]]';
end;
procedure TMvcViewsAbstract.SetViewTemplateFolder(const aFolder: TFileName);
begin
fViewTemplateFolder := IncludeTrailingPathDelimiter(aFolder);
fViewStaticFolder := MakePath([fViewTemplateFolder, STATIC_URI], true);
end;
function TMvcViewsAbstract.GetStaticFile(
const aFileName: TFileName): RawByteString;
begin
result := StringFromFile(fViewStaticFolder + aFileName);
end;
{ Customization of HTML CSS tables }
type
TExpressionHelperForTable = class
public
Rest: TRest;
Table: TOrmClass;
TableProps: TOrmProperties;
HtmlTableStyle: TExpressionHtmlTableStyleClass;
constructor Create(aRest: TRest; aTable: TOrmClass;
var aHelpers: TSynMustacheHelpers;
aHtmlTableStyle: TExpressionHtmlTableStyleClass);
procedure ExpressionGet(const Value: variant; out Result: variant);
procedure ExpressionHtmlTable(const Value: variant; out Result: variant);
end;
{ TExpressionHelperForTable }
constructor TExpressionHelperForTable.Create(aRest: TRest; aTable: TOrmClass;
var aHelpers: TSynMustacheHelpers;
aHtmlTableStyle: TExpressionHtmlTableStyleClass);
var
HelperName: RawUtf8;
begin
aRest.PrivateGarbageCollector.Add(self);
Rest := aRest;
HelperName := RawUtf8(aTable.ClassName);
Table := aTable;
TableProps := aTable.OrmProps;
TSynMustache.HelperAdd(aHelpers, HelperName, ExpressionGet);
if aHtmlTableStyle = nil then
aHtmlTableStyle := TExpressionHtmlTableStyleBootstrap;
HtmlTableStyle := aHtmlTableStyle;
TSynMustache.HelperAdd(aHelpers,
HelperName + '.HtmlTable', ExpressionHtmlTable);
end;
procedure TExpressionHelperForTable.ExpressionHtmlTable(const Value: variant;
out Result: variant);
var
Rec: PDocVariantData;
f, i, j: PtrInt;
int: integer;
Field: TOrmPropInfo;
timelog: TTimeLogBits;
caption: string;
sets: TStringList;
u: RawUtf8;
W: TTextWriter;
tmp: TTextWriterStackBuffer;
const
ONOFF: array[boolean] of THtmlTableStyleLabel = (
labelOff, labelOn);
ENUM: array[boolean, boolean] of THtmlTableStyleLabel = (
(labelValue, labelValue),
(labelFalse, labelTrue));
begin
if _SafeObject(Value, Rec) then
begin
W := TTextWriter.CreateOwnedStream(tmp);
try
HtmlTableStyle.StartTable(W);
for f := 0 to TableProps.Fields.Count - 1 do
begin
Field := TableProps.Fields.List[f];
i := Rec^.GetValueIndex(Field.Name);
if i < 0 then
continue;
if not (Field.OrmFieldType in
[oftAnsiText,
oftUtf8Text,
oftInteger,
oftFloat,
oftCurrency,
oftTimeLog,