diff options
-rw-r--r-- | etc/couchdb/default.ini.tpl.in | 1 | ||||
-rw-r--r-- | etc/couchdb/local_dev.ini | 3 | ||||
-rw-r--r-- | share/server/main.js | 182 | ||||
-rw-r--r-- | share/www/script/couch_tests.js | 253 | ||||
-rw-r--r-- | src/couchdb/Makefile.am | 2 | ||||
-rw-r--r-- | src/couchdb/couch_external_manager.erl | 6 | ||||
-rw-r--r-- | src/couchdb/couch_external_server.erl | 28 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_external.erl | 93 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_form.erl | 114 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_misc_handlers.erl | 4 | ||||
-rw-r--r-- | src/couchdb/couch_query_servers.erl | 13 |
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 |