Anakunda / Requests

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

'use strict';

class XHR {
	static defaultErrorHandler(responseRoot) {
		console.error('HTTP error:', responseRoot);
		let reason = 'HTTP error ' + responseRoot.status;
		if (responseRoot.status == 0) reason += '/' + responseRoot.readyState;
		let statusText = [responseRoot.statusText];
		if (responseRoot.response) try {
			if (typeof responseRoot.response.error == 'string') statusText.push(responseRoot.response.error);
		} catch(e) { }
		statusText = statusText.filter(Boolean);
		if (statusText.length > 0) reason += ' (' + statusText.join(' / ') + ')';
		return reason;
	}

	static defaultTimeoutHandler(responseRoot) {
		console.error('HTTP timeout:', responseRoot);
		let reason = 'HTTP timeout';
		if (responseRoot.timeout) reason += ' (' + responseRoot.timeout + ')';
		return reason;
	}

	static caselessProxy(target, writable = true) {
		const findProperty = (target, property) => (property = (property || '').toLowerCase(),
			Object.keys(target).find(key => key.toLowerCase() == property));
		return new Proxy(target || { }, {
			get: (target, property) => Reflect.get(target, findProperty(target, property)),
			has: (target, property) => Boolean(findProperty(target, property)),
			set: writable ? (target, property, value) => Reflect.set(target, findProperty(target, property) || property, value) : () => false,
			deleteProperty: writable ? (target, property) => Reflect.deleteProperty(target, findProperty(target, property)) : () => false,
		});
	}

	static extendResponse(response, responseType) {
		function extendResponse(prototype, sourceParser, property, sourceProperty = 'response') {
			try {
				let descriptor = [Object.getOwnPropertyDescriptor(response, sourceProperty)];
				console.assert(descriptor[0] instanceof Object, response);
				if (!(descriptor[0] instanceof Object)) return response;
				if (XHR.gmInfo.scriptHandler != 'Violentmonkey') {
					function makeStatic() {
						descriptor[1] = { value: sourceValue };
						if (descriptor[0].configurable) Object.defineProperty(response, sourceProperty, Object.assign(descriptor[1], { enumerable: true }));
					}

					let sourceValue = response[sourceProperty];
					if (sourceValue instanceof prototype) { if (XHR.resolvedResponse) makeStatic() }
                        else if (response.responseText && (sourceValue = sourceParser(response.responseText)) instanceof prototype) makeStatic();
				}
				descriptor = descriptor[1] || ('get' in descriptor[0] ? { get: descriptor[0].get } : 'value' in descriptor[0] ? { value: descriptor[0].value } : undefined);
				if (descriptor) Object.defineProperty(response, property, Object.assign(descriptor, { enumerable: true }));
			} catch(e) { console.warn('XHR.request(...) response not valid %s (%s):', prototype.name, e, response) }
		}

		switch (responseType) {
			case 'document': case 'text/html': case 'html':
				extendResponse(HTMLDocument, responseText => new DOMParser().parseFromString(responseText, 'text/html'), 'document');
				break;
			case 'json': case 'application/json':
				extendResponse(Object, responseText => JSON.parse(response.responseText), 'json');
				break;
			case 'xml': case 'text/xml':
				extendResponse(XMLDocument, responseText => new DOMParser().parseFromString(responseText, 'text/xml'), 'xml', 'responseXML');
				break;
		}
		return response;
	}

	static responseSuccess(root) {
		console.assert(root, 'Void response root');
		return root && root.status >= 200 && root.status < 400;
	}

	static getHostRoot(location) {
		location = location.hostname.toLowerCase().split('.');
		const offset = location.reverse().findIndex(name => !XHR.TLDs.includes(name));
		return (offset < 0 ? location : location.slice(0, Math.max(offset, 1) + 1)).reverse().join('.');
	}

	static request(url, params, postData) {
		let xhrClass;
		try {
			if (!/^https?:\/\//i.test(url) || XHR.getHostRoot(new URL(url)) == XHR.getHostRoot(document.location))
				xhrClass = LocalXHR;
		} catch(e) { }
		return (xhrClass || GlobalXHR).request(url, params, postData);
	}
}
Object.defineProperties(XHR, {
	maxRetries: { value: 60, writable: true, enumerable: true },
	retryTimeout: { value: 1000, writable: true, enumerable: true },
	recoverableErrors: { value: new Set([
		500, 502, 503, 504,
		520, 522, 523, 524, 525, 527,
		530,
	]), enumerable: true },
	TLDs: { value: [
		'aaa', 'aarp', 'abb', 'abbott', 'abbvie', 'abc', 'able', 'abogado', 'abudhabi', 'ac', 'academy', 'accenture', 'accountant', 'accountants', 'aco', 'actor', 'ad', 'ads', 'adult', 'ae', 'aeg', 'aero', 'aetna', 'af', 'afl', 'africa', 'ag', 'agakhan', 'agency', 'ai', 'aig', 'airbus', 'airforce', 'airtel', 'akdn', 'al', 'alibaba', 'alipay', 'allfinanz', 'allstate', 'ally', 'alsace', 'alstom', 'am', 'amazon', 'americanexpress', 'americanfamily', 'amex', 'amfam', 'amica', 'amsterdam', 'analytics', 'android', 'anquan', 'anz', 'ao', 'aol', 'apartments', 'app', 'apple', 'aq', 'aquarelle', 'ar', 'arab', 'aramco', 'archi', 'army', 'arpa', 'art', 'arte', 'as', 'asda', 'asia', 'associates', 'at', 'athleta', 'attorney', 'au', 'auction', 'audi', 'audible', 'audio', 'auspost', 'author', 'auto', 'autos', 'aw', 'aws', 'ax', 'axa', 'az', 'azure',
		'ba', 'baby', 'baidu', 'banamex', 'band', 'bank', 'bar', 'barcelona', 'barclaycard', 'barclays', 'barefoot', 'bargains', 'baseball', 'basketball', 'bauhaus', 'bayern', 'bb', 'bbc', 'bbt', 'bbva', 'bcg', 'bcn', 'bd', 'be', 'beats', 'beauty', 'beer', 'bentley', 'berlin', 'best', 'bestbuy', 'bet', 'bf', 'bg', 'bh', 'bharti', 'bi', 'bible', 'bid', 'bike', 'bing', 'bingo', 'bio', 'biz', 'bj', 'black', 'blackfriday', 'blockbuster', 'blog', 'bloomberg', 'blue', 'bm', 'bms', 'bmw', 'bn', 'bnpparibas', 'bo', 'boats', 'boehringer', 'bofa', 'bom', 'bond', 'boo', 'book', 'booking', 'bosch', 'bostik', 'boston', 'bot', 'boutique', 'box', 'br', 'bradesco', 'bridgestone', 'broadway', 'broker', 'brother', 'brussels', 'bs', 'bt', 'build', 'builders', 'business', 'buy', 'buzz', 'bv', 'bw', 'by', 'bz', 'bzh',
		'ca', 'cab', 'cafe', 'cal', 'call', 'calvinklein', 'cam', 'camera', 'camp', 'canon', 'capetown', 'capital', 'capitalone', 'car', 'caravan', 'cards', 'care', 'career', 'careers', 'cars', 'casa', 'case', 'cash', 'casino', 'cat', 'catering', 'catholic', 'cba', 'cbn', 'cbre', 'cc', 'cd', 'center', 'ceo', 'cern', 'cf', 'cfa', 'cfd', 'cg', 'ch', 'chanel', 'channel', 'charity', 'chase', 'chat', 'cheap', 'chintai', 'christmas', 'chrome', 'church', 'ci', 'cipriani', 'circle', 'cisco', 'citadel', 'citi', 'citic', 'city', 'ck', 'cl', 'claims', 'cleaning', 'click', 'clinic', 'clinique', 'clothing', 'cloud', 'club', 'clubmed', 'cm', 'cn', 'co', 'coach', 'codes', 'coffee', 'college', 'cologne', 'com', 'commbank', 'community', 'company', 'compare', 'computer', 'comsec', 'condos', 'construction', 'consulting', 'contact', 'contractors', 'cooking', 'cool', 'coop', 'corsica', 'country', 'coupon', 'coupons', 'courses', 'cpa', 'cr', 'credit', 'creditcard', 'creditunion', 'cricket', 'crown', 'crs', 'cruise', 'cruises', 'cu', 'cuisinella', 'cv', 'cw', 'cx', 'cy', 'cymru', 'cyou', 'cz',
		'dabur', 'dad', 'dance', 'data', 'date', 'dating', 'datsun', 'day', 'dclk', 'dds', 'de', 'deal', 'dealer', 'deals', 'degree', 'delivery', 'dell', 'deloitte', 'delta', 'democrat', 'dental', 'dentist', 'desi', 'design', 'dev', 'dhl', 'diamonds', 'diet', 'digital', 'direct', 'directory', 'discount', 'discover', 'dish', 'diy', 'dj', 'dk', 'dm', 'dnp', 'do', 'docs', 'doctor', 'dog', 'domains', 'dot', 'download', 'drive', 'dtv', 'dubai', 'dunlop', 'dupont', 'durban', 'dvag', 'dvr', 'dz',
		'earth', 'eat', 'ec', 'eco', 'edeka', 'edu', 'education', 'ee', 'eg', 'email', 'emerck', 'energy', 'engineer', 'engineering', 'enterprises', 'epson', 'equipment', 'er', 'ericsson', 'erni', 'es', 'esq', 'estate', 'et', 'eu', 'eurovision', 'eus', 'events', 'exchange', 'expert', 'exposed', 'express', 'extraspace',
		'fage', 'fail', 'fairwinds', 'faith', 'family', 'fan', 'fans', 'farm', 'farmers', 'fashion', 'fast', 'fedex', 'feedback', 'ferrari', 'ferrero', 'fi', 'fidelity', 'fido', 'film', 'final', 'finance', 'financial', 'fire', 'firestone', 'firmdale', 'fish', 'fishing', 'fit', 'fitness', 'fj', 'fk', 'flickr', 'flights', 'flir', 'florist', 'flowers', 'fly', 'fm', 'fo', 'foo', 'food', 'football', 'ford', 'forex', 'forsale', 'forum', 'foundation', 'fox', 'fr', 'free', 'fresenius', 'frl', 'frogans', 'frontier', 'ftr', 'fujitsu', 'fun', 'fund', 'furniture', 'futbol', 'fyi',
		'ga', 'gal', 'gallery', 'gallo', 'gallup', 'game', 'games', 'gap', 'garden', 'gay', 'gb', 'gbiz', 'gd', 'gdn', 'ge', 'gea', 'gent', 'genting', 'george', 'gf', 'gg', 'ggee', 'gh', 'gi', 'gift', 'gifts', 'gives', 'giving', 'gl', 'glass', 'gle', 'global', 'globo', 'gm', 'gmail', 'gmbh', 'gmo', 'gmx', 'gn', 'godaddy', 'gold', 'goldpoint', 'golf', 'goo', 'goodyear', 'goog', 'google', 'gop', 'got', 'gov', 'gp', 'gq', 'gr', 'grainger', 'graphics', 'gratis', 'green', 'gripe', 'grocery', 'group', 'gs', 'gt', 'gu', 'gucci', 'guge', 'guide', 'guitars', 'guru', 'gw', 'gy',
		'hair', 'hamburg', 'hangout', 'haus', 'hbo', 'hdfc', 'hdfcbank', 'health', 'healthcare', 'help', 'helsinki', 'here', 'hermes', 'hiphop', 'hisamitsu', 'hitachi', 'hiv', 'hk', 'hkt', 'hm', 'hn', 'hockey', 'holdings', 'holiday', 'homedepot', 'homegoods', 'homes', 'homesense', 'honda', 'horse', 'hospital', 'host', 'hosting', 'hot', 'hotels', 'hotmail', 'house', 'how', 'hr', 'hsbc', 'ht', 'hu', 'hughes', 'hyatt', 'hyundai',
		'ibm', 'icbc', 'ice', 'icu', 'id', 'ie', 'ieee', 'ifm', 'ikano', 'il', 'im', 'imamat', 'imdb', 'immo', 'immobilien', 'in', 'inc', 'industries', 'infiniti', 'info', 'ing', 'ink', 'institute', 'insurance', 'insure', 'int', 'international', 'intuit', 'investments', 'io', 'ipiranga', 'iq', 'ir', 'irish', 'is', 'ismaili', 'ist', 'istanbul', 'it', 'itau', 'itv',
		'jaguar', 'java', 'jcb', 'je', 'jeep', 'jetzt', 'jewelry', 'jio', 'jll', 'jm', 'jmp', 'jnj', 'jo', 'jobs', 'joburg', 'jot', 'joy', 'jp', 'jpmorgan', 'jprs', 'juegos', 'juniper',
		'kaufen', 'kddi', 'ke', 'kerryhotels', 'kerrylogistics', 'kerryproperties', 'kfh', 'kg', 'kh', 'ki', 'kia', 'kids', 'kim', 'kindle', 'kitchen', 'kiwi', 'km', 'kn', 'koeln', 'komatsu', 'kosher', 'kp', 'kpmg', 'kpn', 'kr', 'krd', 'kred', 'kuokgroup', 'kw', 'ky', 'kyoto', 'kz',
		'la', 'lacaixa', 'lamborghini', 'lamer', 'lancaster', 'land', 'landrover', 'lanxess', 'lasalle', 'lat', 'latino', 'latrobe', 'law', 'lawyer', 'lb', 'lc', 'lds', 'lease', 'leclerc', 'lefrak', 'legal', 'lego', 'lexus', 'lgbt', 'li', 'lidl', 'life', 'lifeinsurance', 'lifestyle', 'lighting', 'like', 'lilly', 'limited', 'limo', 'lincoln', 'link', 'lipsy', 'live', 'living', 'lk', 'llc', 'llp', 'loan', 'loans', 'locker', 'locus', 'lol', 'london', 'lotte', 'lotto', 'love', 'lpl', 'lplfinancial', 'lr', 'ls', 'lt', 'ltd', 'ltda', 'lu', 'lundbeck', 'luxe', 'luxury', 'lv', 'ly',
		'ma', 'madrid', 'maif', 'maison', 'makeup', 'man', 'management', 'mango', 'map', 'market', 'marketing', 'markets', 'marriott', 'marshalls', 'mattel', 'mba', 'mc', 'mckinsey', 'md', 'me', 'med', 'media', 'meet', 'melbourne', 'meme', 'memorial', 'men', 'menu', 'merckmsd', 'mg', 'mh', 'miami', 'microsoft', 'mil', 'mini', 'mint', 'mit', 'mitsubishi', 'mk', 'ml', 'mlb', 'mls', 'mm', 'mma', 'mn', 'mo', 'mobi', 'mobile', 'moda', 'moe', 'moi', 'mom', 'monash', 'money', 'monster', 'mormon', 'mortgage', 'moscow', 'moto', 'motorcycles', 'mov', 'movie', 'mp', 'mq', 'mr', 'ms', 'msd', 'mt', 'mtn', 'mtr', 'mu', 'museum', 'music', 'mv', 'mw', 'mx', 'my', 'mz',
		'na', 'nab', 'nagoya', 'name', 'navy', 'nba', 'nc', 'ne', 'nec', 'net', 'netbank', 'netflix', 'network', 'neustar', 'new', 'news', 'next', 'nextdirect', 'nexus', 'nf', 'nfl', 'ng', 'ngo', 'nhk', 'ni', 'nico', 'nike', 'nikon', 'ninja', 'nissan', 'nissay', 'nl', 'no', 'nokia', 'norton', 'now', 'nowruz', 'nowtv', 'np', 'nr', 'nra', 'nrw', 'ntt', 'nu', 'nyc', 'nz',
		'obi', 'observer', 'office', 'okinawa', 'olayan', 'olayangroup', 'ollo', 'om', 'omega', 'one', 'ong', 'onl', 'online', 'ooo', 'open', 'oracle', 'orange', 'org', 'organic', 'origins', 'osaka', 'otsuka', 'ott', 'ovh',
		'pa', 'page', 'panasonic', 'paris', 'pars', 'partners', 'parts', 'party', 'pay', 'pccw', 'pe', 'pet', 'pf', 'pfizer', 'pg', 'ph', 'pharmacy', 'phd', 'philips', 'phone', 'photo', 'photography', 'photos', 'physio', 'pics', 'pictet', 'pictures', 'pid', 'pin', 'ping', 'pink', 'pioneer', 'pizza', 'pk', 'pl', 'place', 'play', 'playstation', 'plumbing', 'plus', 'pm', 'pn', 'pnc', 'pohl', 'poker', 'politie', 'porn', 'post', 'pr', 'pramerica', 'praxi', 'press', 'prime', 'pro', 'prod', 'productions', 'prof', 'progressive', 'promo', 'properties', 'property', 'protection', 'pru', 'prudential', 'ps', 'pt', 'pub', 'pw', 'pwc', 'py',
		'qa', 'qpon', 'quebec', 'quest',
		'racing', 'radio', 're', 'read', 'realestate', 'realtor', 'realty', 'recipes', 'red', 'redstone', 'redumbrella', 'rehab', 'reise', 'reisen', 'reit', 'reliance', 'ren', 'rent', 'rentals', 'repair', 'report', 'republican', 'rest', 'restaurant', 'review', 'reviews', 'rexroth', 'rich', 'richardli', 'ricoh', 'ril', 'rio', 'rip', 'ro', 'rocks', 'rodeo', 'rogers', 'room', 'rs', 'rsvp', 'ru', 'rugby', 'ruhr', 'run', 'rw', 'rwe', 'ryukyu',
		'sa', 'saarland', 'safe', 'safety', 'sakura', 'sale', 'salon', 'samsclub', 'samsung', 'sandvik', 'sandvikcoromant', 'sanofi', 'sap', 'sarl', 'sas', 'save', 'saxo', 'sb', 'sbi', 'sbs', 'sc', 'scb', 'schaeffler', 'schmidt', 'scholarships', 'school', 'schule', 'schwarz', 'science', 'scot', 'sd', 'se', 'search', 'seat', 'secure', 'security', 'seek', 'select', 'sener', 'services', 'seven', 'sew', 'sex', 'sexy', 'sfr', 'sg', 'sh', 'shangrila', 'sharp', 'shell', 'shia', 'shiksha', 'shoes', 'shop', 'shopping', 'shouji', 'show', 'si', 'silk', 'sina', 'singles', 'site', 'sj', 'sk', 'ski', 'skin', 'sky', 'skype', 'sl', 'sling', 'sm', 'smart', 'smile', 'sn', 'sncf', 'so', 'soccer', 'social', 'softbank', 'software', 'sohu', 'solar', 'solutions', 'song', 'sony', 'soy', 'spa', 'space', 'sport', 'spot', 'sr', 'srl', 'ss', 'st', 'stada', 'staples', 'star', 'statebank', 'statefarm', 'stc', 'stcgroup', 'stockholm', 'storage', 'store', 'stream', 'studio', 'study', 'style', 'su', 'sucks', 'supplies', 'supply', 'support', 'surf', 'surgery', 'suzuki', 'sv', 'swatch', 'swiss', 'sx', 'sy', 'sydney', 'systems', 'sz',
		'tab', 'taipei', 'talk', 'taobao', 'target', 'tatamotors', 'tatar', 'tattoo', 'tax', 'taxi', 'tc', 'tci', 'td', 'tdk', 'team', 'tech', 'technology', 'tel', 'temasek', 'tennis', 'teva', 'tf', 'tg', 'th', 'thd', 'theater', 'theatre', 'tiaa', 'tickets', 'tienda', 'tips', 'tires', 'tirol', 'tj', 'tjmaxx', 'tjx', 'tk', 'tkmaxx', 'tl', 'tm', 'tmall', 'tn', 'to', 'today', 'tokyo', 'tools', 'top', 'toray', 'toshiba', 'total', 'tours', 'town', 'toyota', 'toys', 'tr', 'trade', 'trading', 'training', 'travel', 'travelers', 'travelersinsurance', 'trust', 'trv', 'tt', 'tube', 'tui', 'tunes', 'tushu', 'tv', 'tvs', 'tw', 'tz',
		'ua', 'ubank', 'ubs', 'ug', 'uk', 'unicom', 'university', 'uno', 'uol', 'ups', 'us', 'uy', 'uz',
		'va', 'vacations', 'vana', 'vanguard', 'vc', 've', 'vegas', 'ventures', 'verisign', 'versicherung', 'vet', 'vg', 'vi', 'viajes', 'video', 'vig', 'viking', 'villas', 'vin', 'vip', 'virgin', 'visa', 'vision', 'viva', 'vivo', 'vlaanderen', 'vn', 'vodka', 'volvo', 'vote', 'voting', 'voto', 'voyage', 'vu',
		'wales', 'walmart', 'walter', 'wang', 'wanggou', 'watch', 'watches', 'weather', 'weatherchannel', 'webcam', 'weber', 'website', 'wed', 'wedding', 'weibo', 'weir', 'wf', 'whoswho', 'wien', 'wiki', 'williamhill', 'win', 'windows', 'wine', 'winners', 'wme', 'wolterskluwer', 'woodside', 'work', 'works', 'world', 'wow', 'ws', 'wtc', 'wtf',
		'xbox', 'xerox', 'xihuan', 'xin', 'xxx', 'xyz',
		'yachts', 'yahoo', 'yamaxun', 'yandex', 'ye', 'yodobashi', 'yoga', 'yokohama', 'you', 'youtube', 'yt', 'yun',
		'za', 'zappos', 'zara', 'zero', 'zip', 'zm', 'zone', 'zuerich', 'zw',
	], enumerable: true },
	debugLogging: { value: false, writable: true, enumerable: true },
	loggingUrlFilter: { value: null, writable: true, enumerable: true },
	resolvedResponse: { value: true, writable: true, enumerable: true },
	gmInfo: { value: (function() {
		try {
			if (typeof GM_info == 'object') return GM_info;
			if (typeof GM.info == 'object') return GM.info;
		} catch(e) { }
		return { };
	})() },
	declareStandardMethods: { value: prototype => Object.defineProperties(prototype, {
		get: { value: (url, params) => prototype.request(url, Object.assign({ }, params, { method: 'GET' })), enumerable: true },
		head: { value: (url, params) => prototype.request(url, Object.assign({ }, params, { method: 'HEAD' })), enumerable: true },
		post: { value: (url, postData, params) => prototype.request(url, Object.assign({ }, params, { method: 'POST' }), postData), enumerable: true },
	}) },
});
XHR.declareStandardMethods(XHR);

class LocalXHR {
	static request(url, params, postData) {
		if (!url) throw new Error('URL missing');
		const xhr = new XMLHttpRequest, recoverableErrors = new Set(XHR.recoverableErrors);
		let maxRetries = XHR.maxRetries, retryTimeout = XHR.retryTimeout, retryCounter = 0;
		params = Object.assign({ }, params);
		if ('recoverableErrors' in params) for (let status of params.recoverableErrors) recoverableErrors.add(status);
		if ('fatalErrors' in params) for (let status of params.fatalErrors) recoverableErrors.delete(status);
		if ('maxRetries' in params && params.maxRetries >= 0) maxRetries = params.maxRetries;
		if ('retryTimeout' in params && params.retryTimeout >= 0) retryTimeout = params.retryTimeout;
		params.headers = XHR.caselessProxy(params.headers);
		if (postData === undefined) postData = params.data;
		if (postData === undefined) postData = params.body;
		if (postData instanceof Object && Object.getPrototypeOf(postData).isPrototypeOf({ })) {
			postData = JSON.stringify(postData);
			params.headers['Content-Type'] = 'application/json; charset=UTF-8';
		}
		params.method = (params.method || (postData ? 'POST' : 'GET')).toUpperCase();
		if (params.method != 'HEAD') if (params.responseType === undefined) xhr.responseType = 'document';
		else if (params.responseType) xhr.responseType = params.responseType.toLowerCase();
		switch (xhr.responseType) {
			case 'document': case 'text/html': case 'html': params.headers.Accept = 'text/html'; break;
			case 'xml': case 'text/xml': params.headers.Accept = 'text/xml'; break;
			case 'json': case 'application/json': params.headers.Accept = 'application/json'; break;
			case 'text': case 'text/plain': params.headers.Accept = 'text/plain'; break;
		}
		if (params.timeout > 0) xhr.timeout = params.timeout;
		return new Promise(function(resolve, reject) {
			function request() {
				xhr.open(params.method, url, true);
				for (let header in params.headers) xhr.setRequestHeader(header, params.headers[header]);
				xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
				if (XHR.debugLogging && (typeof XHR.loggingUrlFilter != 'function' || XHR.loggingUrlFilter(url.toString())))
					console.debug('XMLHttpRequest.send(...):', xhr, postData);
				xhr.send(postData);
			}
			function responseAdapter(xhr, haveResponse = true) {
				const properties = [
					'readyState', 'status', 'statusText', 'responseURL',
					'getAllResponseHeaders', 'getResponseHeader',
				];
				if (haveResponse) properties.push('response', 'responseText', 'responseXML');
				return Object.defineProperties({ }, Object.assign.apply(null, properties.map(function(property) {
					const descriptor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, property);
					console.assert(descriptor instanceof Object, property);
					if (!(descriptor instanceof Object)) return;
					console.assert('get' in descriptor != 'value' in descriptor, descriptor);
					if ('value' in descriptor) return { [property]: { value: descriptor.value.bind(xhr), enumerable: true } };
					if ('get' in descriptor) return { [property]: { get: descriptor.get.bind(xhr), enumerable: true } };
				}).filter(Boolean)));
			}
			function errorHandler() {
				if ((this.readyState == XMLHttpRequest.DONE && this.status == 0 || recoverableErrors.has(this.status)
						|| /\b(?:too many requests)\b/i.test(this.statusText)) && retryCounter++ < maxRetries) {
					setTimeout(request, retryTimeout || 0);
					console.log('LocalXHR request retry #%d/%d on HTTP error %d', retryCounter, maxRetries, this.status);
				} else {
					reject(XHR.defaultErrorHandler(this));
					if (['json', 'application/json'].includes(this.responseType)) try {
						console.log('Error response:', this.response);
					} catch(e) { }
				}
			}

			if (params.method == 'HEAD') xhr.onreadystatechange = function() {
				if (this.readyState != XMLHttpRequest.HEADERS_RECEIVED) return;
				if (XHR.responseSuccess(this)) resolve(responseAdapter(this, false)); else errorHandler.call(this);
			}; else if (params.responseType === null) xhr.onreadystatechange = function() {
				if (this.readyState != XMLHttpRequest.DONE) return;
				if (XHR.responseSuccess(this)) resolve(responseAdapter(this, false)); else errorHandler.call(this);
			}; else xhr.onload = function() {
				if (XHR.responseSuccess(this)) resolve(XHR.extendResponse(responseAdapter(this), this.responseType)); else errorHandler.call(this);
			};
			xhr.onerror = errorHandler;
			xhr.ontimeout = function() { reject(XHR.defaultTimeoutHandler(this)) };
			request();
		});
	}
}
XHR.declareStandardMethods(LocalXHR);

class GlobalXHR {
	static responseAdapter(response, haveResponse = true) {
		const headers = { }, cookies = { };
		if (response.responseHeaders) for (let header of response.responseHeaders.split(/(?:\r?\n)+/))
			if ((header = /^([^:]+?)\s*:\s*(.*)$/.exec(header.trim())) != null)
				if (header[1].toLowerCase() == 'set-cookie') {
					header = header[2].split(';').map(attribute => attribute.trim()).filter(Boolean);
					let cookie = { }, name;
					for (let attribute of header) if ((attribute = /^([^=]+)(?:=(.+))?$/.exec(attribute)) != null) if (!name) {
						name = attribute[1];
						cookie.value = attribute[2];
					} else cookie[attribute[1]] = attribute[2];
					if (name) cookies[name] = XHR.caselessProxy(cookie, false);
				} else headers[header[1]] = header[2];
		const properties = [
			'readyState', 'status', 'statusText', 'finalUrl', 'context',
			'lengthComputable', 'loaded', 'total', // VM specific
		];
		if (haveResponse) properties.push('response', 'responseText', 'responseXML');
		if (GlobalXHR.retainRawHeaders) properties.push('responseHeaders');
		response = Object.defineProperties({ }, Object.assign.apply(null, properties.map(function(property) {
			const descriptor = Object.getOwnPropertyDescriptor(response, property);
			if (!(descriptor instanceof Object)) return;
			console.assert('get' in descriptor != 'value' in descriptor, descriptor);
			if ('value' in descriptor) return { [property]: { value: descriptor.value, enumerable: true } };
			if ('get' in descriptor) return { [property]: { get: descriptor.get.bind(response), enumerable: true } };
		}).filter(Boolean)));
		return Object.defineProperties(response, {
			headers: { value: XHR.caselessProxy(headers, false), enumerable: true },
			cookies: { value: XHR.caselessProxy(cookies, false), enumerable: true },
		});
	}

	static request(url, params, postData) {
		if (!url) throw new Error('URL missing');
		const xhr = (function() { try {
			if (typeof GM_xmlhttpRequest == 'function') return GM_xmlhttpRequest;
			if (typeof GM.xmlHttpRequest == 'function') return GM.xmlHttpRequest;
		} catch(e) { } })();
		if (!xhr) throw new Error('No variant of GM xmlhttpRequest function is granted access');
		let maxRetries = XHR.maxRetries, retryTimeout = XHR.retryTimeout;
		params = Object.assign({ }, params, { url: url });
		const headers = XHR.caselessProxy(params.headers = Object.assign({ }, params.headers));
		const recoverableErrors = new Set(XHR.recoverableErrors);
		if ('recoverableErrors' in params) {
			for (let status of params.recoverableErrors) recoverableErrors.add(status);
			delete params.recoverableErrors;
		}
		if ('fatalErrors' in params) {
			for (let status of params.fatalErrors) recoverableErrors.delete(status);
			delete params.fatalErrors;
		}
		if ('maxRetries' in params) {
			if (params.maxRetries >= 0) maxRetries = params.maxRetries;
			delete params.maxRetries;
		}
		if ('retryTimeout' in params) {
			if (params.retryTimeout >= 0) retryTimeout = params.retryTimeout;
			delete params.retryTimeout;
		}
		if (params.cookie instanceof Object && !Array.isArray(params.cookie))
			params.cookie = Object.keys(params.cookie).map(key => key + '=' + params.cookie[key]);
		if (Array.isArray(params.cookie)) params.cookie = params.cookie.join('; ');
		if (postData !== undefined) params.data = postData;
		if ('body' in params) {
			if (params.data === undefined) params.data = params.body;
			delete params.body;
		}
		if (params.data instanceof Object && Object.getPrototypeOf(params.data).isPrototypeOf({ })) {
			params.data = JSON.stringify(params.data);
			headers['Content-Type'] = 'application/json; charset=UTF-8';
		}
		params.method = (params.method || (params.data ? 'POST' : 'GET')).toUpperCase();
		if (params.method != 'HEAD') if (params.responseType === undefined) params.responseType = 'document';
		else if (params.responseType) params.responseType = params.responseType.toLowerCase();
		if (!headers.Accept) switch (params.responseType) {
			case 'document': case 'text/html': case 'html': headers.Accept = 'text/html'; break;
			case 'xml': case 'text/xml': headers.Accept = 'text/xml'; break;
			case 'json': case 'application/json': headers.Accept = 'application/json'; break;
			case 'text': case 'text/plain': headers.Accept = 'text/plain'; break;
		}
		if (params.fetch) delete headers['X-Requested-With']; else headers['X-Requested-With'] = 'XMLHttpRequest';
		if (params.method == 'HEAD' && 'responseType' in params) delete params.responseType;
		return new Promise(function(resolve, reject) {
			function request() {
				if (XHR.debugLogging && (typeof XHR.loggingUrlFilter != 'function' || XHR.loggingUrlFilter(params.url.toString())))
					console.debug('GM_xmlhttpRequest(...):', params);
				hXHR = xhr(params);
			}
			function errorHandler(response) {
				if ((response.readyState == XMLHttpRequest.DONE && response.status == 0 || recoverableErrors.has(response.status)
						|| /\b(?:too many requests)\b/i.test(response.statusText)) && retryCounter++ < maxRetries) {
					setTimeout(request, retryTimeout || 0);
					console.log('GlobalXHR request retry #%d/%d on HTTP error %d', retryCounter, maxRetries, response.status);
				} else {
					reject(XHR.defaultErrorHandler(response));
					if (['json', 'application/json'].includes(response.responseType)) try {
						console.warn('Error response:', response.response);
					} catch(e) { }
				}
			}

			let retryCounter = 0, hXHR;
			if (params.method == 'HEAD') params.onreadystatechange = function(response) {
				if (response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
				if (XHR.responseSuccess(response)) resolve(GlobalXHR.responseAdapter(response, false)); else errorHandler(response);
			}; else if (params.responseType === null) params.onreadystatechange = function(response) {
				if (response.readyState != XMLHttpRequest.DONE) return;
				if (XHR.responseSuccess(response)) resolve(GlobalXHR.responseAdapter(response, false)); else errorHandler(response);
			}; else params.onload = function(response) {
				if (XHR.responseSuccess(response)) resolve(XHR.extendResponse(GlobalXHR.responseAdapter(response), params.responseType));
				else errorHandler(response);
			};
			params.onerror = errorHandler;
			params.ontimeout = response => { reject(XHR.defaultTimeoutHandler(response)) };
			request();
		});
	}
}
Object.defineProperty(GlobalXHR, 'retainRawHeaders', { value: false, writable: true, enumerable: true });
XHR.declareStandardMethods(GlobalXHR);

//////////////////////////////////////////////////////////////////////////
////////////////////////////// SAFE PADDING //////////////////////////////
//////////////////////////////////////////////////////////////////////////