| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| |
| var Mime = (function() { |
| // registerType(name, mime-type, mime-type, ...) |
| // |
| // Available in query server sandbox. TODO: The list is cleared on reset. |
| // This registers a particular name with the set of mimetypes it can handle. |
| // Whoever registers last wins. |
| // |
| // Example: |
| // registerType("html", "text/html; charset=utf-8"); |
| |
| var mimesByKey = {}; |
| var keysByMime = {}; |
| function registerType() { |
| var mimes = [], key = arguments[0]; |
| for (var i=1; i < arguments.length; i++) { |
| mimes.push(arguments[i]); |
| }; |
| mimesByKey[key] = mimes; |
| for (var i=0; i < mimes.length; i++) { |
| keysByMime[mimes[i]] = key; |
| }; |
| } |
| |
| // Some default types |
| // Ported from Ruby on Rails |
| // Build list of Mime types for HTTP responses |
| // http://www.iana.org/assignments/media-types/ |
| // http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/mime_types.rb |
| |
| registerType("all", "*/*"); |
| registerType("text", "text/plain; charset=utf-8", "txt"); |
| registerType("html", "text/html; charset=utf-8"); |
| registerType("xhtml", "application/xhtml+xml", "xhtml"); |
| registerType("xml", "application/xml", "text/xml", "application/x-xml"); |
| registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); |
| registerType("css", "text/css"); |
| registerType("ics", "text/calendar"); |
| registerType("csv", "text/csv"); |
| registerType("rss", "application/rss+xml"); |
| registerType("atom", "application/atom+xml"); |
| registerType("yaml", "application/x-yaml", "text/yaml"); |
| // just like Rails |
| registerType("multipart_form", "multipart/form-data"); |
| registerType("url_encoded_form", "application/x-www-form-urlencoded"); |
| // http://www.ietf.org/rfc/rfc4627.txt |
| registerType("json", "application/json", "text/x-json"); |
| |
| |
| var providesUsed = false; |
| var mimeFuns = []; |
| var responseContentType = null; |
| |
| function provides(type, fun) { |
| providesUsed = true; |
| mimeFuns.push([type, fun]); |
| }; |
| |
| function resetProvides() { |
| // set globals |
| providesUsed = false; |
| mimeFuns = []; |
| responseContentType = null; |
| }; |
| |
| function runProvides(req, ddoc) { |
| var supportedMimes = [], bestFun, bestKey = null, accept = req.headers["Accept"]; |
| if (req.query && req.query.format) { |
| bestKey = req.query.format; |
| responseContentType = mimesByKey[bestKey][0]; |
| } else if (accept) { |
| // log("using accept header: "+accept); |
| mimeFuns.reverse().forEach(function(mimeFun) { |
| var mimeKey = mimeFun[0]; |
| if (mimesByKey[mimeKey]) { |
| supportedMimes = supportedMimes.concat(mimesByKey[mimeKey]); |
| } |
| }); |
| responseContentType = Mimeparse.bestMatch(supportedMimes, accept); |
| bestKey = keysByMime[responseContentType]; |
| } else { |
| // just do the first one |
| bestKey = mimeFuns[0][0]; |
| responseContentType = mimesByKey[bestKey][0]; |
| } |
| |
| if (bestKey) { |
| for (var i=0; i < mimeFuns.length; i++) { |
| if (mimeFuns[i][0] == bestKey) { |
| bestFun = mimeFuns[i][1]; |
| break; |
| } |
| }; |
| }; |
| |
| if (bestFun) { |
| return bestFun.call(ddoc); |
| } else { |
| var supportedTypes = mimeFuns.map(function(mf) { |
| return mimesByKey[mf[0]].join(', ') || mf[0]; |
| }); |
| throw(["error","not_acceptable", |
| "Content-Type "+(accept||bestKey)+" not supported, try one of: "+supportedTypes.join(', ')]); |
| } |
| }; |
| |
| |
| return { |
| registerType : registerType, |
| provides : provides, |
| resetProvides : resetProvides, |
| runProvides : runProvides, |
| providesUsed : function () { |
| return providesUsed; |
| }, |
| responseContentType : function () { |
| return responseContentType; |
| } |
| }; |
| })(); |
| |
| |
| |
| |
| //// |
| //// Render dispatcher |
| //// |
| //// |
| //// |
| //// |
| |
| var Render = (function() { |
| var new_header = false; |
| var chunks = []; |
| |
| |
| // Start chunks |
| var startResp = {}; |
| function start(resp) { |
| startResp = resp || {}; |
| new_header = true; |
| }; |
| |
| function sendStart() { |
| startResp = applyContentType((startResp || {}), Mime.responseContentType()); |
| respond(["start", chunks, startResp]); |
| chunks = []; |
| startResp = {}; |
| new_header = false; |
| } |
| |
| function applyContentType(resp, responseContentType) { |
| resp["headers"] = resp["headers"] || {}; |
| if (responseContentType) { |
| resp["headers"]["Content-Type"] = resp["headers"]["Content-Type"] || responseContentType; |
| } |
| return resp; |
| } |
| |
| function send(chunk) { |
| chunks.push(chunk.toString()); |
| }; |
| |
| function blowChunks(label) { |
| if (new_header) { |
| respond([label||"chunks", chunks, startResp]); |
| new_header = false; |
| } |
| else { |
| respond([label||"chunks", chunks]); |
| } |
| chunks = []; |
| }; |
| |
| var gotRow = false, lastRow = false; |
| function getRow() { |
| if (lastRow) return null; |
| if (!gotRow) { |
| gotRow = true; |
| sendStart(); |
| } else { |
| blowChunks(); |
| } |
| var json = JSON.parse(readline()); |
| if (json[0] == "list_end") { |
| lastRow = true; |
| return null; |
| } |
| if (json[0] != "list_row") { |
| throw(["fatal", "list_error", "not a row '" + json[0] + "'"]); |
| } |
| return json[1]; |
| }; |
| |
| |
| function maybeWrapResponse(resp) { |
| var type = typeof resp; |
| if ((type == "string") || (type == "xml")) { |
| return {body:resp}; |
| } else { |
| return resp; |
| } |
| }; |
| |
| // from http://javascript.crockford.com/remedial.html |
| function typeOf(value) { |
| var s = typeof value; |
| if (s === 'object') { |
| if (value) { |
| if (value instanceof Array) { |
| s = 'array'; |
| } |
| } else { |
| s = 'null'; |
| } |
| } |
| return s; |
| }; |
| |
| function isDocRequestPath(info) { |
| var path = info.path; |
| return path.length > 5; |
| }; |
| |
| function runShow(fun, ddoc, args) { |
| try { |
| resetList(); |
| Mime.resetProvides(); |
| var resp = fun.apply(ddoc, args) || {}; |
| resp = maybeWrapResponse(resp); |
| |
| // handle list() style API |
| if (chunks.length && chunks.length > 0) { |
| resp.headers = resp.headers || {}; |
| for(var header in startResp) { |
| resp.headers[header] = startResp[header]; |
| } |
| resp.body = chunks.join("") + (resp.body || ""); |
| resetList(); |
| } |
| |
| if (Mime.providesUsed()) { |
| var provided_resp = Mime.runProvides(args[1], ddoc) || {}; |
| provided_resp = maybeWrapResponse(provided_resp); |
| resp.body = (resp.body || "") + chunks.join(""); |
| resp.body += provided_resp.body || ""; |
| resp = applyContentType(resp, Mime.responseContentType()); |
| resetList(); |
| } |
| |
| var type = typeOf(resp); |
| if (type == 'object' || type == 'string') { |
| respond(["resp", maybeWrapResponse(resp)]); |
| } else { |
| throw(["error", "render_error", "undefined response from show function"]); |
| } |
| } catch(e) { |
| if (args[0] === null && isDocRequestPath(args[1])) { |
| throw(["error", "not_found", "document not found"]); |
| } else { |
| renderError(e, fun.toString()); |
| } |
| } |
| }; |
| |
| function runUpdate(fun, ddoc, args) { |
| try { |
| var method = args[1].method; |
| // for analytics logging applications you might want to remove the next line |
| if (method == "GET") throw(["error","method_not_allowed","Update functions do not allow GET"]); |
| var result = fun.apply(ddoc, args); |
| var doc = result[0]; |
| var resp = result[1]; |
| var type = typeOf(resp); |
| if (type == 'object' || type == 'string') { |
| respond(["up", doc, maybeWrapResponse(resp)]); |
| } else { |
| throw(["error", "render_error", "undefined response from update function"]); |
| } |
| } catch(e) { |
| renderError(e, fun.toString()); |
| } |
| }; |
| |
| function resetList() { |
| gotRow = false; |
| lastRow = false; |
| chunks = []; |
| startResp = {}; |
| new_header = false; |
| }; |
| |
| function runList(listFun, ddoc, args) { |
| try { |
| Mime.resetProvides(); |
| resetList(); |
| var head = args[0]; |
| var req = args[1]; |
| var tail = listFun.apply(ddoc, args); |
| |
| if (Mime.providesUsed()) { |
| tail = Mime.runProvides(req, ddoc); |
| } |
| if (!gotRow) getRow(); |
| if (typeof tail != "undefined") { |
| chunks.push(tail); |
| } |
| blowChunks("end"); |
| } catch(e) { |
| renderError(e, listFun.toString()); |
| } |
| }; |
| |
| function runRewrite(fun, ddoc, args) { |
| var result; |
| try { |
| result = fun.apply(ddoc, args); |
| } catch(error) { |
| renderError(error, fun.toString(), "rewrite_error"); |
| } |
| |
| if (!result) { |
| respond(["no_dispatch_rule"]); |
| return; |
| } |
| |
| if (typeof result === "string") { |
| result = {path: result, method: args[0].method}; |
| } |
| respond(["ok", result]); |
| } |
| |
| function renderError(e, funSrc, errType) { |
| if (e.error && e.reason || e[0] == "error" || e[0] == "fatal") { |
| throw(e); |
| } else { |
| var logMessage = "function raised error: " + |
| (e.toSource ? e.toSource() : e.toString()) + " \n" + |
| "stacktrace: " + e.stack; |
| log(logMessage); |
| throw(["error", errType || "render_error", logMessage]); |
| } |
| }; |
| |
| function escapeHTML(string) { |
| return string && string.replace(/&/g, "&") |
| .replace(/</g, "<") |
| .replace(/>/g, ">"); |
| }; |
| |
| |
| return { |
| start : start, |
| send : send, |
| getRow : getRow, |
| show : function(fun, ddoc, args) { |
| // var showFun = Couch.compileFunction(funSrc); |
| runShow(fun, ddoc, args); |
| }, |
| update : function(fun, ddoc, args) { |
| // var upFun = Couch.compileFunction(funSrc); |
| runUpdate(fun, ddoc, args); |
| }, |
| list : function(fun, ddoc, args) { |
| runList(fun, ddoc, args); |
| }, |
| rewrite : function(fun, ddoc, args) { |
| runRewrite(fun, ddoc, args); |
| } |
| }; |
| })(); |
| |
| // send = Render.send; |
| // getRow = Render.getRow; |
| // start = Render.start; |
| |
| // unused. this will be handled in the Erlang side of things. |
| // function htmlRenderError(e, funSrc) { |
| // var msg = ["<html><body><h1>Render Error</h1>", |
| // "<p>JavaScript function raised error: ", |
| // e.toString(), |
| // "</p><h2>Stacktrace:</h2><code><pre>", |
| // escapeHTML(e.stack), |
| // "</pre></code><h2>Function source:</h2><code><pre>", |
| // escapeHTML(funSrc), |
| // "</pre></code></body></html>"].join(''); |
| // return {body:msg}; |
| // }; |