{ const form = e.target; if (e instanceof SubmitEvent && form instanceof HTMLFormElement && form.method.toUpperCase() === 'POST' && !getSetting('memberID') && getCsrfKeyExpiry() < Date.now() / 1000) { Debug.log(`Intercepted form submission... fetching fresh CSRF key`); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); getFreshCsrfKey() // .then(() => new Promise(resolve => setTimeout(resolve, 2000))) // DEBUG - wait a couple seconds so I can read the logs .then(() => { Debug.log(`Got a new CSRF key, resuming the form submission`); let submitButton = (e instanceof SubmitEvent || e.submitter instanceof Element) ? e.submitter : form.querySelector('button[type="submit"], button:not([type]), input[type="submit"]'); if (submitButton) { submitButton.click(); } else { // the click() causes the native submit event to fire. However, if there was no submitter (e.g. the user pressed 'Enter'), we need to dispatch our own submit event and see if they call prevent default. const ev = new CustomSubmitEvent(e); form.dispatchEvent(ev); if (ev.defaultPrevented) { return; } form.submit(); } }); } }, {capture: true}); // popover polyfill. todo at somepoint, e.g. 2027 or later, we can remove this and expect devices support popovers natively if (!(HTMLElement.prototype.showPopover instanceof Function)) { Debug.log(`Loading popover polyfill`); setTimeout(() => { ips.loader.getStatic('/applications/core/interface/static/popover/popover.min.js'); }); } // Add a little jQuery plugin that allows us to add callbacks // to CSS animations $.fn.animationComplete = function (callback) { // Use native JS DOM APIs and promises to handle animationComplete, rather than waiting for an animation event. if ('getAnimations' in Element.prototype) { const elements = [...this]; // .animationComplete() is usually called before animations begin, so we wait for a full event loop cycle (new Promise(resolve => setTimeout(resolve, 0))) .then(() => Promise.allSettled((elements.map(el => el.getAnimations()).flat()).map(anim => anim.finished))) .then(() => callback.apply(this)); return $(this); } // fallback to the jquery system. return $( this ).one('webkitAnimationEnd animationend', function (e) { // Important fix: ignore bubbled transition events if( e.target == this ){ callback.apply( this ); } }); }; // In case we already have ips_uid elements on the page, start at the highest found // jQuery plugin that adds a unique id to an element if an ID doesn't // already exist. Based on jQueryUI method. $.fn.identify = function () { return this.each( function () { if( !this.id ) { this.id = 'ips_uid_' + urand + '_' + (++uid); } }); }; // Redefine .prop() so that we can observe events when properties are changed // See: http://stackoverflow.com/questions/16336473/add-event-handler-when-checkbox-becomes-disabled var oldProp = $.fn.prop; var newProp = function () { var retFunc = oldProp.apply( this, arguments ); this.trigger( 'propChanged', this ); return retFunc; }; $.fn.prop = newProp; // Add a utility function for easily adding // methods to a prototype. // From "Javascript: The Good Parts" by Douglas Crockford Function.prototype.method = function (name, func) { this.prototype[name] = func; return this; }; // Set up mustache-style templates for language interpolation in underscore _.templateSettings = { interpolate: /\{\{(.+?)\}\}/g }; // Warn users about pasting stuff into the console if( !Debug.isEnabled() && window.console ){ window.console.log("%cThis is a browser feature intended for developers. Do not paste any code here given to you by someone else. It may compromise your account or have other negative side effects.", "font-weight: bold; font-size: 14px;"); } // Signal that we're ready to begin $( document ) .trigger('doneBooting') .ready( function () { // Set our location _location = $( 'body' ).attr('data-pageLocation') || 'front'; }); }, /** * Allows us to use mock ajax objects if necessary, and fetches a fresh CSRF token if we're on a guest page before sending the request * * @returns {object} Ajax object (jQuery's $.ajax by default) */ getAjax = function () { const wrap = method => (...args) => { let options = {}, url; if (typeof args[0] === 'string') { url = args[0]; options = args[1] || {}; } else if (typeof args[0] === 'object' && args[0]) { options = args[0]; } // In this case, we completely skip redirects on both the front and back end if (options?.bypassRedirect === 'none') { options.headers = options.headers || {}; options.headers['x-bypass-ajax-redirect'] = '1'; } if ((options.method || options.type)?.toLowerCase() !== 'post' || getSetting('memberID') || getCsrfKeyExpiry() >= Date.now() / 1000) { return method(...args); } // Guest with an expired CSRF key? Oh boy, we need to return a proxy object for an api encompassing its own polyfill (our polyfill > $.ajax > XMLHttpRequest) let requestInstance; const decoratorQueues = {}; const properties = {}; let thenResolve, thenReject, mimeTypeOverride; const uploadEventListeners = new Map(); const uploadDispatchedEvents = []; const proxyXHRObject = { _promise: new Promise((resolve, reject) => { thenResolve = resolve; thenReject = reject }), then(cb) { this._promise.then(cb); }, catch(...args) { this._promise.catch(...args) }, finally(...args) { this._promise.finally(...args); }, _aborted: false, get aborted() { return this._aborted; }, _abortedStatusText: undefined, get abortedStatusText() { return this._abortedStatusText; }, abort(statusText) { this._aborted = true; this._abortedStatusText = statusText; requestInstance?.abort(statusText); }, getAllResponseHeaders() { return requestInstance?.getAllResponseHeaders() || null; }, getResponseHeader(header) { return requestInstance?.getResponseHeader(header) || null; }, overrideMimeType(mimeType) { if (requestInstance) { requestInstance.overrideMimeType(mimeType); return; } mimeTypeOverride = mimeType; }, get readyState() { return requestInstance ? requestInstance.readyState : 0; }, get response() { return requestInstance ? ('response' in requestInstance ? requestInstance.response : null) : null; }, get responseText() { if (typeof requestInstance?.responseText === 'string') { return requestInstance.responseText; } return ""; }, get responseType() { if (typeof requestInstance?.responseType === 'string') { return requestInstance.responseType; } return ""; }, get responseURL() { return requestInstance ? requestInstance.responseURL : (url || options.url || window.location.href); }, get responseXML() { return requestInstance ? requestInstance.responseXML : null; }, get status() { return requestInstance ? requestInstance.status : 0; }, get statusText() { return requestInstance ? requestInstance.statusText : ""; }, get timeout() { return properties.timeout || 0; }, set timeout(val) { properties.timeout = val; if (requestInstance) { requestInstance.timeout = val; } }, get withCredentials() { return !!(requestInstance ? requestInstance.withCredentials : properties.withCredentials); }, set withCredentials(val) { val = !!val; properties.withCredentials = val; // track this in case it was set before the request instance was defined if (requestInstance) { requestInstance.withCredentials = val; } }, _upload: { _getEventCollection(event, options) { const path = [ event, (options && options.capture) ? "true" : "false" ]; let collection = uploadEventListeners; for (let i = 0; i < path.length; i++) { const cmp = path[i]; collection.set(cmp, collection.get(cmp) || new Map()); collection = collection.get(cmp); } return collection; }, addEventListener(event, cb, options={}) { requestInstance?.upload?.addEventListener?.(event, cb, options); this._getEventCollection(event, options).set(cb, {options, event}); }, removeEventListener(event, cb, options={}) { requestInstance?.upload?.removeEventListener?.(event, cb, options); this._getEventCollection(event, options).delete(cb); }, dispatchEvent(...args) { if (requestInstance?.upload?.dispatchEvent instanceof Function) { requestInstance.upload.dispatchEvent(...args); return; } uploadDispatchedEvents.push(args); }, set onabort(cb) { this.addEventListener('abort', cb); }, set onerror(cb) { this.addEventListener('error', cb); }, set onload(cb) { this.addEventListener('load', cb); }, set onloadend(cb) { this.addEventListener('loadend', cb); }, set onloadstart(cb) { this.addEventListener('loadstart', cb); }, set onprogress(cb) { this.addEventListener('progress', cb); }, set ontimeout(cb) { this.addEventListener('timeout', cb); } }, get upload() { return requestInstance ? requestInstance.upload : this._upload; } } for (const chainableMethod of ['done','always', 'fail', 'success', 'error', 'complete']) { proxyXHRObject[chainableMethod] = (function(...args) { decoratorQueues[chainableMethod] = decoratorQueues[chainableMethod] || []; decoratorQueues[chainableMethod].push(args); requestInstance?.[chainableMethod]?.(...args); return this; }).bind(proxyXHRObject); } for (const simpleMethod of ['getAllResponseHeaders', 'getResponseHeader']) { proxyXHRObject[simpleMethod] = (function(...args) { return requestInstance?.[simpleMethod]?.(...args); }).bind(proxyXHRObject); } getFreshCsrfKey() .then(() => { Debug.log(`Intercepted Fetch request and get fresh CSRF Key`); requestInstance = method(url, options); if (requestInstance.upload) { if (Object.keys(uploadEventListeners).length) { function evalCollection(collection) { for (const [key, childCollection] of collection.entries()) { if (key instanceof Function) { requestInstance.upload.addEventListener(childCollection.event, key, childCollection.options); } else { evalCollection(childCollection); } } } evalCollection(uploadEventListeners); } for (const args of uploadDispatchedEvents) { requestInstance.upload.dispatchEvent(...args); } } if (mimeTypeOverride) { requestInstance.overrideMimeType(mimeTypeOverride); } for (const queue in decoratorQueues) { requestInstance[queue](...decoratorQueues[queue]); } for (const property in properties) { requestInstance[property] = properties[property]; } requestInstance .then((...result) => { thenResolve(...result); }).catch(err => { thenReject(err); }); if (proxyXHRObject.aborted) { proxyXHRObject.abortedStatusText ? requestInstance.abort(proxyXHRObject.abortedStatusText) : requestInstance.abort(); } }); return proxyXHRObject; } if( getSetting('mock_ajax') ){ return wrap(getSetting('mock_ajax')); } return wrap($.ajax); }, /** * Async wrapper for our getAjax method to act like fetch. Resolves the response data or throws an error on reject * * @typedef ajaxOptions * @property {string} [accepts='depends on dataType'] The content type accpeted in a response * @property {Function} [beforeSend] An optional callback which modifies the jqXHR request before sending it out; see http://api.jquery.com/Types/#jqXHR * @property {boolean} [cache=true] Whether to allow the browser to cache the response of this request * @property {string} [contentType='application/x-www-form-urlencoded; charset=UTF-8'|false] The content type of the data being sent out, or false to not send a content type request header * @property {boolean} [crossDomain=false] Whether to force a cross domain request. NOTE this is automatically set when the url is cross domain, and rarely if ever should be set manually * @property {object|string|array} [data] The request body * @property {"html"|"xml"|"script"|"json"|"jsonp"|"text"|string} [dataType] The content type you are expecting in the response; This is usually not needed and will be interpreted by the mime type. You can also pass a space delimited list of types *@property {Object.} [headers] Additional request headers to send * @property {boolean} [ifModified=false] Only treat the response as successful if the response changed since the last request, determined by the lastModified response header * @property {"GET"|"POST"|"PUT"|"DELETE"|"PATCH"|string} [method="GET"] The HTTP Request method * @property {string} [mimeType] A string to overload the XHR mimetype * @property {string} [password] A password to use for HTTP basic auth * @property {boolean} [processData=false] Whether to process data passed in the `data` property before sending it to the server to match the content type (e.g. an object is converted to a query string) * @property {string} [scriptCharset] If you are using script transport (e.g. appending a