summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--etc/couchdb/default.ini.tpl.in1
-rw-r--r--etc/couchdb/local_dev.ini3
-rw-r--r--share/server/main.js182
-rw-r--r--share/www/script/couch_tests.js253
-rw-r--r--src/couchdb/Makefile.am2
-rw-r--r--src/couchdb/couch_external_manager.erl6
-rw-r--r--src/couchdb/couch_external_server.erl28
-rw-r--r--src/couchdb/couch_httpd_external.erl93
-rw-r--r--src/couchdb/couch_httpd_form.erl114
-rw-r--r--src/couchdb/couch_httpd_misc_handlers.erl4
-rw-r--r--src/couchdb/couch_query_servers.erl13
11 files changed, 641 insertions, 58 deletions
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 06fbee1f..b06c4692 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -47,6 +47,7 @@ _restart = {couch_httpd_misc_handlers, handle_restart_req}
[httpd_db_handlers]
_view = {couch_httpd_view, handle_view_req}
_temp_view = {couch_httpd_view, handle_temp_view_req}
+_form = {couch_httpd_form, handle_form_req}
; The external module takes an optional argument allowing you to narrow it to a
; single script. Otherwise the script name is inferred from the first path section
diff --git a/etc/couchdb/local_dev.ini b/etc/couchdb/local_dev.ini
index 2d26b11f..53a75a51 100644
--- a/etc/couchdb/local_dev.ini
+++ b/etc/couchdb/local_dev.ini
@@ -17,3 +17,6 @@ level = error
[update_notification]
;unique notifier name=/full/path/to/exe -with "cmd line arg"
+
+[test]
+foo = bar
diff --git a/share/server/main.js b/share/server/main.js
index 73fefe19..17387299 100644
--- a/share/server/main.js
+++ b/share/server/main.js
@@ -32,12 +32,181 @@ log = function(message) {
print(toJSON({log: toJSON(message)}));
}
+// mimeparse.js
+// http://code.google.com/p/mimeparse/
+// Code with comments: http://mimeparse.googlecode.com/svn/trunk/mimeparse.js
+// Tests: http://mimeparse.googlecode.com/svn/trunk/mimeparse-js-test.html
+// Ported from version 0.1.2
+
+var Mimeparse = (function() {
+ function strip(string) {
+ return string.replace(/^\s+/, '').replace(/\s+$/, '')
+ };
+ function parseRanges(ranges) {
+ var parsedRanges = [], rangeParts = ranges.split(",");
+ for (var i=0; i < rangeParts.length; i++) {
+ parsedRanges.push(publicMethods.parseMediaRange(rangeParts[i]))
+ };
+ return parsedRanges;
+ };
+ var publicMethods = {
+ parseMimeType : function(mimeType) {
+ var fullType, typeParts, params = {}, parts = mimeType.split(';');
+ for (var i=0; i < parts.length; i++) {
+ var p = parts[i].split('=');
+ if (p.length == 2) {
+ params[strip(p[0])] = strip(p[1]);
+ }
+ };
+ fullType = parts[0].replace(/^\s+/, '').replace(/\s+$/, '');
+ if (fullType == '*') fullType = '*/*';
+ typeParts = fullType.split('/');
+ return [typeParts[0], typeParts[1], params];
+ },
+ parseMediaRange : function(range) {
+ var q, parsedType = this.parseMimeType(range);
+ if (!parsedType[2]['q']) {
+ parsedType[2]['q'] = '1';
+ } else {
+ q = parseFloat(parsedType[2]['q']);
+ if (isNaN(q)) {
+ parsedType[2]['q'] = '1';
+ } else if (q > 1 || q < 0) {
+ parsedType[2]['q'] = '1';
+ }
+ }
+ return parsedType;
+ },
+ fitnessAndQualityParsed : function(mimeType, parsedRanges) {
+ var bestFitness = -1, bestFitQ = 0, target = this.parseMediaRange(mimeType);
+ var targetType = target[0], targetSubtype = target[1], targetParams = target[2];
+
+ for (var i=0; i < parsedRanges.length; i++) {
+ var parsed = parsedRanges[i];
+ var type = parsed[0], subtype = parsed[1], params = parsed[2];
+ if ((type == targetType || type == "*" || targetType == "*") &&
+ (subtype == targetSubtype || subtype == "*" || targetSubtype == "*")) {
+ var matchCount = 0;
+ for (param in targetParams) {
+ if (param != 'q' && params[param] && params[param] == targetParams[param]) {
+ matchCount += 1;
+ }
+ }
+
+ var fitness = (type == targetType) ? 100 : 0;
+ fitness += (subtype == targetSubtype) ? 10 : 0;
+ fitness += matchCount;
+
+ if (fitness > bestFitness) {
+ bestFitness = fitness;
+ bestFitQ = params["q"];
+ }
+ }
+ };
+ return [bestFitness, parseFloat(bestFitQ)];
+ },
+ qualityParsed : function(mimeType, parsedRanges) {
+ return this.fitnessAndQualityParsed(mimeType, parsedRanges)[1];
+ },
+ quality : function(mimeType, ranges) {
+ return this.qualityParsed(mimeType, parseRanges(ranges));
+ },
+
+ // Takes a list of supported mime-types and finds the best
+ // match for all the media-ranges listed in header. The value of
+ // header must be a string that conforms to the format of the
+ // HTTP Accept: header. The value of 'supported' is a list of
+ // mime-types.
+ //
+ // >>> bestMatch(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1')
+ // 'text/xml'
+ bestMatch : function(supported, header) {
+ var parsedHeader = parseRanges(header);
+ var weighted = [];
+ for (var i=0; i < supported.length; i++) {
+ weighted.push([publicMethods.fitnessAndQualityParsed(supported[i], parsedHeader), supported[i]])
+ };
+ weighted.sort();
+ return weighted[weighted.length-1][0][1] ? weighted[weighted.length-1][1] : '';
+ }
+ }
+ return publicMethods;
+})();
+
+
+// this function provides a shortcut for managing responses by Accept header
+respondWith = function(req, responders) {
+ var accept = req.headers["Accept"];
+ if (accept) {
+ var provides = [];
+ for (key in responders) {
+ if (mimesByKey[key]) {
+ provides = provides.concat(mimesByKey[key]);
+ }
+ }
+ var bestMime = Mimeparse.bestMatch(provides, accept);
+ var bestKey = keysByMime[bestMime];
+ var rFunc = responders[bestKey];
+ if (rFunc) {
+ var resp = rFunc();
+ resp["headers"] = resp["headers"] || {};
+ resp["headers"]["Content-Type"] = bestMime;
+ return resp;
+ }
+ }
+ if (responders.default) {
+ return responders[responders.default]();
+ }
+ throw({code:406, body:"Not Acceptable: "+accept});
+}
+
+// whoever registers last wins.
+mimesByKey = {};
+keysByMime = {};
+registerType = function() {
+ 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", "txt");
+registerType("html", "text/html", "application/xhtml+xml", "xhtml");
+registerType("xml", "application/xml", "text/xml", "application/x-xml");
+// http://www.ietf.org/rfc/rfc4627.txt
+registerType("json", "application/json", "text/x-json");
+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");
+
+// ok back to business.
+
try {
// if possible, use evalcx (not always available)
sandbox = evalcx('');
sandbox.emit = emit;
sandbox.sum = sum;
sandbox.log = log;
+ sandbox.respondWith = respondWith;
+ sandbox.registerType = registerType;
} catch (e) {}
// Commands are in the form of json arrays:
@@ -167,6 +336,19 @@ while (cmd = eval(readline())) {
print(toJSON(error));
}
break;
+ case "form":
+ var funSrc = cmd[1];
+ var doc = cmd[2];
+ var req = cmd[3];
+ try {
+ var formFun = compileFunction(funSrc);
+ var rendered = formFun(doc, req);
+ print(toJSON(rendered));
+ } catch (error) {
+ log({error:(error||"undefined error")});
+ print(toJSON(error));
+ }
+ break;
default:
print(toJSON({error: "query_server_error",
reason: "unknown command '" + cmd[0] + "'"}));
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js
index 1eb1836e..5a642f33 100644
--- a/share/www/script/couch_tests.js
+++ b/share/www/script/couch_tests.js
@@ -2029,6 +2029,259 @@ var tests = {
T(xhr.status == 200)
},
+ forms: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var designDoc = {
+ _id:"_design/template",
+ language: "javascript",
+ forms: {
+ "hello" : (function() {
+ return {
+ body : "Hello World"
+ };
+ }).toString(),
+ "just-name" : (function(doc, req) {
+ return {
+ body : "Just " + doc.name
+ };
+ }).toString(),
+ "req-info" : (function(doc, req) {
+ return {
+ json : req
+ }
+ }).toString(),
+ "xml-type" : (function(doc, req) {
+ return {
+ headers : {
+ "Content-Type" : "application/xml"
+ },
+ "body" : <xml><node foo="bar"/></xml>
+ }
+ }).toString(),
+ "no-set-etag" : (function(doc, req) {
+ return {
+ headers : {
+ "Etag" : "skipped"
+ },
+ "body" : "something"
+ }
+ }).toString(),
+ "accept-switch" : (function(doc, req) {
+ if (req.headers["Accept"].match(/image/)) {
+ return {
+ // a 16x16 px version of the CouchDB logo
+ "base64" : ["iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAsV",
+ "BMVEUAAAD////////////////////////5ur3rEBn////////////////wDBL/",
+ "AADuBAe9EB3IEBz/7+//X1/qBQn2AgP/f3/ilpzsDxfpChDtDhXeCA76AQH/v7",
+ "/84eLyWV/uc3bJPEf/Dw/uw8bRWmP1h4zxSlD6YGHuQ0f6g4XyQkXvCA36MDH6",
+ "wMH/z8/yAwX64ODeh47BHiv/Ly/20dLQLTj98PDXWmP/Pz//39/wGyJ7Iy9JAA",
+ "AADHRSTlMAbw8vf08/bz+Pv19jK/W3AAAAg0lEQVR4Xp3LRQ4DQRBD0QqTm4Y5",
+ "zMxw/4OleiJlHeUtv2X6RbNO1Uqj9g0RMCuQO0vBIg4vMFeOpCWIWmDOw82fZx",
+ "vaND1c8OG4vrdOqD8YwgpDYDxRgkSm5rwu0nQVBJuMg++pLXZyr5jnc1BaH4GT",
+ "LvEliY253nA3pVhQqdPt0f/erJkMGMB8xucAAAAASUVORK5CYII="].join(''),
+ headers : {
+ "Content-Type" : "image/png",
+ "Vary" : "Accept" // we set this for proxy caches
+ }
+ };
+ } else {
+ return {
+ "body" : "accepting text requests",
+ headers : {
+ "Content-Type" : "text/html",
+ "Vary" : "Accept"
+ }
+ };
+ }
+ }).toString(),
+ "respondWith" : (function(doc, req) {
+ registerType("foo", "application/foo","application/x-foo");
+ return respondWith(req, {
+ html : function() {
+ return {
+ body:"Ha ha, you said \"" + doc.word + "\"."
+ };
+ },
+ xml : function() {
+ return {
+ body: <xml><node foo={doc.word}/></xml>
+ };
+ },
+ foo : function() {
+ return {
+ body: "foofoo"
+ };
+ },
+ default : "html"
+ });
+ }).toString()
+ }
+ };
+ T(db.save(designDoc).ok);
+
+ var doc = {"word":"plankton", "name":"Rusty"}
+ var resp = db.save(doc);
+ T(resp.ok);
+ var docid = resp.id;
+
+ // form error
+ var xhr = CouchDB.request("GET", "/test_suite_db/_form/");
+ T(xhr.status == 404);
+ T(JSON.parse(xhr.responseText).reason == "Invalid path.");
+
+ // hello template world
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/hello/"+docid);
+ T(xhr.responseText == "Hello World");
+
+ // form with doc
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/just-name/"+docid);
+ T(xhr.responseText == "Just Rusty");
+
+ // form with missing doc
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/just-name/missingdoc");
+ T(xhr.status == 404);
+ var resp = JSON.parse(xhr.responseText);
+ T(resp.error == "not_found");
+ T(resp.reason == "missing");
+
+ // missing design doc
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/missingdoc/just-name/"+docid);
+ T(xhr.status == 404);
+ var resp = JSON.parse(xhr.responseText);
+ T(resp.error == "not_found");
+ T(resp.reason == "missing_design_doc");
+
+ // query parameters
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/req-info/"+docid+"?foo=bar", {
+ headers: {
+ "Accept": "text/html;text/plain;*/*",
+ "X-Foo" : "bar"
+ }
+ });
+ var resp = JSON.parse(xhr.responseText);
+ T(equals(resp.headers["X-Foo"], "bar"));
+ T(equals(resp.query, {foo:"bar"}));
+ T(equals(resp.verb, "GET"));
+ T(equals(resp.path[4], docid));
+ T(equals(resp.info.db_name, "test_suite_db"));
+
+ // returning a content-type
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/xml-type/"+docid);
+ T("application/xml" == xhr.getResponseHeader("Content-Type"));
+ T("Accept" == xhr.getResponseHeader("Vary"));
+
+ // accept header switching
+ // different mime has different etag
+
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/accept-switch/"+docid, {
+ headers: {"Accept": "text/html;text/plain;*/*"}
+ });
+ T("text/html" == xhr.getResponseHeader("Content-Type"));
+ T("Accept" == xhr.getResponseHeader("Vary"));
+ var etag = xhr.getResponseHeader("etag");
+
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/accept-switch/"+docid, {
+ headers: {"Accept": "image/png;*/*"}
+ });
+ T(xhr.responseText.match(/PNG/))
+ T("image/png" == xhr.getResponseHeader("Content-Type"));
+ var etag2 = xhr.getResponseHeader("etag");
+ T(etag2 != etag);
+
+ // proper etags
+ // form with doc
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/just-name/"+docid);
+ // extract the ETag header values
+ etag = xhr.getResponseHeader("etag");
+ // get again with etag in request
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/just-name/"+docid, {
+ headers: {"if-none-match": etag}
+ });
+ // should be 304
+ T(xhr.status == 304);
+
+ // update the doc
+ doc.name = "Crusty";
+ resp = db.save(doc);
+ T(resp.ok);
+ // req with same etag
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/just-name/"+docid, {
+ headers: {"if-none-match": etag}
+ });
+ // status is 200
+ T(xhr.status == 200);
+
+ // get new etag and request again
+ etag = xhr.getResponseHeader("etag");
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/just-name/"+docid, {
+ headers: {"if-none-match": etag}
+ });
+ // should be 304
+ T(xhr.status == 304);
+
+ // update design doc (but not function)
+ designDoc.isChanged = true;
+ T(db.save(designDoc).ok);
+
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/just-name/"+docid, {
+ headers: {"if-none-match": etag}
+ });
+ // should be 304
+ T(xhr.status == 304);
+
+ // update design doc function
+ designDoc.forms["just-name"] = (function(doc, req) {
+ return {
+ body : "Just old " + doc.name
+ };
+ }).toString();
+ T(db.save(designDoc).ok);
+
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/just-name/"+docid, {
+ headers: {"if-none-match": etag}
+ });
+ // status is 200
+ T(xhr.status == 200);
+
+
+ // JS can't set etag
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/no-set-etag/"+docid);
+ // extract the ETag header values
+ etag = xhr.getResponseHeader("etag");
+ T(etag != "skipped")
+
+ // test the respondWith mime matcher
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/respondWith/"+docid, {
+ headers: {
+ "Accept": 'text/html,application/atom+xml; q=0.9'
+ }
+ });
+ T(xhr.getResponseHeader("Content-Type") == "text/html");
+ T(xhr.responseText == "Ha ha, you said \"plankton\".");
+
+ // now with xml
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/respondWith/"+docid, {
+ headers: {
+ "Accept": 'application/xml'
+ }
+ });
+ T(xhr.getResponseHeader("Content-Type") == "application/xml");
+ T(xhr.responseText.match(/plankton/));
+
+ // registering types works
+ xhr = CouchDB.request("GET", "/test_suite_db/_form/template/respondWith/"+docid, {
+ headers: {
+ "Accept": "application/x-foo"
+ }
+ });
+ T(xhr.getResponseHeader("Content-Type") == "application/x-foo");
+ T(xhr.responseText.match(/foofoo/));
+ },
+
compact: function(debug) {
var db = new CouchDB("test_suite_db");
db.deleteDb();
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am
index ae7a06e1..50e51c62 100644
--- a/src/couchdb/Makefile.am
+++ b/src/couchdb/Makefile.am
@@ -55,6 +55,7 @@ source_files = \
couch_httpd.erl \
couch_httpd_db.erl \
couch_httpd_external.erl \
+ couch_httpd_form.erl \
couch_httpd_view.erl \
couch_httpd_misc_handlers.erl \
couch_key_tree.erl \
@@ -90,6 +91,7 @@ compiled_files = \
couch_httpd.beam \
couch_httpd_db.beam \
couch_httpd_external.beam \
+ couch_httpd_form.beam \
couch_httpd_view.beam \
couch_httpd_misc_handlers.beam \
couch_key_tree.beam \
diff --git a/src/couchdb/couch_external_manager.erl b/src/couchdb/couch_external_manager.erl
index d86013eb..9bcef57b 100644
--- a/src/couchdb/couch_external_manager.erl
+++ b/src/couchdb/couch_external_manager.erl
@@ -13,7 +13,7 @@
-module(couch_external_manager).
-behaviour(gen_server).
--export([start_link/0, execute/8, config_change/2]).
+-export([start_link/0, execute/2, config_change/2]).
-export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2]).
-include("couch_db.hrl").
@@ -21,13 +21,13 @@
start_link() ->
gen_server:start_link({local, couch_external_manager}, couch_external_manager, [], []).
-execute(UrlName, Db, Verb, Path, Query, Body, Post, Cookie) ->
+execute(UrlName, JsonReq) ->
Pid = gen_server:call(couch_external_manager, {get, UrlName}),
case Pid of
{error, Reason} ->
Reason;
_ ->
- couch_external_server:execute(Pid, Db, Verb, Path, Query, Body, Post, Cookie)
+ couch_external_server:execute(Pid, JsonReq)
end.
config_change("external", UrlName) ->
diff --git a/src/couchdb/couch_external_server.erl b/src/couchdb/couch_external_server.erl
index afbb729e..49a31c06 100644
--- a/src/couchdb/couch_external_server.erl
+++ b/src/couchdb/couch_external_server.erl
@@ -13,7 +13,7 @@
-module(couch_external_server).
-behaviour(gen_server).
--export([start_link/2, stop/1, execute/8]).
+-export([start_link/2, stop/1, execute/2]).
-export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2, code_change/3]).
-define(TIMEOUT, 5000).
@@ -28,8 +28,8 @@ start_link(Name, Command) ->
stop(Pid) ->
gen_server:cast(Pid, stop).
-execute(Pid, Db, Verb, Path, Query, Body, Post, Cookie) ->
- gen_server:call(Pid, {execute, Db, Verb, Path, Query, Body, Post, Cookie}).
+execute(Pid, JsonReq) ->
+ gen_server:call(Pid, {execute, JsonReq}).
% Gen Server Handlers
@@ -43,18 +43,8 @@ terminate(_Reason, {Name, _Command, Pid}) ->
couch_os_process:stop(Pid),
ok.
-handle_call({execute, Db, Verb, Path, Query, Body, Post, Cookie}, _From, {Name, Command, Pid}) ->
- ?LOG_DEBUG("Query Params ~p",[Query]),
- {ok, Info} = couch_db:get_db_info(Db),
- Json = {[
- {<<"info">>, {Info}},
- {<<"verb">>, Verb},
- {<<"path">>, Path},
- {<<"query">>, to_json_terms(Query)},
- {<<"body">>, Body},
- {<<"form">>, to_json_terms(Post)},
- {<<"cookie">>, to_json_terms(Cookie)}]},
- {reply, couch_os_process:prompt(Pid, Json), {Name, Command, Pid}}.
+handle_call({execute, JsonReq}, _From, {Name, Command, Pid}) ->
+ {reply, couch_os_process:prompt(Pid, JsonReq), {Name, Command, Pid}}.
handle_info({'EXIT', Pid, Reason}, {Name, Command, Pid}) ->
?LOG_INFO("EXTERNAL: Restarting process for ~s (reason: ~w)", [Name, Reason]),
@@ -69,12 +59,4 @@ handle_cast(_Whatever, State) ->
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-% Internal API
-
-to_json_terms(Data) ->
- to_json_terms(Data, []).
-to_json_terms([], Acc) ->
- {lists:reverse(Acc)};
-to_json_terms([{Key, Value} | Rest], Acc) ->
- to_json_terms(Rest, [{list_to_binary(Key), list_to_binary(Value)} | Acc]).
diff --git a/src/couchdb/couch_httpd_external.erl b/src/couchdb/couch_httpd_external.erl
index 84f1c444..7a682ddd 100644
--- a/src/couchdb/couch_httpd_external.erl
+++ b/src/couchdb/couch_httpd_external.erl
@@ -13,6 +13,7 @@
-module(couch_httpd_external).
-export([handle_external_req/2, handle_external_req/3]).
+-export([send_external_response/2, json_req_obj/2]).
-import(couch_httpd,[send_error/4]).
@@ -25,9 +26,44 @@
headers = []
}).
-process_external_req(#httpd{mochi_req=Req,
- method=Verb
- }=HttpReq, Db, Name, Path) ->
+% handle_external_req/2
+% for the old type of config usage:
+% _external = {couch_httpd_external, handle_external_req}
+% with urls like
+% /db/_external/action/design/name
+handle_external_req(#httpd{
+ path_parts=[_DbName, _External, UrlName | _Path]
+ }=HttpReq, Db) ->
+ process_external_req(HttpReq, Db, UrlName);
+handle_external_req(#httpd{path_parts=[_, _]}=Req, _Db) ->
+ send_error(Req, 404, <<"external_server_error">>, <<"No server name specified.">>);
+handle_external_req(Req, _) ->
+ send_error(Req, 404, <<"external_server_error">>, <<"Broken assumption">>).
+
+% handle_external_req/3
+% for this type of config usage:
+% _action = {couch_httpd_external, handle_external_req, <<"action">>}
+% with urls like
+% /db/_action/design/name
+handle_external_req(HttpReq, Db, Name) ->
+ process_external_req(HttpReq, Db, Name).
+
+process_external_req(HttpReq, Db, Name) ->
+
+ Response = couch_external_manager:execute(binary_to_list(Name),
+ json_req_obj(HttpReq, Db)),
+
+ case Response of
+ {unknown_external_server, Msg} ->
+ send_error(HttpReq, 404, <<"external_server_error">>, Msg);
+ _ ->
+ send_external_response(HttpReq, Response)
+ end.
+
+json_req_obj(#httpd{mochi_req=Req,
+ method=Verb,
+ path_parts=Path
+ }, Db) ->
ReqBody = Req:recv_body(),
ParsedForm = case Req:get_primary_header_value("content-type") of
"application/x-www-form-urlencoded" ++ _ ->
@@ -35,40 +71,37 @@ process_external_req(#httpd{mochi_req=Req,
_ ->
[]
end,
- Response = couch_external_manager:execute(binary_to_list(Name),
- Db, Verb, Path, Req:parse_qs(), ReqBody, ParsedForm,
- Req:parse_cookie()),
-
- case Response of
- {unknown_external_server, Msg} ->
- send_error(HttpReq, 404, <<"external_server_error">>, Msg);
- _ ->
- send_external_response(Req, Response)
- end.
+ Headers = Req:get(headers),
+ Hlist = mochiweb_headers:to_list(Headers),
+ {ok, Info} = couch_db:get_db_info(Db),
+ % add headers...
+ {[{<<"info">>, {Info}},
+ {<<"verb">>, Verb},
+ {<<"path">>, Path},
+ {<<"query">>, to_json_terms(Req:parse_qs())},
+ {<<"headers">>, to_json_terms(Hlist)},
+ {<<"body">>, ReqBody},
+ {<<"form">>, to_json_terms(ParsedForm)},
+ {<<"cookie">>, to_json_terms(Req:parse_cookie())}]}.
-handle_external_req(#httpd{
- path_parts=[_DbName, _External, UrlName | Path]
- }=HttpReq, Db) ->
- process_external_req(HttpReq, Db, UrlName, Path);
-handle_external_req(#httpd{path_parts=[_, _]}=Req, _Db) ->
- send_error(Req, 404, <<"external_server_error">>, <<"No server name specified.">>);
-handle_external_req(Req, _) ->
- send_error(Req, 404, <<"external_server_error">>, <<"Broken assumption">>).
+to_json_terms(Data) ->
+ to_json_terms(Data, []).
+to_json_terms([], Acc) ->
+ {lists:reverse(Acc)};
+to_json_terms([{Key, Value} | Rest], Acc) when is_atom(Key) ->
+ to_json_terms(Rest, [{list_to_binary(atom_to_list(Key)), list_to_binary(Value)} | Acc]);
+to_json_terms([{Key, Value} | Rest], Acc) ->
+ to_json_terms(Rest, [{list_to_binary(Key), list_to_binary(Value)} | Acc]).
-handle_external_req(#httpd{
- path_parts=[_DbName, _External | Path]
- }=HttpReq, Db, Name) ->
- process_external_req(HttpReq, Db, Name, Path).
-send_external_response(Req, Response) ->
+send_external_response(#httpd{mochi_req=MochiReq}, Response) ->
#extern_resp_args{
code = Code,
data = Data,
ctype = CType,
headers = Headers
} = parse_external_response(Response),
- ?LOG_DEBUG("External Response ~p",[Response]),
- Resp = Req:respond({Code,
+ Resp = MochiReq:respond({Code,
default_or_content_type(CType, Headers), chunked}),
Resp:write_chunk(Data),
Resp:write_chunk(""),
@@ -87,13 +120,15 @@ parse_external_response({Response}) ->
ctype="application/json"};
{<<"body">>, Value} ->
Args#extern_resp_args{data=Value, ctype="text/html"};
+ {<<"base64">>, Value} ->
+ Args#extern_resp_args{data=couch_util:decodeBase64(Value), ctype="application/binary"};
{<<"headers">>, {Headers}} ->
NewHeaders = lists:map(fun({Header, HVal}) ->
{binary_to_list(Header), binary_to_list(HVal)}
end, Headers),
Args#extern_resp_args{headers=NewHeaders};
_ -> % unknown key
- Msg = lists:flatten(io_lib:format("Invalid data from external server: ~s = ~p", [Key, Value])),
+ Msg = lists:flatten(io_lib:format("Invalid data from external server: ~p", [{Key, Value}])),
throw({external_response_error, Msg})
end
end, #extern_resp_args{}, Response).
diff --git a/src/couchdb/couch_httpd_form.erl b/src/couchdb/couch_httpd_form.erl
new file mode 100644
index 00000000..f4fa2c18
--- /dev/null
+++ b/src/couchdb/couch_httpd_form.erl
@@ -0,0 +1,114 @@
+% 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.
+
+-module(couch_httpd_form).
+
+-export([handle_form_req/2]).
+
+
+-include("couch_db.hrl").
+
+-import(couch_httpd,
+ [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
+ start_json_response/2,send_chunk/2,end_json_response/1,
+ start_chunked_response/3, send_error/4]).
+
+handle_form_req(#httpd{method='GET',path_parts=[_, _, DesignName, FormName, Docid]}=Req, Db) ->
+ DesignId = <<"_design/", DesignName/binary>>,
+ % Anyway we can dry up this error handling?
+ case (catch couch_httpd_db:couch_doc_open(Db, DesignId, [], [])) of
+ {not_found, missing} ->
+ throw({not_found, missing_design_doc});
+ {not_found, deleted} ->
+ throw({not_found, deleted_design_doc});
+ DesignDoc ->
+ #doc{body={Props}} = DesignDoc,
+ Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>),
+ case proplists:get_value(<<"forms">>, Props, nil) of
+ {Forms} ->
+ case proplists:get_value(FormName, Forms, nil) of
+ nil ->
+ throw({not_found, missing_form});
+ FormSrc ->
+ case (catch couch_httpd_db:couch_doc_open(Db, Docid, [], [])) of
+ {not_found, missing} ->
+ throw({not_found, missing});
+ {not_found, deleted} ->
+ throw({not_found, deleted});
+ Doc ->
+ % ok we have everythign we need. let's make it happen.
+ send_form_response(Lang, FormSrc, Doc, Req, Db)
+ end
+ end;
+ nil ->
+ throw({not_found, missing_form})
+ end
+ end;
+
+handle_form_req(#httpd{method='GET'}=Req, _Db) ->
+ send_error(Req, 404, <<"form_error">>, <<"Invalid path.">>);
+
+handle_form_req(Req, _Db) ->
+ send_method_not_allowed(Req, "GET,HEAD").
+
+
+send_form_response(Lang, FormSrc, #doc{revs=[DocRev|_]}=Doc, #httpd{mochi_req=MReq}=Req, Db) ->
+ % make a term with etag-effecting Req components, but not always changing ones.
+ Headers = MReq:get(headers),
+ Hlist = mochiweb_headers:to_list(Headers),
+ Accept = proplists:get_value('Accept', Hlist),
+ <<SigInt:128/integer>> = erlang:md5(term_to_binary({Lang, FormSrc, DocRev, Accept})),
+ CurrentEtag = list_to_binary("\"" ++ lists:flatten(io_lib:format("form_~.36B",[SigInt])) ++ "\""),
+ EtagsToMatch = string:tokens(
+ couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
+ % We know our etag now
+ case lists:member(binary_to_list(CurrentEtag), EtagsToMatch) of
+ true ->
+ % the client has this in their cache.
+ couch_httpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>);
+ false ->
+ % Run the external form renderer.
+ {JsonResponse} = couch_query_servers:render_doc_form(Lang, FormSrc, Doc, Req, Db),
+ % Here we embark on the delicate task of replacing or creating the
+ % headers on the JsonResponse object. We need to control the Etag and
+ % Vary headers. If the external function controls the Etag, we'd have to
+ % run it to check for a match, which sort of defeats the purpose.
+ JsonResponse2 = case proplists:get_value(<<"headers">>, JsonResponse, nil) of
+ nil ->
+ % no JSON headers
+ % add our Etag and Vary headers to the response
+ [{<<"headers">>, {[{<<"Etag">>, CurrentEtag}, {<<"Vary">>, <<"Accept">>}]}} | JsonResponse];
+ {JsonHeaders} ->
+ [case Field of
+ {<<"headers">>, {JsonHeaders}} -> % add our headers
+ JsonHeadersEtagged = set_or_replace_header({<<"Etag">>, CurrentEtag}, JsonHeaders),
+ JsonHeadersVaried = set_or_replace_header({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
+ {<<"headers">>, {JsonHeadersVaried}};
+ _ -> % skip non-header fields
+ Field
+ end || Field <- JsonResponse]
+ end,
+ couch_httpd_external:send_external_response(Req, {JsonResponse2})
+ end.
+
+set_or_replace_header(H, L) ->
+ set_or_replace_header(H, L, []).
+
+set_or_replace_header({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) ->
+ % drop matching keys
+ set_or_replace_header({Key, NewValue}, Headers, Acc);
+set_or_replace_header({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) ->
+ % something else is next, leave it alone.
+ set_or_replace_header({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]);
+set_or_replace_header({Key, NewValue}, [], Acc) ->
+ % end of list, add ours
+ [{Key, NewValue}|Acc]. \ No newline at end of file
diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl
index 9651436c..c9a56c5c 100644
--- a/src/couchdb/couch_httpd_misc_handlers.erl
+++ b/src/couchdb/couch_httpd_misc_handlers.erl
@@ -24,7 +24,7 @@
-import(couch_httpd,
[send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
start_json_response/2,send_chunk/2,end_json_response/1,
- start_chunked_response/3]).
+ start_chunked_response/3, send_error/4]).
% httpd global handlers
@@ -173,5 +173,5 @@ increment_update_seq_req(#httpd{method='POST'}=Req, Db) ->
{update_seq, NewSeq}
]});
increment_update_seq_req(Req, _Db) ->
- send_method_not_allowed(Req, "GET,PUT,DELETE").
+ send_method_not_allowed(Req, "POST").
diff --git a/src/couchdb/couch_query_servers.erl b/src/couchdb/couch_query_servers.erl
index b694d5e3..c4ed5c8b 100644
--- a/src/couchdb/couch_query_servers.erl
+++ b/src/couchdb/couch_query_servers.erl
@@ -17,7 +17,7 @@
-export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2,code_change/3,stop/0]).
-export([start_doc_map/2, map_docs/2, stop_doc_map/1]).
--export([reduce/3, rereduce/3,validate_doc_update/5]).
+-export([reduce/3, rereduce/3,validate_doc_update/5,render_doc_form/5]).
% -export([test/0]).
-include("couch_db.hrl").
@@ -122,6 +122,17 @@ validate_doc_update(Lang, FunSrc, EditDoc, DiskDoc, Ctx) ->
ok = ret_os_process(Lang, Pid)
end.
+render_doc_form(Lang, FormSrc, Doc, Req, Db) ->
+ Pid = get_os_process(Lang),
+ JsonDoc = couch_doc:to_json_obj(Doc, [revs]),
+ JsonReq = couch_httpd_external:json_req_obj(Req, Db),
+ try couch_os_process:prompt(Pid, [<<"form">>, FormSrc, JsonDoc, JsonReq]) of
+ FormResp ->
+ FormResp
+ after
+ ok = ret_os_process(Lang, Pid)
+ end.
+
init([]) ->
% read config and register for configuration changes