blob: 946701ef5438d168e1fefd4d38b1a3f864e92111 [file] [log] [blame]
// 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
};
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};
// };