1 /++ 2 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]. 3 +/ 4 module arsd.web; 5 6 7 static if(__VERSION__ <= 2076) { 8 // compatibility shims with gdc 9 enum JSONType { 10 object = JSON_TYPE.OBJECT, 11 null_ = JSON_TYPE.NULL, 12 false_ = JSON_TYPE.FALSE, 13 true_ = JSON_TYPE.TRUE, 14 integer = JSON_TYPE.INTEGER, 15 float_ = JSON_TYPE.FLOAT, 16 array = JSON_TYPE.ARRAY, 17 string = JSON_TYPE.STRING, 18 uinteger = JSON_TYPE.UINTEGER 19 } 20 } 21 22 23 24 // it would be nice to be able to add meta info to a returned envelope 25 26 // with cookie sessions, you must commit your session yourself before writing any content 27 28 enum RequirePost; 29 enum RequireHttps; 30 enum NoAutomaticForm; 31 32 /// 33 struct GenericContainerType { 34 string type; /// 35 } 36 37 /// Attribute for the default formatting (html, table, json, etc) 38 struct DefaultFormat { 39 string format; 40 } 41 42 /// Sets the preferred request method, used by things like other code generators. 43 /// While this is preferred, the function is still callable from any request method. 44 /// 45 /// By default, the preferred method is GET if the name starts with "get" and POST otherwise. 46 /// 47 /// See also: RequirePost, ensureGoodPost, and using Cgi.RequestMethod as an attribute 48 struct PreferredMethod { 49 Cgi.RequestMethod preferredMethod; 50 } 51 52 /// With this attribute, the function is only called if the input data's 53 /// content type is what you specify here. Makes sense for POST and PUT 54 /// verbs. 55 struct IfInputContentType { 56 string contentType; 57 string dataGoesInWhichArgument; 58 } 59 60 /** 61 URL Mapping 62 63 By default, it is the method name OR the method name separated by dashes instead of camel case 64 */ 65 66 67 /+ 68 Attributes 69 70 // this is different than calling ensureGoodPost because 71 // it is only called on direct calls. ensureGoodPost is flow oriented 72 enum RequirePost; 73 74 // path info? One could be the name of the current function, one could be the stuff past it... 75 76 // Incomplete form handler 77 78 // overrides the getGenericContainer 79 struct DocumentContainer {} 80 81 // custom formatter for json and other user defined types 82 83 // custom title for the page 84 85 // do we prefill from url? something else? default? 86 struct Prefill {} 87 88 // btw prefill should also take a function 89 // perhaps a FormFinalizer 90 91 // for automatic form creation 92 struct ParameterSuggestions { 93 string[] suggestions; 94 bool showDropdown; /* otherwise it is just autocomplete on a text box */ 95 } 96 97 +/ 98 99 // FIXME: if a method has a default value of a non-primitive type, 100 // it's still liable to screw everything else. 101 102 /* 103 Reasonably easy CSRF plan: 104 105 A csrf token can be associated with the entire session, and 106 saved in the session file. 107 108 Each form outputs the token, and it is added as a parameter to 109 the script thingy somewhere. 110 111 It need only be sent on POST items. Your app should handle proper 112 get and post separation. 113 */ 114 115 /* 116 Future directions for web stuff: 117 118 an improved css: 119 add definition nesting 120 add importing things from another definition 121 122 Implemented: see html.d 123 124 All css improvements are done via simple text rewriting. Aside 125 from the nesting, it'd just be a simple macro system. 126 127 128 Struct input functions: 129 static typeof(this) fromWebString(string fromUrl) {} 130 131 Automatic form functions: 132 static Element makeFormElement(Document document) {} 133 134 135 javascript: 136 I'd like to add functions and do static analysis actually. 137 I can't believe I just said that though. 138 139 But the stuff I'd analyze is checking it against the 140 D functions, recognizing that JS is loosely typed. 141 142 So basically it can do a grep for simple stuff: 143 144 CoolApi.xxxxxxx 145 146 if xxxxxxx isn't a function in CoolApi (the name 147 it knows from the server), it can flag a compile 148 error. 149 150 Might not be able to catch usage all the time 151 but could catch typo names. 152 153 */ 154 155 /* 156 FIXME: in params on the wrapped functions generally don't work 157 (can't modify const) 158 159 Running from the command line: 160 161 ./myapp function positional args.... 162 ./myapp --format=json function 163 164 ./myapp --make-nested-call 165 166 167 Formatting data: 168 169 CoolApi.myFunc().getFormat('Element', [...same as get...]); 170 171 You should also be able to ask for json, but with a particular format available as toString 172 173 format("json", "html") -- gets json, but each object has it's own toString. Actually, the object adds 174 a member called formattedSecondarily that is the other thing. 175 Note: the array itself cannot be changed in format, only it's members. 176 Note: the literal string of the formatted object is often returned. This may more than double the bandwidth of the call 177 178 Note: BUG: it only works with built in formats right now when doing secondary 179 180 181 // formats are: text, html, json, table, and xml 182 // except json, they are all represented as strings in json values 183 184 string toString -> formatting as text 185 Element makeHtmlElement -> making it html (same as fragment) 186 JSONValue makeJsonValue -> formatting to json 187 Table makeHtmlTable -> making a table 188 (not implemented) toXml -> making it into an xml document 189 190 191 Arrays can be handled too: 192 193 static (converts to) string makeHtmlArray(typeof(this)[] arr); 194 195 196 Envelope format: 197 198 document (default), json, none 199 */ 200 201 import std.exception; 202 static import std.uri; 203 public import arsd.dom; 204 public import arsd.cgi; // you have to import this in the actual usage file or else it won't link; surely a compiler bug 205 import arsd.sha; 206 207 public import std.string; 208 public import std.array; 209 public import std.stdio : writefln; 210 public import std.conv; 211 import std.random; 212 import std.typetuple; 213 214 import std.datetime; 215 216 public import std.range; 217 218 public import std.traits; 219 import std.json; 220 221 /// This gets your site's base link. note it's really only good if you are using FancyMain. 222 string getSiteLink(Cgi cgi) { 223 return cgi.requestUri[0.. cgi.requestUri.indexOf(cgi.scriptName) + cgi.scriptName.length + 1 /* for the slash at the end */]; 224 } 225 226 /// use this in a function parameter if you want the automatic form to render 227 /// it as a textarea 228 /// FIXME: this should really be an annotation on the parameter... somehow 229 struct Text { 230 string content; 231 alias content this; 232 } 233 234 /// 235 struct URL { 236 string url; /// 237 string title; /// 238 alias url this; 239 } 240 241 /// This is the JSON envelope format 242 struct Envelope { 243 bool success; /// did the call succeed? false if it threw an exception 244 string type; /// static type of the return value 245 string errorMessage; /// if !success, this is exception.msg 246 string userData; /// null unless the user request included passedThroughUserData 247 248 // use result.str if the format was anything other than json 249 JSONValue result; /// the return value of the function 250 251 debug string dFullString; /// exception.toString - includes stack trace, etc. Only available in debug mode for privacy reasons. 252 } 253 254 /// Info about the current request - more specialized than the cgi object directly 255 struct RequestInfo { 256 string mainSitePath; /// the bottom-most ApiProvider's path in this request 257 string objectBasePath; /// the top-most resolved path in the current request 258 259 FunctionInfo currentFunction; /// what function is being called according to the url? 260 261 string requestedFormat; /// the format the returned data was requested to be sent 262 string requestedEnvelopeFormat; /// the format the data is to be wrapped in 263 } 264 265 /+ 266 string linkTo(alias func, T...)(T args) { 267 auto reflection = __traits(parent, func).reflection; 268 assert(reflection !is null); 269 270 auto name = func.stringof; 271 auto idx = name.indexOf("("); 272 if(idx != -1) 273 name = name[0 .. idx]; 274 275 auto funinfo = reflection.functions[name]; 276 277 return funinfo.originalName; 278 } 279 +/ 280 281 /// this is there so there's a common runtime type for all callables 282 class WebDotDBaseType { 283 Cgi cgi; /// lower level access to the request 284 285 /// use this to look at exceptions and set up redirects and such. keep in mind it does NOT change the regular behavior 286 void exceptionExaminer(Throwable e) {} 287 288 // HACK: to enable breaking up the path somehow 289 int pathInfoStartingPoint() { return 0; } 290 291 /// Override this if you want to do something special to the document 292 /// You should probably call super._postProcess at some point since I 293 /// might add some default transformations here. 294 295 /// By default, it forwards the document root to _postProcess(Element). 296 void _postProcess(Document document) { 297 auto td = cast(TemplatedDocument) document; 298 if(td !is null) 299 td.vars["compile.timestamp"] = compiliationStamp; 300 301 if(document !is null && document.root !is null) 302 _postProcessElement(document.root); 303 } 304 305 /// Override this to do something special to returned HTML Elements. 306 /// This is ONLY run if the return type is(: Element). It is NOT run 307 /// if the return type is(: Document). 308 void _postProcessElement(Element element) {} // why the fuck doesn't overloading actually work? 309 310 /// convenience function to enforce that the current method is POST. 311 /// You should use this if you are going to commit to the database or something. 312 void ensurePost() { 313 assert(cgi !is null); 314 enforce(cgi.requestMethod == Cgi.RequestMethod.POST); 315 } 316 } 317 318 /// This is meant to beautify and check links and javascripts to call web.d functions. 319 /// FIXME: this function sucks. 320 string linkCall(alias Func, Args...)(Args args) { 321 static if(!__traits(compiles, Func(args))) { 322 static assert(0, "Your function call doesn't compile. If you need client side dynamic data, try building the call as a string."); 323 } 324 325 // FIXME: this link won't work from other parts of the site... 326 327 //string script = __traits(parent, Func).stringof; 328 auto href = __traits(identifier, Func) ~ "?"; 329 330 bool outputted = false; 331 foreach(i, arg; args) { 332 if(outputted) { 333 href ~= "&"; 334 } else 335 outputted = true; 336 337 href ~= std.uri.encodeComponent("positional-arg-" ~ to!string(i)); 338 href ~= "="; 339 href ~= to!string(arg); // FIXME: this is wrong for all but the simplest types 340 } 341 342 return href; 343 344 } 345 346 /// This is meant to beautify and check links and javascripts to call web.d functions. 347 /// This function works pretty ok. You're going to want to append a string to the return 348 /// value to actually call .get() or whatever; it only does the name and arglist. 349 string jsCall(alias Func, Args...)(Args args) /*if(is(__traits(parent, Func) : WebDotDBaseType))*/ { 350 static if(!is(typeof(Func(args)))) { //__traits(compiles, Func(args))) { 351 static assert(0, "Your function call doesn't compile. If you need client side dynamic data, try building the call as a string."); 352 } 353 354 string script = __traits(parent, Func).stringof; 355 script ~= "." ~ __traits(identifier, Func) ~ "("; 356 357 bool outputted = false; 358 foreach(arg; args) { 359 if(outputted) { 360 script ~= ","; 361 } else 362 outputted = true; 363 364 script ~= toJson(arg); 365 } 366 367 script ~= ")"; 368 return script; 369 } 370 371 /// Everything should derive from this instead of the old struct namespace used before 372 /// Your class must provide a default constructor. 373 class ApiProvider : WebDotDBaseType { 374 /*private*/ ApiProvider builtInFunctions; 375 376 Session session; // note: may be null 377 378 /// override this to change cross-site request forgery checks. 379 /// 380 /// To perform a csrf check, call ensureGoodPost(); in your code. 381 /// 382 /// It throws a PermissionDeniedException if the check fails. 383 /// This might change later to make catching it easier. 384 /// 385 /// If there is no session object, the test always succeeds. This lets you opt 386 /// out of the system. 387 /// 388 /// If the session is null, it does nothing. FancyMain makes a session for you. 389 /// If you are doing manual run(), it is your responsibility to create a session 390 /// and attach it to each primary object. 391 /// 392 /// NOTE: it is important for you use ensureGoodPost() on any data changing things! 393 /// This function alone is a no-op on non-POST methods, so there's no real protection 394 /// without ensuring POST when making changes. 395 /// 396 // FIXME: if someone is OAuth authorized, a csrf token should not really be necessary. 397 // This check is done automatically right now, and doesn't account for that. I guess 398 // people could override it in a subclass though. (Which they might have to since there's 399 // no oauth integration at this level right now anyway. Nor may there ever be; it's kinda 400 // high level. Perhaps I'll provide an oauth based subclass later on.) 401 protected void checkCsrfToken() { 402 assert(cgi !is null); 403 if(cgi.requestMethod == Cgi.RequestMethod.POST) { 404 auto tokenInfo = _getCsrfInfo(); 405 if(tokenInfo is null) 406 return; // not doing checks 407 408 void fail() { 409 throw new PermissionDeniedException("CSRF token test failed " ~ to!string(cgi.postArray)); 410 /* 411 ~ "::::::"~cgi.post[ 412 tokenInfo["key"] 413 ] ~ " != " ~ 414 tokenInfo["token"]); 415 */ 416 } 417 418 // expiration is handled by the session itself expiring (in the Session class) 419 420 if(tokenInfo["key"] !in cgi.post) 421 fail(); 422 if(cgi.post[tokenInfo["key"]] != tokenInfo["token"]) 423 fail(); 424 } 425 } 426 427 protected bool isCsrfTokenCorrect() { 428 auto tokenInfo = _getCsrfInfo(); 429 if(tokenInfo is null) 430 return false; // this means we aren't doing checks (probably because there is no session), but it is a failure nonetheless 431 432 auto token = tokenInfo["key"] ~ "=" ~ tokenInfo["token"]; 433 if("x-arsd-csrf-pair" in cgi.requestHeaders) 434 return cgi.requestHeaders["x-arsd-csrf-pair"] == token; 435 if(tokenInfo["key"] in cgi.post) 436 return cgi.post[tokenInfo["key"]] == tokenInfo["token"]; 437 if(tokenInfo["key"] in cgi.get) 438 return cgi.get[tokenInfo["key"]] == tokenInfo["token"]; 439 440 return false; 441 } 442 443 /// Shorthand for ensurePost and checkCsrfToken. You should use this on non-indempotent 444 /// functions. Override it if doing some custom checking. 445 void ensureGoodPost() { 446 if(_noCsrfChecks) return; 447 ensurePost(); 448 checkCsrfToken(); 449 } 450 451 bool _noCsrfChecks; // this is a hack to let you use the functions internally more easily 452 453 // gotta make sure this isn't callable externally! Oh lol that'd defeat the point... 454 /// Gets the CSRF info (an associative array with key and token inside at least) from the session. 455 /// Note that the actual token is generated by the Session class. 456 protected string[string] _getCsrfInfo() { 457 if(session is null || this._noCsrfChecks) 458 return null; 459 return decodeVariablesSingle(session.csrfToken); 460 } 461 462 /// Adds CSRF tokens to the document for use by script (required by the Javascript API) 463 /// and then calls addCsrfTokens(document.root) to add them to all POST forms as well. 464 protected void addCsrfTokens(Document document) { 465 if(document is null) 466 return; 467 auto bod = document.mainBody; 468 if(bod is null) 469 return; 470 if(!bod.hasAttribute("data-csrf-key")) { 471 auto tokenInfo = _getCsrfInfo(); 472 if(tokenInfo is null) 473 return; 474 if(bod !is null) { 475 bod.setAttribute("data-csrf-key", tokenInfo["key"]); 476 bod.setAttribute("data-csrf-token", tokenInfo["token"]); 477 } 478 479 addCsrfTokens(document.root); 480 } 481 } 482 483 /// we have to add these things to the document... 484 override void _postProcess(Document document) { 485 if(document !is null) { 486 foreach(pp; documentPostProcessors) 487 pp(document); 488 489 addCsrfTokens(document); 490 } 491 super._postProcess(document); 492 } 493 494 /// This adds CSRF tokens to all forms in the tree 495 protected void addCsrfTokens(Element element) { 496 if(element is null) 497 return; 498 auto tokenInfo = _getCsrfInfo(); 499 if(tokenInfo is null) 500 return; 501 502 foreach(formElement; element.getElementsByTagName("form")) { 503 if(formElement.method != "POST" && formElement.method != "post") 504 continue; 505 auto form = cast(Form) formElement; 506 assert(form !is null); 507 508 form.setValue(tokenInfo["key"], tokenInfo["token"]); 509 } 510 } 511 512 // and added to ajax forms.. 513 override void _postProcessElement(Element element) { 514 foreach(pp; elementPostProcessors) 515 pp(element); 516 517 addCsrfTokens(element); 518 super._postProcessElement(element); 519 } 520 521 522 // FIXME: the static is meant to be a performance improvement, but it breaks child modules' reflection! 523 /*static */immutable(ReflectionInfo)* reflection; 524 string _baseUrl; // filled based on where this is called from on this request 525 526 RequestInfo currentRequest; // FIXME: actually fill this in 527 528 /// Override this if you have initialization work that must be done *after* cgi and reflection is ready. 529 /// It should be used instead of the constructor for most work. 530 void _initialize() {} 531 532 /// 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. 533 /// 534 /// Note: if you override _postProcess or _postProcessElement, be sure to call the superclass version for these registered functions to run. 535 void _registerPostProcessor(void delegate(Document) pp) { 536 documentPostProcessors ~= pp; 537 } 538 539 /// ditto 540 void _registerPostProcessor(void delegate(Element) pp) { 541 elementPostProcessors ~= pp; 542 } 543 544 /// ditto 545 void _registerPostProcessor(void function(Document) pp) { 546 documentPostProcessors ~= delegate void(Document d) { pp(d); }; 547 } 548 549 /// ditto 550 void _registerPostProcessor(void function(Element) pp) { 551 elementPostProcessors ~= delegate void(Element d) { pp(d); }; 552 } 553 554 // these only work for one particular call 555 private void delegate(Document d)[] documentPostProcessors; 556 private void delegate(Element d)[] elementPostProcessors; 557 /*private*/ void _initializePerCallInternal() { 558 documentPostProcessors = null; 559 elementPostProcessors = null; 560 561 _initializePerCall(); 562 } 563 564 /// This one is called at least once per call. (_initialize is only called once per process) 565 void _initializePerCall() {} 566 567 /// Returns the stylesheet for this module. Use it to encapsulate the needed info for your output so the module is more easily reusable 568 /// Override this to provide your own stylesheet. (of course, you can always provide it via _catchAll or any standard css file/style element too.) 569 string _style() const { 570 return null; 571 } 572 573 /// Returns the combined stylesheet of all child modules and this module 574 string stylesheet() const { 575 string ret; 576 foreach(i; reflection.objects) { 577 if(i.instantiation !is null) 578 ret ~= i.instantiation.stylesheet(); 579 } 580 581 ret ~= _style(); 582 return ret; 583 } 584 585 int redirectsSuppressed; 586 587 /// Temporarily disables the redirect() call. 588 void disableRedirects() { 589 redirectsSuppressed++; 590 } 591 592 /// Re-enables redirects. Call this once for every call to disableRedirects. 593 void enableRedirects() { 594 if(redirectsSuppressed) 595 redirectsSuppressed--; 596 } 597 598 /// This tentatively redirects the user - depends on the envelope fomat 599 /// You can temporarily disable this using disableRedirects() 600 string redirect(string location, bool important = false, string status = null) { 601 if(redirectsSuppressed) 602 return location; 603 auto f = cgi.request("envelopeFormat", "document"); 604 if(f == "document" || f == "redirect" || f == "json_enable_redirects") 605 cgi.setResponseLocation(location, important, status); 606 return location; 607 } 608 609 /// Returns a list of links to all functions in this class or sub-classes 610 /// You can expose it publicly with alias: "alias _sitemap sitemap;" for example. 611 Element _sitemap() { 612 auto container = Element.make("div", "", "sitemap"); 613 614 void writeFunctions(Element list, in ReflectionInfo* reflection, string base) { 615 string[string] handled; 616 foreach(key, func; reflection.functions) { 617 if(func.originalName in handled) 618 continue; 619 handled[func.originalName] = func.originalName; 620 621 // skip these since the root is what this is there for 622 if(func.originalName == "GET" || func.originalName == "POST") 623 continue; 624 625 // the builtins aren't interesting either 626 if(key.startsWith("builtin.")) 627 continue; 628 629 if(func.originalName.length) 630 list.addChild("li", new Link(base ~ func.name, beautify(func.originalName))); 631 } 632 633 handled = null; 634 foreach(obj; reflection.objects) { 635 if(obj.name in handled) 636 continue; 637 handled[obj.name] = obj.name; 638 639 auto li = list.addChild("li", new Link(base ~ obj.name, obj.name)); 640 641 auto ul = li.addChild("ul"); 642 writeFunctions(ul, obj, base ~ obj.name ~ "/"); 643 } 644 } 645 646 auto list = container.addChild("ul"); 647 auto starting = _baseUrl; 648 if(starting is null) 649 starting = cgi.logicalScriptName ~ cgi.pathInfo; // FIXME 650 writeFunctions(list, reflection, starting ~ "/"); 651 652 return container; 653 } 654 655 /// If the user goes to your program without specifying a path, this function is called. 656 // FIXME: should it return document? That's kinda a pain in the butt. 657 Document _defaultPage() { 658 throw new Exception("no default"); 659 } 660 661 /// forwards to [_getGenericContainer]("default") 662 Element _getGenericContainer() { 663 return _getGenericContainer("default"); 664 } 665 666 /// When the html document envelope is used, this function is used to get a html element 667 /// where the return value is appended. 668 669 /// It's the main function to override to provide custom HTML templates. 670 /// 671 /// 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). 672 Element _getGenericContainer(string containerName) 673 out(ret) { 674 assert(ret !is null); 675 } 676 body { 677 auto document = new TemplatedDocument( 678 "<!DOCTYPE html> 679 <html> 680 <head> 681 <title></title> 682 <link rel=\"stylesheet\" id=\"webd-styles-css\" href=\"styles.css?"~compiliationStamp~"\" /> 683 <script> var delayedExecutionQueue = []; </script> <!-- FIXME do some better separation --> 684 <script> 685 if(document.cookie.indexOf(\"timezone=\") == -1) { 686 var d = new Date(); 687 var tz = -d.getTimezoneOffset() / 60; 688 document.cookie = \"timezone=\" + tz + \"; path=/\"; 689 } 690 </script> 691 <style> 692 .format-row { display: none; } 693 .validation-failed { background-color: #ffe0e0; } 694 </style> 695 </head> 696 <body> 697 <div id=\"body\"></div> 698 <script id=\"webd-functions-js\" src=\"functions.js?"~compiliationStamp~"\"></script> 699 " ~ deqFoot ~ " 700 </body> 701 </html>"); 702 if(this.reflection !is null) 703 document.title = this.reflection.name; 704 auto container = document.requireElementById("body"); 705 return container; 706 } 707 708 // FIXME: set a generic container for a particular call 709 710 /// If the given url path didn't match a function, it is passed to this function 711 /// for further handling. By default, it throws a NoSuchFunctionException. 712 713 /// Overriding it might be useful if you want to serve generic filenames or an opDispatch kind of thing. 714 /// (opDispatch itself won't work because it's name argument needs to be known at compile time!) 715 /// 716 /// Note that you can return Documents here as they implement 717 /// the FileResource interface too. 718 FileResource _catchAll(string path) { 719 throw new NoSuchFunctionException(_errorMessageForCatchAll); 720 } 721 722 private string _errorMessageForCatchAll; 723 /*private*/ FileResource _catchallEntry(string path, string funName, string errorMessage) { 724 if(!errorMessage.length) { 725 /* 726 string allFuncs, allObjs; 727 foreach(n, f; reflection.functions) 728 allFuncs ~= n ~ "\n"; 729 foreach(n, f; reflection.objects) 730 allObjs ~= n ~ "\n"; 731 errorMessage = "no such function " ~ funName ~ "\n functions are:\n" ~ allFuncs ~ "\n\nObjects are:\n" ~ allObjs; 732 */ 733 734 errorMessage = "No such page: " ~ funName; 735 } 736 737 _errorMessageForCatchAll = errorMessage; 738 739 return _catchAll(path); 740 } 741 742 /// When in website mode, you can use this to beautify the error message 743 Document delegate(Throwable) _errorFunction; 744 } 745 746 enum string deqFoot = " 747 <script>delayedExecutionQueue.runCode = function() { 748 var a = 0; 749 for(a = 0; a < this.length; a++) { 750 try { 751 this[a](); 752 } catch(e) {/*ignore*/} 753 } 754 this.length = 0; 755 }; delayedExecutionQueue.runCode();</script> 756 "; 757 758 /// Implement subclasses of this inside your main provider class to do a more object 759 /// oriented site. 760 class ApiObject : WebDotDBaseType { 761 /* abstract this(ApiProvider parent, string identifier) */ 762 763 /// Override this to make json out of this object 764 JSONValue makeJsonValue() { 765 return toJsonValue(null); 766 } 767 } 768 769 class DataFile : FileResource { 770 this(string contentType, immutable(void)[] contents) { 771 _contentType = contentType; 772 _content = contents; 773 } 774 775 private string _contentType; 776 private immutable(void)[] _content; 777 778 string contentType() const { 779 return _contentType; 780 } 781 782 immutable(ubyte)[] getData() const { 783 return cast(immutable(ubyte)[]) _content; 784 } 785 } 786 787 /// Describes the info collected about your class 788 struct ReflectionInfo { 789 immutable(FunctionInfo)*[string] functions; /// the methods 790 EnumInfo[string] enums; /// . 791 StructInfo[string] structs; ///. 792 const(ReflectionInfo)*[string] objects; /// ApiObjects and ApiProviders 793 794 bool needsInstantiation; // internal - does the object exist or should it be new'd before referenced? 795 796 ApiProvider instantiation; // internal (for now) - reference to the actual object being described 797 798 WebDotDBaseType delegate(string) instantiate; 799 800 // the overall namespace 801 string name; /// this is also used as the object name in the JS api 802 803 804 // these might go away. 805 806 string defaultOutputFormat = "default"; 807 int versionOfOutputFormat = 2; // change this in your constructor if you still need the (deprecated) old behavior 808 // bool apiMode = false; // no longer used - if format is json, apiMode behavior is assumed. if format is html, it is not. 809 // FIXME: what if you want the data formatted server side, but still in a json envelope? 810 // should add format-payload: 811 } 812 813 /// describes an enum, iff based on int as the underlying type 814 struct EnumInfo { 815 string name; ///. 816 int[] values; ///. 817 string[] names; ///. 818 } 819 820 /// describes a plain data struct 821 struct StructInfo { 822 string name; ///. 823 // a struct is sort of like a function constructor... 824 StructMemberInfo[] members; ///. 825 } 826 827 ///. 828 struct StructMemberInfo { 829 string name; ///. 830 string staticType; ///. 831 string defaultValue; ///. 832 } 833 834 ///. 835 struct FunctionInfo { 836 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 837 838 const(ReflectionInfo)* parentObject; 839 840 // should I also offer dispatchers for other formats like Variant[]? 841 842 string name; /// the URL friendly name 843 string originalName; /// the original name in code 844 845 //string uriPath; 846 847 Parameter[] parameters; ///. 848 849 string returnType; ///. static type to string 850 bool returnTypeIsDocument; // internal used when wrapping 851 bool returnTypeIsElement; // internal used when wrapping 852 853 bool requireHttps; 854 855 string genericContainerType = "default"; 856 857 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 858 } 859 860 /// Function parameter 861 struct Parameter { 862 string name; /// name (not always accurate) 863 string value; // ??? 864 865 string type; /// type of HTML element to create when asking 866 string staticType; /// original type 867 string validator; /// FIXME 868 869 bool hasDefault; /// if there was a default defined in the function 870 string defaultValue; /// the default value defined in D, but as a string, if present 871 872 // for radio and select boxes 873 string[] options; /// possible options for selects 874 string[] optionValues; ///. 875 876 Element function(Document, string) makeFormElement; 877 } 878 879 // these are all filthy hacks 880 881 template isEnum(alias T) if(is(T)) { 882 static if (is(T == enum)) 883 enum bool isEnum = true; 884 else 885 enum bool isEnum = false; 886 } 887 888 template isEnum(alias T) if(!is(T)) { 889 enum bool isEnum = false; 890 } 891 892 // WTF, shouldn't is(T == xxx) already do this? 893 template isEnum(T) if(!is(T)) { 894 enum bool isEnum = false; 895 } 896 897 template isStruct(alias T) { 898 static if (is(T == struct)) 899 enum bool isStruct = true; 900 else 901 enum bool isStruct = false; 902 } 903 904 template isApiObject(alias T) { 905 static if (is(T : ApiObject)) 906 enum bool isApiObject = true; 907 else 908 enum bool isApiObject = false; 909 } 910 911 template isApiProvider(alias T) { 912 static if (is(T : ApiProvider)) 913 enum bool isApiProvider = true; 914 else 915 enum bool isApiProvider = false; 916 } 917 918 template Passthrough(T) { 919 T Passthrough; 920 } 921 922 template PassthroughType(T) { 923 alias T PassthroughType; 924 } 925 926 // sets up the reflection object. now called automatically so you probably don't have to mess with it 927 immutable(ReflectionInfo*) prepareReflection(alias PM)(PM instantiation) if(is(PM : ApiProvider) || is(PM: ApiObject) ) { 928 return prepareReflectionImpl!(PM, PM)(instantiation); 929 } 930 931 // FIXME: this doubles the compile time and can add megabytes to the executable. 932 immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Parent instantiation) 933 if(is(PM : WebDotDBaseType) && is(Parent : ApiProvider)) 934 { 935 assert(instantiation !is null); 936 937 ReflectionInfo* reflection = new ReflectionInfo; 938 reflection.name = PM.stringof; 939 940 static if(is(PM: ApiObject)) { 941 reflection.needsInstantiation = true; 942 reflection.instantiate = delegate WebDotDBaseType(string i) { 943 auto n = new PM(instantiation, i); 944 return n; 945 }; 946 } else { 947 reflection.instantiation = instantiation; 948 949 static if(!is(PM : BuiltInFunctions)) { 950 auto builtins = new BuiltInFunctions(instantiation, reflection); 951 instantiation.builtInFunctions = builtins; 952 foreach(k, v; builtins.reflection.functions) 953 reflection.functions["builtin." ~ k] = v; 954 } 955 } 956 957 static if(is(PM : ApiProvider)) {{ // double because I want a new scope 958 auto f = new FunctionInfo; 959 f.parentObject = reflection; 960 f.dispatcher = generateWrapper!(PM, "_defaultPage", PM._defaultPage)(reflection, instantiation); 961 f.returnTypeIsDocument = true; 962 reflection.functions["/"] = cast(immutable) f; 963 964 /+ 965 // catchAll here too 966 f = new FunctionInfo; 967 f.parentObject = reflection; 968 f.dispatcher = generateWrapper!(PM, "_catchAll", PM._catchAll)(reflection, instantiation); 969 f.returnTypeIsDocument = true; 970 reflection.functions["/_catchAll"] = cast(immutable) f; 971 +/ 972 }} 973 974 // derivedMembers is changed from allMembers 975 976 // 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 977 foreach(Class; TypeTuple!(PM, BaseClassesTuple!(PM))) 978 static if((is(Class : ApiProvider) && !is(Class == ApiProvider)) || is(Class : ApiObject)) 979 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. 980 static if(member[0] != '_') { 981 // FIXME: the filthiest of all hacks... 982 static if(!__traits(compiles, 983 !is(typeof(__traits(getMember, Class, member)) == function) && 984 isEnum!(__traits(getMember, Class, member)))) 985 continue; // must be a data member or something... 986 else 987 // DONE WITH FILTHIEST OF ALL HACKS 988 989 //if(member.length == 0) 990 // continue; 991 static if( 992 !is(typeof(__traits(getMember, Class, member)) == function) && 993 isEnum!(__traits(getMember, Class, member)) 994 ) { 995 EnumInfo i; 996 i.name = member; 997 foreach(m; __traits(allMembers, __traits(getMember, Class, member))) { 998 i.names ~= m; 999 i.values ~= cast(int) __traits(getMember, __traits(getMember, Class, member), m); 1000 } 1001 1002 reflection.enums[member] = i; 1003 1004 } else static if( 1005 !is(typeof(__traits(getMember, Class, member)) == function) && 1006 isStruct!(__traits(getMember, Class, member)) 1007 ) { 1008 StructInfo i; 1009 i.name = member; 1010 1011 typeof(Passthrough!(__traits(getMember, Class, member))) s; 1012 foreach(idx, m; s.tupleof) { 1013 StructMemberInfo mem; 1014 1015 mem.name = s.tupleof[idx].stringof[2..$]; 1016 mem.staticType = typeof(m).stringof; 1017 1018 mem.defaultValue = null; // FIXME 1019 1020 i.members ~= mem; 1021 } 1022 1023 reflection.structs[member] = i; 1024 } else static if( 1025 is(typeof(__traits(getMember, Class, member)) == function) 1026 && __traits(getProtection, __traits(getMember, Class, member)) == "export" 1027 && 1028 ( 1029 member.length < 5 || 1030 ( 1031 member[$ - 5 .. $] != "_Page" && 1032 member[$ - 5 .. $] != "_Form") && 1033 !(member.length > 16 && member[$ - 16 .. $] == "_PermissionCheck") 1034 )) { 1035 FunctionInfo* f = new FunctionInfo; 1036 ParameterTypeTuple!(__traits(getMember, Class, member)) fargs; 1037 1038 f.requireHttps = hasAnnotation!(__traits(getMember, Class, member), RequireHttps); 1039 f.returnType = ReturnType!(__traits(getMember, Class, member)).stringof; 1040 f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, Class, member)) : Document); 1041 f.returnTypeIsElement = is(ReturnType!(__traits(getMember, Class, member)) : Element); 1042 static if(hasValueAnnotation!(__traits(getMember, Class, member), GenericContainerType)) 1043 f.genericContainerType = getAnnotation!(__traits(getMember, Class, member), GenericContainerType).type; 1044 1045 f.parentObject = reflection; 1046 1047 f.name = toUrlName(member); 1048 f.originalName = member; 1049 1050 assert(instantiation !is null); 1051 f.dispatcher = generateWrapper!(Class, member, __traits(getMember, Class, member))(reflection, instantiation); 1052 1053 //f.uriPath = f.originalName; 1054 1055 auto namesAndDefaults = parameterInfoImpl!(__traits(getMember, Class, member)); 1056 auto names = namesAndDefaults[0]; 1057 auto defaults = namesAndDefaults[1]; 1058 assert(names.length == defaults.length); 1059 1060 foreach(idx, param; fargs) { 1061 if(idx >= names.length) 1062 assert(0, to!string(idx) ~ " " ~ to!string(names)); 1063 1064 Parameter p = reflectParam!(typeof(param))(); 1065 1066 p.name = names[idx]; 1067 auto d = defaults[idx]; 1068 p.defaultValue = d == "null" ? "" : d; 1069 p.hasDefault = d.length > 0; 1070 1071 f.parameters ~= p; 1072 } 1073 1074 static if(__traits(hasMember, Class, member ~ "_Form")) { 1075 f.createForm = &__traits(getMember, instantiation, member ~ "_Form"); 1076 } 1077 1078 reflection.functions[f.name] = cast(immutable) (f); 1079 // also offer the original name if it doesn't 1080 // conflict 1081 //if(f.originalName !in reflection.functions) 1082 reflection.functions[f.originalName] = cast(immutable) (f); 1083 } 1084 else static if( 1085 !is(typeof(__traits(getMember, Class, member)) == function) && 1086 isApiObject!(__traits(getMember, Class, member)) 1087 ) { 1088 reflection.objects[member] = prepareReflectionImpl!( 1089 __traits(getMember, Class, member), Parent) 1090 (instantiation); 1091 } else static if( // child ApiProviders are like child modules 1092 !is(typeof(__traits(getMember, Class, member)) == function) && 1093 isApiProvider!(__traits(getMember, Class, member)) 1094 ) { 1095 PassthroughType!(__traits(getMember, Class, member)) i; 1096 static if(__traits(compiles, i = new typeof(i)(instantiation))) 1097 i = new typeof(i)(instantiation); 1098 else 1099 i = new typeof(i)(); 1100 auto r = prepareReflectionImpl!(__traits(getMember, Class, member), typeof(i))(i); 1101 i.reflection = cast(immutable) r; 1102 reflection.objects[member] = r; 1103 if(toLower(member) !in reflection.objects) // web filenames are often lowercase too 1104 reflection.objects[member.toLower] = r; 1105 } 1106 } 1107 } 1108 1109 return cast(immutable) reflection; 1110 } 1111 1112 Parameter reflectParam(param)() { 1113 Parameter p; 1114 1115 p.staticType = param.stringof; 1116 1117 static if( __traits(compiles, p.makeFormElement = &(param.makeFormElement))) { 1118 p.makeFormElement = &(param.makeFormElement); 1119 } else static if( __traits(compiles, PM.makeFormElement!(param)(null, null))) { 1120 alias PM.makeFormElement!(param) LOL; 1121 p.makeFormElement = &LOL; 1122 } else static if( is( param == enum )) { 1123 p.type = "select"; 1124 1125 foreach(opt; __traits(allMembers, param)) { 1126 p.options ~= opt; 1127 p.optionValues ~= to!string(__traits(getMember, param, opt)); 1128 } 1129 } else static if (is(param == bool)) { 1130 p.type = "checkbox"; 1131 } else static if (is(Unqual!(param) == Cgi.UploadedFile)) { 1132 p.type = "file"; 1133 } else static if(is(Unqual!(param) == Text)) { 1134 p.type = "textarea"; 1135 } else { 1136 p.type = "text"; 1137 } 1138 1139 return p; 1140 } 1141 1142 struct CallInfo { 1143 string objectIdentifier; 1144 immutable(FunctionInfo)* func; 1145 void delegate(Document)[] postProcessors; 1146 } 1147 1148 class NonCanonicalUrlException : Exception { 1149 this(CanonicalUrlOption option, string properUrl = null) { 1150 this.howToFix = option; 1151 this.properUrl = properUrl; 1152 super("The given URL needs this fix: " ~ to!string(option) ~ " " ~ properUrl); 1153 } 1154 1155 CanonicalUrlOption howToFix; 1156 string properUrl; 1157 } 1158 1159 enum CanonicalUrlOption { 1160 cutTrailingSlash, 1161 addTrailingSlash 1162 } 1163 1164 1165 CallInfo parseUrl(in ReflectionInfo* reflection, string url, string defaultFunction, in bool hasTrailingSlash) { 1166 CallInfo info; 1167 1168 if(url.length && url[0] == '/') 1169 url = url[1 .. $]; 1170 1171 if(reflection.needsInstantiation) { 1172 // FIXME: support object identifiers that span more than one slash... maybe 1173 auto idx = url.indexOf("/"); 1174 if(idx != -1) { 1175 info.objectIdentifier = url[0 .. idx]; 1176 url = url[idx + 1 .. $]; 1177 } else { 1178 info.objectIdentifier = url; 1179 url = null; 1180 } 1181 } 1182 1183 string name; 1184 auto idx = url.indexOf("/"); 1185 if(idx != -1) { 1186 name = url[0 .. idx]; 1187 url = url[idx + 1 .. $]; 1188 } else { 1189 name = url; 1190 url = null; 1191 } 1192 1193 bool usingDefault = false; 1194 if(name.length == 0) { 1195 name = defaultFunction; 1196 usingDefault = true; 1197 if(name !in reflection.functions) 1198 name = "/"; // should call _defaultPage 1199 } 1200 1201 if(reflection.instantiation !is null) 1202 info.postProcessors ~= &((cast()(reflection.instantiation))._postProcess); 1203 1204 if(name in reflection.functions) { 1205 info.func = reflection.functions[name]; 1206 1207 // if we're using a default thing, we need as slash on the end so relative links work 1208 if(usingDefault) { 1209 if(!hasTrailingSlash) 1210 throw new NonCanonicalUrlException(CanonicalUrlOption.addTrailingSlash); 1211 } else { 1212 if(hasTrailingSlash) 1213 throw new NonCanonicalUrlException(CanonicalUrlOption.cutTrailingSlash); 1214 } 1215 } 1216 1217 if(name in reflection.objects) { 1218 info = parseUrl(reflection.objects[name], url, defaultFunction, hasTrailingSlash); 1219 } 1220 1221 return info; 1222 } 1223 1224 /// If you're not using FancyMain, this is the go-to function to do most the work. 1225 /// instantiation should be an object of your ApiProvider type. 1226 /// pathInfoStartingPoint is used to make a slice of it, incase you already consumed part of the path info before you called this. 1227 1228 void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint = 0, bool handleAllExceptions = true, Session session = null) if(is(Provider : ApiProvider)) { 1229 assert(instantiation !is null); 1230 1231 instantiation.cgi = cgi; 1232 1233 if(instantiation.reflection is null) { 1234 instantiation.reflection = prepareReflection!(Provider)(instantiation); 1235 instantiation._initialize(); 1236 // FIXME: what about initializing child objects? 1237 } 1238 1239 auto reflection = instantiation.reflection; 1240 instantiation._baseUrl = cgi.logicalScriptName ~ cgi.pathInfo[0 .. pathInfoStartingPoint]; 1241 1242 // everything assumes the url isn't empty... 1243 if(cgi.pathInfo.length < pathInfoStartingPoint + 1) { 1244 cgi.setResponseLocation(cgi.logicalScriptName ~ cgi.pathInfo ~ "/" ~ (cgi.queryString.length ? "?" ~ cgi.queryString : "")); 1245 return; 1246 } 1247 1248 // kinda a hack, but this kind of thing should be available anyway 1249 string funName = cgi.pathInfo[pathInfoStartingPoint + 1..$]; 1250 if(funName == "functions.js") { 1251 cgi.gzipResponse = true; 1252 cgi.setResponseContentType("text/javascript"); 1253 cgi.setCache(true); 1254 cgi.write(makeJavascriptApi(reflection, replace(cast(string) cgi.pathInfo, "functions.js", "")), true); 1255 cgi.close(); 1256 return; 1257 } 1258 if(funName == "styles.css") { 1259 cgi.gzipResponse = true; 1260 cgi.setResponseContentType("text/css"); 1261 cgi.setCache(true); 1262 cgi.write(instantiation.stylesheet(), true); 1263 cgi.close(); 1264 return; 1265 } 1266 1267 CallInfo info; 1268 1269 try 1270 info = parseUrl(reflection, cgi.pathInfo[pathInfoStartingPoint + 1 .. $], to!string(cgi.requestMethod), cgi.pathInfo[$-1] == '/'); 1271 catch(NonCanonicalUrlException e) { 1272 final switch(e.howToFix) { 1273 case CanonicalUrlOption.cutTrailingSlash: 1274 cgi.setResponseLocation(cgi.logicalScriptName ~ cgi.pathInfo[0 .. $ - 1] ~ 1275 (cgi.queryString.length ? ("?" ~ cgi.queryString) : "")); 1276 break; 1277 case CanonicalUrlOption.addTrailingSlash: 1278 cgi.setResponseLocation(cgi.logicalScriptName ~ cgi.pathInfo ~ "/" ~ 1279 (cgi.queryString.length ? ("?" ~ cgi.queryString) : "")); 1280 break; 1281 } 1282 1283 return; 1284 } 1285 1286 auto fun = info.func; 1287 auto instantiator = info.objectIdentifier; 1288 1289 Envelope result; 1290 result.userData = cgi.request("passedThroughUserData"); 1291 1292 auto envelopeFormat = cgi.request("envelopeFormat", "document"); 1293 1294 WebDotDBaseType base = instantiation; 1295 WebDotDBaseType realObject = instantiation; 1296 if(instantiator.length == 0) 1297 if(fun !is null && fun.parentObject !is null && fun.parentObject.instantiation !is null) 1298 realObject = cast() fun.parentObject.instantiation; // casting away transitive immutable... 1299 1300 // FIXME 1301 if(cgi.pathInfo.indexOf("builtin.") != -1 && instantiation.builtInFunctions !is null) 1302 base = instantiation.builtInFunctions; 1303 1304 if(base !is realObject) { 1305 auto hack1 = cast(ApiProvider) base; 1306 auto hack2 = cast(ApiProvider) realObject; 1307 1308 if(hack1 !is null && hack2 !is null && hack2.session is null) 1309 hack2.session = hack1.session; 1310 } 1311 1312 bool returnedHoldsADocument = false; 1313 string[][string] want; 1314 string format, secondaryFormat; 1315 void delegate(Document d) moreProcessing; 1316 WrapperReturn ret; 1317 1318 try { 1319 if(fun is null) { 1320 auto d = instantiation._catchallEntry( 1321 cgi.pathInfo[pathInfoStartingPoint + 1..$], 1322 funName, 1323 ""); 1324 1325 result.success = true; 1326 1327 if(d !is null) { 1328 auto doc = cast(Document) d; 1329 if(doc) 1330 instantiation._postProcess(doc); 1331 1332 cgi.setResponseContentType(d.contentType()); 1333 cgi.write(d.getData(), true); 1334 } 1335 1336 // we did everything we need above... 1337 envelopeFormat = "no-processing"; 1338 goto do_nothing_else; 1339 } 1340 1341 assert(fun !is null); 1342 assert(fun.dispatcher !is null); 1343 assert(cgi !is null); 1344 1345 if(fun.requireHttps && !cgi.https) { 1346 cgi.setResponseLocation("https://" ~ cgi.host ~ cgi.logicalScriptName ~ cgi.pathInfo ~ 1347 (cgi.queryString.length ? "?" : "") ~ cgi.queryString); 1348 envelopeFormat = "no-processing"; 1349 goto do_nothing_else; 1350 } 1351 1352 if(instantiator.length) { 1353 assert(fun !is null); 1354 assert(fun.parentObject !is null); 1355 assert(fun.parentObject.instantiate !is null); 1356 realObject = fun.parentObject.instantiate(instantiator); 1357 } 1358 1359 1360 result.type = fun.returnType; 1361 1362 format = cgi.request("format", reflection.defaultOutputFormat); 1363 secondaryFormat = cgi.request("secondaryFormat", ""); 1364 if(secondaryFormat.length == 0) secondaryFormat = null; 1365 1366 { // scope so we can goto over this 1367 JSONValue res; 1368 1369 // FIXME: hackalicious garbage. kill. 1370 want = cast(string[][string]) (cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.postArray : cgi.getArray); 1371 version(fb_inside_hack) { 1372 if(cgi.referrer.indexOf("apps.facebook.com") != -1) { 1373 auto idx = cgi.referrer.indexOf("?"); 1374 if(idx != -1 && cgi.referrer[idx + 1 .. $] != cgi.queryString) { 1375 // so fucking broken 1376 cgi.setResponseLocation(cgi.logicalScriptName ~ cgi.pathInfo ~ cgi.referrer[idx .. $]); 1377 return; 1378 } 1379 } 1380 if(cgi.requestMethod == Cgi.RequestMethod.POST) { 1381 foreach(k, v; cgi.getArray) 1382 want[k] = cast(string[]) v; 1383 foreach(k, v; cgi.postArray) 1384 want[k] = cast(string[]) v; 1385 } 1386 } 1387 1388 realObject.cgi = cgi; 1389 ret = fun.dispatcher(cgi, realObject, want, format, secondaryFormat); 1390 if(ret.completed) { 1391 envelopeFormat = "no-processing"; 1392 goto do_nothing_else; 1393 } 1394 1395 res = ret.value; 1396 1397 //if(cgi) 1398 // cgi.setResponseContentType("application/json"); 1399 result.success = true; 1400 result.result = res; 1401 } 1402 1403 do_nothing_else: {} 1404 1405 } 1406 catch (Throwable e) { 1407 result.success = false; 1408 result.errorMessage = e.msg; 1409 result.type = e.classinfo.name; 1410 debug result.dFullString = e.toString(); 1411 1412 realObject.exceptionExaminer(e); 1413 1414 if(envelopeFormat == "document" || envelopeFormat == "html") { 1415 if(auto fve = cast(FormValidationException) e) { 1416 auto thing = fve.formFunction; 1417 if(thing is null) 1418 thing = fun; 1419 fun = thing; 1420 ret = fun.dispatcher(cgi, realObject, want, format, secondaryFormat); 1421 result.result = ret.value; 1422 1423 if(fun.returnTypeIsDocument) 1424 returnedHoldsADocument = true; // we don't replace the success flag, so this ensures no double document 1425 1426 moreProcessing = (Document d) { 1427 Form f; 1428 if(fve.getForm !is null) 1429 f = fve.getForm(d); 1430 else 1431 f = d.requireSelector!Form("form"); 1432 1433 foreach(k, v; want) 1434 f.setValue(k, v[$-1]); 1435 1436 foreach(idx, failure; fve.failed) { 1437 auto ele = f.requireSelector("[name=\""~failure~"\"]"); 1438 ele.addClass("validation-failed"); 1439 ele.dataset.validationMessage = fve.messagesForUser[idx]; 1440 ele.parentNode.addChild("span", fve.messagesForUser[idx]).addClass("validation-message"); 1441 } 1442 1443 if(fve.postProcessor !is null) 1444 fve.postProcessor(d, f, fve); 1445 }; 1446 } else if(auto ipe = cast(InsufficientParametersException) e) { 1447 assert(fun !is null); 1448 Form form; 1449 if(fun.createForm !is null) { 1450 // go ahead and use it to make the form page 1451 auto doc = fun.createForm(cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.post : cgi.get); 1452 1453 form = doc.requireSelector!Form("form.created-by-create-form, form.automatic-form, form"); 1454 } else { 1455 Parameter[] params = (cast(Parameter[])fun.parameters).dup; 1456 foreach(i, p; fun.parameters) { 1457 string value = ""; 1458 if(p.name in cgi.get) 1459 value = cgi.get[p.name]; 1460 if(p.name in cgi.post) 1461 value = cgi.post[p.name]; 1462 params[i].value = value; 1463 } 1464 1465 form = createAutomaticForm(new Document("<html></html>", true, true), fun);// params, beautify(fun.originalName)); 1466 foreach(k, v; cgi.get) 1467 form.setValue(k, v); 1468 1469 instantiation.addCsrfTokens(form); 1470 form.setValue("envelopeFormat", envelopeFormat); 1471 1472 auto n = form.getElementById("function-name"); 1473 if(n) 1474 n.innerText = beautify(fun.originalName); 1475 1476 // FIXME: I like having something, but it needs to not 1477 // show it on the first user load. 1478 // form.prependChild(Element.make("p", ipe.msg)); 1479 } 1480 1481 assert(form !is null); 1482 1483 foreach(k, v; cgi.get) 1484 form.setValue(k, v); // carry what we have for params over 1485 foreach(k, v; cgi.post) 1486 form.setValue(k, v); // carry what we have for params over 1487 1488 result.result.str = form.toString(); 1489 } else { 1490 auto fourOhFour = cast(NoSuchPageException) e; 1491 if(fourOhFour !is null) 1492 cgi.setResponseStatus("404 File Not Found"); 1493 1494 if(instantiation._errorFunction !is null) { 1495 auto document = instantiation._errorFunction(e); 1496 if(document is null) 1497 goto gotnull; 1498 result.result.str = (document.toString()); 1499 returnedHoldsADocument = true; 1500 } else { 1501 gotnull: 1502 if(!handleAllExceptions) { 1503 envelopeFormat = "internal"; 1504 throw e; // pass it up the chain 1505 } 1506 auto code = Element.make("div"); 1507 code.addClass("exception-error-message"); 1508 import arsd.characterencodings; 1509 code.addChild("p", convertToUtf8Lossy(cast(immutable(ubyte)[]) e.msg, "utf8")); 1510 debug code.addChild("pre", convertToUtf8Lossy(cast(immutable(ubyte)[]) e.toString(), "utf8")); 1511 1512 result.result.str = (code.toString()); 1513 } 1514 } 1515 } 1516 } finally { 1517 // the function must have done its own thing; we need to quit or else it will trigger an assert down here 1518 version(webd_cookie_sessions) { 1519 if(cgi.canOutputHeaders() && session !is null) 1520 session.commit(); 1521 } 1522 if(!cgi.isClosed()) 1523 switch(envelopeFormat) { 1524 case "no-processing": 1525 case "internal": 1526 break; 1527 case "redirect": 1528 auto redirect = cgi.request("_arsd_redirect_location", cgi.referrer); 1529 1530 // FIXME: is this safe? it'd make XSS super easy 1531 // add result to url 1532 1533 if(!result.success) 1534 goto case "none"; 1535 1536 cgi.setResponseLocation(redirect, false); 1537 break; 1538 case "json": 1539 case "json_enable_redirects": 1540 // this makes firefox ugly 1541 //cgi.setResponseContentType("application/json"); 1542 auto json = toJsonValue(result); 1543 cgi.write(toJSON(json), true); 1544 break; 1545 case "script": 1546 case "jsonp": 1547 bool securityPass = false; 1548 version(web_d_unrestricted_jsonp) { 1549 // unrestricted is opt-in because i worry about fetching user info from across sites 1550 securityPass = true; 1551 } else { 1552 // we check this on both get and post to ensure they can't fetch user private data cross domain. 1553 auto hack1 = cast(ApiProvider) base; 1554 if(hack1) 1555 securityPass = hack1.isCsrfTokenCorrect(); 1556 } 1557 1558 if(securityPass) { 1559 if(envelopeFormat == "script") 1560 cgi.setResponseContentType("text/html"); 1561 else 1562 cgi.setResponseContentType("application/javascript"); 1563 1564 auto json = cgi.request("jsonp", "throw new Error") ~ "(" ~ toJson(result) ~ ");"; 1565 1566 if(envelopeFormat == "script") 1567 json = "<script type=\"text/javascript\">" ~ json ~ "</script>"; 1568 cgi.write(json, true); 1569 } else { 1570 // if the security check fails, you just don't get anything at all data wise... 1571 cgi.setResponseStatus("403 Forbidden"); 1572 } 1573 break; 1574 case "csv": 1575 cgi.setResponseContentType("text/csv"); 1576 cgi.header("Content-Disposition: attachment; filename=\"export.csv\""); 1577 1578 if(result.result.type == JSONType..string) { 1579 cgi.write(result.result.str, true); 1580 } else assert(0); 1581 break; 1582 case "download": 1583 cgi.header("Content-Disposition: attachment; filename=\"data.csv\""); 1584 goto case; 1585 case "none": 1586 cgi.setResponseContentType("text/plain"); 1587 1588 if(result.success) { 1589 if(result.result.type == JSONType..string) { 1590 cgi.write(result.result.str, true); 1591 } else { 1592 cgi.write(toJSON(result.result), true); 1593 } 1594 } else { 1595 cgi.write(result.errorMessage, true); 1596 } 1597 break; 1598 case "document": 1599 case "html": 1600 default: 1601 cgi.setResponseContentType("text/html; charset=utf-8"); 1602 1603 if(result.result.type == JSONType..string) { 1604 auto returned = result.result.str; 1605 1606 if(envelopeFormat != "html") { 1607 Document document; 1608 // this big condition means the returned holds a document too 1609 if(returnedHoldsADocument || (result.success && fun !is null && fun.returnTypeIsDocument && returned.length)) { 1610 // probably not super efficient... 1611 document = new TemplatedDocument(returned); 1612 } else { 1613 // auto e = instantiation._getGenericContainer(); 1614 Element e; 1615 auto hack = cast(ApiProvider) realObject; 1616 if(hack !is null) 1617 e = hack._getGenericContainer(fun is null ? "default" : fun.genericContainerType); 1618 else 1619 e = instantiation._getGenericContainer(fun is null ? "default" : fun.genericContainerType); 1620 1621 1622 document = e.parentDocument; 1623 //assert(0, document.toString()); 1624 // FIXME: a wee bit slow, esp if func return element 1625 e.innerHTML = returned; 1626 if(fun !is null) 1627 e.setAttribute("data-from-function", fun.originalName); 1628 } 1629 1630 if(document !is null) { 1631 if(envelopeFormat == "document") { 1632 // forming a nice chain here... 1633 // FIXME: this isn't actually a nice chain! 1634 bool[void delegate(Document)] run; 1635 1636 auto postProcessors = info.postProcessors; 1637 if(base !is instantiation) 1638 postProcessors ~= &(instantiation._postProcess); 1639 if(realObject !is null) 1640 postProcessors ~= &(realObject._postProcess); 1641 postProcessors ~= &(base._postProcess); 1642 1643 // FIXME: cgi is sometimes null in te post processor... wtf 1644 foreach(pp; postProcessors) { 1645 if(pp in run) 1646 continue; 1647 run[pp] = true; 1648 pp(document); 1649 } 1650 } 1651 1652 if(moreProcessing !is null) 1653 moreProcessing(document); 1654 1655 returned = document.toString; 1656 } 1657 } 1658 1659 cgi.write(returned, true); 1660 } else 1661 cgi.write(htmlEntitiesEncode(toJSON(result.result)), true); 1662 break; 1663 } 1664 1665 if(envelopeFormat != "internal") 1666 cgi.close(); 1667 } 1668 } 1669 1670 class BuiltInFunctions : ApiProvider { 1671 const(ReflectionInfo)* workingFor; 1672 ApiProvider basedOn; 1673 this(ApiProvider basedOn, in ReflectionInfo* other) { 1674 this.basedOn = basedOn; 1675 workingFor = other; 1676 if(this.reflection is null) 1677 this.reflection = prepareReflection!(BuiltInFunctions)(this); 1678 1679 assert(this.reflection !is null); 1680 } 1681 1682 Form getAutomaticForm(string method) { 1683 if(method !in workingFor.functions) 1684 throw new Exception("no such method " ~ method); 1685 auto f = workingFor.functions[method]; 1686 1687 Form form; 1688 if(f.createForm !is null) { 1689 form = f.createForm(null).requireSelector!Form("form"); 1690 } else 1691 form = createAutomaticForm(new Document("<html></html>", true, true), f); 1692 auto idx = basedOn.cgi.requestUri.indexOf("builtin.getAutomaticForm"); 1693 if(idx == -1) 1694 idx = basedOn.cgi.requestUri.indexOf("builtin.get-automatic-form"); 1695 assert(idx != -1); 1696 form.action = basedOn.cgi.requestUri[0 .. idx] ~ form.action; // make sure it works across the site 1697 1698 return form; 1699 } 1700 } 1701 1702 // what about some built in functions? 1703 /+ 1704 // Built-ins 1705 // Basic integer operations 1706 builtin.opAdd 1707 builtin.opSub 1708 builtin.opMul 1709 builtin.opDiv 1710 1711 // Basic array operations 1712 builtin.opConcat // use to combine calls easily 1713 builtin.opIndex 1714 builtin.opSlice 1715 builtin.length 1716 1717 // Basic floating point operations 1718 builtin.round 1719 builtin.floor 1720 builtin.ceil 1721 1722 // Basic object operations 1723 builtin.getMember 1724 1725 // Basic functional operations 1726 builtin.filter // use to slice down on stuff to transfer 1727 builtin.map // call a server function on a whole array 1728 builtin.reduce 1729 1730 // Access to the html items 1731 builtin.getAutomaticForm(method) 1732 +/ 1733 1734 1735 /// fancier wrapper to cgi.d's GenericMain - does most the work for you, so you can just write your class and be done with it 1736 /// Note it creates a session for you too, and will write to the disk - a csrf token. Compile with -version=no_automatic_session 1737 /// to disable this. 1738 mixin template FancyMain(T, Args...) { 1739 mixin CustomCgiFancyMain!(Cgi, T, Args); 1740 } 1741 1742 /// Like FancyMain, but you can pass a custom subclass of Cgi 1743 mixin template CustomCgiFancyMain(CustomCgi, T, Args...) if(is(CustomCgi : Cgi)) { 1744 void fancyMainFunction(Cgi cgi) { //string[] args) { 1745 version(catch_segfault) { 1746 import etc.linux.memoryerror; 1747 // NOTE: this is private on stock dmd right now, just 1748 // open the file (src/druntime/import/etc/linux/memoryerror.d) and make it public 1749 registerMemoryErrorHandler(); 1750 } 1751 1752 // auto cgi = new Cgi; 1753 1754 // there must be a trailing slash for relative links.. 1755 if(cgi.pathInfo.length == 0) { 1756 cgi.setResponseLocation(cgi.requestUri ~ "/"); 1757 cgi.close(); 1758 return; 1759 } 1760 1761 // FIXME: won't work for multiple objects 1762 T instantiation = new T(); 1763 instantiation.cgi = cgi; 1764 auto reflection = prepareReflection!(T)(instantiation); 1765 1766 version(no_automatic_session) {} 1767 else { 1768 auto session = new Session(cgi); 1769 version(webd_cookie_sessions) { } // cookies have to be outputted before here since they are headers 1770 else { 1771 scope(exit) { 1772 // I only commit automatically on non-bots to avoid writing too many files 1773 // looking for bot should catch most them without false positives... 1774 // empty user agent is prolly a tester too so i'll let that slide 1775 if(cgi.userAgent.length && cgi.userAgent.toLower.indexOf("bot") == -1) 1776 session.commit(); 1777 } 1778 } 1779 instantiation.session = session; 1780 } 1781 1782 version(webd_cookie_sessions) 1783 run(cgi, instantiation, instantiation.pathInfoStartingPoint, true, session); 1784 else 1785 run(cgi, instantiation, instantiation.pathInfoStartingPoint); 1786 1787 /+ 1788 if(args.length > 1) { 1789 string[string][] namedArgs; 1790 foreach(arg; args[2..$]) { 1791 auto lol = arg.indexOf("="); 1792 if(lol == -1) 1793 throw new Exception("use named args for all params"); 1794 //namedArgs[arg[0..lol]] = arg[lol+1..$]; // FIXME 1795 } 1796 1797 if(!(args[1] in reflection.functions)) { 1798 throw new Exception("No such function"); 1799 } 1800 1801 //writefln("%s", reflection.functions[args[1]].dispatcher(null, namedArgs, "string")); 1802 } else { 1803 +/ 1804 // } 1805 } 1806 1807 mixin CustomCgiMain!(CustomCgi, fancyMainFunction, Args); 1808 } 1809 1810 /// Given a function from reflection, build a form to ask for it's params 1811 Form createAutomaticForm(Document document, in FunctionInfo* func, string[string] fieldTypes = null) { 1812 return createAutomaticForm(document, func.name, func.parameters, beautify(func.originalName), "POST", fieldTypes); 1813 } 1814 1815 /// ditto 1816 // FIXME: should there be something to prevent the pre-filled options from url? It's convenient but 1817 // someone might use it to trick people into submitting badness too. I'm leaning toward meh. 1818 Form createAutomaticForm(Document document, string action, in Parameter[] parameters, string submitText = "Submit", string method = "POST", string[string] fieldTypes = null) { 1819 auto form = cast(Form) Element.make("form"); 1820 form.parentDocument = document; 1821 form.addClass("automatic-form"); 1822 1823 form.action = action; 1824 1825 assert(form !is null); 1826 form.method = method; 1827 1828 1829 auto fieldset = form.addChild("fieldset"); 1830 auto legend = fieldset.addChild("legend", submitText); 1831 1832 auto table = cast(Table) fieldset.addChild("table"); 1833 assert(table !is null); 1834 1835 table.addChild("tbody"); 1836 1837 static int count = 0; 1838 1839 foreach(param; parameters) { 1840 Element input; 1841 string type; 1842 1843 if(param.makeFormElement !is null) { 1844 input = param.makeFormElement(document, param.name); 1845 goto gotelement; 1846 } 1847 1848 type = param.type; 1849 if(param.name in fieldTypes) 1850 type = fieldTypes[param.name]; 1851 1852 if(type == "select") { 1853 input = Element.make("select"); 1854 1855 foreach(idx, opt; param.options) { 1856 auto option = Element.make("option"); 1857 option.name = opt; 1858 option.value = param.optionValues[idx]; 1859 1860 option.innerText = beautify(opt); 1861 1862 if(option.value == param.value) 1863 option.selected = "selected"; 1864 1865 input.appendChild(option); 1866 } 1867 1868 input.name = param.name; 1869 } else if (type == "radio") { 1870 assert(0, "FIXME"); 1871 } else { 1872 if(type.startsWith("textarea")) { 1873 input = Element.make("textarea"); 1874 input.name = param.name; 1875 input.innerText = param.value; 1876 1877 input.attrs.rows = "7"; 1878 1879 auto idx = type.indexOf("-"); 1880 if(idx != -1) { 1881 idx++; 1882 input.attrs.rows = type[idx .. $]; 1883 } 1884 } else { 1885 input = Element.make("input"); 1886 1887 // hack to support common naming convention 1888 if(type == "text" && param.name.toLower.indexOf("password") != -1) 1889 input.type = "password"; 1890 else 1891 input.type = type; 1892 input.name = param.name; 1893 input.value = param.value; 1894 1895 if(type == "file") { 1896 form.method = "POST"; 1897 form.enctype = "multipart/form-data"; 1898 } 1899 } 1900 } 1901 1902 gotelement: 1903 1904 string n = param.name ~ "_auto-form-" ~ to!string(count); 1905 1906 input.id = n; 1907 1908 if(type == "hidden") { 1909 form.appendChild(input); 1910 } else { 1911 auto th = Element.make("th"); 1912 auto label = Element.make("label"); 1913 label.setAttribute("for", n); 1914 label.innerText = beautify(param.name) ~ ": "; 1915 th.appendChild(label); 1916 1917 table.appendRow(th, input); 1918 } 1919 1920 count++; 1921 } 1922 1923 auto fmt = Element.make("select"); 1924 fmt.name = "format"; 1925 fmt.addChild("option", "Automatic").setAttribute("value", "default"); 1926 fmt.addChild("option", "html").setAttribute("value", "html"); 1927 fmt.addChild("option", "table").setAttribute("value", "table"); 1928 fmt.addChild("option", "json").setAttribute("value", "json"); 1929 fmt.addChild("option", "string").setAttribute("value", "string"); 1930 auto th = table.th(""); 1931 th.addChild("label", "Format:"); 1932 1933 table.appendRow(th, fmt).className = "format-row"; 1934 1935 1936 auto submit = Element.make("input"); 1937 submit.value = submitText; 1938 submit.type = "submit"; 1939 1940 table.appendRow(Html(" "), submit); 1941 1942 // form.setValue("format", reflection.defaultOutputFormat); 1943 1944 return form; 1945 } 1946 1947 1948 /* * 1949 * Returns the parameter names of the given function 1950 * 1951 * Params: 1952 * func = the function alias to get the parameter names of 1953 * 1954 * Returns: an array of strings containing the parameter names 1955 */ 1956 /+ 1957 string parameterNamesOf( alias fn )( ) { 1958 string fullName = typeof(&fn).stringof; 1959 1960 int pos = fullName.lastIndexOf( ')' ); 1961 int end = pos; 1962 int count = 0; 1963 do { 1964 if ( fullName[pos] == ')' ) { 1965 count++; 1966 } else if ( fullName[pos] == '(' ) { 1967 count--; 1968 } 1969 pos--; 1970 } while ( count > 0 ); 1971 1972 return fullName[pos+2..end]; 1973 } 1974 +/ 1975 1976 1977 template parameterNamesOf (alias func) { 1978 const parameterNamesOf = parameterInfoImpl!(func)[0]; 1979 } 1980 1981 // FIXME: I lost about a second on compile time after adding support for defaults :-( 1982 template parameterDefaultsOf (alias func) { 1983 const parameterDefaultsOf = parameterInfoImpl!(func)[1]; 1984 } 1985 1986 bool parameterHasDefault(alias func)(int p) { 1987 auto a = parameterDefaultsOf!(func); 1988 if(a.length == 0) 1989 return false; 1990 return a[p].length > 0; 1991 } 1992 1993 template parameterDefaultOf (alias func, int paramNum) { 1994 alias parameterDefaultOf = ParameterDefaultValueTuple!func[paramNum]; 1995 //auto a = parameterDefaultsOf!(func); 1996 //return a[paramNum]; 1997 } 1998 1999 sizediff_t indexOfNew(string s, char a) { 2000 foreach(i, c; s) 2001 if(c == a) 2002 return i; 2003 return -1; 2004 } 2005 2006 sizediff_t lastIndexOfNew(string s, char a) { 2007 for(sizediff_t i = s.length; i > 0; i--) 2008 if(s[i - 1] == a) 2009 return i - 1; 2010 return -1; 2011 } 2012 2013 2014 // FIXME: a problem here is the compiler only keeps one stringof 2015 // for a particular type 2016 // 2017 // so if you have void a(string a, string b); and void b(string b, string c), 2018 // both a() and b() will show up as params == ["a", "b"]! 2019 // 2020 // 2021 private string[][2] parameterInfoImpl (alias func) () 2022 { 2023 string funcStr = typeof(func).stringof; // this might fix the fixme above... 2024 // it used to be typeof(&func).stringof 2025 2026 auto start = funcStr.indexOfNew('('); 2027 auto end = funcStr.lastIndexOfNew(')'); 2028 2029 assert(start != -1); 2030 assert(end != -1); 2031 2032 const firstPattern = ' '; 2033 const secondPattern = ','; 2034 2035 funcStr = funcStr[start + 1 .. end]; 2036 2037 if (funcStr == "") // no parameters 2038 return [null, null]; 2039 2040 funcStr ~= secondPattern; 2041 2042 string token; 2043 string[] arr; 2044 2045 foreach (c ; funcStr) 2046 { 2047 if (c != firstPattern && c != secondPattern) 2048 token ~= c; 2049 2050 else 2051 { 2052 if (token) 2053 arr ~= token; 2054 2055 token = null; 2056 } 2057 } 2058 2059 if (arr.length == 1) 2060 return [arr, [""]]; 2061 2062 string[] result; 2063 string[] defaults; 2064 bool skip = false; 2065 2066 bool gettingDefault = false; 2067 2068 string currentName = ""; 2069 string currentDefault = ""; 2070 2071 foreach (str ; arr) 2072 { 2073 if(str == "=") { 2074 gettingDefault = true; 2075 continue; 2076 } 2077 2078 if(gettingDefault) { 2079 assert(str.length); 2080 currentDefault = str; 2081 gettingDefault = false; 2082 continue; 2083 } 2084 2085 skip = !skip; 2086 2087 if (skip) { 2088 if(currentName.length) { 2089 result ~= currentName; 2090 defaults ~= currentDefault; 2091 currentName = null; 2092 } 2093 continue; 2094 } 2095 2096 currentName = str; 2097 } 2098 2099 if(currentName !is null) { 2100 result ~= currentName; 2101 defaults ~= currentDefault; 2102 } 2103 2104 assert(result.length == defaults.length); 2105 2106 return [result, defaults]; 2107 } 2108 ///////////////////////////////// 2109 2110 /// Formats any given type as HTML. In custom types, you can write Element makeHtmlElement(Document document = null); to provide 2111 /// custom html. (the default arg is important - it won't necessarily pass a Document in at all, and since it's silently duck typed, 2112 /// not having that means your function won't be called and you can be left wondering WTF is going on.) 2113 2114 /// Alternatively, static Element makeHtmlArray(T[]) if you want to make a whole list of them. By default, it will just concat a bunch of individual 2115 /// elements though. 2116 string toHtml(T)(T a) { 2117 string ret; 2118 2119 static if(is(T == typeof(null))) 2120 ret = null; 2121 else static if(is(T : Document)) { 2122 if(a is null) 2123 ret = null; 2124 else 2125 ret = a.toString(); 2126 } else 2127 static if(isArray!(T) && !isSomeString!(T)) { 2128 static if(__traits(compiles, typeof(a[0]).makeHtmlArray(a))) { 2129 ret = to!string(typeof(a[0]).makeHtmlArray(a)); 2130 } else { 2131 ret ~= "<ul>"; 2132 foreach(v; a) 2133 ret ~= "<li>" ~ toHtml(v) ~ "</li>"; 2134 ret ~= "</ul>"; 2135 } 2136 } else static if(is(T : Element)) 2137 ret = a.toString(); 2138 else static if(__traits(compiles, a.makeHtmlElement().toString())) 2139 ret = a.makeHtmlElement().toString(); 2140 else static if(is(T == Html)) 2141 ret = a.source; 2142 else { 2143 auto str = to!string(a); 2144 if(str.indexOf("\t") == -1) 2145 ret = std.array.replace(htmlEntitiesEncode(str), "\n", "<br />\n"); 2146 else // if there's tabs in it, output it as code or something; the tabs are probably there for a reason. 2147 ret = "<pre>" ~ htmlEntitiesEncode(str) ~ "</pre>"; 2148 } 2149 2150 return ret; 2151 } 2152 2153 /// Translates a given type to a JSON string. 2154 2155 /// TIP: if you're building a Javascript function call by strings, toJson("your string"); will build a nicely escaped string for you of any type. 2156 string toJson(T)(T a) { 2157 auto v = toJsonValue(a); 2158 return toJSON(v); 2159 } 2160 2161 // FIXME: are the explicit instantiations of this necessary? 2162 /// like toHtml - it makes a json value of any given type. 2163 2164 /// It can be used generically, or it can be passed an ApiProvider so you can do a secondary custom 2165 /// format. (it calls api.formatAs!(type)(typeRequestString)). Why would you want that? Maybe 2166 /// your javascript wants to do work with a proper object,but wants to append it to the document too. 2167 /// Asking for json with secondary format = html means the server will provide both to you. 2168 2169 /// Implement JSONValue makeJsonValue() in your struct or class to provide 100% custom Json. 2170 2171 /// Elements from DOM are turned into JSON strings of the element's html. 2172 JSONValue toJsonValue(T, R = ApiProvider)(T a, string formatToStringAs = null, R api = null) 2173 if(is(R : ApiProvider)) 2174 { 2175 JSONValue val; 2176 static if(is(T == typeof(null)) || is(T == void*)) { 2177 /* void* goes here too because we don't know how to make it work... */ 2178 val.object = null; 2179 //val.type = JSONType.null_; 2180 } else static if(is(T == JSONValue)) { 2181 val = a; 2182 } else static if(__traits(compiles, val = a.makeJsonValue())) { 2183 val = a.makeJsonValue(); 2184 // FIXME: free function to emulate UFCS? 2185 2186 // FIXME: should we special case something like struct Html? 2187 } else static if(is(T : DateTime)) { 2188 //val.type = JSONType.string; 2189 val.str = a.toISOExtString(); 2190 } else static if(is(T : Element)) { 2191 if(a is null) { 2192 //val.type = JSONType.null_; 2193 val = null; 2194 } else { 2195 //val.type = JSONType.string; 2196 val.str = a.toString(); 2197 } 2198 } else static if(is(T == long)) { 2199 // FIXME: let's get a better range... I think it goes up to like 1 << 50 on positive and negative 2200 // but this works for now 2201 if(a < int.max && a > int.min) { 2202 //val.type = JSONType.integer; 2203 val.integer = to!long(a); 2204 } else { 2205 // WHY? because javascript can't actually store all 64 bit numbers correctly 2206 //val.type = JSONType.string; 2207 val.str = to!string(a); 2208 } 2209 } else static if(isIntegral!(T)) { 2210 //val.type = JSONType.integer; 2211 val.integer = to!long(a); 2212 } else static if(isFloatingPoint!(T)) { 2213 //val.type = JSONType.float_; 2214 val.floating = to!real(a); 2215 } else static if(isPointer!(T)) { 2216 if(a is null) { 2217 //val.type = JSONType.null_; 2218 val = null; 2219 } else { 2220 val = toJsonValue!(typeof(*a), R)(*a, formatToStringAs, api); 2221 } 2222 } else static if(is(T == bool)) { 2223 if(a == true) 2224 val = true; // .type = JSONType.true_; 2225 if(a == false) 2226 val = false; // .type = JSONType.false_; 2227 } else static if(isSomeString!(T)) { 2228 //val.type = JSONType.string; 2229 val.str = to!string(a); 2230 } else static if(isAssociativeArray!(T)) { 2231 //val.type = JSONType.object; 2232 JSONValue[string] valo; 2233 foreach(k, v; a) { 2234 valo[to!string(k)] = toJsonValue!(typeof(v), R)(v, formatToStringAs, api); 2235 } 2236 val = valo; 2237 } else static if(isArray!(T)) { 2238 //val.type = JSONType.array; 2239 JSONValue[] arr; 2240 arr.length = a.length; 2241 foreach(i, v; a) { 2242 arr[i] = toJsonValue!(typeof(v), R)(v, formatToStringAs, api); 2243 } 2244 2245 val.array = arr; 2246 } else static if(is(T == struct)) { // also can do all members of a struct... 2247 //val.type = JSONType.object; 2248 2249 JSONValue[string] valo; 2250 2251 foreach(i, member; a.tupleof) { 2252 string name = a.tupleof[i].stringof[2..$]; 2253 static if(a.tupleof[i].stringof[2] != '_') 2254 valo[name] = toJsonValue!(typeof(member), R)(member, formatToStringAs, api); 2255 } 2256 // HACK: bug in dmd can give debug members in a non-debug build 2257 //static if(__traits(compiles, __traits(getMember, a, member))) 2258 val = valo; 2259 } else { /* our catch all is to just do strings */ 2260 //val.type = JSONType.string; 2261 val.str = to!string(a); 2262 // FIXME: handle enums 2263 } 2264 2265 2266 // don't want json because it could recurse 2267 if(val.type == JSONType.object && formatToStringAs !is null && formatToStringAs != "json") { 2268 JSONValue formatted; 2269 //formatted.type = JSONType.string; 2270 formatted.str = ""; 2271 2272 formatAs!(T, R)(a, formatToStringAs, api, &formatted, null /* only doing one level of special formatting */); 2273 assert(formatted.type == JSONType..string); 2274 val.object["formattedSecondarily"] = formatted; 2275 } 2276 2277 return val; 2278 } 2279 2280 /+ 2281 Document toXml(T)(T t) { 2282 auto xml = new Document; 2283 xml.parse(emptyTag(T.stringof), true, true); 2284 xml.prolog = `<?xml version="1.0" encoding="UTF-8" ?>` ~ "\n"; 2285 2286 xml.root = toXmlElement(xml, t); 2287 return xml; 2288 } 2289 2290 Element toXmlElement(T)(Document document, T t) { 2291 Element val; 2292 static if(is(T == Document)) { 2293 val = t.root; 2294 //} else static if(__traits(compiles, a.makeJsonValue())) { 2295 // val = a.makeJsonValue(); 2296 } else static if(is(T : Element)) { 2297 if(t is null) { 2298 val = document.createElement("value"); 2299 val.innerText = "null"; 2300 val.setAttribute("isNull", "true"); 2301 } else 2302 val = t; 2303 } else static if(is(T == void*)) { 2304 val = document.createElement("value"); 2305 val.innerText = "null"; 2306 val.setAttribute("isNull", "true"); 2307 } else static if(isPointer!(T)) { 2308 if(t is null) { 2309 val = document.createElement("value"); 2310 val.innerText = "null"; 2311 val.setAttribute("isNull", "true"); 2312 } else { 2313 val = toXmlElement(document, *t); 2314 } 2315 } else static if(isAssociativeArray!(T)) { 2316 val = document.createElement("value"); 2317 foreach(k, v; t) { 2318 auto e = document.createElement(to!string(k)); 2319 e.appendChild(toXmlElement(document, v)); 2320 val.appendChild(e); 2321 } 2322 } else static if(isSomeString!(T)) { 2323 val = document.createTextNode(to!string(t)); 2324 } else static if(isArray!(T)) { 2325 val = document.createElement("array"); 2326 foreach(i, v; t) { 2327 auto e = document.createElement("item"); 2328 e.appendChild(toXmlElement(document, v)); 2329 val.appendChild(e); 2330 } 2331 } else static if(is(T == struct)) { // also can do all members of a struct... 2332 val = document.createElement(T.stringof); 2333 foreach(member; __traits(allMembers, T)) { 2334 if(member[0] == '_') continue; // FIXME: skip member functions 2335 auto e = document.createElement(member); 2336 e.appendChild(toXmlElement(document, __traits(getMember, t, member))); 2337 val.appendChild(e); 2338 } 2339 } else { /* our catch all is to just do strings */ 2340 val = document.createTextNode(to!string(t)); 2341 // FIXME: handle enums 2342 } 2343 2344 return val; 2345 } 2346 +/ 2347 2348 2349 /// throw this if your function needs something that is missing. 2350 2351 /// Done automatically by the wrapper function 2352 class InsufficientParametersException : Exception { 2353 this(string functionName, string msg, string file = __FILE__, size_t line = __LINE__) { 2354 this.functionName = functionName; 2355 super(functionName ~ ": " ~ msg, file, line); 2356 } 2357 2358 string functionName; 2359 string argumentName; 2360 string formLocation; 2361 } 2362 2363 /// helper for param checking 2364 bool isValidLookingEmailAddress(string e) { 2365 import std.net.isemail; 2366 return isEmail(e, CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid; 2367 } 2368 2369 /// Looks for things like <a or [url - the kind of stuff I often see in blatantly obvious comment spam 2370 bool isFreeOfTypicalPlainTextSpamLinks(string txt) { 2371 if(txt.indexOf("href=") != -1) 2372 return false; 2373 if(txt.indexOf("[url") != -1) 2374 return false; 2375 return true; 2376 } 2377 2378 /** 2379 --- 2380 auto checker = new ParamCheckHelper(); 2381 2382 checker.finish(); // this will throw if any of the checks failed 2383 // now go ahead and use the params 2384 --- 2385 */ 2386 class ParamCheckHelper { 2387 this(in Cgi cgi) { 2388 this.cgi = cgi; 2389 } 2390 2391 string[] failed; 2392 string[] messagesForUser; 2393 const(Cgi) cgi; 2394 2395 void failure(string name, string messageForUser = null) { 2396 failed ~= name; 2397 messagesForUser ~= messageForUser; 2398 } 2399 2400 string checkParam(in string[string] among, in string name, bool delegate(string) ok, string messageForUser = null) { 2401 return checkTypedParam!string(among, name, ok, messageForUser); 2402 } 2403 2404 T checkTypedParam(T)(in string[string] among, in string name, bool delegate(T) ok, string messageForUser = null) { 2405 T value; 2406 2407 bool isOk = false; 2408 string genericErrorMessage = "Please complete this field"; 2409 2410 2411 try { 2412 //auto ptr = "name" in among; 2413 //if(ptr !is null) { 2414 // value = *ptr; 2415 //} else { 2416 // D's in operator is sometimes buggy, so let's confirm this with a linear search ugh) 2417 // FIXME: fix D's AA 2418 foreach(k, v; among) 2419 if(k == name) { 2420 value = fromUrlParam!T(v, name, null); 2421 break; 2422 } 2423 //} 2424 2425 if(ok !is null) 2426 isOk = ok(value); 2427 else 2428 isOk = true; // no checker means if we were able to convert above, we're ok 2429 } catch(Exception e) { 2430 genericErrorMessage = e.msg; 2431 isOk = false; 2432 } 2433 2434 if(!isOk) { 2435 failure(name, messageForUser is null ? genericErrorMessage : messageForUser); 2436 } 2437 2438 return value; 2439 } 2440 2441 // int a = checkParam!int(cgi, "cool", (a) => a > 10); 2442 T checkCgiParam(T)(string name, T defaultValue, bool delegate(T) ok, string messageForUser = null) { 2443 auto value = cgi.request(name, defaultValue); 2444 if(!ok(value)) { 2445 failure(name, messageForUser); 2446 } 2447 2448 return value; 2449 } 2450 2451 void finish( 2452 immutable(FunctionInfo)* formFunction, 2453 Form delegate(Document) getForm, 2454 void delegate(Document, Form, FormValidationException) postProcessor, 2455 string file = __FILE__, size_t line = __LINE__) 2456 { 2457 if(failed.length) 2458 throw new FormValidationException( 2459 formFunction, getForm, postProcessor, 2460 failed, messagesForUser, 2461 to!string(failed), file, line); 2462 } 2463 } 2464 2465 auto check(alias field)(ParamCheckHelper helper, bool delegate(typeof(field)) ok, string messageForUser = null) { 2466 if(!ok(field)) { 2467 helper.failure(field.stringof, messageForUser); 2468 } 2469 2470 return field; 2471 } 2472 2473 bool isConvertableTo(T)(string v) { 2474 try { 2475 auto t = fromUrlParam!(T)(v, null, null); 2476 return true; 2477 } catch(Exception e) { 2478 return false; 2479 } 2480 } 2481 2482 class FormValidationException : Exception { 2483 this( 2484 immutable(FunctionInfo)* formFunction, 2485 Form delegate(Document) getForm, 2486 void delegate(Document, Form, FormValidationException) postProcessor, 2487 string[] failed, string[] messagesForUser, 2488 string msg, string file = __FILE__, size_t line = __LINE__) 2489 { 2490 this.formFunction = formFunction; 2491 this.getForm = getForm; 2492 this.postProcessor = postProcessor; 2493 this.failed = failed; 2494 this.messagesForUser = messagesForUser; 2495 2496 super(msg, file, line); 2497 } 2498 2499 // this will be called by the automatic catch 2500 // it goes: Document d = formatAs(formFunction, document); 2501 // then : Form f = getForm(d); 2502 // it will add the values used in the current call to the form with the error conditions 2503 // and finally, postProcessor(d, f, this); 2504 immutable(FunctionInfo)* formFunction; 2505 Form delegate(Document) getForm; 2506 void delegate(Document, Form, FormValidationException) postProcessor; 2507 string[] failed; 2508 string[] messagesForUser; 2509 } 2510 2511 /// throw this if a paramater is invalid. Automatic forms may present this to the user in a new form. (FIXME: implement that) 2512 class InvalidParameterException : Exception { 2513 this(string param, string value, string expected, string file = __FILE__, size_t line = __LINE__) { 2514 this.param = param; 2515 super("bad param: " ~ param ~ ". got: " ~ value ~ ". Expected: " ~expected, file, line); 2516 } 2517 2518 /* 2519 The way these are handled automatically is if something fails, web.d will 2520 redirect the user to 2521 2522 formLocation ~ "?" ~ encodeVariables(cgi.get|postArray) 2523 */ 2524 2525 string functionName; 2526 string param; 2527 string formLocation; 2528 } 2529 2530 /// convenience for throwing InvalidParameterExceptions 2531 void badParameter(alias T)(string expected = "") { 2532 throw new InvalidParameterException(T.stringof, T, expected); 2533 } 2534 2535 /// throw this if the user's access is denied 2536 class PermissionDeniedException : Exception { 2537 this(string msg, string file = __FILE__, int line = __LINE__) { 2538 super(msg, file, line); 2539 } 2540 } 2541 2542 /// throw if the request path is not found. Done automatically by the default catch all handler. 2543 class NoSuchPageException : Exception { 2544 this(string msg, string file = __FILE__, int line = __LINE__) { 2545 super(msg, file, line); 2546 } 2547 } 2548 2549 class NoSuchFunctionException : NoSuchPageException { 2550 this(string msg, string file = __FILE__, int line = __LINE__) { 2551 super(msg, file, line); 2552 } 2553 } 2554 2555 type fromUrlParam(type)(in string ofInterest, in string name, in string[][string] all) { 2556 type ret; 2557 2558 static if(!is(type == enum) && isArray!(type) && !isSomeString!(type)) { 2559 // how do we get an array out of a simple string? 2560 // FIXME 2561 static assert(0); 2562 } else static if(__traits(compiles, ret = type.fromWebString(ofInterest))) { // for custom object handling... 2563 ret = type.fromWebString(ofInterest); 2564 } else static if(is(type : Element)) { 2565 auto doc = new Document(ofInterest, true, true); 2566 2567 ret = doc.root; 2568 } else static if(is(type : Text)) { 2569 ret = ofInterest; 2570 } else static if(is(type : Html)) { 2571 ret.source = ofInterest; 2572 } else static if(is(type : TimeOfDay)) { 2573 ret = TimeOfDay.fromISOExtString(ofInterest); 2574 } else static if(is(type : Date)) { 2575 ret = Date.fromISOExtString(ofInterest); 2576 } else static if(is(type : DateTime)) { 2577 ret = DateTime.fromISOExtString(ofInterest); 2578 } else static if(is(type == struct)) { 2579 auto n = name.length ? (name ~ ".") : ""; 2580 foreach(idx, thing; ret.tupleof) { 2581 enum fn = ret.tupleof[idx].stringof[4..$]; 2582 auto lol = n ~ fn; 2583 if(lol in all) 2584 ret.tupleof[idx] = fromUrlParam!(typeof(thing))(all[lol], lol, all); 2585 } 2586 } else static if(is(type == enum)) { 2587 sw: switch(ofInterest) { 2588 /*static*/ foreach(N; __traits(allMembers, type)) { 2589 case N: 2590 ret = __traits(getMember, type, N); 2591 break sw; 2592 } 2593 default: 2594 throw new InvalidParameterException(name, ofInterest, ""); 2595 } 2596 2597 } 2598 /* 2599 else static if(is(type : struct)) { 2600 static assert(0, "struct not supported yet"); 2601 } 2602 */ 2603 else { 2604 ret = to!type(ofInterest); 2605 } // FIXME: can we support classes? 2606 2607 return ret; 2608 } 2609 2610 /// turns a string array from the URL into a proper D type 2611 type fromUrlParam(type)(in string[] ofInterest, in string name, in string[][string] all) { 2612 type ret; 2613 2614 // Arrays in a query string are sent as the name repeating... 2615 static if(!is(type == enum) && isArray!(type) && !isSomeString!type) { 2616 foreach(a; ofInterest) { 2617 ret ~= fromUrlParam!(ElementType!(type))(a, name, all); 2618 } 2619 } else static if(isArray!(type) && isSomeString!(ElementType!type)) { 2620 foreach(a; ofInterest) { 2621 ret ~= fromUrlParam!(ElementType!(type))(a, name, all); 2622 } 2623 } else 2624 ret = fromUrlParam!type(ofInterest[$-1], name, all); 2625 2626 return ret; 2627 } 2628 2629 auto getMemberDelegate(alias ObjectType, string member)(ObjectType object) if(is(ObjectType : WebDotDBaseType)) { 2630 if(object is null) 2631 throw new NoSuchFunctionException("no such object " ~ ObjectType.stringof); 2632 return &__traits(getMember, object, member); 2633 } 2634 2635 /// generates the massive wrapper function for each of your class' methods. 2636 /// it is responsible for turning strings to params and return values back to strings. 2637 WrapperFunction generateWrapper(alias ObjectType, string funName, alias f, R)(ReflectionInfo* reflection, R api) 2638 if(is(R: ApiProvider) && (is(ObjectType : WebDotDBaseType)) ) 2639 { 2640 WrapperReturn wrapper(Cgi cgi, WebDotDBaseType object, in string[][string] sargs, in string format, in string secondaryFormat = null) { 2641 2642 JSONValue returnValue; 2643 returnValue.str = ""; 2644 //returnValue.type = JSONType.string; 2645 2646 auto instantiation = getMemberDelegate!(ObjectType, funName)(cast(ObjectType) object); 2647 2648 api._initializePerCallInternal(); 2649 2650 ParameterTypeTuple!(f) args; 2651 2652 // Actually calling the function 2653 // FIXME: default parameters 2654 foreach(i, type; ParameterTypeTuple!(f)) { 2655 string name = parameterNamesOf!(f)[i]; 2656 2657 // We want to check the named argument first. If it's not there, 2658 // try the positional arguments 2659 string using = name; 2660 if(name !in sargs) 2661 using = "positional-arg-" ~ to!string(i); 2662 2663 // FIXME: if it's a struct, we should do it's pieces independently here 2664 2665 static if(is(type == bool)) { 2666 // bool is special cased because HTML checkboxes don't send anything if it isn't checked 2667 if(using in sargs) { 2668 if( 2669 sargs[using][$-1] != "false" && 2670 sargs[using][$-1] != "False" && 2671 sargs[using][$-1] != "FALSE" && 2672 sargs[using][$-1] != "no" && 2673 sargs[using][$-1] != "off" && 2674 sargs[using][$-1] != "0" 2675 ) 2676 args[i] = true; 2677 else 2678 args[i] = false; 2679 } 2680 else { 2681 static if(parameterHasDefault!(f)(i)) { 2682 // args[i] = mixin(parameterDefaultOf!(f)(i)); 2683 args[i] = cast(type) parameterDefaultOf!(f, i); 2684 } else 2685 args[i] = false; 2686 } 2687 2688 // FIXME: what if the default is true? 2689 } else static if(is(Unqual!(type) == Cgi.UploadedFile)) { 2690 if(using !in cgi.files) 2691 throw new InsufficientParametersException(funName, "file " ~ using ~ " is not present"); 2692 args[i] = cast() cgi.files[using]; // casting away const for the assignment to compile FIXME: shouldn't be needed 2693 } else { 2694 if(using !in sargs) { 2695 static if(isArray!(type) && !isSomeString!(type)) { 2696 args[i] = null; 2697 } else static if(parameterHasDefault!(f)(i)) { 2698 //args[i] = mixin(parameterDefaultOf!(f)(i)); 2699 args[i] = cast(type) parameterDefaultOf!(f, i); 2700 } else static if(is(type == struct)) { 2701 // try to load a struct as obj.members 2702 args[i] = fromUrlParam!type(cast(string) null, name, sargs); 2703 } else { 2704 throw new InsufficientParametersException(funName, "arg " ~ name ~ " is not present"); 2705 } 2706 } else { 2707 2708 // We now check the type reported by the client, if there is one 2709 // Right now, only one type is supported: ServerResult, which means 2710 // it's actually a nested function call 2711 2712 string[] ofInterest = cast(string[]) sargs[using]; // I'm changing the reference, but not the underlying stuff, so this cast is ok 2713 2714 if(using ~ "-type" in sargs) { 2715 string reportedType = sargs[using ~ "-type"][$-1]; 2716 if(reportedType == "ServerResult") { 2717 2718 // FIXME: doesn't handle functions that return 2719 // compound types (structs, arrays, etc) 2720 2721 ofInterest = null; 2722 2723 string str = sargs[using][$-1]; 2724 auto idx = str.indexOf("?"); 2725 string callingName, callingArguments; 2726 if(idx == -1) { 2727 callingName = str; 2728 } else { 2729 callingName = str[0..idx]; 2730 callingArguments = str[idx + 1 .. $]; 2731 } 2732 2733 // find it in reflection 2734 ofInterest ~= reflection.functions[callingName]. 2735 dispatcher(cgi, object, decodeVariables(callingArguments), "string", null).value.str; 2736 } 2737 } 2738 2739 2740 args[i] = fromUrlParam!type(ofInterest, using, sargs); 2741 } 2742 } 2743 } 2744 2745 static if(!is(ReturnType!f == void)) 2746 ReturnType!(f) ret; 2747 else 2748 void* ret; 2749 2750 static if(!is(ReturnType!f == void)) 2751 ret = instantiation(args); 2752 else 2753 instantiation(args); 2754 2755 WrapperReturn r; 2756 2757 static if(is(ReturnType!f : Element)) { 2758 if(ret is null) { 2759 r.value = returnValue; 2760 return r; // HACK to handle null returns 2761 } 2762 // we need to make sure that it's not called again when _postProcess(Document) is called! 2763 // FIXME: is this right? 2764 if(cgi.request("envelopeFormat", "document") != "document") 2765 api._postProcessElement(ret); // need to post process the element here so it works in ajax modes. 2766 } 2767 2768 static if(is(ReturnType!f : FileResource) && !is(ReturnType!f : Document)) { 2769 if(ret !is null && cgi !is null) { 2770 cgi.setResponseContentType(ret.contentType()); 2771 cgi.write(ret.getData(), true); 2772 cgi.close(); 2773 r.completed = true; 2774 } 2775 } 2776 2777 formatAs(ret, format, api, &returnValue, secondaryFormat); 2778 2779 r.value = returnValue; 2780 2781 return r; 2782 } 2783 2784 return &wrapper; 2785 } 2786 2787 2788 /// This is the function called to turn return values into strings. 2789 2790 /// Implement a template called _customFormat in your apiprovider class to make special formats. 2791 2792 /// Otherwise, this provides the defaults of html, table, json, etc. 2793 2794 /// call it like so: JSONValue returnValue; formatAs(value, this, returnValue, "type"); 2795 2796 // FIXME: it's awkward to call manually due to the JSONValue ref thing. Returning a string would be mega nice. 2797 string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue = null, string formatJsonToStringAs = null) if(is(R : ApiProvider)) { 2798 2799 if(format == "default") { 2800 static if(is(typeof(ret) : K[N][V], size_t N, K, V)) { 2801 format = "table"; 2802 } else { 2803 format = "html"; 2804 } 2805 2806 static if(is(typeof(ret) : K[], K)) { 2807 static if(is(K == struct)) 2808 format = "table"; 2809 } 2810 } 2811 2812 string retstr; 2813 if(api !is null) { 2814 static if(__traits(compiles, api._customFormat(ret, format))) { 2815 auto customFormatted = api._customFormat(ret, format); 2816 if(customFormatted !is null) { 2817 if(returnValue !is null) 2818 returnValue.str = customFormatted; 2819 return customFormatted; 2820 } 2821 } 2822 } 2823 switch(format) { 2824 case "html": 2825 retstr = toHtml(ret); 2826 if(returnValue !is null) 2827 returnValue.str = retstr; 2828 break; 2829 case "string": // FIXME: this is the most expensive part of the compile! Two seconds in one of my apps. 2830 static if(is(typeof(ret) == string)) { 2831 returnValue.str = ret; 2832 break; 2833 } else 2834 /+ 2835 static if(__traits(compiles, to!string(ret))) { 2836 retstr = to!string(ret); 2837 if(returnValue !is null) 2838 returnValue.str = retstr; 2839 } 2840 else goto badType; 2841 +/ 2842 goto badType; // FIXME 2843 case "json": 2844 assert(returnValue !is null); 2845 *returnValue = toJsonValue!(typeof(ret), R)(ret, formatJsonToStringAs, api); 2846 break; 2847 case "table": 2848 case "csv": 2849 auto document = new Document("<root></root>"); 2850 2851 void gotATable(Table table) { 2852 if(format == "csv") { 2853 retstr = tableToCsv(table); 2854 } else if(format == "table") { 2855 auto div = Element.make("div"); 2856 if(api !is null) { 2857 auto cgi = api.cgi; 2858 div.addChild("a", "Download as CSV", cgi.pathInfo ~ "?" ~ cgi.queryString ~ "&format=csv&envelopeFormat=csv"); 2859 } 2860 div.appendChild(table); 2861 retstr = div.toString(); 2862 } else assert(0); 2863 2864 2865 if(returnValue !is null) 2866 returnValue.str = retstr; 2867 } 2868 2869 static if(__traits(compiles, structToTable(document, ret))) 2870 { 2871 auto table = structToTable(document, ret); 2872 gotATable(table); 2873 break; 2874 } else static if(is(typeof(ret) : Element)) { 2875 auto table = cast(Table) ret; 2876 if(table is null) 2877 goto badType; 2878 gotATable(table); 2879 break; 2880 } else static if(is(typeof(ret) : K[N][V], size_t N, K, V)) { 2881 auto table = cast(Table) Element.make("table"); 2882 table.addClass("data-display"); 2883 auto headerRow = table.addChild("tr"); 2884 foreach(n; 0 .. N) 2885 table.addChild("th", "" ~ cast(char)(n + 'A')); 2886 foreach(k, v; ret) { 2887 auto row = table.addChild("tr"); 2888 foreach(cell; v) 2889 row.addChild("td", to!string(cell)); 2890 } 2891 gotATable(table); 2892 break; 2893 2894 } else 2895 goto badType; 2896 default: 2897 badType: 2898 throw new Exception("Couldn't get result as " ~ format); 2899 } 2900 2901 return retstr; 2902 } 2903 2904 string toCsv(string text) { 2905 return `"`~text.replace(`"`, `""`)~`"`; 2906 } 2907 2908 string tableToCsv(Table table) { 2909 string csv; 2910 foreach(tr; table.querySelectorAll("tr")) { 2911 if(csv.length) 2912 csv ~= "\r\n"; 2913 2914 bool outputted = false; 2915 foreach(item; tr.querySelectorAll("td, th")) { 2916 if(outputted) 2917 csv ~= ","; 2918 else 2919 outputted = true; 2920 2921 if(item.firstChild && item.firstChild.tagName == "ul") { 2922 string c; 2923 foreach(i, node; item.firstChild.childNodes) { 2924 if(c.length) c ~= "; "; 2925 c ~= node.innerText; 2926 } 2927 csv ~= toCsv(c); 2928 } else { 2929 csv ~= toCsv(item.innerText); 2930 } 2931 } 2932 } 2933 2934 return csv; 2935 } 2936 2937 2938 private string emptyTag(string rootName) { 2939 return ("<" ~ rootName ~ "></" ~ rootName ~ ">"); 2940 } 2941 2942 struct WrapperReturn { 2943 JSONValue value; 2944 bool completed; 2945 } 2946 2947 /// The definition of the beastly wrapper function 2948 alias WrapperReturn delegate(Cgi cgi, WebDotDBaseType, in string[][string] args, in string format, in string secondaryFormat = null) WrapperFunction; 2949 2950 /// tries to take a URL name and turn it into a human natural name. so get rid of slashes, capitalize, etc. 2951 string urlToBeauty(string url) { 2952 string u = url.replace("/", ""); 2953 2954 string ret; 2955 2956 bool capitalize = true; 2957 foreach(c; u) { 2958 if(capitalize) { 2959 ret ~= ("" ~ c).toUpper; 2960 capitalize = false; 2961 } else { 2962 if(c == '-') { 2963 ret ~= " "; 2964 capitalize = true; 2965 } else 2966 ret ~= c; 2967 } 2968 } 2969 2970 return ret; 2971 } 2972 2973 /// turns camelCase into dash-separated 2974 string toUrlName(string name) { 2975 string res; 2976 foreach(c; name) { 2977 if(c >= 'a' && c <= 'z') 2978 res ~= c; 2979 else { 2980 res ~= '-'; 2981 if(c >= 'A' && c <= 'Z') 2982 res ~= c + 0x20; 2983 else 2984 res ~= c; 2985 } 2986 } 2987 return res; 2988 } 2989 2990 /// turns camelCase into human presentable capitalized words with spaces 2991 string beautify(string name) { 2992 string n; 2993 2994 // really if this is cap and the following is lower, we want a space. 2995 // or in other words, if this is lower and previous is cap, we want a space injected before previous 2996 2997 // all caps names shouldn't get spaces 2998 if(name.length == 0 || name.toUpper() == name) 2999 return name; 3000 3001 n ~= toUpper(name[0..1]); 3002 3003 dchar last; 3004 foreach(idx, dchar c; name[1..$]) { 3005 if(c >= 'A' && c <= 'Z') { 3006 if(idx + 1 < name[1 .. $].length && name[1 + idx + 1] >= 'a' && name[1 + idx + 1] <= 'z') 3007 n ~= " "; 3008 } else if(c >= '0' && c <= '9') { 3009 if(last != ' ') 3010 n ~= " "; 3011 } 3012 3013 if(c == '_') 3014 n ~= " "; 3015 else 3016 n ~= c; 3017 last = c; 3018 } 3019 return n; 3020 } 3021 3022 3023 3024 3025 3026 3027 import core.stdc.stdlib; 3028 import core.stdc.time; 3029 import std.file; 3030 3031 /// meant to give a generic useful hook for sessions. kinda sucks at this point. 3032 /// use the Session class instead. If you just construct it, the sessionId property 3033 /// works fine. Don't set any data and it won't save any file. 3034 version(none) 3035 deprecated string getSessionId(Cgi cgi) { 3036 string token; // FIXME: should this actually be static? it seems wrong 3037 if(token is null) { 3038 if("_sess_id" in cgi.cookies) 3039 token = cgi.cookies["_sess_id"]; 3040 else { 3041 auto tmp = uniform(0, int.max); 3042 token = to!string(tmp); 3043 3044 cgi.setCookie("_sess_id", token, /*60 * 8 * 1000*/ 0, "/", null, true); 3045 } 3046 } 3047 3048 import std.md5; 3049 return getDigestString(cgi.remoteAddress ~ "\r\n" ~ cgi.userAgent ~ "\r\n" ~ token); 3050 } 3051 3052 version(Posix) { 3053 static import linux = core.sys.linux.unistd; 3054 static import sys_stat = core.sys.posix.sys.stat; 3055 } 3056 3057 /// This is cookie parameters for the Session class. The default initializers provide some simple default 3058 /// values for a site-wide session cookie. 3059 struct CookieParams { 3060 string name = "_sess_id"; 3061 string host = null; 3062 string path = "/"; 3063 long expiresIn = 0; 3064 bool httpsOnly = false; 3065 } 3066 3067 /// Provides some persistent storage, kinda like PHP 3068 /// But, you have to manually commit() the data back to a file. 3069 /// You might want to put this in a scope(exit) block or something like that. 3070 class Session { 3071 static Session loadReadOnly(Cgi cgi, CookieParams cookieParams = CookieParams(), bool useFile = true) { 3072 return new Session(cgi, cookieParams, useFile, true); 3073 } 3074 3075 version(webd_memory_sessions) { 3076 // FIXME: make this a full fledged option, maybe even with an additional 3077 // process to hold the information 3078 __gshared static string[string][string] sessions; 3079 } 3080 3081 /// Loads the session if available, and creates one if not. 3082 /// May write a session id cookie to the passed cgi object. 3083 this(Cgi cgi, CookieParams cookieParams = CookieParams(), bool useFile = true, bool readOnly = false) { 3084 // uncomment these two to render session useless (it has no backing) 3085 // but can be good for benchmarking the rest of your app 3086 //useFile = false; 3087 //_readOnly = true; 3088 3089 3090 // assert(cgi.https); // you want this for best security, but I won't be an ass and require it. 3091 this.cookieParams = cookieParams; 3092 this.cgi = cgi; 3093 this._readOnly = readOnly; 3094 3095 bool isNew = false; 3096 // string token; // using a member, see the note below 3097 if(cookieParams.name in cgi.cookies && cgi.cookies[cookieParams.name].length) { 3098 token = cgi.cookies[cookieParams.name]; 3099 } else { 3100 if("x-arsd-session-override" in cgi.requestHeaders) { 3101 loadSpecialSession(cgi); 3102 return; 3103 } else { 3104 // there is no session; make a new one. 3105 token = makeNewCookie(); 3106 isNew = true; 3107 } 3108 } 3109 3110 makeSessionId(token); 3111 3112 if(useFile) 3113 reload(); 3114 if(isNew) 3115 addDefaults(); 3116 } 3117 3118 /// This loads a session that the user requests, without the normal 3119 /// checks. The idea is to allow debugging or local request sharing. 3120 /// 3121 /// It is private because you never have to call it yourself, but read on 3122 /// to understand how it works and some potential security concerns. 3123 /// 3124 /// It loads the requested session read-only (it does not commit), 3125 /// if and only if the request asked for the correct hash and id. 3126 /// 3127 /// If they have enough info to build the correct hash, they must 3128 /// already know the contents of the file, so there should be no 3129 /// risk of data contamination here. (A traditional session hijack 3130 /// is surely much easier.) 3131 /// 3132 /// It is possible for them to forge a request as a particular user 3133 /// if they can read the file, but otherwise not write. For that reason, 3134 /// especially with this functionality here, it is very important for you 3135 /// to lock down your session files. If on a shared host, be sure each user's 3136 /// processes run as separate operating system users, so the file permissions 3137 /// set in commit() actually help you. 3138 /// 3139 /// If you can't reasonably protect the session file, compile this out with 3140 /// -version=no_session_override and only access unauthenticated functions 3141 /// from other languages. They can still read your sessions, and potentially 3142 /// hijack it, but it will at least be a little harder. 3143 /// 3144 /// Also, don't use this over the open internet at all. It's supposed 3145 /// to be local only. If someone sniffs the request, hijacking it 3146 /// becomes very easy; even easier than a normal session id since they just reply it. 3147 /// (you should really ssl encrypt all sessions for any real protection btw) 3148 private void loadSpecialSession(Cgi cgi) { 3149 // Note: this won't work with memory sessions 3150 version(webd_memory_sessions) 3151 throw new Exception("You cannot access sessions this way."); 3152 else version(webd_cookie_sessions) { 3153 // FIXME: implement 3154 } else { 3155 version(no_session_override) 3156 throw new Exception("You cannot access sessions this way."); 3157 else { 3158 // the header goes full-session-id;file-contents-hash 3159 auto info = split(cgi.requestHeaders["x-arsd-session-override"], ";"); 3160 3161 _sessionId = info[0]; 3162 auto hash = info[1]; 3163 3164 if(_sessionId.length == 0 || !std.file.exists(getFilePath())) { 3165 // there is no session 3166 _readOnly = true; 3167 return; 3168 } 3169 3170 // FIXME: race condition if the session changes? 3171 auto file = getFilePath(); 3172 auto contents = readText(file); 3173 auto ourhash = hashToString(SHA256(contents)); 3174 enforce(ourhash == hash);//, ourhash); 3175 _readOnly = true; 3176 reload(); 3177 } 3178 } 3179 } 3180 3181 /// Call this periodically to clean up old session files. The finalizer param can cancel the deletion 3182 /// of a file by returning false. 3183 public static void garbageCollect(bool delegate(string[string] data) finalizer = null, Duration maxAge = dur!"hours"(4)) { 3184 version(webd_memory_sessions) 3185 return; // blargh. FIXME really, they should be null so the gc can free them 3186 version(webd_cookie_sessions) 3187 return; // nothing needed to be done here 3188 3189 auto ctime = Clock.currTime(); 3190 foreach(DirEntry e; dirEntries(getTempDirectory(), "arsd_session_file_*", SpanMode.shallow)) { 3191 try { 3192 if(ctime - e.timeLastAccessed() > maxAge) { 3193 auto data = Session.loadData(e.name); 3194 3195 if(finalizer is null || !finalizer(data)) 3196 std.file.remove(e.name); 3197 } 3198 } catch(Exception except) { 3199 // if it is bad, kill it 3200 if(std.file.exists(e.name)) 3201 std.file.remove(e.name); 3202 } 3203 } 3204 } 3205 3206 private void addDefaults() { 3207 set("csrfToken", generateCsrfToken()); 3208 set("creationTime", Clock.currTime().toISOExtString()); 3209 3210 // this is there to help control access to someone requesting a specific session id (helpful for debugging or local access from other languages) 3211 // the idea is if there's some random stuff in there that you can only know if you have access to the file, it doesn't hurt to load that 3212 // session, since they have to be able to read it to know this value anyway, so you aren't giving them anything they don't already have. 3213 set("randomRandomness", to!string(uniform(0, ulong.max))); 3214 } 3215 3216 private string makeSessionId(string cookieToken) { 3217 // the remote address changes too much on some ISPs to be a useful check; 3218 // using it means sessions get lost regularly and users log out :( 3219 _sessionId = hashToString(SHA256(/*cgi.remoteAddress ~ "\r\n" ~*/ cgi.userAgent ~ "\r\n" ~ cookieToken)); 3220 return _sessionId; 3221 } 3222 3223 private string generateCsrfToken() { 3224 string[string] csrf; 3225 3226 csrf["key"] = to!string(uniform(0, ulong.max)); 3227 csrf["token"] = to!string(uniform(0, ulong.max)); 3228 3229 return encodeVariables(csrf); 3230 } 3231 3232 private CookieParams cookieParams; 3233 3234 // don't forget to make the new session id and set a new csrfToken after this too. 3235 private string makeNewCookie() { 3236 auto tmp = uniform(0, ulong.max); 3237 auto token = to!string(tmp); 3238 3239 version(webd_cookie_sessions) {} 3240 else 3241 setOurCookie(token); 3242 3243 return token; 3244 } 3245 3246 // FIXME: hack, see note on member string token 3247 // don't use this, it is meant to be private (...probably) 3248 /*private*/ void setOurCookie(string data) { 3249 this.token = data; 3250 if(!_readOnly) 3251 cgi.setCookie(cookieParams.name, data, 3252 cookieParams.expiresIn, cookieParams.path, cookieParams.host, true, cookieParams.httpsOnly); 3253 } 3254 3255 /// Kill the current session. It wipes out the disk file and memory, and 3256 /// changes the session ID. 3257 /// 3258 /// You should do this if the user's authentication info changes 3259 /// at all. 3260 void invalidate() { 3261 setOurCookie(""); 3262 clear(); 3263 3264 regenerateId(); 3265 } 3266 3267 /// Creates a new cookie, session id, and csrf token, deleting the old disk data. 3268 /// If you call commit() after doing this, it will save your existing data back to disk. 3269 /// (If you don't commit, the data will be lost when this object is deleted.) 3270 void regenerateId() { 3271 // we want to clean up the old file, but keep the data we have in memory. 3272 3273 version(webd_memory_sessions) { 3274 synchronized { 3275 if(sessionId in sessions) 3276 sessions.remove(sessionId); 3277 } 3278 } else version(webd_cookie_sessions) { 3279 // intentionally blank; the cookie will replace the old one 3280 } else { 3281 if(std.file.exists(getFilePath())) 3282 std.file.remove(getFilePath()); 3283 } 3284 3285 // and new cookie -> new session id -> new csrf token 3286 makeSessionId(makeNewCookie()); 3287 addDefaults(); 3288 3289 if(hasData) 3290 changed = true; 3291 3292 } 3293 3294 /// Clears the session data from both memory and disk. 3295 /// The session id is not changed by this function. To change it, 3296 /// use invalidate() if you want to clear data and change the ID 3297 /// or regenerateId() if you want to change the session ID, but not change the data. 3298 /// 3299 /// Odds are, invalidate() is what you really want. 3300 void clear() { 3301 assert(!_readOnly); // or should I throw an exception or just silently ignore it??? 3302 version(webd_memory_sessions) { 3303 synchronized { 3304 if(sessionId in sessions) 3305 sessions.remove(sessionId); 3306 } 3307 } else version(webd_cookie_sessions) { 3308 // nothing needed, setting data to null will do the trick 3309 } else { 3310 if(std.file.exists(getFilePath())) 3311 std.file.remove(getFilePath()); 3312 } 3313 data = null; 3314 _hasData = false; 3315 changed = false; 3316 } 3317 3318 // FIXME: what about expiring a session id and perhaps issuing a new 3319 // one? They should automatically invalidate at some point. 3320 3321 // Both an idle timeout and a forced reauth timeout is good to offer. 3322 // perhaps a helper function to do it in js too? 3323 3324 3325 // I also want some way to attach to a session if you have root 3326 // on the server, for convenience of debugging... 3327 3328 string sessionId() const { 3329 return _sessionId; 3330 } 3331 3332 bool hasData() const { 3333 return _hasData; 3334 } 3335 3336 /// like opIn 3337 bool hasKey(string key) const { 3338 auto ptr = key in data; 3339 if(ptr is null) 3340 return false; 3341 else 3342 return true; 3343 } 3344 3345 void removeKey(string key) { 3346 data.remove(key); 3347 _hasData = true; 3348 changed = true; 3349 } 3350 3351 /// get/set for strings 3352 @property string opDispatch(string name)(string v = null) if(name != "popFront") { 3353 if(v !is null) 3354 set(name, v); 3355 if(hasKey(name)) 3356 return get(name); 3357 return null; 3358 } 3359 3360 string opIndex(string key) const { 3361 return get(key); 3362 } 3363 3364 string opIndexAssign(string value, string field) { 3365 set(field, value); 3366 return value; 3367 } 3368 3369 // FIXME: doesn't seem to work 3370 string* opBinary(string op)(string key) if(op == "in") { 3371 return key in fields; 3372 } 3373 3374 void set(string key, string value) { 3375 data[key] = value; 3376 _hasData = true; 3377 changed = true; 3378 } 3379 3380 string get(string key) const { 3381 if(key !in data) 3382 throw new Exception("No such key in session: " ~ key); 3383 return data[key]; 3384 } 3385 3386 private string getFilePath() const { 3387 string path = getTempDirectory(); 3388 path ~= "arsd_session_file_" ~ sessionId; 3389 3390 return path; 3391 } 3392 3393 private static string[string] loadData(string path) { 3394 auto json = std.file.readText(path); 3395 return loadDataFromJson(json); 3396 } 3397 3398 private static string[string] loadDataFromJson(string json) { 3399 string[string] data = null; 3400 auto obj = parseJSON(json); 3401 enforce(obj.type == JSONType.object); 3402 foreach(k, v; obj.object) { 3403 string ret; 3404 final switch(v.type) { 3405 case JSONType..string: 3406 ret = v.str; 3407 break; 3408 case JSONType.uinteger: 3409 ret = to!string(v.integer); 3410 break; 3411 case JSONType.integer: 3412 ret = to!string(v.integer); 3413 break; 3414 case JSONType.float_: 3415 ret = to!string(v.floating); 3416 break; 3417 case JSONType.object: 3418 case JSONType.array: 3419 enforce(0, "invalid session data"); 3420 break; 3421 case JSONType.true_: 3422 ret = "true"; 3423 break; 3424 case JSONType.false_: 3425 ret = "false"; 3426 break; 3427 case JSONType.null_: 3428 ret = null; 3429 break; 3430 } 3431 3432 data[k] = ret; 3433 } 3434 3435 return data; 3436 } 3437 3438 // FIXME: there's a race condition here - if the user is using the session 3439 // from two windows, one might write to it as we're executing, and we won't 3440 // see the difference.... meaning we'll write the old data back. 3441 3442 /// Discards your changes, reloading the session data from the disk file. 3443 void reload() { 3444 data = null; 3445 3446 version(webd_memory_sessions) { 3447 synchronized { 3448 if(auto s = sessionId in sessions) { 3449 foreach(k, v; *s) 3450 data[k] = v; 3451 _hasData = true; 3452 } else { 3453 _hasData = false; 3454 } 3455 } 3456 } else version(webd_cookie_sessions) { 3457 // FIXME 3458 if(cookieParams.name in cgi.cookies) { 3459 auto cookie = cgi.cookies[cookieParams.name]; 3460 if(cookie.length) { 3461 import std.base64; 3462 import std.zlib; 3463 3464 auto cd = Base64URL.decode(cookie); 3465 3466 if(cd[0] == 'Z') 3467 cd = cast(ubyte[]) uncompress(cd[1 .. $]); 3468 3469 if(cd.length > 20) { 3470 auto hash = cd[$ - 20 .. $]; 3471 auto json = cast(string) cd[0 .. $-20]; 3472 3473 data = loadDataFromJson(json); 3474 } 3475 } 3476 } 3477 } else { 3478 auto path = getFilePath(); 3479 try { 3480 data = Session.loadData(path); 3481 _hasData = true; 3482 } catch(Exception e) { 3483 // it's a bad session... 3484 _hasData = false; 3485 data = null; 3486 if(std.file.exists(path)) 3487 std.file.remove(path); 3488 } 3489 } 3490 } 3491 3492 version(webd_cookie_sessions) 3493 private ubyte[20] getSignature(string jsonData) { 3494 import arsd.hmac; 3495 return hmac!SHA1(import("webd-cookie-signature-key.txt"), jsonData)[0 .. 20]; 3496 } 3497 3498 // FIXME: there's a race condition here - if the user is using the session 3499 // from two windows, one might write to it as we're executing, and we won't 3500 // see the difference.... meaning we'll write the old data back. 3501 3502 /// Commits your changes back to disk. 3503 void commit(bool force = false) { 3504 if(_readOnly) 3505 return; 3506 if(force || changed) { 3507 version(webd_memory_sessions) { 3508 synchronized { 3509 sessions[sessionId] = data; 3510 changed = false; 3511 } 3512 } else version(webd_cookie_sessions) { 3513 immutable(ubyte)[] dataForCookie; 3514 auto jsonData = toJson(data); 3515 auto hash = getSignature(jsonData); 3516 if(jsonData.length > 64) { 3517 import std.zlib; 3518 // file format: JSON ~ hash[20] 3519 auto compressor = new Compress(); 3520 dataForCookie ~= "Z"; 3521 dataForCookie ~= cast(ubyte[]) compressor.compress(toJson(data)); 3522 dataForCookie ~= cast(ubyte[]) compressor.compress(hash); 3523 dataForCookie ~= cast(ubyte[]) compressor.flush(); 3524 } else { 3525 dataForCookie = cast(immutable(ubyte)[]) jsonData; 3526 dataForCookie ~= hash; 3527 } 3528 3529 import std.base64; 3530 setOurCookie(Base64URL.encode(dataForCookie)); 3531 changed = false; 3532 } else { 3533 std.file.write(getFilePath(), toJson(data)); 3534 changed = false; 3535 // We have to make sure that only we can read this file, 3536 // since otherwise, on shared hosts, our session data might be 3537 // easily stolen. Note: if your shared host doesn't have different 3538 // users on the operating system for each user, it's still possible 3539 // for them to access this file and hijack your session! 3540 version(linux) 3541 enforce(sys_stat.chmod(toStringz(getFilePath()), octal!600) == 0, "chmod failed"); 3542 // FIXME: ensure the file's read permissions are locked down 3543 // on Windows too. 3544 } 3545 } 3546 } 3547 3548 string[string] data; 3549 private bool _hasData; 3550 private bool changed; 3551 private bool _readOnly; 3552 private string _sessionId; 3553 private Cgi cgi; // used to regenerate cookies, etc. 3554 3555 string token; // this isn't private, but don't use it FIXME this is a hack to allow cross domain session sharing on the same server.... 3556 3557 //private Variant[string] data; 3558 /* 3559 Variant* opBinary(string op)(string key) if(op == "in") { 3560 return key in data; 3561 } 3562 3563 T get(T)(string key) { 3564 if(key !in data) 3565 throw new Exception(key ~ " not in session data"); 3566 3567 return data[key].coerce!T; 3568 } 3569 3570 void set(T)(string key, T t) { 3571 Variant v; 3572 v = t; 3573 data[key] = t; 3574 } 3575 */ 3576 } 3577 3578 /// sets a site-wide cookie, meant to simplify login code. Note: you often might not want a side wide cookie, but I usually do since my projects need single sessions across multiple thingies, hence, this. 3579 void setLoginCookie(Cgi cgi, string name, string value) { 3580 cgi.setCookie(name, value, 0, "/", null, true); 3581 } 3582 3583 3584 3585 immutable(string[]) monthNames = [ 3586 null, 3587 "January", 3588 "February", 3589 "March", 3590 "April", 3591 "May", 3592 "June", 3593 "July", 3594 "August", 3595 "September", 3596 "October", 3597 "November", 3598 "December" 3599 ]; 3600 3601 immutable(string[]) weekdayNames = [ 3602 "Sunday", 3603 "Monday", 3604 "Tuesday", 3605 "Wednesday", 3606 "Thursday", 3607 "Friday", 3608 "Saturday" 3609 ]; 3610 3611 3612 // this might be temporary 3613 struct TemplateFilters { 3614 // arguments: 3615 // args (space separated on pipe), context element, attribute name (if we're in an attribute) 3616 // string (string replacement, string[], in Element, string) 3617 3618 string date(string replacement, string[], in Element, string) { 3619 if(replacement.length == 0) 3620 return replacement; 3621 SysTime date; 3622 if(replacement.isNumeric) { 3623 auto dateTicks = to!long(replacement); 3624 date = SysTime( unixTimeToStdTime(cast(time_t)(dateTicks/1_000)) ); 3625 } else { 3626 date = cast(SysTime) DateTime.fromISOExtString(replacement); 3627 } 3628 3629 auto day = date.day; 3630 auto year = date.year; 3631 assert(date.month < monthNames.length, to!string(date.month)); 3632 auto month = monthNames[date.month]; 3633 replacement = format("%s %d, %d", month, day, year); 3634 3635 return replacement; 3636 } 3637 3638 string limitSize(string replacement, string[] args, in Element, string) { 3639 auto limit = to!int(args[0]); 3640 3641 if(replacement.length > limit) { 3642 replacement = replacement[0 .. limit]; 3643 while(replacement.length && replacement[$-1] > 127) 3644 replacement = replacement[0 .. $-1]; 3645 3646 if(args.length > 1) 3647 replacement ~= args[1]; 3648 } 3649 3650 return replacement; 3651 } 3652 3653 string uri(string replacement, string[], in Element, string) { 3654 return std.uri.encodeComponent(replacement); 3655 } 3656 3657 string js(string replacement, string[], in Element, string) { 3658 return toJson(replacement); 3659 } 3660 3661 string article(string replacement, string[], in Element, string) { 3662 if(replacement.length && replacement[0].isVowel()) 3663 return "an " ~ replacement; 3664 else if(replacement.length) 3665 return "a " ~ replacement; 3666 return replacement; 3667 } 3668 3669 string capitalize(string replacement, string[], in Element, string) { 3670 return std..string.capitalize(replacement); 3671 } 3672 3673 string possessive(string replacement, string[], in Element, string) { 3674 if(replacement.length && replacement[$ - 1] == 's') 3675 return replacement ~ "'"; 3676 else if(replacement.length) 3677 return replacement ~ "'s"; 3678 else 3679 return replacement; 3680 } 3681 3682 // {$count|plural singular plural} 3683 string plural(string replacement, string[] args, in Element, string) { 3684 return pluralHelper(replacement, args.length ? args[0] : null, args.length > 1 ? args[1] : null); 3685 } 3686 3687 string pluralHelper(string number, string word, string pluralWord = null) { 3688 if(word.length == 0) 3689 return word; 3690 3691 int count = 0; 3692 if(number.length && std..string.isNumeric(number)) 3693 count = to!int(number); 3694 3695 if(count == 1) 3696 return word; // it isn't actually plural 3697 3698 if(pluralWord !is null) 3699 return pluralWord; 3700 3701 switch(word[$ - 1]) { 3702 case 's': 3703 case 'a', 'i', 'o', 'u': 3704 return word ~ "es"; 3705 case 'f': 3706 return word[0 .. $-1] ~ "ves"; 3707 case 'y': 3708 return word[0 .. $-1] ~ "ies"; 3709 default: 3710 return word ~ "s"; 3711 } 3712 } 3713 3714 // replacement is the number here, and args is some text to write 3715 // it goes {$count|cnt thing(s)} 3716 string cnt(string replacement, string[] args, in Element, string) { 3717 string s = replacement; 3718 foreach(arg; args) { 3719 s ~= " "; 3720 if(arg.endsWith("(s)")) 3721 s ~= pluralHelper(replacement, arg[0 .. $-3]); 3722 else 3723 s ~= arg; 3724 } 3725 3726 return s; 3727 } 3728 3729 string stringArray(string replacement, string[] args, in Element, string) { 3730 if(replacement.length == 0) 3731 return replacement; 3732 int idx = to!int(replacement); 3733 if(idx < 0 || idx >= args.length) 3734 return replacement; 3735 return args[idx]; 3736 } 3737 3738 string boolean(string replacement, string[] args, in Element, string) { 3739 if(replacement == "1") 3740 return "yes"; 3741 return "no"; 3742 } 3743 3744 static auto defaultThings() { 3745 string delegate(string, string[], in Element, string)[string] pipeFunctions; 3746 TemplateFilters filters; 3747 3748 string delegate(string, string[], in Element, string) tmp; 3749 foreach(member; __traits(allMembers, TemplateFilters)) { 3750 static if(__traits(compiles, tmp = &__traits(getMember, filters, member))) { 3751 if(member !in pipeFunctions) 3752 pipeFunctions[member] = &__traits(getMember, filters, member); 3753 } 3754 } 3755 3756 return pipeFunctions; 3757 } 3758 } 3759 3760 3761 string applyTemplateToText( 3762 string text, 3763 in string[string] vars, 3764 in string delegate(string, string[], in Element, string)[string] pipeFunctions = TemplateFilters.defaultThings()) 3765 { 3766 // kinda hacky, but meh 3767 auto element = Element.make("body"); 3768 element.innerText = text; 3769 applyTemplateToElement(element, vars, pipeFunctions); 3770 return element.innerText; 3771 } 3772 3773 void applyTemplateToElement( 3774 Element e, 3775 in string[string] vars, 3776 in string delegate(string, string[], in Element, string)[string] pipeFunctions = TemplateFilters.defaultThings()) 3777 { 3778 3779 foreach(ele; e.tree) { 3780 auto tc = cast(TextNode) ele; 3781 if(tc !is null) { 3782 // text nodes have no attributes, but they do have text we might replace. 3783 tc.contents = htmlTemplateWithData(tc.contents, vars, pipeFunctions, false, tc, null); 3784 } else { 3785 if(ele.hasAttribute("data-html-from")) { 3786 ele.innerHTML = htmlTemplateWithData(ele.dataset.htmlFrom, vars, pipeFunctions, false, ele, null); 3787 ele.removeAttribute("data-html-from"); 3788 } 3789 3790 auto rs = cast(RawSource) ele; 3791 if(rs !is null) { 3792 bool isSpecial; 3793 if(ele.parentNode) 3794 isSpecial = ele.parentNode.tagName == "script" || ele.parentNode.tagName == "style"; 3795 rs.source = htmlTemplateWithData(rs.source, vars, pipeFunctions, !isSpecial, rs, null); /* FIXME: might be wrong... */ 3796 } 3797 // if it is not a text node, it has no text where templating is valid, except the attributes 3798 // note: text nodes have no attributes, which is why this is in the separate branch. 3799 foreach(k, v; ele.attributes) { 3800 if(k == "href" || k == "src") { 3801 // FIXME: HACK this should be properly context sensitive.. 3802 v = v.replace("%7B%24", "{$"); 3803 v = v.replace("%7D", "}"); 3804 } 3805 ele.attributes[k] = htmlTemplateWithData(v, vars, pipeFunctions, false, ele, k); 3806 } 3807 } 3808 } 3809 } 3810 3811 // this thing sucks a little less now. 3812 // set useHtml to false if you're working on internal data (such as TextNode.contents, or attribute); 3813 // it should only be set to true if you're doing input that has already been ran through toString or something. 3814 // NOTE: I'm probably going to change the pipe function thing a bit more, but I'm basically happy with it now. 3815 string htmlTemplateWithData(in string text, in string[string] vars, in string delegate(string, string[], in Element, string)[string] pipeFunctions, bool useHtml, Element contextElement = null, string contextAttribute = null) { 3816 if(text is null) 3817 return null; 3818 3819 int state = 0; 3820 3821 string newText = null; 3822 3823 size_t nameStart; 3824 size_t replacementStart; 3825 size_t lastAppend = 0; 3826 3827 string name = null; 3828 bool replacementPresent = false; 3829 string replacement = null; 3830 string currentPipe = null; 3831 3832 foreach(i, c; text) { 3833 void stepHandler() { 3834 if(name is null) 3835 name = text[nameStart .. i]; 3836 else if(nameStart != i) 3837 currentPipe = text[nameStart .. i]; 3838 3839 nameStart = i + 1; 3840 3841 if(!replacementPresent) { 3842 auto it = name in vars; 3843 if(it !is null) { 3844 replacement = *it; 3845 replacementPresent = true; 3846 } 3847 } 3848 } 3849 3850 void pipeHandler() { 3851 if(currentPipe is null || replacement is null) 3852 return; 3853 3854 auto pieces = currentPipe.split(" "); 3855 assert(pieces.length); 3856 auto pipeName = pieces[0]; 3857 auto pipeArgs = pieces[1 ..$]; 3858 3859 foreach(ref arg; pipeArgs) { 3860 if(arg.length && arg[0] == '$') { 3861 string n = arg[1 .. $]; 3862 auto idx = n.indexOf("("); 3863 string moar; 3864 if(idx != -1) { 3865 moar = n[idx .. $]; 3866 n = n[0 .. idx]; 3867 } 3868 3869 if(n in vars) 3870 arg = vars[n] ~ moar; 3871 } 3872 } 3873 3874 if(pipeName in pipeFunctions) { 3875 replacement = pipeFunctions[pipeName](replacement, pipeArgs, contextElement, contextAttribute); 3876 // string, string[], in Element, string 3877 } 3878 3879 currentPipe = null; 3880 } 3881 3882 switch(state) { 3883 default: assert(0); 3884 case 0: 3885 if(c == '{') { 3886 replacementStart = i; 3887 state++; 3888 replacementPresent = false; 3889 } 3890 break; 3891 case 1: 3892 if(c == '$') 3893 state++; 3894 else 3895 state--; // not a variable 3896 break; 3897 case 2: // just started seeing a name 3898 if(c == '}') { 3899 state = 0; // empty names aren't allowed; ignore it 3900 } else { 3901 nameStart = i; 3902 name = null; 3903 state++; 3904 } 3905 break; 3906 case 3: // reading a name 3907 // the pipe operator lets us filter the text 3908 if(c == '|') { 3909 pipeHandler(); 3910 stepHandler(); 3911 } else if(c == '}') { 3912 // just finished reading it, let's do our replacement. 3913 pipeHandler(); // anything that was there 3914 stepHandler(); // might make a new pipe if the first... 3915 pipeHandler(); // new names/pipes since this is the last go 3916 if(name !is null && replacementPresent /*&& replacement !is null*/) { 3917 newText ~= text[lastAppend .. replacementStart]; 3918 if(useHtml) 3919 replacement = htmlEntitiesEncode(replacement).replace("\n", "<br />"); 3920 newText ~= replacement; 3921 lastAppend = i + 1; 3922 replacement = null; 3923 } 3924 3925 state = 0; 3926 } 3927 break; 3928 } 3929 } 3930 3931 if(newText is null) 3932 newText = text; // nothing was found, so no need to risk allocating anything... 3933 else 3934 newText ~= text[lastAppend .. $]; // make sure we have everything here 3935 3936 return newText; 3937 } 3938 3939 /// a specialization of Document that: a) is always in strict mode and b) provides some template variable text replacement, in addition to DOM manips. The variable text is valid in text nodes and attribute values. It takes the format of {$variable}, where variable is a key into the vars member. 3940 class TemplatedDocument : Document { 3941 override string toString() const { 3942 if(this.root !is null) 3943 applyTemplateToElement(cast() this.root, vars, viewFunctions); /* FIXME: I shouldn't cast away const, since it's rude to modify an object in any toString.... but that's what I'm doing for now */ 3944 3945 return super.toString(); 3946 } 3947 3948 public: 3949 string[string] vars; /// use this to set up the string replacements. document.vars["name"] = "adam"; then in doc, <p>hellp, {$name}.</p>. Note the vars are converted lazily at toString time and are always HTML escaped. 3950 /// In the html templates, you can write {$varname} or {$varname|func} (or {$varname|func arg arg|func} and so on). This holds the functions available these. The TemplatedDocument constructor puts in a handful of generic ones. 3951 string delegate(string, string[], in Element, string)[string] viewFunctions; 3952 3953 3954 this(string src) { 3955 super(); 3956 viewFunctions = TemplateFilters.defaultThings(); 3957 parse(src, true, true); 3958 } 3959 3960 this() { 3961 viewFunctions = TemplateFilters.defaultThings(); 3962 } 3963 3964 void delegate(TemplatedDocument)[] preToStringFilters; 3965 void delegate(ref string)[] postToStringFilters; 3966 } 3967 3968 /// a convenience function to do filters on your doc and write it out. kinda useless still at this point. 3969 void writeDocument(Cgi cgi, TemplatedDocument document) { 3970 foreach(f; document.preToStringFilters) 3971 f(document); 3972 3973 auto s = document.toString(); 3974 3975 foreach(f; document.postToStringFilters) 3976 f(s); 3977 3978 cgi.write(s); 3979 } 3980 3981 /* Password helpers */ 3982 3983 /// These added a dependency on arsd.sha, but hashing passwords is somewhat useful in a lot of apps so I figured it was worth it. 3984 /// use this to make the hash to put in the database... 3985 string makeSaltedPasswordHash(string userSuppliedPassword, string salt = null) { 3986 if(salt is null) 3987 salt = to!string(uniform(0, int.max)); 3988 3989 // FIXME: sha256 is actually not ideal for this, but meh it's what i have. 3990 return hashToString(SHA256(salt ~ userSuppliedPassword)) ~ ":" ~ salt; 3991 } 3992 3993 /// and use this to check it. 3994 bool checkPassword(string saltedPasswordHash, string userSuppliedPassword) { 3995 auto parts = saltedPasswordHash.split(":"); 3996 3997 return makeSaltedPasswordHash(userSuppliedPassword, parts[1]) == saltedPasswordHash; 3998 } 3999 4000 4001 /// implements the "table" format option. Works on structs and associative arrays (string[string][]) 4002 Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) if(isArray!(T) && !isAssociativeArray!(T)) { 4003 auto t = cast(Table) document.createElement("table"); 4004 t.attrs.border = "1"; 4005 4006 static if(is(T == string[string][])) { 4007 string[string] allKeys; 4008 foreach(row; arr) { 4009 foreach(k; row.keys) 4010 allKeys[k] = k; 4011 } 4012 4013 auto sortedKeys = allKeys.keys.sort; 4014 Element tr; 4015 4016 auto thead = t.addChild("thead"); 4017 auto tbody = t.addChild("tbody"); 4018 4019 tr = thead.addChild("tr"); 4020 foreach(key; sortedKeys) 4021 tr.addChild("th", key); 4022 4023 bool odd = true; 4024 foreach(row; arr) { 4025 tr = tbody.addChild("tr"); 4026 foreach(k; sortedKeys) { 4027 tr.addChild("td", k in row ? row[k] : ""); 4028 } 4029 if(odd) 4030 tr.addClass("odd"); 4031 4032 odd = !odd; 4033 } 4034 } else static if(is(typeof(arr[0]) == struct)) { 4035 { 4036 auto thead = t.addChild("thead"); 4037 auto tr = thead.addChild("tr"); 4038 if(arr.length) { 4039 auto s = arr[0]; 4040 foreach(idx, member; s.tupleof) 4041 tr.addChild("th", s.tupleof[idx].stringof[2..$]); 4042 } 4043 } 4044 4045 bool odd = true; 4046 auto tbody = t.addChild("tbody"); 4047 foreach(s; arr) { 4048 auto tr = tbody.addChild("tr"); 4049 foreach(member; s.tupleof) { 4050 static if(is(typeof(member) == URL[])) { 4051 auto td = tr.addChild("td"); 4052 foreach(i, link; member) { 4053 td.addChild("a", link.title.length ? link.title : to!string(i), link.url); 4054 td.appendText(" "); 4055 } 4056 4057 } else { 4058 tr.addChild("td", Html(toHtml(member))); 4059 } 4060 } 4061 4062 if(odd) 4063 tr.addClass("odd"); 4064 4065 odd = !odd; 4066 } 4067 } else static assert(0, T.stringof); 4068 4069 return t; 4070 } 4071 4072 // this one handles horizontal tables showing just one item 4073 /// does a name/field table for just a singular object 4074 Table structToTable(T)(Document document, T s, string[] fieldsToSkip = null) if(!isArray!(T) || isAssociativeArray!(T)) { 4075 static if(__traits(compiles, s.makeHtmlTable(document))) 4076 return s.makeHtmlTable(document); 4077 else { 4078 4079 auto t = cast(Table) document.createElement("table"); 4080 4081 static if(is(T == struct)) { 4082 main: foreach(i, member; s.tupleof) { 4083 string name = s.tupleof[i].stringof[2..$]; 4084 foreach(f; fieldsToSkip) 4085 if(name == f) 4086 continue main; 4087 4088 string nameS = name.idup; 4089 name = ""; 4090 foreach(idx, c; nameS) { 4091 if(c >= 'A' && c <= 'Z') 4092 name ~= " " ~ c; 4093 else if(c == '_') 4094 name ~= " "; 4095 else 4096 name ~= c; 4097 } 4098 4099 t.appendRow(t.th(name.capitalize), 4100 to!string(member)); 4101 } 4102 } else static if(is(T == string[string])) { 4103 foreach(k, v; s){ 4104 t.appendRow(t.th(k), v); 4105 } 4106 } else static assert(0); 4107 4108 return t; 4109 } 4110 } 4111 4112 /// This adds a custom attribute to links in the document called qsa which modifies the values on the query string 4113 void translateQsa(Document document, Cgi cgi, string logicalScriptName = null) { 4114 if(document is null || cgi is null) 4115 return; 4116 4117 if(logicalScriptName is null) 4118 logicalScriptName = cgi.logicalScriptName; 4119 4120 foreach(a; document.querySelectorAll("a[qsa]")) { 4121 string href = logicalScriptName ~ cgi.pathInfo ~ "?"; 4122 4123 int matches, possibilities; 4124 4125 string[][string] vars; 4126 foreach(k, v; cgi.getArray) 4127 vars[k] = cast(string[]) v; 4128 foreach(k, v; decodeVariablesSingle(a.attrs.qsa)) { 4129 if(k in cgi.get && cgi.get[k] == v) 4130 matches++; 4131 possibilities++; 4132 4133 if(k !in vars || vars[k].length <= 1) 4134 vars[k] = [v]; 4135 else 4136 assert(0, "qsa doesn't work here"); 4137 } 4138 4139 string[] clear = a.getAttribute("qsa-clear").split("&"); 4140 clear ~= "ajaxLoading"; 4141 if(a.parentNode !is null) 4142 clear ~= a.parentNode.getAttribute("qsa-clear").split("&"); 4143 4144 bool outputted = false; 4145 varskip: foreach(k, varr; vars) { 4146 foreach(item; clear) 4147 if(k == item) 4148 continue varskip; 4149 foreach(v; varr) { 4150 if(outputted) 4151 href ~= "&"; 4152 else 4153 outputted = true; 4154 4155 href ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); 4156 } 4157 } 4158 4159 a.href = href; 4160 4161 a.removeAttribute("qsa"); 4162 4163 if(matches == possibilities) 4164 a.addClass("current"); 4165 } 4166 } 4167 4168 /// This uses reflection info to generate Javascript that can call the server with some ease. 4169 /// Also includes javascript base (see bottom of this file) 4170 string makeJavascriptApi(const ReflectionInfo* mod, string base, bool isNested = false) { 4171 assert(mod !is null); 4172 4173 string script; 4174 4175 if(0 && isNested) 4176 script = `'`~mod.name~`': { 4177 "_apiBase":'`~base~`',`; 4178 else 4179 script = `var `~mod.name~` = { 4180 "_apiBase":'`~base~`',`; 4181 4182 script ~= javascriptBase; 4183 4184 script ~= "\n\t"; 4185 4186 bool[string] alreadyDone; 4187 4188 bool outp = false; 4189 4190 foreach(s; mod.enums) { 4191 if(outp) 4192 script ~= ",\n\t"; 4193 else 4194 outp = true; 4195 4196 script ~= "'"~s.name~"': {\n"; 4197 4198 bool outp2 = false; 4199 foreach(i, n; s.names) { 4200 if(outp2) 4201 script ~= ",\n"; 4202 else 4203 outp2 = true; 4204 4205 // auto v = s.values[i]; 4206 auto v = "'" ~ n ~ "'"; // we actually want to use the name here because to!enum() uses member name. 4207 4208 script ~= "\t\t'"~n~"':" ~ to!string(v); 4209 } 4210 4211 script ~= "\n\t}"; 4212 } 4213 4214 foreach(s; mod.structs) { 4215 if(outp) 4216 script ~= ",\n\t"; 4217 else 4218 outp = true; 4219 4220 script ~= "'"~s.name~"': function("; 4221 4222 bool outp2 = false; 4223 foreach(n; s.members) { 4224 if(outp2) 4225 script ~= ", "; 4226 else 4227 outp2 = true; 4228 4229 script ~= n.name; 4230 4231 } 4232 script ~= ") { return {\n"; 4233 4234 outp2 = false; 4235 4236 script ~= "\t\t'_arsdTypeOf':'"~s.name~"'"; 4237 if(s.members.length) 4238 script ~= ","; 4239 script ~= " // metadata, ought to be read only\n"; 4240 4241 // outp2 is still false because I put the comma above 4242 foreach(n; s.members) { 4243 if(outp2) 4244 script ~= ",\n"; 4245 else 4246 outp2 = true; 4247 4248 auto v = n.defaultValue; 4249 4250 script ~= "\t\t'"~n.name~"': (typeof "~n.name~" == 'undefined') ? "~n.name~" : '" ~ to!string(v) ~ "'"; 4251 } 4252 4253 script ~= "\n\t}; }"; 4254 } 4255 4256 foreach(key, func; mod.functions) { 4257 if(func.originalName in alreadyDone) 4258 continue; // there's url friendly and code friendly, only need one 4259 4260 alreadyDone[func.originalName] = true; 4261 4262 if(outp) 4263 script ~= ",\n\t"; 4264 else 4265 outp = true; 4266 4267 4268 string args; 4269 string obj; 4270 bool outputted = false; 4271 /+ 4272 foreach(i, arg; func.parameters) { 4273 if(outputted) { 4274 args ~= ","; 4275 obj ~= ","; 4276 } else 4277 outputted = true; 4278 4279 args ~= arg.name; 4280 4281 // FIXME: we could probably do better checks here too like on type 4282 obj ~= `'`~arg.name~`':(typeof `~arg.name ~ ` == "undefined" ? this._raiseError('InsufficientParametersException', '`~func.originalName~`: argument `~to!string(i) ~ " (" ~ arg.staticType~` `~arg.name~`) is not present') : `~arg.name~`)`; 4283 } 4284 +/ 4285 4286 /* 4287 if(outputted) 4288 args ~= ","; 4289 args ~= "callback"; 4290 */ 4291 4292 script ~= `'` ~ func.originalName ~ `'`; 4293 script ~= ":"; 4294 script ~= `function(`~args~`) {`; 4295 if(obj.length) 4296 script ~= ` 4297 var argumentsObject = { 4298 `~obj~` 4299 }; 4300 return this._serverCall('`~key~`', argumentsObject, '`~func.returnType~`');`; 4301 else 4302 script ~= ` 4303 return this._serverCall('`~key~`', arguments, '`~func.returnType~`');`; 4304 4305 script ~= ` 4306 }`; 4307 } 4308 4309 script ~= "\n}"; 4310 4311 // some global stuff to put in 4312 if(!isNested) 4313 script ~= ` 4314 if(typeof arsdGlobalStuffLoadedForWebDotD == "undefined") { 4315 arsdGlobalStuffLoadedForWebDotD = true; 4316 var oldObjectDotPrototypeDotToString = Object.prototype.toString; 4317 Object.prototype.toString = function() { 4318 if(this.formattedSecondarily) 4319 return this.formattedSecondarily; 4320 4321 return oldObjectDotPrototypeDotToString.call(this); 4322 } 4323 } 4324 `; 4325 4326 // FIXME: it should output the classes too 4327 // FIXME: hax hax hax 4328 foreach(n, obj; mod.objects) { 4329 script ~= ";"; 4330 //if(outp) 4331 // script ~= ",\n\t"; 4332 //else 4333 // outp = true; 4334 4335 script ~= makeJavascriptApi(obj, base ~ n ~ "/", true); 4336 } 4337 4338 return script; 4339 } 4340 4341 bool isVowel(char c) { 4342 return ( 4343 c == 'a' || c == 'A' || 4344 c == 'e' || c == 'E' || 4345 c == 'i' || c == 'I' || 4346 c == 'o' || c == 'O' || 4347 c == 'u' || c == 'U' 4348 ); 4349 } 4350 4351 4352 debug string javascriptBase = ` 4353 // change this in your script to get fewer error popups 4354 "_debugMode":true,` ~ javascriptBaseImpl; 4355 else string javascriptBase = ` 4356 // change this in your script to get more details in errors 4357 "_debugMode":false,` ~ javascriptBaseImpl; 4358 4359 /// The Javascript code used in the generated JS API. 4360 /** 4361 It provides the foundation to calling the server via background requests 4362 and handling the response in callbacks. (ajax style stuffs). 4363 4364 The names with a leading underscore are meant to be private. 4365 4366 4367 Generally: 4368 4369 YourClassName.yourMethodName(args...).operation(args); 4370 4371 4372 CoolApi.getABox("red").useToReplace(document.getElementById("playground")); 4373 4374 for example. 4375 4376 When you call a method, it doesn't make the server request. Instead, it returns 4377 an object describing the call. This means you can manipulate it (such as requesting 4378 a custom format), pass it as an argument to other functions (thus saving http requests) 4379 and finally call it at the end. 4380 4381 The operations are: 4382 get(callback, args to callback...); 4383 4384 See below. 4385 4386 useToReplace(element) // pass an element reference. Example: useToReplace(document.querySelector(".name")); 4387 useToReplace(element ID : string) // you pass a string, it calls document.getElementById for you 4388 4389 useToReplace sets the given element's innerHTML to the return value. The return value is automatically requested 4390 to be formatted as HTML. 4391 4392 appendTo(element) 4393 appendTo(element ID : String) 4394 4395 Adds the return value, as HTML, to the given element's inner html. 4396 4397 useToReplaceElement(element) 4398 4399 Replaces the given element entirely with the return value. (basically element.outerHTML = returnValue;) 4400 4401 useToFillForm(form) 4402 4403 Takes an object. Loop through the members, setting the form.elements[key].value = value. 4404 4405 Does not work if the return value is not a javascript object (so use it if your function returns a struct or string[string]) 4406 4407 getSync() 4408 4409 Does a synchronous get and returns the server response. Not recommended. 4410 4411 get() : 4412 4413 The generic get() function is the most generic operation to get a response. It's arguments implement 4414 partial application for you, so you can pass just about any callback to it. 4415 4416 Despite the name, the underlying operation may be HTTP GET or HTTP POST. This is determined from the 4417 function's server side attributes. (FIXME: implement smarter thing. Currently it actually does it by name - if 4418 the function name starts with get, do get. Else, do POST.) 4419 4420 4421 Usage: 4422 4423 CoolApi.getABox('red').get(alert); // calls alert(returnedValue); so pops up the returned value 4424 4425 CoolApi.getABox('red').get(fadeOut, this); // calls fadeOut(this, returnedValue); 4426 4427 4428 Since JS functions generally ignore extra params, this lets you call just about anything: 4429 4430 CoolApi.getABox('red').get(alert, "Success"); // pops a box saying "Success", ignoring the actual return value 4431 4432 4433 Passing arguments to the functions let you reuse a lot of things that might not have been designed with this in mind. 4434 If you use arsd.js, there's other little functions that let you turn properties into callbacks too. 4435 4436 4437 Passing "this" to a callback via get is useful too since inside the callback, this probably won't refer to what you 4438 wanted. As an argument though, it all remains sane. 4439 4440 4441 4442 4443 Error Handling: 4444 4445 D exceptions are translated into Javascript exceptions by the serverCall function. They are thrown, but since it's 4446 async, catching them is painful. 4447 4448 It will probably show up in your browser's error console, or you can set the returned object's onerror function 4449 to something to handle it callback style. FIXME: not sure if this actually works right! 4450 */ 4451 // FIXME: this should probably be rewritten to make a constructable prototype object instead of a literal. 4452 enum string javascriptBaseImpl = q{ 4453 "_doRequest": function(url, args, callback, method, async) { 4454 var xmlHttp; 4455 try { 4456 xmlHttp=new XMLHttpRequest(); 4457 } 4458 catch (e) { 4459 try { 4460 xmlHttp=new ActiveXObject("Msxml2.XMLHTTP"); 4461 } 4462 catch (e) { 4463 xmlHttp=new ActiveXObject("Microsoft.XMLHTTP"); 4464 } 4465 } 4466 4467 if(async) 4468 xmlHttp.onreadystatechange=function() { 4469 if(xmlHttp.readyState==4) { 4470 // either if the function is nor available or if it returns a good result, we're set. 4471 // it might get to this point without the headers if the request was aborted 4472 if(callback && (!xmlHttp.getAllResponseHeaders || xmlHttp.getAllResponseHeaders())) { 4473 callback(xmlHttp.responseText, xmlHttp.responseXML); 4474 } 4475 } 4476 } 4477 4478 var argString = this._getArgString(args); 4479 if(method == "GET" && url.indexOf("?") == -1) 4480 url = url + "?" + argString; 4481 4482 xmlHttp.open(method, url, async); 4483 4484 var a = ""; 4485 4486 var csrfKey = document.body.getAttribute("data-csrf-key"); 4487 var csrfToken = document.body.getAttribute("data-csrf-token"); 4488 var csrfPair = ""; 4489 if(csrfKey && csrfKey.length > 0 && csrfToken && csrfToken.length > 0) { 4490 csrfPair = encodeURIComponent(csrfKey) + "=" + encodeURIComponent(csrfToken); 4491 // we send this so it can be easily verified for things like restricted jsonp 4492 xmlHttp.setRequestHeader("X-Arsd-Csrf-Pair", csrfPair); 4493 } 4494 4495 if(method == "POST") { 4496 xmlHttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); 4497 a = argString; 4498 // adding the CSRF stuff, if necessary 4499 if(csrfPair.length) { 4500 if(a.length > 0) 4501 a += "&"; 4502 a += csrfPair; 4503 } 4504 } else { 4505 xmlHttp.setRequestHeader("Content-Type", "text/plain"); 4506 } 4507 4508 xmlHttp.setRequestHeader("X-Requested-With", "XMLHttpRequest"); 4509 xmlHttp.send(a); 4510 4511 if(!async && callback) { 4512 xmlHttp.timeout = 500; 4513 return callback(xmlHttp.responseText, xmlHttp.responseXML); 4514 } 4515 return xmlHttp; 4516 }, 4517 4518 "_raiseError":function(type, message) { 4519 var error = new Error(message); 4520 error.name = type; 4521 throw error; 4522 }, 4523 4524 "_getUriRelativeToBase":function(name, args) { 4525 var str = name; 4526 var argsStr = this._getArgString(args); 4527 if(argsStr.length) 4528 str += "?" + argsStr; 4529 4530 return str; 4531 }, 4532 4533 "_getArgString":function(args) { 4534 var a = ""; 4535 var outputted = false; 4536 var i; // wow Javascript sucks! god damned global loop variables 4537 for(i in args) { 4538 if(outputted) { 4539 a += "&"; 4540 } else outputted = true; 4541 var arg = args[i]; 4542 var argType = ""; 4543 // Make sure the types are all sane 4544 4545 if(arg && arg._arsdTypeOf && arg._arsdTypeOf == "ServerResult") { 4546 argType = arg._arsdTypeOf; 4547 arg = this._getUriRelativeToBase(arg._serverFunction, arg._serverArguments); 4548 4549 // this arg is a nested server call 4550 a += encodeURIComponent(i) + "="; 4551 a += encodeURIComponent(arg); 4552 } else if(arg && arg.length && typeof arg != "string") { 4553 // FIXME: are we sure this is actually an array? It might be an object with a length property... 4554 4555 var outputtedHere = false; 4556 for(var idx = 0; idx < arg.length; idx++) { 4557 if(outputtedHere) { 4558 a += "&"; 4559 } else outputtedHere = true; 4560 4561 // FIXME: ought to be recursive 4562 a += encodeURIComponent(i) + "="; 4563 a += encodeURIComponent(arg[idx]); 4564 } 4565 } else { 4566 // a regular argument 4567 a += encodeURIComponent(i) + "="; 4568 a += encodeURIComponent(arg); 4569 } 4570 // else if: handle arrays and objects too 4571 4572 if(argType.length > 0) { 4573 a += "&"; 4574 a += encodeURIComponent(i + "-type") + "="; 4575 a += encodeURIComponent(argType); 4576 } 4577 } 4578 4579 return a; 4580 }, 4581 4582 "_onError":function(error) { 4583 throw error; 4584 }, 4585 4586 /// returns an object that can be used to get the actual response from the server 4587 "_serverCall": function (name, passedArgs, returnType) { 4588 var me = this; // this is the Api object 4589 var args; 4590 // FIXME: is there some way to tell arguments apart from other objects? dynamic languages suck. 4591 if(!passedArgs.length) 4592 args = passedArgs; 4593 else { 4594 args = new Object(); 4595 for(var a = 0; a < passedArgs.length; a++) 4596 args["positional-arg-" + a] = passedArgs[a]; 4597 } 4598 return { 4599 // type info metadata 4600 "_arsdTypeOf":"ServerResult", 4601 "_staticType":(typeof returnType == "undefined" ? null : returnType), 4602 4603 // Info about the thing 4604 "_serverFunction":name, 4605 "_serverArguments":args, 4606 "_moreArguments":{}, 4607 "_methodOverride":null, 4608 4609 // lower level implementation 4610 "_get":function(callback, onError, async) { 4611 var resObj = this; // the request/response object. var me is the ApiObject. 4612 if(args == null) 4613 args = {}; 4614 if(!args.format) 4615 args.format = "json"; 4616 args.envelopeFormat = "json"; 4617 4618 for(i in this._moreArguments) 4619 args[i] = this._moreArguments[i]; 4620 4621 return me._doRequest(me._apiBase + name, args, function(t, xml) { 4622 /* 4623 if(me._debugMode) { 4624 try { 4625 var obj = eval("(" + t + ")"); 4626 } catch(e) { 4627 alert("Bad server json: " + e + 4628 "\nOn page: " + (me._apiBase + name) + 4629 "\nGot:\n" + t); 4630 } 4631 } else { 4632 */ 4633 var obj; 4634 if(JSON && JSON.parse) 4635 obj = JSON.parse(t); 4636 else 4637 obj = eval("(" + t + ")"); 4638 //} 4639 4640 var returnValue; 4641 4642 if(obj.success) { 4643 if(typeof callback == "function") 4644 callback(obj.result); 4645 else if(typeof resObj.onSuccess == "function") { 4646 resObj.onSuccess(obj.result); 4647 } else if(typeof me.onSuccess == "function") { // do we really want this? 4648 me.onSuccess(obj.result); 4649 } else { 4650 // can we automatically handle it? 4651 // If it's an element, we should replace innerHTML by ID if possible 4652 // if a callback is given and it's a string, that's an id. Return type of element 4653 // should replace that id. return type of string should be appended 4654 // FIXME: meh just do something here. 4655 } 4656 4657 returnValue = obj.result; 4658 } else { 4659 // how should we handle the error? I guess throwing is better than nothing 4660 // but should there be an error callback too? 4661 var error = new Error(obj.errorMessage); 4662 error.name = obj.type; 4663 error.functionUrl = me._apiBase + name; 4664 error.functionArgs = args; 4665 error.errorMessage = obj.errorMessage; 4666 4667 // myFunction.caller should be available and checked too 4668 // btw arguments.callee is like this for functions 4669 4670 if(me._debugMode) { 4671 var ourMessage = obj.type + ": " + obj.errorMessage + 4672 "\nOn: " + me._apiBase + name; 4673 if(args.toSource) 4674 ourMessage += args.toSource(); 4675 if(args.stack) 4676 ourMessage += "\n" + args.stack; 4677 4678 error.message = ourMessage; 4679 4680 // alert(ourMessage); 4681 } 4682 4683 if(onError) // local override first... 4684 returnValue = onError(error); 4685 else if(resObj.onError) // then this object 4686 returnValue = resObj.onError(error); 4687 else if(me._onError) // then the global object 4688 returnValue = me._onError(error); 4689 else 4690 throw error; // if all else fails... 4691 } 4692 4693 if(typeof resObj.onComplete == "function") { 4694 resObj.onComplete(); 4695 } 4696 4697 if(typeof me._onComplete == "function") { 4698 me._onComplete(resObj); 4699 } 4700 4701 return returnValue; 4702 4703 // assert(0); // not reached 4704 }, this._methodOverride === null ? ((name.indexOf("get") == 0) ? "GET" : "POST") : this._methodOverride, async); // FIXME: hack: naming convention used to figure out method to use 4705 }, 4706 4707 // should pop open the thing in HTML format 4708 // "popup":null, // FIXME not implemented 4709 4710 "onError":null, // null means call the global one 4711 4712 "onSuccess":null, // a generic callback. generally pass something to get instead. 4713 4714 "formatSet":false, // is the format overridden? 4715 4716 // gets the result. Works automatically if you don't pass a callback. 4717 // You can also curry arguments to your callback by listing them here. The 4718 // result is put on the end of the arg list to the callback 4719 "get":function(callbackObj) { 4720 var callback = null; 4721 var errorCb = null; 4722 var callbackThis = null; 4723 if(callbackObj) { 4724 if(typeof callbackObj == "function") 4725 callback = callbackObj; 4726 else { 4727 if(callbackObj.length) { 4728 // array 4729 callback = callbackObj[0]; 4730 4731 if(callbackObj.length >= 2) 4732 errorCb = callbackObj[1]; 4733 } else { 4734 if(callbackObj.onSuccess) 4735 callback = callbackObj.onSuccess; 4736 if(callbackObj.onError) 4737 errorCb = callbackObj.onError; 4738 if(callbackObj.self) 4739 callbackThis = callbackObj.self; 4740 else 4741 callbackThis = callbackObj; 4742 } 4743 } 4744 } 4745 if(arguments.length > 1) { 4746 var ourArguments = []; 4747 for(var a = 1; a < arguments.length; a++) 4748 ourArguments.push(arguments[a]); 4749 4750 function cb(obj, xml) { 4751 ourArguments.push(obj); 4752 ourArguments.push(xml); 4753 4754 // that null is the this object inside the function... can 4755 // we make that work? 4756 return callback.apply(callbackThis, ourArguments); 4757 } 4758 4759 function cberr(err) { 4760 ourArguments.unshift(err); 4761 4762 // that null is the this object inside the function... can 4763 // we make that work? 4764 return errorCb.apply(callbackThis, ourArguments); 4765 } 4766 4767 4768 this._get(cb, errorCb ? cberr : null, true); 4769 } else { 4770 this._get(callback, errorCb, true); 4771 } 4772 }, 4773 4774 // If you need a particular format, use this. 4775 "getFormat":function(format /* , same args as get... */) { 4776 this.format(format); 4777 var forwardedArgs = []; 4778 for(var a = 1; a < arguments.length; a++) 4779 forwardedArgs.push(arguments[a]); 4780 this.get.apply(this, forwardedArgs); 4781 }, 4782 4783 // sets the format of the request so normal get uses it 4784 // myapi.someFunction().format('table').get(...); 4785 // see also: getFormat and getHtml 4786 // the secondaryFormat only makes sense if format is json. It 4787 // sets the format returned by object.toString() in the returned objects. 4788 "format":function(format, secondaryFormat) { 4789 if(args == null) 4790 args = {}; 4791 args.format = format; 4792 4793 if(typeof secondaryFormat == "string" && secondaryFormat) { 4794 if(format != "json") 4795 me._raiseError("AssertError", "secondaryFormat only works if format == json"); 4796 args.secondaryFormat = secondaryFormat; 4797 } 4798 4799 this.formatSet = true; 4800 return this; 4801 }, 4802 4803 "getHtml":function(/* args to get... */) { 4804 this.format("html"); 4805 this.get.apply(this, arguments); 4806 }, 4807 4808 // FIXME: add post aliases 4809 4810 // don't use unless you're deploying to localhost or something 4811 "getSync":function() { 4812 function cb(obj) { 4813 // no nothing, we're returning the value below 4814 } 4815 4816 return this._get(cb, null, false); 4817 }, 4818 // takes the result and appends it as html to the given element 4819 4820 // FIXME: have a type override 4821 "appendTo":function(what) { 4822 if(!this.formatSet) 4823 this.format("html"); 4824 this.get(me._appendContent(what)); 4825 }, 4826 // use it to replace the content of the given element 4827 "useToReplace":function(what) { 4828 if(!this.formatSet) 4829 this.format("html"); 4830 this.get(me._replaceContent(what)); 4831 }, 4832 // use to replace the given element altogether 4833 "useToReplaceElement":function(what) { 4834 if(!this.formatSet) 4835 this.format("html"); 4836 this.get(me._replaceElement(what)); 4837 }, 4838 "useToFillForm":function(what) { 4839 this.get(me._fillForm(what)); 4840 }, 4841 "setValue":function(key, value) { 4842 this._moreArguments[key] = value; 4843 return this; 4844 }, 4845 "setMethod":function(method) { 4846 this._methodOverride = method; 4847 return this; 4848 } 4849 // runAsScript has been removed, use get(eval) instead 4850 // FIXME: might be nice to have an automatic popin function too 4851 }; 4852 }, 4853 4854 "_fillForm": function(what) { 4855 var e = this._getElement(what); 4856 if(this._isListOfNodes(e)) 4857 alert("FIXME: list of forms not implemented"); 4858 else return function(obj) { 4859 if(e.elements && typeof obj == "object") { 4860 for(i in obj) 4861 if(e.elements[i]) 4862 e.elements[i].value = obj[i]; // FIXME: what about checkboxes, selects, etc? 4863 } else 4864 throw new Error("unsupported response"); 4865 }; 4866 }, 4867 4868 "_getElement": function(what) { 4869 // FIXME: what about jQuery users? If they do useToReplace($("whatever")), we ought to do what we can with it for the most seamless experience even if I loathe that bloat. 4870 // though I guess they should be ok in doing $("whatever")[0] or maybe $("whatever").get() so not too awful really. 4871 var e; 4872 if(typeof what == "string") 4873 e = document.getElementById(what); 4874 else 4875 e = what; 4876 4877 return e; 4878 }, 4879 4880 "_isListOfNodes": function(what) { 4881 // length is on both arrays and lists, but some elements 4882 // have it too. We disambiguate with getAttribute 4883 return (what && (what.length && !what.getAttribute)) 4884 }, 4885 4886 // These are some convenience functions to use as callbacks 4887 "_replaceContent": function(what) { 4888 var e = this._getElement(what); 4889 var me = this; 4890 if(this._isListOfNodes(e)) 4891 return function(obj) { 4892 // I do not want scripts accidentally running here... 4893 for(var a = 0; a < e.length; a++) { 4894 if( (e[a].tagName.toLowerCase() == "input" 4895 && 4896 e[a].getAttribute("type") == "text") 4897 || 4898 e[a].tagName.toLowerCase() == "textarea") 4899 { 4900 e[a].value = obj; 4901 } else 4902 e[a].innerHTML = obj; 4903 } 4904 } 4905 else 4906 return function(obj) { 4907 var data = me._extractHtmlScript(obj); 4908 if( (e.tagName.toLowerCase() == "input" 4909 && 4910 e.getAttribute("type") == "text") 4911 || 4912 e.tagName.toLowerCase() == "textarea") 4913 { 4914 e.value = obj; // might want script looking thing as a value 4915 } else 4916 e.innerHTML = data[0]; 4917 if(me._wantScriptExecution && data[1].length) 4918 eval(data[1]); 4919 } 4920 }, 4921 4922 // note: what must be only a single element, FIXME: could check the static type 4923 "_replaceElement": function(what) { 4924 var e = this._getElement(what); 4925 if(this._isListOfNodes(e)) 4926 throw new Error("Can only replace individual elements since removal from a list may be unstable."); 4927 var me = this; 4928 return function(obj) { 4929 var data = me._extractHtmlScript(obj); 4930 var n = document.createElement("div"); 4931 n.innerHTML = data[0]; 4932 4933 if(n.firstChild) { 4934 e.parentNode.replaceChild(n.firstChild, e); 4935 } else { 4936 e.parentNode.removeChild(e); 4937 } 4938 if(me._wantScriptExecution && data[1].length) 4939 eval(data[1]); 4940 } 4941 }, 4942 4943 "_appendContent": function(what) { 4944 var e = this._getElement(what); 4945 var me = this; 4946 if(this._isListOfNodes(e)) // FIXME: repeating myself... 4947 return function(obj) { 4948 var data = me._extractHtmlScript(obj); 4949 for(var a = 0; a < e.length; a++) 4950 e[a].innerHTML += data[0]; 4951 if(me._wantScriptExecution && data[1].length) 4952 eval(data[1]); 4953 } 4954 else 4955 return function(obj) { 4956 var data = me._extractHtmlScript(obj); 4957 e.innerHTML += data[0]; 4958 if(me._wantScriptExecution && data[1].length) 4959 eval(data[1]); 4960 } 4961 }, 4962 4963 "_extractHtmlScript": function(response) { 4964 var scriptRegex = new RegExp("<script>([\\s\\S]*?)<\\/script>", "g"); 4965 var scripts = ""; 4966 var match; 4967 while(match = scriptRegex.exec(response)) { 4968 scripts += match[1]; 4969 } 4970 var html = response.replace(scriptRegex, ""); 4971 4972 return [html, scripts]; 4973 }, 4974 4975 // we say yes by default because these always come from your own domain anyway; 4976 // it should be safe (as long as your app is sane). You can turn it off though if you want 4977 // by setting this to false somewhere in your code. 4978 "_wantScriptExecution" : true, 4979 }; 4980 4981 4982 template hasAnnotation(alias f, Attr) { 4983 static bool helper() { 4984 foreach(attr; __traits(getAttributes, f)) 4985 static if(is(attr == Attr) || is(typeof(attr) == Attr)) 4986 return true; 4987 return false; 4988 4989 } 4990 enum bool hasAnnotation = helper; 4991 } 4992 4993 template hasValueAnnotation(alias f, Attr) { 4994 static bool helper() { 4995 foreach(attr; __traits(getAttributes, f)) 4996 static if(is(typeof(attr) == Attr)) 4997 return true; 4998 return false; 4999 5000 } 5001 enum bool hasValueAnnotation = helper; 5002 } 5003 5004 5005 5006 template getAnnotation(alias f, Attr) if(hasValueAnnotation!(f, Attr)) { 5007 static auto helper() { 5008 foreach(attr; __traits(getAttributes, f)) 5009 static if(is(typeof(attr) == Attr)) 5010 return attr; 5011 assert(0); 5012 } 5013 5014 enum getAnnotation = helper; 5015 } 5016 5017 // use this as a query string param to all forever-cached resources 5018 string makeCompileTimestamp(string ts) { 5019 string ret; 5020 foreach(t; ts) 5021 if((t >= '0' && t <= '9')) 5022 ret ~= t; 5023 return ret; 5024 } 5025 5026 enum compiliationStamp = makeCompileTimestamp(__TIMESTAMP__); 5027 5028 /* 5029 Copyright: Adam D. Ruppe, 2010 - 2012 5030 License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>. 5031 Authors: Adam D. Ruppe, with contributions by Nick Sabalausky 5032 5033 Copyright Adam D. Ruppe 2010-2012. 5034 Distributed under the Boost Software License, Version 1.0. 5035 (See accompanying file LICENSE_1_0.txt or copy at 5036 http://www.boost.org/LICENSE_1_0.txt) 5037 */