Browse files

HACK $browser, $xhr

$xhr to allow sending headers
$browser to forward received headers to callback

This commit should be removed when implemented in angular
  • Loading branch information...
1 parent 5e532a4 commit 8ac69716b068a271e920f3de855e62dabaec8511 Vojta Jina committed May 14, 2011
Showing with 495 additions and 0 deletions.
  1. +2 −0 app/index.html
  2. +450 −0 app/js/hackBrowser.js
  3. +42 −0 app/js/hackXhr.js
  4. +1 −0 config/jsTestDriver.conf
View
2 app/index.html
@@ -53,5 +53,7 @@
<script src="js/controllers.js"></script>
<script src="js/filters.js"></script>
<script src="js/widgets.js"></script>
+ <script src="js/hackXhr.js"></script>
+ <script src="js/hackBrowser.js"></script>
</body>
</html>
View
450 app/js/hackBrowser.js
@@ -0,0 +1,450 @@
+/**
+ * HACKED $BROWSER SERVICE
+ *
+ * Current core $browser service doesn't forward headers, so we can't access them
+ * TODO(vojta) Remove this hack when fixed in angular
+ *
+ * This is complete Browser code copied, because
+ * it's glued by closures so we can't rewrite just one method of the browser
+ */
+
+//////////////////////////////
+// Browser
+//////////////////////////////
+var XHR = window.XMLHttpRequest || function () {
+ try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {}
+ try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {}
+ try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {}
+ throw new Error("This browser does not support XMLHttpRequest.");
+};
+
+function noop() {};
+var msie = parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10);
+
+// default xhr headers
+var XHR_HEADERS = {
+ DEFAULT: {
+ "Accept": "application/json, text/plain, */*",
+ "X-Requested-With": "XMLHttpRequest"
+ },
+ POST: {'Content-Type': 'application/x-www-form-urlencoded'}
+};
+
+/**
+ * @private
+ * @name Browser
+ *
+ * @description
+ * Constructor for the object exposed as $browser service.
+ *
+ * This object has two goals:
+ *
+ * - hide all the global state in the browser caused by the window object
+ * - abstract away all the browser specific features and inconsistencies
+ *
+ * @param {object} window The global window object.
+ * @param {object} document jQuery wrapped document.
+ * @param {object} body jQuery wrapped document.body.
+ * @param {function()} XHR XMLHttpRequest constructor.
+ * @param {object} $log console.log or an object with the same interface.
+ */
+function HackedBrowser(window, document, body, XHR, $log) {
+ var self = this,
+ location = window.location,
+ setTimeout = window.setTimeout;
+
+ self.isMock = false;
+
+ //////////////////////////////////////////////////////////////
+ // XHR API
+ //////////////////////////////////////////////////////////////
+ var idCounter = 0;
+ var outstandingRequestCount = 0;
+ var outstandingRequestCallbacks = [];
+
+
+ /**
+ * Executes the `fn` function (supports currying) and decrements the `outstandingRequestCallbacks`
+ * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed.
+ */
+ function completeOutstandingRequest(fn) {
+ try {
+ fn.apply(null, Array.prototype.slice.call(arguments, 1));
+ } finally {
+ outstandingRequestCount--;
+ if (outstandingRequestCount === 0) {
+ while(outstandingRequestCallbacks.length) {
+ try {
+ outstandingRequestCallbacks.pop()();
+ } catch (e) {
+ console.log(e);
+ $log.error(e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#xhr
+ * @methodOf angular.service.$browser
+ *
+ * @param {string} method Requested method (get|post|put|delete|head|json)
+ * @param {string} url Requested url
+ * @param {?string} post Post data to send (null if nothing to post)
+ * @param {function(number, string)} callback Function that will be called on response
+ * @param {object=} header additional HTTP headers to send with XHR.
+ * Standard headers are:
+ * <ul>
+ * <li><tt>Content-Type</tt>: <tt>application/x-www-form-urlencoded</tt></li>
+ * <li><tt>Accept</tt>: <tt>application/json, text/plain, &#42;/&#42;</tt></li>
+ * <li><tt>X-Requested-With</tt>: <tt>XMLHttpRequest</tt></li>
+ * </ul>
+ *
+ * @description
+ * Send ajax request
+ */
+ self.xhr = function(method, url, post, callback, headers) {
+ outstandingRequestCount ++;
+ if (method.toLowerCase() == 'json') {
+ var callbackId = ("angular_" + Math.random() + '_' + (idCounter++)).replace(/\d\./, '');
+ var script = angular.element('<script>')
+ .attr({type: 'text/javascript', src: url.replace('JSON_CALLBACK', callbackId)});
+ window[callbackId] = function(data){
+ window[callbackId] = undefined;
+ script.remove();
+ completeOutstandingRequest(callback, 200, data);
+ };
+ body.append(script);
+ } else {
+ var xhr = new XHR();
+ xhr.open(method, url, true);
+ angular.forEach(angular.extend({}, XHR_HEADERS.DEFAULT, XHR_HEADERS[method.toUpperCase()] || {}, headers || {}),
+ function(value, key) {
+ if (value) xhr.setRequestHeader(key, value);
+ });
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ completeOutstandingRequest(callback, xhr.status || 200, xhr.responseText, xhr.getAllResponseHeaders());
+ }
+ };
+ xhr.send(post || '');
+ }
+ };
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#notifyWhenNoOutstandingRequests
+ * @methodOf angular.service.$browser
+ *
+ * @param {function()} callback Function that will be called when no outstanding request
+ */
+ self.notifyWhenNoOutstandingRequests = function(callback) {
+ if (outstandingRequestCount === 0) {
+ callback();
+ } else {
+ outstandingRequestCallbacks.push(callback);
+ }
+ };
+
+ //////////////////////////////////////////////////////////////
+ // Poll Watcher API
+ //////////////////////////////////////////////////////////////
+ var pollFns = [];
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#poll
+ * @methodOf angular.service.$browser
+ */
+ self.poll = function() {
+ angular.forEach(pollFns, function(pollFn){ pollFn(); });
+ };
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#addPollFn
+ * @methodOf angular.service.$browser
+ *
+ * @param {function()} fn Poll function to add
+ *
+ * @description
+ * Adds a function to the list of functions that poller periodically executes
+ *
+ * @returns {function()} the added function
+ */
+ self.addPollFn = function(fn) {
+ pollFns.push(fn);
+ return fn;
+ };
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#startPoller
+ * @methodOf angular.service.$browser
+ *
+ * @param {number} interval How often should browser call poll functions (ms)
+ * @param {function()} setTimeout Reference to a real or fake `setTimeout` function.
+ *
+ * @description
+ * Configures the poller to run in the specified intervals, using the specified
+ * setTimeout fn and kicks it off.
+ */
+ self.startPoller = function(interval, setTimeout) {
+ (function check(){
+ self.poll();
+ setTimeout(check, interval);
+ })();
+ };
+
+ //////////////////////////////////////////////////////////////
+ // URL API
+ //////////////////////////////////////////////////////////////
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#setUrl
+ * @methodOf angular.service.$browser
+ *
+ * @param {string} url New url
+ *
+ * @description
+ * Sets browser's url
+ */
+ self.setUrl = function(url) {
+ var existingURL = location.href;
+ if (!existingURL.match(/#/)) existingURL += '#';
+ if (!url.match(/#/)) url += '#';
+ location.href = url;
+ };
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#getUrl
+ * @methodOf angular.service.$browser
+ *
+ * @description
+ * Get current browser's url
+ *
+ * @returns {string} Browser's url
+ */
+ self.getUrl = function() {
+ return location.href;
+ };
+
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#onHashChange
+ * @methodOf angular.service.$browser
+ *
+ * @description
+ * Detects if browser support onhashchange events and register a listener otherwise registers
+ * $browser poller. The `listener` will then get called when the hash changes.
+ *
+ * The listener gets called with either HashChangeEvent object or simple object that also contains
+ * `oldURL` and `newURL` properties.
+ *
+ * NOTE: this api is intended for use only by the $location service. Please use the
+ * {@link angular.service.$location $location service} to monitor hash changes in angular apps.
+ *
+ * @param {function(event)} listener Listener function to be called when url hash changes.
+ * @return {function()} Returns the registered listener fn - handy if the fn is anonymous.
+ */
+ self.onHashChange = function(listener) {
+ if ('onhashchange' in window) {
+ angular.element(window).bind('hashchange', listener);
+ } else {
+ var lastBrowserUrl = self.getUrl();
+
+ self.addPollFn(function() {
+ if (lastBrowserUrl != self.getUrl()) {
+ listener();
+ lastBrowserUrl = self.getUrl();
+ }
+ });
+ }
+ return listener;
+ };
+
+ //////////////////////////////////////////////////////////////
+ // Cookies API
+ //////////////////////////////////////////////////////////////
+ var rawDocument = document[0];
+ var lastCookies = {};
+ var lastCookieString = '';
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#cookies
+ * @methodOf angular.service.$browser
+ *
+ * @param {string=} name Cookie name
+ * @param {string=} value Cokkie value
+ *
+ * @description
+ * The cookies method provides a 'private' low level access to browser cookies.
+ * It is not meant to be used directly, use the $cookie service instead.
+ *
+ * The return values vary depending on the arguments that the method was called with as follows:
+ * <ul>
+ * <li>cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it</li>
+ * <li>cookies(name, value) -> set name to value, if value is undefined delete the cookie</li>
+ * <li>cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)</li>
+ * </ul>
+ *
+ * @returns {Object} Hash of all cookies (if called without any parameter)
+ */
+ self.cookies = function (name, value) {
+ var cookieLength, cookieArray, cookie, i, keyValue, index;
+
+ if (name) {
+ if (value === undefined) {
+ rawDocument.cookie = escape(name) + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ } else {
+ if (isString(value)) {
+ rawDocument.cookie = escape(name) + '=' + escape(value);
+
+ cookieLength = name.length + value.length + 1;
+ if (cookieLength > 4096) {
+ $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+
+ cookieLength + " > 4096 bytes)!");
+ }
+ if (lastCookies.length > 20) {
+ $log.warn("Cookie '"+ name +"' possibly not set or overflowed because too many cookies " +
+ "were already set (" + lastCookies.length + " > 20 )");
+ }
+ }
+ }
+ } else {
+ if (rawDocument.cookie !== lastCookieString) {
+ lastCookieString = rawDocument.cookie;
+ cookieArray = lastCookieString.split("; ");
+ lastCookies = {};
+
+ for (i = 0; i < cookieArray.length; i++) {
+ cookie = cookieArray[i];
+ index = cookie.indexOf('=');
+ if (index > 0) { //ignore nameless cookies
+ lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1));
+ }
+ }
+ }
+ return lastCookies;
+ }
+ };
+
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#defer
+ * @methodOf angular.service.$browser
+ * @param {function()} fn A function, who's execution should be defered.
+ * @param {number=} [delay=0] of milliseconds to defer the function execution.
+ *
+ * @description
+ * Executes a fn asynchroniously via `setTimeout(fn, delay)`.
+ *
+ * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using
+ * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed via
+ * `$browser.defer.flush()`.
+ *
+ */
+ self.defer = function(fn, delay) {
+ outstandingRequestCount++;
+ setTimeout(function() { completeOutstandingRequest(fn); }, delay || 0);
+ };
+
+ //////////////////////////////////////////////////////////////
+ // Misc API
+ //////////////////////////////////////////////////////////////
+ var hoverListener = noop;
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#hover
+ * @methodOf angular.service.$browser
+ *
+ * @description
+ * Set hover listener.
+ *
+ * @param {function(Object, boolean)} listener Function that will be called when a hover event
+ * occurs.
+ */
+ self.hover = function(listener) { hoverListener = listener; };
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#bind
+ * @methodOf angular.service.$browser
+ *
+ * @description
+ * Register hover function to real browser
+ */
+ self.bind = function() {
+ document.bind("mouseover", function(event){
+ hoverListener(angular.element(msie ? event.srcElement : event.target), true);
+ return true;
+ });
+ document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){
+ hoverListener(angular.element(event.target), false);
+ return true;
+ });
+ };
+
+
+ /**
+ * @workInProgress
+ * @ngdoc method
+ * @name angular.service.$browser#addCss
+ * @methodOf angular.service.$browser
+ *
+ * @param {string} url Url to css file
+ * @description
+ * Adds a stylesheet tag to the head.
+ */
+ self.addCss = function(url) {
+ var link = angular.element(rawDocument.createElement('link'));
+ link.attr('rel', 'stylesheet');
+ link.attr('type', 'text/css');
+ link.attr('href', url);
+ body.append(link);
+ };
+
+ self.addJs = function(url, dom_id) {
+ var script = angular.element(rawDocument.createElement('script'));
+ script.attr('type', 'text/javascript');
+ script.attr('src', url);
+ if (dom_id) script.attr('id', dom_id);
+ body.append(script);
+ };
+}
+
+var browserSingleton;
+angular.service('$browser', function($log) {
+ if (!browserSingleton) {
+ browserSingleton = new HackedBrowser(window, angular.element(window.document), angular.element(window.document.body),
+ XHR, $log);
+ var addPollFn = browserSingleton.addPollFn;
+ browserSingleton.addPollFn = function() {
+ browserSingleton.addPollFn = addPollFn;
+ browserSingleton.startPoller(100, function(delay, fn) {setTimeout(delay,fn);});
+ return addPollFn.apply(browserSingleton, arguments);
+ };
+ browserSingleton.bind();
+ }
+ return browserSingleton;
+}, {$inject:['$log']});
View
42 app/js/hackXhr.js
@@ -0,0 +1,42 @@
+/**
+ * HACKED $XHR SERVICE
+ *
+ * Core $xhr service doesn't allow to send custom headers during any request
+ * TODO(vojta) remove, when fixed in angular
+ */
+angular.service('$xhr', function($browser, $error, $log, $updateView) {
+ return function(method, url, post, callback, headers) {
+ if (angular.isFunction(post)) {
+ callback = post;
+ post = null;
+ }
+ if (post && angular.isObject(post)) {
+ post = angular.toJson(post);
+ }
+
+ headers = headers || {};
+ headers['X-XSRF-TOKEN'] = $browser.cookies()['XSRF-TOKEN'];
+
+ $browser.xhr(method, url, post, function(code, response, headers) {
+ try {
+ if (angular.isString(response)) {
+ if (response.match(/^\)\]\}',\n/)) response = response.substr(6);
+ if (/^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) {
+ response = angular.fromJson(response, true);
+ }
+ }
+ if (200 <= code && code < 300) {
+ callback(code, response, headers || {});
+ } else {
+ $error(
+ {method: method, url:url, data:post, callback:callback},
+ {status: code, body:response});
+ }
+ } catch (e) {
+ $log.error(e);
+ } finally {
+ $updateView();
+ }
+ }, headers);
+ };
+}, {$inject: ['$browser', '$xhr.error', '$log', '$updateView']});
View
1 config/jsTestDriver.conf
@@ -9,4 +9,5 @@ load:
- test/unit/*.js
exclude:
+ - app/js/hackBrowser.js

0 comments on commit 8ac6971

Please sign in to comment.