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