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