您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto-renew token from Midway Gateway on XHR requests and perform background fetches every 5 minutes to avoid token timeout issues
// ==UserScript== // @name Twitch Midway Gateway // @namespace http://devinfra.internal.justin.tv/ // @version 0.1 // @description Auto-renew token from Midway Gateway on XHR requests and perform background fetches every 5 minutes to avoid token timeout issues // @author hvr // @match https://git.xarth.tv/* // @grant none // ==/UserScript== (function() { var method; var noop = function () {}; var methods = [ 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn' ]; var length = methods.length; var console = (window.console = window.console || {}); while (length--) { method = methods[length]; // Only stub undefined methods. if (!console[method]) { console[method] = noop; } } }()); (function(window, undefined) { var $ = window.jQuery; var namespace = function() { window.Amazon = window.Amazon || {}; window.Amazon.IDP = window.Amazon.IDP || {}; window.Amazon.IDP.config = window.Amazon.IDP.config || {}; window.Amazon.IDP.internal = window.Amazon.IDP.internal || {}; return window.Amazon.IDP; }(); // A factory for the browser's native XHR. Initialize to a plain-old function returning a new XHR. // We'll overwrite appropriately later on in this function if we override the XHR.prototype (we usually do). var nativeXhrFactory = function() { return new XMLHttpRequest(); }; // A place to hold a user-defined blacklist that specifies domains that should be omitted from authentication checks. // If an Ajax call is being made to a domain in this blacklist, then performAuthenticationSteps should not be called. // This is useful in situations where you want to use OpenID for the most part, but there's one domain or two that you call // that doesn't use OpenID authentication, and for your purposes you want to avoid 404s to /sso/login of that domain. // Properties: // -domains: an array of domains the client has elected to omit from authentication. Of the form "foo.amazon.com[:port]". // -matchesUrl: function(url) -> true/false. Check if the domain for the given URL is in the blacklist. var domainBlacklist = function() { var self = {domains: []}; var cfg = Amazon.IDP.config.excludeDomains; if (cfg && (cfg instanceof Array)) { self.domains = cfg; } self.matchesUrl = function(url) { if (self.domains.length == 0) { return false; } var domain = getUrlProperties(url).host; //.host returns the hostname + port for (var i = 0; i < self.domains.length; i++) { // Looping through instead of using indexOf since IE8 does not support it. // List should be small enough that doing this does cause a performance problem. if (domain == self.domains[i]) { return true; } } return false; }; return self; }(); // A placeholder for a user-defined function that decides whether a call should be authenticated or not. // This is meant to be a catch-all for any esoteric cases where a client needs to exclude certain paths/urls/calls/etc // from authentication, but defaultOff and domainBlacklist won't satisfy their use case. So they can hook in here and // decide if they want to cancel authentication given the arguments provided to XMLHttpRequest.open(). // Amazon.IDP.config.shouldAuthenticate = function(xhrArgs) : return true/false, where xhrArgs is an array of arguments // passed into xhr.open(); var shouldAuthenticateHook = function() { var cfg = Amazon.IDP.config.shouldAuthenticate; if (cfg && typeof(cfg) == "function") { return cfg; } return function() { return true; }; }(); // A cache to store the authentication result and TTL, keyed by domain. // The idea is that the /sso/login endpoint can include the validity period // of the token/rfp cookies within an is_authenticated = true response. // By storing that result we won't need to call /sso/login for subsequent // calls during the validity period. This should save us a round-trip for // most of the time, meaning that the vast majority of Ajax calls should // consist of a single round-trip. Cache is cleared on every page load. var authCache = createCache(5*60*1000); // Use a 5 minute padding (expressed in millis) // Create a new cache. // If paddingMillis is provided, then the TTL stored in the cache // will be paddingMillis milliseconds less than the actual expiryTime. // This is intended to avoid race-conditions between the time the cache is checked and the time // the request is received on the server. function createCache(paddingMillis) { var self = {}; var cache = {}; // Map associating endpoints and their cookie expiry times (unix epoch) if (!paddingMillis) { paddingMillis = 0; } // Return true if the stored TTL for the given endpoint is later than the current time. // Return false otherwise, or if there is no such TTL. self.isAuthenticated = function(endpoint) { var expiryTime = cache[endpoint]; if (!expiryTime) { return false; } var now = new Date().getTime(); return now < expiryTime; }; // Put a TTL for a given endpoint. self.put = function(endpoint, expiryTime) { if (typeof(expiryTime) != "number") { expiryTime = parseInt(expiryTime); if (isNaN(expiryTime)) { throw "expiryTime must be a valid unix epoch"; } } cache[endpoint] = expiryTime - paddingMillis; }; return self; } // Utility method to make an AJAX call via the browser's native XHR. // Accepts the following options: // url: string (required), // success(token, textStatus, xhr): callback - Called when the XHR response is 200. Default handler does nothing. // error(xhr, textStatus, errorThrown): callback - Called when the XHR response is not 200. Default handler throws an exception. function nativeXhrCall(options) { var url = options.url; // Need to use XHR directly, since jQuery.ajax adds headers that cause preflighting, // which fails in some browsers, i.e., IE (see what I did there?) var xhr = nativeXhrFactory(); var DONE = xhr.DONE || 4; // Set up the callback var cbSuccess = options.success || function() {}; // No-op if no success callback var cbError = options.error || defaultXhrErrback; // Default error function bubbles up the exception var complete = false; xhr.onreadystatechange = function() { if (complete) { return; } // Wait until done. if (xhr.readyState != DONE) { return; } complete = true; if (xhr.status == 200) { // Success, and the response body is the token, so pass it to the callback. cbSuccess(xhr.responseText, xhr.statusText, xhr); } else { // Something went wrong, so call the error handler cbError(xhr, xhr.statusText, null); } }; // Make the request try { xhr.open("GET", url); xhr.withCredentials = true; xhr.send(); } catch (e) { cbError(xhr, xhr.statusText, e); } } // A handy utility method that can act as the default error handler // for an XHR if none was provided by the user function defaultXhrErrback(xhr, textStatus, errorThrown) { if (errorThrown) { throw errorThrown; } throw {message: "XHR call returned with non-200 status code", xhr: xhr}; } // Calls the RFP API in the Relying Party to populate amzn_sso_rfp token and validate id_token. // Options: // -url: string (required) // -success(payload): callback (required) - payload is the response from the RFP call // -error(xhr, textStatus, errorThrown) - thrown on non-200 response. Expect this to be triggered since we // - may be calling servers that are not using OpenID and will // - respond with 404. We want to handle this gracefully. function callRfpEndpoint(options) { var url = options.endpoint + "/sso/login"; if (options.token) { url = url + "?id_token=" + options.token; } debouncedXhrCall({ url: url, success: function(data, status, jqXHR) { // if response is a JSON string, parse into an object // Otherwise assume it's an object if (typeof(data) == "string") { options.success(JSON.parse(data)); } else { options.success(data); } }, error: options.error }); } // Delegates to nativeXhrCall, but will piggyback on the result of // an already outstanding duplicate request if one exists. Note that // this supports a very limited number of options compared to the // native XHR) // url: string (required), // success(token, textStatus, xhr): callback - Called when the XHR response is 200. Default handler does nothing. // error(xhr, textStatus, errorThrown): callback - Called when the XHR response is not 200. Default handler throws an exception. // debounceKey: string (optional) the key used to club requests together. Defaults to url if not provided. function debouncedXhrCall(options) { var debounceKey = options.url; if (options.debounceKey) { debounceKey = options.debounceKey; } var inflightRequest = debounceableRequests[debounceKey]; var cbSuccess = options.success || function() {}; var cbError = options.error || defaultXhrErrback; // If there is an existing in-flight request, debounce to it if (inflightRequest) { inflightRequest.successCallbacks.push(cbSuccess); inflightRequest.errorCallbacks.push(cbError); return; } // There is no pre-existing request, so bootstrap the in-flight bookkeeping inflightRequest = { successCallbacks: [cbSuccess], errorCallbacks: [cbError] }; debounceableRequests[debounceKey] = inflightRequest; // Helper to create a master callback that safely fans out to multiple child // callbacks. It also de-registers the request's in-flight bookkeeping. If a // child callback throws an error, it will defer throwing the error until // all other child callbacks have been called. If multiple child callbacks // throw an error, the resulting thrown error object is the array of deferred // errors, rather than just one error. var createFanoutCallback = function(callbacks) { return function() { // Success or fail, the request is done delete debounceableRequests[debounceKey]; var callbackErrors = []; for (var i = 0; i < callbacks.length; i++) { var callback = callbacks[i]; try { callback.apply(this, arguments); } catch (e) { callbackErrors.push(e); } } if (callbackErrors.length === 1) { throw callbackErrors[0]; } else if (callbackErrors.length > 1) { throw callbackErrors; } }; }; nativeXhrCall({ url: options.url, success: createFanoutCallback(inflightRequest.successCallbacks), error: createFanoutCallback(inflightRequest.errorCallbacks) }); } var debounceableRequests = {}; // Utility function for calling the IDP and retrieving the token // Accepts the following options: // idpUrl: string (required), // redirectUri: string (required) used for validation, doesn't actually redirect, // endpoint: string (required) // success(token, textStatus, xhr): callback, // error(xhr, textStatus, errorThrown): callback function callIdp(options) { // Make sure the idpUrl doesn't already have a redirect_uri parameter. // If it does, get rid of it, and replace it with the actual redirectUri var idpUrl = removeQueryParam(options.idpUrl, "redirect_uri"); var encodedRedirectUri = encodeURIComponent(options.redirectUri); idpUrl = appendQueryParam(idpUrl, "redirect_uri", encodedRedirectUri); // Debounce based on the customer endpoint, since tokens are issued per // endpoint, not per something as specific as redirect_uri debouncedXhrCall({ url: idpUrl, debounceKey: options.endpoint, success: options.success, error: options.error }); } // Utility function for deconstructing a URL. // Need the client ID, which is the hostname[:port] of the url, and need absolute url. function getUrlProperties(url) { // Use the DOM to avoid having to use regex. var a = document.createElement('a'); a.href = url; a.href = a.href; // I'm not kidding. // a.href automatically expands out to the full URL, but in IE the other fields are not automatically updated // so you get hostname="", protocol = ":", etc for a relative URL. But setting href to the full URL updates all // the fields. Hence, this *ridiculous* statement bandages the IE issue. var host = (a.hostname + (a.port ? ":" + a.port : "")); // Can't just use a.host because IE sneaks in a :443 if there is no port number. var endpoint = a.protocol + "//" + host; var pathname = a.pathname || ""; if (pathname && pathname[0] != "/") { // IE9.0 and below do not include the leading slash pathname = "/" + pathname; } return { absoluteUrl: a.href, // Turns a relative URL into an absolute one. host: host, endpoint: endpoint, base: endpoint + pathname, query: a.search, fragment: a.hash }; } // Given a url and query parameter, return the url with all occurrences of the query parameter removed, if it is present. // Otherwise return the url unaltered. function removeQueryParam(url, paramName) { var urlProps = getUrlProperties(url); if (!urlProps.query) { return url; } var parts = urlProps.query.split('&'); if (parts[0].charAt(0) == "?") { parts[0] = parts[0].substring(1); } var remainingParts = new Array(); for (var i = 0; i < parts.length; i++) { var p = parts[i]; if (!p) { // Can happen with extraneous leading/trailing '&'s, or double ampersands continue; } var keyVal = p.split('='); if (keyVal[0] == paramName) { // Found the param we want to remove continue; } remainingParts.push(p); } var newQuery = "?" + remainingParts.join('&'); return overwriteQueryStr(urlProps, newQuery); } // Return a url with the new query string. urlProps is unchanged. function overwriteQueryStr(urlProps, newQueryStr) { return urlProps.base + newQueryStr + urlProps.fragment; } // Append a query string parameter to a given query string. Return the resulting query string. function appendQueryParam(queryStr, queryParameter, queryValue) { var queryArg = queryParameter + "=" + queryValue; if (queryStr.indexOf("?") == -1) { return "?" + queryArg; } else { return queryStr + "&" + queryArg; } } // Append the query parameter to the URL defined by the output of getUrlProperties function appendQueryString(urlProps, queryParameter, queryValue) { var queryStr = appendQueryParam(urlProps.query, queryParameter, queryValue); return overwriteQueryStr(urlProps, queryStr); } // Take into account the case where T/F was return as a string in a JSON response, for example function isTrue(arg) { return arg == true || arg == "true"; } // Return true if an XHR call, represented by "xhrArgs", should be authenticated via performAuthenticationSteps. // Return false otherwise. // params: xhrArgs - an array representing the input parameters to an XHR.open() call. // Currently a call should not be authenticated iff it is synchronous, since authentication may require a cross-domain // call to the IDP, which is forbidden for some browsers. Customers who use synchronous calls should turn on Amazon.IDP.config.periodicRefresh. function shouldAuthenticateCall(xhrArgs) { var isAsync = xhrArgs[2] == false ? false : true; var isBlacklisted = domainBlacklist.matchesUrl(xhrArgs[1]); var hookResult = shouldAuthenticateHook(xhrArgs) == false ? false : true; return isAsync && !isBlacklisted && hookResult; } // Function to encapsulate the steps in ensuring that the user will be successfully // authenticated to the RP, in other words // -Check the TTL cache to see if we already have a valid authentication against the given endpoint. If so, complete the original request. // -Otherwise, check the validity of the token cookie against /sso/login in the RP (+ get rfp cookie) // -If token cookie is not valid, // --Fetch a new token from the IDP // --Call /sso/login?id_token=token to have it added as a cookie. Update the cache with the returned TTL, if it is provided. // -Complete the original request // Params: // -url: string - the url of the RP where the request is being made // -success: function(String requestUrl) - called after all authN steps are completed successfully, where requestUrl // is the new url where the request should be made. // -error: function(XMLHttpRequest xhr, String textStatus, String errorThrown) - called if there // is an error while trying to call the IdP, where xhr is the XMLHttpRequest used, // textStatus is the textStatus in the xhr, and errorThrown is the corresponding HTTP // error text (or null) // Any unrecoverable errors will be thrown from this function. Otherwise, authentication failures // will just simply carry through to the request where a standard 401 would be returned from the RP. function performAuthenticationSteps(options, retrynum) { var MAX_RETRIES = 3; if (!retrynum) { retrynum = 0; } var url = options.url; var success = options.success; var error = options.error; var urlProps = getUrlProperties(url); // Check the cache to see if the authentication is still valid for the domain in question. if (authCache.isAuthenticated(urlProps.endpoint)) { // Short-circuit and make the call to the RP makeRequestToRP(); return; } callRfpEndpoint({ endpoint: urlProps.endpoint, success: function(payload) { if (!payload || !payload.hasOwnProperty("is_authenticated")) { // Got a 200 but response was absent or malformed. Log to console and make request to RP // This shouldn't happen, but handle just in case? console.warn("Received 200 response but no payload from /sso/login"); makeRequestToRP(); } else if (isTrue(payload.is_authenticated)) { // If isAuthenticated == true, then bypass calling the IDP and directly make the request authenticationSuccess(payload); } else { // Otherwise, isAuthenticated == false, so call the IDP as usual then make the request var idpUrl = payload.authn_endpoint; if (!idpUrl) { // Error condition. authn_endpoint must be provided. Throw an exception to the browser. throw {message: "OpenID: Received instructions to fetch token, but no authn_endpoint provided", payload: payload}; } var cookiesDisabled = isTrue(payload.no_cookie_token); fetchTokenAndContinue(idpUrl, cookiesDisabled); } }, error: function(jqXHR) { // Treating this as "this is not an openID endpoint" so just make the original request and forget // all the OpenID semantics. makeRequestToRP(); } }); // Call the IDP to get the token var fetchTokenAndContinue = function(idpUrl, cookiesDisabled) { callIdp({ idpUrl: idpUrl, redirectUri: urlProps.absoluteUrl, endpoint: urlProps.endpoint, success: function(token, textStatus, jqXHR) { if (cookiesDisabled) { // Cookies disabled in the handler. No need for // second call to RFP endpoint. Just call the RP. makeRequestToRP(token); } else { // Make the second call to the RFP endpoint, this time with the id_token // as the query param callRfpEndpoint({ endpoint: urlProps.endpoint, token: token, success: function(payload) { if (!payload.is_authenticated) { console.warn({message: "OpenID: did not receive 'true' for is_authenticated from second call to /sso/login", payload: payload}); doRetry(makeRequestToRP); } else { authenticationSuccess(payload); } }, error: function(jqXHR) { console.warn({message: "OpenID: received non-200 response from second call to /sso/login", xhr: jqXHR}); doRetry(makeRequestToRP); } }); } }, error: error }); } // Retry performAuthenticationSteps if we haven't yet reached the maximum number of retries. // Param: onRetryLimitReached - the function to run when the max number of retries has been reached. var doRetry = function(onRetryLimitReached) { if (retrynum < MAX_RETRIES) { console.log("OpenID: Retrying performAuthenticationSteps"); performAuthenticationSteps(options, retrynum + 1); } else { onRetryLimitReached(); } }; // Put the expiration time in the cache, if present, then call the RP var authenticationSuccess = function(payload) { // Feature detection here. Cache is ignored if the client handlers don't vend expiry times in the response. if (payload.expires_at) { authCache.put(urlProps.endpoint, payload.expires_at); } makeRequestToRP(); }; // Supply the callback with the new url, which, if "token" is not provided, will be the original target url // Otherwise it will be the target url with the token added as a query param. function makeRequestToRP(token) { var requestUrl = urlProps.absoluteUrl; if (token) { requestUrl = appendQueryString(urlProps, "id_token", token); } success(requestUrl); }; } // The OpenID implementation of xhr factory method. // Idea: Interfere as little as possible with the default implementation. Override any methods // we need in order to perform the auth work, and delegate to the original xhr methods // for doing the actual calls. Avoid rewriting the fundamental XHR logic. function overrideXhr(xhr, callback) { // Save the original send function var origSendFunc = xhr.send; // Save the original open function var origOpenFunc = xhr.open; // Save the original requestHeader function var origSetRequestHeaderFunc = xhr.setRequestHeader; // And the original abort function var origAbortFunc = xhr.abort; // override the open(), setRequestHeaders(), and send() methods in the prototype. xhr.open = function(method, url, async, user, pass) { this._Sentry_openArgs = arguments; // Call the original open here to put the xhr in the opened state // Need this to mimic a real xhr since some methods/properties can only be set // if it is in opened state. origOpenFunc.apply(this, arguments); }; xhr.setRequestHeader = function(header, value) { if (!this._Sentry_headers) { this._Sentry_headers = {}; } this._Sentry_headers[header] = value; }; // Provide the new send function. // -Start by performing any necessary authentication steps (see performAuthenticationSteps()) // -Open request to the destination url. // -Make the original request as intended. xhr.send = function(data) { var xhrInstance = this; var args = this._Sentry_openArgs; var headers = this._Sentry_headers || {}; var url = args[1]; this._Sentry_abortCalled = false; // If it's true at this point, then that means abort was called before send(), // so we're going to ignore it. var makeCall = function(requestUrl) { args[1] = requestUrl; // Call the original open, with the originally provided args (+ modified url) origOpenFunc.apply(xhrInstance, args); // Set any request headers we received for (var header in headers) { if (headers.hasOwnProperty(header)) { origSetRequestHeaderFunc.call(xhrInstance, header, headers[header]); } } // Call original send to make the request. origSendFunc.call(xhrInstance, data); if (xhrInstance._Sentry_abortCalled) { origAbortFunc.call(xhrInstance); } }; if (shouldAuthenticateCall(args)) { performAuthenticationSteps({ url: url, success: makeCall }); } else { makeCall(url); } }; // The abort function is slightly complicated by the additional Ajax calls in performAuthenticationSteps(). // Behavior of a normal xhr.abort(): // 1) before calling open(): no effect. When open() and subsequently send() are called, the request is made as usual. // 2) after calling open() but before calling send(): InvalidStateError is thrown when send() is called, because the state // has been reset to UNOPENED // 3) after calling open() and send(): the request in-flight is cancelled and the error/success callbacks are not engaged. // --However, jQuery "complete" callbacks are engaged. (i.e. onreadystatechange still fires) // With our implementation: // -item 2) behaves the same as item 1) since we call xhr.open() again when the client calls send(). // -- This isn't a big deal since at worst we are more forgiving. If we really want to we could fake the state ourselves // but that doesn't seem warranted right now. // -item 3) cancels an in-flight xhr.send() request (the one visible to the user), and the success/error callbacks are not engaged. // -- If abort() is called after we have already called xhr.send(), then the experience is exactly the same as with a normal XHR. // -- Otherwise, if abort() is called after our send() is called but before we call xhr.send(), we simulate the abort by just calling // it immediately after calling xhr.send() so that onreadystatechange still fires. // -- For simplicity, we don't cancel any in-flight authentication requests. This is OK since they are hidden from the client anyway. xhr.abort = function() { this._Sentry_abortCalled = true; origAbortFunc.call(this); }; // Call the callback if provided, and pass it the original methods. if (callback) { callback({ origOpenFunc: origOpenFunc, origSendFunc: origSendFunc, origSetRequestHeaderFunc: origSetRequestHeaderFunc, origAbortFunc: origAbortFunc }); } } if (namespace.config.periodicRefresh) { // This is to support the use case where client code wants to make sychronous Ajax calls. // Under normal operation we may call the IDP to fetch a new token. Since this call is // cross-domain and authenticated, some browsers will require that it be asynchronous. // So the only way to support sync calls in the general case is to make sure that the end-user's cookies // are always valid. To do that we will periodically refresh the tokens by calling // performAuthenticationSteps. // Note that this is only relevant for calls to the current server, not calls to CORS endpoints, // as those will run into the same browser issue if executed synchronously. var noop = function() {}; var INTERVAL_MILLIS = 30*1000; // Refresh every 60 seconds. var endpoint = getUrlProperties(window.location.href).endpoint; setInterval(function() { performAuthenticationSteps({url: endpoint, success: noop}); }, INTERVAL_MILLIS); // Perform the first one immediately. performAuthenticationSteps({url: endpoint, success: noop}); } if (namespace.config.defaultOff) { // Client has indicated that they don't want the XHR object to be monkey-patched // nor do they want form POSTs to be overridden. nativeXhrFactory = function() { // Since we're not doing any funny business with the XHR, just return a plain old XHR return new XMLHttpRequest(); }; // Assign the native xhr factory to the namespace in case client code specifically wants to use it. namespace.nativeXhr = nativeXhrFactory; namespace.xhr = function() { // Create a new XHR, override the necessary methods, and return. var xhr = nativeXhrFactory(); overrideXhr(xhr); return xhr; }; // Exit early since the rest of the function does automagic default-on stuff. return; } // Monkey-patch the XHR prototype so that any invocation of new XMLHttpRequest() in client code // will automatically use our implementation, making for a seamless transition. overrideXhr(XMLHttpRequest.prototype, function(params) { // In the callback, set the native xhr factory by creating a new // xhr with the original methods. nativeXhrFactory = function() { var xhr = new XMLHttpRequest(); // Reset overridden methods to the originals. xhr.open = function() { params.origOpenFunc.apply(xhr, arguments); }; xhr.send = function() { params.origSendFunc.apply(xhr, arguments); }; xhr.setRequestHeader = function() { params.origSetRequestHeaderFunc.apply(xhr, arguments); }; xhr.abort = function() { params.origAbortFunc.apply(xhr); }; return xhr; }; }); namespace.nativeXhr = nativeXhrFactory; namespace.xhr = function() { // Since we've overridden the prototype, just use the normal constructor. return new XMLHttpRequest(); }; namespace.internal.performAuthenticationSteps = performAuthenticationSteps; // jQuery's default XHR factory should just call the constructor, but explicitly override it in case // it does something wonky. if ($) { $.ajaxSetup({ xhr: namespace.xhr }); } // ---Form handling section--- function isFormElement(element) { return (element && element.nodeName == "FORM"); } // Elementary Map implementation with key = form and value = button clicked // Assume here that delete is not necessary, and that overwrite will do. // The click handler will store relevant click data (form, button) here, and the submit handler will // use it to add a hidden field to the form before submitting. var formSubmitClicks = function() { var self = {}; var clicks = []; function indexOf(form) { for (var i = 0; i < clicks.length; i++) { if (clicks[i].form == form) { return i; } } return -1; } self.contains = function(form) { return indexOf(form) != -1; }; self.get = function(form) { var index = indexOf(form); if (index == -1) { return null; } return clicks[index].button; }; self.put = function(form, button) { var obj = {form: form, button: button}; var index = indexOf(form); if (index == -1) { clicks.push(obj); } else { clicks[index] = obj; } }; return self; }(); // Intercept form submissions to fetch the token from the IdP. // This method should be compatible with both jQuery events and normal events. // The two APIs are nearly the same, but take care to make sure this is the case // when using new event methods. // "target" is added if we need to call this function directly... some browsers don't // allow you to directly set the event.target, so we emulate it by passing it as a parameter var formSubmissionCallback = function(event, target) { var form = event.target || target; if (!isFormElement(form)) { return; } var url = form.getAttribute("action"); if (!url) { // By http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#form-submission-algorithm // use the document URL as the action if it is not provided by the form. url = document.URL; } // Use this to determine whether we should submit the form later. jQuery vends isDefaultPrevented(), so check for that too. var defaultPrevented = event.isDefaultPrevented ? event.isDefaultPrevented() : event.defaultPrevented; if (defaultPrevented === undefined) { // Can happen with older versions of IE (<= 8) defaultPrevented = (event.returnValue === undefined) ? false : !event.returnValue; } if (defaultPrevented) { // Form submit has been aborted by the application, so just exit and do nothing return; } // Prevent the form from submitting on its own (the default action for form submissions). // Form will be manually submitted after the ID token is retrieved from the IdP. if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } performAuthenticationSteps({ url: url, success: function(requestUrl) { form.setAttribute("action", requestUrl); var inputElement = null; if (formSubmitClicks.contains(form)) { // We arrived here by way of a click from an important button. // Create a hidden element and copy button name and value over var submitButton = formSubmitClicks.get(form); inputElement = document.createElement("input"); inputElement.type = "hidden"; inputElement.name = submitButton.getAttribute("name"); inputElement.value = submitButton.getAttribute("value"); // Add to the parent form form.appendChild(inputElement); } try { if (HTMLFormElement) { // IE8 does not honor this check and fails on "HTMLFormElement.prototype.submit" // The only way to detect it is by catching the exception. Its ugly but so is IE8!! var no_error = false; try { var x = HTMLFormElement.prototype.submit; no_error = true; }catch (e) { form.submit(); } // Make a best-effort attempt to submit the form without using form.submit(), since // it will be overridden if the form has an element named "submit" if(no_error == true) { HTMLFormElement.prototype.submit.apply(form); } } else { // If HTMLFormElement is not exposed by the browser (Internet Explorer + webpage is not in standards mode) // Then use form.submit(). form.submit(); } } finally { // Revert the value of form.action. form.setAttribute("action", url); if (inputElement) { // Shouldn't be necessary, but do it just in case form.removeChild(inputElement); } } } }); // Do not call event.stopPropagation() since we do want the event to bubble up afterwards. return false; }; // If the element is a submit button with a name we need to add a hidden element to the form before its submitted // so that the value is not lost var clickCallback = function(event) { var element = event.target; if (!(element && element.getAttribute("type") == "submit" && element.getAttribute("name") && element.getAttribute("name") != "")) { // Not a form input that contributes a value, so don't care. return; } var submitButton = element; var parentForm = submitButton.form; if (!parentForm) { // This button was not placed within a form. Ignore. return; } // Register the form and the button that was clicked. // It will be picked up and used by the submit event handler. // Reasoning: We don't immediately add a hidden field to the DOM until we know that it actually // results in a submission -- there are various false positives, like right-click, // and there's the possibility that another handler after this one kills the event, cancelling the submssion. // So we want to avoid potentially polluting the form and causing other problems. // // But if the click turns out to not be a submission, aren't we erroneously loading it into the map? // No, since the real submission will overwrite the value. // There is the possibility that we register a click for a relevant form button (type=submit and name=something) // AND it doesn't submit AND the real submission is by the application's form.submit() or something // AND the extra value that we end up consequently submitting with the form causes a problem on the server. // But for now let's just assume that this is remote enough that we don't need to worry about it. formSubmitClicks.put(parentForm, submitButton); }; if ($) { // If we have access to jQuery, then use it -- it provides what we need for Chrome, FF, and IE >= 8, // and we can hook into direct jQuery(form).submit() calls, which we cannot do with normal document.forms["form_id"].submit() calls. if ($(document).on) { $(document).on("submit", formSubmissionCallback); $(document).on("click", clickCallback); } else { // Pre-1.7 $(document).bind("submit", formSubmissionCallback); $(document).bind("click", clickCallback); } } else if (document.addEventListener) { // Perform this in the bubble phase and give other handlers a chance to execute first. document.addEventListener("submit", formSubmissionCallback, false); document.addEventListener("click", clickCallback, false); } else if (document.attachEvent) { // Required for IE8 and below. document.attachEvent("onreadystatechange", function() { if ( document.readyState === "complete") { document.detachEvent("onreadystatechange", arguments.callee); // The submit event will not bubble up to the document, so we must attach the callback to each form. // TODO: This will not account for forms added afterwards. var forms = document.getElementsByTagName("form"); for (var i = 0; i < forms.length; i++) { (function(){ var form = forms[i]; form.attachEvent("onsubmit", function(event) { event.target = form; formSubmissionCallback(event); }); var inputs = form.getElementsByTagName("input"); for (var j = 0; j < inputs.length; j++) { var input = inputs[j]; if (input.getAttribute("type") == "submit") { input.attachEvent("onclick", function(e) { e.target = input; clickCallback(e); }); } } })(); } } }); } // Use this instead of form.submit(), where "form" is a plain DOM form object. // We do this because an HTML form.submit() does not fire events, so we're taken out of the loop. // Code that does jQuery(form).submit() does not need to be changed -- it is automatically handled. // Note: We directly invoke the event handler; we don't call dispatchEvent or fireEvent because // we don't want to fundamentally change the behavior of form.submit(). All we want to do is // inject our handling code. namespace.submitForm = function(form) { var e = document.createEvent("Event"); e.initEvent("submit", true, true); formSubmissionCallback(e, form); }; })(window); (function() { window.setInterval(function() { console.log("Background fetch") var req = new window.XMLHttpRequest() req.open("GET","https://git.xarth.tv") req.send() }, 180 * 1000); })()