diff --git a/README.md b/README.md index f0f51a5c..2c145c10 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Userscripts Safari currently supports the following userscript metadata: - `@name` - This will be the name that displays in the sidebar and be used as the filename - you can *not* use the same name for multiple files of the same type - `@description`- Use this to describe what your userscript does - this will be displayed in the sidebar - there is a setting to hide descriptions +- `@icon` - This doesn't have a function with this userscript manager, but the **first value** provided in the metadata will be accessible in the `GM_/GM.info` object - `@match` - Domain match patterns - you can use several instances of this field if you'd like multiple domain matches - view [this article for more information on constructing patterns](https://developer.chrome.com/extensions/match_patterns) - **Note:** this extension only supports `http/s` - `@exclude-match` - Domain patterns where you do *not* want the script to run @@ -191,6 +192,11 @@ Userscripts currently supports the following api methods. All methods are asynch - on success returns a promise resolved with an object indicating success - `GM.listValues()` - on success returns a promise resolved with an array of the key names of **presently set** values +- `GM.getTab()` + - on success returns a promise resolved with `Any` data that is persistent as long as this tab is open +- `GM.saveTab(tabObj)` + - `tabObj: Any` + - on success returns a promise resolved with an object indicating success - `GM.openInTab(url, openInBackground)` - `url: String`, `openInBackground: Bool` - on success returns a promise resolved with the [tab data](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab) for the tab just opened diff --git a/extension/Userscripts Extension/Functions.swift b/extension/Userscripts Extension/Functions.swift index 85d08bd4..2e25b6ba 100644 --- a/extension/Userscripts Extension/Functions.swift +++ b/extension/Userscripts Extension/Functions.swift @@ -1094,13 +1094,18 @@ func match(_ ptcl: String,_ host: String,_ path: String,_ matchPattern: String) if (ptcl != "http:" && ptcl != "https:") { return false } - let partsPattern = #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*)(\/[^\s]*)$"# + let partsPattern = #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*|[a-z0-9]+)(\/[^\s]*)$"# let partsPatternReg = try! NSRegularExpression(pattern: partsPattern, options: .caseInsensitive) let range = NSMakeRange(0, matchPattern.utf16.count) guard let parts = partsPatternReg.firstMatch(in: matchPattern, options: [], range: range) else { err("malformed regex match pattern") return false } + // ensure url protocol matches pattern protocol + let protocolPattern = matchPattern[Range(parts.range(at: 1), in: matchPattern)!] + if (protocolPattern != "*:" && ptcl != protocolPattern) { + return false + } // construct host regex from matchPattern let matchPatternHost = matchPattern[Range(parts.range(at: 2), in: matchPattern)!] var hostPattern = "^\(matchPatternHost.replacingOccurrences(of: ".", with: "\\."))$" @@ -1294,6 +1299,7 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { let description = metadata["description"]?[0] ?? "" let excludes = metadata["exclude"] ?? [] let excludeMatches = metadata["exclude-match"] ?? [] + let icon = metadata["icon"]?[0] ?? "" let includes = metadata["include"] ?? [] let matches = metadata["match"] ?? [] let requires = metadata["require"] ?? [] @@ -1305,6 +1311,7 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { "exclude-match": excludeMatches, "filename": filename, "grants": grants, + "icon": icon, "includes": includes, "inject-into": injectInto, "matches": matches, diff --git a/extension/Userscripts Extension/Resources/background.js b/extension/Userscripts Extension/Resources/background.js index f6290b83..c0b10409 100644 --- a/extension/Userscripts Extension/Resources/background.js +++ b/extension/Userscripts Extension/Resources/background.js @@ -158,6 +158,53 @@ const apis = { window.postMessage({id: US_uid, name: "API_SET_CLIPBOARD", data: data, type: type}); return undefined; }, + US_getTab() { + const pid = Math.random().toString(36).substring(1, 9); + return new Promise(resolve => { + const callback = e => { + if ( + e.data.pid !== pid + || e.data.id !== US_uid + || e.data.name !== "RESP_GET_TAB" + || e.data.filename !== US_filename + ) return; + const response = e.data.response; + resolve(response); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({ + id: US_uid, + pid: pid, + name: "API_GET_TAB", + filename: US_filename + }); + }); + }, + US_saveTab(tab) { + const pid = Math.random().toString(36).substring(1, 9); + return new Promise(resolve => { + const callback = e => { + if ( + e.data.pid !== pid + || e.data.id !== US_uid + || e.data.name !== "RESP_SAVE_TAB" + || e.data.filename !== US_filename + ) return; + const response = e.data.response; + resolve(response); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({ + id: US_uid, + pid: pid, + name: "API_SAVE_TAB", + filename: US_filename, + tab: tab + }); + }); + }, // when xhr is called it sends a message to the content script // and adds it's own event listener to get responses from content script // each xhr has a unique id so it won't respond to different xhr @@ -207,7 +254,7 @@ const apis = { } else if (name.includes("READYSTATECHANGE") && details.onreadystatechange) { details.onreadystatechange(response); } else if (name.includes("LOADSTART") && details.onloadstart) { - details.onloadtstart(response); + details.onloadstart(response); } else if (name.includes("ABORT") && details.onabort) { details.onabort(response); } else if (name.includes("ERROR") && details.onerror) { @@ -348,6 +395,7 @@ function addApis({userscripts, uid, scriptHandler, scriptHandlerVersion}) { const userscript = userscripts[i]; const filename = userscript.scriptObject.filename; const grants = userscript.scriptObject.grants; + const injectInto = userscript.scriptObject["inject-into"]; // prepare the api string let api = `const US_uid = "${uid}";\nconst US_filename = "${filename}";`; // all scripts get access to US_info / GM./GM_info, prepare that object @@ -361,7 +409,23 @@ function addApis({userscripts, uid, scriptHandler, scriptHandlerVersion}) { api += "\nconst GM_info = US_info;"; gmMethods.push("info: US_info"); // if @grant explicitly set to none, empty grants array - if (grants.includes("none")) grants.length = 0; + if (grants.includes("none")) { + grants.length = 0; + } + // @grant exist for page scoped userscript + if (grants.length && injectInto === "page") { + // remove grants + grants.length = 0; + // provide warning for content script + userscript.warning = `${filename} @grant values changed due to @inject-into value`; + } + // @grant exist for auto scoped userscript + if (grants.length && injectInto === "auto") { + // change scope + userscript.scriptObject["inject-into"] = "content"; + // provide warning for content script + userscript.warning = `${filename} @inject-into value changed due to @grant values`; + } // loop through each @grant for the userscript, add methods as needed for (let j = 0; j < grants.length; j++) { const grant = grants[j]; @@ -405,6 +469,14 @@ function addApis({userscripts, uid, scriptHandler, scriptHandlerVersion}) { api += `\n${apis.US_setClipboardSync}`; api += "\nconst GM_setClipboard = US_setClipboardSync;"; break; + case "GM.getTab": + api += `\n${apis.US_getTab}`; + gmMethods.push("getTab: US_getTab"); + break; + case "GM.saveTab": + api += `\n${apis.US_saveTab}`; + gmMethods.push("saveTab: US_saveTab"); + break; case "GM_xmlhttpRequest": case "GM.xmlHttpRequest": if (!includedMethods.includes("xhr")) { @@ -713,6 +785,33 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { } break; } + case "API_GET_TAB": { + if (typeof sender.tab !== "undefined") { + let tab = null; + const tabData = sessionStorage.getItem(`tab-${sender.tab.id}`); + try { + // if tabData is null, can still parse it and return that + tab = JSON.parse(tabData); + } catch (error) { + console.error("failed to parse tab data for getTab"); + } + sendResponse(tab == null ? {} : tab); + } else { + console.error("unable to deliver tab due to empty tab id"); + sendResponse(null); + } + break; + } + case "API_SAVE_TAB": { + if (typeof sender.tab !== "undefined" && request.tab) { + sessionStorage.setItem(`tab-${sender.tab.id}`, JSON.stringify(request.tab)); + sendResponse({success: true}); + } else { + console.error("unable to save tab due to empty tab id or bad arg"); + sendResponse(null); + } + break; + } case "USERSCRIPT_INSTALL_00": case "USERSCRIPT_INSTALL_01": case "USERSCRIPT_INSTALL_02": { diff --git a/extension/Userscripts Extension/Resources/content.js b/extension/Userscripts Extension/Resources/content.js index ffc483e7..543d7433 100644 --- a/extension/Userscripts Extension/Resources/content.js +++ b/extension/Userscripts Extension/Resources/content.js @@ -29,6 +29,8 @@ browser.runtime.sendMessage({name: "REQ_USERSCRIPTS", uid: uid}, response => { userscript.scriptObject["inject-into"] = "content"; console.warn(`${userscript.scriptObject.filename} had it's @inject-value automatically set to "content" because it has @grant values - see: https://github.com/quoid/userscripts/issues/252#issuecomment-1136637700`); } + // log warning if provided + if (userscript.warning) console.warn(userscript.warning); processJS( userscript.scriptObject.name, userscript.scriptObject.filename, @@ -245,6 +247,31 @@ function handleApiMessages(e) { window.postMessage(respMessage); }); break; + case "API_GET_TAB": + message = { + name: name, + filename: e.data.filename, + pid: pid + }; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + respMessage.filename = e.data.filename; + window.postMessage(respMessage); + }); + break; + case "API_SAVE_TAB": + message = { + name: name, + filename: e.data.filename, + pid: pid, + tab: e.data.tab + }; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + respMessage.filename = e.data.filename; + window.postMessage(respMessage); + }); + break; case "API_XHR_ABORT_INJ": message = {name: "API_XHR_ABORT_CS", xhrId: e.data.xhrId}; browser.runtime.sendMessage(message); @@ -300,6 +327,8 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { || name === "USERSCRIPT_INSTALL_01" || name === "USERSCRIPT_INSTALL_02" ) { + // only response to top frame messages + if (window !== window.top) return; const types = [ "text/plain", "application/ecmascript", @@ -307,10 +336,14 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { "text/ecmascript", "text/javascript" ]; - if (!document.contentType || types.indexOf(document.contentType) === -1) { + if ( + !document.contentType + || types.indexOf(document.contentType) === -1 + || !document.querySelector("pre") + ) { sendResponse({invalid: true}); } else { - const message = {name: name, content: document.body.innerText}; + const message = {name: name, content: document.querySelector("pre").innerText}; browser.runtime.sendMessage(message, response => { sendResponse(response); }); diff --git a/extension/Userscripts Extension/Resources/manifest.json b/extension/Userscripts Extension/Resources/manifest.json index abfb293a..fa2e3d25 100644 --- a/extension/Userscripts Extension/Resources/manifest.json +++ b/extension/Userscripts Extension/Resources/manifest.json @@ -4,7 +4,7 @@ "default_locale": "en", "name": "__MSG_extension_name__", "description": "__MSG_extension_description__", - "version": "4.2.2", + "version": "4.2.3", "icons": { "48": "images/icon-48.png", "96": "images/icon-96.png", diff --git a/extension/Userscripts Extension/Resources/page.js b/extension/Userscripts Extension/Resources/page.js index ada155d3..0b1358ff 100644 --- a/extension/Userscripts Extension/Resources/page.js +++ b/extension/Userscripts Extension/Resources/page.js @@ -1067,7 +1067,9 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o "GM_addStyle", "GM_info", "GM_setClipboard", - "GM_xmlhttpRequest" + "GM_xmlhttpRequest", + "GM.getTab", + "GM.saveTab" ]); const validKeys = new Set([ @@ -1077,6 +1079,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o "exclude", "exclude-match", "grant", + "icon", "include", "inject-into", "match", @@ -19620,7 +19623,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o attr(div22, "class", "truncate svelte-9f6q4c"); attr(div23, "class", "modal__row saveLocation svelte-9f6q4c"); attr(div24, "class", "blacklist svelte-9f6q4c"); - attr(textarea, "placeholder", "Comma separated domain patterns"); + attr(textarea, "placeholder", "Comma separated list of @match patterns"); attr(textarea, "spellcheck", "false"); textarea.value = /*blacklisted*/ ctx[2]; textarea.disabled = textarea_disabled_value = /*$state*/ ctx[4].includes("blacklist-saving") || /*blacklistSaving*/ ctx[1]; diff --git a/extension/Userscripts.xcodeproj/project.pbxproj b/extension/Userscripts.xcodeproj/project.pbxproj index 43933c31..17959b8e 100644 --- a/extension/Userscripts.xcodeproj/project.pbxproj +++ b/extension/Userscripts.xcodeproj/project.pbxproj @@ -663,7 +663,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "Userscripts-iOS/Userscripts-iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS/Info.plist"; @@ -679,7 +679,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -704,7 +704,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "Userscripts-iOS/Userscripts-iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS/Info.plist"; @@ -720,7 +720,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -743,7 +743,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "Userscripts-iOS Extension/Userscripts-iOS Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS Extension/Info.plist"; @@ -755,7 +755,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -776,7 +776,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "Userscripts-iOS Extension/Userscripts-iOS Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS Extension/Info.plist"; @@ -788,7 +788,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -929,7 +929,7 @@ CODE_SIGN_ENTITLEMENTS = "Userscripts Extension/Userscripts Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 55; + CURRENT_PROJECT_VERSION = 59; DEVELOPMENT_TEAM = J74Q8V8V8N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Userscripts Extension/Info.plist"; @@ -939,7 +939,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.2.2; + MARKETING_VERSION = 4.2.3; PRODUCT_BUNDLE_IDENTIFIER = "com.userscripts.macos.Userscripts-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -953,7 +953,7 @@ CODE_SIGN_ENTITLEMENTS = "Userscripts Extension/Userscripts Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 55; + CURRENT_PROJECT_VERSION = 59; DEVELOPMENT_TEAM = J74Q8V8V8N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Userscripts Extension/Info.plist"; @@ -963,7 +963,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.2.2; + MARKETING_VERSION = 4.2.3; PRODUCT_BUNDLE_IDENTIFIER = "com.userscripts.macos.Userscripts-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -980,7 +980,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 55; + CURRENT_PROJECT_VERSION = 59; DEVELOPMENT_TEAM = J74Q8V8V8N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Userscripts/Info.plist; @@ -989,7 +989,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.2.2; + MARKETING_VERSION = 4.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.userscripts.macos; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1005,7 +1005,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 55; + CURRENT_PROJECT_VERSION = 59; DEVELOPMENT_TEAM = J74Q8V8V8N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Userscripts/Info.plist; @@ -1014,7 +1014,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.2.2; + MARKETING_VERSION = 4.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.userscripts.macos; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/extension/UserscriptsTests/UserscriptsTests.swift b/extension/UserscriptsTests/UserscriptsTests.swift index c3e1a608..e708ecc9 100644 --- a/extension/UserscriptsTests/UserscriptsTests.swift +++ b/extension/UserscriptsTests/UserscriptsTests.swift @@ -118,27 +118,86 @@ class UserscriptsTests: XCTestCase { } func testMatching() throws { - let pattern = "*://www.google.com/*" - let urls = [ - "https://www.google.com/://aa", - "https://www.google.com/preferences?prev=https://www.google.com/", - "https://www.google.com/preferences?prev=", - "https://www.google.com/" - ] + var count = 0 var result = [String]() - for url in urls { - if - let parts = getUrlProps(url), - let ptcl = parts["protocol"], - let host = parts["host"], - let path = parts["pathname"] - { - if match(ptcl, host, path, pattern) { - result.append("1") + let patternDict = [ + "*://*/*": [ + "https://www.bing.com/", + "https://example.org/foo/bar.html", + "https://a.org/some/path/" + ], + "*://*.mozilla.org/*": [ + "http://mozilla.org/", + "https://mozilla.org/", + "https://b.mozilla.org/path/" + ], + "*://www.google.com/*": [ + "https://www.google.com/://aa", + "https://www.google.com/preferences?prev=https://www.google.com/", + "https://www.google.com/preferences?prev=", + "https://www.google.com/" + ], + "*://localhost/*": [ + "http://localhost:8000/", + "https://localhost:3000/foo.html" + ], + "http://127.0.0.1/*": [ + "http://127.0.0.1/", + "http://127.0.0.1/foo/bar.html" + ] + ] + let patternDictFails = [ + "https://www.example.com/*": [ + "file://www.example.com/", + "ftp://www.example.com/", + "ws://www.example.com/", + "http://www.example.com/" + ], + "http://www.example.com/index.html": [ + "http://www.example.com/", + "https://www.example.com/index.html" + ], + "*://localhost/*": [ + "https://localhost.com/", + "ftp://localhost:8080/" + ], + "https://www.example*/*": [ + "https://www.example.com/" + ] + ] + for (pattern, urls) in patternDict { + count = count + urls.count + for url in urls { + if + let parts = getUrlProps(url), + let ptcl = parts["protocol"], + let host = parts["host"], + let path = parts["pathname"] + { + if match(ptcl, host, path, pattern) { + result.append("1") + } } } } - XCTAssert(result.count == urls.count) + for (pattern, urls) in patternDictFails { + // don't increment count since these tests should fail + for url in urls { + if + let parts = getUrlProps(url), + let ptcl = parts["protocol"], + let host = parts["host"], + let path = parts["pathname"] + { + if match(ptcl, host, path, pattern) { + // if these match, results will get an extra element + // and then the test will fail + result.append("1") + } + } + } + } + XCTAssert(result.count == count) } func testPerformanceExample() throws { diff --git a/src/page/utils.js b/src/page/utils.js index c8ff9225..b2d0b660 100644 --- a/src/page/utils.js +++ b/src/page/utils.js @@ -152,7 +152,9 @@ export const validGrants = new Set([ "GM_addStyle", "GM_info", "GM_setClipboard", - "GM_xmlhttpRequest" + "GM_xmlhttpRequest", + "GM.getTab", + "GM.saveTab" ]); export const validKeys = new Set([ @@ -162,6 +164,7 @@ export const validKeys = new Set([ "exclude", "exclude-match", "grant", + "icon", "include", "inject-into", "match",