diff --git a/src/popup/App.svelte b/src/popup/App.svelte index 085394cd..9a9d57ec 100644 --- a/src/popup/App.svelte +++ b/src/popup/App.svelte @@ -31,9 +31,12 @@ let header; let warn; let err; + let scriptChecking; + let scriptInstalled; let showInstallPrompt; let showInstall; - let installViewUserscript; + let installUserscript; // url, content + let installViewUserscript; // metadata let installViewUserscriptError; let showAll; let allItems = []; @@ -139,6 +142,9 @@ function refreshView() { errorNotification = undefined; + scriptChecking = undefined; + scriptInstalled = undefined; + showInstallPrompt = undefined; loading = true; disabled = true; items = []; @@ -333,26 +339,12 @@ // check if current page url is a userscript if (strippedUrl.endsWith(".user.js")) { - // if it does, send message to content script - // context script will check the document contentType - // if it's not an applicable type, it'll return {invalid: true} response and no install prompt shown - // if the contentType is applicable, what is mentioned below happens - // content script will get dom content, and send it to the bg page - // the bg page will send the content to the swift side for parsing - // when swift side parses and returns, the bg page will send a response to the content script - // then the content script will send response to the popup - // Content scripts that are injected into web content cannot send messages to the native app - // https://developer.apple.com/documentation/safariservices/safari_web_extensions/messaging_between_the_app_and_javascript_in_a_safari_web_extension - const response = await browser.tabs.sendMessage(currentTab.id, {name: "USERSCRIPT_INSTALL_00"}); - if (response.error) { - console.error(`Error checking .user.js url: ${response.error}`); - errorNotification = response.error; - } else if (!response.invalid) { - // the response will contain the string to display - // ex: {success: "Click to install"} - const prompt = response.success; - showInstallPrompt = prompt; - } + // set checking state + scriptChecking = true; + // show checking banner + showInstallPrompt = "checking..."; + // start async check + installCheck(currentTab); } loading = false; @@ -398,43 +390,69 @@ }, 25); } - async function showInstallView() { - // disable all buttons - disabled = true; - // show the install view - showInstall = true; - // get the active tab - const currentTab = await browser.tabs.getCurrent(); - // send content script a message on the active tab - const response = await browser.tabs.sendMessage(currentTab.id, {name: "USERSCRIPT_INSTALL_01"}); - // when above message is sent, content script will get active tab's stringified dom content - // and then send that content and a message to the bg page - // the bg page will send a message and the content to the swift side for parsing - // swift side will parse then send a response to the bg page - // the bg page will then send the response to the content script - // the content script will then send a response here - - // if the response includes an error, display it in the view + async function installCheck(currentTab) { + // refetch script from URL to avoid tampered DOM content + const res = await fetch(currentTab.url); + if (!res.ok) { + console.error(`Error fetching .user.js url: httpcode-${res.status}`); + errorNotification = `Fetching failed, refresh to retry. (${res.status})`; + showInstallPrompt = undefined; + return; + } + const content = await res.text(); + // caching script data + installUserscript = {url: currentTab.url, content}; + // send native swift a message, parse metadata and check if installed + const response = await browser.runtime.sendNativeMessage({name: "POPUP_INSTALL_CHECK", content}); + console.info("POPUP_INSTALL_CHECK:", response); if (response.error) { - console.error(`Can not install userscript: ${response.error}`); + console.error(`Error checking .user.js url: ${response.error}`); + // errorNotification = response.error; installViewUserscriptError = response.error; } else { - installViewUserscript = response; + scriptInstalled = response.installed; + // caching script metadata + installViewUserscript = response.metadata; + // the response will contain the string to display + // ex: {success: "Click to install"} + showInstallPrompt = response.success; } - disabled = false; + scriptChecking = false; + } + + async function showInstallView() { + // show the install view + showInstall = true; } async function installConfirm() { + // clear all banner during installation + errorNotification = undefined; + showInstallPrompt = undefined; // disabled all buttons disabled = true; // show loading element loading = true; // go back to main view showInstall = false; - // get the active tab + // double check before send install message + if (!installUserscript || !installUserscript.content) { + errorNotification = "install failed: userscript missing"; + } const currentTab = await browser.tabs.getCurrent(); - // send content script a message on the active tab, which will start the install process - const response = await browser.tabs.sendMessage(currentTab.id, {name: "USERSCRIPT_INSTALL_02"}); + if (currentTab.url !== installUserscript.url) { + errorNotification = "install failed: tab changed unexpectedly"; + } + if (errorNotification) { + disabled = false; + loading = false; + return; + } + // send native swift a message, which will start the install process + const response = await browser.runtime.sendNativeMessage({ + name: "POPUP_INSTALL_SCRIPT", + content: installUserscript.content + }); if (response.error) { errorNotification = response.error; disabled = false; @@ -494,8 +512,14 @@ {/if} {#if showInstallPrompt} -
- Userscript Detected: {showInstallPrompt} +
+ Userscript + {#if scriptChecking} + {showInstallPrompt} + {:else} + {scriptInstalled ? "Installed" : "Detected"}: + {showInstallPrompt} + {/if}
{/if} {#if errorNotification} @@ -650,6 +674,10 @@ background-color: var(--color-yellow); } + .warn.done { + background-color: var(--color-green); + } + .warn span { border-bottom: 1px dotted var(--color-bg-secondary); } diff --git a/src/shared/dev.js b/src/shared/dev.js index 5a2c0eae..b376101d 100644 --- a/src/shared/dev.js +++ b/src/shared/dev.js @@ -392,19 +392,19 @@ const _browser = { }, sendMessage(tabId, message, responseCallback) { let response = {}; - if (message.name === "USERSCRIPT_INSTALL_00") { - response = {success: "Click to install"}; - // response.error = "something went wrong"; - } else if (message.name === "USERSCRIPT_INSTALL_01") { + if (message.name === "POPUP_INSTALL_CHECK") { response = { - description: "This userscript re-implements the \"View Image\" and \"Search by image\" buttons into google images.", - grant: ["GM.getValue", "GM.setValue", "GM.xmlHttpRequest"], - match: ["https://www.example.com/*", "https://www.example.com/somethingReallylong/goesRightHere"], - name: "Test Install Userscript", - require: ["https://code.jquery.com/jquery-3.5.1.min.js", "https://code.jquery.com/jquery-1.7.1.min.js"], - source: "https://greasyforx.org/scripts/00000-something-something-long-name/code/Something%20something%20long20name.user.js" + success: "Click to install (test)", + metadata: { + description: "This userscript re-implements the \"View Image\" and \"Search by image\" buttons into google images.", + grant: ["GM.getValue", "GM.setValue", "GM.xmlHttpRequest"], + match: ["https://www.example.com/*", "https://www.example.com/somethingReallylong/goesRightHere"], + name: "Test Install Userscript", + require: ["https://code.jquery.com/jquery-3.5.1.min.js", "https://code.jquery.com/jquery-1.7.1.min.js"], + source: "https://greasyforx.org/scripts/00000-something-something-long-name/code/Something%20something%20long20name.user.js" + } }; - // response = {error: "a userscript with this @name value already exists, @name needs to be unique"}; + // response.error = "something went wrong (dev)"; } if (!responseCallback) { return new Promise(resolve => { diff --git a/xcode/Safari-Extension/Functions.swift b/xcode/Safari-Extension/Functions.swift index f7b20659..1e06aaef 100644 --- a/xcode/Safari-Extension/Functions.swift +++ b/xcode/Safari-Extension/Functions.swift @@ -1966,12 +1966,12 @@ func popupInit() -> [String: String]? { } // userscript install -func installCheck(_ content: String) -> [String: String]? { +func installCheck(_ content: String) -> [String: Any] { // this func checks a userscript's metadata to determine if it's already installed guard let files = getAllFiles() else { err("installCheck failed at (1)") - return nil + return ["error": "installCheck failed at (1)"] } guard @@ -2002,30 +2002,28 @@ func installCheck(_ content: String) -> [String: String]? { #endif if names.contains(newName) { - return ["success": "\(directive) to re-install"] + return [ + "success": "\(directive) to re-install", + "metadata": metadata, + "installed": true + ] } - return ["success": "\(directive) to install"]; -} - -func installParse(_ content: String) -> [String: Any]? { - guard - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]] - else { - return ["error": "userscript metadata is invalid"] - } - return metadata + return [ + "success": "\(directive) to install", + "metadata": metadata, + "installed": false + ]; } -func installUserscript(_ content: String) -> [String: Any]? { +func installUserscript(_ content: String) -> [String: Any] { guard let parsed = parse(content), let metadata = parsed["metadata"] as? [String: [String]], let n = metadata["name"]?[0] else { err("installUserscript failed at (1)") - return nil + return ["error": "installUserscript failed at (1)"] } let name = sanitize(n) let filename = "\(name).user.js" diff --git a/xcode/Safari-Extension/Resources/background.js b/xcode/Safari-Extension/Resources/background.js index 7f7726d0..3b7d243f 100644 --- a/xcode/Safari-Extension/Resources/background.js +++ b/xcode/Safari-Extension/Resources/background.js @@ -436,15 +436,6 @@ function handleMessage(request, sender, sendResponse) { } break; } - case "USERSCRIPT_INSTALL_00": - case "USERSCRIPT_INSTALL_01": - case "USERSCRIPT_INSTALL_02": { - const message = {name: request.name, content: request.content}; - browser.runtime.sendNativeMessage(message, response => { - sendResponse(response); - }); - return true; - } case "REFRESH_SESSION_RULES": { setSessionRules(); break; diff --git a/xcode/Safari-Extension/Resources/content.js b/xcode/Safari-Extension/Resources/content.js index f1dcdf41..85c62f9a 100644 --- a/xcode/Safari-Extension/Resources/content.js +++ b/xcode/Safari-Extension/Resources/content.js @@ -429,38 +429,7 @@ function listeners() { // listens for messages from background, popup, etc... browser.runtime.onMessage.addListener((request, sender, sendResponse) => { const name = request.name; - if ( - name === "USERSCRIPT_INSTALL_00" - || name === "USERSCRIPT_INSTALL_01" - || name === "USERSCRIPT_INSTALL_02" - ) { - // only respond to top frame messages - if (window !== window.top) return; - const types = [ - "text/plain", - "application/ecmascript", - "application/javascript", - "text/ecmascript", - "text/javascript" - ]; - if ( - !document.contentType - || types.indexOf(document.contentType) === -1 - || !document.querySelector("pre") - ) { - sendResponse({invalid: true}); - } else { - const message = { - name, - content: - document.querySelector("pre").innerText - }; - browser.runtime.sendMessage(message, response => { - sendResponse(response); - }); - return true; - } - } else if (name === "CONTEXT_RUN") { + if (name === "CONTEXT_RUN") { // from bg script when context-menu item is clicked // double check to ensure context-menu scripts only run in top windows if (window !== window.top) return; diff --git a/xcode/Safari-Extension/SafariWebExtensionHandler.swift b/xcode/Safari-Extension/SafariWebExtensionHandler.swift index 94db00f4..4805adf0 100644 --- a/xcode/Safari-Extension/SafariWebExtensionHandler.swift +++ b/xcode/Safari-Extension/SafariWebExtensionHandler.swift @@ -179,28 +179,18 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } #endif } - else if name == "USERSCRIPT_INSTALL_00" { - if - let content = message?["content"] as? String, - let reply = installCheck(content) - { - response.userInfo = [SFExtensionMessageKey: reply] - } - } - else if name == "USERSCRIPT_INSTALL_01" { - if - let content = message?["content"] as? String, - let reply = installParse(content) - { - response.userInfo = [SFExtensionMessageKey: reply] + else if name == "POPUP_INSTALL_CHECK" { + if let content = message?["content"] as? String { + response.userInfo = [SFExtensionMessageKey: installCheck(content)] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content"]] } } - else if name == "USERSCRIPT_INSTALL_02" { - if - let content = message?["content"] as? String, - let reply = installUserscript(content) - { - response.userInfo = [SFExtensionMessageKey: reply] + else if name == "POPUP_INSTALL_SCRIPT" { + if let content = message?["content"] as? String { + response.userInfo = [SFExtensionMessageKey: installUserscript(content)] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content (2)"]] } } else if name == "PAGE_INIT_DATA" {