Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 74 additions & 46 deletions src/popup/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -139,6 +142,9 @@

function refreshView() {
errorNotification = undefined;
scriptChecking = undefined;
scriptInstalled = undefined;
showInstallPrompt = undefined;
loading = true;
disabled = true;
items = [];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -494,8 +512,14 @@
<!-- <div class="warn" bind:this={warn}>Injection disabled</div> -->
{/if}
{#if showInstallPrompt}
<div class="warn" bind:this={warn}>
Userscript Detected: <span on:click={showInstallView}>{showInstallPrompt}</span>
<div class="warn" class:done={scriptInstalled} bind:this={warn}>
Userscript
{#if scriptChecking}
{showInstallPrompt}
{:else}
{scriptInstalled ? "Installed" : "Detected"}:
<span on:click={showInstallView}>{showInstallPrompt}</span>
{/if}
</div>
{/if}
{#if errorNotification}
Expand Down Expand Up @@ -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);
}
Expand Down
22 changes: 11 additions & 11 deletions src/shared/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
30 changes: 14 additions & 16 deletions xcode/Safari-Extension/Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
9 changes: 0 additions & 9 deletions xcode/Safari-Extension/Resources/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 1 addition & 32 deletions xcode/Safari-Extension/Resources/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 10 additions & 20 deletions xcode/Safari-Extension/SafariWebExtensionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down