From 79b2a25b92e2f7693390a1b1ed904b339556b43c Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Sat, 16 Feb 2013 11:18:15 -0500 Subject: [PATCH] catching up on random stuff --- cgi.d | 4 +- dom.d | 24 ++++-- engine.d | 43 +++++++++- htmltotext.d | 15 +++- jpg.d | 16 ++-- mysql.d | 10 ++- png.d | 35 ++++++++ web.d | 226 +++++++++++++++++++++++++++++++++++++++++++++++---- 8 files changed, 334 insertions(+), 39 deletions(-) diff --git a/cgi.d b/cgi.d index 849b5d7..d15d66f 100644 --- a/cgi.d +++ b/cgi.d @@ -1163,10 +1163,12 @@ class Cgi { /// This gets a full url for the current request, including port, protocol, host, path, and query string getCurrentCompleteUri() const { + ushort defaultPort = https ? 443 : 80; + return format("http%s://%s%s%s", https ? "s" : "", host, - port == 80 ? "" : ":" ~ to!string(port), + port == defaultPort ? "" : ":" ~ to!string(port), requestUri); } diff --git a/dom.d b/dom.d index 9c4bf6e..c623928 100644 --- a/dom.d +++ b/dom.d @@ -20,6 +20,9 @@ */ module arsd.dom; +// FIXME: something like
    spam
      with no closing
    should read the second tag as the closer in garbage mode +// FIXME: failing to close a paragraph sometimes messes things up too + // FIXME: it would be kinda cool to have some support for internal DTDs // and maybe XPath as well, to some extent /* @@ -1583,7 +1586,7 @@ class Element { /** Takes some html and replaces the element's children with the tree made from the string. */ - Element innerHTML(string html) { + Element innerHTML(string html, bool strict = false) { if(html.length) selfClosed = false; @@ -1595,7 +1598,7 @@ class Element { } auto doc = new Document(); - doc.parse("" ~ html ~ ""); // FIXME: this should preserve the strictness of the parent document + doc.parse("" ~ html ~ "", strict, strict); // FIXME: this should preserve the strictness of the parent document children = doc.root.children; foreach(c; children) { @@ -3029,7 +3032,7 @@ class TableCell : Element { ///. -class MarkupError : Exception { +class MarkupException : Exception { ///. this(string message) { @@ -3208,7 +3211,7 @@ class Document : FileResource { if(dataEncoding is null) { if(strict) - throw new MarkupError("I couldn't figure out the encoding of this document."); + throw new MarkupException("I couldn't figure out the encoding of this document."); else // if we really don't know by here, it means we already tried UTF-8, // looked for utf 16 and 32 byte order marks, and looked for xml or meta @@ -3282,7 +3285,7 @@ class Document : FileResource { } void parseError(string message) { - throw new MarkupError(format("char %d (line %d): %s", pos, getLineNumber(pos), message)); + throw new MarkupException(format("char %d (line %d): %s", pos, getLineNumber(pos), message)); } void eatWhitespace() { @@ -3312,7 +3315,7 @@ class Document : FileResource { data[pos] != ' ' && data[pos] != '\n' && data[pos] != '\t') { if(data[pos] == '<') - throw new MarkupError("The character < can never appear in an attribute name."); + throw new MarkupException("The character < can never appear in an attribute name."); pos++; } @@ -3326,11 +3329,14 @@ class Document : FileResource { switch(data[pos]) { case '\'': case '"': + auto started = pos; char end = data[pos]; pos++; auto start = pos; - while(data[pos] != end) + while(pos < data.length && data[pos] != end) pos++; + if(strict && pos == data.length) + throw new MarkupException("Unclosed attribute value, started on char " ~ to!string(started)); string v = htmlEntitiesDecode(data[start..pos], strict); pos++; // skip over the end return v; @@ -3387,7 +3393,7 @@ class Document : FileResource { if(pos >= data.length) { if(strict) { - throw new MarkupError("Gone over the input (is there no root element?), chain: " ~ to!string(parentChain)); + throw new MarkupException("Gone over the input (is there no root element?), chain: " ~ to!string(parentChain)); } else { if(parentChain.length) return Ele(1, null, parentChain[0]); // in loose mode, we just assume the document has ended @@ -3672,7 +3678,7 @@ class Document : FileResource { } if(strict && attrName in attributes) - throw new MarkupError("Repeated attribute: " ~ attrName); + throw new MarkupException("Repeated attribute: " ~ attrName); attributes[attrName] = attrValue; goto moreAttributes; diff --git a/engine.d b/engine.d index b943e3b..5957c11 100644 --- a/engine.d +++ b/engine.d @@ -8,6 +8,13 @@ */ module arsd.engine; //@-L-lSDL -L-lSDL_mixer -L-lSDL_ttf -L-lSDL_image -L-lGL -L-lSDL_net +pragma(lib, "SDL"); +pragma(lib, "SDL_mixer"); +pragma(lib, "SDL_ttf"); +pragma(lib, "SDL_image"); +pragma(lib, "SDL_net"); +pragma(lib, "GL"); + // FIXME: the difference between directions and buttons should be removed @@ -45,8 +52,6 @@ else import std.stdio; //version(linux) pragma(lib, "kbhit.o"); -extern(C) bool kbhit(); - int randomNumber(int min, int max){ if(min == max) return min; @@ -1211,3 +1216,37 @@ bool directionIsDown(Engine.Direction d, int which = 0){ */ + +version(linux) { + version(D_Version2) { + import sys = core.sys.posix.sys.select; + version=CustomKbhit; + + int kbhit() + { + sys.timeval tv; + sys.fd_set read_fd; + + tv.tv_sec=0; + tv.tv_usec=0; + sys.FD_ZERO(&read_fd); + sys.FD_SET(0,&read_fd); + + if(sys.select(1, &read_fd, null, null, &tv) == -1) + return 0; + + if(sys.FD_ISSET(0,&read_fd)) + return 1; + + return 0; + } + } + + // else, use kbhit.o from the C file +} + +version(CustomKbhit) {} else + extern(C) bool kbhit(); + + + diff --git a/htmltotext.d b/htmltotext.d index 163f8d4..ec67193 100644 --- a/htmltotext.d +++ b/htmltotext.d @@ -21,8 +21,8 @@ string htmlToText(string html, bool wantWordWrap = true, int wrapAmount = 74) { html = html.replace(" ", " "); html = html.replace(" ", " "); html = html.replace(" ", " "); - html = html.replace("\n", ""); - html = html.replace("\r", ""); + html = html.replace("\n", " "); + html = html.replace("\r", " "); html = std.regex.replace(html, std.regex.regex("[\n\r\t \u00a0]+", "gm"), " "); document.parse("" ~ html ~ ""); @@ -75,6 +75,7 @@ string htmlToText(string html, bool wantWordWrap = true, int wrapAmount = 74) { ele.stripOut(); goto again; break; + case "td": case "p": /* if(ele.innerHTML.length > 1) @@ -121,7 +122,14 @@ string htmlToText(string html, bool wantWordWrap = true, int wrapAmount = 74) { start.innerHTML = start.innerHTML().replace("\u0001", "\n"); foreach(ele; start.tree) { - if(ele.tagName == "p") { + if(ele.tagName == "td") { + if(ele.directText().strip().length) { + ele.prependText("\r"); + ele.appendText("\r"); + } + ele.stripOut(); + goto again2; + } else if(ele.tagName == "p") { if(strip(ele.innerText()).length > 1) { string res = ""; string all = ele.innerText().replace("\n \n", "\n\n"); @@ -136,6 +144,7 @@ string htmlToText(string html, bool wantWordWrap = true, int wrapAmount = 74) { } result = start.innerText(); + result = squeeze(result, " "); result = result.replace("\r ", "\r"); result = result.replace(" \r", "\r"); diff --git a/jpg.d b/jpg.d index c614b9b..8f23ab3 100644 --- a/jpg.d +++ b/jpg.d @@ -5,7 +5,6 @@ import std.stdio; import std.conv; struct JpegSection { - ushort length; ubyte identifier; ubyte[] data; } @@ -37,16 +36,16 @@ struct LazyJpegFile { throw new Exception("not lined up in file"); _front.identifier = startingBuffer[1]; - _front.length = cast(ushort) (startingBuffer[2]) * 256 + startingBuffer[3]; + ushort length = cast(ushort) (startingBuffer[2]) * 256 + startingBuffer[3]; - if(_front.length < 2) + if(length < 2) throw new Exception("wtf"); - _front.length -= 2; // the length in the file includes the block header, but we just want the data here + length -= 2; // the length in the file includes the block header, but we just want the data here - _front.data = new ubyte[](_front.length); + _front.data = new ubyte[](length); read = f.rawRead(_front.data); - if(read.length != _front.length) - throw new Exception("didn't read the file right, got " ~ to!string(read.length) ~ " instead of " ~ to!string(_front.length)); + if(read.length != length) + throw new Exception("didn't read the file right, got " ~ to!string(read.length) ~ " instead of " ~ to!string(length)); _frontIsValid = true; } @@ -60,7 +59,7 @@ struct LazyJpegFile { } } -// http://www.obrador.com/essentialjpeg/headerinfo.htm +// returns width, height Tuple!(int, int) getSizeFromFile(string filename) { import std.stdio; @@ -71,6 +70,7 @@ Tuple!(int, int) getSizeFromFile(string filename) { auto firstSection = jpeg.front(); jpeg.popFront(); + // commented because exif and jfif are both readable by this so no need to be picky //if(firstSection.identifier != 0xe0) //throw new Exception("bad header"); diff --git a/mysql.d b/mysql.d index 9e5ee1f..3b32954 100644 --- a/mysql.d +++ b/mysql.d @@ -699,16 +699,22 @@ Ret queryOneRow(Ret = Row, DB, string file = __FILE__, size_t line = __LINE__, T static if(is(Ret : DataObject) && is(DB == MySql)) { auto res = db.queryDataObject!Ret(sql, t); if(res.empty) - throw new Exception("result was empty", file, line); + throw new EmptyResultException("result was empty", file, line); return res.front; } else static if(is(Ret == Row)) { auto res = db.query(sql, t); if(res.empty) - throw new Exception("result was empty", file, line); + throw new EmptyResultException("result was empty", file, line); return res.front; } else static assert(0, "Unsupported single row query return value, " ~ Ret.stringof); } +class EmptyResultException : Exception { + this(string message, string file = __FILE__, size_t line = __LINE__) { + super(message, file, line); + } +} + /* void main() { diff --git a/png.d b/png.d index 1030741..b7f131a 100644 --- a/png.d +++ b/png.d @@ -230,6 +230,41 @@ ubyte[] writePng(PNG* p) { return a; } +PNGHeader getHeaderFromFile(string filename) { + import std.stdio; + auto file = File(filename, "rb"); + ubyte[12] initialBuffer; // file header + size of first chunk (should be IHDR) + auto data = file.rawRead(initialBuffer[]); + if(data.length != 12) + throw new Exception("couldn't get png file header off " ~ filename); + + if(data[0..8] != [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) + throw new Exception("file " ~ filename ~ " is not a png"); + + auto pos = 8; + uint size; + size |= data[pos++] << 24; + size |= data[pos++] << 16; + size |= data[pos++] << 8; + size |= data[pos++] << 0; + + size += 4; // chunk type + size += 4; // checksum + + ubyte[] more; + more.length = size; + + auto chunk = file.rawRead(more); + if(chunk.length != size) + throw new Exception("couldn't get png image header off " ~ filename); + + + more = data ~ chunk; + + auto png = readPng(more); + return getHeader(png); +} + PNG* readPng(ubyte[] data) { auto p = new PNG; diff --git a/web.d b/web.d index f19c526..5f10723 100644 --- a/web.d +++ b/web.d @@ -1,6 +1,8 @@ module arsd.web; enum RequirePost; +enum RequireHttps; +enum NoAutomaticForm; /// Attribute for the default formatting (html, table, json, etc) struct DefaultFormat { @@ -624,7 +626,10 @@ class ApiProvider : WebDotDBaseType { document.cookie = \"timezone=\" + tz + \"; path=/\"; } - +
    @@ -784,6 +789,8 @@ struct FunctionInfo { bool returnTypeIsDocument; // internal used when wrapping bool returnTypeIsElement; // internal used when wrapping + bool requireHttps; + 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 } @@ -978,6 +985,7 @@ immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Parent FunctionInfo* f = new FunctionInfo; ParameterTypeTuple!(__traits(getMember, Class, member)) fargs; + f.requireHttps = hasAnnotation!(__traits(getMember, Class, member), RequireHttps); f.returnType = ReturnType!(__traits(getMember, Class, member)).stringof; f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, Class, member)) : Document); f.returnTypeIsElement = is(ReturnType!(__traits(getMember, Class, member)) : Element); @@ -1248,6 +1256,10 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint } bool returnedHoldsADocument = false; + string[][string] want; + string format, secondaryFormat; + void delegate(Document d) moreProcessing; + WrapperReturn ret; try { if(fun is null) { @@ -1276,6 +1288,12 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint assert(fun.dispatcher !is null); assert(cgi !is null); + if(fun.requireHttps && !cgi.https) { + cgi.setResponseLocation("https://" ~ cgi.host ~ cgi.logicalScriptName ~ cgi.pathInfo ~ + (cgi.queryString.length ? "?" : "") ~ cgi.queryString); + envelopeFormat = "no-processing"; + goto do_nothing_else; + } if(instantiator.length) { assert(fun !is null); @@ -1287,14 +1305,14 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint result.type = fun.returnType; - string format = cgi.request("format", reflection.defaultOutputFormat); - string secondaryFormat = cgi.request("secondaryFormat", ""); + format = cgi.request("format", reflection.defaultOutputFormat); + 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); + 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("?"); @@ -1313,7 +1331,7 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint } realObject.cgi = cgi; - auto ret = fun.dispatcher(cgi, realObject, want, format, secondaryFormat); + ret = fun.dispatcher(cgi, realObject, want, format, secondaryFormat); if(ret.completed) { envelopeFormat = "no-processing"; goto do_nothing_else; @@ -1336,8 +1354,38 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint debug result.dFullString = e.toString(); if(envelopeFormat == "document" || envelopeFormat == "html") { - auto ipe = cast(InsufficientParametersException) e; - if(ipe !is null) { + if(auto fve = cast(FormValidationException) e) { + auto thing = fve.formFunction; + if(thing is null) + thing = fun; + fun = thing; + ret = fun.dispatcher(cgi, realObject, want, format, secondaryFormat); + result.result = ret.value; + + if(fun.returnTypeIsDocument) + returnedHoldsADocument = true; // we don't replace the success flag, so this ensures no double document + + moreProcessing = (Document d) { + Form f; + if(fve.getForm !is null) + f = fve.getForm(d); + else + f = d.requireSelector!Form("form"); + + foreach(k, v; want) + f.setValue(k, v[$-1]); + + foreach(idx, failure; fve.failed) { + auto ele = f.querySelector("[name=\""~failure~"\"]"); + ele.addClass("validation-failed"); + ele.dataset.validationMessage = fve.messagesForUser[idx]; + ele.parentNode.addChild("span", fve.messagesForUser[idx]).addClass("validation-message"); + } + + if(fve.postProcessor !is null) + fve.postProcessor(d, f, fve); + }; + } else if(auto ipe = cast(InsufficientParametersException) e) { assert(fun !is null); Form form; if(fun.createForm !is null) { @@ -1376,6 +1424,8 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint foreach(k, v; cgi.get) form.setValue(k, v); // carry what we have for params over + foreach(k, v; cgi.post) + form.setValue(k, v); // carry what we have for params over result.result.str = form.toString(); } else { @@ -1504,6 +1554,7 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint document = e.parentDocument; + //assert(0, document.toString()); // FIXME: a wee bit slow, esp if func return element e.innerHTML = returned; if(fun !is null) @@ -1532,6 +1583,9 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint } } + if(moreProcessing !is null) + moreProcessing(document); + returned = document.toString; } } @@ -1623,6 +1677,13 @@ mixin template FancyMain(T, Args...) { /// Like FancyMain, but you can pass a custom subclass of Cgi mixin template CustomCgiFancyMain(CustomCgi, T, Args...) if(is(CustomCgi : Cgi)) { void fancyMainFunction(Cgi cgi) { //string[] args) { + version(catch_segfault) { + import etc.linux.memoryerror; + // NOTE: this is private on stock dmd right now, just + // open the file (src/druntime/import/etc/linux/memoryerror.d) and make it public + registerMemoryErrorHandler(); + } + // auto cgi = new Cgi; // there must be a trailing slash for relative links.. @@ -2192,16 +2253,146 @@ Element toXmlElement(T)(Document document, T t) { /// Done automatically by the wrapper function class InsufficientParametersException : Exception { - this(string functionName, string msg) { - super(functionName ~ ": " ~ msg); + this(string functionName, string msg, string file = __FILE__, size_t line = __LINE__) { + this.functionName = functionName; + super(functionName ~ ": " ~ msg, file, line); } + + string functionName; + string argumentName; + string formLocation; +} + +/// helper for param checking +bool isValidLookingEmailAddress(string e) { + import std.net.isemail; + return isEmail(e, CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid; +} + +/// Looks for things like a > 10); + T checkCgiParam(T)(string name, T defaultValue, bool delegate(T) ok, string messageForUser = null) { + auto value = cgi.request(name, defaultValue); + if(!ok(value)) { + failure(name, messageForUser); + } + + return value; + } + + void finish( + immutable(FunctionInfo)* formFunction, + Form delegate(Document) getForm, + void delegate(Document, Form, FormValidationException) postProcessor, + string file = __FILE__, size_t line = __LINE__) + { + if(failed.length) + throw new FormValidationException( + formFunction, getForm, postProcessor, + failed, messagesForUser, + to!string(failed), file, line); + } +} + +auto check(alias field)(ParamCheckHelper helper, bool delegate(typeof(field)) ok, string messageForUser = null) { + if(!ok(field)) { + helper.failure(field.stringof, messageForUser); + } + + return field; +} + + +class FormValidationException : Exception { + this( + immutable(FunctionInfo)* formFunction, + Form delegate(Document) getForm, + void delegate(Document, Form, FormValidationException) postProcessor, + string[] failed, string[] messagesForUser, + string msg, string file = __FILE__, size_t line = __LINE__) + { + this.formFunction = formFunction; + this.getForm = getForm; + this.postProcessor = postProcessor; + this.failed = failed; + this.messagesForUser = messagesForUser; + + super(msg, file, line); + } + + // this will be called by the automatic catch + // it goes: Document d = formatAs(formFunction, document); + // then : Form f = getForm(d); + // it will add the values used in the current call to the form with the error conditions + // and finally, postProcessor(d, f, this); + immutable(FunctionInfo)* formFunction; + Form delegate(Document) getForm; + void delegate(Document, Form, FormValidationException) postProcessor; + string[] failed; + string[] messagesForUser; } /// throw this if a paramater is invalid. Automatic forms may present this to the user in a new form. (FIXME: implement that) class InvalidParameterException : Exception { - this(string param, string value, string expected) { - super("bad param: " ~ param ~ ". got: " ~ value ~ ". Expected: " ~expected); + this(string param, string value, string expected, string file = __FILE__, size_t line = __LINE__) { + this.param = param; + super("bad param: " ~ param ~ ". got: " ~ value ~ ". Expected: " ~expected, file, line); } + + /* + The way these are handled automatically is if something fails, web.d will + redirect the user to + + formLocation ~ "?" ~ encodeVariables(cgi.get|postArray) + */ + + string functionName; + string param; + string formLocation; } /// convenience for throwing InvalidParameterExceptions @@ -2347,7 +2538,9 @@ WrapperFunction generateWrapper(alias ObjectType, string funName, alias f, R)(Re args[i] = cast() cgi.files[using]; // casting away const for the assignment to compile FIXME: shouldn't be needed } else { if(using !in sargs) { - static if(parameterHasDefault!(f)(i)) { + static if(isArray!(type) && !isSomeString!(type)) { + args[i] = null; + } else static if(parameterHasDefault!(f)(i)) { args[i] = mixin(parameterDefaultOf!(f)(i)); } else { throw new InsufficientParametersException(funName, "arg " ~ name ~ " is not present"); @@ -3132,10 +3325,10 @@ struct TemplateFilters { } string plural(string replacement, string[] args, in Element, string) { - return pluralHelper(args.length ? args[0] : null, replacement); + return pluralHelper(args.length ? args[0] : null, replacement, args.length > 1 ? args[1] : null); } - string pluralHelper(string number, string word) { + string pluralHelper(string number, string word, string pluralWord = null) { if(word.length == 0) return word; @@ -3146,12 +3339,17 @@ struct TemplateFilters { if(count == 1) return word; // it isn't actually plural + if(pluralWord !is null) + return pluralWord; + switch(word[$ - 1]) { case 's': case 'a', 'e', 'i', 'o', 'u': return word ~ "es"; case 'f': return word[0 .. $-1] ~ "ves"; + case 'y': + return word[0 .. $-1] ~ "ies"; default: return word ~ "s"; }