// ==UserScript==
// @name 画像全置換
// @name:ja 画像全置換
// @name:en All image replace
// @namespace https://snowshome.page.link/p
// @version 1.3.7
// @description ページ上の画像を全て指定画像に置換する。シンプル故に悪質。
// @description:ja ページ上の画像を全て指定画像に置換する。シンプル故に悪質。
// @description:en Replace all images on the page with the specified image. It's malicious because it's simple.
// @author tromtub(snows)
// @license GPL-3.0
// @match *://*/*
// @match file:///*/*
// @icon https://i.gifer.com/ZKZg.gif
// @supportURL https://github.com/hi2ma-bu4/all-img-replace
// @supportURL https://gf.qytechs.cn/ja/scripts/496146-%E7%94%BB%E5%83%8F%E5%85%A8%E7%BD%AE%E6%8F%9B
// @compatible chrome
// @compatible edge
// @compatible opera chromium製なので動くと仮定
// @grant GM.addStyle
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM.registerMenuCommand
// @run-at document-start
// ==/UserScript==
/*
*/
(function() {
'use strict';
// 被った時はここを変更
const PRO_NAME = "IMRP";
// 保存用Key
const SD_URIS_KEY = "URIS";
const SD_SETTING_KEY = "SETTING";
// メニュー内部使用
const HIDE_CLASS = PRO_NAME + "_HIDE";
const MENU_ID = PRO_NAME + "_MENU";
const MENU_INNER_CLASS = MENU_ID + "_INNER";
const MENU_TOOLBAR_ID = MENU_ID + "_TOOLBAR";
const MENU_CLOSE_ID = MENU_ID + "_CLOSE";
const FILE_INPUT_ID = MENU_ID + "_FILE";
const URL_INPUT_ID = MENU_ID + "_URL_IN";
const URL_ADD_ID = MENU_ID + "_URL_ADD";
const IMAGE_SELECT_ID = MENU_ID + "_IMSE";
const IMAGE_DEL_ID = IMAGE_SELECT_ID + "_DEL";
const OPT_URL_ID = IMAGE_SELECT_ID + "_URL";
const OPT_BASE64_ID = IMAGE_SELECT_ID + "_BASE64";
const PREVIEW_ID = MENU_ID + "_PREVIEW";
// 探査用
const REPLACE_BASE = PRO_NAME + "_REPLACE";
const REPLACE_EXCLUSION_CLASS = REPLACE_BASE + "_EXCLUSION";
const REPLACE_CHECK_CLASS = REPLACE_BASE + "_CHECK";
const REPLACE_REDETECTION_CLASS = REPLACE_BASE + "_REDETECTION";
const REPLACE_SVG_IMAGE_CLASS = REPLACE_BASE + "_IMAGE";
// data属性用
const DATA_ORIGINAL = PRO_NAME.toLowerCase() + "_origin";
const DATA_ORIGINAL_TYPE = DATA_ORIGINAL + "_type";
const DATA_CHANGE_KEY = PRO_NAME.toLowerCase() + "_key";
const MAX_DATA_NAME_LEN = 50;
const SUB_FRAME_LOG_STYLE = "color:greenyellow;font-weight:bold";
const CORS_EVASION_URL = "https://script.google.com/macros/s/AKfycbyVHzqlKX4FvmPkvUktdu0H3hAUDfVYP5jsIgngxFoXEdKgFVUbT0cjwFebJA4ZcCfW/exec";
const XLINK_NS = "http://www.w3.org/1999/xlink";
const SVG_NS = "http://www.w3.org/2000/svg"
const EXCLUSION_DOM = ["."+REPLACE_EXCLUSION_CLASS,"script", "style", "frame", "iframe", "meta"];
const ALL_DOM_QUERY = `*:not(:is(${EXCLUSION_DOM.join(",")}))`;
const BASE_CSS = `
.${HIDE_CLASS} {
display: none;
}
#${MENU_ID} {
position: fixed;
top: 0;
left: 0;
max-width: 100dvw;
min-width: 20em;
max-height: 95dvh;
min-height: 10em;
background-color: rgba(230,230,230,.8);
border: black 2px solid;
z-index: 114514;
user-select: none;
overflow: auto;
}
#${MENU_ID} > div:not(#${MENU_TOOLBAR_ID}) {
padding: 1em;
}
#${MENU_ID} input, #${PREVIEW_ID} {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.${MENU_INNER_CLASS} {
width: 100%;
background-color: rgba(200,200,200,.2);
margin-bottom: .5em;
}
.${MENU_INNER_CLASS} label {
display: inline-block;
width: 100%;
}
.${MENU_INNER_CLASS} :is(input, select) {
width:100%;
}
#${MENU_ID} h3 {
margin-bottom: .5em;
}
#${MENU_ID} h4 {
margin: 0 0 .5em .5em;
font-size: 1.1em;
}
#${PREVIEW_ID} {
width: 100%;
background-color: greenyellow;
border: black 1px solid;
}
#${PREVIEW_ID} img {
width: 100%;
min-height: 3em;
}
#${MENU_TOOLBAR_ID} {
display: flex;
position: sticky;
flex-direction: row-reverse;
right: 0;
bottom: 0;
}
`
const settingData = {
menuOpen: false,
repSVG: true,
repCSS: true,
repTag: true,
repCooltime: 5000,
};
const uriData = {
url: {},
base64: {},
};
// キャッシュを保持して軽量化
let c_urlKeys = null;
let c_base64Keys = null;
const isTopWindow = window == window.top;
const menu_command_id_1 = GM.registerMenuCommand("Open Settings", function (event) {
menuOpen();
}, {
accessKey: "s",
autoClose: true,
});
try {
GM.addStyle(BASE_CSS);
}
catch (e) {
err(e);
}
// ====================================================================================================
function log(...args){
if(isTopWindow){
console.log(`[${PRO_NAME}]`, ...args);
}
else{
console.log(`%c[${PRO_NAME}]`, SUB_FRAME_LOG_STYLE, ...args);
}
}
function err(...args){
if(isTopWindow){
console.error(`[${PRO_NAME}]`, ...args);
}
else{
console.error(`%c[${PRO_NAME}]`, SUB_FRAME_LOG_STYLE, ...args);
}
}
function acq(id){
return document.getElementById(id);
}
function isDataUrl(input) {
return /^data:([a-zA-Z0-9-]+\/[a-zA-Z0-9-]+)?(;[a-zA-Z0-9-]+=[a-zA-Z0-9-]+)*(;base64)?,[a-zA-Z0-9+/]+={0,2}$/.test(input);
}
function init(){
// 設定読み込み
loadSettingData();
// URI読み込み
loadUriData();
// 読み込み待機
let si = setInterval(() => {
if(document?.body){
clearInterval(si);
DCL();
}
},1000);
}
function DCL(){
log("body読み込み完了確認")
// メニューを自動で開く(デバッグ用)
if(settingData.menuOpen){
menuOpen();
}
const observer = new MutationObserver(obs);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: false,
});
observer.observe(document.head, {
childList: true,
subtree: true,
attributes: true,
characterData: false,
});
let cou = getAllDOM(document.head);
cou += getAllDOM(document.body);
log("初回実行-変更数:", cou);
setTimeout(()=>{
if(!document.querySelector(`link:is([rel="icon"],[rel="shortcut icon"])`)){
addFavicon();
}
}, 1000);
// 定期リサーチ設定
reSearch();
}
function obs(mutations){
let cou = 0;
for (const mutation of mutations) {
if(mutation.target){
cou += getAllDOM(mutation.target);
}
}
if(cou){
log("obs実行-変更数:", cou);
}
}
function getAllDOM(elem){
let cou = 0
if(elem.matches(ALL_DOM_QUERY)){
cou += changeDOM(elem);
}
const elems = elem.querySelectorAll(ALL_DOM_QUERY);
for(let e of elems){
cou += changeDOM(e);
}
return cou;
}
function changeDOM(elem){
const ec = elem.classList;
if(ec.contains(REPLACE_CHECK_CLASS)){
if(!ec.contains(REPLACE_REDETECTION_CLASS)){
return 0;
}
}
else{
ec.add(REPLACE_CHECK_CLASS);
}
let cou = 0;
if(c_urlKeys.length){
// ファビコン置き換え
cou += replaceFavicon(elem);
// src置き換え
cou += replaceSrcElem(elem);
// style置き換え
cou += replaceStyleElem(elem);
}
if(c_base64Keys.length){
// svg置き換え(これは必ず最後に設置)
cou += replaceSvgElem(elem);
}
return cou;
}
function reSearch(){
let cou = 0;
// スタイルタグ再巡回
cou += styleReSearch();
if(cou){
log("reSearch実行-変更数:", cou);
}
// wait
setTimeout(reSearch, 1000);
}
function styleReSearch(){
let cou = 0;
const elems = document.querySelectorAll(`:is([style],[class]).${REPLACE_CHECK_CLASS}:not(.${REPLACE_REDETECTION_CLASS})`);
for(let e of elems){
e.classList.remove(REPLACE_CHECK_CLASS);
cou += changeDOM(e);
}
return cou;
}
// ====================================================================================================
function menuOpen(){
if(!isTopWindow){
log("サブフレームなのでメニュー表示をブロック");
return;
}
let menu_elem = acq(MENU_ID);
if(!menu_elem){
menu_elem = menuInit();
updateList();
log("メニューOpen");
}
if(menu_elem.classList.contains(HIDE_CLASS)) {
menu_elem.classList.remove(HIDE_CLASS);
updateList();
log("メニューOpen");
}
}
function menuClose(){
let menu_elem = acq(MENU_ID);
if(menu_elem && !menu_elem.classList.contains(HIDE_CLASS)) {
menu_elem.classList.add(HIDE_CLASS);
log("メニューClose");
}
}
function menuInit(){
log("メニューInit");
let menu_elem = document.createElement("div");
menu_elem.id = MENU_ID;
menu_elem.classList.add(REPLACE_EXCLUSION_CLASS);
menu_elem.innerHTML = `
<div>
<h3>画像全置換-設定</h3>
<div class="${MENU_INNER_CLASS}">
<h4>画像をローカルから追加</h4>
<label>
<input id="${FILE_INPUT_ID}" type="file" accept=".png,.jpeg,.jpg,.gif">
</label>
</div>
<div class="${MENU_INNER_CLASS}">
<h4>画像をURL(dataURL)から追加</h4>
<label>
<input id="${URL_INPUT_ID}" type="url" placeholder="https://example.com/test.png">
<input id="${URL_ADD_ID}" type="button" value="追加">
</label>
</div>
<div class="${MENU_INNER_CLASS}">
<h4>登録画像の確認と削除</h4>
<label>
<select id="${IMAGE_SELECT_ID}" size="10">
<optgroup id="${OPT_URL_ID}" label="img置換URL">
<option value="">--指定されていません--</option>
</optgroup>
<optgroup id="${OPT_BASE64_ID}" label="svg置換URL">
<option value="">--指定されていません--</option>
</optgroup>
</select>
<input id="${IMAGE_DEL_ID}" type="button" value="削除">
</label>
<p>プレビュー</p>
<div id="${PREVIEW_ID}">
<img class="${REPLACE_EXCLUSION_CLASS}" src="" alt="">
</div>
</div>
</div>
<div id="${MENU_TOOLBAR_ID}">
<input id="${MENU_CLOSE_ID}" type="button" value="閉じる">
</div>
`;
document.body.appendChild(menu_elem);
autoEvent("#"+MENU_CLOSE_ID, menuClose);
autoEvent("#"+FILE_INPUT_ID, uploadFile, "change");
autoEvent("#"+URL_ADD_ID, uploadUrl);
autoEvent("#"+IMAGE_SELECT_ID, changePreview, "change");
autoEvent("#"+IMAGE_DEL_ID, delUrl);
return menu_elem;
}
function autoEvent(elem, callback, type="click"){
if(typeof elem === "string"){
elem = document.querySelector(elem);
}
elem.addEventListener(type, callback);
}
async function loadSettingData(){
let saveData = await GM.getValue(SD_SETTING_KEY, null);
if (saveData != null) {
let jsonData = null;
try {
jsonData = JSON.parse(saveData);
log("設定ロード完了");
}
catch (e) {
err(e);
}
if(jsonData != null){
for (let key in settingData) {
if (key in jsonData) {
settingData[key] = jsonData[key];
}
}
}
}
}
async function saveSettingData(){
try {
await GM.setValue(SD_SETTING_KEY, JSON.stringify(settingData));
log("設定保存完了");
}
catch (e) {
err(e);
}
}
async function loadUriData(){
let saveData = await GM.getValue(SD_URIS_KEY);
if (saveData != null) {
let jsonData = null;
try {
jsonData = JSON.parse(saveData);
log("URIロード完了");
}
catch (e) {
err(e);
}
if(jsonData != null){
for (let key in uriData) {
if (key in jsonData) {
uriData[key] = jsonData[key];
}
}
}
}
updateCache();
}
async function saveUriData(){
try {
await GM.setValue(SD_URIS_KEY, JSON.stringify(uriData));
log("URI保存完了");
updateCache();
}
catch (e) {
err(e);
}
}
function updateCache(){
c_urlKeys = Object.keys(uriData.url);
c_base64Keys = Object.keys(uriData.base64);
}
function uploadFile(e){
const file = e.target.files[0];
e.target.value = "";
convertBase64(file).then(uri => {
addUriData("url", uri, file.name);
addUriData("base64", uri, file.name);
saveUriData();
updateList();
}).catch(err);
}
function uploadUrl(){
let input = acq(URL_INPUT_ID);
const url = input.value?.trim();
if(url == ""){
return;
}
if(isDataUrl(url)){
input.value = "";
addUriData("url", url, url);
addUriData("base64", url, url);
saveUriData();
updateList();
return;
}
imageUrlToBase64(url).then(uri => {
input.value = "";
addUriData("url", url, url);
addUriData("base64", uri, url);
saveUriData();
updateList();
}).catch(err)
}
function addUriData(type, uri, name){
if(uriData[type]){
let key = name ?? uri;
if (key.length > MAX_DATA_NAME_LEN) {
key = "..." + key.slice(-MAX_DATA_NAME_LEN+3);
}
if(uriData[type][key]){
err(`既に「${key}」は存在します!`);
return false;
}
uriData[type][key] = uri;
}
return false;
}
function convertBase64(image){
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("load", e => {
const base64Text = e.currentTarget.result;
resolve(base64Text);
return;
});
reader.addEventListener("error", reject);
reader.readAsDataURL(image);
})
}
function updateList(){
const optUrl = acq(OPT_URL_ID);
if(optUrl){
let ht = "";
for(let key in uriData.url){
ht += `<option value="url-${key}">${key}</option>`;
}
optUrl.innerHTML = ht;
}
const optBase64 = acq(OPT_BASE64_ID);
if(optBase64){
let ht = "";
for(let key in uriData.base64){
ht += `<option value="base64-${key}">${key}</option>`;
}
optBase64.innerHTML = ht;
}
}
function changePreview(e){
const preview = document.querySelector(`#${PREVIEW_ID} > img`);
if(preview){
if(e.target.value == ""){
return;
}
let sp = e.target.value.split("-");
let key = sp[0];
if(uriData[key]){
sp.shift();
let name = sp.join("-");
preview.src = uriData[key][name];
preview.alt = name;
preview.title = uriData[key][name];
}
}
}
function imageUrlToBase64(imageUrl) {
return new Promise(async (resolve, reject) => {
try {
// CORSオリジンで怒られるのでゴリ押す
const proxyUrl = `${CORS_EVASION_URL}?url=${encodeURIComponent(imageUrl)}`;
const response = await fetch(proxyUrl);
if (!response.ok) {
reject(`HTTP error! status: ${response.status}`);
return;
}
const jsonResponse = await response.json();
resolve(jsonResponse.dataUrl);
} catch (e) {
err('画像の取得または変換中にエラーが発生しました');
reject(e);
}
});
}
function delUrl(){
const se = acq(IMAGE_SELECT_ID);
if(se){
let v = se.value.trim();
if(v == ""){
return;
}
let sp = v.split("-");
let key = sp[0];
if(uriData[key]){
sp.shift();
let name = sp.join("-");
if(!confirm(`データ「${name}(${key})」を削除しますか?\n(この操作は取り消せません)`)){
return;
}
uriData[key][name] = undefined;
delete uriData[key][name];
saveUriData();
updateList();
}
}
}
// ====================================================================================================
function randomArr(arr){
return arr[Math.random() * arr.length|0];
}
function setOriginalData(e, data, type){
if(e.dataset[DATA_ORIGINAL]){
return;
}
e.dataset[DATA_ORIGINAL] = data;
e.dataset[DATA_ORIGINAL_TYPE] = type;
}
function changeUrlIO(e, key){
if(key){
e.dataset[DATA_CHANGE_KEY] = key;
return key;
}
return e.dataset[DATA_CHANGE_KEY];
}
function svgImgageHerfIO(e, data){
if(data){
e.setAttribute("href", data);
e.setAttributeNS(XLINK_NS, "href", data);
return data;
}
let hrefValue = e.getAttribute("href");
if (!hrefValue) {
hrefValue = e.getAttributeNS(XLINK_NS, "href");
}
return hrefValue;
}
function autoTagSet(type, e){
if(e[type]){
let key = changeUrlIO(e);
if(key && uriData.url[key] === e[type]){
return 0;
}
if(!key){
setOriginalData(e, e[type], type);
key = randomArr(c_urlKeys);
changeUrlIO(e, key);
}
e[type] = uriData.url[key];
e.classList.add(REPLACE_REDETECTION_CLASS);
return 1;
}
return 0;
}
function autoStyleSet(type, e, cs){
const v = cs.getPropertyValue(type);
if(v != "none"){
const an = type.replace(/-([a-z])/g, (m, a) => a.toUpperCase());
const s = e.style[an];
const m = s.match(/url\(["'](.+?)["']\)/);
let key;
if(m){
key = changeUrlIO(e);
if(key && uriData.url[key] === m[1]){
return 0;
}
}
else if(/url\(["'].+?["']\)/.test(v)){
if(!e.classList.contains(REPLACE_REDETECTION_CLASS)){
setOriginalData(e, v, type);
e.classList.add(REPLACE_REDETECTION_CLASS);
}
}
else{
return 0;
}
if(!key){
key = randomArr(c_urlKeys);
}
let tmp = v.replace(/url\(["'].+?["']\)/g, `url("${uriData.url[key]}")`);
if(tmp == v){
return 0;
}
changeUrlIO(e, key);
e.style[an] = tmp;
//console.log(e, v);
return 1;
}
return 0;
}
function addFavicon(){
log("アイコン生成")
const icon = document.createElement("link");
icon.rel = "icon";
icon.href = "";
document.head.appendChild(icon);
}
function replaceFavicon(e){
if(e.tagName !== "LINK"){
return 0;
}
switch(e.rel){
case "icon":
case "shortcut icon":
log("アイコン変更")
break;
default:
return 0;
}
let key = changeUrlIO(e);
if(key && uriData.url[key] === e.href){
return 0;
}
if(!key){
setOriginalData(e, e.href, "href");
key = randomArr(c_urlKeys);
changeUrlIO(e, key);
e.classList.add(REPLACE_REDETECTION_CLASS);
}
e.href = uriData.url[key];
return 1;
}
function replaceSrcElem(e){
let cou = 0;
cou += autoTagSet("src", e);
cou += autoTagSet("srcset", e);
return cou;
}
function replaceStyleElem(e){
let cou = 0;
const s = getComputedStyle(e);
cou += autoStyleSet("background-image", e, s);
cou += autoStyleSet("background", e, s);
return cou;
}
function replaceSvgElem(e){
if(e.tagName !== "svg"){
return 0;
}
let key = changeUrlIO(e);
const img = e.querySelector("."+REPLACE_SVG_IMAGE_CLASS);
if(key && img && e.children.length == 1 && uriData.base64[key] === svgImgageHerfIO(img)){
return 0;
}
if(!key){
key = randomArr(c_base64Keys);
}
setOriginalData(e, "", "svg");
changeUrlIO(e, key)
let ne = e.cloneNode(false);
e.parentNode.replaceChild(ne, e);
e = ne;
const image = document.createElementNS(SVG_NS, "image");
image.classList.add(REPLACE_CHECK_CLASS, REPLACE_SVG_IMAGE_CLASS);
image.setAttribute("x",0);
image.setAttribute("y",0);
image.setAttribute("width","100%");
svgImgageHerfIO(image, uriData.base64[key]);
e.appendChild(image);
e.classList.add(REPLACE_REDETECTION_CLASS);
return 1;
}
init();
})();