diff --git a/public/ext/safari-15/manifest.json b/public/ext/safari-15/manifest.json index 1b4e52db..16e415e2 100644 --- a/public/ext/safari-15/manifest.json +++ b/public/ext/safari-15/manifest.json @@ -34,20 +34,14 @@ }, { "js": ["dist/content-scripts/dot-user-js.js"], - "matches": [ - "*://*/*.user.js", - "*://*/*.user.js?*", - "*://*/*.user.css", - "*://*/*.user.css?*" - ], - "run_at": "document_start", - "all_frames": false + "matches": ["*://*/*.user.js", "*://*/*.user.js?*"], + "run_at": "document_start" }, { - "js": ["dist/content-scripts/greasyfork.js"], + "js": ["dist/content-scripts/script-market.js"], "matches": ["*://*.greasyfork.org/*"], - "run_at": "document_start", - "all_frames": false + "exclude_matches": ["*://*/*.user.js", "*://*/*.user.js?*"], + "run_at": "document_end" } ], "permissions": [ diff --git a/public/ext/shared/_locales/en/messages.json b/public/ext/shared/_locales/en/messages.json index 8ef4fea6..a39bfc24 100644 --- a/public/ext/shared/_locales/en/messages.json +++ b/public/ext/shared/_locales/en/messages.json @@ -130,6 +130,12 @@ "settings_settings_sync_desc": { "message": "Sync settings across devices" }, + "settings_augmented_userjs_install": { + "message": "Enhanced installation prompts" + }, + "settings_augmented_userjs_install_desc": { + "message": "Automatically pop up the installation interface when opening a user script URL (.user.js), and takes over the install button of the user scripts market" + }, "settings_toolbar_badge_count": { "message": "Show Toolbar Count Badge" }, diff --git a/public/ext/shared/_locales/zh/messages.json b/public/ext/shared/_locales/zh/messages.json index d9fbaff9..7338f30a 100644 --- a/public/ext/shared/_locales/zh/messages.json +++ b/public/ext/shared/_locales/zh/messages.json @@ -126,6 +126,12 @@ "settings_settings_sync_desc": { "message": "跨设备同步设置" }, + "settings_augmented_userjs_install": { + "message": "增强的脚本安装提示" + }, + "settings_augmented_userjs_install_desc": { + "message": "当打开一个用户脚本 URL(.user.js)时自动弹出安装界面,并接管用户脚本市场的安装按钮" + }, "settings_toolbar_badge_count": { "message": "工具栏图标显示计数徽章" }, diff --git a/public/ext/shared/_locales/zh_HK/messages.json b/public/ext/shared/_locales/zh_HK/messages.json index bec11460..07853cb3 100644 --- a/public/ext/shared/_locales/zh_HK/messages.json +++ b/public/ext/shared/_locales/zh_HK/messages.json @@ -126,6 +126,12 @@ "settings_settings_sync_desc": { "message": "跨裝置同步設定" }, + "settings_augmented_userjs_install": { + "message": "增強的腳本安裝提示" + }, + "settings_augmented_userjs_install_desc": { + "message": "當開啟一個使用者腳本 URL(.user.js)時自動彈出安裝介面,並接管使用者腳本市場的安裝按鈕" + }, "settings_toolbar_badge_count": { "message": "工具欄圖示顯示計數徽章" }, diff --git a/public/ext/shared/_locales/zh_MO/messages.json b/public/ext/shared/_locales/zh_MO/messages.json index bec11460..07853cb3 100644 --- a/public/ext/shared/_locales/zh_MO/messages.json +++ b/public/ext/shared/_locales/zh_MO/messages.json @@ -126,6 +126,12 @@ "settings_settings_sync_desc": { "message": "跨裝置同步設定" }, + "settings_augmented_userjs_install": { + "message": "增強的腳本安裝提示" + }, + "settings_augmented_userjs_install_desc": { + "message": "當開啟一個使用者腳本 URL(.user.js)時自動彈出安裝介面,並接管使用者腳本市場的安裝按鈕" + }, "settings_toolbar_badge_count": { "message": "工具欄圖示顯示計數徽章" }, diff --git a/public/ext/shared/_locales/zh_TW/messages.json b/public/ext/shared/_locales/zh_TW/messages.json index bec11460..07853cb3 100644 --- a/public/ext/shared/_locales/zh_TW/messages.json +++ b/public/ext/shared/_locales/zh_TW/messages.json @@ -126,6 +126,12 @@ "settings_settings_sync_desc": { "message": "跨裝置同步設定" }, + "settings_augmented_userjs_install": { + "message": "增強的腳本安裝提示" + }, + "settings_augmented_userjs_install_desc": { + "message": "當開啟一個使用者腳本 URL(.user.js)時自動彈出安裝介面,並接管使用者腳本市場的安裝按鈕" + }, "settings_toolbar_badge_count": { "message": "工具欄圖示顯示計數徽章" }, diff --git a/scripts/build-ext-demo.js b/scripts/build-ext-demo.js index 076f2d70..93548ade 100644 --- a/scripts/build-ext-demo.js +++ b/scripts/build-ext-demo.js @@ -23,6 +23,7 @@ const defineConfig = { define: { "import.meta.env.BROWSER": JSON.stringify("Safari"), "import.meta.env.NATIVE_APP": JSON.stringify("app"), + "import.meta.env.SAFARI_VERSION": JSON.stringify(15), "import.meta.env.SAFARI_PLATFORM": JSON.stringify( process.env.SAFARI_PLATFORM, ), diff --git a/scripts/build-ext-safari-15.js b/scripts/build-ext-safari-15.js index 70974b22..58a3ce87 100644 --- a/scripts/build-ext-safari-15.js +++ b/scripts/build-ext-safari-15.js @@ -33,6 +33,7 @@ const defineConfig = { define: { "import.meta.env.BROWSER": JSON.stringify("Safari"), "import.meta.env.NATIVE_APP": JSON.stringify("app"), + "import.meta.env.SAFARI_VERSION": JSON.stringify(15), "import.meta.env.SAFARI_PLATFORM": JSON.stringify( process.env.SAFARI_PLATFORM, ), @@ -52,7 +53,7 @@ cp("public/ext/safari-15", SAFARI_EXT_RESOURCES); [ { userscripts: "src/ext/content-scripts/entry-userscripts.js" }, { "dot-user-js": "src/ext/content-scripts/entry-dot-user-js.js" }, - { greasyfork: "src/ext/content-scripts/entry-greasyfork.js" }, + { "script-market": "src/ext/content-scripts/entry-script-market.js" }, ].forEach((input) => { build({ ...defineConfig, diff --git a/scripts/build-ext-safari-16.4.js b/scripts/build-ext-safari-16.4.js index 75321132..919cd5e1 100644 --- a/scripts/build-ext-safari-16.4.js +++ b/scripts/build-ext-safari-16.4.js @@ -32,6 +32,7 @@ const defineConfig = { define: { "import.meta.env.BROWSER": JSON.stringify("Safari"), "import.meta.env.NATIVE_APP": JSON.stringify("app"), + "import.meta.env.SAFARI_VERSION": JSON.stringify(16.4), "import.meta.env.SAFARI_PLATFORM": JSON.stringify( process.env.SAFARI_PLATFORM, ), @@ -51,7 +52,7 @@ cp("public/ext/safari-16.4", SAFARI_EXT_RESOURCES); [ { userscripts: "src/ext/content-scripts/entry-userscripts.js" }, { "dot-user-js": "src/ext/content-scripts/entry-dot-user-js.js" }, - { greasyfork: "src/ext/content-scripts/entry-greasyfork.js" }, + { "script-market": "src/ext/content-scripts/entry-script-market.js" }, ].forEach((input) => { build({ ...defineConfig, diff --git a/scripts/dev-ext-safari.js b/scripts/dev-ext-safari.js index 58fd5c73..27e282e4 100644 --- a/scripts/dev-ext-safari.js +++ b/scripts/dev-ext-safari.js @@ -34,6 +34,7 @@ const defineConfig = { define: { "import.meta.env.BROWSER": JSON.stringify("Safari"), "import.meta.env.NATIVE_APP": JSON.stringify("app"), + "import.meta.env.SAFARI_VERSION": JSON.stringify(16.4), "import.meta.env.SAFARI_PLATFORM": JSON.stringify( process.env.SAFARI_PLATFORM, ), @@ -70,7 +71,7 @@ async function buildResources(server, origin) { [ { userscripts: "src/ext/content-scripts/entry-userscripts.js" }, { "dot-user-js": "src/ext/content-scripts/entry-dot-user-js.js" }, - { greasyfork: "src/ext/content-scripts/entry-greasyfork.js" }, + { "script-market": "src/ext/content-scripts/entry-script-market.js" }, ].forEach((input) => { /** build proxy content scripts replace actual code */ build({ @@ -82,15 +83,19 @@ async function buildResources(server, origin) { const name = id.replace(/.+entry-/, ""); const url = `${origin}/dist/content-scripts/${name}`; return `// proxy content - const xhr = new XMLHttpRequest(); - xhr.open("GET", "${url}", false); - xhr.send(); - const code = xhr.responseText; - try { - Function(code + "//# sourceURL=proxy-${name}")(); - } catch (error) { - console.error(error); - }`; + (function () { + if (window["${id}"]) return; + window["${id}"] = 1; + const xhr = new XMLHttpRequest(); + xhr.open("GET", "${url}", false); + xhr.send(); + const code = xhr.responseText; + try { + Function(code + "//# sourceURL=proxy-${name}")(); + } catch (error) { + console.error(error); + } + })();`; }, }, ], diff --git a/src/ext/action-popup/App.svelte b/src/ext/action-popup/App.svelte index 09bfe195..be597bf1 100644 --- a/src/ext/action-popup/App.svelte +++ b/src/ext/action-popup/App.svelte @@ -37,7 +37,15 @@ let scriptInstalled; let showInstallPrompt; let showInstall; - let installUserscript; // url, content + /** + * @typedef CheckedUserscript + * @prop {import("webextension-polyfill").Tabs.Tab} tab - checked tab + * @prop {URL} url - userjs/usercss url + * @prop {"js"|"css"} type - userscript or userstyle + * @prop {string} content - userjs/usercss content + */ + /** @type {CheckedUserscript} */ + let checkedUserscript; let installViewUserscript; // metadata let installViewUserscriptError; let showAll; @@ -336,15 +344,8 @@ abort = false; } - // check if current page url is a userscript - if (strippedUrl.endsWith(".user.js")) { - // set checking state - scriptChecking = true; - // show checking banner - showInstallPrompt = "checking..."; - // start async check - installCheck(currentTab); - } + // start async check + installCheck(currentTab); loading = false; disabled = false; @@ -389,21 +390,43 @@ }, 25); } + /** + * Check if the current page contains a user script + * @param {import("webextension-polyfill").Tabs.Tab} currentTab + */ async function installCheck(currentTab) { + const tabUrl = new URL(currentTab.url); + /** @type {URL} */ + let url; + // check if current page url is a userscript + if (tabUrl.pathname.endsWith(".user.js")) { + url = tabUrl; + } else { + const res = await browser.tabs.sendMessage( + currentTab.id, + "TAB_CLICK_USERJS", + ); + if (!res) return; + url = new URL(res); + } + // set checking state + scriptChecking = true; + // show checking banner + showInstallPrompt = "checking..."; // refetch script from URL to avoid tampered DOM content let res; // fetch response try { - res = await fetch(currentTab.url); + res = await fetch(url); if (!res.ok) throw new Error(`httpcode-${res.status}`); } catch (error) { console.error("Error fetching .user.js url", error); - errorNotification = "Fetching failed, refresh to retry."; + errorNotification = `Userscript fetching failed (${res.status})`; showInstallPrompt = undefined; return; } const content = await res.text(); // caching script data - installUserscript = { url: currentTab.url, content }; + checkedUserscript = { tab: currentTab, url, type: "js", content }; // send native swift a message, parse metadata and check if installed const response = await sendNativeMessage({ name: "POPUP_INSTALL_CHECK", @@ -423,6 +446,7 @@ showInstallPrompt = response.success; } scriptChecking = false; + scriptInstalled || showInstallView(); } async function showInstallView() { @@ -441,11 +465,11 @@ // go back to main view showInstall = false; // double check before send install message - if (!installUserscript || !installUserscript.content) { + if (!checkedUserscript || !checkedUserscript.content) { errorNotification = "Install failed: userscript missing"; } const currentTab = await browser.tabs.getCurrent(); - if (currentTab.url !== installUserscript.url) { + if (currentTab.id !== checkedUserscript.tab.id) { errorNotification = "Install failed: tab changed unexpectedly"; } if (errorNotification) { @@ -456,7 +480,9 @@ // send native swift a message, which will start the install process const response = await sendNativeMessage({ name: "POPUP_INSTALL_SCRIPT", - content: installUserscript.content, + url: checkedUserscript.url.href, + type: checkedUserscript.type, + content: checkedUserscript.content, }); if (response.error) { errorNotification = response.error; diff --git a/src/ext/background/main.js b/src/ext/background/main.js index 131ab431..2d90f256 100644 --- a/src/ext/background/main.js +++ b/src/ext/background/main.js @@ -1,4 +1,7 @@ -import { openExtensionPage } from "../shared/utils.js"; +import { + contentScriptRegistration, + openExtensionPage, +} from "../shared/utils.js"; import * as settingsStorage from "../shared/settings.js"; import { connectNative, sendNativeMessage } from "../shared/native.js"; @@ -285,7 +288,12 @@ async function nativeChecks() { return true; } -// handles messages sent with browser.runtime.sendMessage +/** + * handles messages sent with browser.runtime.sendMessage + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage#listener} + * @type {Parameters[0]} + * @param {(response: any) => void} sendResponse send a response to the message + */ async function handleMessage(request, sender, sendResponse) { switch (request.name) { case "REQ_USERSCRIPTS": { @@ -469,10 +477,19 @@ async function handleMessage(request, sender, sendResponse) { getContextMenuItems(); break; } + case "WEB_USERJS_POPUP": { + const currentTab = await browser.tabs.getCurrent(); + if (currentTab.id === sender.tab.id) { + browser.browserAction.openPopup(); + } + break; + } } } browser.runtime.onInstalled.addListener(async () => { - nativeChecks(); + await nativeChecks(); + const enable = await settingsStorage.get("augmented_userjs_install"); + await contentScriptRegistration(enable); }); browser.runtime.onStartup.addListener(async () => { setSessionRules(); diff --git a/src/ext/content-scripts/entry-dot-user-js.js b/src/ext/content-scripts/entry-dot-user-js.js index e69de29b..66757ff0 100644 --- a/src/ext/content-scripts/entry-dot-user-js.js +++ b/src/ext/content-scripts/entry-dot-user-js.js @@ -0,0 +1,12 @@ +async function initialize() { + // avoid duplicate injection of content scripts + if (window["CS_ENTRY_DOT_USER_JS"]) return; + window["CS_ENTRY_DOT_USER_JS"] = 1; + // check user settings + const key = "US_AUGMENTED_USERJS_INSTALL"; + if ((await browser.storage.local.get(key))[key] === false) return; + // actual execution content + browser.runtime.sendMessage({ name: "WEB_USERJS_POPUP" }); +} + +initialize(); diff --git a/src/ext/content-scripts/entry-greasyfork.js b/src/ext/content-scripts/entry-greasyfork.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/ext/content-scripts/entry-script-market.js b/src/ext/content-scripts/entry-script-market.js new file mode 100644 index 00000000..67022e47 --- /dev/null +++ b/src/ext/content-scripts/entry-script-market.js @@ -0,0 +1,50 @@ +let url; + +async function injection() { + const tabUrl = new URL(location.href); + let links; + if (tabUrl.hostname.endsWith("greasyfork.org")) { + links = document.querySelectorAll( + '#install-area a.install-link[data-install-format="js"]', + ); + } + for (const link of links) { + link.addEventListener( + "click", + (e) => { + url = link["href"]; + e.stopImmediatePropagation(); + e.preventDefault(); + browser.runtime.sendMessage({ name: "WEB_USERJS_POPUP" }); + }, + true, + ); + } +} + +async function listeners() { + browser.runtime.onMessage.addListener(async (message) => { + if (import.meta.env.MODE === "development") { + console.debug(message, url); + } + if (message === "TAB_CLICK_USERJS") { + const response = url; + url = undefined; // respond only once of click event + return response; + } + }); +} + +async function initialize() { + // avoid duplicate injection of content scripts + if (window["CS_ENTRY_SCRIPT_MARKET"]) return; + window["CS_ENTRY_SCRIPT_MARKET"] = 1; + // check user settings + const key = "US_AUGMENTED_USERJS_INSTALL"; + if ((await browser.storage.local.get(key))[key] === false) return; + // actual execution content + injection(); + listeners(); +} + +initialize(); diff --git a/src/ext/content-scripts/entry-userscripts.js b/src/ext/content-scripts/entry-userscripts.js index 94ca0633..6bf6b971 100644 --- a/src/ext/content-scripts/entry-userscripts.js +++ b/src/ext/content-scripts/entry-userscripts.js @@ -251,9 +251,13 @@ function listeners() { } async function initialize() { - const results = await browser.storage.local.get("US_GLOBAL_ACTIVE"); - if (results?.US_GLOBAL_ACTIVE === false) - return console.info("Userscripts off"); + // avoid duplicate injection of content scripts + if (window["CS_ENTRY_USERSCRIPTS"]) return; + window["CS_ENTRY_USERSCRIPTS"] = 1; + // check user settings + const key = "US_GLOBAL_ACTIVE"; + const results = await browser.storage.local.get(key); + if (results[key] === false) return console.info("Userscripts off"); // start the injection process and add the listeners injection(); listeners(); diff --git a/src/ext/extension-page/store.js b/src/ext/extension-page/store.js index 6c94a35f..48a9359c 100644 --- a/src/ext/extension-page/store.js +++ b/src/ext/extension-page/store.js @@ -1,5 +1,5 @@ import { writable } from "svelte/store"; -import { uniqueId } from "../shared/utils.js"; +import { uniqueId, contentScriptRegistration } from "../shared/utils.js"; import * as settingsStorage from "../shared/settings.js"; import { sendNativeMessage } from "../shared/native.js"; @@ -162,6 +162,10 @@ function settingsStore() { update((settings) => ((settings[key] = value), settings)); // save settings to persistence storage settingsStorage.set({ [key]: value }); + // make specific changes take effect + if (key === "augmented_userjs_install") { + contentScriptRegistration(value); + } // legacy updates updateSingleSettingOld(key, value); }; diff --git a/src/ext/shared/settings.js b/src/ext/shared/settings.js index 223faf01..0b3c4c28 100644 --- a/src/ext/shared/settings.js +++ b/src/ext/shared/settings.js @@ -125,23 +125,30 @@ const settingsDefinition = /** @type {const} */ [ nodeType: "Toggle", }, { - name: "toolbar_badge_count", + name: "global_active", type: "boolean", + local: true, default: true, - platforms: { macos: true, ipados: false, ios: false }, group: "general", - legacy: "showCount", + legacy: "active", nodeType: "Toggle", + nodeClass: { warn: false }, }, { - name: "global_active", + name: "augmented_userjs_install", type: "boolean", - local: true, default: true, group: "general", - legacy: "active", nodeType: "Toggle", - nodeClass: { warn: false }, + }, + { + name: "toolbar_badge_count", + type: "boolean", + default: true, + platforms: { macos: true, ipados: false, ios: false }, + group: "general", + legacy: "showCount", + nodeType: "Toggle", }, { name: "scripts_settings", diff --git a/src/ext/shared/utils.js b/src/ext/shared/utils.js index 208030e5..96b76af3 100644 --- a/src/ext/shared/utils.js +++ b/src/ext/shared/utils.js @@ -467,3 +467,48 @@ async function downloadToFile_old(filename, content, type = "text/plain") { browser.tabs.onRemoved.addListener(handleRemoved); } } + +/** + * Dynamic registration of content scripts [Safari >= 16.4] + * @param {boolean} enable - whether to register content scripts + */ +export async function contentScriptRegistration(enable) { + if (import.meta.env.SAFARI_VERSION < 16.4) return; + /** + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/RegisteredContentScript} + * @type {Parameters[0]} + */ + const scripts = [ + { + id: "dot-user-js", + js: ["dist/content-scripts/dot-user-js.js"], + matches: ["*://*/*.user.js", "*://*/*.user.js?*"], + runAt: "document_start", + }, + { + id: "script-market", + js: ["dist/content-scripts/script-market.js"], + matches: ["*://*.greasyfork.org/*"], + excludeMatches: ["*://*/*.user.js", "*://*/*.user.js?*"], + runAt: "document_end", + }, + ]; + const regScripts = await browser.scripting.getRegisteredContentScripts(); + const ids = regScripts.map((script) => script.id); + for (const script of scripts) { + if (enable && !ids.includes(script.id)) { + await browser.scripting.registerContentScripts([script]); + if (import.meta.env.MODE === "development") { + console.debug("registerContentScripts", script.id); + } + } + if (!enable && ids.includes(script.id)) { + await browser.scripting.unregisterContentScripts({ + ids: [script.id], + }); + if (import.meta.env.MODE === "development") { + console.debug("unregisterContentScripts", script.id); + } + } + } +} diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 7465ed8f..477c8c75 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -1942,7 +1942,7 @@ func installCheck(_ content: String) -> [String: Any] { ]; } -func installUserscript(_ content: String) -> [String: Any] { +func installUserscript(_ url: String, _ type: String, _ content: String) -> [String: Any] { guard let parsed = parse(content), let metadata = parsed["metadata"] as? [String: [String]], @@ -1952,8 +1952,8 @@ func installUserscript(_ content: String) -> [String: Any] { return ["error": "installUserscript failed at (1)"] } let name = sanitize(n) - let filename = "\(name).user.js" + let filename = "\(name).user.\(type)" - let saved = saveFile(["filename": filename, "type": "js"], content) + let saved = saveFile(["filename": filename, "type": type], content) return saved } diff --git a/xcode/Ext-Safari/SafariWebExtensionHandler.swift b/xcode/Ext-Safari/SafariWebExtensionHandler.swift index 0dbb99cb..413d76fe 100644 --- a/xcode/Ext-Safari/SafariWebExtensionHandler.swift +++ b/xcode/Ext-Safari/SafariWebExtensionHandler.swift @@ -179,8 +179,12 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } } else if name == "POPUP_INSTALL_SCRIPT" { - if let content = message?["content"] as? String { - response.userInfo = [SFExtensionMessageKey: installUserscript(content)] + if + let url = message?["url"] as? String, + let type = message?["type"] as? String, + let content = message?["content"] as? String + { + response.userInfo = [SFExtensionMessageKey: installUserscript(url, type, content)] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content (2)"]] }