Skip to content

Instantly share code, notes, and snippets.

@ShirtlessKirk
Last active August 29, 2015 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ShirtlessKirk/d99bb9994f6bd5d85d51 to your computer and use it in GitHub Desktop.
Save ShirtlessKirk/d99bb9994f6bd5d85d51 to your computer and use it in GitHub Desktop.
CORS (Cross-Origin Resource Sharing) library
/**
* @preserve CORS (Cross-Origin Resource Sharing) library (https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
*
* @author ShirtlessKirk copyright 2014
* @license WTFPL (http://www.wtfpl.net/txt/copying)
*/
/*jslint unparam: true */
/*global define: false, module: false, require: false */
(function (global, definition) { // non-exporting module magic dance
'use strict';
var
amd = 'amd',
exports = 'exports'; // keeps the method names for CommonJS / AMD from being compiled to single character variable
if (typeof define === 'function' && define[amd]) {
define(function () {
return definition(global);
});
} else if (typeof module === 'function' && module[exports]) {
module[exports] = definition(global);
} else {
definition(global);
}
}(this, function (window) {
'use strict';
var
abort = 'abort',
arrayPrototypeSlice = Array.prototype.slice,
/** @type {CORS} */
cors,
corsType,
customErrorPrototype,
document = window.document,
jsonpHttpRequestPrototype,
key,
onLoad = 'onload',
onReadyStateChange = 'onreadystatechange',
open = 'open',
readyState = 'readyState',
response = 'response',
responseText = 'responseText',
send = 'send',
status = 'status',
CORSPrototype,
CORSStr = 'CORS',
/** @enum {number} */
HTTPCODE = {
'CONTINUE': 100,
'CREATED': 201,
'OK': 200,
'ACCEPTED': 202,
'SERVERERROR': 500
},
NULL = null,
/** @enum {number} */
STATE = {
'UNSENT': 0,
'OPENED': 1,
'HEADERS_RECEIVED': 2,
'LOADING': 3,
'DONE': 4
},
TYPE = 'TYPE',
/** @enum {string} */
TYPES = {
XMLHTTP: 'XMLHttpRequest',
XDOMAIN: 'XDomainRequest',
JSONP: 'JSONPHttpRequest'
};
if (!!window[CORSStr]) { // sanity check
return;
}
/**
* @this {CustomError}
* @return {string}
*/
function customErrorToString() {
var
message = this.name;
if (this.message.length !== 0) {
message += ': ' + this.message;
}
return message;
}
/**
* @constructor
* @param {string} name
* @param {string=} opt_message
*/
function CustomError(name, opt_message) {
this.name = name;
this.message = opt_message || '';
}
CustomError.prototype = new Error();
customErrorPrototype = CustomError.prototype;
customErrorPrototype.toString = customErrorToString;
/**
* @this {CORS}
* @param {number} state
* @param {number=} opt_status
*/
function changeState(state, opt_status) {
if (opt_status !== undefined && opt_status !== this[status]) {
this[status] = opt_status;
}
if (this[readyState] !== state) {
this[readyState] = state;
if (typeof this[onReadyStateChange] === 'function') { // run onreadystatechange function if defined
this[onReadyStateChange]();
}
}
}
/**
* @this {JSONPHttpRequest|XDomainRequest|XMLHttpRequest}
* propagates changes up to parent CORS object (if present) to trigger onreadystatechange (if defined) on that as well
*/
function transportOnReadyStateChange() {
var
parent = this.parent,
state = HTTPCODE.OK; // default to OK
if (parent !== undefined) {
if (this[readyState] === STATE.DONE) {
parent[response] = this[response];
parent[responseText] = this[responseText];
}
try {
state = this[status]; // Firefox < 14 throws error on trying to read the status property of XmlHttpRequest if it is undefined
} catch (ignore) {}
changeState.call(parent, this[readyState], state);
}
}
/**
* @this {JSONPHttpRequest|XDomainRequest}
* @param {number} state
* @param {number=} opt_status
*/
function transportReadyStateChange(state, opt_status) {
this[readyState] = state;
if (opt_status !== undefined) {
this[status] = opt_status;
}
if (typeof this[onReadyStateChange] === 'function') {
this[onReadyStateChange]();
}
}
/**
* @this {JSONPHttpRequest}
*/
function jsonpHttpRequestOnLoad() {
var
complete = 'complete',
loaded = 'loaded',
script = this.script,
state;
state = script[readyState] || complete;
if ((state === loaded || state === complete) && !script[loaded]) {
script[loaded] = true;
script[onLoad] = script[onReadyStateChange] = NULL;
script.parentNode.removeChild(script);
delete this.script;
transportReadyStateChange.call(this, STATE.DONE, HTTPCODE.OK);
}
}
/**
* @this {JSONPHttpRequest}
* @param {string} method
* @param {string} url
* @param {boolean=} opt_async
*/
function jsonpHttpRequestOpen(method, url, opt_async) {
var
async = opt_async !== undefined ? opt_async : true,
fnId,
script = document.createElement('script'),
that = this;
if (async) {
script.async = async;
}
script.id = TYPES.JSONP + '_' + (new Date()).getTime();
script.loaded = false;
script[onLoad] = script[onReadyStateChange] = function () {
jsonpHttpRequestOnLoad.call(that);
};
fnId = '__' + script.id;
window[fnId] = function (data) { // set up auto-callback function
that[response] = that[responseText] = data;
window[fnId] = undefined; // self-undefinition
if (typeof that.callback === 'function') { // if callback function sent, execute in global scope, passing request object
that.callback.call(window, that);
}
};
script.src = (url.indexOf('//') === 0 ? document.location.protocol : '') + url; // prepend the protocol just in case for old browsers (see IE)
transportReadyStateChange.call(this, STATE.UNSENT, HTTPCODE.CONTINUE);
this.script = script;
transportReadyStateChange.call(this, STATE.OPENED, HTTPCODE.CREATED);
}
/**
* @this {JSONPHttpRequest}
* @param {Object=} opt_data
*/
function jsonpHttpRequestSend(opt_data) {
var
callback = 'callback',
counter,
data,
head,
length,
param,
query,
qS,
queryString,
script;
if (this[readyState] !== STATE.OPENED) {
throw new CustomError('InvalidStateError', 'Failed to execute \'' + send + '\' on \'' + TYPES.JSONP + '\': the object\'s state must be OPENED.');
}
script = this.script;
data = opt_data || NULL;
head = document.head || document.getElementsByTagName('head')[0];
query = script.src.split('?');
qS = queryString = [];
if (query.length > 1) {
qS = query[1].split('&');
script.src = query[0];
}
for (counter = 0, length = qS.length; counter < length; counter += 1) {
if (qS[counter].indexOf(callback + '=') === 0) { // was callback sent in querystring?
this[callback] = queryString[counter].split('=')[1]; // save in object
} else {
queryString.push(qS[counter]);
}
}
for (param in data) {
if (data.hasOwnProperty(param)) {
if (param === callback) { // callback sent in data, save in object (overwrites any existing one)
this[callback] = data[param];
} else {
queryString.push(encodeURIComponent(param) + '=' + encodeURIComponent(data[param]));
}
}
}
queryString.push(callback + '=__' + script.id); // add auto-callback reference
script.src += '?' + queryString.join('&');
head.appendChild(script);
transportReadyStateChange.call(this, STATE.LOADING, HTTPCODE.ACCEPTED);
}
/**
* @this {XDomainRequest}
*/
function xDomainRequestOnError() {
transportReadyStateChange.call(this, STATE.DONE, HTTPCODE.SERVERERROR);
}
/**
* @this {XDomainRequest}
*/
function xDomainRequestOnLoad() {
this[response] = this[responseText];
transportReadyStateChange.call(this, STATE.DONE, HTTPCODE.OK); // OK
}
/**
* @this {CORS}
*/
function xDomainRequestOpen() {
var
args = arrayPrototypeSlice.call(arguments),
transport = this.transport;
this.method = args[0].toUpperCase();
transport.onerror = xDomainRequestOnError;
transport[onLoad] = xDomainRequestOnLoad;
transportReadyStateChange.call(transport, STATE.UNSENT, HTTPCODE.CONTINUE);
transport[open].apply(transport, args);
transportReadyStateChange.call(transport, STATE.OPENED, HTTPCODE.CREATED);
}
/**
* @this {CORS}
*/
function xDomainRequestSend() {
var
transport = this.transport;
transport[send].apply(transport, arguments);
transportReadyStateChange.call(transport, STATE.LOADING, HTTPCODE.ACCEPTED);
}
/**
* @this {CORS}
*/
function xmlHttpRequestOpen() {
var
args = arrayPrototypeSlice.call(arguments),
transport = this.transport;
this.method = args[0].toUpperCase();
transport[open].apply(transport, args); // automatically triggers onreadystatechange, no need to call here
}
/**
* @this {CORS}
*/
function xmlHttpRequestSend() {
var
setRequestHeader = 'setRequestHeader',
transport = this.transport;
transport[setRequestHeader]('Content-Type', (this.method === 'POST' ? 'multipart/form-data' : 'application/x-www-form-urlencoded') + '; charset=UTF-8');
transport[setRequestHeader]('X-Requested-With', TYPES.XMLHTTP);
transport[send].apply(transport, arguments); // automatically triggers onreadystatechange, no need to call here
}
/**
* @this {CORS}
*/
function corsAbort() {
try {
this.transport[abort]();
} catch (ignore) {}
}
/**
* @this {CORS}
*/
function corsJSONPOpen() {
var
args = arrayPrototypeSlice.call(arguments),
transport = this.transport;
args[0] = 'get';
this.method = args[0].toUpperCase();
transport[onLoad] = jsonpHttpRequestOnLoad;
transportReadyStateChange.call(transport, STATE.UNSENT, HTTPCODE.CONTINUE);
transport[open].apply(transport, args);
transportReadyStateChange.call(transport, STATE.OPENED, HTTPCODE.CREATED);
}
/**
* @this {CORS}
*/
function corsJSONPSend() {
var
transport = this.transport;
transport[send].apply(transport, arguments);
transportReadyStateChange.call(transport, STATE.LOADING, HTTPCODE.ACCEPTED);
}
/**
* @this {CORS}
* @param {string} type
*/
function corsInit(type) {
var
transport;
this[readyState] = this[response] = this[status] = NULL;
this[responseText] = '';
this.transport = new window[type]();
transport = this.transport;
transport[onReadyStateChange] = transportOnReadyStateChange;
transport.parent = this;
}
/**
* @constructor
* @param {boolean=} forceJSONP
*/
function CORS(forceJSONP) {
var
type = forceJSONP ? TYPES.JSONP : corsType;
corsInit.call(this, type);
if (forceJSONP) { // override the prototype methods for this instance
this[TYPE] = TYPES.JSONP;
this[open] = corsJSONPOpen;
this[send] = corsJSONPSend;
}
}
/**
* @constructor
*/
function JSONPHttpRequest() {
this[readyState] = this[status] = NULL;
this[responseText] = '';
}
if (window.XMLHttpRequest !== undefined && (new window.XMLHttpRequest()).withCredentials !== undefined) { // XMLHttp v2
corsType = TYPES.XMLHTTP;
} else if (window.XDomainRequest !== undefined) { // 7 < IE < 10
corsType = TYPES.XDOMAIN;
} else { // JSONP call fallback
corsType = TYPES.JSONP;
}
jsonpHttpRequestPrototype = JSONPHttpRequest.prototype;
jsonpHttpRequestPrototype[open] = jsonpHttpRequestOpen;
jsonpHttpRequestPrototype[send] = jsonpHttpRequestSend;
CORSPrototype = CORS.prototype;
CORSPrototype[TYPE] = corsType;
CORSPrototype[abort] = corsAbort;
switch (corsType) {
case TYPES.JSONP:
CORSPrototype[open] = corsJSONPOpen;
CORSPrototype[send] = corsJSONPSend;
break;
case TYPES.XDOMAIN:
CORSPrototype[open] = xDomainRequestOpen;
CORSPrototype[send] = xDomainRequestSend;
break;
case TYPES.XMLHTTP:
CORSPrototype[open] = xmlHttpRequestOpen;
CORSPrototype[send] = xmlHttpRequestSend;
break;
}
for (key in STATE) {
if (STATE.hasOwnProperty(key)) {
CORSPrototype[key] = jsonpHttpRequestPrototype[key] = STATE[key];
}
}
window[CORSStr] = CORS;
window[TYPES.JSONP] = JSONPHttpRequest;
if (!window.JSON) { // shim JSON if we don't have it intrinsically
cors = new CORS(true);
cors[open]('GET', '//cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.min.js', false);
cors[send]();
}
return CORS;
}));
/*
CORS (Cross-Origin Resource Sharing) library (https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
@author ShirtlessKirk copyright 2014
@license WTFPL (http://www.wtfpl.net/txt/copying)
*/
(function(c,q){"function"===typeof define&&define.amd?define(function(){return q(c)}):"function"===typeof module&&module.exports?module.exports=q(c):q(c)})(this,function(c){function q(){var a=this.name;0!==this.message.length&&(a+=": "+this.message);return a}function v(a,b){this.name=a;this.message=b||""}function B(a,b){void 0!==b&&b!==this.status&&(this.status=b);if(this.readyState!==a&&(this.readyState=a,"function"===typeof this.onreadystatechange))this.onreadystatechange()}function C(){var a=this.parent,
b=g.i;if(void 0!==a){this.readyState===d.DONE&&(a.response=this.response,a.responseText=this.responseText);try{b=this.status}catch(c){}B.call(a,this.readyState,b)}}function l(a,b){this.readyState=a;void 0!==b&&(this.status=b);if("function"===typeof this.onreadystatechange)this.onreadystatechange()}function x(){var a=this.e,b;b=a.readyState||"complete";"loaded"!==b&&"complete"!==b||a.loaded||(a.loaded=!0,a.onload=a.onreadystatechange=null,a.parentNode.removeChild(a),delete this.e,l.call(this,d.DONE,
g.i))}function D(a,b,n){a=void 0!==n?n:!0;var e;n=r.createElement("script");var h=this;a&&(n.async=a);n.id=m.b+"_"+(new Date).getTime();n.loaded=!1;n.onload=n.onreadystatechange=function(){x.call(h)};e="__"+n.id;c[e]=function(a){h.response=h.responseText=a;c[e]=void 0;"function"===typeof h.l&&h.l.call(c,h)};n.src=(0===b.indexOf("//")?r.location.protocol:"")+b;l.call(this,d.j,g.g);this.e=n;l.call(this,d.c,g.h)}function E(a){var b,c,e,h,f,k,p;if(this.readyState!==d.c)throw new v("InvalidStateError",
"Failed to execute 'send' on '"+m.b+"': the object's state must be OPENED.");p=this.e;a=a||null;c=r.head||r.getElementsByTagName("head")[0];b=p.src.split("?");f=k=[];1<b.length&&(f=b[1].split("&"),p.src=b[0]);b=0;for(e=f.length;b<e;b+=1)0===f[b].indexOf("callback=")?this.callback=k[b].split("=")[1]:k.push(f[b]);for(h in a)a.hasOwnProperty(h)&&("callback"===h?this.callback=a[h]:k.push(encodeURIComponent(h)+"="+encodeURIComponent(a[h])));k.push("callback=__"+p.id);p.src+="?"+k.join("&");c.appendChild(p);
l.call(this,d.LOADING,g.f)}function F(){l.call(this,d.DONE,g.m)}function G(){this.response=this.responseText;l.call(this,d.DONE,g.i)}function H(){var a=w.call(arguments),b=this.a;this.method=a[0].toUpperCase();b.onerror=F;b.onload=G;l.call(b,d.j,g.g);b.open.apply(b,a);l.call(b,d.c,g.h)}function I(){var a=this.a;a.send.apply(a,arguments);l.call(a,d.LOADING,g.f)}function J(){var a=w.call(arguments),b=this.a;this.method=a[0].toUpperCase();b.open.apply(b,a)}function K(){var a=this.a;a.setRequestHeader("Content-Type",
("POST"===this.method?"multipart/form-data":"application/x-www-form-urlencoded")+"; charset=UTF-8");a.setRequestHeader("X-Requested-With",m.d);a.send.apply(a,arguments)}function L(){try{this.a.abort()}catch(a){}}function y(){var a=w.call(arguments),b=this.a;a[0]="get";this.method=a[0].toUpperCase();b.onload=x;l.call(b,d.j,g.g);b.open.apply(b,a);l.call(b,d.c,g.h)}function z(){var a=this.a;a.send.apply(a,arguments);l.call(a,d.LOADING,g.f)}function t(a){var b=a?m.b:u;this.readyState=this.response=this.status=
null;this.responseText="";b=this.a=new c[b];b.onreadystatechange=C;b.parent=this;a&&(this.TYPE=m.b,this.open=y,this.send=z)}function A(){this.readyState=this.status=null;this.responseText=""}var w=Array.prototype.slice,f,u,k,r=c.document,e,g={CONTINUE:100,CREATED:201,OK:200,ACCEPTED:202,SERVERERROR:500},d={UNSENT:0,OPENED:1,HEADERS_RECEIVED:2,LOADING:3,DONE:4},m={d:"XMLHttpRequest",k:"XDomainRequest",b:"JSONPHttpRequest"};if(!c.CORS){v.prototype=Error();k=v.prototype;k.toString=q;u=void 0!==c.XMLHttpRequest&&
void 0!==(new c.XMLHttpRequest).withCredentials?m.d:void 0!==c.XDomainRequest?m.k:m.b;k=A.prototype;k.open=D;k.send=E;e=t.prototype;e.TYPE=u;e.abort=L;switch(u){case m.b:e.open=y;e.send=z;break;case m.k:e.open=H;e.send=I;break;case m.d:e.open=J,e.send=K}for(f in d)d.hasOwnProperty(f)&&(e[f]=k[f]=d[f]);c.CORS=t;c[m.b]=A;c.JSON||(f=new t(!0),f.open("GET","//cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.min.js",!1),f.send());return t}});
@ShirtlessKirk
Copy link
Author

CORS.js

A small (1.85KB minified and gzipped) cross-browser library to support Cross Origin Resource Sharing requests. The script exposes two global constructors; window.CORS and window.JSONPHttpRequest.

window.CORS

Acts as a wrapper around the transport used to communicate across origins. The transport is automatically chosen from what the browser supports, either XmlHttpRequest v2 (newer browsers) or XDomainRequest (IE 8 - 9) or JSONPHttpRequest (all others).

Parameters

  • forceJSONP (boolean, optional, default false) Forces the use of the JSONPHttpRequest object as the transport even if the browser can use XmlHttpRequest v2 or XDomainRequest (useful for script loading).

var cors = new CORS(true);

window.JSONPHttpRequest

This is used as a fallback for browsers that support neither XmlHttpRequest v2 or XDomainRequest and supports the same methods.

Usage

Invoke as if using XmlHttpRequest. That's it.

The same methods and properties as XmlHttpRequest are available and use the same parameters. readyState and status are updated as necessary on the object and onreadystatechange is settable and is invoked appropriately.

Example

var request = new CORS();

request.onreadystatechange = function () { // 'this' is the CORS object
    var text;

    if (this.readyState == 4) {
        text = this.responseText;
        // do something with the response
    }
};
request.open('GET', '//mydomain.me/somefile');
request.send();

Browser compatibility

  • Internet Explorer 6+
  • Firefox 3.6.28+
  • Chrome 3+
  • Opera 12+
  • Safari 4+
  • iOS Safari 3.2+
  • Android 2.1+

(earlier versions of listed browsers will probably work as well)

File sizes

As reported by Closure Compiler:

  • Unminified: 13.98KB (3.5KB gzipped)
  • Minified: 4.12KB (1.85KB gzipped)

Notes and interesting features

In the spirit of dogfooding, if a browser doesn't support JSON (I'm looking at you, old IE) the library uses itself to load a shim from cloudflare.com's CDN via a JSONPHttpRequest object. Incidentally, this also means that any script can be loaded using JSONPHttpRequest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment