module arsd.web; /* Running from the command line: ./myapp function positional args.... ./myapp --format=json function _GET _POST _PUT _DELETE ./myapp --make-nested-call Procedural vs Object Oriented right now it is procedural: root/function root/module/function what about an object approach: root/object root/class/object static ApiProvider.getObject Formatting data: CoolApi.myFunc().getFormat('Element', [...same as get...]); You should also be able to ask for json, but with a particular format available as toString format("json", "html") -- gets json, but each object has it's own toString. Actually, the object adds a member called formattedSecondarily that is the other thing. Note: the array itself cannot be changed in format, only it's members. Note: the literal string of the formatted object is often returned. This may more than double the bandwidth of the call Note: BUG: it only works with built in formats right now when doing secondary // formats are: text, html, json, table, and xml // except json, they are all represented as strings in json values string toString -> formatting as text Element makeHtmlElement -> making it html (same as fragment) JSONValue makeJsonValue -> formatting to json Table makeHtmlTable -> making a table (not implemented) toXml -> making it into an xml document Arrays can be handled too: static (converts to) string makeHtmlArray(typeof(this)[] arr); Envelope format: document (default), json, none */ public import arsd.dom; public import arsd.cgi; // you have to import this in the actual usage file or else it won't link; surely a compiler bug import arsd.sha; public import std.string; public import std.array; public import std.stdio : writefln; public import std.conv; import std.random; public import std.range; public import std.traits; import std.json; struct Envelope { bool success; string type; string errorMessage; string userData; JSONValue result; // use result.str if the format was anything other than json debug string dFullString; } string linkTo(alias func, T...)(T args) { auto reflection = __traits(parent, func).reflection; assert(reflection !is null); auto name = func.stringof; int idx = name.indexOf("("); if(idx != -1) name = name[0 .. idx]; auto funinfo = reflection.functions[name]; return funinfo.originalName; } /// Everything should derive from this instead of the old struct namespace used before /// Your class must provide a default constructor. class ApiProvider { Cgi cgi; static immutable(ReflectionInfo)* reflection; string _baseUrl; // filled based on where this is called from on this request /// Override this if you have initialization work that must be done *after* cgi and reflection is ready. /// It should be used instead of the constructor for most work. void _initialize() {} /// This one is called at least once per call. (_initialize is only called once per process) void _initializePerCall() {} /// Override this if you want to do something special to the document void _postProcess(Document document) {} /// This tentatively redirects the user - depends on the envelope fomat void redirect(string location) { if(cgi.request("envelopeFormat", "document") == "document") cgi.setResponseLocation(location, false); } Element _sitemap() { auto container = _getGenericContainer(); auto list = container.addChild("ul"); string[string] handled; foreach(func; reflection.functions) { if(func.originalName in handled) continue; handled[func.originalName] = func.originalName; list.addChild("li", new Link(_baseUrl ~ "/" ~ func.name, beautify(func.originalName))); } return list.parentNode.removeChild(list); } Document _defaultPage() { throw new Exception("no default"); return null; } Element _getGenericContainer() out(ret) { assert(ret !is null); } body { auto document = new Document(""); auto container = document.getElementById("body"); return container; } /// When in website mode, you can use this to beautify the error message Document delegate(Throwable) _errorFunction; } class ApiObject { /* abstract this(ApiProvider parent, string identifier) */ } struct ReflectionInfo { FunctionInfo[string] functions; EnumInfo[string] enums; StructInfo[string] structs; const(ReflectionInfo)*[string] objects; bool needsInstantiation; // the overall namespace string name; // this is also used as the object name in the JS api string defaultOutputFormat = "html"; int versionOfOutputFormat = 2; // change this in your constructor if you still need the (deprecated) old behavior // bool apiMode = false; // no longer used - if format is json, apiMode behavior is assumed. if format is html, it is not. // FIXME: what if you want the data formatted server side, but still in a json envelope? // should add format-payload: } struct EnumInfo { string name; int[] values; string[] names; } struct StructInfo { string name; // a struct is sort of like a function constructor... StructMemberInfo[] members; } struct StructMemberInfo { string name; string staticType; string defaultValue; } struct FunctionInfo { WrapperFunction dispatcher; JSONValue delegate(Cgi cgi, in string[][string] sargs) documentDispatcher; // should I also offer dispatchers for other formats like Variant[]? string name; string originalName; //string uriPath; Parameter[] parameters; string returnType; bool returnTypeIsDocument; Document function(in string[string] args) createForm; } struct Parameter { string name; string value; string type; string staticType; string validator; // for radio and select boxes string[] options; string[] optionValues; } string makeJavascriptApi(const ReflectionInfo* mod, string base) { assert(mod !is null); string script = `var `~mod.name~` = { "_apiBase":'`~base~`',`; script ~= javascriptBase; script ~= "\n\t"; bool[string] alreadyDone; bool outp = false; foreach(s; mod.enums) { if(outp) script ~= ",\n\t"; else outp = true; script ~= "'"~s.name~"': {\n"; bool outp2 = false; foreach(i, n; s.names) { if(outp2) script ~= ",\n"; else outp2 = true; // auto v = s.values[i]; auto v = "'" ~ n ~ "'"; // we actually want to use the name here because to!enum() uses member name. script ~= "\t\t'"~n~"':" ~ to!string(v); } script ~= "\n\t}"; } foreach(s; mod.structs) { if(outp) script ~= ",\n\t"; else outp = true; script ~= "'"~s.name~"': function("; bool outp2 = false; foreach(n; s.members) { if(outp2) script ~= ", "; else outp2 = true; script ~= n.name; } script ~= ") { return {\n"; outp2 = false; script ~= "\t\t'_arsdTypeOf':'"~s.name~"'"; if(s.members.length) script ~= ","; script ~= " // metadata, ought to be read only\n"; // outp2 is still false because I put the comma above foreach(n; s.members) { if(outp2) script ~= ",\n"; else outp2 = true; auto v = n.defaultValue; script ~= "\t\t'"~n.name~"': (typeof "~n.name~" == 'undefined') ? "~n.name~" : '" ~ to!string(v) ~ "'"; } script ~= "\n\t}; }"; } // FIXME: it should output the classes too /* foreach(obj; mod.objects) { if(outp) script ~= ",\n\t"; else outp = true; script ~= makeJavascriptApi(obj, base); } */ foreach(func; mod.functions) { if(func.originalName in alreadyDone) continue; // there's url friendly and code friendly, only need one alreadyDone[func.originalName] = true; if(outp) script ~= ",\n\t"; else outp = true; string args; string obj; bool outputted = false; foreach(i, arg; func.parameters) { if(outputted) { args ~= ","; obj ~= ","; } else outputted = true; args ~= arg.name; // FIXME: we could probably do better checks here too like on type 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~`)`; } /* if(outputted) args ~= ","; args ~= "callback"; */ script ~= `'` ~ func.originalName ~ `'`; script ~= ":"; script ~= `function(`~args~`) {`; if(obj.length) script ~= ` var argumentsObject = { `~obj~` }; return this._serverCall('`~func.name~`', argumentsObject, '`~func.returnType~`');`; else script ~= ` return this._serverCall('`~func.name~`', null, '`~func.returnType~`');`; script ~= ` }`; } script ~= "\n}"; // some global stuff to put in script ~= ` if(typeof arsdGlobalStuffLoadedForWebDotD == "undefined") { arsdGlobalStuffLoadedForWebDotD = true; var oldObjectDotPrototypeDotToString = Object.prototype.toString; Object.prototype.toString = function() { if(this.formattedSecondarily) return this.formattedSecondarily; return oldObjectDotPrototypeDotToString.call(this); } } `; return script; } template isEnum(alias T) if(is(T)) { static if (is(T == enum)) enum bool isEnum = true; else enum bool isEnum = false; } // WTF, shouldn't is(T == xxx) already do this? template isEnum(T) if(!is(T)) { enum bool isEnum = false; } template isStruct(alias T) if(is(T)) { static if (is(T == struct)) enum bool isStruct = true; else enum bool isStruct = false; } // WTF template isStruct(T) if(!is(T)) { enum bool isStruct = false; } template isApiObject(alias T) if(is(T)) { static if (is(T : ApiObject)) enum bool isApiObject = true; else enum bool isApiObject = false; } // WTF template isApiObject(T) if(!is(T)) { enum bool isApiObject = false; } template isApiProvider(alias T) if(is(T)) { static if (is(T : ApiProvider)) enum bool isApiProvider = true; else enum bool isApiProvider = false; } // WTF template isApiProvider(T) if(!is(T)) { enum bool isApiProvider = false; } template Passthrough(T) { T Passthrough; } template PassthroughType(T) { alias T PassthroughType; } auto generateGetter(PM, Parent, string member, alias hackToEnsureMultipleFunctionsWithTheSameSignatureGetTheirOwnInstantiations)(string io, Parent instantiation) { static if(is(PM : ApiObject)) { auto i = new PM(instantiation, io); return &__traits(getMember, i, member); } else { return &__traits(getMember, instantiation, member); } } immutable(ReflectionInfo*) prepareReflection(alias PM)(Cgi cgi, PM instantiation, ApiObject delegate(string) instantiateObject = null) if(is(PM : ApiProvider) || is(PM: ApiObject) ) { return prepareReflectionImpl!(PM, PM)(cgi, instantiation, instantiateObject); } immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Cgi cgi, Parent instantiation, ApiObject delegate(string) instantiateObject = null) if((is(PM : ApiProvider) || is(PM: ApiObject)) && is(Parent : ApiProvider) ) { assert(instantiation !is null); ReflectionInfo* reflection = new ReflectionInfo; reflection.name = PM.stringof; static if(is(PM: ApiObject)) reflection.needsInstantiation = true; // derivedMembers is changed from allMembers foreach(member; __traits(derivedMembers, PM)) { // FIXME: the filthiest of all hacks... static if(!__traits(compiles, !is(typeof(__traits(getMember, PM, member)) == function) && isEnum!(__traits(getMember, PM, member)))) continue; // must be a data member or something... else // DONE WITH FILTHIEST OF ALL HACKS //if(member.length == 0) // continue; static if( !is(typeof(__traits(getMember, PM, member)) == function) && isEnum!(__traits(getMember, PM, member)) && member[0] != '_' ) { EnumInfo i; i.name = member; foreach(m; __traits(allMembers, __traits(getMember, PM, member))) { i.names ~= m; i.values ~= cast(int) __traits(getMember, __traits(getMember, PM, member), m); } reflection.enums[member] = i; } else static if( !is(typeof(__traits(getMember, PM, member)) == function) && isStruct!(__traits(getMember, PM, member)) && member[0] != '_' ) { StructInfo i; i.name = member; typeof(Passthrough!(__traits(getMember, PM, member))) s; foreach(idx, m; s.tupleof) { StructMemberInfo mem; mem.name = s.tupleof[idx].stringof[2..$]; mem.staticType = typeof(m).stringof; mem.defaultValue = null; // FIXME i.members ~= mem; } reflection.structs[member] = i; } else static if( is(typeof(__traits(getMember, PM, member)) == function) && ( member[0] != '_' && ( member.length < 5 || ( member[$ - 5 .. $] != "_Page" && member[$ - 5 .. $] != "_Form") && !(member.length > 16 && member[$ - 16 .. $] == "_PermissionCheck") ))) { FunctionInfo f; ParameterTypeTuple!(__traits(getMember, PM, member)) fargs; f.returnType = ReturnType!(__traits(getMember, PM, member)).stringof; f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, PM, member)) : Document); f.name = toUrlName(member); f.originalName = member; assert(instantiation !is null); f.dispatcher = generateWrapper!( generateGetter!(PM, Parent, member, __traits(getMember, PM, member)), __traits(getMember, PM, member), Parent, member )(reflection, instantiation); //f.uriPath = f.originalName; auto names = parameterNamesOf!(__traits(getMember, PM, member)); foreach(idx, param; fargs) { Parameter p; p.name = names[idx]; p.staticType = typeof(fargs[idx]).stringof; static if( is( typeof(param) == enum )) { p.type = "select"; foreach(opt; __traits(allMembers, typeof(param))) { p.options ~= opt; p.optionValues ~= to!string(__traits(getMember, param, opt)); } } else static if (is(typeof(param) == bool)) { p.type = "checkbox"; } else { if(p.name.toLower.indexOf("password") != -1) // hack to support common naming convention p.type = "password"; else p.type = "text"; } f.parameters ~= p; } static if(__traits(hasMember, PM, member ~ "_Form")) { f.createForm = &__traits(getMember, PM, member ~ "_Form"); } reflection.functions[f.name] = f; // also offer the original name if it doesn't // conflict //if(f.originalName !in reflection.functions) reflection.functions[f.originalName] = f; } else static if( !is(typeof(__traits(getMember, PM, member)) == function) && isApiObject!(__traits(getMember, PM, member)) && member[0] != '_' ) { reflection.objects[member] = prepareReflectionImpl!( __traits(getMember, PM, member), Parent) (cgi, instantiation); } else static if( // child ApiProviders are like child modules !is(typeof(__traits(getMember, PM, member)) == function) && isApiProvider!(__traits(getMember, PM, member)) && member[0] != '_' ) { PassthroughType!(__traits(getMember, PM, member)) i; i = new typeof(i)(); auto r = prepareReflection!(__traits(getMember, PM, member))(cgi, i); reflection.objects[member] = r; if(toLower(member) !in reflection.objects) // web filenames are often lowercase too reflection.objects[member.toLower] = r; } } static if(is(PM: ApiProvider)) { instantiation.cgi = cgi; instantiation.reflection = cast(immutable) reflection; instantiation._initialize(); } return cast(immutable) reflection; } void run(Provider)(Cgi cgi, Provider instantiation, int pathInfoStartingPoint = 0) if(is(Provider : ApiProvider)) { assert(instantiation !is null); immutable(ReflectionInfo)* reflection; if(instantiation.reflection is null) prepareReflection!(Provider)(cgi, instantiation); reflection = instantiation.reflection; instantiation._baseUrl = cgi.scriptName ~ cgi.pathInfo[0 .. pathInfoStartingPoint]; if(cgi.pathInfo[pathInfoStartingPoint .. $].length <= 1) { auto document = instantiation._defaultPage(); if(document !is null) { instantiation._postProcess(document); cgi.write(document.toString()); } cgi.close(); return; } string funName = cgi.pathInfo[pathInfoStartingPoint + 1..$]; // kinda a hack, but this kind of thing should be available anyway if(funName == "functions.js") { cgi.setResponseContentType("text/javascript"); cgi.write(makeJavascriptApi(reflection, replace(cast(string) cgi.requestUri, "functions.js", ""))); cgi.close(); return; } instantiation._initializePerCall(); // what about some built in functions? /* // Basic integer operations builtin.opAdd builtin.opSub builtin.opMul builtin.opDiv // Basic array operations builtin.opConcat // use to combine calls easily builtin.opIndex builtin.opSlice builtin.length // Basic floating point operations builtin.round builtin.floor builtin.ceil // Basic object operations builtin.getMember // Basic functional operations builtin.filter // use to slice down on stuff to transfer builtin.map // call a server function on a whole array builtin.reduce // Access to the html items builtin.getAutomaticForm(method) */ const(FunctionInfo)* fun; auto envelopeFormat = cgi.request("envelopeFormat", "document"); Envelope result; result.userData = cgi.request("passedThroughUserData"); string instantiator; string objectName; try { // Built-ins string errorMessage; if(funName.length > 8 && funName[0..8] == "builtin.") { funName = funName[8..$]; switch(funName) { default: assert(0); case "getAutomaticForm": auto mfun = new FunctionInfo; mfun.returnType = "Form"; mfun.dispatcher = delegate JSONValue (Cgi cgi, string, in string[][string] sargs, in string format, in string secondaryFormat = null) { auto rfun = cgi.request("method") in reflection.functions; if(rfun is null) throw new NoSuchPageException("no such function " ~ cgi.request("method")); auto form = createAutomaticForm(new Document, *rfun); auto idx = cgi.requestUri.indexOf("builtin.getAutomaticForm"); form.action = cgi.requestUri[0 .. idx] ~ form.action; // make sure it works across the site JSONValue v; v.type = JSON_TYPE.STRING; v.str = form.toString(); return v; }; fun = cast(immutable) mfun; break; } } else { // User-defined // FIXME: modules? should be done with dots since slashes is used for api objects fun = funName in reflection.functions; if(fun is null) { auto parts = funName.split("/"); const(ReflectionInfo)* currentReflection = reflection; if(parts.length > 1) while(parts.length) { if(parts.length > 1) { objectName = parts[0]; auto object = objectName in reflection.objects; if(object is null) { // || object.instantiate is null) errorMessage = "no such object: " ~ objectName; goto noSuchFunction; } currentReflection = *object; if(!currentReflection.needsInstantiation) { parts = parts[1 .. $]; continue; } auto objectIdentifier = parts[1]; instantiator = objectIdentifier; //obj = object.instantiate(objectIdentifier); parts = parts[2 .. $]; if(parts.length == 0) { // gotta run the default function fun = (to!string(cgi.requestMethod)) in currentReflection.functions; } } else { fun = parts[0] in currentReflection.functions; if(fun is null) errorMessage = "no such method in class "~objectName~": " ~ parts[0]; parts = parts[1 .. $]; } } } } if(fun is null) { noSuchFunction: if(errorMessage.length) throw new NoSuchPageException(errorMessage); string allFuncs, allObjs; foreach(n, f; reflection.functions) allFuncs ~= n ~ "\n"; foreach(n, f; reflection.objects) allObjs ~= n ~ "\n"; throw new NoSuchPageException("no such function " ~ funName ~ "\n functions are:\n" ~ allFuncs ~ "\n\nObjects are:\n" ~ allObjs); } assert(fun !is null); assert(fun.dispatcher !is null); assert(cgi !is null); result.type = fun.returnType; string format = cgi.request("format", reflection.defaultOutputFormat); string secondaryFormat = cgi.request("secondaryFormat", ""); if(secondaryFormat.length == 0) secondaryFormat = null; JSONValue res; if(envelopeFormat == "document" && fun.documentDispatcher !is null) { res = fun.documentDispatcher(cgi, cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.postArray : cgi.getArray); envelopeFormat = "html"; } else res = fun.dispatcher(cgi, instantiator, cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.postArray : cgi.getArray, format, secondaryFormat); //if(cgi) // cgi.setResponseContentType("application/json"); result.success = true; result.result = res; } catch (Throwable e) { result.success = false; result.errorMessage = e.msg; result.type = e.classinfo.name; debug result.dFullString = e.toString(); if(envelopeFormat == "document" || envelopeFormat == "html") { auto ipe = cast(InsufficientParametersException) e; if(ipe !is null) { assert(fun !is null); Form form; if(0 || fun.createForm !is null) { // FIXME: if 0 // go ahead and use it to make the form page auto doc = fun.createForm(cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.post : cgi.get); } else { Parameter[] params = fun.parameters.dup; foreach(i, p; fun.parameters) { string value = ""; if(p.name in cgi.get) value = cgi.get[p.name]; if(p.name in cgi.post) value = cgi.post[p.name]; params[i].value = value; } form = createAutomaticForm(new Document, *fun);// params, beautify(fun.originalName)); foreach(k, v; cgi.get) form.setValue(k, v); form.setValue("envelopeFormat", envelopeFormat); auto n = form.getElementById("function-name"); if(n) n.innerText = beautify(fun.originalName); } assert(form !is null); result.result.str = form.toString(); } else { if(instantiation._errorFunction !is null) { auto document = instantiation._errorFunction(e); if(document is null) goto gotnull; result.result.str = (document.toString()); } else { gotnull: auto document = new Document; auto code = document.createElement("pre"); code.innerText = e.toString(); result.result.str = (code.toString()); } } } } finally { switch(envelopeFormat) { case "redirect": auto redirect = cgi.request("_arsd_redirect_location", cgi.referrer); // FIXME: is this safe? it'd make XSS super easy // add result to url if(!result.success) goto case "none"; cgi.setResponseLocation(redirect, false); break; case "json": // this makes firefox ugly //cgi.setResponseContentType("application/json"); auto json = toJsonValue(result); cgi.write(toJSON(&json)); break; case "none": cgi.setResponseContentType("text/plain"); if(result.success) { if(result.result.type == JSON_TYPE.STRING) { cgi.write(result.result.str); } else { cgi.write(toJSON(&result.result)); } } else { cgi.write(result.errorMessage); } break; case "document": case "html": default: cgi.setResponseContentType("text/html"); if(result.result.type == JSON_TYPE.STRING) { auto returned = result.result.str; if((fun !is null) && envelopeFormat != "html") { Document document; if(fun.returnTypeIsDocument) { // probably not super efficient... document = new TemplatedDocument(returned); } else { auto e = instantiation._getGenericContainer(); document = e.parentDocument; // FIXME: slow, esp if func return element e.innerHTML = returned; } if(envelopeFormat == "document") instantiation._postProcess(document); returned = document.toString; } cgi.write(returned); } else cgi.write(htmlEntitiesEncode(toJSON(&result.result))); break; } cgi.close(); } } mixin template FancyMain(T, Args...) { void fancyMainFunction(Cgi cgi) { //string[] args) { // auto cgi = new Cgi; // there must be a trailing slash for relative links.. if(cgi.pathInfo.length == 0) { cgi.setResponseLocation(cgi.requestUri ~ "/"); cgi.close(); return; } // FIXME: won't work for multiple objects T instantiation = new T(); auto reflection = prepareReflection!(T)(cgi, instantiation); run(cgi, instantiation); /+ if(args.length > 1) { string[string][] namedArgs; foreach(arg; args[2..$]) { auto lol = arg.indexOf("="); if(lol == -1) throw new Exception("use named args for all params"); //namedArgs[arg[0..lol]] = arg[lol+1..$]; // FIXME } if(!(args[1] in reflection.functions)) { throw new Exception("No such function"); } //writefln("%s", reflection.functions[args[1]].dispatcher(null, namedArgs, "string")); } else { +/ // } } mixin GenericMain!(fancyMainFunction, Args); } Form createAutomaticForm(Document document, in FunctionInfo func, string[string] fieldTypes = null) { return createAutomaticForm(document, func.name, func.parameters, beautify(func.originalName), "POST", fieldTypes); } Form createAutomaticForm(Document document, string action, in Parameter[] parameters, string submitText = "Submit", string method = "POST", string[string] fieldTypes = null) { assert(document !is null); auto form = cast(Form) document.createElement("form"); form.action = action; assert(form !is null); form.method = method; auto fieldset = document.createElement("fieldset"); auto legend = document.createElement("legend"); legend.innerText = submitText; fieldset.appendChild(legend); auto table = cast(Table) document.createElement("table"); assert(table !is null); form.appendChild(fieldset); fieldset.appendChild(table); table.appendChild(document.createElement("tbody")); static int count = 0; foreach(param; parameters) { Element input; string type = param.type; if(param.name in fieldTypes) type = fieldTypes[param.name]; if(type == "select") { input = document.createElement("select"); foreach(idx, opt; param.options) { auto option = document.createElement("option"); option.name = opt; option.value = param.optionValues[idx]; option.innerText = beautify(opt); if(option.value == param.value) option.selected = "selected"; input.appendChild(option); } input.name = param.name; } else if (type == "radio") { assert(0, "FIXME"); } else { if(type.startsWith("textarea")) { input = document.createElement("textarea"); input.name = param.name; input.innerText = param.value; auto idx = type.indexOf("-"); if(idx != -1) { idx++; input.rows = type[idx .. $]; } } else { input = document.createElement("input"); input.type = type; input.name = param.name; input.value = param.value; } } string n = param.name ~ "_auto-form-" ~ to!string(count); input.id = n; if(type == "hidden") { form.appendChild(input); } else { auto th = document.createElement("th"); auto label = document.createElement("label"); label.setAttribute("for", n); label.innerText = beautify(param.name) ~ ": "; th.appendChild(label); table.appendRow(th, input); } count++; }; auto fmt = document.createElement("select"); fmt.name = "format"; fmt.addChild("option", "html").setAttribute("value", "html"); fmt.addChild("option", "table").setAttribute("value", "table"); fmt.addChild("option", "json").setAttribute("value", "json"); fmt.addChild("option", "string").setAttribute("value", "string"); auto th = table.th(""); th.addChild("label", "Format:"); table.appendRow(th, fmt).className = "format-row"; auto submit = document.createElement("input"); submit.value = submitText; submit.type = "submit"; table.appendRow(Html(" "), submit); // form.setValue("format", reflection.defaultOutputFormat); return form; } /** * Returns the parameter names of the given function * * Params: * func = the function alias to get the parameter names of * * Returns: an array of strings containing the parameter names */ /+ string parameterNamesOf( alias fn )( ) { string fullName = typeof(&fn).stringof; int pos = fullName.lastIndexOf( ')' ); int end = pos; int count = 0; do { if ( fullName[pos] == ')' ) { count++; } else if ( fullName[pos] == '(' ) { count--; } pos--; } while ( count > 0 ); return fullName[pos+2..end]; } +/ template parameterNamesOf (alias func) { const parameterNamesOf = parameterNamesOfImpl!(func); } int indexOfNew(string s, char a) { foreach(i, c; s) if(c == a) return i; return -1; } /** * Returns the parameter names of the given function * * Params: * func = the function alias to get the parameter names of * * Returns: an array of strings containing the parameter names */ private string[] parameterNamesOfImpl (alias func) () { string funcStr = typeof(&func).stringof; auto start = funcStr.indexOfNew('('); auto end = funcStr.indexOfNew(')'); const firstPattern = ' '; const secondPattern = ','; funcStr = funcStr[start + 1 .. end]; if (funcStr == "") return null; funcStr ~= secondPattern; string token; string[] arr; foreach (c ; funcStr) { if (c != firstPattern && c != secondPattern) token ~= c; else { if (token) arr ~= token; token = null; } } if (arr.length == 1) return arr; string[] result; bool skip = false; foreach (str ; arr) { skip = !skip; if (skip) continue; result ~= str; } return result; } ///////////////////////////////// string toHtml(T)(T a) { string ret; static if(is(T : Document)) ret = a.toString(); else static if(isArray!(T)) { static if(__traits(compiles, typeof(T[0]).makeHtmlArray(a))) ret = to!string(typeof(T[0]).makeHtmlArray(a)); else foreach(v; a) ret ~= toHtml(v); } else static if(is(T : Element)) ret = a.toString(); else static if(__traits(compiles, a.makeHtmlElement().toString())) ret = a.makeHtmlElement().toString(); else static if(is(T == Html)) ret = a.source; else ret = htmlEntitiesEncode(std.array.replace(to!string(a), "\n", "
\n")); return ret; } string toJson(T)(T a) { auto v = toJsonValue(a); return toJSON(&v); } // FIXME: are the explicit instantiations of this necessary? JSONValue toJsonValue(T, R = ApiProvider)(T a, string formatToStringAs = null, R api = null) if(is(R : ApiProvider)) { JSONValue val; static if(is(T == JSONValue)) { val = a; } else static if(__traits(compiles, val = a.makeJsonValue())) { val = a.makeJsonValue(); // FIXME: free function to emulate UFCS? // FIXME: should we special case something like struct Html? } else static if(is(T : Element)) { if(a is null) { val.type = JSON_TYPE.NULL; } else { val.type = JSON_TYPE.STRING; val.str = a.toString(); } } else static if(isIntegral!(T)) { val.type = JSON_TYPE.INTEGER; val.integer = to!long(a); } else static if(isFloatingPoint!(T)) { val.type = JSON_TYPE.FLOAT; val.floating = to!real(a); static assert(0); } else static if(is(T == void*)) { val.type = JSON_TYPE.NULL; } else static if(isPointer!(T)) { if(a is null) { val.type = JSON_TYPE.NULL; } else { val = toJsonValue!(typeof(*a), R)(*a, formatToStringAs, api); } } else static if(is(T == bool)) { if(a == true) val.type = JSON_TYPE.TRUE; if(a == false) val.type = JSON_TYPE.FALSE; } else static if(isSomeString!(T)) { val.type = JSON_TYPE.STRING; val.str = to!string(a); } else static if(isAssociativeArray!(T)) { val.type = JSON_TYPE.OBJECT; foreach(k, v; a) { val.object[to!string(k)] = toJsonValue!(typeof(v), R)(v, formatToStringAs, api); } } else static if(isArray!(T)) { val.type = JSON_TYPE.ARRAY; val.array.length = a.length; foreach(i, v; a) { val.array[i] = toJsonValue!(typeof(v), R)(v, formatToStringAs, api); } } else static if(is(T == struct)) { // also can do all members of a struct... val.type = JSON_TYPE.OBJECT; foreach(i, member; a.tupleof) { string name = a.tupleof[i].stringof[2..$]; static if(a.tupleof[i].stringof[2] != '_') val.object[name] = toJsonValue!(typeof(member), R)(member, formatToStringAs, api); } // HACK: bug in dmd can give debug members in a non-debug build //static if(__traits(compiles, __traits(getMember, a, member))) } else { /* our catch all is to just do strings */ val.type = JSON_TYPE.STRING; val.str = to!string(a); // FIXME: handle enums } // don't want json because it could recurse if(val.type == JSON_TYPE.OBJECT && formatToStringAs !is null && formatToStringAs != "json") { JSONValue formatted; formatted.type = JSON_TYPE.STRING; formatAs!(T, R)(a, api, formatted, formatToStringAs, null /* only doing one level of special formatting */); assert(formatted.type == JSON_TYPE.STRING); val.object["formattedSecondarily"] = formatted; } return val; } /+ Document toXml(T)(T t) { auto xml = new Document; xml.parse(emptyTag(T.stringof), true, true); xml.prolog = `` ~ "\n"; xml.root = toXmlElement(xml, t); return xml; } Element toXmlElement(T)(Document document, T t) { Element val; static if(is(T == Document)) { val = t.root; //} else static if(__traits(compiles, a.makeJsonValue())) { // val = a.makeJsonValue(); } else static if(is(T : Element)) { if(t is null) { val = document.createElement("value"); val.innerText = "null"; val.setAttribute("isNull", "true"); } else val = t; } else static if(is(T == void*)) { val = document.createElement("value"); val.innerText = "null"; val.setAttribute("isNull", "true"); } else static if(isPointer!(T)) { if(t is null) { val = document.createElement("value"); val.innerText = "null"; val.setAttribute("isNull", "true"); } else { val = toXmlElement(document, *t); } } else static if(isAssociativeArray!(T)) { val = document.createElement("value"); foreach(k, v; t) { auto e = document.createElement(to!string(k)); e.appendChild(toXmlElement(document, v)); val.appendChild(e); } } else static if(isSomeString!(T)) { val = document.createTextNode(to!string(t)); } else static if(isArray!(T)) { val = document.createElement("array"); foreach(i, v; t) { auto e = document.createElement("item"); e.appendChild(toXmlElement(document, v)); val.appendChild(e); } } else static if(is(T == struct)) { // also can do all members of a struct... val = document.createElement(T.stringof); foreach(member; __traits(allMembers, T)) { if(member[0] == '_') continue; // FIXME: skip member functions auto e = document.createElement(member); e.appendChild(toXmlElement(document, __traits(getMember, t, member))); val.appendChild(e); } } else { /* our catch all is to just do strings */ val = document.createTextNode(to!string(t)); // FIXME: handle enums } return val; } +/ class InsufficientParametersException : Exception { this(string functionName, string msg) { super(functionName ~ ": " ~ msg); } } class InvalidParameterException : Exception { this(string param, string value, string expected) { super("bad param: " ~ param ~ ". got: " ~ value ~ ". Expected: " ~expected); } } void badParameter(alias T)(string expected = "") { throw new InvalidParameterException(T.stringof, T, expected); } class PermissionDeniedException : Exception { this(string msg) { super(msg); } } class NoSuchPageException : Exception { this(string msg) { super(msg); } } type fromUrlParam(type)(string[] ofInterest) { type ret; // Arrays in a query string are sent as the name repeating... static if(isArray!(type) && !isSomeString!(type)) { foreach(a; ofInterest) { ret ~= to!(ElementType!(type))(a); } } else static if(is(type : Element)) { auto doc = new Document(ofInterest[$-1], true, true); ret = doc.root; } /* else static if(is(type : struct)) { static assert(0, "struct not supported yet"); } */ else { // enum should be handled by this too ret = to!type(ofInterest[$-1]); } // FIXME: can we support classes? return ret; } WrapperFunction generateWrapper(alias getInstantiation, alias f, alias group, string funName, R)(ReflectionInfo* reflection, R api) if(is(R: ApiProvider)) { JSONValue wrapper(Cgi cgi, string instantiationIdentifier, in string[][string] sargs, in string format, in string secondaryFormat = null) { JSONValue returnValue; returnValue.type = JSON_TYPE.STRING; auto instantiation = getInstantiation(instantiationIdentifier, api); ParameterTypeTuple!(f) args; Throwable t; // the error we see // this permission check thing might be removed. It's just there so you can check before // doing the automatic form... but I think that would be better done some other way. static if(__traits(hasMember, group, funName ~ "_PermissionCheck")) { ParameterTypeTuple!(__traits(getMember, group, funName ~ "_PermissionCheck")) argsperm; foreach(i, type; ParameterTypeTuple!(__traits(getMember, group, funName ~ "_PermissionCheck"))) { string name = parameterNamesOf!(__traits(getMember, group, funName ~ "_PermissionCheck"))[i]; static if(is(type == bool)) { if(name in sargs) args[i] = true; else args[i] = false; } else { if(!(name in sargs)) { t = new InsufficientParametersException(funName, "arg " ~ name ~ " is not present for permission check"); goto maybeThrow; } argsperm[i] = to!type(sargs[name][$-1]); } } __traits(getMember, group, funName ~ "_PermissionCheck")(argsperm); } // done with arguably useless permission check // Actually calling the function foreach(i, type; ParameterTypeTuple!(f)) { string name = parameterNamesOf!(f)[i]; // We want to check the named argument first. If it's not there, // try the positional arguments string using = name; if(name !in sargs) using = "positional-arg-" ~ to!string(i); // FIXME: if it's a struct, we should do it's pieces independently here static if(is(type == bool)) { // bool is special cased because HTML checkboxes don't send anything if it isn't checked if(using in sargs) args[i] = true; // FIXME: should try looking at the value else args[i] = false; } else { if(using !in sargs) { throw new InsufficientParametersException(funName, "arg " ~ name ~ " is not present"); } // We now check the type reported by the client, if there is one // Right now, only one type is supported: ServerResult, which means // it's actually a nested function call string[] ofInterest = cast(string[]) sargs[using]; // I'm changing the reference, but not the underlying stuff, so this cast is ok if(using ~ "-type" in sargs) { string reportedType = sargs[using ~ "-type"][$-1]; if(reportedType == "ServerResult") { // FIXME: doesn't handle functions that return // compound types (structs, arrays, etc) ofInterest = null; string str = sargs[using][$-1]; int idx = str.indexOf("?"); string callingName, callingArguments; if(idx == -1) { callingName = str; } else { callingName = str[0..idx]; callingArguments = str[idx + 1 .. $]; } // find it in reflection ofInterest ~= reflection.functions[callingName]. dispatcher(cgi, null, decodeVariables(callingArguments), "string").str; } } args[i] = fromUrlParam!type(ofInterest); } } static if(!is(ReturnType!f == void)) ReturnType!(f) ret; else void* ret; static if(!is(ReturnType!f == void)) ret = instantiation(args); // version 1 didn't handle exceptions else instantiation(args); formatAs(ret, api, returnValue, format, secondaryFormat); done: return returnValue; } return &wrapper; } void formatAs(T, R)(T ret, R api, ref JSONValue returnValue, string format, string formatJsonToStringAs = null) if(is(R : ApiProvider)) { if(api !is null) { static if(__traits(compiles, api.customFormat(ret, format))) { auto customFormatted = api.customFormat(ret, format); if(customFormatted !is null) { returnValue.str = customFormatted; return; } } } switch(format) { case "html": // FIXME: should we actually post process here? /+ static if(is(typeof(ret) : Document)) { instantiation._postProcess(ret); return ret.toString(); break; } static if(__traits(hasMember, group, funName ~ "_Page")) { auto doc = __traits(getMember, group, funName ~ "_Page")(ret); instantiation._postProcess(doc); return doc.toString(); break; } +/ returnValue.str = toHtml(ret); break; case "string": static if(__traits(compiles, to!string(ret))) returnValue.str = to!string(ret); else goto badType; break; case "json": returnValue = toJsonValue!(typeof(ret), R)(ret, formatJsonToStringAs, api); break; case "table": auto document = new Document(""); static if(__traits(compiles, structToTable(document, ret))) returnValue.str = structToTable(document, ret).toString(); else goto badType; break; default: badType: throw new Exception("Couldn't get result as " ~ format); } } private string emptyTag(string rootName) { return ("<" ~ rootName ~ ">"); } alias JSONValue delegate(Cgi cgi, string, in string[][string] args, in string format, in string secondaryFormat = null) WrapperFunction; string urlToBeauty(string url) { string u = url.replace("/", ""); string ret; bool capitalize = true; foreach(c; u) { if(capitalize) { ret ~= ("" ~ c).toUpper; capitalize = false; } else { if(c == '-') { ret ~= " "; capitalize = true; } else ret ~= c; } } return ret; } string toUrlName(string name) { string res; foreach(c; name) { if(c >= 'a' && c <= 'z') res ~= c; else { res ~= '-'; if(c >= 'A' && c <= 'Z') res ~= c + 0x20; else res ~= c; } } return res; } string beautify(string name) { string n; n ~= toUpper(name[0..1]); dchar last; foreach(dchar c; name[1..$]) { if((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { if(last != ' ') n ~= " "; } if(c == '_') n ~= " "; else n ~= c; last = c; } return n; } import std.md5; import core.stdc.stdlib; import core.stdc.time; import std.file; string getSessionId(Cgi cgi) { static string token; // FIXME: should this actually be static? it seems wrong if(token is null) { if("_sess_id" in cgi.cookies) token = cgi.cookies["_sess_id"]; else { auto tmp = uniform(0, int.max); token = to!string(tmp); cgi.setCookie("_sess_id", token, /*60 * 8 * 1000*/ 0, "/", null, true); } } return getDigestString(cgi.remoteAddress ~ "\r\n" ~ cgi.userAgent ~ "\r\n" ~ token); } void setLoginCookie(Cgi cgi, string name, string value) { cgi.setCookie(name, value, 0, "/", null, true); } string htmlTemplateWithData(in string text, in string[string] vars) { assert(text !is null); string newText = text; if(vars !is null) foreach(k, v; vars) { //assert(k !is null); //assert(v !is null); newText = newText.replace("{$" ~ k ~ "}", htmlEntitiesEncode(v).replace("\n", "
")); } return newText; } string htmlTemplate(string filename, string[string] vars) { return htmlTemplateWithData(readText(filename), vars); } class TemplatedDocument : Document { const override string toString() { string s; if(vars !is null) s = htmlTemplateWithData(super.toString(), vars); else s = super.toString(); return s; } public: string[string] vars; this(string src) { super(); parse(src, true, true); } this() { } void delegate(TemplatedDocument)[] preToStringFilters; void delegate(ref string)[] postToStringFilters; } void writeDocument(Cgi cgi, TemplatedDocument document) { foreach(f; document.preToStringFilters) f(document); auto s = document.toString(); foreach(f; document.postToStringFilters) f(s); cgi.write(s); } /* Password helpers */ string makeSaltedPasswordHash(string userSuppliedPassword, string salt = null) { if(salt is null) salt = to!string(uniform(0, int.max)); return hashToString(SHA256(salt ~ userSuppliedPassword)) ~ ":" ~ salt; } bool checkPassword(string saltedPasswordHash, string userSuppliedPassword) { auto parts = saltedPasswordHash.split(":"); return makeSaltedPasswordHash(userSuppliedPassword, parts[1]) == saltedPasswordHash; } Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) if(isArray!(T) && !isAssociativeArray!(T)) { auto t = cast(Table) document.createElement("table"); t.border = "1"; static if(is(T == string[string][])) { string[string] allKeys; foreach(row; arr) { foreach(k; row.keys) allKeys[k] = k; } auto sortedKeys = allKeys.keys.sort; Element tr; auto thead = t.addChild("thead"); auto tbody = t.addChild("tbody"); tr = thead.addChild("tr"); foreach(key; sortedKeys) tr.addChild("th", key); bool odd = true; foreach(row; arr) { tr = tbody.addChild("tr"); foreach(k; sortedKeys) { tr.addChild("td", k in row ? row[k] : ""); } if(odd) tr.addClass("odd"); odd = !odd; } } else static if(is(typeof(T[0]) == struct)) { { auto thead = t.addChild("thead"); auto tr = thead.addChild("tr"); auto s = arr[0]; foreach(idx, member; s.tupleof) tr.addChild("th", s.tupleof[idx].stringof[2..$]); } bool odd = true; auto tbody = t.addChild("tbody"); foreach(s; arr) { auto tr = tbody.addChild("tr"); foreach(member; s.tupleof) { tr.addChild("td", to!string(member)); } if(odd) tr.addClass("odd"); odd = !odd; } } else static assert(0); return t; } // this one handles horizontal tables showing just one item Table structToTable(T)(Document document, T s, string[] fieldsToSkip = null) if(!isArray!(T) || isAssociativeArray!(T)) { static if(__traits(compiles, s.makeHtmlTable(document))) return s.makeHtmlTable(document); else { auto t = cast(Table) document.createElement("table"); static if(is(T == struct)) { main: foreach(i, member; s.tupleof) { string name = s.tupleof[i].stringof[2..$]; foreach(f; fieldsToSkip) if(name == f) continue main; string nameS = name.idup; name = ""; foreach(idx, c; nameS) { if(c >= 'A' && c <= 'Z') name ~= " " ~ c; else if(c == '_') name ~= " "; else name ~= c; } t.appendRow(t.th(name.capitalize), to!string(member)); } } else static if(is(T == string[string])) { foreach(k, v; s){ t.appendRow(t.th(k), v); } } else static assert(0); return t; } } debug string javascriptBase = ` // change this in your script to get fewer error popups "_debugMode":true,` ~ javascriptBaseImpl; else string javascriptBase = ` // change this in your script to get more details in errors "_debugMode":false,` ~ javascriptBaseImpl; enum string javascriptBaseImpl = q{ "_doRequest": function(url, args, callback, method, async) { var xmlHttp; try { xmlHttp=new XMLHttpRequest(); } catch (e) { try { xmlHttp=new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { xmlHttp=new ActiveXObject("Microsoft.XMLHTTP"); } } if(async) xmlHttp.onreadystatechange=function() { if(xmlHttp.readyState==4) { if(callback) { callback(xmlHttp.responseText, xmlHttp.responseXML); } } } var argString = this._getArgString(args); if(method == "GET" && url.indexOf("?") == -1) url = url + "?" + argString; xmlHttp.open(method, url, async); var a = ""; if(method == "POST") { xmlHttp.setRequestHeader("Content-type","application/x-www-form-urlencoded"); a = argString; } else { xmlHttp.setRequestHeader("Content-type", "text/plain"); } xmlHttp.send(a); if(!async && callback) { return callback(xmlHttp.responseText, xmlHttp.responseXML); } return xmlHttp; }, "_raiseError":function(type, message) { var error = new Error(message); error.name = type; throw error; }, "_getUriRelativeToBase":function(name, args) { var str = name; var argsStr = this._getArgString(args); if(argsStr.length) str += "?" + argsStr; return str; }, "_getArgString":function(args) { var a = ""; var outputted = false; var i; // wow Javascript sucks! god damned global loop variables for(i in args) { if(outputted) { a += "&"; } else outputted = true; var arg = args[i]; var argType = ""; // Make sure the types are all sane if(arg && arg._arsdTypeOf && arg._arsdTypeOf == "ServerResult") { argType = arg._arsdTypeOf; arg = this._getUriRelativeToBase(arg._serverFunction, arg._serverArguments); // this arg is a nested server call a += encodeURIComponent(i) + "="; a += encodeURIComponent(arg); } else if(arg && arg.length && typeof arg != "string") { // FIXME: are we sure this is actually an array? var outputtedHere = false; for(var idx = 0; idx < arg.length; idx++) { if(outputtedHere) { a += "&"; } else outputtedHere = true; // FIXME: ought to be recursive a += encodeURIComponent(i) + "="; a += encodeURIComponent(arg[idx]); } } else { // a regular argument a += encodeURIComponent(i) + "="; a += encodeURIComponent(arg); } // else if: handle arrays and objects too if(argType.length > 0) { a += "&"; a += encodeURIComponent(i + "-type") + "="; a += encodeURIComponent(argType); } } return a; }, "_onError":function(error) { throw error; }, /// returns an object that can be used to get the actual response from the server "_serverCall": function (name, passedArgs, returnType) { var me = this; // this is the Api object var args = passedArgs; return { // type info metadata "_arsdTypeOf":"ServerResult", "_staticType":(typeof returnType == "undefined" ? null : returnType), // Info about the thing "_serverFunction":name, "_serverArguments":args, // lower level implementation "_get":function(callback, onError, async) { var resObj = this; if(args == null) args = {}; if(!args.format) args.format = "json"; args.envelopeFormat = "json"; return me._doRequest(me._apiBase + name, args, function(t, xml) { if(me._debugMode) { try { var obj = eval("(" + t + ")"); } catch(e) { alert("Bad server json: " + e + "\nOn page: " + (me._apiBase + name) + "\nGot:\n" + t); } } else { var obj = eval("(" + t + ")"); } if(obj.success) { if(typeof callback == "function") callback(obj.result); else if(typeof resObj.onSuccess == "function") { resObj.onSuccess(obj.result); } else if(typeof me.onSuccess == "function") { // do we really want this? me.onSuccess(obj.result); } else { // can we automatically handle it? // If it's an element, we should replace innerHTML by ID if possible // if a callback is given and it's a string, that's an id. Return type of element // should replace that id. return type of string should be appended // FIXME: meh just do something here. } return obj.result; } else { // how should we handle the error? I guess throwing is better than nothing // but should there be an error callback too? var error = new Error(obj.errorMessage); error.name = obj.type; error.functionUrl = me._apiBase + name; error.functionArgs = args; error.errorMessage = obj.errorMessage; // myFunction.caller should be available and checked too // btw arguments.callee is like this for functions if(me._debugMode) { var ourMessage = obj.type + ": " + obj.errorMessage + "\nOn: " + me._apiBase + name; if(args.toSource) ourMessage += args.toSource(); if(args.stack) ourMessage += "\n" + args.stack; error.message = ourMessage; // alert(ourMessage); } if(onError) // local override first... return onError(error); else if(resObj.onError) // then this object return resObj.onError(error); else if(me._onError) // then the global object return me._onError(error); throw error; // if all else fails... } // assert(0); // not reached }, (name.indexOf("get") == 0) ? "GET" : "POST", async); // FIXME: hack: naming convention used to figure out method to use }, // should pop open the thing in HTML format // "popup":null, // FIXME not implemented "onError":null, // null means call the global one "onSuccess":null, // a generic callback. generally pass something to get instead. "formatSet":false, // is the format overridden? // gets the result. Works automatically if you don't pass a callback. // You can also curry arguments to your callback by listing them here. The // result is put on the end of the arg list to the callback "get":function(callbackObj) { var callback = null; var errorCb = null; var callbackThis = null; if(callbackObj) { if(typeof callbackObj == "function") callback = callbackObj; else { if(callbackObj.length) { // array callback = callbackObj[0]; if(callbackObj.length >= 2) errorCb = callbackObj[1]; } else { if(callbackObj.onSuccess) callback = callbackObj.onSuccess; if(callbackObj.onError) errorCb = callbackObj.onError; if(callbackObj.self) callbackThis = callbackObj.self; else callbackThis = callbackObj; } } } if(arguments.length > 1) { var ourArguments = []; for(var a = 1; a < arguments.length; a++) ourArguments.push(arguments[a]); function cb(obj, xml) { ourArguments.push(obj); ourArguments.push(xml); // that null is the this object inside the function... can // we make that work? return callback.apply(callbackThis, ourArguments); } function cberr(err) { ourArguments.unshift(err); // that null is the this object inside the function... can // we make that work? return errorCb.apply(callbackThis, ourArguments); } this._get(cb, errorCb ? cberr : null, true); } else { this._get(callback, errorCb, true); } }, // If you need a particular format, use this. "getFormat":function(format /* , same args as get... */) { this.format(format); var forwardedArgs = []; for(var a = 1; a < arguments.length; a++) forwardedArgs.push(arguments[a]); this.get.apply(this, forwardedArgs); }, // sets the format of the request so normal get uses it // myapi.someFunction().format('table').get(...); // see also: getFormat and getHtml // the secondaryFormat only makes sense if format is json. It // sets the format returned by object.toString() in the returned objects. "format":function(format, secondaryFormat) { if(args == null) args = {}; args.format = format; if(typeof secondaryFormat == "string" && secondaryFormat) { if(format != "json") me._raiseError("AssertError", "secondaryFormat only works if format == json"); args.secondaryFormat = secondaryFormat; } this.formatSet = true; return this; }, "getHtml":function(/* args to get... */) { this.format("html"); this.get.apply(this, arguments); }, // FIXME: add post aliases // don't use unless you're deploying to localhost or something "getSync":function() { function cb(obj) { // no nothing, we're returning the value below } return this._get(cb, null, false); }, // takes the result and appends it as html to the given element // FIXME: have a type override "appendTo":function(what) { if(!this.formatSet) this.format("html"); this.get(me._appendContent(what)); }, // use it to replace the content of the given element "useToReplace":function(what) { if(!this.formatSet) this.format("html"); this.get(me._replaceContent(what)); }, // use to replace the given element altogether "useToReplaceElement":function(what) { if(!this.formatSet) this.format("html"); this.get(me._replaceElement(what)); }, "useToFillForm":function(what) { this.get(me._fillForm(what)); } // runAsScript has been removed, use get(eval) instead // FIXME: might be nice to have an automatic popin function too }; }, "getAutomaticForm":function(method) { return this._serverCall("builtin.getAutomaticForm", {"method":method}, "Form"); }, "_fillForm": function(what) { var e = this._getElement(what); if(this._isListOfNodes(e)) alert("FIXME: list of forms not implemented"); else return function(obj) { if(e.elements && typeof obj == "object") { for(i in obj) if(e.elements[i]) e.elements[i].value = obj[i]; // FIXME: what about checkboxes, selects, etc? } else throw new Error("unsupported response"); }; }, "_getElement": function(what) { var e; if(typeof what == "string") e = document.getElementById(what); else e = what; return e; }, "_isListOfNodes": function(what) { // length is on both arrays and lists, but some elements // have it too. We disambiguate with getAttribute return (what && (what.length && !what.getAttribute)) }, // These are some convenience functions to use as callbacks "_replaceContent": function(what) { var e = this._getElement(what); if(this._isListOfNodes(e)) return function(obj) { for(var a = 0; a < obj.length; a++) { if( (e[a].tagName.toLowerCase() == "input" && e[a].getAttribute("type") == "text") || e[a].tagName.toLowerCase() == "textarea") { e[a].value = obj; } else e[a].innerHTML = obj; } } else return function(obj) { if( (e.tagName.toLowerCase() == "input" && e.getAttribute("type") == "text") || e.tagName.toLowerCase() == "textarea") { e.value = obj; } else e.innerHTML = obj; } }, // note: what must be only a single element, FIXME: could check the static type "_replaceElement": function(what) { var e = this._getElement(what); if(this._isListOfNodes(e)) throw new Error("Can only replace individual elements since removal from a list may be unstable."); return function(obj) { var n = document.createElement("div"); n.innerHTML = obj; if(n.firstChild) { e.parentNode.replaceChild(n.firstChild, e); } else { e.parentNode.removeChild(e); } } }, "_appendContent": function(what) { var e = this._getElement(what); if(this._isListOfNodes(e)) // FIXME: repeating myself... return function(obj) { for(var a = 0; a < e.length; a++) e[a].innerHTML += obj; } else return function(obj) { e.innerHTML += obj; } }, };