module arsd.web;
// FIXME: if a method has a default value of a non-primitive type,
// it's still liable to screw everything else.
/*
Reasonably easy CSRF plan:
A csrf token can be associated with the entire session, and
saved in the session file.
Each form outputs the token, and it is added as a parameter to
the script thingy somewhere.
It need only be sent on POST items. Your app should handle proper
get and post separation.
*/
/*
Future directions for web stuff:
an improved css:
add definition nesting
add importing things from another definition
Implemented: see html.d
All css improvements are done via simple text rewriting. Aside
from the nesting, it'd just be a simple macro system.
Struct input functions:
static typeof(this) fromWebString(string fromUrl) {}
Automatic form functions:
static Element makeFormElement(Document document) {}
javascript:
I'd like to add functions and do static analysis actually.
I can't believe I just said that though.
But the stuff I'd analyze is checking it against the
D functions, recognizing that JS is loosely typed.
So basically it can do a grep for simple stuff:
CoolApi.xxxxxxx
if xxxxxxx isn't a function in CoolApi (the name
it knows from the server), it can flag a compile
error.
Might not be able to catch usage all the time
but could catch typo names.
*/
/*
FIXME: in params on the wrapped functions generally don't work
(can't modify const)
Running from the command line:
./myapp function positional args....
./myapp --format=json function
./myapp --make-nested-call
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
*/
import std.exception;
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;
import std.datetime;
public import std.range;
public import std.traits;
import std.json;
/// This gets your site's base link. note it's really only good if you are using FancyMain.
string getSiteLink(Cgi cgi) {
return cgi.requestUri[0.. cgi.requestUri.indexOf(cgi.scriptName) + cgi.scriptName.length + 1 /* for the slash at the end */];
}
/// use this in a function parameter if you want the automatic form to render
/// it as a textarea
/// FIXME: this should really be an annotation on the parameter... somehow
struct Text {
string content;
alias content this;
}
/// This is the JSON envelope format
struct Envelope {
bool success; /// did the call succeed? false if it threw an exception
string type; /// static type of the return value
string errorMessage; /// if !success, this is exception.msg
string userData; /// null unless the user request included passedThroughUserData
// use result.str if the format was anything other than json
JSONValue result; /// the return value of the function
debug string dFullString; /// exception.toString - includes stack trace, etc. Only available in debug mode for privacy reasons.
}
/// Info about the current request - more specialized than the cgi object directly
struct RequestInfo {
string mainSitePath; /// the bottom-most ApiProvider's path in this request
string objectBasePath; /// the top-most resolved path in the current request
FunctionInfo currentFunction; /// what function is being called according to the url?
string requestedFormat; /// the format the returned data was requested to be sent
string requestedEnvelopeFormat; /// the format the data is to be wrapped in
}
string linkTo(alias func, T...)(T args) {
auto reflection = __traits(parent, func).reflection;
assert(reflection !is null);
auto name = func.stringof;
auto idx = name.indexOf("(");
if(idx != -1)
name = name[0 .. idx];
auto funinfo = reflection.functions[name];
return funinfo.originalName;
}
/// this is there so there's a common runtime type for all callables
class WebDotDBaseType {
Cgi cgi; /// lower level access to the request
/// Override this if you want to do something special to the document
/// You should probably call super._postProcess at some point since I
/// might add some default transformations here.
/// By default, it forwards the document root to _postProcess(Element).
void _postProcess(Document document) {
if(document !is null && document.root !is null)
_postProcessElement(document.root);
}
/// Override this to do something special to returned HTML Elements.
/// This is ONLY run if the return type is(: Element). It is NOT run
/// if the return type is(: Document).
void _postProcessElement(Element element) {} // why the fuck doesn't overloading actually work?
/// convenience function to enforce that the current method is POST.
/// You should use this if you are going to commit to the database or something.
void ensurePost() {
assert(cgi !is null);
enforce(cgi.requestMethod == Cgi.RequestMethod.POST);
}
}
/// Everything should derive from this instead of the old struct namespace used before
/// Your class must provide a default constructor.
class ApiProvider : WebDotDBaseType {
private ApiProvider builtInFunctions;
Session session; // note: may be null
/// override this to change cross-site request forgery checks.
///
/// The default is done on POST requests, using the session object. It throws
/// a PermissionDeniedException if the check fails. This might change later
/// to make catching it easier.
///
/// If there is no session object, the test always succeeds. This lets you opt
/// out of the system. FIXME: should I add ensureGoodPost or something to combine
/// enforce(session !is null); ensurePost() and checkCsrfToken();????
///
/// If the session is null, it does nothing. FancyMain makes a session for you.
/// If you are doing manual run(), it is your responsibility to create a session
/// and attach it to each primary object.
///
/// NOTE: it is important for you use ensurePost() on any data changing things!
/// Even though this function is called automatically by run(), it is a no-op on
/// non-POST methods, so there's no real protection without ensuring POST when
/// making changes.
///
// FIXME: if someone is OAuth authorized, a csrf token should not really be necessary.
// This check is done automatically right now, and doesn't account for that. I guess
// people could override it in a subclass though. (Which they might have to since there's
// no oauth integration at this level right now anyway. Nor may there ever be; it's kinda
// high level. Perhaps I'll provide an oauth based subclass later on.)
protected void checkCsrfToken() {
assert(cgi !is null);
if(cgi.requestMethod == Cgi.RequestMethod.POST) {
auto tokenInfo = _getCsrfInfo();
if(tokenInfo is null)
return; // not doing checks
void fail() {
throw new PermissionDeniedException("CSRF token test failed");
}
// expiration is handled by the session itself expiring (in the Session class)
if(tokenInfo["key"] !in cgi.post)
fail();
if(cgi.post[tokenInfo["key"]] != tokenInfo["token"])
fail();
}
}
/// Shorthand for ensurePost and checkCsrfToken. You should use this on non-indempotent
/// functions. Override it if doing some custom checking.
void ensureGoodPost() {
ensurePost();
checkCsrfToken();
}
// gotta make sure this isn't callable externally! Oh lol that'd defeat the point...
/// Gets the CSRF info (an associative array with key and token inside at least) from the session.
/// Note that the actual token is generated by the Session class.
protected string[string] _getCsrfInfo() {
if(session is null)
return null;
return decodeVariablesSingle(session.csrfToken);
}
/// Adds CSRF tokens to the document for use by script (required by the Javascript API)
/// and then calls addCsrfTokens(document.root) to add them to all POST forms as well.
protected void addCsrfTokens(Document document) {
if(document is null)
return;
if(!csrfTokenAddedToScript) {
auto tokenInfo = _getCsrfInfo();
if(tokenInfo is null)
return;
auto bod = document.mainBody;
if(bod !is null) {
bod.setAttribute("data-csrf-key", tokenInfo["key"]);
bod.setAttribute("data-csrf-token", tokenInfo["token"]);
csrfTokenAddedToScript = true;
}
addCsrfTokens(document.root);
}
}
/// we have to add these things to the document...
override void _postProcess(Document document) {
addCsrfTokens(document);
super._postProcess(document);
}
private bool csrfTokenAddedToScript;
//private bool csrfTokenAddedToForms;
/// This adds CSRF tokens to all forms in the tree
protected void addCsrfTokens(Element element) {
if(element is null)
return;
//if(!csrfTokenAddedToForms) {
auto tokenInfo = _getCsrfInfo();
if(tokenInfo is null)
return;
foreach(formElement; element.getElementsByTagName("form")) {
if(formElement.method != "POST" && formElement.method != "post")
continue;
auto form = cast(Form) formElement;
assert(form !is null);
form.setValue(tokenInfo["key"], tokenInfo["token"]);
}
//csrfTokenAddedToForms = true;
//}
}
// and added to ajax forms..
override void _postProcessElement(Element element) {
addCsrfTokens(element);
super._postProcessElement(element);
}
// FIXME: the static is meant to be a performance improvement, but it breaks child modules' reflection!
/*static */immutable(ReflectionInfo)* reflection;
string _baseUrl; // filled based on where this is called from on this request
RequestInfo currentRequest; // FIXME: actually fill this in
/// 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() {}
/// Returns the stylesheet for this module. Use it to encapsulate the needed info for your output so the module is more easily reusable
/// Override this to provide your own stylesheet. (of course, you can always provide it via _catchAll or any standard css file/style element too.)
string _style() const {
return null;
}
/// Returns the combined stylesheet of all child modules and this module
string stylesheet() const {
string ret;
foreach(i; reflection.objects) {
if(i.instantiation !is null)
ret ~= i.instantiation.stylesheet();
}
ret ~= _style();
return ret;
}
/// This tentatively redirects the user - depends on the envelope fomat
void redirect(string location, bool important = false) {
auto f = cgi.request("envelopeFormat", "document");
if(f == "document" || f == "redirect")
cgi.setResponseLocation(location, important);
}
/// Returns a list of links to all functions in this class or sub-classes
/// You can expose it publicly with alias: "alias _sitemap sitemap;" for example.
Element _sitemap() {
auto container = _getGenericContainer();
void writeFunctions(Element list, in ReflectionInfo* reflection, string base) {
string[string] handled;
foreach(func; reflection.functions) {
if(func.originalName in handled)
continue;
handled[func.originalName] = func.originalName;
list.addChild("li", new Link(base ~ func.name, beautify(func.originalName)));
}
handled = null;
foreach(obj; reflection.objects) {
if(obj.name in handled)
continue;
handled[obj.name] = obj.name;
auto li = list.addChild("li", new Link(base ~ obj.name, obj.name));
auto ul = li.addChild("ul");
writeFunctions(ul, obj, base ~ obj.name ~ "/");
}
}
auto list = container.addChild("ul");
writeFunctions(list, reflection, _baseUrl ~ "/");
return list.parentNode.removeChild(list);
}
/// If the user goes to your program without specifying a path, this function is called.
// FIXME: should it return document? That's kinda a pain in the butt.
Document _defaultPage() {
throw new Exception("no default");
}
/// When the html document envelope is used, this function is used to get a html element
/// where the return value is appended.
/// It's the main function to override to provide custom HTML templates.
Element _getGenericContainer()
out(ret) {
assert(ret !is null);
}
body {
auto document = new Document("
", true, true);
if(this.reflection !is null)
document.title = this.reflection.name;
auto container = document.getElementById("body");
return container;
}
/// If the given url path didn't match a function, it is passed to this function
/// for further handling. By default, it throws a NoSuchPageException.
/// Overriding it might be useful if you want to serve generic filenames or an opDispatch kind of thing.
/// (opDispatch itself won't work because it's name argument needs to be known at compile time!)
///
/// Note that you can return Documents here as they implement
/// the FileResource interface too.
FileResource _catchAll(string path) {
throw new NoSuchPageException(_errorMessageForCatchAll);
}
private string _errorMessageForCatchAll;
private FileResource _catchallEntry(string path, string funName, string errorMessage) {
if(!errorMessage.length) {
string allFuncs, allObjs;
foreach(n, f; reflection.functions)
allFuncs ~= n ~ "\n";
foreach(n, f; reflection.objects)
allObjs ~= n ~ "\n";
errorMessage = "no such function " ~ funName ~ "\n functions are:\n" ~ allFuncs ~ "\n\nObjects are:\n" ~ allObjs;
}
_errorMessageForCatchAll = errorMessage;
return _catchAll(path);
}
/// When in website mode, you can use this to beautify the error message
Document delegate(Throwable) _errorFunction;
}
/// Implement subclasses of this inside your main provider class to do a more object
/// oriented site.
class ApiObject : WebDotDBaseType {
/* abstract this(ApiProvider parent, string identifier) */
/// Override this to make json out of this object
JSONValue makeJsonValue() {
return toJsonValue(null);
}
}
class DataFile : FileResource {
this(string contentType, immutable(void)[] contents) {
_contentType = contentType;
_content = contents;
}
private string _contentType;
private immutable(void)[] _content;
string contentType() const {
return _contentType;
}
immutable(ubyte)[] getData() const {
return cast(immutable(ubyte)[]) _content;
}
}
/// Describes the info collected about your class
struct ReflectionInfo {
immutable(FunctionInfo)*[string] functions; /// the methods
EnumInfo[string] enums; /// .
StructInfo[string] structs; ///.
const(ReflectionInfo)*[string] objects; /// ApiObjects and ApiProviders
bool needsInstantiation; // internal - does the object exist or should it be new'd before referenced?
ApiProvider instantiation; // internal (for now) - reference to the actual object being described
WebDotDBaseType delegate(string) instantiate;
// the overall namespace
string name; /// this is also used as the object name in the JS api
// these might go away.
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:
}
/// describes an enum, iff based on int as the underlying type
struct EnumInfo {
string name; ///.
int[] values; ///.
string[] names; ///.
}
/// describes a plain data struct
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; /// 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
const(ReflectionInfo)* parentObject;
// should I also offer dispatchers for other formats like Variant[]?
string name; /// the URL friendly name
string originalName; /// the original name in code
//string uriPath;
Parameter[] parameters; ///.
string returnType; ///. static type to string
bool returnTypeIsDocument; // internal used when wrapping
bool returnTypeIsElement; // internal used when wrapping
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
}
/// Function parameter
struct Parameter {
string name; /// name (not always accurate)
string value; // ???
string type; /// type of HTML element to create when asking
string staticType; /// original type
string validator; /// FIXME
bool hasDefault; /// if there was a default defined in the function
string defaultValue; /// the default value defined in D, but as a string, if present
// for radio and select boxes
string[] options; /// possible options for selects
string[] optionValues; ///.
Element function(Document, string) makeFormElement;
}
// these are all filthy hacks
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;
}
// sets up the reflection object. now called automatically so you probably don't have to mess with it
immutable(ReflectionInfo*) prepareReflection(alias PM)(PM instantiation) if(is(PM : ApiProvider) || is(PM: ApiObject) ) {
return prepareReflectionImpl!(PM, PM)(instantiation);
}
// FIXME: this doubles the compile time and can add megabytes to the executable.
immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Parent instantiation)
if(is(PM : WebDotDBaseType) && is(Parent : ApiProvider))
{
assert(instantiation !is null);
ReflectionInfo* reflection = new ReflectionInfo;
reflection.name = PM.stringof;
static if(is(PM: ApiObject)) {
reflection.needsInstantiation = true;
reflection.instantiate = delegate WebDotDBaseType(string i) {
auto n = new PM(instantiation, i);
return n;
};
} else {
reflection.instantiation = instantiation;
static if(!is(PM : BuiltInFunctions)) {
auto builtins = new BuiltInFunctions(instantiation, reflection);
instantiation.builtInFunctions = builtins;
foreach(k, v; builtins.reflection.functions)
reflection.functions["builtin." ~ k] = v;
}
}
static if(is(PM : ApiProvider)) {{ // double because I want a new scope
auto f = new FunctionInfo;
f.parentObject = reflection;
f.dispatcher = generateWrapper!(PM, "_defaultPage", PM._defaultPage)(reflection, instantiation);
f.returnTypeIsDocument = true;
reflection.functions["/"] = cast(immutable) f;
/+
// catchAll here too
f = new FunctionInfo;
f.parentObject = reflection;
f.dispatcher = generateWrapper!(PM, "_catchAll", PM._catchAll)(reflection, instantiation);
f.returnTypeIsDocument = true;
reflection.functions["/_catchAll"] = cast(immutable) f;
+/
}}
// derivedMembers is changed from allMembers
foreach(member; __traits(derivedMembers, PM)) {
static if(member[0] != '_') {
// 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))
) {
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))
) {
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.length < 5 ||
(
member[$ - 5 .. $] != "_Page" &&
member[$ - 5 .. $] != "_Form") &&
!(member.length > 16 && member[$ - 16 .. $] == "_PermissionCheck")
)) {
FunctionInfo* f = new FunctionInfo;
ParameterTypeTuple!(__traits(getMember, PM, member)) fargs;
f.returnType = ReturnType!(__traits(getMember, PM, member)).stringof;
f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, PM, member)) : Document);
f.returnTypeIsElement = is(ReturnType!(__traits(getMember, PM, member)) : Element);
f.parentObject = reflection;
f.name = toUrlName(member);
f.originalName = member;
assert(instantiation !is null);
f.dispatcher = generateWrapper!(PM, member, __traits(getMember, PM, member))(reflection, instantiation);
//f.uriPath = f.originalName;
auto namesAndDefaults = parameterInfoImpl!(__traits(getMember, PM, member));
auto names = namesAndDefaults[0];
auto defaults = namesAndDefaults[1];
assert(names.length == defaults.length);
foreach(idx, param; fargs) {
if(idx >= names.length)
assert(0, to!string(idx) ~ " " ~ to!string(names));
Parameter p = reflectParam!(typeof(param))();
p.name = names[idx];
auto d = defaults[idx];
p.defaultValue = d == "null" ? "" : d;
p.hasDefault = d.length > 0;
f.parameters ~= p;
}
static if(__traits(hasMember, PM, member ~ "_Form")) {
f.createForm = &__traits(getMember, instantiation, member ~ "_Form");
}
reflection.functions[f.name] = cast(immutable) (f);
// also offer the original name if it doesn't
// conflict
//if(f.originalName !in reflection.functions)
reflection.functions[f.originalName] = cast(immutable) (f);
}
else static if(
!is(typeof(__traits(getMember, PM, member)) == function) &&
isApiObject!(__traits(getMember, PM, member))
) {
reflection.objects[member] = prepareReflectionImpl!(
__traits(getMember, PM, member), Parent)
(instantiation);
} else static if( // child ApiProviders are like child modules
!is(typeof(__traits(getMember, PM, member)) == function) &&
isApiProvider!(__traits(getMember, PM, member))
) {
PassthroughType!(__traits(getMember, PM, member)) i;
i = new typeof(i)();
auto r = prepareReflection!(__traits(getMember, PM, member))(i, null, member);
reflection.objects[member] = r;
if(toLower(member) !in reflection.objects) // web filenames are often lowercase too
reflection.objects[member.toLower] = r;
}
}
}
return cast(immutable) reflection;
}
Parameter reflectParam(param)() {
Parameter p;
p.staticType = param.stringof;
static if( __traits(compiles, p.makeFormElement = &(param.makeFormElement))) {
p.makeFormElement = &(param.makeFormElement);
} else static if( __traits(compiles, PM.makeFormElement!(param)(null, null))) {
alias PM.makeFormElement!(param) LOL;
p.makeFormElement = &LOL;
} else static if( is( param == enum )) {
p.type = "select";
foreach(opt; __traits(allMembers, param)) {
p.options ~= opt;
p.optionValues ~= to!string(__traits(getMember, param, opt));
}
} else static if (is(param == bool)) {
p.type = "checkbox";
} else static if (is(Unqual!(param) == Cgi.UploadedFile)) {
p.type = "file";
} else static if(is(Unqual!(param) == Text)) {
p.type = "textarea";
} else {
if(p.name.toLower.indexOf("password") != -1) // hack to support common naming convention
p.type = "password";
else
p.type = "text";
}
return p;
}
struct CallInfo {
string objectIdentifier;
immutable(FunctionInfo)* func;
}
class NonCanonicalUrlException : Exception {
this(CanonicalUrlOption option, string properUrl = null) {
this.howToFix = option;
this.properUrl = properUrl;
super("The given URL needs this fix: " ~ to!string(option) ~ " " ~ properUrl);
}
CanonicalUrlOption howToFix;
string properUrl;
}
enum CanonicalUrlOption {
cutTrailingSlash,
addTrailingSlash
}
CallInfo parseUrl(in ReflectionInfo* reflection, string url, string defaultFunction, in bool hasTrailingSlash) {
CallInfo info;
if(url.length && url[0] == '/')
url = url[1 .. $];
if(reflection.needsInstantiation) {
// FIXME: support object identifiers that span more than one slash... maybe
auto idx = url.indexOf("/");
if(idx != -1) {
info.objectIdentifier = url[0 .. idx];
url = url[idx + 1 .. $];
} else {
info.objectIdentifier = url;
url = null;
}
}
string name;
auto idx = url.indexOf("/");
if(idx != -1) {
name = url[0 .. idx];
url = url[idx + 1 .. $];
} else {
name = url;
url = null;
}
bool usingDefault = false;
if(name.length == 0) {
name = defaultFunction;
usingDefault = true;
if(name !in reflection.functions)
name = "/"; // should call _defaultPage
}
if(name in reflection.functions) {
info.func = reflection.functions[name];
// if we're using a default thing, we need as slash on the end so relative links work
if(usingDefault) {
if(!hasTrailingSlash)
throw new NonCanonicalUrlException(CanonicalUrlOption.addTrailingSlash);
} else {
if(hasTrailingSlash)
throw new NonCanonicalUrlException(CanonicalUrlOption.cutTrailingSlash);
}
}
if(name in reflection.objects) {
info = parseUrl(reflection.objects[name], url, defaultFunction, hasTrailingSlash);
}
return info;
}
/// If you're not using FancyMain, this is the go-to function to do most the work.
/// instantiation should be an object of your ApiProvider type.
/// pathInfoStartingPoint is used to make a slice of it, incase you already consumed part of the path info before you called this.
void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint = 0) if(is(Provider : ApiProvider)) {
assert(instantiation !is null);
if(instantiation.reflection is null) {
instantiation.reflection = prepareReflection!(Provider)(instantiation);
instantiation.cgi = cgi;
instantiation._initialize();
// FIXME: what about initializing child objects?
}
auto reflection = instantiation.reflection;
instantiation._baseUrl = cgi.scriptName ~ cgi.pathInfo[0 .. pathInfoStartingPoint];
// everything assumes the url isn't empty...
if(cgi.pathInfo.length < pathInfoStartingPoint + 1) {
cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo ~ "/" ~ (cgi.queryString.length ? "?" ~ cgi.queryString : ""));
return;
}
// kinda a hack, but this kind of thing should be available anyway
string funName = cgi.pathInfo[pathInfoStartingPoint + 1..$];
if(funName == "functions.js") {
cgi.gzipResponse = true;
cgi.setResponseContentType("text/javascript");
cgi.write(makeJavascriptApi(reflection, replace(cast(string) cgi.requestUri, "functions.js", "")), true);
cgi.close();
return;
}
if(funName == "styles.css") {
cgi.gzipResponse = true;
cgi.setResponseContentType("text/css");
cgi.write(instantiation.stylesheet(), true);
cgi.close();
return;
}
CallInfo info;
try
info = parseUrl(reflection, cgi.pathInfo[pathInfoStartingPoint + 1 .. $], to!string(cgi.requestMethod), cgi.pathInfo[$-1] == '/');
catch(NonCanonicalUrlException e) {
final switch(e.howToFix) {
case CanonicalUrlOption.cutTrailingSlash:
cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo[0 .. $ - 1] ~
(cgi.queryString.length ? ("?" ~ cgi.queryString) : ""));
break;
case CanonicalUrlOption.addTrailingSlash:
cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo ~ "/" ~
(cgi.queryString.length ? ("?" ~ cgi.queryString) : ""));
break;
}
return;
}
auto fun = info.func;
auto instantiator = info.objectIdentifier;
Envelope result;
result.userData = cgi.request("passedThroughUserData");
auto envelopeFormat = cgi.request("envelopeFormat", "document");
WebDotDBaseType base = instantiation;
// FIXME
if(cgi.pathInfo.indexOf("builtin.") != -1 && instantiation.builtInFunctions !is null)
base = instantiation.builtInFunctions;
if(instantiator.length) {
assert(fun !is null);
assert(fun.parentObject !is null);
assert(fun.parentObject.instantiate !is null);
base = fun.parentObject.instantiate(instantiator);
}
try {
version(fb_inside_hack) {
// FIXME: this almost renders the whole thing useless.
if(cgi.referrer.indexOf("apps.facebook.com") == -1)
instantiation.checkCsrfToken();
} else
// you know, I wonder if this should even be automatic. If I
// just put it in the ensureGoodPost function or whatever
// it prolly works - such needs to be there anyway for it to be properly
// right.
instantiation.checkCsrfToken();
if(fun is null) {
auto d = instantiation._catchallEntry(
cgi.pathInfo[pathInfoStartingPoint + 1..$],
funName,
"");
result.success = true;
if(d !is null) {
auto doc = cast(Document) d;
if(doc)
instantiation._postProcess(doc);
cgi.setResponseContentType(d.contentType());
cgi.write(d.getData(), true);
}
// we did everything we need above...
envelopeFormat = "no-processing";
goto do_nothing_else;
}
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;
// FIXME: hackalicious garbage. kill.
string[][string] want = cast(string[][string]) (cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.postArray : cgi.getArray);
version(fb_inside_hack) {
if(cgi.referrer.indexOf("apps.facebook.com") != -1) {
auto idx = cgi.referrer.indexOf("?");
if(idx != -1 && cgi.referrer[idx + 1 .. $] != cgi.queryString) {
// so fucking broken
cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo ~ cgi.referrer[idx .. $]);
return;
}
}
if(cgi.requestMethod == Cgi.RequestMethod.POST) {
foreach(k, v; cgi.getArray)
want[k] = cast(string[]) v;
foreach(k, v; cgi.postArray)
want[k] = cast(string[]) v;
}
}
res = fun.dispatcher(cgi, base, want, format, secondaryFormat);
//if(cgi)
// cgi.setResponseContentType("application/json");
result.success = true;
result.result = res;
do_nothing_else: {}
}
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(fun.createForm !is null) {
// go ahead and use it to make the form page
auto doc = fun.createForm(cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.post : cgi.get);
form = doc.requireSelector!Form("form");
} else {
Parameter[] params = (cast(Parameter[])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("