From 116508806ce69c8a490f7d41b706f96ef5cee260 Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Wed, 14 Oct 2009 18:17:12 -0700 Subject: [PATCH] Change body() to read, cache, and return ByteString. Implement hasFormData() and friends. --- lib/jack/request.js | 79 +++++++++++++++++++++++++++++++++---- tests/jack/request-tests.js | 30 ++++++++++++++ 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/lib/jack/request.js b/lib/jack/request.js index 259ce29..95c2487 100644 --- a/lib/jack/request.js +++ b/lib/jack/request.js @@ -9,7 +9,13 @@ var Request = exports.Request = function(env) { this.env["jack.request"] = this; } -Request.prototype.body = function() { return this.env["jsgi.input"]; }; +Request.prototype.body = function() { + if (!this.env["jack.request.body"]) + this.env["jack.request.body"] = this.env["jsgi.input"].read(); + + return this.env["jack.request.body"]; +}; + Request.prototype.scheme = function() { return this.env["jsgi.url_scheme"]; }; Request.prototype.scriptName = function() { return this.env["SCRIPT_NAME"]; }; Request.prototype.pathInfo = function() { return this.env["PATH_INFO"]; }; @@ -19,7 +25,7 @@ Request.prototype.queryString = function() { return this.env["QUERY_STRING"] Request.prototype.referer = function() { return this.env["HTTP_REFERER"]; }; Request.prototype.referrer = Request.prototype.referer; Request.prototype.contentLength = function() { return parseInt(this.env["CONTENT_LENGTH"], 10); }; -Request.prototype.contentType = function() { return this.env["CONTENT_TYPE"]; }; +Request.prototype.contentType = function() { return this.env["CONTENT_TYPE"] || null; }; Request.prototype.host = function() { // Remove port number. @@ -32,6 +38,62 @@ Request.prototype.isPut = function() { return this.requestMethod() === Request.prototype.isDelete = function() { return this.requestMethod() === "DELETE"; }; Request.prototype.isHead = function() { return this.requestMethod() === "HEAD"; }; +// The set of form-data media-types. Requests that do not indicate +// one of the media types presents in this list will not be eligible +// for form-data / param parsing. +var FORM_DATA_MEDIA_TYPES = [ + null, + 'application/x-www-form-urlencoded', + 'multipart/form-data' +] + +// Determine whether the request body contains form-data by checking +// the request media_type against registered form-data media-types: +// "application/x-www-form-urlencoded" and "multipart/form-data". The +// list of form-data media types can be modified through the +// +FORM_DATA_MEDIA_TYPES+ array. +Request.prototype.hasFormData = function() { + var mediaType = this.mediaType(); + return FORM_DATA_MEDIA_TYPES.reduce(function(x, type) { return x || type == mediaType; }, false); +} + +// The media type (type/subtype) portion of the CONTENT_TYPE header +// without any media type parameters. e.g., when CONTENT_TYPE is +// "text/plain;charset=utf-8", the media-type is "text/plain". +// +// For more information on the use of media types in HTTP, see: +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 +Request.prototype.mediaType = function() { + var contentType = this.contentType(); + return (contentType && contentType.split(/\s*[;,]\s*/, 2)[0].toLowerCase()) || null; +} + +// The media type parameters provided in CONTENT_TYPE as a Hash, or +// an empty Hash if no CONTENT_TYPE or media-type parameters were +// provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8", +// this method responds with the following Hash: +// { 'charset' => 'utf-8' } +Request.prototype.mediaTypeParams = function() { + var contentType = this.contentType(); + if (!contentType) return {}; + + return contentType.split(/\s*[;,]\s*/).slice(1).map( + function (s) { return s.split('=', 2); }).reduce( + function (hash, pair) { + hash[pair[0].toLowerCase()] = pair[1]; + return hash; + }, {}); +} + +// The character set of the request body if a "charset" media type +// parameter was given, or nil if no "charset" was specified. Note +// that, per RFC2616, text/* media types that specify no explicit +// charset are to be considered ISO-8859-1. +Request.prototype.contentCharset = function() { + return this.mediaTypeParams()['charset'] || null; +} + +// Returns the data recieved in the query string. Request.prototype.GET = function() { // cache the parsed query: if (this.env["jack.request.query_string"] !== this.queryString()) { @@ -45,16 +107,19 @@ Request.prototype.GET = function() { return this.env["jack.request.query_hash"]; } +// Returns the data recieved in the request body. +// +// This method support both application/x-www-form-urlencoded and +// multipart/form-data. Request.prototype.POST = function() { var hash = {}; if (this.env["jack.request.form_input"] === this.env["jsgi.input"]) hash = this.env["jack.request.form_hash"]; - // TODO: implement hasFormData - else if (true || this.hasFormData()) { + else if (this.hasFormData()) { this.env["jack.request.form_input"] = this.env["jsgi.input"]; this.env["jack.request.form_hash"] = utils.parseMultipart(this.env); if (!this.env["jack.request.form_hash"]) { - this.env["jack.request.form_vars"] = this.env["jsgi.input"].read().decodeToString("utf-8"); + this.env["jack.request.form_vars"] = this.body().decodeToString("utf-8"); this.env["jack.request.form_hash"] = utils.parseQuery(this.env["jack.request.form_vars"]); //this.env["jsgi.input"].rewind(); } @@ -120,9 +185,7 @@ Request.prototype.uri = function() { var XHR_RE = new RegExp("XMLHttpRequest", "i"); -/** - * http://www.dev411.com/blog/2006/06/30/should-there-be-a-xmlhttprequest-user-agent - */ +// http://www.dev411.com/blog/2006/06/30/should-there-be-a-xmlhttprequest-user-agent Request.prototype.isXHR = Request.prototype.isXMLHTTPRequest = function() { return XHR_RE.test(this.env["HTTP_X_REQUESTED_WITH"]); } diff --git a/tests/jack/request-tests.js b/tests/jack/request-tests.js index 566b819..fa01dc5 100644 --- a/tests/jack/request-tests.js +++ b/tests/jack/request-tests.js @@ -16,3 +16,33 @@ exports.testParseCookiesRFC2109 = function() { assert.isSame({"foo" : "bar"}, req.cookies()); } + +exports.testMediaType = function() { + var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "text/html" })); + assert.isEqual(req.mediaType(), "text/html"); + assert.isEqual(req.contentType(), "text/html"); + + var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "text/html; charset=utf-8" })); + assert.isEqual(req.mediaType(), "text/html"); + assert.isEqual(req.contentType(), "text/html; charset=utf-8"); + assert.isEqual(req.mediaTypeParams()["charset"], "utf-8"); + assert.isEqual(req.contentCharset(), "utf-8"); + + var req = new Request(MockRequest.envFor(null, "", {})); + assert.isEqual(req.mediaType(), null); + assert.isEqual(req.contentType(), null); +} + +exports.testHasFormData = function() { + var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "application/x-www-form-urlencoded" })); + assert.isEqual(req.hasFormData(), true); + + var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "multipart/form-data" })); + assert.isEqual(req.hasFormData(), true); + + var req = new Request(MockRequest.envFor(null, "", {})); + assert.isEqual(req.hasFormData(), true); + + var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "text/html" })); + assert.isEqual(req.hasFormData(), false); +}