diff options
Diffstat (limited to 'share')
-rw-r--r-- | share/server/loop.js | 62 | ||||
-rw-r--r-- | share/server/main.js | 600 | ||||
-rwxr-xr-x | share/server/mainjs.sh | 24 | ||||
-rw-r--r-- | share/server/render.js | 252 | ||||
-rw-r--r-- | share/server/state.js | 32 | ||||
-rw-r--r-- | share/server/util.js | 112 | ||||
-rw-r--r-- | share/server/validate.js | 23 | ||||
-rw-r--r-- | share/server/views.js | 117 | ||||
-rw-r--r-- | share/www/script/test/list_views.js | 12 | ||||
-rw-r--r-- | share/www/script/test/show_documents.js | 7 |
10 files changed, 984 insertions, 257 deletions
diff --git a/share/server/loop.js b/share/server/loop.js new file mode 100644 index 00000000..170a8dc8 --- /dev/null +++ b/share/server/loop.js @@ -0,0 +1,62 @@ +// 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 sandbox = null; + +try { + // if possible, use evalcx (not always available) + sandbox = evalcx(''); + sandbox.emit = emit; + sandbox.sum = sum; + sandbox.log = log; + sandbox.toJSON = toJSON; + sandbox.respondWith = respondWith; + sandbox.registerType = registerType; +} catch (e) {} + +// Commands are in the form of json arrays: +// ["commandname",..optional args...]\n +// +// Responses are json values followed by a new line ("\n") + +var cmd, cmdkey; + +var dispatch = { + "reset" : State.reset, + "add_fun" : State.addFun, + "map_doc" : Views.mapDoc, + "reduce" : Views.reduce, + "rereduce" : Views.rereduce, + "validate" : Validate.validate, + "show_doc" : Render.showDoc, + "list_begin" : Render.listBegin, + "list_row" : Render.listRow, + "list_tail" : Render.listTail +}; + +while (cmd = eval(readline())) { + try { + cmdkey = cmd.shift(); + if (dispatch[cmdkey]) { + // run the correct responder with the cmd body + dispatch[cmdkey].apply(this, cmd); + } else { + // unknown command, quit and hope the restarted version is better + respond({ + error: "query_server_error", + reason: "unknown command '" + cmdkey + "'"}); + quit(); + } + } catch(e) { + respond(e); + } +}; diff --git a/share/server/main.js b/share/server/main.js index 3c59892b..4f57fa6b 100644 --- a/share/server/main.js +++ b/share/server/main.js @@ -10,33 +10,6 @@ // License for the specific language governing permissions and limitations under // the License. -var cmd; -var funs = []; // holds functions used for computation -var map_results = []; // holds temporary emitted values during doc map -var row_line = {}; // holds row number in list per func - -var sandbox = null; - -emit = function(key, value) { - map_results.push([key, value]); -} - -sum = function(values) { - var rv = 0; - for (var i in values) { - rv += values[i]; - } - return rv; -} - -log = function(message) { - if (typeof message == "undefined") { - message = "Error: attempting to log message of 'undefined'."; - } else if (typeof message != "string") { - message = toJSON(message); - } - print(toJSON({log: message})); -} // mimeparse.js // http://code.google.com/p/mimeparse/ @@ -46,12 +19,12 @@ log = function(message) { var Mimeparse = (function() { function strip(string) { - return string.replace(/^\s+/, '').replace(/\s+$/, '') + 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])) + parsedRanges.push(publicMethods.parseMediaRange(rangeParts[i])); }; return parsedRanges; }; @@ -130,12 +103,12 @@ var Mimeparse = (function() { var parsedHeader = parseRanges(header); var weighted = []; for (var i=0; i < supported.length; i++) { - weighted.push([publicMethods.fitnessAndQualityParsed(supported[i], parsedHeader), supported[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; })(); @@ -163,7 +136,7 @@ respondWith = function(req, responders) { } else { throw({code:406, body:"Not Acceptable: "+accept}); } -} +}; // whoever registers last wins. mimesByKey = {}; @@ -203,204 +176,33 @@ registerType("yaml", "application/x-yaml", "text/yaml"); 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.toJSON = toJSON; - sandbox.respondWith = respondWith; - sandbox.registerType = registerType; -} catch (e) {} - -// Commands are in the form of json arrays: -// ["commandname",..optional args...]\n -// -// Responses are json values followed by a new line ("\n") - -while (cmd = eval(readline())) { - try { - switch (cmd[0]) { - case "reset": - // clear the globals and run gc - funs = []; - gc(); - print("true"); // indicates success - break; - case "add_fun": - // The second arg is a string that will compile to a function. - // and then we add it to funs array - funs.push(compileFunction(cmd[1])); - print("true"); - break; - case "map_doc": - // The second arg is a document. We compute all the map functions against - // it. - // - // Each function can output multiple keys value, pairs for each document - // - // Example output of map_doc after three functions set by add_fun cmds: - // [ - // [["Key","Value"]], <- fun 1 returned 1 key value - // [], <- fun 2 returned 0 key values - // [["Key1","Value1"],["Key2","Value2"]] <- fun 3 returned 2 key values - // ] - // - var doc = cmd[1]; - /* - Immutable document support temporarily removed. - - Removed because the seal function no longer works on JS 1.8 arrays, - instead returning an error. The sealing is meant to prevent map - functions from modifying the same document that is passed to other map - functions. However, only map functions in the same design document are - run together, so we have a reasonable expectation they can trust each - other. Any map fun that can't be trusted can be placed in its own - design document, and it cannot affect other map functions. - - recursivelySeal(doc); // seal to prevent map functions from changing doc - */ - var buf = []; - for (var i = 0; i < funs.length; i++) { - map_results = []; - try { - funs[i](doc); - buf.push(toJSON(map_results)); - } catch (err) { - if (err == "fatal_error") { - // Only if it's a "fatal_error" do we exit. What's a fatal error? - // That's for the query to decide. - // - // This will make it possible for queries to completely error out, - // by catching their own local exception and rethrowing a - // fatal_error. But by default if they don't do error handling we - // just eat the exception and carry on. - throw {error: "map_runtime_error", - reason: "function raised fatal exception"}; - } - print(toJSON({log: "function raised exception (" + err - + ") with doc._id " + doc._id})); - buf.push("[]"); - } - } - print("[" + buf.join(", ") + "]"); - break; - - case "rereduce": - case "reduce": - { - var keys = null; - var values = null; - var reduceFuns = cmd[1]; - var rereduce = false; - - if (cmd[0] == "reduce") { - var kvs = cmd[2]; - keys = new Array(kvs.length); - values = new Array(kvs.length); - for(var i = 0; i < kvs.length; i++) { - keys[i] = kvs[i][0]; - values[i] = kvs[i][1]; - } - } else { - values = cmd[2]; - rereduce = true; - } - - for (var i in reduceFuns) { - reduceFuns[i] = compileFunction(reduceFuns[i]); - } - - var reductions = new Array(funs.length); - for(var i = 0; i < reduceFuns.length; i++) { - try { - reductions[i] = reduceFuns[i](keys, values, rereduce); - } catch (err) { - if (err == "fatal_error") { - throw {error: "reduce_runtime_error", - reason: "function raised fatal exception"}; - } - print(toJSON({log: "function raised exception (" + err + ")"})); - reductions[i] = null; - } - } - print("[true," + toJSON(reductions) + "]"); - } - break; - case "validate": - var funSrc = cmd[1]; - var newDoc = cmd[2]; - var oldDoc = cmd[3]; - var userCtx = cmd[4]; - var validateFun = compileFunction(funSrc); - try { - validateFun(newDoc, oldDoc, userCtx); - print("1"); - } catch (error) { - respond(error); - } - break; - case "show_doc": - var funSrc = cmd[1]; - var doc = cmd[2]; - var req = cmd[3]; - var formFun = compileFunction(funSrc); - runRenderFunction(formFun, [doc, req]); - break; - case "list_begin": - var listFun = funs[0]; - var head = cmd[1]; - var req = cmd[2]; - row_line[listFun] = { first_key: null, row_number: 0, prev_key: null }; - runRenderFunction(listFun, [head, null, req, null]); - break; - case "list_row": - var listFun = funs[0]; - var row = cmd[1]; - var req = cmd[2]; - var row_info = row_line[listFun]; - runRenderFunction(listFun, [null, row, req, row_info]); - if (row_info.first_key == null) { - row_info.first_key = row.key; - } - row_info.prev_key = row.key; - row_info.row_number++; - row_line[listFun] = row_info; - break; - case "list_tail": - var listFun = funs[0]; - var req = cmd[1]; - var row_info = null; - try { - row_info = row_line[listFun]; - delete row_line[listFun]; - } catch (e) {} - runRenderFunction(listFun, [null, null, req, row_info]); - break; - default: - print(toJSON({error: "query_server_error", - reason: "unknown command '" + cmd[0] + "'"})); - quit(); +var Render = (function() { + var row_info; + return { + showDoc : function(funSrc, doc, req) { + var formFun = compileFunction(funSrc); + runRenderFunction(formFun, [doc, req], funSrc); + }, + listBegin : function(head, req) { + row_info = { first_key: null, row_number: 0, prev_key: null }; + runRenderFunction(funs[0], [head, null, req, null], funsrc[0]); + }, + listRow : function(row, req) { + if (row_info.first_key == null) { + row_info.first_key = row.key; + } + runRenderFunction(funs[0], [null, row, req, row_info], funsrc[0], true); + row_info.prev_key = row.key; + row_info.row_number++; + }, + listTail : function(req) { + runRenderFunction(funs[0], [null, null, req, row_info], funsrc[0]); } - } catch (exception) { - print(toJSON(exception)); } -} - -function maybeWrapResponse(resp) { - var type = typeof resp; - if ((type == "string") || (type == "xml")) { - return {body:resp}; - } else { - return resp; - } -}; +})(); -var responseSent; -function runRenderFunction(renderFun, args) { +function runRenderFunction(renderFun, args, funSrc, htmlErrors) { responseSent = false; try { var resp = renderFun.apply(null, args); @@ -412,46 +214,85 @@ function runRenderFunction(renderFun, args) { } } } catch(e) { - log("function raised error: "+e.toString()); - log("stacktrace: "+e.stack); - var errorMessage = "JavaScript function raised error: "+e.toString()+"\nSee CouchDB logfile for stacktrace."; - respond({error:"render_error",reason:errorMessage}); + var logMessage = "function raised error: "+e.toString(); + log(logMessage); + // log("stacktrace: "+e.stack); + var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; + respond({ + error:"render_error", + reason:errorMessage}); } }; -// prints the object as JSON, and rescues and logs any toJSON() related errors -function respond(obj) { - responseSent = true; - try { - print(toJSON(obj)); - } catch(e) { - log("Error converting object to JSON: " + e.toString()); - } +function escapeHTML(string) { + return string.replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">"); } -function compileFunction(source) { - try { - var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); - } catch (err) { - throw {error: "compilation_error", - reason: err.toString() + " (" + source + ")"}; - } - if (typeof(functionObject) == "function") { - return functionObject; +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}; +}; + +function maybeWrapResponse(resp) { + var type = typeof resp; + if ((type == "string") || (type == "xml")) { + return {body:resp}; } else { - throw {error: "compilation_error", - reason: "expression does not eval to a function. (" + source + ")"}; + return resp; } -} +}; +// 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. -function recursivelySeal(obj) { - seal(obj); - for (var propname in obj) { - if (typeof doc[propname] == "object") { - recursivelySeal(doc[propname]); +// globals used by other modules and functions +var funs = []; // holds functions used for computation +var funsrc = []; // holds function source for debug info +var State = (function() { + return { + reset : function() { + // clear the globals and run gc + funs = []; + funsrc = []; + gc(); + print("true"); // indicates success + }, + addFun : function(newFun) { + // Compile to a function and add it to funs array + funsrc.push(newFun); + funs.push(compileFunction(newFun)); + print("true"); } } -} +})(); +// 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. function toJSON(val) { if (typeof(val) == "undefined") { @@ -474,7 +315,7 @@ function toJSON(val) { return v.toString(); }, "Date": function(v) { - var f = function(n) { return n < 10 ? '0' + n : n } + var f = function(n) { return n < 10 ? '0' + n : n }; return '"' + v.getUTCFullYear() + '-' + f(v.getUTCMonth() + 1) + '-' + f(v.getUTCDate()) + 'T' + @@ -509,3 +350,248 @@ function toJSON(val) { } }[val != null ? val.constructor.name : "Object"](val); } + +function compileFunction(source) { + try { + var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); + } catch (err) { + throw {error: "compilation_error", + reason: err.toString() + " (" + source + ")"}; + } + if (typeof(functionObject) == "function") { + return functionObject; + } else { + throw {error: "compilation_error", + reason: "expression does not eval to a function. (" + source + ")"}; + } +} + +function recursivelySeal(obj) { + seal(obj); + for (var propname in obj) { + if (typeof doc[propname] == "object") { + recursivelySeal(doc[propname]); + } + } +} + +var responseSent; +// prints the object as JSON, and rescues and logs any toJSON() related errors +function respond(obj) { + responseSent = true; + try { + print(toJSON(obj)); + } catch(e) { + log("Error converting object to JSON: " + e.toString()); + } +}; + +log = function(message) { + if (typeof message == "undefined") { + message = "Error: attempting to log message of 'undefined'."; + } else if (typeof message != "string") { + message = toJSON(message); + } + print(toJSON({log: message})); +}; +// 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 Validate = { + validate : function(funSrc, newDoc, oldDoc, userCtx) { + var validateFun = compileFunction(funSrc); + try { + validateFun(newDoc, oldDoc, userCtx); + print("1"); + } catch (error) { + respond(error); + } + } +};// 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. + +// globals used by views +var map_results = []; // holds temporary emitted values during doc map + +// view helper functions +emit = function(key, value) { + map_results.push([key, value]); +} + +sum = function(values) { + var rv = 0; + for (var i in values) { + rv += values[i]; + } + return rv; +} + +var Views = (function() { + + function runReduce(reduceFuns, keys, values, rereduce) { + for (var i in reduceFuns) { + reduceFuns[i] = compileFunction(reduceFuns[i]); + } + var reductions = new Array(reduceFuns.length); + for(var i = 0; i < reduceFuns.length; i++) { + try { + reductions[i] = reduceFuns[i](keys, values, rereduce); + } catch (err) { + if (err == "fatal_error") { + throw { + error: "reduce_runtime_error", + reason: "function raised fatal exception"}; + } + log("function raised exception (" + err + ")"); + reductions[i] = null; + } + } + print("[true," + toJSON(reductions) + "]"); + }; + + return { + reduce : function(reduceFuns, kvs) { + var keys = new Array(kvs.length); + var values = new Array(kvs.length); + for(var i = 0; i < kvs.length; i++) { + keys[i] = kvs[i][0]; + values[i] = kvs[i][1]; + } + runReduce(reduceFuns, keys, values, false); + }, + rereduce : function(reduceFuns, values) { + runReduce(reduceFuns, null, values, true); + }, + mapDoc : function(doc) { + // Compute all the map functions against the document. + // + // Each function can output multiple key/value pairs for each document. + // + // Example output of map_doc after three functions set by add_fun cmds: + // [ + // [["Key","Value"]], <- fun 1 returned 1 key value + // [], <- fun 2 returned 0 key values + // [["Key1","Value1"],["Key2","Value2"]] <- fun 3 returned 2 key values + // ] + // + + /* + Immutable document support temporarily removed. + + Removed because the seal function no longer works on JS 1.8 arrays, + instead returning an error. The sealing is meant to prevent map + functions from modifying the same document that is passed to other map + functions. However, only map functions in the same design document are + run together, so we have a reasonable expectation they can trust each + other. Any map fun that can't be trusted can be placed in its own + design document, and it cannot affect other map functions. + + recursivelySeal(doc); // seal to prevent map functions from changing doc + */ + var buf = []; + for (var i = 0; i < funs.length; i++) { + map_results = []; + try { + funs[i](doc); + buf.push(toJSON(map_results)); + } catch (err) { + if (err == "fatal_error") { + // Only if it's a "fatal_error" do we exit. What's a fatal error? + // That's for the query to decide. + // + // This will make it possible for queries to completely error out, + // by catching their own local exception and rethrowing a + // fatal_error. But by default if they don't do error handling we + // just eat the exception and carry on. + throw { + error: "map_runtime_error", + reason: "function raised fatal exception"}; + } + log("function raised exception (" + err + ") with doc._id " + doc._id); + buf.push("[]"); + } + } + print("[" + buf.join(", ") + "]"); + } + } +})(); +// 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 sandbox = null; + +try { + // if possible, use evalcx (not always available) + sandbox = evalcx(''); + sandbox.emit = emit; + sandbox.sum = sum; + sandbox.log = log; + sandbox.toJSON = toJSON; + sandbox.respondWith = respondWith; + sandbox.registerType = registerType; +} catch (e) {} + +// Commands are in the form of json arrays: +// ["commandname",..optional args...]\n +// +// Responses are json values followed by a new line ("\n") + +var cmd, cmdkey; + +var dispatch = { + "reset" : State.reset, + "add_fun" : State.addFun, + "map_doc" : Views.mapDoc, + "reduce" : Views.reduce, + "rereduce" : Views.rereduce, + "validate" : Validate.validate, + "show_doc" : Render.showDoc, + "list_begin" : Render.listBegin, + "list_row" : Render.listRow, + "list_tail" : Render.listTail +}; + +while (cmd = eval(readline())) { + try { + cmdkey = cmd.shift(); + if (dispatch[cmdkey]) { + // run the correct responder with the cmd body + dispatch[cmdkey].apply(this, cmd); + } else { + // unknown command, quit and hope the restarted version is better + respond({ + error: "query_server_error", + reason: "unknown command '" + cmdkey + "'"}); + quit(); + } + } catch(e) { + respond(e); + } +}; diff --git a/share/server/mainjs.sh b/share/server/mainjs.sh new file mode 100755 index 00000000..86128423 --- /dev/null +++ b/share/server/mainjs.sh @@ -0,0 +1,24 @@ +#! /bin/sh -e + +# 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. + +dirname=`dirname $0` + +cat \ + $dirname/render.js \ + $dirname/state.js \ + $dirname/util.js \ + $dirname/validate.js \ + $dirname/views.js \ + $dirname/loop.js \ + > $dirname/main.js diff --git a/share/server/render.js b/share/server/render.js new file mode 100644 index 00000000..89c31ea1 --- /dev/null +++ b/share/server/render.js @@ -0,0 +1,252 @@ +// 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. + + +// 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 bestKey = null, accept = req.headers["Accept"]; + if (accept && !req.query.format) { + var provides = []; + for (key in responders) { + if (mimesByKey[key]) { + provides = provides.concat(mimesByKey[key]); + } + } + var bestMime = Mimeparse.bestMatch(provides, accept); + bestKey = keysByMime[bestMime]; + } else { + bestKey = req.query.format; + } + var rFunc = responders[bestKey || responders.fallback || "html"]; + if (rFunc) { + var resp = maybeWrapResponse(rFunc()); + resp["headers"] = resp["headers"] || {}; + resp["headers"]["Content-Type"] = bestMime; + respond(resp); + } else { + 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"); +registerType("xhtml", "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"); + + +var Render = (function() { + var row_info; + return { + showDoc : function(funSrc, doc, req) { + var formFun = compileFunction(funSrc); + runRenderFunction(formFun, [doc, req], funSrc); + }, + listBegin : function(head, req) { + row_info = { first_key: null, row_number: 0, prev_key: null }; + runRenderFunction(funs[0], [head, null, req, null], funsrc[0]); + }, + listRow : function(row, req) { + if (row_info.first_key == null) { + row_info.first_key = row.key; + } + runRenderFunction(funs[0], [null, row, req, row_info], funsrc[0], true); + row_info.prev_key = row.key; + row_info.row_number++; + }, + listTail : function(req) { + runRenderFunction(funs[0], [null, null, req, row_info], funsrc[0]); + } + } +})(); + +function runRenderFunction(renderFun, args, funSrc, htmlErrors) { + responseSent = false; + try { + var resp = renderFun.apply(null, args); + if (!responseSent) { + if (resp) { + respond(maybeWrapResponse(resp)); + } else { + respond({error:"render_error",reason:"undefined response from render function"}); + } + } + } catch(e) { + var logMessage = "function raised error: "+e.toString(); + log(logMessage); + // log("stacktrace: "+e.stack); + var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; + respond({ + error:"render_error", + reason:errorMessage}); + } +}; + +function escapeHTML(string) { + return string.replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">"); +} + +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}; +}; + +function maybeWrapResponse(resp) { + var type = typeof resp; + if ((type == "string") || (type == "xml")) { + return {body:resp}; + } else { + return resp; + } +}; diff --git a/share/server/state.js b/share/server/state.js new file mode 100644 index 00000000..05c55a1b --- /dev/null +++ b/share/server/state.js @@ -0,0 +1,32 @@ +// 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. + +// globals used by other modules and functions +var funs = []; // holds functions used for computation +var funsrc = []; // holds function source for debug info +var State = (function() { + return { + reset : function() { + // clear the globals and run gc + funs = []; + funsrc = []; + gc(); + print("true"); // indicates success + }, + addFun : function(newFun) { + // Compile to a function and add it to funs array + funsrc.push(newFun); + funs.push(compileFunction(newFun)); + print("true"); + } + } +})(); diff --git a/share/server/util.js b/share/server/util.js new file mode 100644 index 00000000..7faf2f0b --- /dev/null +++ b/share/server/util.js @@ -0,0 +1,112 @@ +// 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. + +function toJSON(val) { + if (typeof(val) == "undefined") { + throw "Cannot encode 'undefined' value as JSON"; + } + var subs = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', + '\r': '\\r', '"' : '\\"', '\\': '\\\\'}; + if (typeof(val) == "xml") { // E4X support + val = val.toXMLString(); + } + return { + "Array": function(v) { + var buf = []; + for (var i = 0; i < v.length; i++) { + buf.push(toJSON(v[i])); + } + return "[" + buf.join(",") + "]"; + }, + "Boolean": function(v) { + return v.toString(); + }, + "Date": function(v) { + var f = function(n) { return n < 10 ? '0' + n : n }; + return '"' + v.getUTCFullYear() + '-' + + f(v.getUTCMonth() + 1) + '-' + + f(v.getUTCDate()) + 'T' + + f(v.getUTCHours()) + ':' + + f(v.getUTCMinutes()) + ':' + + f(v.getUTCSeconds()) + 'Z"'; + }, + "Number": function(v) { + return isFinite(v) ? v.toString() : "null"; + }, + "Object": function(v) { + if (v === null) return "null"; + var buf = []; + for (var k in v) { + if (!v.hasOwnProperty(k) || typeof(k) !== "string" || v[k] === undefined) { + continue; + } + buf.push(toJSON(k, val) + ": " + toJSON(v[k])); + } + return "{" + buf.join(",") + "}"; + }, + "String": function(v) { + if (/["\\\x00-\x1f]/.test(v)) { + v = v.replace(/([\x00-\x1f\\"])/g, function(a, b) { + var c = subs[b]; + if (c) return c; + c = b.charCodeAt(); + return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); + }); + } + return '"' + v + '"'; + } + }[val != null ? val.constructor.name : "Object"](val); +} + +function compileFunction(source) { + try { + var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); + } catch (err) { + throw {error: "compilation_error", + reason: err.toString() + " (" + source + ")"}; + } + if (typeof(functionObject) == "function") { + return functionObject; + } else { + throw {error: "compilation_error", + reason: "expression does not eval to a function. (" + source + ")"}; + } +} + +function recursivelySeal(obj) { + seal(obj); + for (var propname in obj) { + if (typeof doc[propname] == "object") { + recursivelySeal(doc[propname]); + } + } +} + +var responseSent; +// prints the object as JSON, and rescues and logs any toJSON() related errors +function respond(obj) { + responseSent = true; + try { + print(toJSON(obj)); + } catch(e) { + log("Error converting object to JSON: " + e.toString()); + } +}; + +log = function(message) { + if (typeof message == "undefined") { + message = "Error: attempting to log message of 'undefined'."; + } else if (typeof message != "string") { + message = toJSON(message); + } + print(toJSON({log: message})); +}; diff --git a/share/server/validate.js b/share/server/validate.js new file mode 100644 index 00000000..41d3c1e3 --- /dev/null +++ b/share/server/validate.js @@ -0,0 +1,23 @@ +// 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 Validate = { + validate : function(funSrc, newDoc, oldDoc, userCtx) { + var validateFun = compileFunction(funSrc); + try { + validateFun(newDoc, oldDoc, userCtx); + print("1"); + } catch (error) { + respond(error); + } + } +};
\ No newline at end of file diff --git a/share/server/views.js b/share/server/views.js new file mode 100644 index 00000000..c6d71579 --- /dev/null +++ b/share/server/views.js @@ -0,0 +1,117 @@ +// 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. + +// globals used by views +var map_results = []; // holds temporary emitted values during doc map + +// view helper functions +emit = function(key, value) { + map_results.push([key, value]); +} + +sum = function(values) { + var rv = 0; + for (var i in values) { + rv += values[i]; + } + return rv; +} + +var Views = (function() { + + function runReduce(reduceFuns, keys, values, rereduce) { + for (var i in reduceFuns) { + reduceFuns[i] = compileFunction(reduceFuns[i]); + } + var reductions = new Array(reduceFuns.length); + for(var i = 0; i < reduceFuns.length; i++) { + try { + reductions[i] = reduceFuns[i](keys, values, rereduce); + } catch (err) { + if (err == "fatal_error") { + throw { + error: "reduce_runtime_error", + reason: "function raised fatal exception"}; + } + log("function raised exception (" + err + ")"); + reductions[i] = null; + } + } + print("[true," + toJSON(reductions) + "]"); + }; + + return { + reduce : function(reduceFuns, kvs) { + var keys = new Array(kvs.length); + var values = new Array(kvs.length); + for(var i = 0; i < kvs.length; i++) { + keys[i] = kvs[i][0]; + values[i] = kvs[i][1]; + } + runReduce(reduceFuns, keys, values, false); + }, + rereduce : function(reduceFuns, values) { + runReduce(reduceFuns, null, values, true); + }, + mapDoc : function(doc) { + // Compute all the map functions against the document. + // + // Each function can output multiple key/value pairs for each document. + // + // Example output of map_doc after three functions set by add_fun cmds: + // [ + // [["Key","Value"]], <- fun 1 returned 1 key value + // [], <- fun 2 returned 0 key values + // [["Key1","Value1"],["Key2","Value2"]] <- fun 3 returned 2 key values + // ] + // + + /* + Immutable document support temporarily removed. + + Removed because the seal function no longer works on JS 1.8 arrays, + instead returning an error. The sealing is meant to prevent map + functions from modifying the same document that is passed to other map + functions. However, only map functions in the same design document are + run together, so we have a reasonable expectation they can trust each + other. Any map fun that can't be trusted can be placed in its own + design document, and it cannot affect other map functions. + + recursivelySeal(doc); // seal to prevent map functions from changing doc + */ + var buf = []; + for (var i = 0; i < funs.length; i++) { + map_results = []; + try { + funs[i](doc); + buf.push(toJSON(map_results)); + } catch (err) { + if (err == "fatal_error") { + // Only if it's a "fatal_error" do we exit. What's a fatal error? + // That's for the query to decide. + // + // This will make it possible for queries to completely error out, + // by catching their own local exception and rethrowing a + // fatal_error. But by default if they don't do error handling we + // just eat the exception and carry on. + throw { + error: "map_runtime_error", + reason: "function raised fatal exception"}; + } + log("function raised exception (" + err + ") with doc._id " + doc._id); + buf.push("[]"); + } + } + print("[" + buf.join(", ") + "]"); + } + } +})(); diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index e71ebc4e..0bf03900 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -130,6 +130,15 @@ couchTests.list_views = function(debug) { }), emptyList: stringFun(function(head, row, req, row_info) { return { body: "" }; + }), + rowError : stringFun(function(head, row, req, row_info) { + if (head) { + return "head"; + } else if(row) { + return missingValue; + } else { + return "tail" + } }) } }; @@ -272,4 +281,7 @@ couchTests.list_views = function(debug) { }); T(xhr.status == 400); T(/query_parse_error/.test(xhr.responseText)); + + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/rowError/basicView"); + T(/<h1>Render Error<\/h1>/.test(xhr.responseText)); }; diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index c755898e..bf12e4c6 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -54,6 +54,9 @@ couchTests.show_documents = function(debug) { json : req } }), + "render-error" : stringFun(function(doc, req) { + return noSuchVariable; + }), "xml-type" : stringFun(function(doc, req) { return { "headers" : { @@ -142,6 +145,10 @@ couchTests.show_documents = function(debug) { // hello template world xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/"+docid); T(xhr.responseText == "Hello World"); + + // error stacktraces + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/render-error/"+docid); + T(JSON.parse(xhr.responseText).error == "render_error"); // hello template world (no docid) xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello"); |