Skip to content

Commit

Permalink
Merge pull request #138 from quoid/v3.2.0
Browse files Browse the repository at this point in the history
V3.2.0
  • Loading branch information
quoid authored Jun 9, 2021
2 parents 3ca6c93 + 2c8445b commit ca8408a
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 89 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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
- `@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
- `@include` - An alias for `@match` - functions exactly like `@match`
- `@exclude` - An alias for `@exclude-match` - functions exactly like `@exclude-match`
Expand Down Expand Up @@ -146,6 +147,14 @@ Once the host app is open, you will see a button called "Change save location".

## FAQs

**"Refused to execute a script" error(s), what should I do!?**

> You are seeing this error because of the website's [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). Currently there is no way to allow extension content scripts to bypass CSPs in Safari.
>
> Automatically, the extension will attempt to circumvent strict CSPs, but if you are still experiencing issues, trying setting the userscript metadata key/val `// @inject-into auto` or `// @inject-into content`.
>
> You can read more about this in [this issue](https://github.com/quoid/userscripts/issues/106#issuecomment-797320450).
**Do I need to use the extension's editor to create new userscripts or to edit existing?**

> You can use your own editor to update and manage your files. As long as you are saving the files to the save location, and they are properly formatted, they should be injected. However, you **must open the extension page** beforehand. That means, if you create a new userscript and save it to the save location, before injection will occur properly, the extension page must be open by clicking the extension button in Safari.
Expand Down
107 changes: 94 additions & 13 deletions extension/Userscripts Extension/Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,6 @@ func unsanitize(_ str: String) -> String {
return s
}

func patternMatch(_ string: String,_ pattern: String) -> Bool {
let predicate = NSPredicate(format: "self LIKE %@", pattern)
return !NSArray(object: string).filtered(using: predicate).isEmpty
}

func normalizeWeight(_ weight: String) -> String {
if let w = Int(weight) {
if w > 999 {
Expand Down Expand Up @@ -261,7 +256,7 @@ let defaultSettings = [
"lint": "false",
"log": "false",
"sortOrder": "lastModifiedDesc",
"showCount": "false",
"showCount": "true",
"showInvisibles": "true",
"tabSize": "4"
]
Expand Down Expand Up @@ -603,7 +598,7 @@ func getInitData() -> [String: Any]? {
}
}

// check if default save location directory exists, if not create it
// check if default require location directory exists, if not create it
if !FileManager.default.fileExists(atPath: requireLocation.path) {
do {
try FileManager.default.createDirectory(at: requireLocation, withIntermediateDirectories: false)
Expand Down Expand Up @@ -1022,15 +1017,25 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri
}

// injection
func getMatchedFiles(_ url: String) -> [String]? {
func getMatchedFiles(_ location: [String: Any]) -> [String]? {
// get the manifest data
guard
let manifestKeys = getManifestKeys(),
let active = manifestKeys.settings["active"]
else {
err("could not read manifest when attempting to get page script count")
err("could not read manifest when attempting to get matched files")
return nil
}
// get the protocol, host, pathname
guard
let ptcl = location["protocol"] as? String,
let host = location["host"] as? String,
let path = location["pathname"] as? String
else {
err("could not get values from location object when attempting to get matched files")
return nil
}

// domains where loading is excluded for file
var excludedFilenames:[String] = []
// when code is loaded from a file, it's filename will be populated in the below array, to avoid duplication
Expand All @@ -1048,7 +1053,7 @@ func getMatchedFiles(_ url: String) -> [String]? {
// url matches a pattern in blacklist
// essentially all scripts are disabled, there are 0 active scripts for url
for pattern in manifestKeys.blacklist {
if patternMatch(url, pattern) {
if match(ptcl, host, path, pattern) {
return matchedFilenames
}
}
Expand All @@ -1059,7 +1064,7 @@ func getMatchedFiles(_ url: String) -> [String]? {
// loop through exclude patterns and see if any match against page url
for pattern in excludePatterns {
// if pattern matches page url, add filenames from page url to excludes array, code from those filenames won't be loaded
if patternMatch(url, pattern) {
if match(ptcl, host, path, pattern) {
guard let filenames = manifestKeys.exclude[pattern] else {
err("error parsing manifest.keys when attempting to get code for injected script")
continue
Expand All @@ -1074,7 +1079,7 @@ func getMatchedFiles(_ url: String) -> [String]? {

// loop through all match patterns from manifest to see if they match against the current page url
for pattern in matchPatterns {
if patternMatch(url, pattern) {
if match(ptcl, host, path, pattern) {
// the filenames listed for the pattern that match page url
guard let filenames = manifestKeys.match[pattern] else {
err("error parsing manifestKets.match when attempting to get code for injected script")
Expand Down Expand Up @@ -1205,6 +1210,77 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String:
return allFiles
}

// matching
func getURLProps(_ url: String) -> [String: String]? {
let pattern = #"^(.*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+))(\/.*)?$"#
let regex = try! NSRegularExpression(pattern: pattern, options: .caseInsensitive)
guard
let result = regex.firstMatch(in: url, options: [], range: NSMakeRange(0, url.utf16.count)),
let ptclRange = Range(result.range(at: 1), in: url),
let hostRange = Range(result.range(at: 2), in: url)
else {
return nil
}
let ptcl = String(url[ptclRange])
let host = String(url[hostRange])
var path = "/"
if let pathRange = Range(result.range(at: 3), in: url) {
path = String(url[pathRange])
}
return ["protocol": ptcl, "host": host, "pathname": path]
}

func stringToRegex(_ stringPattern: String) -> NSRegularExpression? {
let pattern = #"[\.|\?|\^|\$|\+|\{|\}|\[|\]|\||\\(|\)|\/]"#
var patternReplace = "^\(stringPattern.replacingOccurrences(of: pattern, with: #"\\$0"#, options: .regularExpression))$"
patternReplace = patternReplace.replacingOccurrences(of: "*", with: ".*")
guard let regex = try? NSRegularExpression(pattern: patternReplace, options: .caseInsensitive) else {
return nil
}
return regex
}

func match(_ ptcl: String,_ host: String,_ path: String,_ matchPattern: String) -> Bool {
// matchPattern is the value from metatdata key @match or @exclude-match
if (matchPattern == "<all_urls>") {
return true
}
// currently only http/s supported
if (ptcl != "http:" && ptcl != "https:") {
return false
}
let partsPattern = #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*)(\/[^\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
}
// construct host regex from matchPattern
let matchPatternHost = matchPattern[Range(parts.range(at: 2), in: matchPattern)!]
var hostPattern = "^\(matchPatternHost.replacingOccurrences(of: ".", with: "\\."))$"
hostPattern = hostPattern.replacingOccurrences(of: "^*$", with: ".*")
hostPattern = hostPattern.replacingOccurrences(of: "*\\.", with: "(.*\\.)?")
guard let hostRegEx = try? NSRegularExpression(pattern: hostPattern, options: .caseInsensitive) else {
err("invalid host regex")
return false
}
// contruct path regex from matchPattern
let matchPatternPath = matchPattern[Range(parts.range(at: 3), in: matchPattern)!]
guard let pathRegEx = stringToRegex(String(matchPatternPath)) else {
err("invalid path regex")
return false
}
guard
(hostRegEx.firstMatch(in: host, options: [], range: NSMakeRange(0, host.utf16.count)) != nil),
(pathRegEx.firstMatch(in: path, options: [], range: NSMakeRange(0, path.utf16.count)) != nil)
else {
return false
}

return true
}

// popover
func updateBadgeCount(_ frames: [[String : Any]]) {
guard
Expand All @@ -1222,7 +1298,12 @@ func updateBadgeCount(_ frames: [[String : Any]]) {
urls.append(url)
}
for url in urls {
guard let m = getMatchedFiles(url) else { return }
guard
let parts = getURLProps(url),
let m = getMatchedFiles(parts)
else {
return
}
// add values not already present in the matched array
matched.append(contentsOf: m.filter{!matched.contains($0)})
}
Expand Down
4 changes: 2 additions & 2 deletions extension/Userscripts Extension/SafariExtensionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
responseName = "RESP_USERSCRIPTS"
if
let data = userInfo,
let url = data["url"] as? String,
let location = data["location"] as? [String: Any],
let isTop = data["top"] as? Bool,
let id = data["id"] as? String,
let matched = getMatchedFiles(url),
let matched = getMatchedFiles(location),
let code = getCode(matched, isTop)
{
responseData = ["code": code, "id": id]
Expand Down
16 changes: 9 additions & 7 deletions extension/Userscripts Extension/UserscriptsSafari.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
let data;
// determines whether strict csp injection has already run (JS only)
let cspFallbackAttempted = 0;
// send the url to swift side for fetching applicable code
const url = window.location.href;
// send the location object to swift side for fetching applicable code
const loc = window.location;
// tell swift side if requester is window.top, needed to filter code for @noframes
const isTop = window === window.top;
// unique id per requester to avoid repeat execution
Expand Down Expand Up @@ -121,10 +121,12 @@ function parseCode(data, fallback = false) {
}

function cspFallback(e) {
const src = e.sourceFile.toUpperCase();
const ext = safari.extension.baseURI.toUpperCase();
// ensure that violation came from the extension
if ((ext.startsWith(src) || src.startsWith(ext))) {
// if a security policy violation event has occurred, and the directive is script-src
// it's fair to assume that there is a strict CSP for scripts
// if there's a strict CSP for scripts, it's unlikely this extension uri is whitelisted
// when any script-src violation is detected, re-attempt injection
// since it's fair to assume injection was blocked for extension's content script
if (e.effectiveDirective === "script-src") {
// get all "auto" code
if (Object.keys(data.js.auto).length != 0 && cspFallbackAttempted < 1) {
let n = {"js": {"auto": {}}};
Expand Down Expand Up @@ -171,4 +173,4 @@ document.addEventListener("securitypolicyviolation", cspFallback);
// event listener to handle messages
safari.self.addEventListener("message", handleMessage);
// request code for page url
safari.extension.dispatchMessage("REQ_USERSCRIPTS", {id: id, top: isTop, url: url});
safari.extension.dispatchMessage("REQ_USERSCRIPTS", {id: id, top: isTop, location: loc});
2 changes: 1 addition & 1 deletion extension/Userscripts Extension/index.html

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions extension/Userscripts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@
CODE_SIGN_ENTITLEMENTS = "Userscripts Extension/Userscripts Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = J74Q8V8V8N;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = "Userscripts Extension/Info.plist";
Expand All @@ -426,7 +426,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.userscripts.macos.Userscripts-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -440,7 +440,7 @@
CODE_SIGN_ENTITLEMENTS = "Userscripts Extension/Userscripts Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = J74Q8V8V8N;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = "Userscripts Extension/Info.plist";
Expand All @@ -449,7 +449,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.userscripts.macos.Userscripts-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -466,15 +466,15 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = J74Q8V8V8N;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = Userscripts/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.userscripts.macos;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand All @@ -490,15 +490,15 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = J74Q8V8V8N;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = Userscripts/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.userscripts.macos;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand Down
6 changes: 3 additions & 3 deletions extension/Userscripts/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
Expand Down Expand Up @@ -143,7 +143,7 @@
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="backgroundColor">
<color key="value" name="systemRedColor" catalog="System" colorSpace="catalog"/>
<color key="value" name="systemYellowColor" catalog="System" colorSpace="catalog"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="wantsLayer" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
Expand Down
Loading

0 comments on commit ca8408a

Please sign in to comment.