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