สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/401726/804713/imageHostUploader.js
// ==UserScript==
// @name imageHostUploader
// @namespace https://greasyfork.org/cs/users/321857-anakunda
// @version 1.20
// @author Anakunda
// @description ×
// @require https://greasyfork.org/scripts/401725-globalfetch/code/globalFetch.js
// @require https://greasyfork.org/scripts/394414-ua-resource/code/UA-resource.js
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
'use strict';
const ulTimeFactor = 128;
const rehostTimeout = 30000;
const urlParser = /^\s*(https?:\/\/\S+)\s*$/i;
const ptpimgOrigin = 'https://ptpimg.me';
const ptpSiteWhitelist = ['passthepopcorn.me', 'redacted.ch', 'orpheus.network', 'notwhat.cd', 'dicmusic.club', 'broadcasthe.net'];
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tif', 'tiff', 'heic'];
const imageHostHandlers = {
'ptpimg': ['PtpImg', upload2PTPIMG, rehost2PTPIMG],
'malzo': ['Malzo', (images, elem) => upload2ImageHost('malzo.com', images, elem), urls => rehost2ImageHost('malzo.com', urls)],
'imgbb': ['ImgBB', (images, elem) => upload2ImageHost('imgbb.com', images, elem), urls => rehost2ImageHost('imgbb.com', urls)],
'imgbox': ['ImgBox', upload2ImgBox, null],
'pixhost': ['PixHost', upload2PixHost, rehost2PixHost],
'catbox': ['Catbox', upload2Catbox, rehost2Catbox],
'jerking': ['Jerking', (images, elem) => upload2ImageHost('jerking.empornium.ph', images, elem),
urls => rehost2ImageHost('jerking.empornium.ph', urls)],
'picload': ['PicLoad', (images, elem) => upload2ImageHost('free-picload.com', images, elem),
urls => rehost2ImageHost('free-picload.com', urls)],
'postimage': ['PostImage', upload2PostImg, rehost2PostImg],
'imgur': ['Imgur', upload2Imgur, rehost2Imgur],
'imagevenue': ['ImageVenue', upload2ImageVenue, null],
};
[
'ptpimg_api_key',
'malzo_uid', 'malzo_password',
'imgbb_uid', 'imgbb_password',
'catbox_userhash',
'imgbox_uid', 'imgbox_password',
'jerking_uid', 'jerking_password',
'picload_uid', 'picload_password',
'postimg_uid', 'postimg_password',
'imagevenue_uid', 'imagevenue_password',
].forEach(propName => { if (!GM_getValue(propName)) GM_setValue(propName, '') });
['upload_hosts', 'rehost_hosts'].forEach(propName => { if (!GM_getValue(propName)) GM_setValue(propName, [
'PtpImg', 'Malzo', 'PixHost', 'ImgBB', 'ImgBox', 'Catbox', 'Jerking', 'PicLoad', 'PostImage', 'Imgur', 'ImageVenue',
].join(', ')) });
String.prototype.toASCII = function() {
return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, '');
};
Array.prototype.flatten = function() {
return this.reduce(function(flat, toFlatten) {
return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten);
}, []);
}
function uploadImages(files, elem) {
if (typeof files != 'object') return Promise.reject('Invalid argument');
if (!Array.isArray(files)) files = Array.from(files);
//if (files.length > 1) files.push(files.shift());
var hostChain = GM_getValue('upload_hosts');
if (!hostChain) return Promise.reject('No hosts where to upload (local storage "upload_hosts")');
hostChain = hostChain.split(/\s*[\,\;\|\/]\s*/).map(alias => imageHostHandlers[alias.toLowerCase()])
.filter(handler => Array.isArray(handler) && typeof handler[1] == 'function'
&& (handler[0].toLowerCase() != 'ptpimg' || ptpSiteWhitelist.includes(document.domain)));
if (hostChain.length <= 0) return Promise.reject('No hosts where to upload');
var frs = files.filter(function(file) {
return file instanceof File && imageExtensions.some(ext => file.type == 'image/' + ext);
})/*.sort((file1, file2) => file1.name.localeCompare(file2.name))*/.map(file => new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function() {
if (reader.result.length != file.size) console.warn('FileReader: binary string read length mismatch (',
reader.result.length, '≠', file.size, ')');
resolve({ name: file.name, type: file.type, size: file.size, data: reader.result });
};
reader.onerror = reader.ontimeout = function() { reject('FileReader error (' + file.name + ')') };
reader.readAsBinaryString(file);
}));
if (frs.length <= 0) return Promise.reject('Nothing to upload');
return Promise.all(frs).then(function(images) {
return uploadInternal();
function uploadInternal(hostIndex = 0) {
return hostIndex >= 0 && hostIndex < hostChain.length ? hostChain[hostIndex][1](images, elem).catch(function(reason) {
if (++hostIndex >= hostChain.length) return Promise.reject('Upload failed to all hosts');
console.warn('Upload to', hostChain[hostIndex - 1][0], 'failed (' + reason + '), falling back to', hostChain[hostIndex][0]);
return uploadInternal(hostIndex);
}) : Promise.reject('Host index out of bounds ('.concat(hostIndex, ')'));
}
});
}
function rehostImages(urls) {
if (!Array.isArray(urls)) return Promise.reject('Invalid parameter');
if (urls.length <= 0) return Promise.reject('Nothing to rehost');
var hostChain = GM_getValue('rehost_hosts');
if (!hostChain) return Promise.resolve(urls);
hostChain = hostChain.split(/\s*[\,\;\|\/]\s*/).map(alias => imageHostHandlers[alias.toLowerCase()])
.filter(handler => Array.isArray(handler) && typeof handler[2] == 'function'
&& (handler[0].toLowerCase() == 'ptpimg' ? ptpSiteWhitelist.includes(document.domain) : document.domain != 'redacted.ch'));
return hostChain.length > 0 ? rehostInternal() : Promise.resolve(urls);
function rehostInternal(hostIndex = 0) {
return hostIndex >= 0 && hostIndex < hostChain.length ? hostChain[hostIndex][2](urls).catch(function(reason) {
if (++hostIndex >= hostChain.length) return Promise.reject('Rehost failed to all hosts');
console.warn('Rehost to', hostChain[hostIndex - 1][0], 'failed (' + reason + '), falling back to', hostChain[hostIndex][0]);
return rehostInternal(hostIndex);
}) : Promise.reject('Host index out of bounds ('.concat(hostIndex, ')'));
}
}
function urlResolver(url) {
if (!urlParser.test(url)) return Promise.reject('Invalid URL:\n\n'.concat(url));
try { if (!(url instanceof URL)) url = new URL(url) } catch(e) { return Promise.reject(e) }
switch (url.hostname) {
case 'rutracker.org':
if (url.pathname != '/forum/out.php') break;
return globalFetch(url, { method: 'HEAD' }).then(response => urlResolver(response.finalUrl));
case 'www.anonymz.com': case 'anonymz.com': case 'anonym.to': case 'dereferer.me':
var resolved = decodeURIComponent(url.search.slice(1));
return urlParser.test(resolved) ? urlResolver(resolved) : genericResolver();
// case 'reho.st':
// resolved = url.pathname.concat(url.search, url.hash).slice(1);
// if (/\b(?:https?):\/\/(?:\w+\.)*(?:discogs\.com|omdb\.org)\//i.test(resolved)) break;
// return urlParser.test(resolved) ? urlResolver(resolved) : genericResolver();
// URL shorteners
case 'tinyurl.com': case 'bit.ly': case 'j.mp': case 't.co': case 'goo.gl': case 'apple.co':
case 'rebrand.ly': case 'b.link': case 't2m.io': case 'zpr.io': case 'yourls.org':
return genericResolver();
}
return Promise.resolve(url.href);
function genericResolver() {
return globalFetch(url).then(function(response) {
var redirect = response.document.querySelector('meta[http-equiv="refresh"]');
if (redirect != null && (redirect = redirect.content.replace(/^.*?\b(?:URL)\s*=\s*/i, '')) != url.href)
return urlResolver(redirect);
if (/^ *(?:Location) *: *(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$1) != url.href)
return urlResolver(redirect);
return response.finalUrl != url.href ? urlResolver(response.finalUrl) : Promise.resolve(url.href);
});
}
}
function verifyImageUrl(url, resolve = false) {
return urlResolver(url).then(function(url) {
//if (!strict && imageExtensions.some(ext => url.toLowerCase().endsWith('.'.concat(ext)))) return Promise.resolve(url); // weak
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = load => { resolve(url) };
img.onerror = function(error) {
if (img.src.includes('?')) img.src = url.replace(/\?.*?(?=\#|$)/, '');
else reject('Not valid image:\n\n'.concat(url));
};
img.ontimeout = timeout => { reject('Image load timed out:\n\n'.concat(url)) };
img.src = url;
}).catch(reason => resolve ? imageUrlResolver(url) : Promise.reject(reason));
});
}
function verifyImageUrls(urls, resolve = false) {
if (!Array.isArray(urls)) return Promise.reject('URLs not an array');
var verified = Promise.all(urls.map(url => verifyImageUrl(url, resolve)));
return !resolve ? verified : verified.then(verified => verified.flatten());
}
function upload2PTPIMG(images, elem) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
return getPTPIMGapiKey().then(apiKey => new Promise(function(resolve, reject) {
var now = Date.now();
const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
var formData = '--' + boundary + '\r\n';
images.filter(function(image) {
return image.data && image.name && ['png', 'jpg', 'jpeg', 'gif', 'bmp'].some(ext => image.type == 'image/'.concat(ext));
}).forEach(function(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: ptpimgOrigin + '/upload.php',
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': formData.length,
},
data: formData,
binary: true,
timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
onload: function(response) {
if (response.status >= 200 && response.status < 400) {
if (response.response) resolve(response.response.map(item => ptpimgOrigin + '/' + item.code + '.' + item.ext));
else reject('void response');
} else reject(defaultErrorHandler(response));
},
onprogress: elem instanceof HTMLElement && 'value' in elem ? function(progress) {
var pct = progress.position * 100 / progress.total;
//elem.value = 'Uploading... (' + Math.round(pct) + '%)';
} : undefined,
onerror: error => reject(defaultErrorHandler(error)),
ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
});
}));
}
function rehost2PTPIMG(urls) {
if (!Array.isArray(urls)) return Promise.reject('Invalid parameter');
if (urls.length <= 0) return Promise.resolve([]); //Promise.reject('Nothing to rehost');
return Promise.all(urls.map(function(url) {
if (!urlParser.test(url)) return Promise.reject('URL not valid ('.concat(url, ')'));
var hostname = new URL(url).hostname;
if (hostname.endsWith('discogs.com') || hostname.endsWith('omdb.org')) {
return verifyImageUrl('https://reho.st/'.concat(url))
.catch(reason => rehost2Catbox([url]).then(imgUrls => imgUrls[0]))
.catch(reason => rehost2PixHost([url]).then(imgUrls => imgUrls[0]))
.catch(reason => reupload2PTPIMG(url));
} else if (!['png', 'jpg', 'jpeg', 'gif', 'bmp'].some(ext => url.toLowerCase().endsWith('.'.concat(ext)))) {
return verifyImageUrl(url.concat('#.jpg'))
.catch(reason => rehost2ImageHost('malzo.com', [url]).then(imgUrls => imgUrls[0]))
.catch(reason => rehost2PixHost([url]).then(imgUrls => imgUrls[0]))
.catch(reason => rehost2ImageHost('imgbb.com', [url]).then(imgUrls => imgUrls[0]))
.catch(reason => rehost2ImageHost('jerking.empornium.ph', [url]).then(imgUrls => imgUrls[0]))
.catch(reason => rehost2ImageHost('free-picload.com', [url]).then(imgUrls => imgUrls[0]));
//.catch(reason => rehost2Imgur([url]).then(imgUrls => imgUrls[0]));
}
return verifyImageUrl(url);
})).then(imageUrls => getPTPIMGapiKey().then(function(apiKey) {
console.debug('rehost2PTPIMG(...) input:', imageUrls);
var formData = new URLSearchParams({
'link-upload': imageUrls.join('\r\n'),
'api_key': apiKey,
});
return globalFetch(ptpimgOrigin + '/upload.php', {
responseType: 'json',
timeout: imageUrls.length * rehostTimeout,
}, formData).then(function(response) {
if (!response.response) return Promise.reject('PTPIMG void response');
if (!response.response.length < imageUrls.length)
return Promise.reject(`not all images rehosted (${response.response.length}/${imageUrls.length})`);
return response.response.map(item => ptpimgOrigin.concat('/', item.code, '.', item.ext));
});
}));
}
function reupload2PTPIMG(imgUrl) {
console.warn('PTPIMG rehoster fallback to local reupload');
return globalFetch(imgUrl, { responseType: 'blob' }).then(function(response) {
var image = {
name: imgUrl.replace(/^.*\//, ''),
data: response.responseText,
};
switch (imgUrl.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 upload2PTPIMG([image]).then(imgUrls => imgUrls[0]);
});
}
function getPTPIMGapiKey() {
var ptpimg_api_key = GM_getValue('ptpimg_api_key');
if (ptpimg_api_key) return Promise.resolve(ptpimg_api_key);
try {
var apiKey = JSON.parse(window.localStorage.ptpimg_it).api_key;
if (apiKey) {
GM_setValue('ptpimg_api_key', ptpimg_api_key = apiKey);
return Promise.resolve(apiKey);
}
} catch(e) { console.debug('getPTPIMGapiKey():', e) }
return globalFetch(ptpimgOrigin).then(function(response) {
if ((apiKey = response.document.getElementById('api_key')) == null) {
let counter = GM_getValue('ptpimg_reminder_read', 0);
if (counter < 3) {
alert(`
PTPIMG API key could not be captured. Please login to ${ptpimgOrigin}/ and redo the action.
If you don\'t have PTPIMG account, consider to remove PtpImg from upload_hosts and rehost_hosts entries in local storage.
Uploading and rehosting is still available to fallback image hosts.
`);
GM_setValue('ptpimg_reminder_read', ++counter);
}
return Promise.reject('PTPIMG API key not configured');
}
if (!apiKey.value) return Promise.reject('Assertion failed: missing PTPIMG API key');
GM_setValue('ptpimg_api_key', ptpimg_api_key = apiKey.value);
Promise.resolve(ptpimg_api_key).then(apiKey => { alert(`Your PTPIMG API key [${apiKey}] was successfully configured`) });
return ptpimg_api_key;
});
}
function upload2ImageHost(hostname, images, elem) {
if (hostname.toLowerCase() == 'free-picload.com' && ['passthepopcorn.me'].some(domain => document.domain == domain))
return Promise.reject(hostname.concat(' blacklisted for this site'));
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
const anonSessionLimits = {
'malzo.com': 2,
'imgbb.com': 2,
'jerking.empornium.ph': 5,
'free-picload.com': 50,
};
return setImageHostSession(hostname).then(session => Promise.all(images.map(image => new Promise(function(resolve, reject) {
switch (hostname) {
case 'malzo.com':
case 'jerking.empornium.ph':
case 'free-picload.com':
if (!['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].some(ext => image.type == 'image/'.concat(ext)))
throw 'MIME type not supported: '.concat(image.type);
break;
case 'imgbb.com':
if (!['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tif', 'tiff', 'heic'].some(ext => image.type == 'image/'.concat(ext)))
throw 'MIME type not supported: '.concat(image.type);
break;
}
var anonSessionLimit = anonSessionLimits[hostname.toLowerCase()];
if (!session.username && anonSessionLimit > 0 && image.size > anonSessionLimit * 2**20)
throw 'image size exceeds anonymous upload limit';
const boundary = '----WebKitFormBoundary'.concat(Date.now().toString(16).toUpperCase());
var formData = '--' + boundary + '\r\n', params = Object.assign({
action: 'upload',
type: 'file',
nsfw: 0,
}, session);
Object.keys(params).forEach(function(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="source"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n' + image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
GM_xmlhttpRequest({
method: 'POST',
url: 'https://'.concat(hostname, '/json'),
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': formData.length,
'Referer': 'https://'.concat(hostname, '/'),
},
data: formData,
binary: true,
timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
onload: function(response) {
if (response.status >= 200 && response.status < 400) {
if (response.response.success) resolve(response.response.image.url);
else reject(response.response.error.message.concat(' (', response.response.status_code, ')'));
} else reject(defaultErrorHandler(response));
},
onerror: error => reject(defaultErrorHandler(error)),
ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
});
function formField(key, value) {
return 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n' + value + '\r\n--' + boundary;
}
}))));
}
function rehost2ImageHost(hostname, urls) {
if (hostname.toLowerCase() == 'free-picload.com' && ['passthepopcorn.me'].some(domain => document.domain == domain))
return Promise.reject(hostname.concat(' blacklisted for this site'));
return setImageHostSession(hostname).then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(function(imageUrl) {
var formData = new URLSearchParams(Object.assign({
action: 'upload',
type: 'url',
nsfw: 0,
source: imageUrl,
}, session));
return globalFetch('https://'.concat(hostname, '/json'), {
responseType: 'json',
headers: { 'Referer': 'https://'.concat(hostname, '/') },
timeout: urls.length * rehostTimeout,
}, formData).then(function(response) {
return response.response.success ? response.response.image.url
: Promise.reject(hostname.concat(': ', response.response.error.message,' (', response.response.status_code, ')'));
});
}))));
}
function imageHostGalleryResolver(hostname, url) {
var albumId = /^\/(?:album|al)\/(\w+)\b/.test(url.pathname) && RegExp.$1;
if (!albumId) return Promise.reject('Invlaid gallery URL');
return setImageHostSession(hostname).then(function(session) {
var formData = new URLSearchParams(Object.assign({
action: 'get-album-contents',
albumid: albumId,
}, session));
return globalFetch(url.origin.concat('/json'), {
responseType: 'json',
headers: { 'Referer': url },
}, formData).then(function(response) {
return response.response.status_txt == 'OK' && Array.isArray(response.response.contents) ?
response.response.contents.map(image => image.url)
: Promise.reject(hostname.concat(': ', response.response.error.message,' (', response.response.status_code, ')'));
});
});
}
function setImageHostSession(hostname) {
return globalFetch('https://'.concat(hostname, '/')).then(function(response) {
if (!/\b(?:auth_token)\s*=\s*"(\w+)"/.test(response.responseText)) return Promise.reject('Auth token detection failure');
var session = {
auth_token: RegExp.$1,
timestamp: Date.now(),
};
if (getUser(response)) return session;
if (hostname.toLowerCase() == 'free-picload.com') var hostPrefix = 'picload_';
else if (/^([\w\-]+)(?:\.[\w\-]+)+$/.test(hostname)) hostPrefix = RegExp.$1.toLowerCase().concat('_');
if (!hostPrefix) return session;
var login = GM_getValue(hostPrefix.concat('uid')), password = GM_getValue(hostPrefix.concat('password'));
if (!login || !password) return session;
var formData = new URLSearchParams({
'login-subject': login,
'password': password,
'auth_token': session.auth_token,
});
return new Promise(function(resolve, reject) {
GM_xmlhttpRequest({ method: 'POST', url: 'https://'.concat(hostname, '/login'),
headers: {
'Accept': '*/*',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Content-Length': formData.toString().length,
'Referer': 'https://'.concat(hostname, '/'),
}, data: formData.toString(), onload: function(response) {
// Server response HTTP/400 - "Bad Request" even if the login succeeds
if (response.status < 200 || response.status > 400) reject(defaultErrorHandler(response)); else {
if (getUser(response)) console.debug(hostname, 'authorized token:', session.auth_token);
resolve(session);
}
},
onerror: error => reject(defaultErrorHandler(error)),
ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
});
});
function getUser(response) {
if (!/\b(?:logged_user)\s*=\s*(\{.*?\});/.test(response.responseText)) return false;
try {
let logged_user = JSON.parse(RegExp.$1);
session.username = logged_user.username;
session.userid = logged_user.id;
return true;
} catch(e) {
console.warn(e);
return false;
}
}
});
}
function upload2PixHost(images, elem) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
return Promise.all(images.map(image => new Promise(function(resolve, reject) {
if (!['png', 'jpg', 'jpeg', 'gif'].some(ext => image.type == 'image/'.concat(ext)))
throw 'MIME type not supported: '.concat(image.type);
var now = Date.now();
const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
var formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="img"; 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';
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.pixhost.to/images',
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': formData.length,
},
data: formData,
binary: true,
timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
onload: function(response) {
if (response.status >= 200 && response.status < 400) resolve(response.response.show_url);
else reject(defaultErrorHandler(response));
},
onerror: error => reject(defaultErrorHandler(error)),
ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
});
}).then(imageUrlResolver)));
}
function rehost2PixHost(urls) {
return verifyImageUrls(urls).then(function(imageUrls) {
//console.debug('rehost2PixHost(...) input:', imageUrls.join('\n'));
var formData = new URLSearchParams({
imgs: imageUrls.join('\r\n'),
content_type: 0,
tos: 'on',
});
return globalFetch('https://pixhost.to/remote/', {
responseType: 'text',
timeout: imageUrls.length * rehostTimeout,
}, formData).then(function(response) {
if (!/\b(?:upload_results)\s*=\s*(\{.*\});$/m.test(response.responseText)) return Promise.reject('page parsing error');
var images = JSON.parse(RegExp.$1).images;
if (images.length < imageUrls.length) return Promise.reject(`not all images rehosted (${images.length}/${imageUrls.length})`);
return Promise.all(images.map(image => imageUrlResolver(image.show_url)));
});
});
}
function upload2Catbox(images, elem) {
if (!Array.isArray(images)) return Promise.reject('Invalid argument');
if (images.length <= 0) return Promise.reject('Nothing to upload or format not supported');
return getCatboxUserHash().catch(reason => undefined).then(userHash => Promise.all(images.map(image => new Promise(function(resolve, reject) {
var now = Date.now();
const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
var 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';
GM_xmlhttpRequest({
method: 'POST',
url: 'https://catbox.moe/user/api.php',
responseType: 'text',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': formData.length,
},
data: formData,
binary: true,
timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
onload: function(response) {
if (response.status >= 200 && response.status < 400) resolve(response.responseText);
else reject(defaultErrorHandler(response));
},
onerror: error => reject(defaultErrorHandler(error)),
ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
});
}))));
}
function rehost2Catbox(urls) {
return getCatboxUserHash().catch(reason => undefined).then(userHash => Promise.all(urls.map(url => verifyImageUrl(url).then(function(imageUrl) {
var formData = new URLSearchParams({
reqtype: 'urlupload',
url: imageUrl,
});
if (userHash) formData.set('userhash', userHash);
return globalFetch('https://catbox.moe/user/api.php', {
responseType: 'text',
timeout: urls.length * rehostTimeout,
}, formData).then(response => response.responseText);
}))));
}
function getCatboxUserHash() {
var catbox_userhash = GM_getValue('catbox_userhash');
return catbox_userhash ? Promise.resolve(catbox_userhash) : globalFetch('https://catbox.moe/').then(function(response) {
catbox_userhash = response.document.querySelector('input[name="userhash"][value]');
return catbox_userhash != null && catbox_userhash.value || Promise.reject('Catbox.moe: not logged in or userhash not found');
});
}
function upload2ImageVenue(images, elem) {
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
return setImageVenueSession().then(session => new Promise(function(resolve, reject) {
var now = Date.now();
const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
var formData = '--' + boundary + '\r\n';
Object.keys(session).forEach(function(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(function(image, index, arr) {
if (!['png', 'jpg', 'jpeg', 'gif'].some(ext => image.type == 'image/'.concat(ext)))
throw 'MIME type not supported: '.concat(image.type);
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: 'http://www.imagevenue.com/upload',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': formData.length,
},
data: formData,
responseType: 'json',
binary: true,
timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
onload: function(response) {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
resolve(response.response.success);
},
onerror: error => reject(defaultErrorHandler(error)),
ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
});
})).then(resultUrl => globalFetch(resultUrl)).then(function(response) {
var links = response.document.querySelectorAll('div.row > div > a');
return Promise.all(Array.from(links).map(a => imageUrlResolver(a.href)));
});
}
function setImageVenueSession() {
return globalFetch('http://www.imagevenue.com/').then(function(response) {
var 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;
var uid = GM_getValue('imagevenue_uid'), password = GM_getValue('imagevenue_password');
if (!uid || !password) return csrfToken.content;
var formData = new URLSearchParams({
'_token': csrfToken.content,
'email': uid,
'password': password,
});
GM_xmlhttpRequest({ method: 'POST', url: 'http://www.imagevenue.com/auth/login', headers: {
'Referer': 'http://www.imagevenue.com/auth/login',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Content-Length': formData.toString().length,
}, data: formData.toString() });
return new Promise(function(resolve, reject) {
setTimeout(function() {
globalFetch('http://www.imagevenue.com/').then(function(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 => globalFetch('http://www.imagevenue.com/upload/session', {
responseType: 'json',
headers: { 'X-CSRF-TOKEN': csrfToken },
}, new URLSearchParams({
thumbnail_size: 2,
content_type: 'sfw',
comments_enabled: false,
})).then(response => ({
data: response.response.data,
_token: csrfToken,
})));
}
function upload2ImgBox(images, elem) {
if (['passthepopcorn.me'].some(domain => document.domain == domain)) return Promise.reject('ImgBox blacklisted for this site');
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
return setImgBoxSession().then(session => Promise.all(images.map(image => new Promise(function(resolve, reject) {
if (!['png', 'jpg', 'jpeg', 'gif'].some(ext => image.type == 'image/'.concat(ext)))
throw 'MIME type not supported: '.concat(image.type);
var now = Date.now();
const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
var formData = '--' + boundary + '\r\n';
Object.keys(session.params).forEach(function(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';
GM_xmlhttpRequest({
method: 'POST',
url: 'https://imgbox.com/upload/process',
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': formData.length,
'X-CSRF-Token': session.csrf_token,
},
data: formData,
responseType: 'json',
binary: true,
timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
onload: function(response) {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
resolve(response.response.files[0].original_url);
},
onerror: error => reject(defaultErrorHandler(error)),
ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
});
}))));
}
function setImgBoxSession() {
return globalFetch('https://imgbox.com/').then(function(response) {
var 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;
var uid = GM_getValue('imgbox_uid'), password = GM_getValue('imgbox_password');
if (!uid || !password) return csrfToken.content;
var formData = new URLSearchParams({
"utf8": "✓",
"authenticity_token": csrfToken.content,
"user[login]": uid,
"user[password]": 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',
'Content-Length': formData.toString().length,
}, data: formData.toString() });
return new Promise(function(resolve, reject) {
setTimeout(() => { globalFetch('http://imgbox.com/').then(function(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 => globalFetch('https://imgbox.com/ajax/token/generate', {
method: 'POST',
responseType: 'json',
headers: { 'X-CSRF-Token': csrfToken },
}).then(response => ({
csrf_token: csrfToken,
params: {
token_id: response.response.token_id,
token_secret: response.response.token_secret,
content_type: 1,
thumbnail_size: '100c',
gallery_id: null,
gallery_secret: null,
comments_enabled: 0,
},
})));
}
function upload2Imgur(images, elem) {
if (['passthepopcorn.me'].some(domain => document.domain == domain)) return Promise.reject('Imgur blacklisted for this site');
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
return Promise.all(images.map(image => new Promise(function(resolve, reject) {
const requestUrl = 'https://imgur.com/upload';
var now = Date.now();
const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
var formData = '--' + boundary + '\r\n';
formData += 'Content-Disposition: form-data; name="Filedata"; filename="' + image.name.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
GM_xmlhttpRequest({
method: 'POST',
url: requestUrl,
responseType: 'json',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': formData.length,
'Referer': requestUrl,
},
data: formData,
binary: true,
timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
onload: function(response) {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
resolve('https://i.imgur.com/'.concat(response.response.data.hash, response.response.data.ext));
},
onerror: error => reject(defaultErrorHandler(error)),
ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
});
})));
}
function rehost2Imgur(urls) {
if (['passthepopcorn.me'].some(domain => document.domain == domain)) return Promise.reject('Imgur blacklisted for this site');
return Promise.all(urls => urls.map(url => verifyImageUrl(url).then(function(imageUrl) {
const requestUrl = 'https://imgur.com/upload';
var formData = new URLSearchParams({ url: imageUrl });
return globalFetch(requestUrl, {
responseType: 'json',
headers: { Referer: requestUrl },
timeout: urls.length * rehostTimeout,
}, formData).then(function(result) {
if (!result.response.success) return Promise.reject(result.response.status);
return 'https://i.imgur.com/'.concat(result.response.data.hash, result.response.data.ext);
});
})));
}
function upload2PostImg(images, elem) {
if (['passthepopcorn.me'].some(domain => document.domain == domain)) return Promise.reject('PostImages blacklisted for this site');
if (!Array.isArray(images)) return Promise.reject('invalid argument');
if (images.length <= 0) return Promise.reject('nothing to upload');
return setPostImgSession().then(session => Promise.all(images.map(image => new Promise(function(resolve, reject) {
var now = Date.now();
const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
var formData = '--' + boundary + '\r\n';
Object.keys(session).forEach(function(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.toASCII() + '"\r\n';
formData += 'Content-Type: ' + image.type + '\r\n\r\n';
formData += image.data + '\r\n';
formData += '--' + boundary + '--\r\n';
GM_xmlhttpRequest({
method: 'POST',
url: 'https://postimages.org/json/rr',
responseType: 'json',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': formData.length,
//'Referer': 'https://postimages.org/',
},
data: formData,
binary: true,
timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
onload: function(response) {
if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
if (response.response.status != 'OK') return reject(response.response.status);
resolve(response.response.url);
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).then(imageUrlResolver))));
}
function rehost2PostImg(urls) {
if (['passthepopcorn.me'].some(domain => document.domain == domain)) return Promise.reject('PostImages blacklisted for this site');
return setPostImgSession().then(session => Promise.all(urls => urls.map(url => verifyImageUrl(url).then(function(imageUrl) {
var formData = new URLSearchParams(Object.assign({ url: imageUrl }, session));
return globalFetch('https://postimages.org/json/rr', {
responseType: 'json',
timeout: urls.length * rehostTimeout,
}, formData).then(function(response) {
if (response.status < 200 || response.status >= 400) return Promise.reject(defaultErrorHandler(response));
if (response.response.status != 'OK') return Promise.reject(response.response.status);
return imageUrlResolver(response.response.url);
});
}))));
}
function setPostImgSession() {
return globalFetch('https://postimages.org/').then(function(response) {
var session = {
session_upload: Date.now(),
upload_session: rand_string(32),
optsize: 0,
expire: 0,
numfiles: 1,
//upload_referer: btoa('https://postimages.org/'),
};
if (/"token","(\w+)"/.test(response.responseText)) session.token = RegExp.$1;
if (response.document.querySelector('nav.authorized') != null) return session;
var uid = GM_getValue('postimg_uid'), password = GM_getValue('postimg_password');
if (!uid || !password) return session;
var formData = new URLSearchParams({
'email': uid,
'password': password,
});
return new Promise((resolve, reject) => GM_xmlhttpRequest({
method: 'POST', url: 'https://postimages.org/login',
data: formData.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Content-Length': formData.toString().length,
//'Referer': 'https://postimages.org/login',
},
onload: function(response) {
if (response.status >= 200 && response.status <= 400) resolve(session);
else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}));
});
function rand_string(e) {
for (var t = "", i = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", n = 0; n < e; n++)
t += i.charAt(Math.floor(Math.random()*i.length));
return t;
}
}
function getRemoteFileSize(url) {
return new Promise(function(resolve, reject) {
var imageSize, abort = GM_xmlhttpRequest({
method: 'GET', url: url, responseType: 'arraybuffer',
onreadystatechange: function(response) {
if (imageSize || response.readyState < XMLHttpRequest.HEADERS_RECEIVED
|| !/^Content-Length:\s*(\d+)\b/im.test(response.responseHeaders)) return;
if (!(imageSize = parseInt(RegExp.$1))) return;
resolve(imageSize);
abort.abort();
},
onload: function(response) { // fail-safe
if (imageSize) return;
if (response.status >= 200 && response.status < 400) resolve(response.responseText.length /*response.response.byteLength*/);
else reject(new Error('Image not accessible'));
},
onerror: response => reject('Image not accessible'),
ontimeout: response => reject('Image not accessible'),
});
});
}
function formattedSize(size) {
return size < 1024**1 ? Math.round(size) + ' B'
: size < 1024**2 ? (Math.round(size * 10 / 2**10) / 10) + ' KiB'
: size < 1024**3 ? (Math.round(size * 100 / 2**20) / 100) + ' MiB'
: size < 1024**4 ? (Math.round(size * 100 / 2**30) / 100) + ' GiB'
: size < 1024**5 ? (Math.round(size * 100 / 2**40) / 100) + ' TiB'
: (Math.round(size * 100 / 2**50) / 100) + ' PiB';
}
function voidDragHandler0(evt) { return false }
function imageDropHandler(evt) { return !evt.shiftKey ? imageDataHandler(evt, evt.dataTransfer) : true }
function imagePasteHandler(evt) { return imageDataHandler(evt, evt.clipboardData) }
function imageClear(evt) {
evt.target.value = '';
coverPreview(evt.target, null);
}
function setInputHandlers(node) {
node.ondragover = voidDragHandler0;
node.ondblclick = imageClear;
node.ondrop = imageDropHandler;
node.onpaste = imagePasteHandler;
node.placeholder = 'Paste/drop local or remote image';
}
function setTextAreahandlers(node) {
node.ondragover = voidDragHandler0;
node.ondrop = descDropHandler;
node.onpaste = descPasteHandler;
};