ajax
// ==UserScript==
// @name ajax
// @author cxxjackie
// @version 1.0.2
// ==/UserScript==
var ajax = function() {
let defaultOptions = {};
const requests = new WeakMap();
function isString(str) {
return typeof str === 'string' || str instanceof String;
}
function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false;
let proto = obj;
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(obj) === proto;
}
function isSimpleObject(obj) {
if (!isPlainObject(obj)) return false;
for (const key in obj) {
if (typeof obj[key] === 'object') {
return false;
}
}
return true;
}
function parseUrlString(obj) {
if (typeof obj !== 'object' || obj === null) return '';
let str = '';
for (const key in obj) {
str += `&${key}=${encodeURIComponent(obj[key])}`;
}
return str.substr(1);
}
function lowerHeaders(headers) {
if (typeof headers !== 'object' || headers === null) return {};
const lheaders = {};
for (const key in headers) {
lheaders[key.toLowerCase()] = headers[key];
}
return lheaders;
}
function parseHeaders(str) {
if (!str) return {};
const headers = {};
const arr = str.trim().split(/[\r\n]+/);
for (const line of arr) {
const parts = line.split(/:\s*/);
const lheader = parts[0].toLowerCase();
if (lheader in headers) {
headers[lheader] += ', ' + parts[1];
} else {
headers[lheader] = parts[1];
}
}
return headers;
}
function parseJson(str) {
let result = str;
try {
while (typeof result !== 'object') {
result = JSON.parse(result);
}
} catch (e) {
result = null;
}
return result;
}
function parseDom(str, contentType) {
let mimeType = contentType ? contentType.split(';').shift() : 'text/html';
switch (mimeType) {
case 'text/html':
case 'text/xml':
case 'application/xml':
case 'application/xhtml+xml':
case 'image/svg+xml':
break;
default:
mimeType = 'text/html';
}
return new DOMParser().parseFromString(str, mimeType);
}
function objToString() {
return JSON.stringify(this);
}
function adaptData(options) {
if (!('data' in options)) return;
if (options.method === 'get') {
let search;
if (isString(options.data)) {
search = options.data;
} else if (isSimpleObject(options.data)) {
search = parseUrlString(options.data);
}
if (search) options.url += (options.url.includes('?') ? '&' : '?') + search;
}
if (options.method === 'post') {
if (!('content-type' in options.headers)) {
switch (true) {
case isString(options.data):
options.headers['content-type'] = 'application/x-www-form-urlencoded';
break;
case isSimpleObject(options.data):
options.headers['content-type'] = 'application/x-www-form-urlencoded';
options.data = parseUrlString(options.data);
break;
case isPlainObject(options.data):
options.headers['content-type'] = 'application/json';
options.data = JSON.stringify(options.data);
break;
}
} else if (!isString(options.data)) {
const mimeType = options.headers['content-type'].split(';').shift();
switch (mimeType) {
case 'application/x-www-form-urlencoded':
options.data = parseUrlString(options.data);
break;
case 'application/json':
options.data = JSON.stringify(options.data);
break;
}
}
}
}
function ajaxGM(GM_xhr, options) {
const {_status, _detail, _nocatch, ..._options} = options;
let abort = () => {};
const promise = new Promise((resolve, reject) => {
const _resolve = data => {
requests.delete(promise);
resolve(_detail ? data : data.response);
};
const _reject = (reason, status = 0) => {
requests.delete(promise);
const err = {
url: _options.url,
status: status,
error: reason
};
err.toString = objToString;
_nocatch ? resolve(_detail ? err : null) : reject(err);
};
const responseType = _options.responseType;
if (['text', 'json', 'document'].includes(responseType)) delete _options.responseType;
const xhr = GM_xhr({
..._options,
onload: res => {
if (_status.includes(res.status)) {
let result;
const headers = parseHeaders(res.responseHeaders);
switch (responseType) {
case 'text':
result = res.responseText;
break;
case 'json':
result = parseJson(res.responseText);
if (!result) return _reject('parse json failed', res.status);
break;
case 'document':
const contentType = _options.overrideMimeType || headers['content-type'];
result = parseDom(res.responseText, contentType);
break;
default:
result = res.response;
}
_resolve({
finalUrl: res.finalUrl,
status: res.status,
response: result,
responseHeaders: headers
});
} else {
_reject('bad status', res.status);
}
},
onerror: () => _reject('request failed'),
onabort: () => _reject('aborted'), // Don't work in Tampermonkey < 4.9.5927
ontimeout: () => _reject('timeout')
});
if (xhr && typeof xhr.abort === 'function') {
abort = () => {
xhr.abort();
_reject('aborted');
};
}
});
requests.set(promise, {abort});
return promise;
}
function ajaxXHR(options) {
const {_status, _detail, _nocatch, ..._options} = options;
const xhr = new XMLHttpRequest();
const abort = xhr.abort.bind(xhr);
const promise = new Promise((resolve, reject) => {
const _resolve = data => {
requests.delete(promise);
resolve(_detail ? data : data.response);
};
const _reject = reason => {
requests.delete(promise);
const err = {
url: _options.url,
status: xhr.status,
error: reason
};
err.toString = objToString;
_nocatch ? resolve(_detail ? err : null) : reject(err);
};
xhr.open(_options.method, _options.url, true, _options.user, _options.password);
for (const header in _options.headers) {
xhr.setRequestHeader(header, _options.headers[header]);
}
if ('overrideMimeType' in _options) {
xhr.overrideMimeType(_options.overrideMimeType);
delete _options.overrideMimeType;
}
const responseType = _options.responseType;
if (responseType === 'json') _options.responseType = 'text';
for (const key in _options) {
switch (typeof xhr[key]) {
case 'object':
if (key.startsWith('on')) {
xhr[key] = e => _options[key].call(xhr, e.target);
} else {
xhr[key] = _options[key];
}
break;
case 'function':
case 'undefined':
break;
default:
xhr[key] = _options[key];
}
}
xhr.onload = () => {
if (_status.includes(xhr.status)) {
let result;
if (responseType === 'json') {
result = parseJson(xhr.responseText);
if (!result) _reject('parse json failed');
} else {
result = xhr.response;
}
_resolve({
finalUrl: xhr.responseURL,
status: xhr.status,
response: result,
responseHeaders: parseHeaders(xhr.getAllResponseHeaders())
});
} else {
_reject('bad status');
}
};
xhr.onerror = () => _reject('request failed');
xhr.onabort = () => _reject('aborted');
xhr.ontimeout = () => _reject('timeout');
xhr.send(_options.data);
});
requests.set(promise, {abort});
return promise;
}
function ajax(desc, obj = {}) {
switch (desc) {
case 'default':
defaultOptions = obj;
defaultOptions.headers = lowerHeaders(defaultOptions.headers);
return;
case 'abort':
if (requests.has(obj)) {
requests.get(obj).abort();
}
return;
}
const {onload, onerror, onabort, ontimeout, _mode, ...options} = {
method: 'get',
responseType: 'text',
_status: [200],
_detail: false,
_nocatch: false,
...defaultOptions,
...obj,
url: desc
};
options.method = options.method.toLowerCase();
options.headers = lowerHeaders(options.headers);
if ('headers' in defaultOptions) {
options.headers = {...defaultOptions.headers, ...options.headers};
}
if ('cookie' in options) {
options.headers.cookie = options.cookie;
delete options.cookie;
}
adaptData(options);
let GM_xhr;
switch (_mode) {
case 'GM_xhr':
case 'GM_xmlhttpRequest':
if (!window.GM_xmlhttpRequest) {
return Promise.reject('缺少GM_xmlhttpRequest,请检查你的脚本声明。');
}
GM_xhr = window.GM_xmlhttpRequest;
break;
case 'GM.xhr':
case 'GM.xmlHttpRequest':
if (!window.GM || !window.GM.xmlHttpRequest) {
return Promise.reject('缺少GM.xmlHttpRequest,请检查你的脚本声明。');
}
GM_xhr = window.GM.xmlHttpRequest;
break;
case 'xhr':
case 'XMLHttpRequest':
break;
default:
GM_xhr = window.GM_xmlhttpRequest || (window.GM && window.GM.xmlHttpRequest);
}
return GM_xhr ? ajaxGM(GM_xhr.bind(window), options) : ajaxXHR(options);
}
return ajax;
}();