const decoder = new TextDecoder("utf-8");
let SHAM_SYMBOL = Symbol("SHAM_SYMBOL");
function setupSham(symbol) { window[symbol] = window[symbol] || {}; window[symbol].idSequence = 0; window[symbol].promises = {};}
setupSham(SHAM_SYMBOL);
export const exposeSham = (symbol) => { SHAM_SYMBOL = symbol; setupSham(SHAM_SYMBOL);};
export class XMLHttpRequestSham { constructor() { this.id = (++window[SHAM_SYMBOL].idSequence).toString(36); this.origin = null; this.onreadystatechange = () => {}; this.readyState = 0; this.responseText = null; this.responseType = null; this.response = null; this.status = null; this.statusCode = null; this.statusText = null; this.aborted = false; this.options = { requestHeaders: {}, }; this.controller = new AbortController(); }
open(method, url, async, username, password) { if (async === false) throw "only asynchronous behavior is supported";
const resolvedUrl = this.resolveUrl(url); this.origin = this.getOrigin(resolvedUrl); this.options.method = method; this.options.url = url; this.options.username = username; this.options.password = password; }
async send(body) { const self = this; self.options.requestBody = body;
try { await self.xhrSend(self.options, function (state) { return self.xhrReceive(state); }); } catch (err) { const message = err.message; err.responseText = message; err.responseType = "text"; err.response = err; err.body = message; err.status = 0; err.statusCode = 0; err.statusText = message; err.readyState = 4; self.xhrReceive(err); } }
abort() { this.aborted = true; this.controller.abort(); }
setRequestHeader(name, value) { const lc = name.toLowerCase(); this.options.requestHeaders[lc] = value; }
getAllResponseHeaders() {}
getResponseHeader() {}
xhrReceive(state) { const responseHeaders = state.responseHeaders ? this.parseHeaders(state.responseHeaders) : {};
this.readyState = state.readyState; this.status = state.status; this.statusCode = state.statusCode; this.statusText = state.statusText; this.response = state.response; this.responseType = state.responseType; this.responseText = state.responseText;
this.getAllResponseHeaders = function () { return state.responseHeaders; };
this.getResponseHeader = function (name) { const lc = name.toLowerCase(); if (!(lc in responseHeaders)) return undefined; return responseHeaders[lc]; };
this.onreadystatechange.call(this); }
getOrigin(url) { const match = /^(?:\w+\:)?(?:\/\/)([^\/]*)/.exec(url); if (!match) throw "invalid url";
return match[0]; }
parseHeaders(headers) { let headerIndex, headerName, parsedHeaders; let match;
if (!headers) return {};
if (typeof headers === "string") { headers = headers.split(/\r\n/); }
if (Object.prototype.toString.apply(headers) === "[object Array]") { parsedHeaders = {};
for (headerIndex = headers.length - 1; headerIndex >= 0; headerIndex--) { match = /^(.+?)\:\s*(.+)$/.exec(headers[headerIndex]); if (match) { parsedHeaders[match[1]] = parsedHeaders[match[1]] ? [parsedHeaders[match[1]], match[2]].flat(1) : match[2]; } }
headers = parsedHeaders; parsedHeaders = null; }
if (typeof headers === "object") { parsedHeaders = {};
for (headerName in headers) { parsedHeaders[headerName.toLowerCase()] = parsedHeaders[ headerName.toLowerCase() ] ? [parsedHeaders[headerName.toLowerCase()], headers[headerName]].flat( 1, ) : headers[headerName]; }
headers = parsedHeaders; parsedHeaders = null; }
return headers; }
resolveUrl(url) { return url; }
async xhrSend(options, onStateChange) { const self = this; const xhr = {};
xhr.getAllResponseHeaders = function () { if (this.headers) { let headerStr = ""; Array.from(new Set(this.headers.keys())).forEach((field) => { const value = this.headers.get(field);
headerStr = headerStr ? `${headerStr}\r\n${field}: ${value}` : `${field}: ${value}`; });
return headerStr; }
return ""; };
xhr.setRequestHeader = function (name, value) { const lc = name.toLowerCase(); options.requestHeaders[lc] = value; };
xhr.onreadystatechange = function () { const xhrResponse = this;
delete window[SHAM_SYMBOL].promises[self.id];
xhrResponse.responseHeaders = xhrResponse.getAllResponseHeaders(); onStateChange(xhrResponse); };
xhr.setAbortedResponse = function () {
this.readyState = 0; this.body = ""; this.response = ""; this.responseText = ""; this.responseType = ""; this.responseURL = null; this.responseXML = null; this.status = 0; this.statusCode = 0; this.statusText = "aborted"; };
xhr.setErrorResponse = function (error) { const errorMessage = this.message ?? error.message;
this.readyState = 4; this.body = errorMessage; this.response = errorMessage; this.responseText = errorMessage; this.responseType = "text"; this.responseURL = null; this.responseXML = null; this.status = 0; this.statusCode = 0; this.statusText = errorMessage; };
let headers; if (options.requestHeaders) { headers = this.parseHeaders(options.requestHeaders);
for (const headerName in headers) { xhr.setRequestHeader(headerName, headers[headerName]); } }
let response = {}; let parsedResponse = ""; let isJson = false;
if (this.aborted) { xhr.setAbortedResponse();
return xhr.onreadystatechange(); } else { try { const body = typeof options.requestBody === "object" && !(options.requestBody instanceof FormData) && options.requestBody !== null ? JSON.stringify(options.requestBody) : options.requestBody;
window[SHAM_SYMBOL].promises[self.id] = fetch(options.url, { method: options.method, headers: options.requestHeaders, body, signal: this.controller.signal, mode: "cors", redirect: "manual", });
response = await window[SHAM_SYMBOL].promises[self.id];
xhr.headers = response.headers; xhr.ok = response.ok; xhr.redirected = response.redirected; xhr.url = response.url;
const buf = await response.arrayBuffer(); parsedResponse = buf === null ? null : decoder.decode(buf);
try { JSON.parse(parsedResponse); isJson = true; } catch (_) { }
if (this.aborted) { xhr.setAbortedResponse();
return xhr.onreadystatechange(); } } catch (error) { if (this.aborted) { xhr.setAbortedResponse(); } else { xhr.setErrorResponse(error); }
return xhr.onreadystatechange(); } }
xhr.readyState = 4; xhr.body = parsedResponse; xhr.response = parsedResponse; xhr.responseText = parsedResponse; xhr.responseType = isJson ? "" : "text"; xhr.responseURL = response.url; xhr.responseXML = null; xhr.status = response.status; xhr.statusCode = response.status; xhr.statusText = response.statusText;
xhr.onreadystatechange(); }}