diff --git a/cgi.d b/cgi.d index bf236e8..242e4ec 100644 --- a/cgi.d +++ b/cgi.d @@ -52,6 +52,14 @@ +/ module arsd.cgi; +/* + + To do a file download offer in the browser: + + cgi.setResponseContentType("text/csv"); + cgi.header("Content-Disposition: attachment; filename=\"customers.csv\""); +*/ + // FIXME: the location header is supposed to be an absolute url I guess. // FIXME: would be cool to flush part of a dom document before complete diff --git a/email.d b/email.d new file mode 100644 index 0000000..d379283 --- /dev/null +++ b/email.d @@ -0,0 +1,153 @@ +module arsd.email; + +import std.net.curl; +pragma(lib, "curl"); + +import std.base64; + +// SEE ALSO: std.net.curl.SMTP + +struct RelayInfo { + string server; + string username; + string password; +} + +class EmailMessage { + void setHeader(string name, string value) { + headers ~= name ~ ": " ~ value; + } + + string[] to; + string[] cc; + string[] bcc; + string from; + string replyTo; + string inReplyTo; + string textBody; + string htmlBody; + string subject; + + string[] headers; + + private bool isMime = false; + private bool isHtml = false; + + void setTextBody(string text) {} + void setHtmlBody(string html) { + isMime = true; + isHtml = true; + htmlBody = html; + + import arsd.htmltotext; + if(textBody is null) + textBody = htmlToText(html); + } + + struct MimeAttachment { + string type; + string filename; + const(void)[] content; + } + + const(MimeAttachment)[] attachments; + + void addAttachment(string mimeType, string filename, in void[] content) { + isMime = true; + attachments ~= MimeAttachment(mimeType, filename, content); + } + + + string toString() { + string boundary = "0016e64be86203dd36047610926a"; // FIXME + + assert(!isHtml || (isHtml && isMime)); + + auto headers = this.headers; + + string toHeader = "To: "; + bool toHeaderOutputted = false; + foreach(t; to) { + if(toHeaderOutputted) + toHeader ~= ", "; + else + toHeaderOutputted = true; + + toHeader ~= t; + } + + if(to.length) + headers ~= toHeader; + + if(subject !is null) + headers ~= "Subject: " ~ subject; + + if(isMime) + headers ~= "MIME-Version: 1.0"; + + if(attachments.length) + headers ~= "Content-Type: multipart/mixed; boundary=" ~ boundary; + else if(isHtml) + headers ~= "Content-Type: multipart/alternative; boundary=" ~ boundary; + else + headers ~= "Content-Type: text/plain; charset=UTF-8"; + + string msg; + msg.reserve(htmlBody.length + textBody.length + 1024); + + foreach(header; headers) + msg ~= header ~ "\r\n"; + if(msg.length) // has headers + msg ~= "\r\n"; + + if(isMime) { + msg ~= "--" ~ boundary ~ "\r\n"; + msg ~= "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; + } + + msg ~= textBody; + + if(isMime) + msg ~= "\r\n--" ~ boundary; + if(isHtml) { + msg ~= "\r\n"; + msg ~= "Content-Type: text/html; charset=UTF-8\r\n\r\n"; + msg ~= htmlBody; + msg ~= "\r\n--" ~ boundary; + } + + foreach(attachment; attachments) { + assert(isMime); + msg ~= "\r\n"; + msg ~= "Content-Type: " ~ attachment.type ~ "\r\n"; + msg ~= "Content-Disposition: attachment; filename=\""~attachment.filename~"\"\r\n"; + msg ~= "Content-Transfer-Encoding: base64\r\n"; + msg ~= "\r\n"; + msg ~= Base64.encode(cast(const(ubyte)[]) attachment.content); + msg ~= "\r\n--" ~ boundary; + } + + if(isMime) + msg ~= "--\r\n"; + + return msg; + } + + void send(RelayInfo mailServer = RelayInfo("smtp://localhost")) { + auto smtp = new SMTP(mailServer.server); + const(char)[][] allRecipients = cast(const(char)[][]) (to ~ cc ~ bcc); // WTF cast + smtp.mailTo(allRecipients); + smtp.mailFrom = from; + smtp.message = this.toString(); + smtp.perform(); + } +} + +void email(string to, string subject, string message, string from) { + auto msg = new EmailMessage(); + msg.from = from; + msg.to = [to]; + msg.subject = subject; + msg.textBody = message; + msg.send(); +} diff --git a/html.d b/html.d index 2883e78..b0d3734 100644 --- a/html.d +++ b/html.d @@ -233,61 +233,10 @@ Html linkify(string text) { /// Returns true of the string appears to be html/xml - if it matches the pattern /// for tags or entities. bool appearsToBeHtml(string src) { - return false; + import std.regex; + return cast(bool) match(src, `.*\<[A-Za-z]+>.*`); } -/+ -void qsaFilter(string logicalScriptName) { - string logicalScriptName = siteBase[0 .. $-1]; - - foreach(a; document.querySelectorAll("a[qsa]")) { - string href = logicalScriptName ~ _cgi.pathInfo ~ "?"; - - int matches, possibilities; - - string[][string] vars; - foreach(k, v; _cgi.getArray) - vars[k] = cast(string[]) v; - foreach(k, v; decodeVariablesSingle(a.qsa)) { - if(k in _cgi.get && _cgi.get[k] == v) - matches++; - possibilities++; - - if(k !in vars || vars[k].length <= 1) - vars[k] = [v]; - else - assert(0, "qsa doesn't work here"); - } - - string[] clear = a.getAttribute("qsa-clear").split("&"); - clear ~= "ajaxLoading"; - if(a.parentNode !is null) - clear ~= a.parentNode.getAttribute("qsa-clear").split("&"); - - bool outputted = false; - varskip: foreach(k, varr; vars) { - foreach(item; clear) - if(k == item) - continue varskip; - foreach(v; varr) { - if(outputted) - href ~= "&"; - else - outputted = true; - - href ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); - } - } - - a.href = href; - - a.removeAttribute("qsa"); - - if(matches == possibilities) - a.addClass("current"); - } -} -+/ string favicon(Document document) { auto item = document.querySelector("link[rel~=icon]"); if(item !is null) diff --git a/htmltotext.d b/htmltotext.d new file mode 100644 index 0000000..163f8d4 --- /dev/null +++ b/htmltotext.d @@ -0,0 +1,155 @@ +module arsd.htmltotext; + +import arsd.dom; +import std.string; +import std.array : replace; + +string repeat(string s, int num) { + string ret; + foreach(i; 0 .. num) + ret ~= s; + return ret; +} + +import std.stdio; + +static import std.regex; +string htmlToText(string html, bool wantWordWrap = true, int wrapAmount = 74) { + Document document = new Document; + + + html = html.replace(" ", " "); + html = html.replace(" ", " "); + html = html.replace(" ", " "); + html = html.replace("\n", ""); + html = html.replace("\r", ""); + html = std.regex.replace(html, std.regex.regex("[\n\r\t \u00a0]+", "gm"), " "); + + document.parse("" ~ html ~ ""); + + Element start; + auto bod = document.getElementsByTagName("body"); + if(bod.length) + start = bod[0]; + else + start = document.root; + + start.innerHTML = start.innerHTML().replace("
", "\u0001"); + + again: + string result = ""; + foreach(ele; start.tree) { + if(ele is start) continue; + if(ele.nodeType != 1) continue; + + switch(ele.tagName) { + case "b": + case "strong": + ele.innerText = "*" ~ ele.innerText ~ "*"; + ele.stripOut(); + goto again; + break; + case "i": + case "em": + ele.innerText = "/" ~ ele.innerText ~ "/"; + ele.stripOut(); + goto again; + break; + case "u": + ele.innerText = "_" ~ ele.innerText ~ "_"; + ele.stripOut(); + goto again; + break; + case "h1": + ele.innerText = "\r" ~ ele.innerText ~ "\n" ~ repeat("=", ele.innerText.length) ~ "\r"; + ele.stripOut(); + goto again; + break; + case "h2": + ele.innerText = "\r" ~ ele.innerText ~ "\n" ~ repeat("-", ele.innerText.length) ~ "\r"; + ele.stripOut(); + goto again; + break; + case "h3": + ele.innerText = "\r" ~ ele.innerText.toUpper ~ "\r"; + ele.stripOut(); + goto again; + break; + case "p": + /* + if(ele.innerHTML.length > 1) + ele.innerHTML = "\r" ~ wrap(ele.innerHTML) ~ "\r"; + ele.stripOut(); + goto again; + */ + break; + case "a": + string href = ele.getAttribute("href"); + if(href) { + if(ele.innerText != href) + ele.innerText = ele.innerText ~ " <" ~ href ~ "> "; + } + ele.stripOut(); + goto again; + break; + case "ol": + case "ul": + ele.innerHTML = "\r" ~ ele.innerHTML ~ "\r"; + break; + case "li": + ele.innerHTML = "\t* " ~ ele.innerHTML ~ "\r"; + ele.stripOut(); + break; + case "sup": + ele.innerText = "^" ~ ele.innerText; + ele.stripOut(); + break; + /* + case "img": + string alt = ele.getAttribute("alt"); + if(alt) + result ~= ele.alt; + break; + */ + default: + ele.stripOut(); + goto again; + } + } + + again2: + start.innerHTML = start.innerHTML().replace("\u0001", "\n"); + + foreach(ele; start.tree) { + if(ele.tagName == "p") { + if(strip(ele.innerText()).length > 1) { + string res = ""; + string all = ele.innerText().replace("\n \n", "\n\n"); + foreach(part; all.split("\n\n")) + res ~= "\r" ~ strip( wantWordWrap ? wrap(part, /*74*/ wrapAmount) : part ) ~ "\r"; + ele.innerText = res; + } else + ele.innerText = strip(ele.innerText); + ele.stripOut(); + goto again2; + } + } + + result = start.innerText(); + + result = result.replace("\r ", "\r"); + result = result.replace(" \r", "\r"); + + //result = result.replace("\u00a0", " "); + + + result = squeeze(result, "\r"); + result = result.replace("\r", "\n\n"); + + result = result.replace("舗", "'"); // HACK: this shouldn't be needed, but apparently is in practice surely due to a bug elsewhere + result = result.replace(""", "\""); // HACK: this shouldn't be needed, but apparently is in practice surely due to a bug elsewhere + //result = htmlEntitiesDecode(result); // for special chars mainly + + //a = std.regex.replace(a, std.regex.regex("(\n\t)+", "g"), "\n"); //\t"); + return result.strip; +} diff --git a/web.d b/web.d index 4e743a1..ac2c379 100644 --- a/web.d +++ b/web.d @@ -3201,6 +3201,58 @@ Table structToTable(T)(Document document, T s, string[] fieldsToSkip = null) if( } } +/// This adds a custom attribute to links in the document called qsa which modifies the values on the query string +void translateQsa(Document document, Cgi cgi, string logicalScriptName = null) { + if(logicalScriptName is null) + logicalScriptName = cgi.scriptName; + + foreach(a; document.querySelectorAll("a[qsa]")) { + string href = logicalScriptName ~ cgi.pathInfo ~ "?"; + + int matches, possibilities; + + string[][string] vars; + foreach(k, v; cgi.getArray) + vars[k] = cast(string[]) v; + foreach(k, v; decodeVariablesSingle(a.qsa)) { + if(k in cgi.get && cgi.get[k] == v) + matches++; + possibilities++; + + if(k !in vars || vars[k].length <= 1) + vars[k] = [v]; + else + assert(0, "qsa doesn't work here"); + } + + string[] clear = a.getAttribute("qsa-clear").split("&"); + clear ~= "ajaxLoading"; + if(a.parentNode !is null) + clear ~= a.parentNode.getAttribute("qsa-clear").split("&"); + + bool outputted = false; + varskip: foreach(k, varr; vars) { + foreach(item; clear) + if(k == item) + continue varskip; + foreach(v; varr) { + if(outputted) + href ~= "&"; + else + outputted = true; + + href ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); + } + } + + a.href = href; + + a.removeAttribute("qsa"); + + if(matches == possibilities) + a.addClass("current"); + } +} /// This uses reflection info to generate Javascript that can call the server with some ease. /// Also includes javascript base (see bottom of this file)