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