NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // ==UserLibrary== // @name imageHostUploader // @namespace https://openuserjs.org/users/Anakunda // @exclude * // @version 2.53 // @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.sh', '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(isSupportgredType.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' && (!['redacted.sh', 'redacted.ch'].includes(document.domain) || ['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/'; const checkInterval = 250, async = true; const defaultParams = { url: referer, responseType: 'json', headers: { Referer: referer, Origin: 'https://www.img2go.com' }, cookie: ['qg_locale_suggest=true', 'QGID=' + uuid(), 'qgrole=unregistered'].join('; '), }; setUserAgent(defaultParams); delete defaultParams.url; const getErrorString = response => response.errors.map(error => `Error ${error.code} (${error.message})`).join(', '); const waitJobStatus = (id, status, async = undefined) => 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: 'convert', 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 reactRequest(url, operationName, variables) { if (!url || !operationName) throw 'Invalid argument'; const reactRequest = (refreshCache = false) => (function() { if ('discogsGraphqlHashes' in localStorage) try { if (refreshCache) localStorage.removeItem('discogsGraphqlHashes'); else return Promise.resolve(JSON.parse(localStorage.getItem('discogsGraphqlHashes'))); } catch(e) { console.warn(e) } return globalXHR(url, { anonymous: true }).then(function({document}) { const script = Array.prototype.find.call(document.getElementsByTagName('script'), function(script) { if (script.src) try { script = new URL(script.src) } catch(e) { return false } else return false; return script.hostname == 'catalog-assets.discogs.com' && /^\/main\.\w+\.js$/i.test(script.pathname); }); return script ? globalXHR(script.src, { responseType: 'text', anonymous: true }) : Promise.reject('Unexpected document structure'); }).then(function({responseText}) { let 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 instanceof Object)) throw 'Malformed SHA256 hashes extracted'; localStorage.setItem('discogsGraphqlHashes', JSON.stringify(sha256Hashes)); refreshCache = true; return sha256Hashes; }); })().then(function(sha256Hashes) { const requestUrl = new URL('/service/catalog/api/graphql', 'https://www.discogs.com'); requestUrl.searchParams.set('operationName', operationName); if (variables) requestUrl.searchParams.set('variables', JSON.stringify(variables)); if (!(sha256Hashes = sha256Hashes[operationName])) throw 'Assertion failed: SHA256 hash was not found'; requestUrl.searchParams.set('extensions', JSON.stringify({ persistedQuery: { sha256Hash: sha256Hashes, version: 1 } })); return globalXHR(requestUrl, { responseType: 'json', anonymous: true, headers: { Referer: url } }); }).then(({json}) => json.data || Promise.reject('Invalid response format')).catch(function(reason) { if (!refreshCache) return reactRequest(true); console.warn('React request error:', reason); return Promise.reject(reason); }); return reactRequest(); } function getDiscogsImages(entity, discogsId) { // non-API query React method if (entity && (discogsId = parseInt(discogsId)) > 0) entity = entity.toLowerCase(); else throw 'Invalid argument'; const operationName = { artist: 'ArtistAllImages', master: 'MasterReleaseAllImages', release: 'ReleaseAllImages', label: 'LabelAllImages', }[entity]; if (!operationName) return Promise.reject(`Unsupported entity (${entity})`); return reactRequest(`https://www.discogs.com/${entity}/${discogsId}`, operationName, { discogsId: discogsId, count: 2000, }).then(function(data) { switch(entity) { case 'artist': case 'release': case 'label': var root = data[entity]; break; case 'master': root = data.masterRelease.keyRelease; break; } console.assert(root, entity, data); if (!root) return Promise.reject(`Images root not found for current entity (${entity})`); if (!(root.images.totalCount > 0)) return Promise.reject('No images for entity'); return root.images.edges.map(edge => edge.node.fullsize.sourceUrl); }); } 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 ////////////////////////////// //////////////////////////////////////////////////////////////////////////