Anakunda / xhrLib

// ==UserScript==
// ==UserLibrary==
// @name         xhrLib
// @namespace    https://openuserjs.org/users/Anakunda
// @copyright    2021, Anakunda (https://openuserjs.org/users/Anakunda)
// @license      GPL-3.0-or-later
// @version      1.22.1
// @author       Anakunda
// @exclude      *
// ==/UserScript==
// ==/UserLibrary==

'use strict';

var domParser = new DOMParser;

let isBrokenTM;
try {
	isBrokenTM = GM_info.scriptHandler == 'Tampermonkey' && /^(?:4\.12)\b/.test(GM_info.version);
} catch(e) { isBrokenTM = false }

function localXHR(url, params, data) {
	return url ? new Promise(function(resolve, reject) {
		function getParam(key) {
			if (!key || typeof key != 'string' || typeof params != 'object') return undefined;
			key = Object.keys(params).find(_key => _key.toLowerCase() == key.toLowerCase());
			return key !== undefined ? params[key] : undefined;
		}

		let xhr = new XMLHttpRequest, method = (getParam('method') || 'GET').toUpperCase(), noResponse = false;
		if (data) method = 'POST';
		xhr.open(method, url, true);
		xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		const responseType = getParam('responseType');
		if (method != 'HEAD') switch (responseType) {
			case null: noResponse = true; break;
			case undefined: xhr.responseType = 'document'; break;
			default: if (responseType) xhr.responseType = responseType;
		}
		switch (xhr.responseType) {
			case 'document': xhr.setRequestHeader('Accept', 'text/html'); break;
			case 'xml': xhr.setRequestHeader('Accept', 'text/xml'); break;
			case 'json': xhr.setRequestHeader('Accept', 'application/json'); break;
			default: xhr.setRequestHeader('Accept', '*/*');
		}
		if (data === undefined) data = getParam('body');
		if (data !== undefined) {
			if (typeof data == 'string') {
				xhr.setRequestHeader('Content-Type', 'text/plain; charset=UTF-8');
				//xhr.setRequestHeader('Content-Length', data.length);
			} else if (data instanceof FormData) {
				//xhr.setRequestHeader('Content-Type', 'multipart/form-data; charset=utf-8');
				//xhr.setRequestHeader('Content-Length', ???);
			} else if (data instanceof URLSearchParams) {
				xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
				//xhr.setRequestHeader('Content-Length', data.toString().length);
				//data = data.toString();
			} else if (data instanceof ArrayBuffer) {
				//xhr.setRequestHeader('Content-Type', 'application/arraybuffer');
				//xhr.setRequestHeader('Content-Length', data.byteLength);
			} else if (data && typeof data == 'object') {
				xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
				data = JSON.stringify(data);
				//xhr.setRequestHeader('Content-Length', data.length);
			}
		}
		let headers = getParam('headers');
		if (typeof headers == 'object') Object.keys(headers).forEach(key => { xhr.setRequestHeader(key, headers[key]) });
		if (method == 'HEAD') xhr.onreadystatechange = function() {
			if (this.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
			if (this.status == 0 || this.status >= 200 && this.status < 400) resolve(this);
				else reject(defaultErrorHandler(this));
		}; else if (noResponse) xhr.onreadystatechange = function() {
			if (this.readyState < XMLHttpRequest.DONE) return;
			if (this.status >= 200 && this.status < 400) resolve(this.status);
				else reject(defaultErrorHandler(this));
		}; else xhr.onload = function() {
			if (this.status >= 200 && this.status < 400) resolve(this.response);
				else reject(defaultErrorHandler(this));
		};
		xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
		xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
		//xhr.timeout = 60000;
		//console.debug('XMLHttpRequest.send(...):', xhr);
		xhr.send(data);
	}) : Promise.reject(new Error('URL missing'));
}

function globalXHR(url, params, data) {
	return url ? new Promise(function(resolve, reject) {
		function addFunc(response) {
			response.getHeaderValue = function(key) {
				if (!key || typeof key != 'string') return undefined;
				let entries = this.responseHeaders.split(/\r?\n/)
					.map(line => /^(\S+)\s*:\s*(.*)$/.test(line) ? [RegExp.$1.toLowerCase(), RegExp.$2] : null)
					.filter(Array.isArray);
				return new Map(entries).get(key.toLowerCase());
			};
		}

		const localParams = Object.assign({
			url: url,
			method: data ? 'POST' : 'GET',
			headers: { },
		}, params);
		if (localParams.method.toUpperCase() != 'HEAD' && localParams.responseType === undefined)
			localParams.responseType = 'document';
		if (typeof localParams.headers != 'object') localParams.headers = { };
		if (!localParams.headers.Accept) switch (localParams.responseType ? localParams.responseType.toLowerCase() : undefined) {
			case 'document': case 'text/html': case 'html': localParams.headers.Accept = 'text/html'; break;
			case 'xml': case 'text/xml': localParams.headers.Accept = 'text/xml'; break;
			case 'json': case 'application/json': localParams.headers.Accept = 'application/json'; break;
			default: localParams.headers.Accept = '*/*';
		}
		if (isBrokenTM && localParams.responseType && localParams.responseType.toLowerCase() == 'document')
		    localParams.responseType = 'text/html';
		if (!localParams.fetch) localParams.headers['X-Requested-With'] = 'XMLHttpRequest';
		if (localParams.method == 'HEAD') {
			localParams.onreadystatechange = function(response) {
				if (response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
				if (response.status >= 200 && response.status < 400) {
					addFunc(response);
					resolve(response);
				} else reject(defaultErrorHandler(response));
				hXHR.abort();
			};
			delete localParams.responseType;
		} else localParams.onload = function(response) {
			if ((response.status < 200 || response.status >= 400) && (response.finalUrl || response.status != 0)) {
				let error = defaultErrorHandler(response);
				if (localParams.responseType && localParams.responseType.toLowerCase() == 'json') try {
					reject(`HTTP error ${response.status}: response code ${response.response.error.code} (${response.response.error.message})`);
					return;
				} catch(e) { }
				return reject(error);
			}
			if (localParams.responseType) switch (localParams.responseType.toLowerCase()) {
				case 'document': case 'text/html': case 'html':
					if (response.response instanceof HTMLDocument) response.document = response.response;
					else if (response.responseText) try {
						response.document = domParser.parseFromString(response.responseText, 'text/html');
					} catch(e) {
						console.warn('globalXHR(...) response not valid HTML:', response.responseText, e);
						response.document = null;
					}
					break;
				case 'application/json': case 'json':
					if (typeof response.response == 'object') response.json = response.response;
					else if (response.responseText) try {
						response.json = JSON.parse(response.responseText);
					} catch(e) {
						console.warn('globalXHR(...) response not valid JSON:', response.responseText, e);
						response.json = null;
					}
					break;
				case 'text/xml': case 'xml':
					if (response.response instanceof XMLDocument) response.xml = response.response;
					else if (response.responseText) try {
						response.xml = domParser.parseFromString(response.responseText, 'text/xml');
					} catch(e) {
						console.warn('globalXHR(...) response not valid XML:', response.responseText, e);
						response.xml = null;
					}
					break;
			}
			addFunc(response);
			resolve(response);
		};
		localParams.onerror = response => { reject(defaultErrorHandler(response)) };
		localParams.ontimeout = response => { reject(defaultTimeoutHandler(response)) };
		if (data === undefined) data = localParams.body;
		if (data !== undefined) {
			if (typeof data == 'string') {
				if (!localParams.headers['Content-Type']) localParams.headers['Content-Type'] = 'text/plain; charset=UTF-8';
				//localParams.headers['Content-Length'] = data.length;
				localParams.data = data;
			} else if (data instanceof FormData) {
				//if (!localParams.headers['Content-Type']) xhr.setRequestHeader('Content-Type', 'multipart/form-data; charset=utf-8');
				//xhr.setRequestHeader('Content-Length', ???);
				localParams.data = data;
			} else if (data instanceof URLSearchParams) {
				if (!localParams.headers['Content-Type']) localParams.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
				//localParams.headers['Content-Length'] = data.toString().length;
				localParams.data = data.toString();
			} else if (data instanceof ArrayBuffer) {
				//if (!localParams.headers['Content-Type']) localParams.headers['Content-Type'] = 'application/arraybuffer';
				//localParams.headers['Content-Length'] = data.byteLength;
				localParams.data = data;
			} else if (data instanceof Blob) {
				localParams.data = data;
			} else if (data && typeof data == 'object') {
				if (!localParams.headers['Content-Type']) localParams.headers['Content-Type'] = 'application/json; charset=UTF-8';
				let json = JSON.stringify(data);
				//localParams.headers['Content-Length'] = json.length;
				localParams.data = json;
			} else localParams.data = data;
		}
		if ('body' in localParams) delete localParams.body;
		//console.debug('GM_xmlhttpRequest(...):', localParams);
		var hXHR = GM_xmlhttpRequest(localParams);
	}) : Promise.reject('URL missing');
}

function fileReader(filePath, params) {
	let unc = new URL('file:' + filePath);
	return globalXHR(unc, params);
}
function textFileReader(filePath, params) {
	return fileReader(filePath, Object.assign(params || { }, { responseType: 'text' }))
		.then(response => response.responseText);
}

function defaultErrorHandler(response) {
	console.error('HTTP error:', response);
	let e = 'HTTP error ' + response.status;
	if (response.statusText) e += ' (' + response.statusText + ')';
	if (response.error) e += ' (' + response.error + ')';
	return e;
}

function defaultTimeoutHandler(response) {
	console.error('HTTP timeout:', response);
	const e = 'HTTP timeout';
	return e;
}