// ==UserScript==
// ==UserLibrary==
// @name imageHostUploader
// @namespace https://openuserjs.org/users/Anakunda
// @exclude *
// @version 2.48.45
// @author Anakunda
// @license GPL-3.0-or-later
// @copyright 2024, Anakunda (https://openuserjs.org/users/Anakunda)
// ==/UserScript==
// ==/UserLibrary==
'use strict';
const minUploadSpeed = parseFloat(GM_getValue('min_upload_speed')); // in Mbit/s
const rehostTimeout = GM_getValue('rehost_timeout', 60);
const httpParser = /^(https?:\/\/.+)$/i;
const nonWordStripper = /[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\xFF]+/g;
var testRemoteSizes = GM_getValue('test_remote_sizes');
if (testRemoteSizes === undefined) GM_setValue('test_remote_sizes', (testRemoteSizes = false)); // time consuming
var inputDataHandler, textAreaDropHandler, textAreaPasteHandler, imageUrlResolver;
if ('verifiedImageUrls' in sessionStorage) try {
var verifiedImageUrls = JSON.parse(sessionStorage.getItem('verifiedImageUrls'));
} catch(e) { console.warn(e) }
if (!verifiedImageUrls || typeof verifiedImageUrls != 'object') verifiedImageUrls = { };
if ('fileSizeCache' in sessionStorage) try {
var fileSizeCache = new Map(JSON.parse(sessionStorage.getItem('fileSizeCache')));
} catch(e) { console.warn(e) }
if (!fileSizeCache) fileSizeCache = new Map;
if ('fileTypeCache' in sessionStorage) try {
var fileTypeCache = new Map(JSON.parse(sessionStorage.getItem('fileTypeCache'))) ;
} catch(e) { console.warn(e) }
if (!fileTypeCache) fileTypeCache = new Map;
function imageHostUploaderInit(_inputDataHandler, _textAreaDropHandler, _textAreaPasteHandler, _imageUrlResolver) {
inputDataHandler = _inputDataHandler;
textAreaDropHandler = _textAreaDropHandler;
textAreaPasteHandler = _textAreaPasteHandler;
imageUrlResolver = _imageUrlResolver;
}
String.prototype.toASCII = function() {
return this.normalize('NFKD').replace(/[\x00-\x1F\u0300-\u036F]/gu, '');
};
Array.prototype.flatten = function() {
return this.reduce(function(flat, toFlatten) {
return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten);
}, [ ]);
};
Blob.prototype.getContent = function() {
return new Promise((resolve, reject) => {
var reader = new FileReader;
reader.onload = () => {
if (reader.result.length != this.size)
console.warn(`FileReader: binary string read length mismatch (${reader.result.length} ≠ ${this.size})`);
resolve({ name: this.name, type: this.type, size: reader.result.length, data: reader.result });
};
reader.onerror = reader.ontimeout = () => { reject(`FileReader error (${this.name})`) };
reader.readAsBinaryString(this);
});
};
Blob.prototype.getText = function(encoding) {
return new Promise((resolve, reject) => {
var reader = new FileReader;
reader.onload = () => {
if (reader.result.length != this.size)
console.warn(`FileReader: text string read length mismatch (${reader.result.length} ≠ ${this.size})`);
resolve(reader.result);
};
reader.onerror = reader.ontimeout = () => { reject(`FileReader error (${this.name})`) };
reader.readAsText(this, encoding);
});
};
function getUploadTimeout(totalSize) {
return minUploadSpeed > 0 && totalSize > 0 ?
Math.max(Math.ceil(totalSize * 8000 / minUploadSpeed / 2**20), 10000) : undefined;
}
function uuid() {
let dt = new Date().getTime();
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
class PTPimg {
constructor() {
this.alias = 'PTPimg';
this.origin = 'https://ptpimg.me';
this.types = ['png', 'jpeg', 'gif', 'webp', 'bmp'];
this.batchLimit = 20;
this.whitelist = [
'passthepopcorn.me', 'redacted.ch', 'orpheus.network', 'notwhat.cd', 'dicmusic.club', 'broadcasthe.net',
];
if (!(this.apiKey = GM_getValue('ptpimg_api_key')) && window.localStorage.ptpimg_it) try {
if (this.apiKey = JSON.parse(window.localStorage.ptpimg_it).api_key) GM_setValue('ptpimg_api_key', this.apiKey);
} catch(e) { console.debug(e) }
if (this.apiKey === undefined) GM_setValue('ptpimg_api_key', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
if (this.batchLimit && images.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return this.setSession().then(apiKey => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
images.forEach((image, ndx) => {
formData += 'Content-Disposition: form-data; name="file-upload[' + ndx + ']"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="api_key"\r\n\r\n';
formData += apiKey + '\r\n';
formData += '--' + boundary + '--\r\n';
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload.php',
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
},
data: formData,
binary: true,
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) reject(defaultErrorHandler(response));
else if (response.response) {
if (response.response.length < images.length) {
console.warn('PTPimg returning incomplete list of images (', response.response, ')');
return reject(`not all images uploaded (${response.response.length}/${images.length})`);
}
if (response.response.length > images.length)
console.warn('PTPimg returns more links than expected (', response.response, images, ')');
resolve(response.response.map((item, ndx) => {
if (!item.ext && /\.([a-z]+)(?=$|[\#\?])/i.test(images[ndx].name)) item.ext = RegExp.$1;
return this.origin + '/' + item.code + '.' + item.ext;
}));
} else {
const htmlMessage = domParser.parseFromString(response.responseText, 'text/html').body.textContent.trim();
if (/\b(?:Fatal error|Uncaught Exception):/i.test(htmlMessage)) {
console.warn('PTPimg throws fatal error, trying to redirect.\n\n', htmlMessage);
let redirects = GM_getValue('ptpimg_http500_redirects', [/*'imgcdn', */'imgbb', 'pixhost', 'postimage', 'gifyu']);
if (typeof redirects == 'string') redirects = redirects.split(/\W+/).filter(Boolean);
if (!Array.isArray(redirects)) return reject('invalid redirects list format');
redirects = redirects.map(alias => alias.toLowerCase())
.filter(alias => alias in imageHostHandlers && typeof imageHostHandlers[alias].upload == 'function')
.map(alias => imageHostHandlers[alias]);
if (redirects.length <= 0) throw 'Redirects list for PTPimg is empty';
const redirect = (index = 0) => {
if (!(index >= 0 && index < redirects.length)) return Promise.reject('all redirects failed');
if (typeof progressHandler == 'function') progressHandler(null);
return redirects[index].upload(images, progressHandler, true).catch(reason => redirect(index + 1));
};
redirect().then(results => this.rehost(results.map(directLinkGetter), null)).then(resolve, reject);
} else reject('void or malformed response');
}
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}));
}
rehost(urls) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
if (this.batchLimit && urls.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
const redirect = urls => {
if (typeof urls == 'string') urls = [urls];
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
let redirects = GM_getValue('ptpimg_http500_redirects', ['pixhost', 'postimage', 'imgur']);
if (typeof redirects == 'string') redirects = redirects.split(/\W+/).filter(Boolean);
if (!Array.isArray(redirects)) throw 'invalid redirects list format';
redirects = redirects
.filter(alias => alias in imageHostHandlers && typeof imageHostHandlers[alias].rehost == 'function')
.map(alias => imageHostHandlers[alias]);
if (redirects.length <= 0) return Promise.reject('Redirects list for PTPimg is empty (forcing local reupload)');
const redirect = (index = 0) => (index >= 0 && index < redirects.length ?
redirects[index].rehost(urls, null, true).catch(reason => redirect(index + 1)) : this.reupload(urls))
.then(results => results.map(directLinkGetter));
return redirect();
};
return this.setSession().then(apiKey => {
const rehost = (urls, allowFallback = false) => Promise.all(urls.map(url => {
if (!httpParser.test(url)) return Promise.reject('URL not valid (' + url + ')');
const domain = new URL(url).hostname;
// known blocked hosts
if (['img.discogs.com', 'i.discogs.com'].some(hostname => domain == hostname)
|| [/*'.dzcdn.net', */'omdb.org'].some(hostname => domain.endsWith(hostname)))
return redirect([url]).then(singleImageGetter);
else if (!/\.(?:jpe?g|jfif|png|gif|bmp)$/i.test(url))
return verifyImageUrl(url + '#.jpg').then(finalUrl => url + '#.jpg').catch(reason => {
let redirects = [/*'imgcdn', */'imgbb', 'pixhost', 'gifyu']
.filter(alias => alias in imageHostHandlers && typeof imageHostHandlers[alias].rehost == 'function')
.map(alias => imageHostHandlers[alias]);
const redirect = (index = 0) => (index >= 0 && index < redirects.length ?
redirects[index].rehost([url], null, true).catch(reason => redirect(index + 1))
: Promise.reject('redirection failed on all hosts')).then(results => results.map(directLinkGetter));
return redirect();
});
return verifyImageUrl(url);
})).then(imageUrls => {
console.debug('PTPimg.rehost(...) input:', imageUrls);
let formData = new URLSearchParams({
'link-upload': imageUrls.join('\r\n'),
'api_key': apiKey,
});
return globalXHR(this.origin + '/upload.php', {
responseType: 'json',
timeout: imageUrls.length * rehostTimeout * 1000,
}, formData).then(({response}) => {
if (!response) return this.reupload(urls); //Promise.reject('void response');
if (response.length < imageUrls.length) {
console.warn('PTPimg returning incomplete list of images (', response, ')');
return Promise.reject(`not all images rehosted to (${response.length}/${imageUrls.length})`)
}
if (response.length > imageUrls.length)
console.warn('PTPimg returns more links than expected (', response, imageUrls, ')');
return response.map((item, ndx) => {
if (!item.ext && /\.([a-z]+)(?=$|[\#\?])/i.test(imageUrls[ndx])) item.ext = RegExp.$1;
return this.origin + '/' + item.code + '.' + item.ext;
});
}, reason => {
if (!allowFallback || !/^(?:HTTP error 500)\b/.test(reason)) return Promise.reject(reason);
console.warn('PTPimg internal server error [' + reason +
'] for one or more remote images => trying to redirect');
return redirect(urls).then(rehost);
});
});
return rehost(urls, true);
});
}
reupload(urls) {
return Array.isArray(urls) && urls.length > 0 ? Promise.all(urls.map(url => globalXHR(url, {
responseType: 'blob',
headers: { 'Accept': 'image/*' },
}).then(response => {
let image = {
name: response.finalUrl.replace(/^.*\//, ''),
data: response.responseText,
size: response.getHeaderValue('Content-Length') || response.response.size,
type: response.getHeaderValue('Content-Type'),
};
if (!image.type) switch (response.finalUrl.replace(/[\?\#].*$/, '').replace(/^.*\./, '').toLowerCase()) {
case 'jpg': case 'jpeg': case 'jfif': image.type = 'image/jpeg'; break;
case 'png': image.type = 'image/png'; break;
case 'gif': image.type = 'image/gif'; break;
case 'bmp': image.type = 'image/bmp'; break;
default: return Promise.reject('Unsupported extension');
}
return image;
}))).then(PTPimg.prototype.upload.bind(this)) : Promise.reject('invalid or void argument');
}
setSession() {
return this.apiKey ? Promise.resolve(this.apiKey) : globalXHR(this.origin).then(({document}) => {
const apiKey = document.getElementById('api_key') || document.querySelector('input[name="api_key"]');
if (apiKey == null) {
let counter = GM_getValue('eptpimg_reminder_read', 0);
if (counter < 3) {
alert(`PTPimg API key could not be captured. Please login to ${this.origin}/ and redo the action.`);
GM_setValue('ptpimg_reminder_read', ++counter);
}
return Promise.reject('API key not configured');
} else if (!(this.apiKey = apiKey.value)) return Promise.reject('assertion failed: empty PTPimg API key');
GM_setValue('ptpimg_api_key', this.apiKey);
Promise.resolve(this.apiKey)
.then(apiKey => { alert(`Your PTPimg API key [${apiKey}] was successfully configured`) });
return this.apiKey;
});
}
}
class Chevereto {
constructor(hostName, alias = undefined, types = undefined, sizeLimit = undefined, params = undefined) {
if (typeof hostName != 'string' || !hostName) throw 'Chevereto adapter: missing mandatory host name';
this.origin = httpParser.test(hostName) ? hostName.replace(/\/+$/, '') : 'https://' + hostName;
this.alias = alias;
if (Array.isArray(types)) this.types = types;
if (typeof params != 'object') params = { };
this.sizeLimit = sizeLimit || params.sizeLimitAnonymous;
if (!(this.sizeLimit > 0)) this.sizeLimit = undefined;
if (params.sizeLimitAnonymous < this.sizeLimit) this.sizeLimitAnonymous = params.sizeLimitAnonymous;
if (alias) var al = alias.replace(nonWordStripper, '');
if (!params.configPrefix && al) params.configPrefix = al.toLowerCase();
if (!params.configPrefix && /^(?:www\.)?([\w\-]+)(?:\.[\w\-]+)+$/.test(hostName))
params.configPrefix = RegExp.$1.toLowerCase();
if (this.configPrefix = params.configPrefix) {
this.uid = GM_getValue(this.configPrefix + '_uid');
if (this.uid === undefined && alias) GM_setValue(this.configPrefix + '_uid', '');
this.password = GM_getValue(this.configPrefix + '_password');
if (this.password === undefined && alias) GM_setValue(this.configPrefix + '_password', '');
this.apiKey = GM_getValue(this.configPrefix + '_api_key');
if (this.apiKey === undefined && alias && params.apiEndpoint) GM_setValue(this.configPrefix + '_api_key', '');
} else console.warn('Chevereto adapter: config prefix could not be evaluated, authorized operations not available');
this.jsonEndpoint = params.jsonEndpoint;
this.apiEndpoint = params.apiEndpoint;
this.apiFieldName = params.apiFieldName;
this.apiResultKey = params.apiResultKey;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
return this.setSession(false).then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20
|| this.sizeLimitAnonymous >= 0 && !session.username && !session.key
&& image.size > this.sizeLimitAnonymous * 2**20)
return Promise.reject(`image size exceeds site limit (${image.size})`);
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', params = Object.assign({
action: 'upload',
type: 'file',
nsfw: 0,
thumb_width: 200,
//thumb_height: 200,
format: 'json',
}, session);
Object.keys(params).forEach((field, index, arr) => {
formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
formData += params[field] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="' + (session.key && this.apiFieldName || 'source') +
'"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n' + image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: session.key ? (this.apiEndpoint || this.origin + '/api/1') + '/upload'
: this.jsonEndpoint || this.origin + '/json',
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) try {
if (response.response.success)
resolve(Chevereto.resultHandler(response.response[session.key && this.apiResultKey || 'image']));
else reject((response.response.error ? response.response.error.message
: response.response.status_txt) + ' (' + response.response.status_code + ')');
} catch(e) { reject(e) } else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null, temporary = false) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
return this.setSession(false).then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => {
let formData = new URLSearchParams(Object.assign({
action: 'upload',
type: 'url',
nsfw: 0,
thumb_width: 200,
//thumb_height: 200,
format: 'json',
}, session));
if (temporary) formData.set('expiration', 'PT5M');
formData.set(session.key && this.apiFieldName || 'source', imageUrl);
return globalXHR(session.key ? (this.apiEndpoint || this.origin + '/api/1') + '/upload' : this.jsonEndpoint || this.origin + '/json', {
responseType: 'json',
headers: { 'Referer': this.origin },
timeout: urls.length * rehostTimeout * 1000,
}, formData).then(({response}) => {
if (!response.success)
return Promise.reject(`${this.alias || this.origin}: ${response.error.message} (${response.status_code})`);
if (typeof progressHandler == 'function') progressHandler(true);
return Chevereto.resultHandler(response[session.key && this.apiResultKey || 'image']);
});
}))));
}
static resultHandler(result) {
try {
return {
original: result.image && result.image.url || result.url,
thumb: result.thumb.url,
share: result.url_viewer,
};
} catch(e) { return result.url }
}
galleryResolver(url) {
var albumId = /^\/(?:album|al)\/(\w+)\b/.test(url.pathname) && RegExp.$1;
if (!albumId) return Promise.reject('Invlaid gallery URL');
return this.setSession(true, false).then(session => {
let formData = new URLSearchParams(Object.assign({
action: 'get-album-contents',
albumid: albumId,
}, session));
return globalXHR(this.origin + '/json', {
responseType: 'json',
headers: { 'Referer': url },
}, formData).then(({response}) => {
return response.status_txt == 'OK' && Array.isArray(response.contents) ?
response.contents.map(image => image.url)
: Promise.reject(`${this.alias || this.origin}: ${response.error.message} (${response.status_code})`);
});
}).catch(reason => {
console.warn(this.alias || this.origin, 'gallery couldn\'t be resolved via API:', reason, '(falling back to HTML parser)');
return new Promise((resolve, reject) => {
var urls = [ ];
getPage(url);
function getPage(url) {
GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'document', headers: { Referer: url },
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
Array.prototype.push.apply(urls, Array.from(response.document.querySelectorAll('div.list-item-image > a.image-container')).map(a => a.href));
const next = response.document.querySelector('a[data-pagination="next"][href]');
if (next == null || !next.href) resolve(urls); else getPage(next.href);
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}
}).then(urls => Promise.all(urls.map(url => imageUrlResolver(url))));
});
}
setSession(requireToken = true, requireLogin = false) {
let session = { timestamp: Date.now() };
if (this.uid) session.login = this.uid;
if (this.password) session.password = this.password;
if (this.apiKey) {
session.key = this.apiKey;
if (!requireToken && !requireLogin) return Promise.resolve(session);
}
return globalXHR(this.origin).then(response => {
let authToken = /\b(?:auth_token)\s*=\s*"(\S+?)"/m.exec(response.responseText);
if (authToken != null) authToken = authToken[1]; else {
authToken = response.document.querySelector('input[name="auth_token"][value]');
if (authToken != null) authToken = authToken.value;
}
if (authToken) session.auth_token = authToken; else {
console.warn('Chevereto auth_token detection failure:', this.alias || this.origin, '\n\n', response.responseText);
return Promise.reject('auth_token detection failure');
}
if (getUser(response)) return session;
if (!this.configPrefix || !this.uid || !this.password)
return !requireLogin ? session : Promise.reject('not logged in');
let payLoad = new URLSearchParams({
'login-subject': this.uid,
'password': this.password,
'auth_token': session.auth_token,
});
return globalXHR(this.origin + '/login', {
headers: { 'Referer': this.origin + '/login' },
responseType: 'text',
}, payLoad).then(response => {
if (!getUser(response)) return Promise.reject('unknown reason');
console.debug(this.alias || this.origin, 'login session:', session);
return session;
}).catch(reason => {
console.warn('Chevereto login failed:', reason);
return requireLogin ? Promise.reject('login failed (' + reason + ')') : session;
});
function getUser(response) {
if (/\b(?:logged_user)\s*=\s*(\{.*\});/.test(response.responseText)) try {
let logged_user = JSON.parse(RegExp.$1);
session.username = logged_user.username;
session.userid = logged_user.id;
return Boolean(logged_user.username || logged_user.id);
} catch(e) { console.warn(e) }
return false;
}
});
}
}
class PixHost {
constructor() {
this.alias = 'PixHost';
this.origin = 'https://pixhost.to';
this.types = ['png', 'jpeg', 'gif'];
this.sizeLimit = 10;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="name"\r\n\r\n';
formData += image.name.toASCII() + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="content_type"\r\n\r\n';
formData += '0\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="ajax"\r\n\r\n';
formData += 'yes\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: 'https://pixhost.to/new-upload/',
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve(PixHost.resultHandler(response.response));
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
})));
}
rehost(urls) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
if (this.batchLimit && urls.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return verifyImageUrls(urls).then(imageUrls => {
//console.debug('rehost2PixHost(...) input:', imageUrls.join('\n'));
let formData = new URLSearchParams({
imgs: imageUrls.join('\r\n'),
content_type: 0,
tos: 'on',
});
return globalXHR(this.origin + '/remote/', {
responseType: 'text',
timeout: imageUrls.length * rehostTimeout * 1000,
}, formData).then(({responseText}) => {
if (!/\b(?:upload_results)\s*=\s*(\{.*\});$/m.test(responseText))
return Promise.reject('page parsing error');
let images = JSON.parse(RegExp.$1).images;
if (images.length < imageUrls.length)
return Promise.reject(`not all images rehosted (${images.length}/${imageUrls.length})`);
if (images.length > imageUrls.length)
console.warn('PixHost: server returns more results than expected:', images.length, imageUrls.length);
return Promise.all(images.map(PixHost.resultHandler));
});
});
}
static resultHandler(result) {
try {
return imageUrlResolver(result.show_url).then(imgUrl => ({
original: imgUrl,
thumb: result.th_url,
share: result.show_url,
}));
} catch(e) { return Promise.reject(e) }
}
}
class Catbox {
constructor() {
this.alias = 'Catbox';
this.origin = 'https://catbox.moe';
this.sizeLimit = 200;
if ((this.userHash = GM_getValue('catbox_userhash')) === undefined) GM_setValue('catbox_userhash', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().catch(reason => undefined).then(userHash => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="reqtype"\r\n\r\n';
formData += 'fileupload\r\n';
formData += '--' + boundary + '\r\n';
if (userHash) {
formData += 'Content-Disposition: form-data; name="userhash"\r\n\r\n';
formData += userHash + '\r\n';
formData += '--' + boundary + '\r\n';
}
formData += 'Content-Disposition: form-data; name="fileToUpload"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/user/api.php',
responseType: 'text',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve(response.responseText);
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
return this.setSession().catch(reason => undefined).then(userHash => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => {
let formData = new URLSearchParams({
reqtype: 'urlupload',
url: imageUrl,
});
if (userHash) formData.set('userhash', userHash);
return globalXHR(this.origin + '/user/api.php', {
responseType: 'text',
timeout: urls.length * rehostTimeout * 1000,
}, formData).then(({responseText}) => {
if (typeof progressHandler == 'function') progressHandler(true);
return responseText;
});
}))));
}
setSession() {
return this.userHash ? Promise.resolve(this.userHash) : globalXHR(this.origin).then(response => {
var userHash = response.document.querySelector('input[name="userhash"][value]');
if (userHash == null) return Promise.reject('userhash not configured; please log-in to Catbox.moe to autodetect it');
if (!(this.userHash = userHash.value)) return Promise.reject('assertion failed: empty userhash value');
GM_setValue('catbox_userhash', this.userHash);
return this.userHash;
});
}
}
class ImgBox {
constructor() {
this.alias = 'ImgBox';
this.origin = 'https://imgbox.com';
this.types = ['jpeg', 'gif', 'png'];
this.sizeLimit = 10;
if ((this.uid = GM_getValue('imgbox_uid')) === undefined) GM_setValue('imgbox_uid', '');
if ((this.password = GM_getValue('imgbox_password')) === undefined) GM_setValue('imgbox_password', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
Object.keys(session.params).forEach((field, index, arr) => {
formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
formData += session.params[field] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="files[]"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload/process',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'X-CSRF-Token': session.csrf_token,
},
data: formData,
responseType: 'json',
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve({
original: response.response.files[0].original_url,
thumb: response.response.files[0].thumbnail_url,
share: response.response.files[0].url,
}); else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
setSession() {
return globalXHR(this.origin + '/').then(response => {
let csrfToken = response.document.querySelector('meta[name="csrf-token"]');
if (csrfToken == null) return Promise.reject('ImgBox.com session token not found');
console.debug('ImgBox.com session token:', csrfToken.content);
if (response.document.querySelector('div.btn-group > ul.dropdown-menu') != null) return csrfToken.content;
if (!this.uid || !this.password) return csrfToken.content;
let formData = new URLSearchParams({
"utf8": "✓",
"authenticity_token": csrfToken.content,
"user[login]": this.uid,
"user[password]": this.password,
});
GM_xmlhttpRequest({ method: 'POST', url: 'https://imgbox.com/login', headers: {
'Referer': 'https://imgbox.com/login',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
}, data: formData.toString() });
return new Promise((resolve, reject) => {
setTimeout(() => { globalXHR('http://imgbox.com/').then(response => {
if (response.document.querySelector('div.btn-group > ul.dropdown-menu') == null)
console.warn('ImgBox.com login failed, continuing as anonymous', response);
if ((csrfToken = response.document.querySelector('meta[name="csrf-token"]')) != null) {
console.debug('ImgBox.com session token after login:', csrfToken.content);
resolve(csrfToken.content);
} else reject('ImgBox.com session token not found');
}) }, 1000);
});
}).then(csrfToken => globalXHR(this.origin + '/ajax/token/generate', {
method: 'POST',
responseType: 'json',
headers: { 'X-CSRF-Token': csrfToken },
}).then(({response}) => ({
csrf_token: csrfToken,
params: {
token_id: response.token_id,
token_secret: response.token_secret,
content_type: 1,
thumbnail_size: '150r',
gallery_id: null,
gallery_secret: null,
comments_enabled: 0,
},
})));
}
}
class Imgur {
constructor() {
this.alias = 'Imgur';
this.origin = 'https://imgur.com';
this.ulEndpoint = 'https://api.imgur.com/3/image';
this.types = ['jpeg', 'png', 'gif', 'apng', 'tiff', 'bmp', 'icf', 'webp'];
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(clientId => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="type"\r\n\r\n';
formData += 'file\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="name"\r\n\r\n';
formData += image.name + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="image"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.ulEndpoint + '?client_id=' + encodeURIComponent(clientId),
responseType: 'json',
headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary },
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) {
if (response.response.success) resolve(this.resultHandler(response.response));
else reject(response.response.status);
} else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
return this.setSession().then(clientId => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => {
let formData = new URLSearchParams({ image: imageUrl, type: 'URL' });
return globalXHR(this.ulEndpoint + '?client_id=' + encodeURIComponent(clientId), {
responseType: 'json',
timeout: urls.length * rehostTimeout * 1000,
}, formData).then(({response}) => {
if (!response.success) return Promise.reject(response.status);
if (typeof progressHandler == 'function') progressHandler(true);
return this.resultHandler(response);
});
}))));
}
setSession() {
if ('imgurClientId' in sessionStorage) return Promise.resolve(sessionStorage.getItem('imgurClientId'));
return globalXHR(this.origin).then(({document}) => {
let jsMain = document.querySelector('body > script:last-of-type');
return jsMain != null && jsMain.src.includes('/desktop-assets/js/main.') ? globalXHR(jsMain.src, {
responseType: 'text',
}).then(({responseText}) => {
let clientId = /\b[a-z]="([a-z\d]{15})"/.exec(responseText);
if (clientId == null) return Promise.reject('client id extraction failed');
sessionStorage.setItem('imgurClientId', clientId[1]);
return clientId[1];
}) : Promise.reject('/js/main not located');
});
}
resultHandler(result) {
return {
original: result.data.link,
thumb: result.data.link.replace(/(?=\.\w+$)/, 't'),
share: this.origin + '/' + result.data.id,
};
}
}
class PostImage {
constructor() {
this.alias = 'PostImage';
this.origin = 'https://postimages.org';
this.sizeLimit = 24;
if ((this.uid = GM_getValue('postimg_uid')) === undefined) GM_setValue('postimg_uid', '');
if ((this.password = GM_getValue('postimg_password')) === undefined) GM_setValue('postimg_password', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', payLoad = Object.assign({
numfiles: images.length,
optsize: 0,
expire: 0,
}, session);
Object.keys(payLoad).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += payLoad[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/json/rr',
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.status == 'OK') resolve(response.response.url); else reject(response.response.error);
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))).then(PostImage.resultsHandler));
}
rehost(urls, progressHandler = null, temporary = false) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => {
let formData = new URLSearchParams(Object.assign({
url: imageUrl,
numfiles: urls.length,
optsize: 0,
expire: 0,
}, session));
if (temporary) formData.set('expire', 1);
return globalXHR(this.origin + '/json/rr', {
responseType: 'json',
timeout: urls.length * rehostTimeout * 1000,
}, formData).then(({response}) => {
if (response.status != 'OK') return Promise.reject(response.error);
if (typeof progressHandler == 'function') progressHandler(true);
return response.url;
});
}))).then(PostImage.resultsHandler));
}
static resultsHandler(urls) {
const imgBase = 'https://i.postimg.cc/', shareBase = 'https://postimg.cc/';
return Promise.all(urls.map(url => globalXHR(url).then(response => {
try {
let embed_value = /\b(?:embed_value)=(\{.+?\});/.exec(response.responseText);
if (embed_value == null) throw 'no embed_value';
var results1 = Object.keys(embed_value = JSON.parse(embed_value[1])).map(key => ({
original: imgBase + embed_value[key][2] + '/' + embed_value[key][0] + '.' + embed_value[key][1],
thumb: imgBase + key + '/' + embed_value[key][0] + '.' + embed_value[key][1],
share: shareBase + key,
}));
} catch(e) { results1 = undefined }
try {
var results2 = Array.from(response.document.querySelectorAll('div#thumb-list > div.thumb-container')).map(div => ({
original: imgBase + div.dataset.hotlink + '/' + div.dataset.name + '.' + div.dataset.ext,
thumb: imgBase + div.dataset.image + '/' + div.dataset.name + '.' + div.dataset.ext,
share: shareBase + div.dataset.image,
}));
} catch(e) { results2 = undefined }
if (Array.isArray(results1) && results1.length > 0) return results1;
else if (Array.isArray(results2) && results2.length > 0) return results2;
let result = {
thumb: 'input#code_web_thumb',
share: 'input#code_html',
};
for (let key in result) try { result[key] = response.document.querySelector(result[key]).value.trim() } catch(e) {
console.error(e, result[key]);
result[key] = undefined;
}
return result.share ? globalXHR(result.share).then(function({document}) {
result.original = document.querySelector('a#download');
if (result.original == null) return Promise.reject('PostImage: full image not found');
result.original = result.original.origin + result.original.pathname;
if (result.thumb) result.thumb = /\b(?:src)='(.*?)'/i.exec(result.thumb) || /\b(?:src)="(.*?)"/i.exec(result.thumb);
result.thumb = result.thumb ? result.thumb[1] : undefined;
return [result];
}) : Promise.reject('PostImage: share page link not found');
}))).then(r => r.flatten());
}
setSession() {
let session = {
session_upload: Date.now(),
upload_session: randomString(32),
upload_referer: btoa(this.origin + '/'),
};
return globalXHR(this.origin).then(response => {
if ((session.token = /"token"\s*,\s*"(\w+)"/.exec(response.responseText)) != null) session.token = session.token[1];
if (response.document.querySelector('nav.authorized') != null || !this.uid || !this.password) return response;
return globalXHR(this.origin + '/login').then(response => {
let csrfHash = response.document.querySelector('input[name="csrf_hash"][value]');
if (csrfHash == null) {
console.warn('postImage: csrf hash not found');
return response;
}
let payLoad = new URLSearchParams({
'email': this.uid,
'password': this.password,
'csrf_hash': csrfHash.value,
});
return globalXHR(this.origin + '/login', { headers: { 'Referer': this.origin + '/login' } }, payLoad);
});
}).then(response => {
if ((session.user = response.document.querySelector('nav.authorized a > i.fa-user')) != null)
session.user = session.user.nextSibling.textContent.trim();
return session;
});
}
}
class ImageVenue {
constructor() {
this.alias = 'ImageVenue';
this.origin = 'https://www.imagevenue.com';
this.types = ['jpeg', 'png', 'gif'];
if ((this.uid = GM_getValue('imagevenue_uid')) === undefined) GM_setValue('imagevenue_uid', '');
if ((this.password = GM_getValue('imagevenue_password')) === undefined) GM_setValue('imagevenue_password', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
if (this.batchLimit && images.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return this.setSession().then(session => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
Object.keys(session).forEach((field, index, arr) => {
formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
formData += session[field] + '\r\n';
formData += '--' + boundary + '\r\n';
});
images.forEach((image, index, arr) => {
formData += 'Content-Disposition: form-data; name="files[' + index + ']"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary;
if (index + 1 >= arr.length) formData += '--';
formData += '\r\n';
});
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
},
data: formData,
responseType: 'json',
binary: true,
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
resolve(response.response.success);
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
})).then(resultUrl => globalXHR(resultUrl)).then(response => {
let thumbs = response.document.querySelectorAll('div.row > div > a > img');
return Promise.all(Array.from(thumbs).map(img => imageUrlResolver(img.parentNode.href).then(imgUrl => ({
original: imgUrl,
thumb: img.src,
share: img.parentNode.href,
}))));
});
}
setSession() {
return globalXHR(this.origin + '/').then(response => {
let csrfToken = response.document.querySelector('meta[name="csrf-token"]');
if (csrfToken == null) return Promise.reject('ImageVenue.com session token not found');
console.debug('ImageVenue.com session token:', csrfToken.content);
if (response.document.getElementById('navbarDropdown') != null) return csrfToken.content;
if (!this.uid || !this.password) return csrfToken.content;
let formData = new URLSearchParams({
'_token': csrfToken.content,
'email': this.uid,
'password': this.password,
});
GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/auth/login', headers: {
'Referer': this.origin + '/auth/login',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
}, data: formData.toString() });
return new Promise((resolve, reject) => {
setTimeout(() => {
globalXHR(this.origin + '/').then(response => {
if (response.document.getElementById('navbarDropdown') == null)
console.warn('ImageVenue.com login failed, continuing as anonymous', response);
if ((csrfToken = response.document.querySelector('meta[name="csrf-token"]')) != null) {
console.debug('ImageVenue.com session token after login:', csrfToken.content);
resolve(csrfToken.content);
} else reject('ImageVenue.com session token not found');
});
}, 1000);
});
}).then(csrfToken => globalXHR(this.origin + '/upload/session', {
responseType: 'json',
headers: { 'X-CSRF-TOKEN': csrfToken },
}, new URLSearchParams({
thumbnail_size: 2,
content_type: 'sfw',
comments_enabled: false,
})).then(({response}) => ({
data: response.data,
_token: csrfToken,
})));
}
}
class FastPic {
constructor() {
this.alias = 'FastPic';
this.origin = 'https://fastpic.ru';
this.types = ['jpeg', 'png', 'gif'];
this.sizeLimit = 25;
this.batchLimit = 30;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
if (this.batchLimit && images.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
images.forEach(image => {
formData += 'Content-Disposition: form-data; name="file[]"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="uploading"\r\n\r\n';
formData += '1\r\n';
formData += '--' + boundary + '--\r\n';
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/uploadmulti',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (/^\s*(?:Refresh)\s*:\s*(\d+);url=(\S+)\s*$/im.test(response.responseHeaders)) resolve(RegExp.$2); else {
console.warn('FastPic.ru invalid response header:', response.responseHeaders);
reject('invalid response header');
}
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(resultUrl => globalXHR(resultUrl).then(response => {
let thumbs = Array.from(response.document.querySelectorAll('div.picinfo > div.dCenter > a > img')).map(img => img.src);
return Promise.all(Array.from(response.document.querySelectorAll('ul.codes-list > li:first-of-type > input'))
.map((input, index) => globalXHR(input.value).then(response => ({
original: response.document.querySelector('img.image').src,
thumb: thumbs[index],
share: response.finalUrl,
}))));
console.warn(`FastPic.ru: not all images uploaded (${directLinks.length}/${images.length})`, response.finalUrl);
return Promise.reject(`not all images uploaded (${directLinks.length}/${images.length})`);
}));
}
}
class NWCD {
constructor() {
this.alias = 'NotWhatCd';
this.whitelist = ['notwhat.cd'];
this.types = ['jpeg', 'png', 'gif'];
this.sizeLimit = 5;
this.upload.acceptFiles = true;
}
upload(files) {
if (!Array.isArray(files)) return Promise.reject('invalid argument');
files = files.filter(file => isSupportedType.call(this, file)
&& (!(this.sizeLimit > 0) || !file.size || file.size <= this.sizeLimit * 2**20));
if (files.length <= 0) return Promise.reject('nothing to upload');
return NWCD.loadJS().then(upload => Promise.all(files.map(upload)).then(results =>
results.map(result => result.url || Promise.reject('site image uploader returned void result'))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
return NWCD.loadJS().then(upload => Promise.all(urls.map(url => verifyImageUrl(url).then(upload).then(result => {
if (typeof progressHandler == 'function') progressHandler(true);
return result.url || Promise.reject('site image uploader returned void result');
}))));
}
static loadJS() {
if (document.domain != 'notwhat.cd') return Promise.reject('uploadToImagehost not available');
return typeof uploadToImagehost == 'function' ? Promise.resolve(uploadToImagehost) : new Promise((resolve, reject) => {
let imageUpload = document.createElement('script');
imageUpload.type = 'text/javascript';
imageUpload.src = '/static/functions/image_upload.js';
imageUpload.onload = evt => {
if (typeof uploadToImagehost == 'function') resolve(uploadToImagehost);
else reject('uploadToImagehost() not loaded'); // assertion fail
};
imageUpload.onerror = evt => { reject('Script load error: ' + evt.message) };
document.head.append(imageUpload);
});
}
}
class Abload {
constructor() {
this.alias = 'Abload';
this.origin = 'https://abload.de';
this.types = ['bmp', 'bmp2', 'bmp3', 'gif', 'jpeg', 'png'];
this.sizeLimit = 10;
this.batchLimit = 20;
if ((this.uid = GM_getValue('abload_uid')) === undefined) GM_setValue('abload_uid', '');
if ((this.password = GM_getValue('abload_password')) === undefined) GM_setValue('abload_password', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let id = 'o_' + (index + 1).toString().padStart(2, '0') + randomString(28).toLowerCase(), params = {
name: id, // + image.type.replace('image/', '.'),
chunk: 0,
chunks: 1,
}, formData = '--' + boundary + '\r\n';
Object.keys(params).forEach(field => {
formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
formData += params[field] + '\r\n';
formData += '--' + boundary + '\r\n';
});
Object.keys(session).forEach(field => {
formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
formData += session[field] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: 'https://' + session.server + '.abload.de/calls/newUpload.php',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin + '/',
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve({ id: id, name: image.name });
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))).then(uploadMapping => {
let formData = new URLSearchParams(Object.assign({}, session, {
resize: 'none',
rules: 'on',
gallery: '',
upload_mapping: JSON.stringify(uploadMapping).replace(/"/g, '\\"'),
}));
return globalXHR('https://' + session.server + '.abload.de/flashUploadFinished.php?server=' + session.server, {
headers: { Referer: this.origin + '/' },
}, formData).then(Abload.resolveRedirect);
}));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
if (this.batchLimit && urls.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return this.setSession().then(session => verifyImageUrls(urls).then(imageUrls => {
let formData = { };
imageUrls.forEach((imageUrl, index) => { formData['img' + index] = imageUrl });
formData = new URLSearchParams(Object.assign(formData, session, {
resize: 'none',
rules: 'on',
gallery: '',
upload_mapping: JSON.stringify([]),
}));
return globalXHR('https://' + session.server + '.abload.de/flashUploadFinished.php?server=' + session.server, {
headers: { Referer: this.origin + '/' },
timeout: imageUrls.length * rehostTimeout * 1000,
}, formData).then(Abload.resolveRedirect);
}));
}
static resolveRedirect(response) {
let form = response.document.querySelector('form#weiter');
if (form == null) return Promise.reject(response.responseText);
let formData = new FormData(form);
formData = new URLSearchParams(formData);
return globalXHR(form.action, { headers: { Referer: response.finalUrl } }, formData).then(response =>
Array.from(response.document.body.querySelectorAll('table.image_links > tbody > tr > td > input[type="text"]'))
.filter(input => httpParser.test(input.value)
&& input.parentNode.previousElementSibling.textContent.startsWith('Dire'))
.map(input => ({
original: input.value.trim(),
thumb: input.value.trim().replace('/img/', '/thumb/'),
share: input.value.trim().replace('/img/', '/image.php?img='),
}))
);
}
setSession() {
return globalXHR(this.origin).then(response => {
var session = { userID: randomUser(32) };
if (!/^(?:Server)\s*:\s*(?:Abload)\s+(\w+)\b/im.test(response.responseHeaders))
return Promise.reject('Invalid response header');
session.server = RegExp.$1;
if (/\b(?:user_logged_in)\s*=\s*true\b/.test(response.responseText)) return session;
if (!this.uid || !this.password) return session;
let formData = new URLSearchParams({ name: this.uid, password: this.password });
return globalXHR(this.origin + '/login.php', {
method: 'HEAD',
headers: { 'Referer': this.origin },
}, formData).catch(reason => { console.warn(reason) }).then(response => session);
function randomUser(length) {
const possible = "abcdefABCDEF0123456789";
let text = "";
for (let i = 0; i < length; ++i) text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
});
}
}
class Radikal {
constructor() {
this.alias = 'Radikal';
this.origin = 'https://radikal.ru';
this.sizeLimit = 40;
if ((this.uid = GM_getValue('radikal_uid')) === undefined) GM_setValue('radikal_uid', '');
if ((this.password = GM_getValue('radikal_password')) === undefined) GM_setValue('radikal_password', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', params = {
OriginalFileName: image.name,
MaxSize: 99999,
PrevMaxSize: 500,
IsPublic: false,
NeedResize: false,
Rotate: 0,
RotateMetadataRelative: false,
};
Object.keys(params).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += params[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="File"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/Img/SaveImg2',
responseType: 'json',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin + '/',
'Cookie': 'USER_ID=' + session.Id || '',
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.IsError)
return reject(`${response.response.ErrorSrvMsg} (${response.response.Errors._allerrors_.join(' / ')})`);
resolve({
original: response.response.Url,
thumb: response.response.PublicPrevUrl,
share: response.response.PrevPageUrl,
});
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => {
let formData = new URLSearchParams({
OriginalFileName: imageUrl,
MaxSize: 99999,
PrevMaxSize: 500,
IsPublic: false,
NeedResize: false,
Rotate: 0,
RotateMetadataRelative: false,
Url: imageUrl,
});
return globalXHR(this.origin + '/Img/SaveImg2', {
responseType: 'json',
headers: { 'Referer': this.origin },
cookie: 'USER_ID=' + session.Id || '',
timeout: urls.length * rehostTimeout * 1000,
}, formData).then(({response}) => {
if (response.IsError)
return Promise.reject(`${response.ErrorSrvMsg} (${response.Errors._allerrors_.join(' / ')})`);
if (typeof progressHandler == 'function') progressHandler(true);
return {
original: response.Url,
thumb: response.PublicPrevUrl,
share: response.PrevPageUrl,
};
});
}))));
}
setSession() {
return globalXHR(this.origin, { responseType: 'text' }).then(response => {
let session = getUserInfo(response);
if (!session) return Promise.reject('Invalida page format');
if (!session.IsAnonym) return session;
if (!this.uid || !this.password) return session;
let formData = new URLSearchParams({
Login: this.uid,
Password: this.password,
IsRemember: false,
ReturnUrl: '/',
});
return globalXHR(this.origin + '/Auth/Login', {
responseType: 'json',
headers: { 'Referer': this.origin },
}, formData).then(({response}) => response.IsError ? session
: globalXHR(this.origin, { responseType: 'text' }).then(response => getUserInfo(response) || session),
reason => { console.warn(reason) });
});
function getUserInfo({responseText}) {
if (/\b(?:var\s+serverVm)\s*=\s*(\{.*\});$/m.test(responseText)) try {
return JSON.parse(RegExp.$1).CommonUserData;
} catch(e) { console.warn(e) }
return null;
}
}
}
class SVGshare {
constructor() {
this.alias = 'SVGshare';
this.origin = 'https://svgshare.com';
this.types = ['svg+xml'];
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(submitUrl => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', params = {
name: image.name,
submit: 'Share',
};
Object.keys(params).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += params[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: submitUrl,
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
fetch: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
domParser.parseFromString(response.responseText, 'text/html')
.querySelectorAll('ul#shares > li > input[type="text"]')
.forEach(input => { if (/^(?:https?:\/\/.+\.svg)$/.test(input.value)) resolve(input.value) });
reject('image URL could not be found');
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
setSession() {
return globalXHR(this.origin).then(response => {
let form = response.document.getElementById('filereader');
return form != null && form.action || Promise.reject('Invalid document format');
});
}
}
class GeekPic {
constructor() {
this.alias = 'GeekPic';
this.origin = 'https://geekpic.net';
//this.types = [];
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/ajax.php?PHPSESSID=' + randomString(26).toLowerCase(),
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
responseType: 'json',
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (!response.response.success) return reject(response.response.msg);
resolve(this.origin + response.response.img);
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(imageUrlResolver)));
}
}
class LightShot {
constructor() {
this.alias = 'LightShot';
this.origin = 'https://prntscr.com';
//this.types = [];
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(userInfo => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="image"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload.php',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
responseType: 'json',
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.status == 'success') resolve(response.response.data);
else reject(response.response.status);
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(imageUrlResolver))));
}
setSession() {
let params = {
id: 1,
jsonrpc: '2.0',
method: 'get_userinfo',
params: {},
};
return globalXHR('https://api.prntscr.com/v1/', { responseType: 'json' }, JSON.stringify(params)).then(({response}) => {
if (response.result.success) return response.result;
// TODO: login
return response.result;
});
}
}
class ImageBan {
constructor() {
this.alias = 'ImageBan';
this.origin = 'https://imageban.ru';
this.types = ['jpeg', 'png', 'gif', 'webp'];
this.sizeLimit = 10;
this.batchLimit = 100;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="Filedata"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/up',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
responseType: 'json',
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.files[0].error) return reject(response.response.files[0].error);
resolve({
original: response.response.files[0].link,
thumb: response.response.files[0].thumbs,
share: response.response.files[0].piclink,
});
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
if (this.batchLimit && urls.length > this.batchLimit)
return Promise.reject(`batch limit exceeded (${this.batchLimit})`);
return this.setSession().then(session => verifyImageUrls(urls).then(imageUrls => {
let formData = new URLSearchParams(Object.assign({
u_url: imageUrls.join('\n'),
}, session));
return globalXHR(this.origin + '/urlup', {
headers: { 'Referer': this.origin },
timeout: imageUrls.length * rehostTimeout * 1000,
}, formData).then(response => Array.from(response.document.querySelectorAll('div.container > div[align="left"] ~ div.row')).map(row => ({
original: row.querySelector('div.input-group > input[id^="g"]').value,
thumb: row.querySelector(':scope > a > img').src,
share: row.querySelector('div.input-group > input[id^="a"]').value,
})));
}));
}
setSession() {
return Promise.resolve({});
}
}
class PicaBox {
constructor() {
this.alias = 'PicaBox';
this.origin = 'https://picabox.ru';
//this.types = ['jpeg', 'png', 'gif', 'webp'];
//this.sizeLimit = 10;
//this.batchLimit = 100;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
Object.keys(session).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += session[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="ImagesForm[imageFiles][]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/image/load',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin + '/image/load',
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
fetch: true,
cookie: '_csrf=' + session._csrf,
onload: response => {
if (response.status >= 200 && response.status < 400) resolve(this.extractLinks(response));
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(results => results[0]))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
if (this.batchLimit && urls.length > this.batchLimit)
return Promise.reject(`batch limit exceeded (${this.batchLimit})`);
return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => {
let formData = new URLSearchParams(session);
formData.set('ImagesForm[file_url]', imageUrl);
return globalXHR(this.origin + '/image/load', {
responseType: 'text',
fetch: true,
headers: { 'Referer': this.origin + '/image/load' },
timeout: urls.length * rehostTimeout * 1000,
cookie: '_csrf=' + session._csrf,
}, formData).then(PicaBox.prototype.extractLinks.bind(this)).then(results => {
if (typeof progressHandler == 'function') progressHandler(true);
return results[0];
});
}))));
}
extractLinks({responseText}) {
return Promise.all(Array.from(domParser.parseFromString(responseText, 'text/html')
.querySelectorAll('input[name="url"]')).map(input => imageUrlResolver(input.value).then(imgUrl => ({
original: imgUrl,
thumb: this.origin + '/img_small/' + input.value.replace(/^.*\//, ''),
share: input.value,
}))));
}
setSession() {
return globalXHR(this.origin + '/image/load').then(response => {
let formData = response.document.querySelector('form[name="form_image"]');
if (formData == null) return Promise.reject('Invalid document format');
formData = new FormData(formData);
let session = { }, val, it = formData.entries();
for (let [key, val] of formData) session[key] = val;
['ImagesForm[file_url]', 'ImagesForm[imageFiles][]', 'imagesform-text_color-source']
.forEach(key => { delete session[key] });
return session;
});
}
}
class PimpAndHost {
constructor() {
this.alias = 'PimpAndHost';
this.origin = 'https://pimpandhost.com';
this.types = ['jpeg', 'png', 'gif'];
this.sizeLimit = 5 * 1000 / 2**10;
this.batchLimit = 100;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', params = {
fileId: `${index.toString().padStart(3, '0')}_${session.albumId}`,
albumId: session.albumId,
};
Object.keys(params).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += params[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="files"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/image/upload-file',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin + '/album/' + session.albumId,
'X-CSRF-Token': session['csrf-token'],
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
responseType: 'json',
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
resolve({
original: 'https:' + response.response.files[0].image[0],
thumb: 'https:' + response.response.files[0].image[180],
share: response.response.files[0].pageUrl,
});
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
if (this.batchLimit && urls.length > this.batchLimit)
return Promise.reject(`batch limit exceeded (${this.batchLimit})`);
return this.setSession().then(session => Promise.all(urls.map((url, index) => (() => {
return['png', 'jpg', 'jpeg', 'jfif', 'gif'].some(ext => url.toLowerCase().endsWith('.' + ext)) ?
verifyImageUrl(url) : imageHostHandlers.imgbb.rehost([url]).then(imgUrls => imgUrls[0])
.catch(reason => imageHostHandlers.jerking.rehost([url]).then(imgUrls => imgUrls[0]))
.catch(reason => imageHostHandlers.pixhost.rehost([url]).then(imgUrls => imgUrls[0]))
})().then(imageUrl => {
let formData = new URLSearchParams({
url: imageUrl,
field: `${index.toString().padStart(3, '0')}_${session.albumId}`,
albumId: session.albumId,
});
return globalXHR(this.origin + '/image/upload-by-url', {
responseType: 'json',
headers: {
'Referer': this.origin + '/album/' + session.albumId,
'X-CSRF-Token': session['csrf-token'],
},
timeout: urls.length * rehostTimeout * 1000,
}, formData).then(({response}) => {
if (response.status != 'ok') return Promise.reject(response.message);
if (response.file.error) return Promise.reject(response.file.error.title);
if (typeof progressHandler == 'function') progressHandler(true);
return {
original: 'https:' + response.file.image[0],
thumb: 'https:' + response.file.image[180],
share: response.file.pageUrl,
};
});
}))));
}
setSession() {
return globalXHR(this.origin).then(response => {
let meta = response.document.querySelector('meta[name="csrf-token"][content]');
if (meta == null) return Promise.reject('Invalid document structure');
let session = { 'csrf-token': meta.content };
return globalXHR(this.origin + '/album/create-by-uploading', {
headers: { 'X-CSRF-Token': session['csrf-token'] },
responseType: 'json',
}).then(({response}) => {
session.albumId = response.albumId;
return session;
});
});
}
}
class ScreenCast {
constructor() {
this.alias = 'ScreenCast';
this.origin = 'https://www.screencast.com';
//this.types = [];
//this.sizeLimit = 10;
//this.batchLimit = 100;
}
upload(images, progressHandler = null) {
}
rehost(urls, progressHandler = null) {
}
setSession() {
}
}
class GoogleAPI {
constructor(scope) {
this.origin = 'https://www.googleapis.com';
this.clientId = '241768952066-r0pojdg0l8m4nqr31psf8rb01btt43c4.apps.googleusercontent.com';
this.apiKey = 'lk9MZc7eSYzi6tDQ-H6jeC-2';
this.scope = scope;
}
setSession() {
return this.isTokenValid() ? Promise.resolve(this.token)
: (this.auth ? Promise.resolve(this.auth) : (typeof gapi == 'object' ? Promise.resolve(gapi) : new Promise((resolve, reject) => {
let gApi = document.createElement('script');
gApi.type = 'text/javascript';
gApi.src = 'https://apis.google.com/js/api.js';
gApi.onload = evt => { if (typeof gapi == 'object') resolve(gapi); else reject('Google API loading error') };
gApi.onerror = evt => { reject('Script load error: ' + evt.message ) };
document.head.append(gApi);
})).then(gapi => new Promise((resolve, reject) => gapi.load('client:auth2', {
callback: () => {
gapi.client.init({
clientId: this.clientId,
//apiKey: this.apiKey,
scope: this.scope,
discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'],
}).then(() => { resolve(this.auth = gapi.auth2.getAuthInstance()) }, error => reject(JSON.stringify(error)));
},
onerror: () => { reject('Google API loading error') },
})))).then(auth => auth.isSignedIn.get() ? auth.currentUser.get() : auth.signIn().catch(e => Promise.reject(e.error)))
.then(user => (this.token = gapi.client.getToken()));
}
isTokenValid() {
if (!this.token || typeof this.token != 'object' || !this.token.token_type || !this.token.access_token) return false;
let now = new Date();
return this.token.expires_at >= now.getTime() + now.getTimezoneOffset() * 60000 + 30000;
}
}
class GoogleDrive extends GoogleAPI {
constructor() {
super('https://www.googleapis.com/auth/drive.file');
this.alias = 'GoogleDrive';
//this.types = [];
//this.sizeLimit = 10;
//this.batchLimit = 100;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(token => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', metadata = {
'name' : image.name,
'mimeType' : image.type,
};
formData += 'Content-Disposition: form-data; name="metadata"\r\n';
formData += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
formData += JSON.stringify(metadata) + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload/drive/v3/files?uploadType=multipart&fields=id,webContentLink',
headers: {
'Content-Type': 'multipart/related; boundary=' + boundary,
'Authorization': token.token_type + ' ' + token.access_token,
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
responseType: 'json',
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
gapi.client.drive.permissions.create({
fileId: response.response.id,
resource: { role: 'reader', type: 'anyone' },
}).execute(result => {
if (result.id == 'anyoneWithLink') resolve(response.response.webContentLink.replace(/&.*$/i, ''));
else reject('failed to enable sharing for this file');
}, error => { reject(JSON.stringify(error)) });
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
}
class GooglePhotos extends GoogleAPI {
constructor() {
super('https://www.googleapis.com/auth/photoslibrary.sharing');
this.alias = 'GooglePhotos';
//this.types = [];
//this.sizeLimit = 10;
//this.batchLimit = 100;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(token => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name;
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
// TODO
}))));
}
}
class DropBox {
constructor() {
this.alias = 'DropBox';
this.origin = 'https://www.dropbox.com';
//this.types = [];
//this.sizeLimit = 10;
//this.batchLimit = 100;
}
upload(images, progressHandler = null) {
}
rehost(urls, progressHandler = null) {
}
setSession() {
}
}
class OneDrive {
constructor() {
this.alias = 'OneDrive';
this.origin = 'https://onedrive.live.com';
//this.types = [];
//this.sizeLimit = 10;
//this.batchLimit = 100;
}
upload(images, progressHandler = null) {
}
rehost(urls, progressHandler = null) {
}
setSession() {
}
}
class VgyMe {
constructor() {
this.alias = 'Vgy.me';
this.origin = 'https://vgy.me';
this.types = ['jpeg', 'png', 'gif'];
this.sizeLimit = 20;
if ((this.userKey = GM_getValue('vgyme_user_key')) === undefined) GM_setValue('vgyme_user_key', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(userKey => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
images.forEach((image, index) => {
formData += 'Content-Disposition: form-data; name="file[' + index + ']"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="userkey"\r\n\r\n';
formData += userKey + '\r\n';
formData += '--' + boundary + '--\r\n';
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
responseType: 'json',
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (!response.response.error) {
if (Array.isArray(response.response.upload_list)) return resolve(response.response.upload_list);
if (response.response.image) return resolve([response.response.image]);
reject('Invalid response');
} else reject('Error');
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}));
}
setSession() {
return this.userKey ? Promise.resolve(this.userKey)
: Promise.reject('user key not configured (https://vgy.me/account/details#userkeys)');
}
}
class ImgURL {
constructor() {
this.alias = 'ImgURL';
this.origin = 'https://www.png8.com';
//this.origin = 'https://imgurl.org';
this.types = ['jpeg', 'png', 'gif', 'bmp'];
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload/localhost',
//url: this.origin + '/upload/ftp',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
responseType: 'json',
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.code != 200) return reject('status: ' + response.response.code);
resolve({
original: response.response.url,
thumb: response.response.thumbnail_url,
share: this.origin + '/img/' + response.response.imgid,
});
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
setSession() {
return Promise.resolve({});
}
}
class Slowpoke {
constructor() {
this.alias = 'Slowpoke';
this.origin = 'https://slow.pics';
//this.types = ['jpeg', 'png', 'gif', 'bmp'];
if ((this.uid = GM_getValue('slowpoke_uid')) === undefined) GM_setValue('slowpoke_uid', '');
if ((this.password = GM_getValue('slowpoke_password')) === undefined) GM_setValue('slowpoke_password', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
if (this.batchLimit && images.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return this.setSession().then(csrfToken => new Promise((resolve, reject) => {
const now = Date.now(), boundary = '--------WebKitFormBoundary-' + now.toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', params = {
collectionName: new Date(now).toISOString(),
public: false,
thumbnailSize: 180,
};
Object.keys(params).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += params[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
images.forEach((image, index) => {
formData += 'Content-Disposition: form-data; name="images[' + index + '].name"\r\n\r\n';
formData += image.name + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="images[' + index + '].file"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary;
if (index >= images.length - 1) formData += '--';
formData += '\r\n';
});
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/api/collection',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
'X-XSRF-TOKEN': csrfToken,
},
data: formData,
binary: true,
responseType: 'text',
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
let shareUrl = this.origin + '/c/' + response.responseText;
console.log('Slowpoke upload gallery link:', shareUrl);
imageUrlResolver(shareUrl).then(result => resolve(Array.isArray(result) ? result : [result]));
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}));
}
setSession() {
return globalXHR(this.origin + '/login').then(response => {
if (response.finalUrl.includes(this.origin)) return response;
if (!this.uid || !this.password) return globalXHR(this.origin);
let token = response.document.querySelector('input[name="_csrf"][value]');
if (token == null) return Promise.reject('invlid page structure');
return new Promise((resolve, reject) => {
let formData = new URLSearchParams({
_csrf: token.value,
username: this.uid,
password: this.password,
});
GM_xmlhttpRequest({ method: 'POST', url: response.finalUrl,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData.toString(),
onload: response => {
if (['my', 'exit'].some(p => response.finalUrl.endsWith('/' + p))) {
console.log('Slowpoke successfull login:', response.finalUrl);
resolve(globalXHR(this.origin + '/login'));
} else if (response.finalUrl.endsWith('/login?credentials')) reject('invalid userid or password'); else {
console.warn('Slowpoke unhandled redirect:', response);
reject('unexpected redirect: ' + response.finalUrl);
}
},
onerror: response => { reject(defaultErrorHandler(response)) },
});
}).catch(reason => {
console.warn('Slowpoke login failed:', reason);
return globalXHR(this.origin);
});
}).then(response => {
let token = response.document.querySelector('input[name="_csrf"][value]');
return token != null ? token.value : Promise.reject('invlid page structure (' + response.finalUrl + ')');
});
}
}
class FunkyIMG {
constructor() {
this.alias = 'FunkyIMG';
this.origin = 'https://funkyimg.com';
this.sizeLimit = 4;
this.types = ['jpeg', 'png', 'gif', 'bmp', 'tiff'];
if ((this.uid = GM_getValue('funkyimg_uid')) === undefined) GM_setValue('funkyimg_uid', '');
if ((this.password = GM_getValue('funkyimg_password')) === undefined) GM_setValue('funkyimg_password', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', params = {
_images: image.name,
wmText: '',
wmPos: 'TOPRIGHT',
wmLayout: 2,
wmFontSize: 14,
wmTransparency: 50,
addInfoType: 'res',
labelText: '',
};
Object.keys(params).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += params[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="images"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload/?' + session,
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
},
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
responseType: 'json',
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.success) resolve(this.resultHandler(response.response.jid));
else reject('failure');
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
if (this.batchLimit && urls.length > this.batchLimit)
return Promise.reject(`batch limit exceeded (${this.batchLimit})`);
return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => {
let formData = new URLSearchParams({
url: imageUrl,
wmText: '',
wmPos: 'TOPRIGHT',
wmLayout: 2,
wmFontSize: 14,
wmTransparency: 50,
addInfoType: 'res',
labelText: '',
});
return globalXHR(this.origin + '/upload/?' + session, {
responseType: 'json',
headers: { 'Referer': this.origin },
timeout: urls.length * rehostTimeout * 1000,
}, formData).then(({response}) => response.success ? this.resultHandler(response.jid) : Promise.reject('failure'));
}))));
}
resultHandler(jid) {
return new Promise((resolve, reject) => {
//let queries = 0;
check.call(this);
function check() {
globalXHR(this.origin + '/upload/check/' + jid + '?_=' + Date.now(), {
headers: { 'Referer': this.origin },
responseType: 'json',
}).then(({response}) => {
//++queries;
if (response.success) try {
//console.debug('FunkyIMG queries to success:', queries, jid);
let dom = domParser.parseFromString(response.bit, 'text/html'), result = {
original: 'ul > li:nth-of-type(2) > input',
thumb: 'ul > li:nth-of-type(2) > input',
share: 'ul > li:nth-of-type(1) > input',
};
Object.keys(result).forEach(key => { result[key] = dom.querySelector(result[key]).value.trim() });
result.thumb = result.thumb.replace('/i/', '/p/');
resolve(result);
} catch(e) { reject('unexpected response: ' + e) } else setTimeout(check.bind(this), 200);
}, reject);
}
});
}
setSession() {
return globalXHR(this.origin).then(response => {
let loggedUser = response.document.querySelector('div.inner > span.welcome > b');
if (loggedUser == null || loggedUser.parentNode.style.display != 'none' || !this.uid || !this.password)
return Promise.resolve(response);
let params = new URLSearchParams({
s: '',
act: 'Login',
CODE: '01',
}), formData = new URLSearchParams({
referer: this.origin + '/',
UserName: this.uid,
PassWord: this.password,
});
return globalXHR('https://forum.funkysouls.org/index.php?' + params, { headers: { 'Referer': this.origin } }, formData);
}).then(response => {
let loggedUser = response.document.querySelector('div.inner > span.welcome > b');
loggedUser = loggedUser != null && loggedUser.parentNode.style.display != 'none' ?
loggedUser.textContent.trim() : undefined;
return 'fileapi' + Date.now();
});
}
}
class Gett {
constructor() {
this.alias = 'Ge.tt';
this.origin = 'http://api.ge.tt';
//this.sizeLimit = 4;
//this.types = ['jpeg', 'png', 'gif', 'bmp', 'tiff'];
if ((this.uid = GM_getValue('gett_uid')) === undefined) GM_setValue('gett_uid', '');
if ((this.password = GM_getValue('gett_password')) === undefined) GM_setValue('gett_password', '');
//if ((this.apiKey = GM_getValue('gett_apikey')) === undefined) GM_setValue('gett_apikey', '');
this.apiKey = 't3bhhhlzg7lb78c0wimxjx4unmihwv6l93n150esdz15anka9k9';
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if ((images = images.filter(isSupportedType.bind(this))).length <= 0) return Promise.reject('nothing to upload');
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => {
const auth = 'accesstoken=' + encodeURIComponent(session.accesstoken);
return globalXHR(this.origin + '/1/shares/create?' + auth, {
method: 'POST',
responseType: 'json',
}).then(({response}) => Promise.all(images.map((image, index) => globalXHR(this.origin + '/1/files/' + response.sharename + '/create?' + auth, {
responseType: 'json',
}, {
filename: image.name,
size: image.size,
type: image.type,
}).then(({response}) => response).then(file => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="' + file.fileid + '"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({ method: 'POST', url: file.upload.posturl,
headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary },
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve(file);
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
})).then(file => {
let result = {
original: this.origin + '/1/files/' + file.sharename + '/' + file.fileid + '/blob',
thumb: this.origin + '/1/files/' + file.sharename + '/' + file.fileid + '/blob/thumb',
share: file.getturl,
};
const url = this.origin + '/1/files/' + file.sharename + '/' + file.fileid +
'/blob?download&referrer=' + encodeURIComponent(session.user.userid);
return globalXHR(url, {
method: 'HEAD',
headers: { 'Referer': this.origin + '/1/files/' + file.sharename + '/' + file.fileid },
}).then(response => Object.assign(result, { original: response.finalUrl }), reason => result);
}))));
});
}
setSession() {
if (Gett.isSessionValid(this.session)) return Promise.resolve(this.session);
const params = { responseType: 'json' };
return (this.uid && this.password && this.apiKey ? globalXHR(this.origin + '/1/users/login', params, {
apikey: this.apiKey,
email: this.uid,
password: this.password,
}) : Promise.reject('user not configured')).catch(reason => globalXHR(this.origin + '/anon/signup', params, {
apikey: this.apiKey,
})).then(({response}) => {
this.session = response;
this.session.expires_at = Date.now() + this.session.expires;
return Gett.isSessionValid(this.session) ? this.session : Promise.reject('login invalid');
});
}
static isSessionValid(session) {
return session && typeof session == 'object' && session.accesstoken && session.expires_at > Date.now() + 30000;
}
}
class SavePhoto {
constructor() {
this.alias = 'SavePhoto';
this.origin = 'http://savephoto.ru';
//this.sizeLimit = 4;
//this.types = ['jpeg', 'png', 'gif', 'bmp', 'tiff'];
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if ((images = images.filter(isSupportedType.bind(this))).length <= 0) return Promise.reject('nothing to upload');
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
const guid = uuid();
return Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="files[]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/Upload/UploadHandler.ashx?id=' + guid,
headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary },
data: formData,
responseType: 'json',
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400)
if (!response.response.error) resolve(response.response[0]); else reject(response.response.error);
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(result => imageUrlResolver(result.pathFile).then(imageUrl => ({
original: imageUrl,
thumb: imageUrl.replace(result.name, result.name + '_t'),
share: result.pathFile,
})))));
}
}
class UuploadIr {
constructor() {
this.alias = 'Uupload.ir';
this.origin = 'https://uupload.ir';
this.types = ['jpeg', 'gif', 'png', 'bmp', 'psd', 'tiff', 'ico'];
//this.sizeLimit = 0;
//this.batchLimit = 4;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
images.forEach((image, index) => {
formData += 'Content-Disposition: form-data; name="__userfile[]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="ittl"\r\n\r\n';
formData += '0\r\n';
//formData += '86400\r\n'; // for debugging
formData += '--' + boundary + '--\r\n';
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/process.php',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
binary: true,
responseType: 'document',
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
resolve(Array.from(domParser.parseFromString(response.responseText, 'text/html')
.querySelectorAll('table#linkst > tbody > tr:nth-of-type(2) > td.wpk > input[type="text"]'))
.map(input => input.value).filter(RegExp.prototype.test.bind(httpParser)));
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(imageUrls => {
if (imageUrls.length < images.length) {
console.warn('Upload.ir: server returned less links than expected', images, imgUrls);
return Promise.reject('server returned less links than expected');
}
if (imageUrls.length > images.length)
console.warn('Upload.ir: server returned more links than expected', images, imgUrls);
return imageUrls.map(UuploadIr.resultHandler);
});
}
static resultHandler(imageUrl) {
return {
original: imageUrl,
thumb: imageUrl.replace(/(\.\w*)?$/, '_thumb$1'),
};
}
}
class CasImages {
constructor() {
this.alias = 'CasImages';
this.origin = 'https://www.casimages.com';
this.types = ['jpeg', 'gif', 'png'];
this.sizeLimit = 10;
//this.batchLimit = 4;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="Filedata"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload_ano_multi.php',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
responseType: 'text',
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve(response.responseText);
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(CasImages.prototype.resultHandler.bind(this))));
}
resultHandler(result) {
let shareUrl = this.origin + '/i/' + result + '.html';
return imageUrlResolver(shareUrl).then(imageUrl => ({
original: imageUrl,
thumb: imageUrl.replace(result, 'mini_' + result),
share: shareUrl,
}));
}
}
class NoelShack {
constructor() {
this.alias = 'NoelShack';
this.origin = 'https://www.noelshack.com';
this.types = ['jpeg', 'gif', 'png', 'bmp', 'tiff', 'svg+xml'];
this.sizeLimit = 4;
//this.batchLimit = 4;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="fichier[]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/envoi.json',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
responseType: 'json',
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve(NoelShack.resultHandler(response.response));
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
})));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
return Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => globalXHR(this.origin + '/telecharger.json?url=' + imageUrl, {
responseType: 'json',
headers: { 'Referer': this.origin },
timeout: urls.length * rehostTimeout * 1000,
}).then(({response}) => NoelShack.resultHandler(response)))));
}
static resultHandler(result) {
return result.erreurs == 'null' ? {
original: result.url,
thumb: result.mini,
} : Promise.reject(result.erreurs);
}
}
class GetaPic {
constructor() {
this.alias = 'GetaPic';
this.origin = 'https://getapic.me';
//this.types = ['jpeg', 'png', 'gif', 'bmp'];
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
if (this.batchLimit && images.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return this.setSession().then(session => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n', params = {
getpreviewsize: 200,
upload_quality: 100,
getpreviewalt: 0,
getreduceimage: 0,
needreduce: 0,
upload_angle: 0,
upload_resizeside: 0,
gettypeofdownload: 'N',
};
Object.keys(params).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += params[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
Object.keys(session).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += session[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
images.forEach((image, index) => {
formData += 'Content-Disposition: form-data; name="file[]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary;
if (index >= images.length - 1) formData += '--';
formData += '\r\n';
});
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
'X-Requested-With': 'XMLHttpRequest',
},
cookie: 'getapime=' + session.session,
data: formData,
binary: true,
responseType: 'json',
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
try {
if (!response.response.result.success) return reject(response.response.result.errors);
resolve(response.response.result.data.url);
} catch(e) {
console.error('GetaPic invalid response structure:', response.response, e);
reject(e);
}
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(resultUrl => globalXHR(resultUrl, { responseType: 'json' }).then(({response}) => {
try {
if (!response.result.success) return Promise.reject(response.result.errors);
if (response.result.data.images.length < images.length) {
console.warn('GetaPic returning incomplete list of images (', response.result.data.images, images, ')');
return Promise.reject(`not all images uploaded (${response.result.data.images.length}/${images.length})`)
}
if (response.result.data.images.length > images.length)
console.warn('GetaPic returns more links than expected (', response.result.data.images, images, ')');
const forceSSL = [/^(?:http)(?=:)/i, 'https'];
return response.result.data.images.map(image => ({
original: image.url/*.replace(...forceSSL)*/,
thumb: image.thumb ? image.thumb.url/*.replace(...forceSSL)*/ : undefined,
share: 'http://getapic.me/v/'.concat(image.marker)/*.replace(...forceSSL)*/,
optimal: image.optimal ? image.optimal.url/*.replace(...forceSSL)*/ : undefined,
}));
} catch(e) {
console.error('GetaPic invalid response structure:', response, e);
return Promise.reject(e);
}
})));
}
setSession() {
return globalXHR(this.origin, { responseType: 'json' }).then(({response}) => response.result.data);
}
}
class SMMS {
constructor() {
this.alias = 'SM.MS';
this.origin = 'https://sm.ms';
this.sizeLimit = 5128 / 2**10;
this.types = ['jpeg', 'png', 'gif', 'bmp'];
this.batchLimit = 10;
if ((this.apiKey = GM_getValue('smms_api_key')) === undefined) GM_setValue('smms_api_key', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="file_id"\r\n\r\n';
formData += index + '\r\n--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="smfile"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="format"\r\n\r\n';
formData += 'json\r\n--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/api/v2/upload',
headers: Object.assign({
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'X-Requested-With': "XMLHttpRequest",
}, session),
data: formData,
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
responseType: 'json',
onload: response => {
if (response.status >= 200 && response.status < 400) {
if (response.response.success) resolve({
original: response.response.data.url,
share: response.response.data.page,
}); else if (response.response.code == 'image_repeated') resolve(response.response.images);
else reject(response.response.message);
} else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
setSession() {
let result = { };
if (this.apiKey) result.Authorization = 'Basic ' + this.apiKey;
return Promise.resolve(result);
}
}
class CubeUpload {
constructor() {
this.alias = 'CubeUpload';
this.origin = 'https://cubeupload.com';
this.types = ['jpeg', 'png', 'gif', 'bmp'];
this.userId = GM_getValue('cubeupload_userid');
this.userHash = GM_getValue('cubeupload_userhash');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
Object.keys(session).forEach(key => {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += session[key] + '\r\n';
formData += '--' + boundary + '\r\n';
});
formData += 'Content-Disposition: form-data; name="name"\r\n\r\n';
formData += image.name + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="fileinput[0]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload_json.php',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
responseType: 'json',
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.error) reject(response.response.status); else resolve({
original: response.response.img_direct_link,
thumb: response.response.img_thumb_link,
share: response.response.img_sharing_link,
});
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
setSession() {
return this.userId && this.userHash ? Promise.resolve({
userID: this.userId,
userHash: this.userHash,
}) : globalXHR(this.origin, { responseType: 'text' }).then(({responseText}) => {
this.userId = /\b(?:var\s+user_id)\s*=\s*(\d+)\b/.exec(responseText);
if (this.userId != null) GM_setValue('cubeupload_userid', this.userId = parseInt(this.userId[1]));
else delete this.userId;
this.userHash = /\b(?:var\s+user_hash)\s*=\s*"(\w+)"/.exec(responseText);
if (this.userHash != null) GM_setValue('cubeupload_userhash', this.userHash = this.userHash[1]);
else delete this.userHash;
return this.userId && this.userHash ? Promise.resolve({
userID: this.userId,
userHash: this.userHash,
}) : Promise.reject('account credentials aren\'t set, please login to your account and repeat the action');
});
}
}
class GooPics {
constructor() {
this.alias = 'GooPics';
this.origin = 'https://goopics.net';
this.types = ['jpeg', 'png', 'gif'];
this.sizeLimit = 15;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="fileinput[0]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
responseType: 'document',
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
let dom = domParser.parseFromString(response.responseText, 'text/html'), result = {
original: dom.querySelector('article input[id^="direct_"]'),
thumb: dom.querySelector('article > a > img'),
share: dom.querySelector('article input[id^="basic_"]'),
};
if (result.original != null) result.original = result.original.value;
else reject('unexpected result structure');
result.thumb = result.thumb != null ? result.thumb.src : undefined;
result.share = result.share != null ? result.share.value : undefined;
resolve(result);
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
})));
}
}
class BilderUpload {
constructor() {
this.alias = 'BilderUpload';
this.origin = 'https://www.bilder-upload.eu';
this.types = ['jpeg', 'png', 'gif'];
this.sizeLimit = 10;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="datei"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="upload"\r\n\r\n';
formData += '1\r\n--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({ method: 'POST', url: this.origin,
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
responseType: 'document',
binary: true,
timeout: getUploadTimeout(images.reduce((acc, image) => acc + image.size, images.length * 1024)),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
let result = {
thumb: response.response.querySelector('div.box img'),
share: response.response.querySelector('textarea[name="URLCode"]'),
};
if (result.thumb != null) {
result.thumb = new URL(result.thumb.src);
result.thumb = this.origin + result.thumb.pathname;
result.original = result.thumb.replace('/thumb/', '/upload/');
}
result.share = result.share != null ? result.share.value : undefined;
if (result.original) resolve(result); else reject('unexpected result structure');
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
})));
}
}
class Ra {
constructor() {
this.alias = 'Ra';
this.origin = 'https://thesungod.xyz';
this.types = ['gif', 'vnd.microsoft.icon', 'jpeg', 'png', 'svg+xml', 'tiff', 'webp', 'bmp'];
this.sizeLimit = 100;
if (this.apiKey = GM_getValue('thesungod_api_key')) {
GM_setValue('ra_api_key', this.apiKey);
GM_deleteValue('thesungod_api_key');
} else if ((this.apiKey = GM_getValue('ra_api_key')) === undefined) GM_setValue('ra_api_key', '');
this.thumbSize = 200;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(apiKey => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="api_key"\r\n\r\n';
formData += apiKey + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="image"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/api/image/upload',
responseType: 'text', //'json' ?
headers: {
'Accept': 'text/plain',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
binary: true,
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve(this.resultHandler(response.responseText));
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
return urls.length > 0 ? this.setSession().then(apiKey => Promise.all(urls.map((url, index) =>
verifyImageUrl(url).then(imageUrl => globalXHR(this.origin + '/api/image/rehost', {
responseType: 'text',
timeout: urls.length * rehostTimeout * 1000,
}, new URLSearchParams({ 'link': imageUrl, 'api_key': apiKey })).then(({responseText}) => {
if (typeof progressHandler == 'function' && httpParser.test(responseText)) progressHandler(true);
return this.resultHandler(responseText);
}))))) : Promise.reject('nothing to rehost');
}
resultHandler(response) {
const result = imageUrl => ['jpeg', 'png'].some(ext => imageUrl.endsWith('.' + ext)) ? {
original: imageUrl,
thumb: imageUrl + '?width=' + this.thumbSize + '&height=' + this.thumbSize,
} : imageUrl;
if (httpParser.test(response)) return result(response);
try {
let links = JSON.parse(response).links;
console.assert(Array.isArray(links) && links.length == 1, 'Array.isArray(links) && links.length == 1', response);
if (links.length > 0 && httpParser.test(links[0])) return result(links[0]);
} catch(e) { }
console.warn('Ra unexpected response:', response);
return Promise.reject('void or invalid result');
}
setSession() {
return this.apiKey ? Promise.resolve(this.apiKey) : globalXHR(this.origin + '/api/user/genkey', {
responseType: 'text',
}).then(({responseText}) => {
if (this.apiKey = responseText) {
GM_setValue('ra_api_key', this.apiKey);
Promise.resolve(this.apiKey)
.then(apiKey => { alert(`Your Ra API key [${apiKey}] was successfully generated.
For later use with different applications or browsers, it\'s saved in script\'s storage`) });
return this.apiKey;
}
let counter = GM_getValue('ra_reminder_read', 0);
if (counter < 3) {
alert(`Ra API key could not be generated. Please login to ${this.origin}/ and redo the action.`);
GM_setValue('ra_reminder_read', ++counter);
}
return Promise.reject('Ra API key not configured');
});
}
}
class Mobilism {
constructor() {
this.alias = 'Mobilism';
this.origin = 'https://images.mobilism.org';
this.types = ['png', 'jpeg', 'gif', 'bmp', 'psd'];
this.sizeLimit = 1;
this.whitelist = ['forum.mobilism.org', 'forum.mobilism.me'];
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
if (this.batchLimit && images.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
images.forEach((image, ndx) => {
formData += 'Content-Disposition: form-data; name="fileName[]"\r\n\r\n';
formData += image.name + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="file[]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary;
if (ndx + 1 >= images.length) formData += '--';
formData += '\r\n';
});
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload.php',
responseType: 'document',
headers: {
'Accept': 'text/html',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': this.origin + '/index.php',
},
data: formData,
binary: true,
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (!response.response) return reject('void response');
console.assert(response.response instanceof HTMLDocument, 'response.response instanceof HTMLDocument', response.response)
let imageUrls = Array.from(response.response.querySelectorAll('input#codedirect[type="text"]'))
.map(input => input.value);
if (imageUrls.length < images.length) {
console.warn('Mobilism returning incomplete list of images (', imageUrls, ')');
return reject(`not all images uploaded (${imageUrls.length}/${images.length})`);
}
if (imageUrls.length > images.length)
console.warn('Mobilism returns more links than expected (', imageUrls, images, ')');
resolve(imageUrls);
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
});
}
rehost(urls) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
if (this.batchLimit && urls.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return Promise.all(urls.map(url => {
if (!httpParser.test(url)) return Promise.reject('URL not valid (' + url + ')');
if (!/\.(?:jpe?g|jfif|png|gif|bmp|psd)$/i.test(url))
return verifyImageUrl(url + '#.jpg').then(finalUrl => url + '#.jpg').catch(reason => {
let redirects = ['imgcdn', 'imgbb', 'pixhost', 'gifyu']
.filter(alias => alias in imageHostHandlers && typeof imageHostHandlers[alias].rehost == 'function')
.map(alias => imageHostHandlers[alias]);
const redirect = (index = 0) => (index >= 0 && index < redirects.length ?
redirects[index].rehost([url], null, true).catch(reason => redirect(index + 1))
: Promise.reject('redirection failed on all hosts')).then(results => results.map(directLinkGetter));
return redirect();
});
return verifyImageUrl(url);
})).then(imageUrls => Promise.all(imageUrls.map(imageUrl => {
let formData = new URLSearchParams({ 'imgUrl': imageUrl });
return globalXHR(this.origin + '/upload.php', {
responseType: 'document',
headers: { 'Referer': this.origin + '/index.php' },
timeout: imageUrls.length * rehostTimeout * 1000,
}, formData).then(response => {
if (response.status < 200 || response.status >= 400) return Promise.reject(defaultErrorHandler(response));
if (!response.response) return Promise.reject('void response');
console.assert(response.response instanceof HTMLDocument, 'response.response instanceof HTMLDocument', response.response)
let imageUrls = Array.from(response.response.querySelectorAll('input#codedirect[type="text"]'))
.map(input => input.value);
if (imageUrls.length <= 0) return Promise.reject('image not uploaded');
if (imageUrls.length > 1) console.warn('Mobilism returns more links than expected (', imageUrls, ')');
return imageUrls;
});
})).then(results => results.flatten()));
}
}
class PomfCat {
constructor() {
this.alias = 'Pomf.cat';
this.origin = 'https://pomf.cat';
this.types = ['jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'tif'];
this.sizeLimit = 75;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
if (this.batchLimit && images.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
images.forEach((image, index) => {
formData += 'Content-Disposition: form-data; name="files[]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary;
if (index >= images.length - 1) formData += '--';
formData += '\r\n';
});
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/upload.php',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
binary: true,
responseType: 'json',
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
try {
if (!response.response.success) return reject(response.response.error);
resolve(response.response.files.map(file => 'https://a.pomf.cat/' + file.url));
} catch(e) {
console.error('Pomf.cat invalid response structure:', response.response, e);
reject(e);
}
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
});
}
}
class LinkPicture {
constructor() {
this.alias = 'Link Picture';
this.origin = 'https://linkpicture.com';
this.types = ['jpeg', 'png', 'gif', 'bmp', 'xbm', 'wxmb'];
this.sizeLimit = 0;
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
if (this.batchLimit && images.length > this.batchLimit)
return Promise.reject('batch limit exceeded (' + this.batchLimit + ')');
return new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
images.forEach((image, index) => {
formData += 'Content-Disposition: form-data; name="mFile[]"; filename="' + image.name + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary;
formData += '\r\n';
});
formData += 'Content-Disposition: form-data; name="sozlesmeCbox"\r\n\r\n';
formData += 'on\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="shareCbox"\r\n\r\n';
formData += 'on\r\n';
formData += '--' + boundary + '--\r\n';
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/en/upload.php',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
binary: true,
responseType: 'document',
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
const results = Array.from(response.document.querySelectorAll('div.card > a > img'))
.map(img => this.origin + img.parentNode.pathname);
if (results.length < images.length)
return reject('Server returned less links than expected (' + results.length + ')');
if (results.length > images.length)
console.warn(this.alias, 'server returns more links than expected (' + results.length + ')');
resolve(results);
},
onprogress: typeof progressHandler == 'function' ? progressHandler : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
});
}
rehost(urls) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
if (urls.length <= 0) return Promise.reject('nothing to rehost');
return verifyImageUrls(urls).then(imageUrls => Promise.all(imageUrls.map(imageUrl => {
const payLoad = new URLSearchParams({ 'weblink': imageUrl });
return globalXHR(this.origin + '/upload.php', {
responseType: 'document',
headers: { 'Referer': this.origin + '/index.php' },
timeout: imageUrls.length * rehostTimeout * 1000,
}, payLoad).then(response => {
if (response.status < 200 || response.status >= 400) return Promise.reject(defaultErrorHandler(response));
const img = response.document.querySelector('div.card > a > img');
return img != null ? this.origin + img.parentNode.pathname : Promise.reject('Unexpected page structure');
});
})));
}
}
class ImageKit {
constructor() {
this.alias = 'ImageKit';
this.origin = 'https://upload.imagekit.io';
this.types = ['jpeg', 'png', 'gif', 'tiff', 'webp', 'bmp', 'svg+xml'];
//this.sizeLimit = 100;
if (!(this.publicKey = GM_getValue('imagekit_public_key'))) GM_setValue('imagekit_public_key', '');
if (!(this.privateKey = GM_getValue('imagekit_private_key'))) GM_setValue('imagekit_private_key', '');
}
upload(images, progressHandler = null) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
images = images.filter(isSupportedType.bind(this));
if (images.length <= 0) return Promise.reject('nothing to upload');
if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20))
return Promise.reject('size limit exceeded by one or more images');
return this.setSession().then(signature => Promise.all(images.map((image, index) => new Promise((resolve, reject) => {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
for (let key in signature) {
formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n';
formData += signature[key] + '\r\n';
formData += '--' + boundary + '\r\n';
}
formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="fileName"\r\n\r\n';
formData += image.name.toASCII() + '\r\n';
formData += '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="useUniqueFileName"\r\n\r\n';
formData += 'true\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: this.origin + '/api/v1/files/upload',
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'X-Requested-With': 'XMLHttpRequest',
},
data: formData,
binary: true,
timeout: getUploadTimeout(formData.length),
onload: response => {
if (response.status >= 200 && response.status < 400) resolve(ImageKit.resultHandler(response.response));
else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))));
}
rehost(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('invalid argument');
return urls.length > 0 ? this.setSession().then(signature => Promise.all(urls.map((url, index) =>
verifyImageUrl(url).then(imageUrl => getRemoteFileType(imageUrl).catch(reason => 'image/jpeg').then(mimeType => {
const payLoad = new FormData;
payLoad.set('file', imageUrl);
payLoad.set('fileName', 'image-' + Date.now().toString(16) + mimeType.toLowerCase().replace(/^(?:\w+)\//, '.'));
payLoad.set('useUniqueFileName', false);
for (let key in signature) payLoad.set(key, signature[key]);
return globalXHR(this.origin + '/api/v1/files/upload', {
responseType: 'json',
timeout: urls.length * rehostTimeout * 1000,
}, payLoad).then(response => {
if (typeof progressHandler == 'function' && httpParser.test(response.responseText)) progressHandler(true);
return ImageKit.resultHandler(response.response);
});
}))))) : Promise.reject('nothing to rehost');
}
static resultHandler(response) {
return {
original: response.url,
thumb: response.thumbnailUrl,
};
}
setSession() {
if (!this.publicKey || !this.privateKey) return Promise.reject('ImageKit API keys not configured');
const result = {
publicKey: this.publicKey,
token: uuid(),
expire: Math.round(Date.now() / 1000) + 10 * 60,
};
return new Promise(function(resolve, reject) {
if (typeof CryptoJS == 'object' && CryptoJS) return resolve(CryptoJS);
const cryptoJS = document.createElement('SCRIPT');
cryptoJS.type = 'text/javascript';
cryptoJS.onload = evt => { resolve(CryptoJS) };
cryptoJS.onerror = evt => { reject('crypto-js failed to load') };
cryptoJS.src = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js';
document.head.append(cryptoJS);
}).then(cryptoJS => Object.assign(result,
{ signature: cryptoJS.HmacSHA1(result.token + result.expire.toString(), this.privateKey).toString(cryptoJS.enc.Hex) }));
}
}
function directLinkGetter(result) {
switch (typeof result) {
case 'string': if (httpParser.test(result)) return result;
case 'object': if (httpParser.test(result.original)) return result.original;
}
if (httpParser.test(result)) return RegExp.$1;
console.warn('directLinkGetter() invalid result of upload:', result);
throw 'invalid result format';
}
function singleImageGetter(results) {
if (!Array.isArray(results)) {
console.error('singleImageGetter(', results, ')');
throw 'invalid result format';
}
if (results.length > 0) return directLinkGetter(results[0]);
throw 'empty links list';
}
var imageHostHandlers = {
'abload' : new Abload,
'bilderupload': new BilderUpload,
'casimages': new CasImages,
'catbox': new Catbox,
'cubeupload': new CubeUpload,
//'dropbox': new DropBox,
'fastpic': new FastPic,
'funkyimg': new FunkyIMG,
'geekpic': new GeekPic,
'getapic': new GetaPic,
'gett': new Gett,
'gifyu': new Chevereto('gifyu.com', 'Gifyu', ['jpeg', 'png', 'gif', 'bmp', 'webp'], 100, { sizeLimitAnonymous: 50 }),
'googledrive': new GoogleDrive,
'goopics': new GooPics,
//'googlephotos': new GooglePhotos,
'imageban': new ImageBan,
'imagekit': new ImageKit,
'imagevenue': new ImageVenue,
'imgbb': new Chevereto('imgbb.com', 'ImgBB', ['jpeg', 'png', 'bmp', 'gif', 'webp', 'tiff', 'heic', 'heif'], 32,
{ apiEndpoint: 'https://api.imgbb.com/1', apiFieldName: 'image', apiResultKey: 'data' }),
'imgbox': new ImgBox,
'imgur': new Imgur,
'imgurl': new ImgURL,
'jerking': new Chevereto('jerking.empornium.ph', 'Jerking', ['jpeg', 'png', 'bmp', 'gif', 'webp'], 5),
'lightshot': new LightShot,
'linkpicture': new LinkPicture,
'mobilism': new Mobilism,
'noelshack' : new NoelShack,
'nwcd' : new NWCD,
//'onedrive': new OneDrive,
'picabox': new PicaBox,
'pimpandhost': new PimpAndHost,
'pixhost': new PixHost,
'pomfcat': new PomfCat,
'postimage': new PostImage,
'ptpimg': new PTPimg,
'ra': new Ra,
'radikal': new Radikal,
'savephoto': new SavePhoto,
//'screencast': new ScreenCast,
'slowpoke': new Slowpoke,
'smms': new SMMS,
'svgshare': new SVGshare,
'uuploadir': new UuploadIr,
'vgyme': new VgyMe,
'z4a': new Chevereto('z4a.net', 'Z4A', ['jpeg', 'png', 'bmp', 'gif'], 50),
};
var siteWhitelists = {
'notwhat.cd': ['nwcd'],
};
var siteBlacklists = {
'passthepopcorn.me': ['imgbox', 'postimage', 'imgur', 'tinypic', 'imageshack', 'imagebam'],
};
class ImageHostManager {
constructor(messageHandler = null, UlHostList = undefined, rhHostList = undefined) {
this.messageHandler = messageHandler;
if (UlHostList) this.buildUploadChain(UlHostList); else this.ulHostChain = [ ];
if (rhHostList) this.buildRehostChain(rhHostList); else this.rhHostChain = [ ];
}
processLists(alias) {
return (!Array.isArray(siteWhitelists[document.domain])
|| siteWhitelists[document.domain].some(whiteAlias => alias == whiteAlias.toLowerCase()))
&& (!Array.isArray(siteBlacklists[document.domain])
|| siteBlacklists[document.domain].every(blackAlias => alias != blackAlias.toLowerCase()));
}
static getAliasArray(list) {
return (Array.isArray(list) ? list : typeof list == 'string' ? list.split(/[\s\,\;\|\/]+/) : [ ]).map(alias => [
['TheSunGod', 'Ra'],
['NotWhat', 'NWCD'],
['NotWhat.cd', 'NWCD'],
['NotWhatCd', 'NWCD'],
['Vgy.me', 'VgyMe'],
['SM.MS', 'SMMS'],
['Radikal.ru', 'Radikal'],
['PTPimg.me', 'PTPimg'],
['PostImages', 'PostImage'],
['PostImg', 'PostImage'],
['Empornium', 'Jerking'],
['Bilder-Upload', 'BilderUpload'],
['Uupload.ir', 'UuploadIr'],
].reduce((acc, def) => acc.toLowerCase() != def[0].toLowerCase() ? acc : def[1], alias)
.replace(nonWordStripper, '').toLowerCase());
}
buildUploadChain(list) {
this.ulHostChain = ImageHostManager.getAliasArray(list)
.filter(ImageHostManager.prototype.processLists.bind(this)).map(alias => imageHostHandlers[alias])
.filter(handler => typeof handler == 'object' && typeof handler.upload == 'function'
&& (!Array.isArray(handler.whitelist) || handler.whitelist.includes(document.domain))
&& (!Array.isArray(handler.blacklist) || !handler.blacklist.includes(document.domain)));
console.debug('Local upload hosts for ' + document.domain + ':',
this.ulHostChain.map(handler => handler.alias).join(', '));
}
buildRehostChain(list) {
this.rhHostChain = ImageHostManager.getAliasArray(list)
.filter(ImageHostManager.prototype.processLists.bind(this)).map(alias => imageHostHandlers[alias])
.filter(handler => typeof handler == 'object' && typeof handler.rehost == 'function'
&& (document.domain != 'redacted.ch' || ['PTPimg'].some(alias => handler.alias == alias))
&& (!Array.isArray(handler.whitelist) || handler.whitelist.includes(document.domain))
&& (!Array.isArray(handler.blacklist) || !handler.blacklist.includes(document.domain)));
console.debug('Remote upload hosts for ' + document.domain + ':',
this.rhHostChain.map(handler => handler.alias).join(', '));
}
uploadImages(files, progressHandler = null) {
if (!Array.isArray(this.ulHostChain) || this.ulHostChain.length <= 0) return Promise.reject('No hosts where to upload');
if (typeof files != 'object') return Promise.reject('Invalid argument');
if (!Array.isArray(files)) files = Array.from(files);
if (files.length > 1) files.sort((a, b) => a.name.localeCompare(b.name));
files = files.filter(file => file instanceof File && file.size > 0 && (!file.type || file.type.startsWith('image/')));
if (files.length <= 0) return Promise.reject('Nothing to upload');
return Promise.all(files.map(file => file.getContent())).then(images => (function uploadInternal(hostIndex = 0) {
return hostIndex >= 0 && hostIndex < this.ulHostChain.length ? (() => {
if (!files.every(isSupportedType.bind(this.ulHostChain[hostIndex])))
return Promise.reject('one or more files of unsupported format');
if (this.ulHostChain[hostIndex].sizeLimit > 0 && files.some(file => file.size > this.ulHostChain[hostIndex].sizeLimit * 2**20))
return Promise.reject(`one or more images exceed size limit (${this.ulHostChain[hostIndex].sizeLimit}MiB)`);
//if (this.ulHostChain[hostIndex].batchLimit && files.length > this.ulHostChain[hostIndex].batchLimit)
// return Promise.reject(`batch limit exceeded (${this.ulHostChain[hostIndex].batchLimit})`);
if (typeof progressHandler == 'function') {
progressHandler(hostIndex, null);
var _progressHandler = (param = null, index = undefined) => progressHandler(hostIndex, param, index);
}
return this.ulHostChain[hostIndex].upload(this.ulHostChain[hostIndex].upload.acceptFiles ?
files : images, _progressHandler);
})().catch(reason => {
console.warn('Upload to', this.ulHostChain[hostIndex].alias, 'failed:', reason);
let msg = `Upload to ${this.ulHostChain[hostIndex].alias} failed (${reason})`;
if (++hostIndex < this.ulHostChain.length) {
if (typeof this.messageHandler == 'function')
this.messageHandler(`${msg}, falling back to ${this.ulHostChain[hostIndex].alias}`);
return uploadInternal.call(this, hostIndex);
}
if (typeof this.messageHandler == 'function') this.messageHandler(msg);
return Promise.reject('Upload failed to all hosts');
}) : Promise.reject(`Host index out of bounds (${hostIndex})`);
}).call(this));
}
rehostImages(urls, progressHandler = null) {
if (!Array.isArray(urls)) return Promise.reject('Invalid argument');
urls = urls.filter(RegExp.prototype.test.bind(httpParser));
if (urls.length <= 0) return Promise.reject('Nothing to rehost');
if (!Array.isArray(this.rhHostChain) || this.rhHostChain.length <= 0) return Promise.resolve(urls);
if (testRemoteSizes) var start = Date.now();
return (testRemoteSizes ? Promise.all(urls.map(url => getRemoteFileSize(url).catch(reason => undefined)))
: Promise.resolve('Size tests skipped')).then(lengths => {
if (testRemoteSizes) console.debug('Size analysis time:', (Date.now() - start) / 1000, 's');
try { var h2 = urls.map(url => new URL(url).hostname) } catch(e) { console.error('Assertion failed: ' + e) }
return (function rehostInternal(hostIndex = 0) {
if (hostIndex < 0 || hostIndex >= this.rhHostChain.length)
return Promise.reject(`Host index out of bounds (${hostIndex})`);
try { // don't rehost again to same site
let h1 = new URL(this.rhHostChain[hostIndex].origin).hostname;
if (h1 && Array.isArray(h2) && h2.every(h2 => h2.includes(h1) || h1.includes(h2))) return Promise.resolve(urls);
} catch(e) { }
//if (this.rhHostChain[hostIndex].batchLimit && urls.length > this.rhHostChain[hostIndex].batchLimit)
// return Promise.reject('batch limit exceeded (' + this.rhHostChain[hostIndex].batchLimit + ')');
if (this.rhHostChain[hostIndex].sizeLimit > 0 && Array.isArray(lengths)
&& !lengths.every(length => !length || length <= this.rhHostChain[hostIndex].sizeLimit * 2**20))
return Promise.reject(`one or more images exceed size limit (${this.rhHostChain[hostIndex].sizeLimit}MiB)`);
if (typeof progressHandler == 'function') {
progressHandler(hostIndex, false);
var _progressHandler = (param = true) => progressHandler(hostIndex, param);
}
return this.rhHostChain[hostIndex].rehost(urls, _progressHandler).catch(reason => {
console.warn('Rehost to', this.rhHostChain[hostIndex].alias, 'failed:', reason);
let msg = `Rehost to ${this.rhHostChain[hostIndex].alias} failed (${reason})`;
if (++hostIndex < this.rhHostChain.length) {
if (typeof this.messageHandler == 'function')
this.messageHandler(`${msg}, falling back to ${this.rhHostChain[hostIndex].alias}`);
return rehostInternal.call(this, hostIndex);
}
if (typeof this.messageHandler == 'function') this.messageHandler(msg);
return Promise.reject('Rehost failed to all hosts');
});
}).call(this);
});
}
}
function forcedRehost(imageUrl) {
const lastInstanceHosts = ['imgbb', 'pixhost', 'postimage'];
return (function rehost(index = 0) {
return index >= 0 && index < lastInstanceHosts.length ? imageHostHandlers[lastInstanceHosts[index]].rehost([imageUrl])
.catch(reason => rehost(index + 1)) : Promise.reject('Rehost failed to all forced hosts');
})().then(singleImageGetter);
}
function urlResolver(url) {
if (Array.isArray(url)) url = url[0];
if (!httpParser.test(url)) return Promise.reject('Invalid URL:\n\n' + url);
try { if (!(url instanceof URL)) url = new URL(url) } catch(e) { return Promise.reject(e) }
switch (url.hostname) {
case 'www.google.com': case 'google.com':
if (url.pathname == '/url') {
let URL = url.searchParams.get('url');
if (httpParser.test(URL)) return urlResolver(URL);
}
break;
case 'rutracker.org':
if (url.pathname != '/forum/out.php') break;
return globalXHR(url, { method: 'HEAD' }).then(response => urlResolver(response.finalUrl));
case 'play.qobuz.com':
if (/^\/album\/(\w+)\b/i.test(url.pathname))
return globalXHR('https://www.qobuz.com/album/-/' + RegExp.$1, { method: 'HEAD' })
.then(response => response.finalUrl);
break;
case 'www.anonymz.com': case 'anonymz.com': case 'anonym.to': case 'dereferer.me':
var resolved = decodeURIComponent(url.search.slice(1));
return httpParser.test(resolved) ? urlResolver(resolved) : genericResolver();
//case 'reho.st':
// resolved = (url.pathname + url.search + url.hash).slice(1);
// if (/\b(?:https?):\/\/(?:\w+\.)*(?:discogs\.com|omdb\.org)\//i.test(resolved)) break;
// return httpParser.test(resolved) ? urlResolver(resolved) : genericResolver();
// URL shorteners
case 'tinyurl.com': case 'bit.ly': case 'j.mp': case 't.co': case 'apple.co': case 'flic.kr':
case 'rebrand.ly': case 'b.link': case 't2m.io': case 'zpr.io': case 'yourls.org': case 'ibn.im':
case 'deezer.page.link':
return genericResolver();
}
if (/\b(?:goo\.gl)$/i.test(url.hostname)) return genericResolver();
return Promise.resolve(url.href);
function genericResolver() {
return globalXHR(url).then(function(response) {
let redirect = response.document.querySelector('meta[http-equiv="refresh"]');
if (redirect != null && (redirect = redirect.content.replace(/^.*?\b(?:URL)\s*=\s*/i, '')) != url.href
|| /^ *(?:Location) *: *(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$1) != url.href
|| /^ *(?:Refresh) *: *(\d+); *url=(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$2) != url.href
|| (redirect = response.finalUrl) != url.href) return urlResolver(redirect);
return Promise.resolve(url.href);
});
}
}
const uaVersions = { };
function setUserAgent(params, suffixLen = 8) {
if (params && typeof params == 'object' && httpParser.test(params.url)) try {
const url = new URL(params.url);
if ([document.location.hostname, 'ptpimg.me'].includes(url.hostname)) return;
//return ['dzcdn.', 'mzstatic.com'].some(pattern => hostname.includes(pattern));
params.anonymous = true;
if (!navigator.userAgent) return;
if (!uaVersions[url.hostname] || ++uaVersions[url.hostname].usageCount > 16) uaVersions[url.hostname] = {
versionSuffix: Math.floor(Math.random() * Math.pow(2, suffixLen * 4)).toString(16).padStart(suffixLen, '0'),
usageCount: 1,
};
if (!params.headers) params.headers = { };
params.headers['User-Agent'] = navigator.userAgent.replace(/\b(Gecko|\w*WebKit|Blink|Goanna|Flow|\w*HTML|Servo|NetSurf)\/(\d+(\.\d+)*)\b/,
(match, engine, engineVersion) => engine + '/' + engineVersion + '.' + uaVersions[url.hostname].versionSuffix);
} catch(e) { console.warn('Invalid url:', params.url) }
}
function verifyImageUrl(url) {
const testByXHR = (resolvedUrl, method = 'HEAD') => new Promise(function(resolve, reject) {
const params = { method: method, url: resolvedUrl, timeout: 90e3 };
setUserAgent(params);
let hXHR = GM_xmlhttpRequest(Object.assign(params, {
onreadystatechange: function(response) {
if (response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
const abort = () => { if (!hXHR) return; if (method != 'HEAD') hXHR.abort(); hXHR = undefined; }
if (response.status < 200 || response.status >= 400) {
reject(defaultErrorHandler(response));
return abort();
}
// let contentType = /^(?:Content-Type)\s*:\s*(.+?)(?:\s*;(.+?))?\s*$/im.exec(response.responseHeaders);
// if (contentType == null || !contentType[1].startsWith('image/')) {
// contentType = ' (' + (contentType != null ? contentType[1] : undefined) + ')';
// reject('Not an image: ' + response.finalUrl + contentType);
// return abort();
// }
const invalidUrls = [
'imgur.com/removed.png',
'gtimg.cn/music/photo_new/T001M000003kfNgb0XXvgV_0.jpg',
//'//discogs.com/8ce89316e3941a67b4829ca9778d6fc10f307715/images/spacer.gif',
'amazon.com/images/I/31CTP6oiIBL.jpg',
'amazon.com/images/I/31zMd62JpyL.jpg',
'amazon.com/images/I/01RmK+J4pJL.gif',
'/0dc61986-bccf-49d4-8fad-6b147ea8f327.jpg', // Beatport
'/ab2d1d04-233d-4b08-8234-9782b34dcab8.jpg', // Beatport
'postimg.cc/wkn3jcyn9/image.jpg',
'tinyimg.io/notfound',
'hdtracks.com/img/logo.jpg',
'vgy.me/Dr3kmf.jpg',
'/images/no-cover.png',
];
if (invalidUrls.some(invalidUrl => response.finalUrl.endsWith(invalidUrl))) {
reject('Dummy image (placeholder): ' + response.finalUrl);
return abort();
}
const invalidEtags = [
'd835884373f4d6c8f24742ceabe74946',
'25d628d3d3a546cc025b3685715e065f42f9cbb735688b773069e82aac16c597f03617314f78375d143876b6d8421542109f86ccd02eab6ba8b0e469b67dc953',
'"55fade2068e7503eae8d7ddf5eb6bd09"',
'"1580238364"',
'"rbFK6Ned4SXbK7Fsn+EfdgKVO8HjvrmlciYi8ZvC9Mc"',
'7ef77ea97052c1abcabeb44ad1d0c4fce4d269b8a4f439ef11050681a789a1814fc7085a96d23212af594b6b2855c99f475b8b61d790f22b9d71490425899efa',
];
let Etag = /^(?:Etag)\s*:\s*(.+?)\s*$/im.exec(response.responseHeaders);
if (Etag != null && invalidEtags.some(etag => etag.toLowerCase() == Etag[1].toLowerCase())) {
reject('Dummy image (placeholder): ' + response.finalUrl);
return abort();
}
resolve(response.finalUrl); abort();
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}));
});
if (!httpParser.test(url)) return Promise.reject('Invalid URL');
if (verifiedImageUrls[url]) return Promise.resolve(verifiedImageUrls[url]);
return urlResolver(url).then(resolvedUrl => verifiedImageUrls[resolvedUrl] || Promise.all([
testByXHR(resolvedUrl, 'HEAD').catch(reason => /^HTTP error (?:400|403|405|406|416)\b/.test(reason) ?
testByXHR(resolvedUrl, 'GET') : Promise.reject(reason)),
new Promise(function(resolve, reject) {
const image = new Image;
image.onload = function(evt) {
if (evt.target.naturalWidth > 0 && evt.target.naturalHeight > 0) resolve(evt.target);
else reject('Zero area image loaded');
};
image.onerror = evt => { reject(evt.message || 'Image loading error (' + resolvedUrl + ')') };
image.loading = 'eager';
image.referrerPolicy = 'same-origin';
image.src = resolvedUrl;
}),
]).then(function(results) {
verifiedImageUrls[url] = results[0];
if (resolvedUrl != url) verifiedImageUrls[resolvedUrl] = results[0];
sessionStorage.setItem('verifiedImageUrls', JSON.stringify(verifiedImageUrls));
return results[0];
}));
}
function verifyImageUrls(urls) {
return Array.isArray(urls) ? Promise.all(urls.map(verifyImageUrl)) : Promise.reject('argument not an array');
}
function reduceImageSize(image, maxImageHeight, jpegQuality, progressHandler) {
const baseUrl = 'https://dragon.img2go.com/api/jobs',
referer = 'https://www.img2go.com/',
checkInterval = 250, // in ms
async = false;
const defaultParams = {
url: referer,
responseType: 'json',
headers: { 'Referer': referer, 'Origin': 'https://www.img2go.com' },
};
setUserAgent(defaultParams);
delete defaultParams.url;
const getErrorString = response => response.errors.map(error => `Error ${error.code} (${error.message})`).join(', ');
function waitJobStatus(id, status, async = undefined) {
return new Promise(function(resolve, reject) {
let params = new URLSearchParams({ _: Date.now() });
if (async !== undefined) params.set('async', async);
function waitInput() {
globalXHR(baseUrl + '/' + id + '?' + params.toString(), defaultParams).then(function({response}) {
//console.debug(response.finalUrl, status, response.status.code, response.errors);
if (response.status.code == status) resolve(response);
else if (Array.isArray(response.errors) && response.errors.length > 0) reject(getErrorString(response));
else if (response.status.code == 'failed') reject('reduceImageSize: the job failed');
else setTimeout(waitInput, checkInterval);
}).catch(reject);
}
waitInput();
});
}
return globalXHR(baseUrl + '?async=' + async, defaultParams, {
operation: 'converttoimage', //'convert image to image',
fail_on_input_error: true,
fail_on_conversion_error: true,
}).then(function({response}) {
if (response.status.code == 'failed') return Promise.reject('reduceImageSize: the job failed');
if (response.status.code != 'init') console.warn('status.code =', response.status.code);
if (response.sat.id_job) return waitJobStatus(response.sat.id_job, 'incomplete', async);
console.warn('reduceImageSize: falling back to old protocol (!sat.id_job)');
if (response.id) return response;
return Promise.reject('reduceImageSize: missing job id');
}).then(job => (httpParser.test(image) ? globalXHR(baseUrl + '/' + job.id + '/input', defaultParams, {
type: 'remote',
source: image,
engine: 'auto',
}).then(function({response}) {
if (response.status != 'ready') console.warn('reduceImageSize: set remote input returns ' + response.status);
return response;
}) : (function() {
if (typeof progressHandler == 'function') progressHandler(-1, null);
if (image instanceof File) return image.getContent();
if (image && typeof image == 'object' && /^(?:image)\//.test(image.type) && image.size >= 0 && image.data)
return image;
return Promise.reject('reduceImageSize: invalid input object');
})().then(image => new Promise(function(resolve, reject) {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="file[]"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n' + image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(-1, formData.length - image.size);
GM_xmlhttpRequest({
method: 'POST',
url: job.server + '/upload-file/' + job.id,
responseType: 'json',
headers: Object.assign({
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'X-Oc-Token': job.token,
'X-Oc-Upload-Uuid': uuid(),
}, defaultParams.headers),
data: formData,
binary: true,
//anonymous: true,
timeout: getUploadTimeout(image.size),
onload: function(response) {
if (response.status >= 200 && response.status < 400) {
if (!response.response.completed) console.warn('img2go upload not completed:', response.response);
resolve(response.response);
} else reject(defaultErrorHandler(response));
},
onprogress: typeof progressHandler == 'function' ? response => { progressHandler(-1, response) } : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}))).then(response => new Promise(function(resolve, reject) {
function waitInput() {
globalXHR(baseUrl + '/' + job.id + '?_=' + Date.now(), defaultParams).then(function(response) {
// console.debug(response.finalUrl, response.response.status.code,
// response.response.input.map(input => input.status), response.response.errors);
if (Array.isArray(response.response.input) && response.response.input.length > 0
&& response.response.input.every(input => input.status == 'ready'))
resolve(response.response.input);
else if (Array.isArray(response.response.errors) && response.response.errors.length > 0)
reject(getErrorString(response.response));
else if (Array.isArray(response.response.input) && response.response.input.some(input => input.status == 'failed'))
reject('One or more images failed to load');
else setTimeout(waitInput, checkInterval);
}).catch(reject);
}
waitInput();
})).then(function(input) {
if (!(maxImageHeight >= 0)) maxImageHeight = 1800;
if (maxImageHeight > 0 && input[0].metadata.image_height > maxImageHeight) {
var imageHeight = 2;
while (input[0].metadata.image_height % imageHeight != 0
|| input[0].metadata.image_height / imageHeight > maxImageHeight) ++imageHeight;
imageHeight = input[0].metadata.image_height / imageHeight;
if (imageHeight < maxImageHeight / 2) imageHeight = Math.ceil(maxImageHeight * 3/4);
}
return globalXHR(baseUrl + '/' + job.id + '/conversions', defaultParams, {
category: 'image', options: {
allow_multiple_outputs: false,
height: imageHeight || undefined,
quality: jpegQuality > 0 ? jpegQuality : imageHeight > 0 ? undefined : 75,
antialias: true,
},
target: 'jpg',
});
}).then(() => waitJobStatus(job.id, 'completed').then(function(response) {
if (response.output.length <= 0) throw 'Assertion failed: output.length == 1 (' + response.output.length + ')';
console.assert(response.output.length == 1, 'response.output.length == 1');
console.debug('img2go conversion result:', response.output.length == 1 ? response.output[0] : response.output);
return response.output[0];
})));
}
function optiPNG(urlOrFile, progressHandler = undefined) {
return (function() {
const url = 'https://ezgif.com/optipng';
if (urlOrFile instanceof File) {
if (!['image/png', 'image/apng'].includes(urlOrFile.type)) return Promise.reject('invalid format');
if (urlOrFile.size > 35 * 2**20) return Promise.reject('image exceeding size limit');
return urlOrFile.getContent().then(image => new Promise(function(resolve, reject) {
const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase();
let formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="new-image"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n' + image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
if (typeof progressHandler == 'function') progressHandler(formData.length - image.size);
GM_xmlhttpRequest({ method: 'POST', url: url,
headers: {
'Accept': 'text/html',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
},
data: formData,
binary: true,
onload: response => {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
resolve(domParser.parseFromString(response.responseText, 'text/html'));
},
onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress) : undefined,
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}));
} else if (httpParser.test(urlOrFile)) return (getRemoteFileType(urlOrFile)).then(function(mimeType) {
if (!['image/png', 'image/apng'].includes(mimeType)) return Promise.reject('not PNG');
//return getRemoteFileSize(urlOrFile, false).catch(reason => undefined).then(function(remoteSize) {
//if (remoteSize > 35 * 2**20) return Promise.reject('image exceeding size limit');
let payLoad = new URLSearchParams({ 'new-image-url': urlOrFile });
return globalXHR(url, undefined, payLoad).then(response => response.document);
//});
}); else return Promise.reject('invalid argument');
})().then(function(dom) {
let srcImg = dom.querySelector('img#target');
if (srcImg != null && srcImg.src) srcImg = srcImg.src;
else return Promise.reject('source image upload failed');
let form = dom.querySelector('form.ajax-form');
if (form == null) {
console.warn('ezgif.com form not found');
return srcImg || Promise.reject('invalid page structure');
}
let action = new URL(form.action);
return globalXHR('https://ezgif.com' + action.pathname, undefined, new FormData(form)).then(function(response) {
let optImg = response.document.querySelector('p.outfile > img');
if (optImg != null && optImg.src) optImg = optImg.src; else {
console.warn('ezgif.com optimisation failed');
return srcImg || Promise.reject('optimisation failed');
}
let saving = response.document.querySelector('p.filestats > span:not([class])');
if (saving != null) {
let success = parseFloat(saving.textContent) < 0 || saving.style.color == 'green';
console.log('optiPNG ' + (success ? 'success' : 'fail') + ':', urlOrFile, optImg, '(' + saving.textContent + ')');
return success ? optImg : srcImg || urlOrFile;
}
console.warn('ezgif.com failed to extract optimisation stats fromm page');
return urlOrFile instanceof File ? getRemoteFileSize(optImg).then(function(optSize) {
let delta = optSize - urlOrFile.size, sign = ['-', '', '+'][Math.sign(delta) + 1];
saving = delta * 100 / urlOrFile.size;
console.log('optiPNG ' + (delta < 0 ? 'success' : 'fail') + ':', urlOrFile, optImg, optSize,
'(' + sign + Math.abs(delta) + ' = ' + sign + Math.abs((Math.round(saving * 10) / 10)) + '%)');
return delta < 0 ? optImg : srcImg || urlOrFile;
}, function(reason) {
console.warn('ezgif.com failed to query result image size', optImg)
return optImg;
}) : optImg;
});
});
}
function getDiscogsImages(entity, discogsId) { // non-API query React method
if (entity && discogsId > 0) entity = entity.toLowerCase(); else throw 'invalid argument';
const origin = 'https://www.discogs.com';
const getImages = (killCache = false) => (function getShaHashes() {
if (typeof GM_getValue == 'function') {
var sha256Hashes = GM_getValue('discogs_graphql_hashes');
if (sha256Hashes) if (!killCache) return Promise.resolve(sha256Hashes);
else if (typeof GM_deleteValue == 'function') GM_deleteValue('discogs_graphql_hashes');
}
if ('discogsGraphqlHashes' in localStorage) try {
if (killCache) localStorage.removeItem('discogsGraphqlHashes');
else return Promise.resolve(JSON.parse(localStorage.getItem('discogsGraphqlHashes')));
} catch(e) { console.warn(e) }
const script = document.querySelector('script[src^="https://catalog-assets.discogs.com/main."][src$=".js"]');
return script != null ? globalXHR(script.src, { responseType: 'text' }).then(function({responseText}) {
sha256Hashes = /\bJSON\.parse\s*\(\s*'(\{\s*"\w+Data".+?)'\);/.exec(responseText);
if (sha256Hashes != null) sha256Hashes = JSON.parse(sha256Hashes[1]);
else throw 'Script pattern was not located';
if (!sha256Hashes || typeof sha256Hashes != 'object') throw 'Malformed SHA256 hashes extracted';
/*if (typeof GM_setValue == 'function') GM_setValue('discogs_graphql_hashes', sha256Hashes);
else */localStorage.setItem('discogsGraphqlHashes', JSON.stringify(sha256Hashes));
return sha256Hashes;
}) : Promise.reject('Unexpected document structure');
})().then(function(sha256Hashes) {
const reactRequest = new URL('/service/catalog/api/graphql', origin);
const operationName = {
artist: 'ArtistAllImages',
master: 'MasterReleaseAllImages',
release: 'ReleaseAllImages',
label: 'LabelAllImages',
}[entity];
if (!operationName) return Promise.reject(`Unsupported entity (${entity})`);
reactRequest.searchParams.set('operationName', operationName);
const sha256Hash = sha256Hashes[operationName];
if (!sha256Hash) throw 'Assertion failed: SHA256 hash was not found';
reactRequest.searchParams.set('variables', JSON.stringify({
discogsId: discogsId,
count: 2000,
}));
reactRequest.searchParams.set('extensions', JSON.stringify({ persistedQuery: {
version: 1,
sha256Hash: sha256Hash,
} }));
return globalXHR(reactRequest, {
responseType: 'json',
headers: { Referer: [origin, entity, discogsId].join('/') },
}).then(function({response}) {
switch(entity) {
case 'artist': case 'release': case 'label': var root = response.data[entity]; break;
case 'master': root = response.data.masterRelease.keyRelease; break;
}
console.assert(root, entity, response.data);
if (!(root.images.totalCount > 0)) return Promise.reject('No images for entity');
return root.images.edges.map(edge => edge.node.fullsize.sourceUrl);
}).catch(reason => reason == 'HTTP error 403' && !killCache ? getImages(true) : reason);
});
return getImages();
}
function googlePhotosResolver(url) {
return globalXHR(url).then(function(response) {
var result;
response.document.querySelectorAll('body > script[nonce]').forEach(function(script) {
if (result) return;
if (!/^(?:AF_initDataCallback)\(\{\s*key\s*:\s*'ds:(\d+)'.+\b(?:data:function)\(\)\s*\{\s*(?:return)\s*(\[[\S\s]+?\])\s*\}\}\);$/
.test(script.text)) return;
let AF_initDataCallback = eval(RegExp.$2);
if (AF_initDataCallback.length == 14 && Array.isArray(AF_initDataCallback[0])) try {
if (httpParser.test(AF_initDataCallback[0][1][0])) result = AF_initDataCallback[0][1][0] + '=s0';
} catch(e) { console.warn(e, AF_initDataCallback) }
else if (AF_initDataCallback.length == 6 && Array.isArray(AF_initDataCallback[1])) try {
result = AF_initDataCallback[1].map(photo => photo[1][0] + '=s0');
} catch(e) { console.warn(e, AF_initDataCallback) }
});
return result || Promise.reject('No image content for this URL');
});
}
function pinterestResolver(url) {
return globalXHR(url).then(function(response) {
let initialState = response.document.querySelector('script#initial-state');
if (initialState != null) try {
initialState = JSON.parse(initialState.text);
let images = Object.keys(initialState.pins).map(pin => initialState.pins[pin].images.orig.url);
if (images.length == 1) return images[0]; else if (images.length > 1) return images;
let boards = Object.keys(initialState.boards.content);
if (boards.length > 0) {
return Promise.all(boards.map(function(board) {
let params = new URLSearchParams({
source_url: response.finalUrl,
data: JSON.stringify({ options: {
board_id: initialState.boards.content[board].id,
board_url: initialState.boards.content[board].url,
} }),
_: Date.now(),
});
return globalXHR(url.origin + '/resource/BoardFeedResource/get/?' + params, {
responseType: 'json',
headers: { Referer: response.finalUrl },
}).then(function(response) {
if (response.response.resource_response.status != 'success') {
console.warn('Pinterest:', response.response.resource_response, response.finalUrl);
return Promise.reject('Pinterest: ' + response.response.resource_response.status);
}
return response.response.resource_response.data.filter(it => it.type == 'pin').map(it => it.images.orig.url);
});
}));
}
} catch(e) { console.warn(e, initialState) }
return Promise.reject('No title image for this URL');
});
}
function _500pxUrlHandler(path) {
return globalXHR('https://api.500px.com/v1/' + path + '&image_size[]=4096', { responseType: 'json' }).then(function({response}) {
let results = Object.keys(response.photos).map(id => response.photos[id].image_url[0]);
return results.length == 1 ? results[0] : results.length > 1 ? results
: Promise.reject('No image content found on this UIRL');
});
}
function pxhereCollectionResolver(url) {
if (!/\/collection\/(\d+)\b/i.test(url.pathname)) return Promise.reject('Invalid URL');
let collectionId = parseInt(RegExp.$1);
return new Promise(function(resolve, reject) {
let photos = [ ];
loadPage();
function loadPage(page = 1) {
GM_xmlhttpRequest({ method: 'GET', url: `https://pxhere.com/en/collection/${collectionId}?page=${page}&format=json`,
responseType: 'json',
onload: function(response) {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response._msg != 'success') return reject(response.response._msg);
if (!response.response.data) return resolve(photos);
let dom = domParser.parseFromString(response.response.data, 'text/html');
Array.prototype.push.apply(photos, Array.from(dom.querySelectorAll('div.item > a')).map(a => a.pathname));
loadPage(page + 1);
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}
}).then(urls => Promise.all(urls.map(url => imageUrlResolver('https://pxhere.com' + url))));
}
function unsplashCollectionResolver(url) {
if (!/\/collections\/(\d+)\b/i.test(url.pathname)) return Promise.reject('Invalid URL');
let collectionId = parseInt(RegExp.$1);
return new Promise(function(resolve, reject) {
let urls = [];
loadPage();
function loadPage(page = 1) {
GM_xmlhttpRequest({ method: 'GET', url: `https://unsplash.com/napi/collections/${collectionId}/photos?page=${page}&per_page=999`,
responseType: 'json',
onload: function(response) {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.length <= 0) return resolve(urls);
Array.prototype.push.apply(urls, response.response.map(photo => photo.urls.raw.replace(/\?.*$/, '')));
loadPage(page + 1);
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}
});
}
function pexelsCollectionResolver(url) {
if (!/\/collections\/([\w\%\-]+)\//i.test(url.pathname)) return Promise.reject('Invalid URL');
let collectionId = RegExp.$1;
return new Promise(function(resolve, reject) {
let urls = [ ];
loadPage();
function loadPage(page = 1) {
GM_xmlhttpRequest({ method: 'GET', url: `https://www.pexels.com/collections/${collectionId}/?format=html&page=${page}`,
onload: function(response) {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
let dom = domParser.parseFromString(response.responseText, 'text/html');
let photos = dom.querySelectorAll('article.photo-item > a.js-photo-link');
if (photos.length <= 0) return resolve(urls);
Array.prototype.push.apply(urls, Array.from(photos).map(a => a.pathname));
loadPage(page + 1);
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}
}).then(urls => Promise.all(urls.map(url => imageUrlResolver('https://www.pexels.com' + url))));
}
function getRemoteFileType(url) {
if (!httpParser.test(url)) return Promise.reject('getRemoteFileType: parameter not valid URL');
if (fileTypeCache.has(url)) return Promise.resolve(fileTypeCache.get(url));
const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) {
const requestParams = { method: method, url: url };
setUserAgent(requestParams);
let contentType, hXHR = GM_xmlhttpRequest(Object.assign(requestParams, {
onreadystatechange: function(response) {
if (contentType !== undefined || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
if ((contentType = /^(?:Content-Type)\s*:\s*(.+?)(?:\s*;(.+?))?\s*$/im.exec(response.responseHeaders)) != null) {
fileTypeCache.set(url, contentType = contentType[1].toLowerCase());
sessionStorage.setItem('fileTypeCache', JSON.stringify(Array.from(fileTypeCache)));
resolve(contentType);
} else reject('MIME type missing in header');
if (method != 'HEAD') hXHR.abort();
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}));
});
return getByXHR('HEAD')
.catch(reason => /^HTTP error (403|416)\b/.test(reason) ? getByXHR('GET') : Promise.reject(reason));
}
function getRemoteFileSize(url, forced = true) {
if (!httpParser.test(url)) return Promise.reject('getRemoteFileSize(...): parameter not valid URL');
if (fileSizeCache.has(url)) return Promise.resolve(fileSizeCache.get(url));
const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) {
function success() {
fileSizeCache.set(url, size);
sessionStorage.setItem('fileSizeCache', JSON.stringify(Array.from(fileSizeCache)));
resolve(size);
}
const requestParams = { method: method, url: url, binary: true, responseType: 'blob' };
setUserAgent(requestParams);
let size, hXHR = GM_xmlhttpRequest(Object.assign(requestParams, {
onreadystatechange: function(response) {
if (typeof size == 'number' && size >= 0 || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
if (/*(size = response.responseText) && (size = new Blob([size]).size) >= 0
|| */(size = response.response) && (size = size.size) >= 0) success();
else if (!forced || method == 'HEAD') reject('Body size missing in header'); else return;
if (method != 'HEAD') hXHR.abort();
},
onload: function(response) { // fail-safe
if (typeof size == 'number' && size >= 0) return;
else if (response.status >= 200 && response.status < 400) {
if (/*(size = response.responseText) && (size = new Blob([size]).size) >= 0
|| */(size = response.response) && (size = size.size) >= 0) resolve(size);
else reject('File content not loaded');
} else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}));
});
return getByXHR(forced ? 'GET' : 'HEAD')
.catch(reason => !forced && /^HTTP error (403|416)\b/.test(reason) ? getByXHR('GET') : Promise.reject(reason));
}
function formattedSize(size) {
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
const e = (size = Math.max(size, 0)) > 0 ? Math.min(Math.floor(Math.log2(size) / 10), units.length - 1) : 0;
return `${(size / Math.pow(2, e * 10)).toFixed(Math.min(e, 3))}\xA0${units[e]}`;
}
function voidDragHandler0(evt) { return false }
function inputDropHandler(evt) {
if (evt.currentTarget.style.backgroundColor)
evt.currentTarget.style.backgroundColor = evt.currentTarget.dataset.backgroundColor || null;
return !evt.shiftKey ? inputDataHandler(evt, evt.dataTransfer) : true;
}
function inputPasteHandler(evt) { return inputDataHandler(evt, evt.clipboardData) }
function inputClear(evt) { evt.currentTarget.value = '' }
function setInputHandlers(elem, highlightDragOver = '#FF04') {
if (!(elem instanceof HTMLInputElement) || elem.disabled || elem.offsetParent == null) return;
elem.ondragover = voidDragHandler0;
elem.ondblclick = inputClear;
elem.ondrop = inputDropHandler;
elem.onpaste = inputPasteHandler;
elem.placeholder = 'Paste/drop local or remote image';
if (!highlightDragOver) return;
const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
elem.ondragenter = function(evt) {
if (evt.currentTarget.disabled || !evt.dataTransfer.types.some(type =>
['Files', 'text/uri-list', 'text/plain'].includes(type))) return false;
evt.currentTarget.backgroundColor = evt.currentTarget.style.backgroundColor || null;
evt.currentTarget.style.backgroundColor = highlightDragOver;
};
elem[`ondrag${isFirefox ? 'exit' : 'leave'}`] = function(evt) {
if (!('backgroundColor' in evt.currentTarget)) return false;
evt.currentTarget.style.backgroundColor = evt.currentTarget.backgroundColor;
delete evt.currentTarget.backgroundColor;
};
}
function setTextAreahandlers(elem, highlightDragOver = '#FF04') {
if (!(elem instanceof HTMLTextAreaElement) || elem.disabled || elem.offsetParent == null) return;
elem.ondragover = voidDragHandler0;
elem.ondrop = function(evt) {
if (evt.currentTarget.style.backgroundColor)
evt.currentTarget.style.backgroundColor = evt.currentTarget.backgroundColor || null;
return textAreaDropHandler(evt);
}
elem.onpaste = textAreaPasteHandler;
if (!highlightDragOver) return;
const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
elem.ondragenter = function(evt) {
if (evt.currentTarget.disabled || !evt.dataTransfer.types.some(type =>
['Files', 'text/uri-list'].includes(type))) return false;
evt.currentTarget.backgroundColor = evt.currentTarget.style.backgroundColor || null;
evt.currentTarget.style.backgroundColor = highlightDragOver;
};
elem[`ondrag${isFirefox ? 'exit' : 'leave'}`] = function(evt) {
if (!('backgroundColor' in evt.currentTarget)) return false;
evt.currentTarget.style.backgroundColor = evt.currentTarget.backgroundColor;
delete evt.currentTarget.backgroundColor;
};
};
function randomString(length) {
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let text = "";
for (let i = 0; i < length; ++i) text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
function isSupportedType(image) {
if (!this || typeof this != 'object' || !image || typeof image != 'object') return false;
if (!Array.isArray(this.types) || this.types.length <= 0) return !image.type || image.type.startsWith('image/');
return this.types.some(function(mimeType) {
if (!mimeType) return false;
if (image.type) return image.type == 'image/' + mimeType.toLowerCase();
const testExt = extensions => extensions.some(ext => image.name.toLowerCase().endsWith('.' + ext.toLowerCase()));
return image.name && testExt([mimeType]);
});
}
//////////////////////////////////////////////////////////////////////////
////////////////////////////// SAFE PADDING //////////////////////////////
//////////////////////////////////////////////////////////////////////////
Wrap
Beautify