forked from adamdruppe/arsd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
web.d
5039 lines (4139 loc) · 144 KB
/
web.d
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
/++
Old magic web wrapper - one of my first applications of CT reflection. Given a class of fairly ordinary C code, it automatically creates HTML pages and forms, a Javascript file to access the functions from the client, and JSON based api responses. I do $(I not) recommend it for new projects though, as a replacement is now built into [arsd.cgi].
+/
module arsd.web;
static if(__VERSION__ <= 2076) {
// compatibility shims with gdc
enum JSONType {
object = JSON_TYPE.OBJECT,
null_ = JSON_TYPE.NULL,
false_ = JSON_TYPE.FALSE,
true_ = JSON_TYPE.TRUE,
integer = JSON_TYPE.INTEGER,
float_ = JSON_TYPE.FLOAT,
array = JSON_TYPE.ARRAY,
string = JSON_TYPE.STRING,
uinteger = JSON_TYPE.UINTEGER
}
}
// it would be nice to be able to add meta info to a returned envelope
// with cookie sessions, you must commit your session yourself before writing any content
enum RequirePost;
enum RequireHttps;
enum NoAutomaticForm;
///
struct GenericContainerType {
string type; ///
}
/// Attribute for the default formatting (html, table, json, etc)
struct DefaultFormat {
string format;
}
/// Sets the preferred request method, used by things like other code generators.
/// While this is preferred, the function is still callable from any request method.
///
/// By default, the preferred method is GET if the name starts with "get" and POST otherwise.
///
/// See also: RequirePost, ensureGoodPost, and using Cgi.RequestMethod as an attribute
struct PreferredMethod {
Cgi.RequestMethod preferredMethod;
}
/// With this attribute, the function is only called if the input data's
/// content type is what you specify here. Makes sense for POST and PUT
/// verbs.
struct IfInputContentType {
string contentType;
string dataGoesInWhichArgument;
}
/**
URL Mapping
By default, it is the method name OR the method name separated by dashes instead of camel case
*/
/+
Attributes
// this is different than calling ensureGoodPost because
// it is only called on direct calls. ensureGoodPost is flow oriented
enum RequirePost;
// path info? One could be the name of the current function, one could be the stuff past it...
// Incomplete form handler
// overrides the getGenericContainer
struct DocumentContainer {}
// custom formatter for json and other user defined types
// custom title for the page
// do we prefill from url? something else? default?
struct Prefill {}
// btw prefill should also take a function
// perhaps a FormFinalizer
// for automatic form creation
struct ParameterSuggestions {
string[] suggestions;
bool showDropdown; /* otherwise it is just autocomplete on a text box */
}
+/
// FIXME: if a method has a default value of a non-primitive type,
// it's still liable to screw everything else.
/*
Reasonably easy CSRF plan:
A csrf token can be associated with the entire session, and
saved in the session file.
Each form outputs the token, and it is added as a parameter to
the script thingy somewhere.
It need only be sent on POST items. Your app should handle proper
get and post separation.
*/
/*
Future directions for web stuff:
an improved css:
add definition nesting
add importing things from another definition
Implemented: see html.d
All css improvements are done via simple text rewriting. Aside
from the nesting, it'd just be a simple macro system.
Struct input functions:
static typeof(this) fromWebString(string fromUrl) {}
Automatic form functions:
static Element makeFormElement(Document document) {}
javascript:
I'd like to add functions and do static analysis actually.
I can't believe I just said that though.
But the stuff I'd analyze is checking it against the
D functions, recognizing that JS is loosely typed.
So basically it can do a grep for simple stuff:
CoolApi.xxxxxxx
if xxxxxxx isn't a function in CoolApi (the name
it knows from the server), it can flag a compile
error.
Might not be able to catch usage all the time
but could catch typo names.
*/
/*
FIXME: in params on the wrapped functions generally don't work
(can't modify const)
Running from the command line:
./myapp function positional args....
./myapp --format=json function
./myapp --make-nested-call
Formatting data:
CoolApi.myFunc().getFormat('Element', [...same as get...]);
You should also be able to ask for json, but with a particular format available as toString
format("json", "html") -- gets json, but each object has it's own toString. Actually, the object adds
a member called formattedSecondarily that is the other thing.
Note: the array itself cannot be changed in format, only it's members.
Note: the literal string of the formatted object is often returned. This may more than double the bandwidth of the call
Note: BUG: it only works with built in formats right now when doing secondary
// formats are: text, html, json, table, and xml
// except json, they are all represented as strings in json values
string toString -> formatting as text
Element makeHtmlElement -> making it html (same as fragment)
JSONValue makeJsonValue -> formatting to json
Table makeHtmlTable -> making a table
(not implemented) toXml -> making it into an xml document
Arrays can be handled too:
static (converts to) string makeHtmlArray(typeof(this)[] arr);
Envelope format:
document (default), json, none
*/
import std.exception;
static import std.uri;
public import arsd.dom;
public import arsd.cgi; // you have to import this in the actual usage file or else it won't link; surely a compiler bug
import arsd.sha;
public import std.string;
public import std.array;
public import std.stdio : writefln;
public import std.conv;
import std.random;
import std.typetuple;
import std.datetime;
public import std.range;
public import std.traits;
import std.json;
/// This gets your site's base link. note it's really only good if you are using FancyMain.
string getSiteLink(Cgi cgi) {
return cgi.requestUri[0.. cgi.requestUri.indexOf(cgi.scriptName) + cgi.scriptName.length + 1 /* for the slash at the end */];
}
/// use this in a function parameter if you want the automatic form to render
/// it as a textarea
/// FIXME: this should really be an annotation on the parameter... somehow
struct Text {
string content;
alias content this;
}
///
struct URL {
string url; ///
string title; ///
alias url this;
}
/// This is the JSON envelope format
struct Envelope {
bool success; /// did the call succeed? false if it threw an exception
string type; /// static type of the return value
string errorMessage; /// if !success, this is exception.msg
string userData; /// null unless the user request included passedThroughUserData
// use result.str if the format was anything other than json
JSONValue result; /// the return value of the function
debug string dFullString; /// exception.toString - includes stack trace, etc. Only available in debug mode for privacy reasons.
}
/// Info about the current request - more specialized than the cgi object directly
struct RequestInfo {
string mainSitePath; /// the bottom-most ApiProvider's path in this request
string objectBasePath; /// the top-most resolved path in the current request
FunctionInfo currentFunction; /// what function is being called according to the url?
string requestedFormat; /// the format the returned data was requested to be sent
string requestedEnvelopeFormat; /// the format the data is to be wrapped in
}
/+
string linkTo(alias func, T...)(T args) {
auto reflection = __traits(parent, func).reflection;
assert(reflection !is null);
auto name = func.stringof;
auto idx = name.indexOf("(");
if(idx != -1)
name = name[0 .. idx];
auto funinfo = reflection.functions[name];
return funinfo.originalName;
}
+/
/// this is there so there's a common runtime type for all callables
class WebDotDBaseType {
Cgi cgi; /// lower level access to the request
/// use this to look at exceptions and set up redirects and such. keep in mind it does NOT change the regular behavior
void exceptionExaminer(Throwable e) {}
// HACK: to enable breaking up the path somehow
int pathInfoStartingPoint() { return 0; }
/// Override this if you want to do something special to the document
/// You should probably call super._postProcess at some point since I
/// might add some default transformations here.
/// By default, it forwards the document root to _postProcess(Element).
void _postProcess(Document document) {
auto td = cast(TemplatedDocument) document;
if(td !is null)
td.vars["compile.timestamp"] = compiliationStamp;
if(document !is null && document.root !is null)
_postProcessElement(document.root);
}
/// Override this to do something special to returned HTML Elements.
/// This is ONLY run if the return type is(: Element). It is NOT run
/// if the return type is(: Document).
void _postProcessElement(Element element) {} // why the fuck doesn't overloading actually work?
/// convenience function to enforce that the current method is POST.
/// You should use this if you are going to commit to the database or something.
void ensurePost() {
assert(cgi !is null);
enforce(cgi.requestMethod == Cgi.RequestMethod.POST);
}
}
/// This is meant to beautify and check links and javascripts to call web.d functions.
/// FIXME: this function sucks.
string linkCall(alias Func, Args...)(Args args) {
static if(!__traits(compiles, Func(args))) {
static assert(0, "Your function call doesn't compile. If you need client side dynamic data, try building the call as a string.");
}
// FIXME: this link won't work from other parts of the site...
//string script = __traits(parent, Func).stringof;
auto href = __traits(identifier, Func) ~ "?";
bool outputted = false;
foreach(i, arg; args) {
if(outputted) {
href ~= "&";
} else
outputted = true;
href ~= std.uri.encodeComponent("positional-arg-" ~ to!string(i));
href ~= "=";
href ~= to!string(arg); // FIXME: this is wrong for all but the simplest types
}
return href;
}
/// This is meant to beautify and check links and javascripts to call web.d functions.
/// This function works pretty ok. You're going to want to append a string to the return
/// value to actually call .get() or whatever; it only does the name and arglist.
string jsCall(alias Func, Args...)(Args args) /*if(is(__traits(parent, Func) : WebDotDBaseType))*/ {
static if(!is(typeof(Func(args)))) { //__traits(compiles, Func(args))) {
static assert(0, "Your function call doesn't compile. If you need client side dynamic data, try building the call as a string.");
}
string script = __traits(parent, Func).stringof;
script ~= "." ~ __traits(identifier, Func) ~ "(";
bool outputted = false;
foreach(arg; args) {
if(outputted) {
script ~= ",";
} else
outputted = true;
script ~= toJson(arg);
}
script ~= ")";
return script;
}
/// Everything should derive from this instead of the old struct namespace used before
/// Your class must provide a default constructor.
class ApiProvider : WebDotDBaseType {
/*private*/ ApiProvider builtInFunctions;
Session session; // note: may be null
/// override this to change cross-site request forgery checks.
///
/// To perform a csrf check, call ensureGoodPost(); in your code.
///
/// It throws a PermissionDeniedException if the check fails.
/// This might change later to make catching it easier.
///
/// If there is no session object, the test always succeeds. This lets you opt
/// out of the system.
///
/// If the session is null, it does nothing. FancyMain makes a session for you.
/// If you are doing manual run(), it is your responsibility to create a session
/// and attach it to each primary object.
///
/// NOTE: it is important for you use ensureGoodPost() on any data changing things!
/// This function alone is a no-op on non-POST methods, so there's no real protection
/// without ensuring POST when making changes.
///
// FIXME: if someone is OAuth authorized, a csrf token should not really be necessary.
// This check is done automatically right now, and doesn't account for that. I guess
// people could override it in a subclass though. (Which they might have to since there's
// no oauth integration at this level right now anyway. Nor may there ever be; it's kinda
// high level. Perhaps I'll provide an oauth based subclass later on.)
protected void checkCsrfToken() {
assert(cgi !is null);
if(cgi.requestMethod == Cgi.RequestMethod.POST) {
auto tokenInfo = _getCsrfInfo();
if(tokenInfo is null)
return; // not doing checks
void fail() {
throw new PermissionDeniedException("CSRF token test failed " ~ to!string(cgi.postArray));
/*
~ "::::::"~cgi.post[
tokenInfo["key"]
] ~ " != " ~
tokenInfo["token"]);
*/
}
// expiration is handled by the session itself expiring (in the Session class)
if(tokenInfo["key"] !in cgi.post)
fail();
if(cgi.post[tokenInfo["key"]] != tokenInfo["token"])
fail();
}
}
protected bool isCsrfTokenCorrect() {
auto tokenInfo = _getCsrfInfo();
if(tokenInfo is null)
return false; // this means we aren't doing checks (probably because there is no session), but it is a failure nonetheless
auto token = tokenInfo["key"] ~ "=" ~ tokenInfo["token"];
if("x-arsd-csrf-pair" in cgi.requestHeaders)
return cgi.requestHeaders["x-arsd-csrf-pair"] == token;
if(tokenInfo["key"] in cgi.post)
return cgi.post[tokenInfo["key"]] == tokenInfo["token"];
if(tokenInfo["key"] in cgi.get)
return cgi.get[tokenInfo["key"]] == tokenInfo["token"];
return false;
}
/// Shorthand for ensurePost and checkCsrfToken. You should use this on non-indempotent
/// functions. Override it if doing some custom checking.
void ensureGoodPost() {
if(_noCsrfChecks) return;
ensurePost();
checkCsrfToken();
}
bool _noCsrfChecks; // this is a hack to let you use the functions internally more easily
// gotta make sure this isn't callable externally! Oh lol that'd defeat the point...
/// Gets the CSRF info (an associative array with key and token inside at least) from the session.
/// Note that the actual token is generated by the Session class.
protected string[string] _getCsrfInfo() {
if(session is null || this._noCsrfChecks)
return null;
return decodeVariablesSingle(session.csrfToken);
}
/// Adds CSRF tokens to the document for use by script (required by the Javascript API)
/// and then calls addCsrfTokens(document.root) to add them to all POST forms as well.
protected void addCsrfTokens(Document document) {
if(document is null)
return;
auto bod = document.mainBody;
if(bod is null)
return;
if(!bod.hasAttribute("data-csrf-key")) {
auto tokenInfo = _getCsrfInfo();
if(tokenInfo is null)
return;
if(bod !is null) {
bod.setAttribute("data-csrf-key", tokenInfo["key"]);
bod.setAttribute("data-csrf-token", tokenInfo["token"]);
}
addCsrfTokens(document.root);
}
}
/// we have to add these things to the document...
override void _postProcess(Document document) {
if(document !is null) {
foreach(pp; documentPostProcessors)
pp(document);
addCsrfTokens(document);
}
super._postProcess(document);
}
/// This adds CSRF tokens to all forms in the tree
protected void addCsrfTokens(Element element) {
if(element is null)
return;
auto tokenInfo = _getCsrfInfo();
if(tokenInfo is null)
return;
foreach(formElement; element.getElementsByTagName("form")) {
if(formElement.method != "POST" && formElement.method != "post")
continue;
auto form = cast(Form) formElement;
assert(form !is null);
form.setValue(tokenInfo["key"], tokenInfo["token"]);
}
}
// and added to ajax forms..
override void _postProcessElement(Element element) {
foreach(pp; elementPostProcessors)
pp(element);
addCsrfTokens(element);
super._postProcessElement(element);
}
// FIXME: the static is meant to be a performance improvement, but it breaks child modules' reflection!
/*static */immutable(ReflectionInfo)* reflection;
string _baseUrl; // filled based on where this is called from on this request
RequestInfo currentRequest; // FIXME: actually fill this in
/// Override this if you have initialization work that must be done *after* cgi and reflection is ready.
/// It should be used instead of the constructor for most work.
void _initialize() {}
/// On each call, you can register another post processor for the generated html. If your delegate takes a Document, it will only run on document envelopes (full pages generated). If you take an Element, it will apply on almost any generated html.
///
/// Note: if you override _postProcess or _postProcessElement, be sure to call the superclass version for these registered functions to run.
void _registerPostProcessor(void delegate(Document) pp) {
documentPostProcessors ~= pp;
}
/// ditto
void _registerPostProcessor(void delegate(Element) pp) {
elementPostProcessors ~= pp;
}
/// ditto
void _registerPostProcessor(void function(Document) pp) {
documentPostProcessors ~= delegate void(Document d) { pp(d); };
}
/// ditto
void _registerPostProcessor(void function(Element) pp) {
elementPostProcessors ~= delegate void(Element d) { pp(d); };
}
// these only work for one particular call
private void delegate(Document d)[] documentPostProcessors;
private void delegate(Element d)[] elementPostProcessors;
/*private*/ void _initializePerCallInternal() {
documentPostProcessors = null;
elementPostProcessors = null;
_initializePerCall();
}
/// This one is called at least once per call. (_initialize is only called once per process)
void _initializePerCall() {}
/// Returns the stylesheet for this module. Use it to encapsulate the needed info for your output so the module is more easily reusable
/// Override this to provide your own stylesheet. (of course, you can always provide it via _catchAll or any standard css file/style element too.)
string _style() const {
return null;
}
/// Returns the combined stylesheet of all child modules and this module
string stylesheet() const {
string ret;
foreach(i; reflection.objects) {
if(i.instantiation !is null)
ret ~= i.instantiation.stylesheet();
}
ret ~= _style();
return ret;
}
int redirectsSuppressed;
/// Temporarily disables the redirect() call.
void disableRedirects() {
redirectsSuppressed++;
}
/// Re-enables redirects. Call this once for every call to disableRedirects.
void enableRedirects() {
if(redirectsSuppressed)
redirectsSuppressed--;
}
/// This tentatively redirects the user - depends on the envelope fomat
/// You can temporarily disable this using disableRedirects()
string redirect(string location, bool important = false, string status = null) {
if(redirectsSuppressed)
return location;
auto f = cgi.request("envelopeFormat", "document");
if(f == "document" || f == "redirect" || f == "json_enable_redirects")
cgi.setResponseLocation(location, important, status);
return location;
}
/// Returns a list of links to all functions in this class or sub-classes
/// You can expose it publicly with alias: "alias _sitemap sitemap;" for example.
Element _sitemap() {
auto container = Element.make("div", "", "sitemap");
void writeFunctions(Element list, in ReflectionInfo* reflection, string base) {
string[string] handled;
foreach(key, func; reflection.functions) {
if(func.originalName in handled)
continue;
handled[func.originalName] = func.originalName;
// skip these since the root is what this is there for
if(func.originalName == "GET" || func.originalName == "POST")
continue;
// the builtins aren't interesting either
if(key.startsWith("builtin."))
continue;
if(func.originalName.length)
list.addChild("li", new Link(base ~ func.name, beautify(func.originalName)));
}
handled = null;
foreach(obj; reflection.objects) {
if(obj.name in handled)
continue;
handled[obj.name] = obj.name;
auto li = list.addChild("li", new Link(base ~ obj.name, obj.name));
auto ul = li.addChild("ul");
writeFunctions(ul, obj, base ~ obj.name ~ "/");
}
}
auto list = container.addChild("ul");
auto starting = _baseUrl;
if(starting is null)
starting = cgi.logicalScriptName ~ cgi.pathInfo; // FIXME
writeFunctions(list, reflection, starting ~ "/");
return container;
}
/// If the user goes to your program without specifying a path, this function is called.
// FIXME: should it return document? That's kinda a pain in the butt.
Document _defaultPage() {
throw new Exception("no default");
}
/// forwards to [_getGenericContainer]("default")
Element _getGenericContainer() {
return _getGenericContainer("default");
}
/// When the html document envelope is used, this function is used to get a html element
/// where the return value is appended.
/// It's the main function to override to provide custom HTML templates.
///
/// The default document provides a default stylesheet, our default javascript, and some timezone cookie handling (which you must handle on the server. Eventually I'll open source my date-time helpers that do this, but the basic idea is it sends an hour offset, and you can add that to any UTC time you have to get a local time).
Element _getGenericContainer(string containerName)
out(ret) {
assert(ret !is null);
}
do {
auto document = new TemplatedDocument(
"<!DOCTYPE html>
<html>
<head>
<title></title>
<link rel=\"stylesheet\" id=\"webd-styles-css\" href=\"styles.css?"~compiliationStamp~"\" />
<script> var delayedExecutionQueue = []; </script> <!-- FIXME do some better separation -->
<script>
if(document.cookie.indexOf(\"timezone=\") == -1) {
var d = new Date();
var tz = -d.getTimezoneOffset() / 60;
document.cookie = \"timezone=\" + tz + \"; path=/\";
}
</script>
<style>
.format-row { display: none; }
.validation-failed { background-color: #ffe0e0; }
</style>
</head>
<body>
<div id=\"body\"></div>
<script id=\"webd-functions-js\" src=\"functions.js?"~compiliationStamp~"\"></script>
" ~ deqFoot ~ "
</body>
</html>");
if(this.reflection !is null)
document.title = this.reflection.name;
auto container = document.requireElementById("body");
return container;
}
// FIXME: set a generic container for a particular call
/// If the given url path didn't match a function, it is passed to this function
/// for further handling. By default, it throws a NoSuchFunctionException.
/// Overriding it might be useful if you want to serve generic filenames or an opDispatch kind of thing.
/// (opDispatch itself won't work because it's name argument needs to be known at compile time!)
///
/// Note that you can return Documents here as they implement
/// the FileResource interface too.
FileResource _catchAll(string path) {
throw new NoSuchFunctionException(_errorMessageForCatchAll);
}
private string _errorMessageForCatchAll;
/*private*/ FileResource _catchallEntry(string path, string funName, string errorMessage) {
if(!errorMessage.length) {
/*
string allFuncs, allObjs;
foreach(n, f; reflection.functions)
allFuncs ~= n ~ "\n";
foreach(n, f; reflection.objects)
allObjs ~= n ~ "\n";
errorMessage = "no such function " ~ funName ~ "\n functions are:\n" ~ allFuncs ~ "\n\nObjects are:\n" ~ allObjs;
*/
errorMessage = "No such page: " ~ funName;
}
_errorMessageForCatchAll = errorMessage;
return _catchAll(path);
}
/// When in website mode, you can use this to beautify the error message
Document delegate(Throwable) _errorFunction;
}
enum string deqFoot = "
<script>delayedExecutionQueue.runCode = function() {
var a = 0;
for(a = 0; a < this.length; a++) {
try {
this[a]();
} catch(e) {/*ignore*/}
}
this.length = 0;
}; delayedExecutionQueue.runCode();</script>
";
/// Implement subclasses of this inside your main provider class to do a more object
/// oriented site.
class ApiObject : WebDotDBaseType {
/* abstract this(ApiProvider parent, string identifier) */
/// Override this to make json out of this object
JSONValue makeJsonValue() {
return toJsonValue(null);
}
}
class DataFile : FileResource {
this(string contentType, immutable(void)[] contents) {
_contentType = contentType;
_content = contents;
}
private string _contentType;
private immutable(void)[] _content;
string contentType() const {
return _contentType;
}
@property string filename() const { return null; }
immutable(ubyte)[] getData() const {
return cast(immutable(ubyte)[]) _content;
}
}
/// Describes the info collected about your class
struct ReflectionInfo {
immutable(FunctionInfo)*[string] functions; /// the methods
EnumInfo[string] enums; /// .
StructInfo[string] structs; ///.
const(ReflectionInfo)*[string] objects; /// ApiObjects and ApiProviders
bool needsInstantiation; // internal - does the object exist or should it be new'd before referenced?
ApiProvider instantiation; // internal (for now) - reference to the actual object being described
WebDotDBaseType delegate(string) instantiate;
// the overall namespace
string name; /// this is also used as the object name in the JS api
// these might go away.
string defaultOutputFormat = "default";
int versionOfOutputFormat = 2; // change this in your constructor if you still need the (deprecated) old behavior
// bool apiMode = false; // no longer used - if format is json, apiMode behavior is assumed. if format is html, it is not.
// FIXME: what if you want the data formatted server side, but still in a json envelope?
// should add format-payload:
}
/// describes an enum, iff based on int as the underlying type
struct EnumInfo {
string name; ///.
int[] values; ///.
string[] names; ///.
}
/// describes a plain data struct
struct StructInfo {
string name; ///.
// a struct is sort of like a function constructor...
StructMemberInfo[] members; ///.
}
///.
struct StructMemberInfo {
string name; ///.
string staticType; ///.
string defaultValue; ///.
}
///.
struct FunctionInfo {
WrapperFunction dispatcher; /// this is the actual function called when a request comes to it - it turns a string[][string] into the actual args and formats the return value
const(ReflectionInfo)* parentObject;
// should I also offer dispatchers for other formats like Variant[]?
string name; /// the URL friendly name
string originalName; /// the original name in code
//string uriPath;
Parameter[] parameters; ///.
string returnType; ///. static type to string
bool returnTypeIsDocument; // internal used when wrapping
bool returnTypeIsElement; // internal used when wrapping
bool requireHttps;
string genericContainerType = "default";
Document delegate(in string[string] args) createForm; /// This is used if you want a custom form - normally, on insufficient parameters, an automatic form is created. But if there's a functionName_Form method, it is used instead. FIXME: this used to work but not sure if it still does
}
/// Function parameter
struct Parameter {
string name; /// name (not always accurate)
string value; // ???
string type; /// type of HTML element to create when asking
string staticType; /// original type
string validator; /// FIXME
bool hasDefault; /// if there was a default defined in the function
string defaultValue; /// the default value defined in D, but as a string, if present
// for radio and select boxes
string[] options; /// possible options for selects
string[] optionValues; ///.
Element function(Document, string) makeFormElement;
}
// these are all filthy hacks
template isEnum(alias T) if(is(T)) {
static if (is(T == enum))
enum bool isEnum = true;
else
enum bool isEnum = false;
}
template isEnum(alias T) if(!is(T)) {
enum bool isEnum = false;
}
// WTF, shouldn't is(T == xxx) already do this?
template isEnum(T) if(!is(T)) {
enum bool isEnum = false;
}
template isStruct(alias T) {
static if (is(T == struct))
enum bool isStruct = true;
else
enum bool isStruct = false;
}
template isApiObject(alias T) {
static if (is(T : ApiObject))
enum bool isApiObject = true;
else
enum bool isApiObject = false;
}
template isApiProvider(alias T) {
static if (is(T : ApiProvider))
enum bool isApiProvider = true;
else
enum bool isApiProvider = false;
}
template Passthrough(T) {
T Passthrough;
}
template PassthroughType(T) {
alias T PassthroughType;
}
// sets up the reflection object. now called automatically so you probably don't have to mess with it
immutable(ReflectionInfo*) prepareReflection(alias PM)(PM instantiation) if(is(PM : ApiProvider) || is(PM: ApiObject) ) {
return prepareReflectionImpl!(PM, PM)(instantiation);
}
// FIXME: this doubles the compile time and can add megabytes to the executable.
immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Parent instantiation)
if(is(PM : WebDotDBaseType) && is(Parent : ApiProvider))
{
assert(instantiation !is null);
ReflectionInfo* reflection = new ReflectionInfo;
reflection.name = PM.stringof;
static if(is(PM: ApiObject)) {
reflection.needsInstantiation = true;
reflection.instantiate = delegate WebDotDBaseType(string i) {
auto n = new PM(instantiation, i);
return n;
};
} else {
reflection.instantiation = instantiation;
static if(!is(PM : BuiltInFunctions)) {
auto builtins = new BuiltInFunctions(instantiation, reflection);
instantiation.builtInFunctions = builtins;
foreach(k, v; builtins.reflection.functions)
reflection.functions["builtin." ~ k] = v;
}
}
static if(is(PM : ApiProvider)) {{ // double because I want a new scope
auto f = new FunctionInfo;
f.parentObject = reflection;
f.dispatcher = generateWrapper!(PM, "_defaultPage", PM._defaultPage)(reflection, instantiation);
f.returnTypeIsDocument = true;
reflection.functions["/"] = cast(immutable) f;
/+
// catchAll here too
f = new FunctionInfo;
f.parentObject = reflection;
f.dispatcher = generateWrapper!(PM, "_catchAll", PM._catchAll)(reflection, instantiation);
f.returnTypeIsDocument = true;
reflection.functions["/_catchAll"] = cast(immutable) f;
+/
}}
// derivedMembers is changed from allMembers
// FIXME: this seems to do the right thing with inheritance.... but I don't really understand why. Isn't the override done first, and thus overwritten by the base class version? you know maybe it is all because it still does a vtable lookup on the real object. eh idk, just confirm what it does eventually
foreach(Class; TypeTuple!(PM, BaseClassesTuple!(PM)))
static if((is(Class : ApiProvider) && !is(Class == ApiProvider)) || is(Class : ApiObject))
foreach(member; __traits(derivedMembers, Class)) { // we do derived on a base class loop because we don't want interfaces (OR DO WE? seriously idk) and we definitely don't want stuff from Object, ApiProvider itself is out too but that might change.
static if(member[0] != '_') {
// FIXME: the filthiest of all hacks...
static if(!__traits(compiles,
!is(typeof(__traits(getMember, Class, member)) == function) &&
isEnum!(__traits(getMember, Class, member))))
continue; // must be a data member or something...
else
// DONE WITH FILTHIEST OF ALL HACKS
//if(member.length == 0)
// continue;
static if(
!is(typeof(__traits(getMember, Class, member)) == function) &&
isEnum!(__traits(getMember, Class, member))
) {
EnumInfo i;
i.name = member;
foreach(m; __traits(allMembers, __traits(getMember, Class, member))) {
i.names ~= m;