1 /** @namespace Networking functionality. */
  2 puredom.net = puredom.extend(new puredom.EventEmitter(), /** @lends puredom.net */ {
  3 	
  4 	/**	@class Represents an HTTP request.
  5 	 *	The raw XMLHttpRequest object is accessible through a *request* property.
  6 	 */
  7 	HttpRequest : function HttpRequest(options){
  8 		puredom.extend(this, options);
  9 	},
 10 	
 11 	
 12 	/**	Make a GET request. <br />
 13 	 *	This is a convenience wrapper around {@link puredom.net.request}.
 14 	 *	@param {String} url				URL to request
 15 	 *	@param {Function} callback		Called on completion. Gets passed <code>(success, response, request)</code>.
 16 	 *	@param {Object} [options]		Additional configuration. See options for {@link puredom.net.request}.
 17 	 *	@returns {puredom.net.HttpRequest} An HTTP request object
 18 	 *	@example
 19 	 *		puredom.net.get("/ajax?f=1", function(success, response) {
 20 	 *			console.log(success===true, response);
 21 	 *		});
 22 	 */
 23 	get : function(url, callback, options) {
 24 		return this.request(puredom.extend({
 25 			url : url,
 26 			method : 'GET'
 27 		}, options || {}), callback);
 28 	},
 29 	
 30 	
 31 	/**	Make a POST request. <br />
 32 	 *	This is a convenience wrapper around {@link puredom.net.request}.
 33 	 *	@param {String} url				URL to request
 34 	 *	@param {Object|String} body		Request body.  If an <code>Object</code>, will be serialized based on the request's Content-Type (defaulting to form-encoded)
 35 	 *	@param {Function} callback		Called on completion. Gets passed <code>(success, response, request)</code>.
 36 	 *	@param {Object} [options]		Additional configuration. See options for {@link puredom.net.request}.
 37 	 *	@returns {puredom.net.HttpRequest} An HTTP request object
 38 	 *	@example
 39 	 *		puredom.net.get("/ajax?f=2", { foo:'bar' }, function(success, res, req) {
 40 	 *			console.log(success===true, res, req.status, req.responseHeaders);
 41 	 *		});
 42 	 */
 43 	post : function(url, body, callback, options) {
 44 		return this.request(puredom.extend({
 45 			url : url,
 46 			method : 'POST',
 47 			body : body
 48 		}, options || {}), callback);
 49 	},
 50 	
 51 	
 52 	/**	Construct and send an HTTP request based on the specified options.
 53 	 *	@param {Object} options			Request options.
 54 	 *	@param {String} options.url						URL to request
 55 	 *	@param {String} [options.method="GET"]			HTTP method to use
 56 	 *	@param {String|Object} [options.body]			Request body. If a <code>String</code> is passed, it is considered pre-serialized.  
 57 	 *													If an <code>Object</code> is passed, it will be serialized based on the request's 
 58 	 *													<code>Content-Type</code> header.
 59 	 *	@param {Any} [options.bodySerialized]			If set, gets assigned unmodified as the request body.  If you're sending something like Blob data, this is for you.
 60 	 *	@param {Object} [options.headers]				A key-value list of request headers to send.
 61 	 *	@param {Object} [options.contentTypeOverride]	If set, overrides the response's <code>Content-Type</code> header with the given value.
 62 	 *	@param {Object} [options.callback]				Alias of <code>callback</code>, a function to call on completion. Gets passed <code>(success, response, request)</code>.
 63 	 *	@param {Function} [callback]	Called on completion. Gets passed <code>(success, response, request)</code>.  If set, takes precidence over <code>options.callback</code>.
 64 	 *	@returns {puredom.net.HttpRequest} An HTTP request object
 65 	 */
 66 	request : function(options, callback) {
 67 		var self = this,
 68 			req;
 69 		options = options || {};
 70 
 71 		if (!options.url) {
 72 			return false;
 73 		}
 74 
 75 		if (!options.method && options.type) {
 76 			options.method = options.type;
 77 			console.warn('puredom.net.request: The `type` option is deprecated. Use `method`.');
 78 		}
 79 
 80 		if (!options.body && options.post) {
 81 			options.body = options.post;
 82 			console.warn('puredom.net.request: The `post` option is deprecated. Use `body`.');
 83 		}
 84 
 85 		req = new puredom.net.HttpRequest({
 86 			url			: options.url,
 87 			type		: options.method || (options.body ? "POST" : "GET"),
 88 			callback	: callback || options.callback,
 89 			body		: options.body,
 90 			headers		: {
 91 				'content-type' : 'application/x-www-form-urlencoded',
 92 				'x-requested-with' : 'XMLHttpRequest'
 93 			}
 94 		});
 95 
 96 		if (options.headers) {
 97 			puredom.forEach(options.headers, function(value, key) {
 98 				var h = req.headers;
 99 				key = String(key).toLowerCase();
100 				if (value===undefined || value===null) {
101 					delete h[key];
102 				}
103 				else {
104 					h[key] = String(value);
105 				}
106 			});
107 		}
108 
109 		if (options.contentTypeOverride) {
110 			req.contentTypeOverride = options.contentTypeOverride;
111 			// @todo: fixme
112 			delete options.contentTypeOverride;
113 		}
114 
115 		if (options.responseType) {
116 			req.responseType = options.responseType;
117 		}
118 
119 		options = callback = null;
120 
121 		/** @ignore */
122 		function handleReadyState() {
123 			var xhr = req.request,
124 				typeMap = {
125 					json : 'JSON',
126 					document : 'XML'
127 				},
128 				headerReg = /^([a-z\-\.\_])\s*?\:/gim,
129 				head, contentType, resType, key;
130 			
131 			if (xhr.readyState!==4) {
132 				return;
133 			}
134 			self.fireEvent('before:response', req);
135 			
136 			req.status = xhr.status;
137 			req.responseType = 'text';
138 			if (!xhr.responseType || xhr.responseType==='text') {
139 				req.responseText = req.response = xhr.responseText;
140 			}
141 			
142 			req.responseHeaders = {};
143 			headerReg.lastIndex = 0;
144 			head = xhr.getAllResponseHeaders();
145 			while ( (key=headerReg.exec(head)) ) {
146 				req.responseHeaders[key[1].toLowerCase()] = xhr.getResponseHeader(key[1]);
147 			}
148 
149 			if (req.contentTypeOverride) {
150 				contentType = req.contentTypeOverride.toLowerCase();
151 			}
152 			else {
153 				try {
154 					contentType = (xhr.getResponseHeader("Content-Type") || '').toLowerCase().split(';')[0];
155 				} catch(err) {}
156 				contentType = contentType || "";
157 			}
158 			
159 			resType = xhr.responseType;
160 			if (resType) {
161 				req.responseType = resType;
162 				req.response = xhr.response;
163 				key = 'response' + (typeMap[resType] || resType.charAt(0).toUpperCase()+resType.substring(1));
164 				req[key] = req.response;
165 			}
166 			else if (contentType.match(/(^|\/)(json|javascript)$/gm)) {
167 				req.responseType = 'json';
168 				try {
169 					req.response = req.responseJSON = JSON.parse(xhr.responseText.replace(/^[^\[\{]*(.*)[^\[\{]*$/g,'$1'));
170 				} catch(parseError) {
171 					req.jsonParseError = parseError;
172 				}
173 			}
174 			else if (contentType==='application/xml' || contentType==='xml') {
175 				req.responseType = 'document';
176 				req.response = req.responseXML = xhr.responseXML;
177 			}
178 			
179 			if (typeof req.callback==='function') {
180 				req.callback(req.status!==0 && req.status<400, req.response, req);
181 			}
182 		}
183 		
184 		/**	@ignore */
185 		this.createXHR(req, function(xhr) {
186 
187 			/**	A reference to the request's underlying XMLHttpRequest instance
188 			 *	@name puredom.net.HttpRequest#xhr
189 			 *	@object
190 			 */
191 			req.xhr = xhr;
192 
193 			req.request = xhr;
194 			
195 			xhr.onreadystatechange = handleReadyState;
196 
197 			xhr.open(req.type, req.url, req.async!==false);
198 
199 			if (req.responseType) {
200 				xhr.responseType = req.responseType;
201 			}
202 
203 			puredom.forEach(req.headers, function(value, key) {
204 				xhr.setRequestHeader(key, value);
205 			});
206 
207 			if (!req.bodySerialized && req.body && puredom.typeOf(req.body)==='object') {
208 				self.serializeRequestBody(req);
209 			}
210 
211 			xhr.send(req.bodySerialized || req.body || null);
212 		});
213 
214 		return req;
215 	},
216 
217 
218 	/**	Lookup for request serialization methods. 
219 	 *	Each must expose a stringify() or encode() method. 
220 	 *	Keys are strings to find within a request's content-type header (lowercase'd).
221 	 *	@private
222 	 */
223 	requestSerializers : {
224 		json : puredom.json,
225 
226 		xml : puredom.xml,
227 
228 		'form-encoded' : puredom.querystring
229 	},
230 
231 
232 	/**	Serialize a request body. Adds a <code>bodySerialized</code> property to <code>req</code>.
233 	 *	@param {puredom.net.HttpRequest} req
234 	 *	@private
235 	 */
236 	serializeRequestBody : function(req) {
237 		var contentType = (req.headers['content-type'] || 'application/x-www-form-urlencoded').toLowerCase();
238 		puredom.forEach(this.serializers, function(api, type) {
239 			if (contentType.indexOf(type)>-1) {
240 				req.bodySerialized = (api.stringify || api.encode || api)(req.body);
241 				return false;
242 			}
243 		});
244 	},
245 
246 
247 	/** Asynchronously create an XMLHttpRequest object.
248 	 *	@private
249 	 */
250 	createXHR : function(req, callback, context) {
251 		var xhr;
252 		context = context || window;
253 		
254 		if (context.XMLHttpRequest) {
255 			xhr = new context.XMLHttpRequest();
256 		}
257 		else {
258 			try {
259 				xhr = new context.ActiveXObject("Msxml2.XMLHTTP");
260 			} catch(err2) {
261 				xhr = new context.ActiveXObject("Microsoft.XMLHTTP");
262 			}
263 		}
264 
265 		callback(xhr);
266 		return xhr;
267 	}
268 	
269 	
270 });