diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 4e68479e..00000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -/node_modules/ -/public/build/ -codemirror/ diff --git a/.eslintrc.json b/.eslintrc.json index b33c1c1e..17589967 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,9 +9,7 @@ "ecmaVersion": 12, "sourceType": "module" }, - "plugins": [ - "svelte3" - ], + "plugins": ["svelte3"], "overrides": [ { "files": ["*.svelte"], @@ -22,92 +20,59 @@ "_browser": "writable", "browser": "readonly" }, + "ignorePatterns": ["/node_modules/", "/public/*/build/", "codemirror/"], "rules": { - "array-bracket-spacing": [ - "error", - "never" - ], - "arrow-parens": [ - "error", - "as-needed" - ], - "comma-spacing": [ - "error", - { - "before": false, - "after": true - } - ], + "array-bracket-spacing": ["error", "never"], + "arrow-parens": ["error", "as-needed"], + "brace-style": ["error", "1tbs", {"allowSingleLine": false}], + "comma-dangle": ["error", "never"], + "comma-spacing": ["error", { + "before": false, + "after": true + }], + "curly": ["error", "multi-line"], "dot-notation": "error", "eqeqeq": ["error", "smart"], - "indent": [ - "error", - 4, - { - "SwitchCase": 1 - } - ], - "linebreak-style": [ - "error", - "unix" - ], - "keyword-spacing": [ - "error", - { - "after": true, - "before": true - } - ], + "indent": ["error", 4, {"SwitchCase": 1}], + "linebreak-style": ["error","unix"], + "key-spacing": ["error", {"afterColon": true}], + "keyword-spacing": ["error", { + "after": true, + "before": true + }], "no-bitwise": "error", "no-mixed-operators": "error", - "no-unused-vars": [ - "error", - { - "args": "none" - } - ], - "no-use-before-define": [ - "error", - { - "classes": true, - "functions": false, - "variables": true - } - ], + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": ["error", {"max": 1, "maxBOF": 2}], // https://github.com/sveltejs/eslint-plugin-svelte3/issues/41 + "no-tabs": "error", + "no-useless-concat": "error", + "no-unused-vars": ["error", {"args": "none"}], + "no-use-before-define": ["error", { + "classes": true, + "functions": false, + "variables": true + }], "no-var": "error", - "object-curly-spacing": [ - "error", - "never" - ], - "operator-assignment": [ - "error", - "always" - ], - "operator-linebreak": [ - "error", - "before" - ], + "nonblock-statement-body-position": ["error", "below", { + "overrides": {"if": "any"} + }], + "object-curly-spacing": ["error", "never"], + "operator-assignment": ["error", "always"], + "operator-linebreak": ["error", "before"], "prefer-const": "error", - "quotes": [ - "error", - "double" - ], - "semi": [ - "error", - "always", - { - "omitLastInOneLineBlock": true - } - ], + "prefer-template": "error", + "quotes": ["error", "double"], + "semi": ["error", "always", {"omitLastInOneLineBlock": true}], + "space-before-blocks": "error", "space-before-function-paren": ["error", { "anonymous": "never", "named": "never", "asyncArrow": "always" }], - "space-in-parens": [ - "error", - "never" - ], + "space-in-parens": ["error", "never"], + "template-curly-spacing": ["error", "never"], + "space-infix-ops": "error", "jsx-a11y/a11y-missing-attribute": "off" } } diff --git a/.stylelintrc.json b/.stylelintrc.json index 30769c11..4f559d3a 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -3,21 +3,20 @@ "stylelint-config-standard", "stylelint-config-html/svelte" ], + "ignoreFiles": ["**/codemirror/*", "**/reset.css", "**/*.js"], "rules": { - "comment-empty-line-before": [ - "always", - { - "ignore": ["after-comment", "stylelint-commands"] - } - ], + "alpha-value-notation": "number", + "comment-empty-line-before": ["always",{ + "ignore": ["after-comment", "stylelint-commands"] + }], + "custom-property-empty-line-before": null, "indentation": 4, + "max-empty-lines": 1, "no-descending-specificity": null, + "property-no-vendor-prefix": null, "selector-class-pattern": null, - "selector-pseudo-class-no-unknown": [ - true, - { - "ignorePseudoClasses": ["global"] - } - ] + "selector-pseudo-class-no-unknown": [true, { + "ignorePseudoClasses": ["global"] + }] } } diff --git a/extension/Userscripts Extension/Functions.swift b/extension/Userscripts Extension/Functions.swift index 3b2e80a0..57534e8c 100644 --- a/extension/Userscripts Extension/Functions.swift +++ b/extension/Userscripts Extension/Functions.swift @@ -246,6 +246,7 @@ func parse(_ content: String) -> [String: Any]? { // manifest struct Manifest: Codable { var blacklist:[String] + var declarativeNetRequest: [String] var disabled:[String] var exclude: [String: [String]] var excludeMatch: [String: [String]] @@ -254,7 +255,7 @@ struct Manifest: Codable { var require: [String: [String]] var settings: [String: String] private enum CodingKeys : String, CodingKey { - case blacklist, disabled, exclude, excludeMatch = "exclude-match", include, match, require, settings + case blacklist, declarativeNetRequest, disabled, exclude, excludeMatch = "exclude-match", include, match, require, settings } } @@ -301,6 +302,7 @@ func getManifest() -> Manifest { // create new manifest with default key/vals let manifest = Manifest( blacklist: [], + declarativeNetRequest: [], disabled: [], exclude: [:], excludeMatch: [:], @@ -318,7 +320,7 @@ func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { logText("updateManifestMatches started") // only get all files if files were not provided var files = [[String: Any]]() - if optionalFilesArray.count < 1 { + if optionalFilesArray.isEmpty { guard let getFiles = getAllFiles() else {return false} files = getFiles } else { @@ -329,6 +331,11 @@ func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { // can be force unwrapped because getAllFiles didn't return nil let metadata = file["metadata"] as! [String: [String]] let filename = file["filename"] as! String + // skip request type userscripts + let runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + continue + } // populate excludes & matches var excludeMatched = [String]() var matched = [String]() @@ -346,6 +353,14 @@ func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if metadata["exclude"] != nil { excluded.append(contentsOf: metadata["exclude"]!) } + // if in declarativeNetRequest array, remove it + if manifest.declarativeNetRequest.contains(filename) { + if let index = manifest.declarativeNetRequest.firstIndex(of: filename) { + manifest.declarativeNetRequest.remove(at: index) + } else { + err("failed to remove \(filename) from declarativeNetRequest array") + } + } // update manifest values manifest.excludeMatch = updatePatternDict(filename, excludeMatched, manifest.excludeMatch) @@ -411,7 +426,7 @@ func updatePatternDict(_ filename: String, _ filePatterns: [String], _ manifestK // remove filename from array by index returnDictionary[pattern]?.remove(at: ind!) // if filename was the last item in array, remove the url pattern from dictionary - if returnDictionary[pattern]!.count < 1 { + if returnDictionary[pattern]!.isEmpty { returnDictionary.removeValue(forKey: pattern) } } @@ -423,7 +438,7 @@ func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool logText("updateManifestRequired started") // only get all files if files were not provided var files = [[String: Any]]() - if optionalFilesArray.count < 1 { + if optionalFilesArray.isEmpty { guard let getFiles = getAllFiles() else { logText("updateManifestRequired count not get files") return false @@ -458,7 +473,7 @@ func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool // if there are values, write them to manifest // if failed to write to manifest, continue to next file & log error - if r.count > 0 && r != manifest.require[filename] { + if !r.isEmpty && r != manifest.require[filename] { manifest.require[filename] = r if !updateManifest(with: manifest) { err("couldn't update manifest when getting required resources") @@ -470,13 +485,92 @@ func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool return true } +func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] = []) -> Bool { + logText("updateManifestDeclarativeNetRequests started") + var files = [[String: Any]]() + if optionalFilesArray.isEmpty { + guard let getFiles = getAllFiles() else { + err("updateManifestDeclarativeNetRequests failed at (1)") + return false + } + files = getFiles + } else { + files = optionalFilesArray + } + var manifest = getManifest() + for file in files { + // can be force unwrapped because getAllFiles didn't return nil + // and getAllFiles always returns the following + let metadata = file["metadata"] as! [String: [String]] + let filename = file["filename"] as! String + let runAt = metadata["run-at"]?[0] ?? "document-end" + // if not a request type, ignore + if runAt != "request" { + continue + } + var update = false + // if filename already in manifest + if !manifest.declarativeNetRequest.contains(filename) { + manifest.declarativeNetRequest.append(filename) + update = true + } + // if filename in another array remove it + for (pattern, filenames) in manifest.match { + for fn in filenames { + if fn == filename, let index = manifest.match[pattern]?.firstIndex(of: filename) { + manifest.match[pattern]?.remove(at: index) + update = true + } else { + err("updateManifestDeclarativeNetRequests failed at (2), \(filename)") + } + } + } + for (pattern, filenames) in manifest.excludeMatch { + for fn in filenames { + if fn == filename, let index = manifest.excludeMatch[pattern]?.firstIndex(of: filename) { + manifest.excludeMatch[pattern]?.remove(at: index) + update = true + } else { + err("updateManifestDeclarativeNetRequests failed at (3), \(filename)") + } + } + } + for (pattern, filenames) in manifest.include { + for fn in filenames { + if fn == filename, let index = manifest.include[pattern]?.firstIndex(of: filename) { + manifest.include[pattern]?.remove(at: index) + update = true + } else { + err("updateManifestDeclarativeNetRequests failed at (4), \(filename)") + } + } + } + for (pattern, filenames) in manifest.exclude { + for fn in filenames { + if fn == filename, let index = manifest.exclude[pattern]?.firstIndex(of: filename) { + manifest.exclude[pattern]?.remove(at: index) + update = true + } else { + err("updateManifestDeclarativeNetRequests failed at (5), \(filename)") + } + } + } + if update, !updateManifest(with: manifest) { + err("updateManifestDeclarativeNetRequests failed at (6)") + return false + } + } + logText("updateManifestDeclarativeNetRequests complete") + return true +} + func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { logText("purgeManifest started") // purge all manifest keys of any stale entries var update = false, manifest = getManifest(), allSaveLocationFilenames = [String]() // only get all files if files were not provided var allFiles = [[String: Any]]() - if optionalFilesArray.count < 1 { + if optionalFilesArray.isEmpty { // if getAllFiles fails to return, ignore and pass an empty array let getFiles = getAllFiles() ?? [] allFiles = getFiles @@ -583,6 +677,16 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { } } } + // loop through manifest declarativeNetRequest + for filename in manifest.declarativeNetRequest { + if !allSaveLocationFilenames.contains(filename) { + if let index = manifest.declarativeNetRequest.firstIndex(of: filename) { + manifest.declarativeNetRequest.remove(at: index) + update = true + logText("Could not find \(filename) in save location, removed from declarativeNetRequest") + } + } + } // remove obsolete settings for setting in manifest.settings { if !defaultSettings.keys.contains(setting.key) { @@ -630,7 +734,7 @@ func updateSettings(_ settings: [String: String]) -> Bool { } // files -func getAllFiles() -> [[String: Any]]? { +func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { // returns all files of proper type with filenames, metadata & more var files = [[String: Any]]() let fm = FileManager.default @@ -673,9 +777,14 @@ func getAllFiles() -> [[String: Any]]? { fileData["filename"] = filename fileData["lastModified"] = dateToMilliseconds(dateMod) fileData["metadata"] = metadata - // for unwrap name since parse ensure it exists + // force unwrap name since parser ensure it exists fileData["name"] = metadata["name"]![0] fileData["type"] = "\(type)" + // add extra data if a request userscript + let runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + fileData["request"] = true + } if metadata["description"] != nil { fileData["description"] = metadata["description"]![0] } @@ -683,6 +792,11 @@ func getAllFiles() -> [[String: Any]]? { fileData["canUpdate"] = true } fileData["noframes"] = metadata["noframes"] != nil ? true : false + // if asked, also return the code string + if (includeCode) { + // can force unwrap because always returned from parser + fileData["code"] = parsed["code"] as! String + } files.append(fileData) } logText("getAllFiles completed") @@ -692,7 +806,7 @@ func getAllFiles() -> [[String: Any]]? { func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: String) -> Bool { let directory = getRequireLocation().appendingPathComponent(filename) // if file requires no resource but directory exists, trash it - if resources.count < 1 && FileManager.default.fileExists(atPath: directory.path) { + if resources.isEmpty && FileManager.default.fileExists(atPath: directory.path) { do { try FileManager.default.trashItem(at: directory, resultingItemURL: nil) } catch { @@ -737,7 +851,7 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) -> [[String: String]]? { // only get all files if files were not provided var files = [[String: Any]]() - if optionalFilesArray.count < 1 { + if optionalFilesArray.isEmpty { guard let getFiles = getAllFiles() else { err("checkForRemoteUpdates failed at (1)") return nil @@ -828,7 +942,7 @@ func getRemoteFileContents(_ url: String) -> String? { } // if made it to this point and contents still an empty string, something went wrong with the request - if contents.count < 1 { + if contents.isEmpty { logText("getRemoteFileContents failed at (4), contents empty, \(url)") return nil } @@ -933,22 +1047,15 @@ func checkDefaultDirectories() -> Bool { // 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) + let parts = URLComponents(string: url), + let ptcl = parts.scheme, + let host = parts.host else { + err("failed to parse url in getUrlProps") 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, "href": url] + return ["protocol": "\(ptcl):", "host": host, "pathname": parts.path, "href": url] } func stringToRegex(_ stringPattern: String) -> NSRegularExpression? { @@ -1024,9 +1131,9 @@ func include(_ url: String,_ pattern: String) -> Bool { return true } -func getMatchedFiles(_ url: String) -> [String] { +func getMatchedFiles(_ url: String, _ optionalManifest: Manifest?, _ checkBlocklist: Bool) -> [String] { logText("Getting matched files for \(url)") - let manifest = getManifest() + let manifest = optionalManifest ?? getManifest() guard let parts = getUrlProps(url), let ptcl = parts["protocol"], @@ -1037,113 +1144,86 @@ func getMatchedFiles(_ url: String) -> [String] { err("getMatchedFiles failed at (1) for \(url)") return [String]() } - // 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 - var matchedFilenames:[String] = [] + + // filenames that should not load for the passed url + // the manifest values from @exclude and @exclude-match populate this set + var excludedFilenames: Set = [] + // filenames that should load for the passed url + // the manifest values from @include and @match populate this set + var matchedFilenames: Set = [] // all exclude-match patterns from manifest let excludeMatchPatterns = manifest.excludeMatch.keys // all match patterns from manifest let matchPatterns = manifest.match.keys - // all include patterns from manifest + // all include expressions from manifest let includeExpressions = manifest.include.keys - // all exclude patterns from manifest + // all exclude expressions from manifest let excludeExpressions = manifest.exclude.keys + + // if url matches a pattern in blocklist, no injection for this url + if (checkBlocklist) { + for pattern in manifest.blacklist { + if match(ptcl, host, path, pattern) { + // return empty array + return Array(matchedFilenames) + } + } + } - // loop through exclude patterns and see if any match against page url + // loop through all the @exclude-match patterns + // if any match passed url, push all filenames to excludedFilenames set for pattern in excludeMatchPatterns { - // if pattern matches page url, add filenames from page url to excludes array, code from those filenames won't be loaded if match(ptcl, host, path, pattern) { guard let filenames = manifest.excludeMatch[pattern] else { - err("getMatchedFiles failed at (2)") + err("getMatchedFiles failed at (2) for \(pattern)") continue } - for filename in filenames { - if !excludedFilenames.contains(filename) { - excludedFilenames.append(filename) - } - } + excludedFilenames = excludedFilenames.union(filenames) } } - // loop through exclude expressions and check for matches for exp in excludeExpressions { if include(href, exp) { guard let filenames = manifest.exclude[exp] else { - err("getMatchedFiles failed at (3)") + err("getMatchedFiles failed at (3) for \(exp)") continue } - for filename in filenames { - if !excludedFilenames.contains(filename) { - excludedFilenames.append(filename) - } - } + excludedFilenames = excludedFilenames.union(filenames) } } - // loop through all match patterns from manifest to see if they match against the current page url for pattern in matchPatterns { if match(ptcl, host, path, pattern) { - // the filenames listed for the pattern that match page url guard let filenames = manifest.match[pattern] else { - err("getMatchedFiles failed at (4)") + err("getMatchedFiles failed at (4) for \(pattern)") continue } - // loop through matched filenames and populate matchedFilenames array - for filename in filenames { - // don't push to array if filename is in excludes or filename already exists in matchedFilenames array (to avoid duplication) - if !excludedFilenames.contains(filename) && !matchedFilenames.contains(filename) { - matchedFilenames.append(filename) - } - } + matchedFilenames = matchedFilenames.union(filenames) } } - // loop through include expressions and check for matches for exp in includeExpressions { if include(href, exp) { guard let filenames = manifest.include[exp] else { - err("getMatchedFiles failed at (5)") + err("getMatchedFiles failed at (5) for \(exp)") continue } - for filename in filenames { - if !excludedFilenames.contains(filename) && !matchedFilenames.contains(filename) { - matchedFilenames.append(filename) - } - } + matchedFilenames = matchedFilenames.union(filenames) } } + matchedFilenames = matchedFilenames.subtracting(excludedFilenames) logText("Got \(matchedFilenames.count) matched files for \(url)") - return matchedFilenames + return Array(matchedFilenames) } // injection -// func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String: Any]]]? { func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { - //var allFiles = [String: [String: [String: Any]]]() - var allFiles = [String: Any]() - var cssFiles = [String:[String:String]]() - var jsFiles = [String: [String: [String: [String: Any]]]]() - jsFiles["auto"] = ["document-start": [:], "document-end": [:], "document-idle": [:]] - jsFiles["content"] = ["document-start": [:], "document-end": [:], "document-idle": [:]] - jsFiles["page"] = ["document-start": [:], "document-end": [:], "document-idle": [:]] - jsFiles["context-menu"] = ["auto": [:], "content": [:], "page": [:]] - var auto_docStart = [String: [String: Any]]() - var auto_docEnd = [String: [String: Any]]() - var auto_docIdle = [String: [String: Any]]() - var content_docStart = [String: [String: Any]]() - var content_docEnd = [String: [String: Any]]() - var content_docIdle = [String: [String: Any]]() - var page_docStart = [String: [String: Any]]() - var page_docEnd = [String: [String: Any]]() - var page_docIdle = [String: [String: Any]]() - - var auto_context_scripts = [String: [String: Any]]() - var content_context_scripts = [String: [String: Any]]() - var page_context_scripts = [String: [String: Any]]() - + var cssFiles = [Any]() + var jsFiles = [Any]() + var menuFiles = [Any]() + guard let saveLocation = getSaveLocation() else { err("getCode failed at (1)") return nil } - + for filename in filenames { guard let contents = getFileContentsParsed(saveLocation.appendingPathComponent(filename)), @@ -1163,16 +1243,21 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { continue } + // get run-at values and set default if missing + // if type request, ignore + var runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + continue + } + // normalize weight var weight = metadata["weight"]?[0] ?? "1" weight = normalizeWeight(weight) - // get inject-into and run-at values - // if either is missing, use default value + // get inject-into and set default if missing var injectInto = metadata["inject-into"]?[0] ?? "auto" - var runAt = metadata["run-at"]?[0] ?? "document-end" - let injectVals = ["auto", "content", "page"] - let runAtVals = ["context-menu", "document-start", "document-end", "document-idle"] + let injectVals: Set = ["auto", "content", "page"] + let runAtVals: Set = ["context-menu", "document-start", "document-end", "document-idle"] // if either is invalid use default value if !injectVals.contains(injectInto) { injectInto = "auto" @@ -1184,7 +1269,9 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { // attempt to get all @grant value var grants = metadata["grant"] ?? [] // remove duplicates, if any exist - grants = Array(Set(grants)) + if !grants.isEmpty { + grants = Array(Set(grants)) + } // set GM.info data let description = metadata["description"]?[0] ?? "" @@ -1199,7 +1286,8 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { "description": description, "excludes": excludes, "exclude-match": excludeMatches, - "grant": grants, + "filename": filename, + "grants": grants, "includes": includes, "inject-into": injectInto, "matches": matches, @@ -1227,7 +1315,7 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { let value = metaline.value // metalines without values aren't included in parsed metadata object // the only exception is @noframes - scriptObject[key] = value.count > 1 ? value : value[0] + scriptObject[key] = !value.isEmpty ? value : value[0] } } let scriptMetaStr = contents["metablock"] as? String ?? "??" @@ -1250,73 +1338,41 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { } if type == "css" { - cssFiles[filename] = ["code": code, "weight": weight, "name": name] - } else if type == "js" { - let data = [ + cssFiles.append([ "code": code, - "weight": weight, - "scriptMetaStr": scriptMetaStr, - "scriptObject": scriptObject - ] as [String : Any] - // add file data to appropriate dict - if injectInto == "auto" && runAt == "document-start" { - auto_docStart[filename] = data - } else if injectInto == "auto" && runAt == "document-end" { - auto_docEnd[filename] = data - } else if injectInto == "auto" && runAt == "document-idle" { - auto_docIdle[filename] = data - } else if injectInto == "content" && runAt == "document-start" { - content_docStart[filename] = data - } else if injectInto == "content" && runAt == "document-end" { - content_docEnd[filename] = data - } else if injectInto == "content" && runAt == "document-idle" { - content_docIdle[filename] = data - } else if injectInto == "page" && runAt == "document-start" { - page_docStart[filename] = data - } else if injectInto == "page" && runAt == "document-end" { - page_docEnd[filename] = data - } else if injectInto == "page" && runAt == "document-idle" { - page_docIdle[filename] = data - } - if runAt == "context-menu" && injectInto == "auto" { - auto_context_scripts[filename] = data - } - if runAt == "context-menu" && injectInto == "content" { - content_context_scripts[filename] = data - } - if runAt == "context-menu" && injectInto == "page" { - page_context_scripts[filename] = data + "filename": filename, + "name": name, + "type": "css", + "weight": weight + ]) + } else if type == "js" { + if runAt == "context-menu" { + #if os(macOS) + menuFiles.append([ + "code": code, + "scriptMetaStr": scriptMetaStr, + "scriptObject": scriptObject, + "type": "js", + "weight": weight + ]) + #endif + } else { + jsFiles.append([ + "code": code, + "scriptMetaStr": scriptMetaStr, + "scriptObject": scriptObject, + "type": "js", + "weight": weight + ]) } } } - - // construct the js specific dictionaries - jsFiles["auto"]!["document-start"] = auto_docStart - jsFiles["auto"]!["document-end"] = auto_docEnd - jsFiles["auto"]!["document-idle"] = auto_docIdle - jsFiles["content"]!["document-start"] = content_docStart - jsFiles["content"]!["document-end"] = content_docEnd - jsFiles["content"]!["document-idle"] = content_docIdle - jsFiles["page"]!["document-start"] = page_docStart - jsFiles["page"]!["document-end"] = page_docEnd - jsFiles["page"]!["document-idle"] = page_docIdle - // the context-menu dictionaries are constructed differently - // they will need to be handled in a unique way on the JS side - jsFiles["context-menu"]!["auto"] = auto_context_scripts - jsFiles["context-menu"]!["content"] = content_context_scripts - jsFiles["context-menu"]!["page"] = page_context_scripts - - // construct the returned dictionary - allFiles["css"] = cssFiles - allFiles["js"] = jsFiles - - // add global values - let scriptHandler = "Userscripts" - let scriptHandlerVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" - allFiles["scriptHandler"] = scriptHandler - allFiles["scriptHandlerVersion"] = scriptHandlerVersion - - return allFiles + let resp = [ + "files": ["css": cssFiles, "js": jsFiles, "menu": menuFiles], + "scriptHandler": "Userscripts", + "scriptHandlerVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" + ] as [String : Any] + return resp } func getFileContentsParsed(_ url: URL) -> [String: Any]? { @@ -1343,14 +1399,8 @@ func getFileContentsParsed(_ url: URL) -> [String: Any]? { func getInjectionFilenames(_ url: String) -> [String]? { var filenames = [String]() let manifest = getManifest() - let matched = getMatchedFiles(url) - guard - let active = manifest.settings["active"], - let parts = getUrlProps(url), - let ptcl = parts["protocol"], - let host = parts["host"], - let path = parts["pathname"] - else { + let matched = getMatchedFiles(url, manifest, true) + guard let active = manifest.settings["active"] else { err("getInjectionFilenames failed at (1)") return nil } @@ -1358,18 +1408,87 @@ func getInjectionFilenames(_ url: String) -> [String]? { if active != "true" { return filenames } - // url matches a pattern in blacklist, no injection for this url - // return empty array - for pattern in manifest.blacklist { - if match(ptcl, host, path, pattern) { - return filenames - } - } // filter out all disabled files filenames = matched.filter{!manifest.disabled.contains($0)} return filenames } +func getRequestScripts() -> [[String: String]]? { + var requestScripts = [[String: String]]() + // check the manifest to see if injection is enabled + let manifest = getManifest() + guard let active = manifest.settings["active"] else { + err("getRequestScripts failed at (1)") + return nil + } + // if not enabled, do not apply any net requests, ie. return empty array + if active != "true" { + return requestScripts + } + guard let files = getAllFiles(includeCode: true) else { + err("getRequestScripts failed at (2)") + return nil + } + for file in files { + let isRequest = file["request"] as? Bool ?? false + // skip any non-request userscripts + if !isRequest { + continue + } + // can be force unwrapped because getAllFiles always returns these + let name = file["name"] as! String + let code = file["code"] as! String + let filename = file["filename"] as! String + + if !manifest.disabled.contains(filename) { + requestScripts.append(["name": name, "code": code]) + } + } + return requestScripts +} + +func getContextMenuScripts() -> [String: Any]? { + var menuFilenames = [String]() + // check the manifest to see if injection is enabled + let manifest = getManifest() + guard let active = manifest.settings["active"] else { + err("getContextMenuScripts failed at (1)") + return nil + } + // if not enabled return empty array + if active != "true" { + return ["files": ["menu": []]] + } + // get all files at save location + guard let files = getAllFiles() else { + err("getContextMenuScripts failed at (2)") + return nil + } + // loop through files and find @run-at context-menu script filenames + for file in files { + if + let fileMetadata = file["metadata"] as? [String: [String]], + let filename = file["filename"] as? String + { + let runAt = fileMetadata["run-at"]?[0] ?? "document-end" + if runAt != "context-menu" { + continue + } + if !manifest.disabled.contains(filename) { + menuFilenames.append(filename) + } + } else { + err("getContextMenuScripts failed at (3), couldn't get metadata for \(file)") + } + } + // get and return script objects for all context-menu scripts + guard let scripts = getCode(menuFilenames, true) else { + err("getContextMenuScripts failed at (4)") + return nil + } + return scripts +} + // popup func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { var matches = [[String: Any]]() @@ -1378,7 +1497,7 @@ func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]] return matches } // get all the files saved to manifest that match the passed url - let matched = getMatchedFiles(url) + let matched = getMatchedFiles(url, nil, false) // get all the files at the save location guard let files = getAllFiles() @@ -1397,7 +1516,7 @@ func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]] let frameUrls = subframeUrls.filter{$0 != url} // for each url just pushed to frameUrls, get all the files saved to manifest that match their url for frameUrl in frameUrls { - let frameMatches = getMatchedFiles(frameUrl) + let frameMatches = getMatchedFiles(frameUrl, nil, false) for frameMatch in frameMatches { // for the match against the frameUrl, see if it has @noframes // if so, it should not be appended to frameUrlsMatches @@ -1405,7 +1524,7 @@ func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]] // can force unwrap filename b/c getAllFiles always returns it let frameMatchMetadata = files.filter{$0["filename"] as! String == frameMatch}.first // can force unwrap noframes b/c getAllFiles always returns it - let noFrames = frameMatchMetadata!["noframes"] != nil ? true : false + let noFrames = frameMatchMetadata?["noframes"] as? Bool ?? false if !matched.contains(frameMatch) && !noFrames { frameUrlsMatches.append(frameMatch) } @@ -1621,7 +1740,13 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { } // update manifest for new file and purge anything from old file - guard updateManifestMatches(), updateManifestRequired(), purgeManifest() else { + guard + let allFiles = getAllFiles(), + updateManifestMatches(allFiles), + updateManifestRequired(allFiles), + updateManifestDeclarativeNetRequests(allFiles), + purgeManifest(allFiles) + else { err("saveFile failed at (4)") return ["error": "file save but manifest couldn't be updated"] } @@ -1641,6 +1766,11 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { if metadata["version"] != nil && metadata["updateURL"] != nil { response["canUpdate"] = true } + // if a request "type" userscript add key/val + let runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + response["request"] = true + } return response } @@ -1745,6 +1875,8 @@ func popupInit() -> [String: String]? { let updateManifestMatches = updateManifestMatches(allFiles) // update the required resources let updateManifestRequired = updateManifestRequired(allFiles) + // update declarativeNetRequest + let updateDeclarativeNetRequest = updateManifestDeclarativeNetRequests(allFiles) // verbose error checking if !checkDefaultDirectories { err("Failed to checkDefaultDirectories in popupInit") @@ -1766,6 +1898,10 @@ func popupInit() -> [String: String]? { err("Failed to updateManifestRequired in popupInit") return nil } + if !updateDeclarativeNetRequest { + err("Failed to updateDeclarativeNetRequest in popupInit") + return nil + } let manifest = getManifest() guard let active = manifest.settings["active"] else { err("Failed at getManifest active in popupInit") @@ -1855,47 +1991,3 @@ func installUserscript(_ content: String) -> [String: Any]? { let saved = saveFile(["filename": filename, "type": "js"], content) return saved } -//func popupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { -// var matches = [[String: Any]]() -// // if the url doesn't start with http/s return empty array -// if !url.starts(with: "http://") && !url.starts(with: "https://") { -// return matches -// } -// // get all the files saved to manifest that match the passed url -// let matched = getMatchedFiles(url) -// // get all the files at the save location -// guard let files = getAllFiles() else { -// err("popupMatches failed at (1)") -// return nil -// } -// // filter out the files that are present in both files and matched -// // force unwrap filename to string since getAllFiles always returns it -// matches = files.filter{matched.contains($0["filename"] as! String)} -// -// // get the subframe url matches -// var frameUrlsMatched = [[String: Any]]() -// var frameUrlsMatches = [String]() -// // filter out the top page url from the frame urls -// let frameUrls = subframeUrls.filter{$0 != url} -// // for each url just pushed to frameUrls, get all the files saved to manifest that match their url -// for frameUrl in frameUrls { -// let frameMatches = getMatchedFiles(frameUrl) -// for frameMatch in frameMatches { -// if !matched.contains(frameMatch) { -// frameUrlsMatches.append(frameMatch) -// } -// } -// } -// // filter out the files that are present in both files and frameUrlsMatches -// // force unwrap filename to string since getAllFiles always returns it -// frameUrlsMatched = files.filter{frameUrlsMatches.contains($0["filename"] as! String)} -// // loop through frameUrlsMatched and add subframe key/val -// for (index, var frameUrlsMatch) in frameUrlsMatched.enumerated() { -// frameUrlsMatch["subframe"] = true -// frameUrlsMatched[index] = frameUrlsMatch -// } -// // add frameUrlsMatched to matches array -// matches.append(contentsOf: frameUrlsMatched) -// -// return matches -//} diff --git a/extension/Userscripts Extension/Resources/background.js b/extension/Userscripts Extension/Resources/background.js index fd1fb056..00c74025 100644 --- a/extension/Userscripts Extension/Resources/background.js +++ b/extension/Userscripts Extension/Resources/background.js @@ -1,200 +1,277 @@ -// store all active context menu items here -// this var will be referenced to determine the removal of the context menu click event handler -// it'll also be used to know if a specific url already has active context menu items -let contextMenuItems = []; -let platformGlobal; +// globally scoped vars in a nonpersistent background page is not advised +// however references to active XMLHttpRequests needed to abort requests +// when an xhr is triggered, the bg page will load +// it's probable that while the xhr is active, the bg page will stay loaded +// xhrs are only kept in this array when active, otherwise array is empty +// that means it's ok that this var gets reset when the bg page unloads let xhrs = []; -browser.runtime.onMessage.addListener((request, sender, sendResponse) => { - // content script listening seems to be the most reliable way to trigger injection - const name = request.name; - if (name === "REQ_USERSCRIPTS") { - const url = sender.url; - // use frameId to determine if request came from top level window - const isTop = sender.frameId === 0 ? true : false; - // ask swift layer to provide code for current url(s) - const message = {name: name, url: url, isTop: isTop}; - browser.runtime.sendNativeMessage(message, response => { - // send code back to content script for parsing and injection - // could use tabs.executeScript(sender.tab.id) for content context injection - // but for now, will rely on eval() in content script - const code = response.code; - sendResponse({code: code}); - // update badge count on injection - // especially useful when injection is deferred (ie. subframes) - setBadgeCount(); - }); - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage#sending_an_asynchronous_response_using_sendresponse - return true; - } else if (name === "CONTEXT_CREATE") { - const menuItemId = request.menuItemId; - const menuObj = { - contexts: ["all"], - documentUrlPatterns: [request.url], - id: menuItemId, - title: request.title, - }; - const onCreate = () => { - if (browser.runtime.lastError) { - console.error(browser.runtime.lastError); - } else { - sendResponse({menuItemId: menuItemId}); - contextMenuItems.push(menuItemId); - // only add listener if not already attached - if (!browser.menus.onClicked.hasListener(contextClick)) { - browser.menus.onClicked.addListener(contextClick); - } - } - }; - // first check if the context menu item is already present for a tab on the same url - // if so, remove the current context menu item & entry in context menu items array - // although already created context menu items automatically apply - // to subsequent tab urls that match the documentUrlPatterns supplied at creation - // the user could have edited the userscript since the first application - if (contextMenuItems.includes(menuItemId)) { - browser.contextMenus.remove(menuItemId, () => { - contextMenuItems = contextMenuItems.filter(a => a !== menuItemId); - browser.contextMenus.create(menuObj, onCreate); - }); - } else { - browser.contextMenus.create(menuObj, onCreate); - } - return true; - } else if (name === "CONTEXT_REMOVE") { - // tab closes events dispatch remove request with a menuItemId - // remove the context menu item associated with the menuItemId when that event comes in - // if tabs with the same url exist, the context menu item will be recreated on right click - const menuItemId = request.menuItemId; - browser.contextMenus.remove(menuItemId, () => { - contextMenuItems = contextMenuItems.filter(a => a !== menuItemId); - purgeContextMenus(); +/* global US_filename, US_uid */ +// filename and uid will be available to functions at runtime +const apis = { + US_openInTab(url, openInBackground) { + 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_OPEN_TAB") return; + resolve(e.data.response); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + const active = (openInBackground === true) ? false : true; + window.postMessage({id: US_uid, pid: pid, name: "API_OPEN_TAB", url: url, active: active}); }); - } else if (name === "API_OPEN_TAB") { - const active = (request.active === true) ? true : false; - browser.tabs.create({active: active, index: sender.tab.index + 1, url: request.url}, response => { - sendResponse(response); + }, + US_closeTab(tabId) { + 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_CLOSE_TAB") return; + resolve(e.data.response); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({id: US_uid, pid: pid, name: "API_CLOSE_TAB", tabId: tabId}); }); - return true; - } else if (name === "API_CLOSE_TAB") { - const tabId = request.tabId !== undefined ? request.tabId : sender.tab.id; - browser.tabs.remove(tabId, () => { - sendResponse({success: true}); + }, + US_setValue(key, value) { + 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_SET_VALUE" + || e.data.filename !== US_filename + ) return; + resolve(e.data.response); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({ + id: US_uid, + pid: pid, + name: "API_SET_VALUE", + filename: US_filename, + key: key, + value: value + }); }); - return true; - } else if (name === "API_SET_VALUE") { - const item = {}; - item[request.filename + "---" + request.key] = request.value; - browser.storage.local.set(item, () => { - sendResponse({success: true}); + }, + US_getValue(key, defaultValue) { + 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_VALUE" + || 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_VALUE", + filename: US_filename, + key: key, + defaultValue: defaultValue + }); }); - return true; - } else if (name === "API_GET_VALUE") { - const key = request.filename + "---" + request.key; - browser.storage.local.get(key, item => { - if (Object.keys(item).length === 0) { - if (request.defaultValue) { - sendResponse(request.defaultValue); - } else { - sendResponse(`undefined--${request.pid}`); - } - } else { - sendResponse(Object.values(item)[0]); - } + }, + US_listValues() { + 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_LIST_VALUES" + || e.data.filename !== US_filename + ) return; + resolve(e.data.response); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({id: US_uid, pid: pid, name: "API_LIST_VALUES", filename: US_filename}); }); - return true; - } else if (name === "API_DELETE_VALUE") { - const key = request.filename + "---" + request.key; - browser.storage.local.remove(key, response => { - sendResponse({success: true}); + }, + US_deleteValue(key) { + 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_DELETE_VALUE" + || e.data.filename !== US_filename + ) return; + resolve(e.data.response); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({ + id: US_uid, + pid: pid, + name: "API_DELETE_VALUE", + filename: US_filename, + key: key + }); }); - return true; - } else if (name === "API_LIST_VALUES") { - const prefix = request.filename + "---"; - const keys = []; - browser.storage.local.get().then(items => { - for (const key in items) { - if (key.startsWith(prefix)) { - const k = key.replace(prefix, ""); - keys.push(k); - } - } - sendResponse(keys); + }, + US_addStyleSync(css) { + window.postMessage({id: US_uid, name: "API_ADD_STYLE_SYNC", css: css}); + return css; + }, + US_addStyle(css) { + 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_ADD_STYLE") return; + resolve(e.data.response); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({id: US_uid, pid: pid, name: "API_ADD_STYLE", css: css}); }); - return true; - } else if (name === "API_ADD_STYLE" || name === "API_ADD_STYLE_SYNC") { - const tabId = sender.tab.id; - browser.tabs.insertCSS(tabId, {code: request.css}, () => { - if (name === "API_ADD_STYLE") sendResponse(request.css); + }, + US_setClipboard(data, type) { + 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_SET_CLIPBOARD") return; + resolve(e.data.response); + if (!e.data.response) console.error("clipboard write failed"); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({id: US_uid, pid: pid, name: "API_SET_CLIPBOARD", data: data, type: type}); }); - return true; - } else if (name === "REQ_PLATFORM") { - (async () => { - try { - const p = await getPlatform(); - sendResponse({platform: p}); - } catch (error) { - console.error(error); - sendResponse({error: "Couldn't get platform"}); - } - })(); - return true; - } else if (name === "API_SET_CLIPBOARD") { - const result = setClipboard(request.data, request.type); - sendResponse(result); - } else if (name === "API_XHR_CS") { - // https://jsonplaceholder.typicode.com/posts - // get tab id and respond only to the content script that sent message - const tab = sender.tab.id; - const details = request.details; - const method = details.method ? details.method : "GET"; - const user = details.user || null; - const password = details.password || null; - let body = details.data || null; - if (body && details.binary) { - const len = body.length; - const arr = new Uint8Array(len); - for (let i = 0; i < len; i++) { - arr[i] = body.charCodeAt(i); - } - body = new Blob([arr], {type: "text/plain"}); - } - const xhr = new XMLHttpRequest(); - // push to global scoped array so it can be aborted - xhrs.push({xhr: xhr, xhrId: request.xhrId}); - xhr.withCredentials = (details.user && details.password); - xhr.timeout = details.timeout || 0; - if (details.overrideMimeType) xhr.overrideMimeType(details.overrideMimeType); - xhrAddListeners(xhr, tab, request.id, request.xhrId, details); - xhr.open(method, details.url, true, user, password); - xhr.responseType = details.responseType || ""; - if (details.headers) { - for (const key in details.headers) { - const val = details.headers[key]; - xhr.setRequestHeader(key, val); + }, + US_setClipboardSync(data, type) { + // there's actually no sync method since a promise needs to be sent to bg page + // however make a dummy sync method for compatibility + window.postMessage({id: US_uid, name: "API_SET_CLIPBOARD", data: data, type: type}); + return undefined; + }, + // 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 + // the content script sends the xhr details to the background script + // the background script sends messages back to the content script for all xhr events + // the content script relays these messages back to the context where xhr was called + // if xhr was called with event handler functions they will be executed when those relays come in + xhr(details) { + // if details didn't include url, do nothing + if (!details.url) return; + // create unique id for the xhr + const xhrId = Math.random().toString(36).substring(1, 9); + // strip out functions from details, kind of hacky + const detailsParsed = JSON.parse(JSON.stringify(details)); + // check which functions are included in the original details object + // add a bool to indicate if event listeners should be attached + if (details.onabort) detailsParsed.onabort = true; + if (details.onerror) detailsParsed.onerror = true; + if (details.onload) detailsParsed.onload = true; + if (details.onloadend) detailsParsed.onloadend = true; + if (details.onloadstart) detailsParsed.onloadstart = true; + if (details.onprogress) detailsParsed.onprogress = true; + if (details.onreadystatechange) detailsParsed.onreadystatechange = true; + if (details.ontimeout) detailsParsed.ontimeout = true; + // abort function gets returned when this function is called + const abort = () => { + window.postMessage({id: US_uid, name: "API_XHR_ABORT_INJ", xhrId: xhrId}); + }; + const callback = e => { + const name = e.data.name; + const response = e.data.response; + // ensure callback is responding to the proper message + if ( + e.data.id !== US_uid + || e.data.xhrId !== xhrId + || !name + || !name.startsWith("RESP_API_XHR_CS") + ) return; + if (name.includes("LOADEND") && details.onloadend) { + details.onloadend(response); + // remove event listener when xhr is complete + window.removeEventListener("message", callback); + } else if (name === "RESP_API_XHR_CS_LOAD" && details.onload) { + details.onload(response); + } else if (name.includes("PROGRESS") && details.onprogress) { + details.onprogress(response); + } else if (name.includes("READYSTATECHANGE") && details.onreadystatechange) { + details.onreadystatechange(response); + } else if (name.includes("LOADSTART") && details.onloadstart) { + details.onloadtstart(response); + } else if (name.includes("ABORT") && details.onabort) { + details.onabort(response); + } else if (name.includes("ERROR") && details.onerror) { + details.onerror(response); + } else if (name.includes("TIMEOUT") && details.ontimeout) { + details.ontimeout(response); + } else if (name === "RESP_API_XHR_CS") { + // ignore } + }; + window.addEventListener("message", callback); + window.postMessage({id: US_uid, name: "API_XHR_INJ", details: detailsParsed, xhrId: xhrId}); + return {abort: abort}; + } +}; + +function xhrMake(request, sender) { + // XMLHttpRequests are a bit complex and involve multiple messages + // when a userscript with proper @grant executes an XMLHttpRequest (xhr) + // from the context of the userscript (page, content script), the xhr function runs + // a custom message event listener is added to the userscript context and then... + // a message from the userscript is sent to the content script with the details of the xhr + // this is because userscript contexts can't directly message bg page + // further, can't run xhrs from content script due to CORS issues + // the bg page receives this message, initiates the xhr and attaches the needed event listeners to the xhr + // when those event listeners fire, messages are sent back to the content script + // these messages include the details of the xhr + // when the content script receives these messages, it passes them back to the userscript context + // all xhrs send a message onloadend which removes the custom event listener in the userscript context + + // get tab id and respond only to the content script that sent message + const tab = sender.tab.id; + const details = request.details; + const method = details.method ? details.method : "GET"; + const user = details.user || null; + const password = details.password || null; + let body = details.data || null; + if (body && details.binary) { + const len = body.length; + const arr = new Uint8Array(len); + for (let i = 0; i < len; i++) { + arr[i] = body.charCodeAt(i); } - xhr.send(body); - // remove xhr from global scope when completed - xhr.onloadend = progressEvent => xhrs = xhrs.filter(x => x.xhrId !== request.xhrId); - // sendResponse({details: details}); - } else if (name === "API_XHR_ABORT_CS") { - // get the xhrId from request - const xhrId = request.xhrId; - const match = xhrs.find(x => x.xhrId === xhrId); - if (match) { - match.xhr.abort(); - // sendResponse(match); - } else { - console.log(`abort message recieved for ${xhrId}, but it couldn't be found`); + body = new Blob([arr], {type: "text/plain"}); + } + const xhr = new XMLHttpRequest(); + // push to global scoped array so it can be aborted + xhrs.push({xhr: xhr, xhrId: request.xhrId}); + xhr.withCredentials = (details.user && details.password); + xhr.timeout = details.timeout || 0; + if (details.overrideMimeType) xhr.overrideMimeType(details.overrideMimeType); + xhrAddListeners(xhr, tab, request.id, request.xhrId, details); + xhr.open(method, details.url, true, user, password); + xhr.responseType = details.responseType || ""; + if (details.headers) { + for (const key in details.headers) { + const val = details.headers[key]; + xhr.setRequestHeader(key, val); } - } else if (["USERSCRIPT_INSTALL_00", "USERSCRIPT_INSTALL_01", "USERSCRIPT_INSTALL_02"].includes(name)) { - const message = {name: name, content: request.content}; - browser.runtime.sendNativeMessage(message, response => { - sendResponse(response); - }); - return true; } -}); + xhr.send(body); + // remove xhr from global scope when completed + xhr.onloadend = () => xhrs = xhrs.filter(x => x.xhrId !== request.xhrId); +} function xhrHandleEvent(e, xhr, tab, id, xhrId) { const name = `RESP_API_XHR_BG_${e.type.toUpperCase()}`; @@ -210,7 +287,9 @@ function xhrHandleEvent(e, xhr, tab, id, xhrId) { withCredentials: xhr.withCredentials }; // only include responseText when applicable - if (["", "text"].includes(xhr.responseType)) x.responseText = xhr.responseText; + if (xhr.responseType === "" || xhr.responseType === "text") { + x.responseText = xhr.responseText; + } // convert data if response is arraybuffer so sendMessage can pass it if (xhr.responseType === "arraybuffer") { const arr = Array.from(new Uint8Array(xhr.response)); @@ -262,79 +341,90 @@ function xhrAddListeners(xhr, tab, id, xhrId, details) { } } -function contextClick(info, tab) { - browser.tabs.query({currentWindow: true, active: true}, tabs => { - browser.tabs.sendMessage(tabs[0].id, {name: "CONTEXT_RUN", menuItemId: info.menuItemId}); - }); -} - -function purgeContextMenus() { - // loop through all tabs and remove context menu items targeting tab urls that don't exist - browser.tabs.query({}, tabs => { - const tabUrls = []; - tabs.forEach(tab => {if (tab.url) tabUrls.push(tab.url);}); - const contextMenuItemsUrls = []; - // parse urls from context menu item ids - contextMenuItems.forEach(item => contextMenuItemsUrls.push(item.split("&$&")[0])); - // get content menu items targeting urls not in tabUrls - const staleUrls = contextMenuItemsUrls.filter(a => !tabUrls.includes(a)); - // remove stale elements from context menu item array & remove menu item - staleUrls.forEach(staleUrl => { - contextMenuItems.forEach(contextMenuItem => { - if (contextMenuItem.includes(staleUrl)) { - contextMenuItems.splice(contextMenuItems.indexOf(contextMenuItem), 1); - browser.contextMenus.remove(contextMenuItem); - } - }); - }); - // if there are no more context menu items in array remove the event listener - browser.menus.onClicked.removeListener(contextClick); - // remove any lingering context menu items - browser.contextMenus.removeAll(); - }); -} - -async function setBadgeCount() { - // only set badge on macOS - const platform = await getPlatform(); - if (platform !== "macos") return; - - const tabs = await new Promise(resolve => { - browser.tabs.query({currentWindow: true, active: true}, tabs => { - resolve(tabs); - }); - }); - if (!tabs) return; - const url = tabs[0].url; - const message = {name: "POPUP_BADGE_COUNT", url: url, frameUrls: []}; - if (url) { - const frames = await new Promise(resolve => { - browser.webNavigation.getAllFrames({tabId: tabs[0].id}, frames => { - resolve(frames); - }); - }); - frames.forEach(frame => message.frameUrls.push(frame.url)); - } - browser.runtime.sendNativeMessage(message, response => { - if (response.error) return console.error(response.error); - const count = response.count; - if (count > 0) { - browser.browserAction.setBadgeText({text: count.toString()}); - } else { - browser.browserAction.setBadgeText({text: ""}); +function addApis({userscripts, uid, scriptHandler, scriptHandlerVersion}) { + for (let i = 0; i < userscripts.length; i++) { + const gmMethods = []; + const includedMethods = []; + const userscript = userscripts[i]; + const filename = userscript.scriptObject.filename; + const grants = userscript.scriptObject.grants; + // 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 + const scriptData = { + "script": userscript.scriptObject, + "scriptHandler": scriptHandler, + "scriptHandlerVersion": scriptHandlerVersion, + "scriptMetaStr": userscript.scriptMetaStr + }; + api += `\nconst US_info = ${JSON.stringify(scriptData)}`; + api += "\nconst GM_info = US_info;"; + gmMethods.push("info: US_info"); + // loop through each @grant for the userscript, add methods as needed + for (let j = 0; j < grants.length; j++) { + const grant = grants[j]; + switch (grant) { + case "GM.openInTab": + api += `\n${apis.US_openInTab}`; + gmMethods.push("openInTab: US_openInTab"); + break; + case "US_closeTab": + api += `\n${apis.US_closeTab}`; + break; + case "GM.setValue": + api += `\n${apis.US_setValue}`; + gmMethods.push("setValue: US_setValue"); + break; + case "GM.getValue": + api += `\n${apis.US_getValue}`; + gmMethods.push("getValue: US_getValue"); + break; + case "GM.deleteValue": + api += `\n${apis.US_deleteValue}`; + gmMethods.push("deleteValue: US_deleteValue"); + break; + case "GM.listValues": + api += `\n${apis.US_listValues}`; + gmMethods.push("listValues: US_listValues"); + break; + case "GM_addStyle": + api += `\n${apis.US_addStyleSync}`; + api += "\nconst GM_addStyle = US_addStyleSync;"; + break; + case "GM.addStyle": + api += `\n${apis.US_addStyle}`; + gmMethods.push("addStyle: US_addStyle"); + break; + case "GM.setClipboard": + api += `\n${apis.US_setClipboard}`; + gmMethods.push("setClipboard: US_setClipboard"); + break; + case "GM_setClipboard": + api += `\n${apis.US_setClipboardSync}`; + api += "\nconst GM_setClipboard = US_setClipboardSync;"; + break; + case "GM_xmlhttpRequest": + case "GM.xmlHttpRequest": + if (!includedMethods.includes("xhr")) { + api += `\n${apis.xhr}`; + includedMethods.push("xhr"); + } + if (grant === "GM_xmlhttpRequest") { + api += "\nconst GM_xmlhttpRequest = xhr;\n"; + } else { + gmMethods.push("xmlHttpRequest: xhr"); + } + break; + } } - }); -} - -async function getPlatform() { - if (platformGlobal) return platformGlobal; - const response = await browser.runtime.sendNativeMessage({name: "REQ_PLATFORM"}); - if (!response.platform) { - console.error("Failed to get platform"); - return ""; + // make the GM api string + const GM = `const GM = {${gmMethods.join(",")}};`; + // update the final code string + userscript.code = `${api}\n${GM}\n${userscript.code}`; } - platformGlobal = response.platform; - return response.platform; + + // return the updated userscripts + return userscripts; } function setClipboard(data, type = "text/plain") { @@ -364,6 +454,348 @@ function setClipboard(data, type = "text/plain") { } } +async function getPlatform() { + let platform = localStorage.getItem("platform"); + if (!platform) { + const response = await browser.runtime.sendNativeMessage({name: "REQ_PLATFORM"}); + if (!response.platform) { + console.error("Failed to get platform"); + return ""; + } + platform = response.platform; + localStorage.setItem("platform", platform); + } + return platform; +} + +async function setBadgeCount() { + // only set badge on macOS + const platform = await getPlatform(); + if (platform !== "macos") return; + const currentTab = await browser.tabs.getCurrent(); + // no active tabs exist (user closed all windows) + if (!currentTab) return; + const url = currentTab.url; + // if url doesn't exist, stop + if (!url) { + browser.browserAction.setBadgeText({text: ""}); + return; + } + // only check for http/s pages + if (!url.startsWith("http://") && !url.startsWith("https://")) { + browser.browserAction.setBadgeText({text: ""}); + return; + } + const frameUrls = new Set(); + const frames = await browser.webNavigation.getAllFrames({tabId: currentTab.id}); + for (let i = 0; i < frames.length; i++) { + const frameUrl = frames[i].url; + if (frameUrl !== url && frameUrl.startsWith("http")) { + frameUrls.add(frameUrl); + } + } + const message = {name: "POPUP_BADGE_COUNT", url: url, frameUrls: Array.from(frameUrls)}; + browser.runtime.sendNativeMessage(message, response => { + if (response.error) return console.error(response.error); + const count = response.count; + if (count > 0) { + browser.browserAction.setBadgeText({text: count.toString()}); + } else { + browser.browserAction.setBadgeText({text: ""}); + } + }); +} + +async function getContextMenuItems() { + // macos exclusive feature + const platform = await getPlatform(); + if (platform !== "macos") return; + // since it's not possible to get a list of currently active menu items + // on update, all context-menu items are clears, then re-added + // this is done to get fresh code changes to context-menu scripts on certain events + await browser.menus.removeAll(); + // get the context-menu scripts + const response = await browser.runtime.sendNativeMessage({name: "REQ_CONTEXT_MENU_SCRIPTS"}); + if (response.error) { + console.error(response.error); + return; + } + // add menus items + const items = response.files?.menu || []; + if (items.length) { + console.info(`Setting ${items.length} context-menu userscripts`); + } + for (let i = 0; i < items.length; i++) { + const item = items[i]; + // context-menu scripts require @match value + // @include values are ignored + if (!item.scriptObject.matches.length) continue; + addContextMenuItem(item); + } +} + +async function addContextMenuItem(userscript) { + // context-menu items persist for a session + // to avoid duplication, when created, save the filename to session storage + const savedItems = sessionStorage.getItem("menu"); + // if the session storage key doesn't exist use empty array + const activeItems = savedItems ? JSON.parse(savedItems) : []; + if (activeItems.indexOf(userscript.scriptObject.filename) !== -1) { + // if already saved, remove it, to get fresh code changes + await browser.menus.remove(userscript.scriptObject.filename); + } + // potential bug? https://developer.apple.com/forums/thread/685273 + // https://stackoverflow.com/q/68431201 + // parse through match values and change pathnames to deal with bug + const patterns = userscript.scriptObject.matches; + patterns.forEach((pattern, index) => { + try { + const url = new URL(pattern); + let pathname = url.pathname; + if (pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1); + patterns[index] = `${url.protocol}//${url.hostname}${pathname}`; + } catch (error) { + // prevent breaking when non-url pattern present + } + }); + + browser.menus.create({ + contexts: ["all"], + documentUrlPatterns: patterns, + id: userscript.scriptObject.filename, + title: userscript.scriptObject.name + }, () => { + // add event listener if needed + if (!browser.menus.onClicked.hasListener(contextClick)) { + browser.menus.onClicked.addListener(contextClick); + } + // save the context-menu item reference to sessionStorage + sessionStorage.setItem("menu", JSON.stringify([userscript.scriptObject.filename])); + }); +} + +function contextClick(info, tab) { + // when any created context-menu item is clicked, send message to tab + // the content script for that tag will have the context-menu code + // which will get send back in the response if/when found + const message = {name: "CONTEXT_RUN", menuItemId: info.menuItemId}; + browser.tabs.sendMessage(tab.id, message, response => { + // if code is returned, execute on that tab + if (!response.code) return; + browser.tabs.executeScript(tab.id, { + code: response.code + }); + }); +} + +browser.runtime.onMessage.addListener((request, sender, sendResponse) => { + const name = request.name; + switch (name) { + case "REQ_USERSCRIPTS": { + // get the page url from the content script that sent request + const url = sender.url; + // use frameId to determine if request came from top level window + const isTop = sender.frameId === 0 ? true : false; + // ask swift layer to provide code for current url(s) + const message = {name: name, url: url, isTop: isTop}; + browser.runtime.sendNativeMessage(message, response => { + // if request failed, send error to content script for logging + if (response.error) return sendResponse(response); + // add api methods to js code + const apiConfig = { + userscripts: response.files.js, + uid: request.uid, + scriptHandler: response.scriptHandler, + scriptHandlerVersion: response.scriptHandlerVersion + }; + response.files.js = addApis(apiConfig); + // sort files by weight + response.files.js.sort((a, b) => Number(a.weight) < Number(b.weight)); + response.files.css.sort((a, b) => Number(a.weight) < Number(b.weight)); + sendResponse(response); + // update badge count on injection + // especially useful when injection is deferred (ie. subframes) + setBadgeCount(); + // refresh context menu items if needed + // if (response.files.menu.length) { + // getContextMenuItems(); + // } + }); + return true; + } + case "API_OPEN_TAB": { + const active = (request.active === true) ? true : false; + const props = {active: active, index: sender.tab.index + 1, url: request.url}; + browser.tabs.create(props, response => { + sendResponse(response); + }); + return true; + } + case "API_CLOSE_TAB": { + const tabId = request.tabId !== undefined ? request.tabId : sender.tab.id; + browser.tabs.remove(tabId, () => { + sendResponse({success: true}); + }); + return true; + } + case "API_SET_VALUE": { + const item = {}; + item[`${request.filename}---${request.key}`] = request.value; + browser.storage.local.set(item, () => { + sendResponse({success: true}); + }); + return true; + } + case "API_GET_VALUE": { + const key = `${request.filename}---${request.key}`; + browser.storage.local.get(key, item => { + if (Object.keys(item).length === 0) { + if (request.defaultValue !== undefined) { + sendResponse(request.defaultValue); + } else { + sendResponse(`undefined--${request.pid}`); + } + } else { + sendResponse(Object.values(item)[0]); + } + }); + return true; + } + case "API_DELETE_VALUE": { + const key = `${request.filename}---${request.key}`; + browser.storage.local.remove(key, response => { + sendResponse({success: true}); + }); + return true; + } + case "API_LIST_VALUES": { + const prefix = `${request.filename}---`; + const keys = []; + browser.storage.local.get().then(items => { + for (const key in items) { + if (key.startsWith(prefix)) { + const k = key.replace(prefix, ""); + keys.push(k); + } + } + sendResponse(keys); + }); + return true; + } + case "API_ADD_STYLE": + case "API_ADD_STYLE_SYNC": { + const tabId = sender.tab.id; + browser.tabs.insertCSS(tabId, {code: request.css, cssOrigin: "user"}, () => { + if (name === "API_ADD_STYLE") sendResponse(request.css); + }); + return true; + } + case "API_SET_CLIPBOARD": { + const result = setClipboard(request.data, request.type); + sendResponse(result); + break; + } + case "API_XHR_CS": { + xhrMake(request, sender); + break; + } + case "API_XHR_ABORT_CS": { + // get the xhrId from request + const xhrId = request.xhrId; + const match = xhrs.find(x => x.xhrId === xhrId); + if (match) { + match.xhr.abort(); + // sendResponse(match); + } else { + console.error(`abort message received for ${xhrId}, but it couldn't be found`); + } + break; + } + case "USERSCRIPT_INSTALL_00": + case "USERSCRIPT_INSTALL_01": + case "USERSCRIPT_INSTALL_02": { + const message = {name: name, content: request.content}; + browser.runtime.sendNativeMessage(message, response => { + sendResponse(response); + }); + return true; + } + case "REFRESH_SESSION_RULES": { + setSessionRules(); + break; + } + case "REFRESH_CONTEXT_MENU_SCRIPTS": { + getContextMenuItems(); + break; + } + } +}); + +async function setSessionRules() { + await clearAllSessionRules(); + const response = await browser.runtime.sendNativeMessage({name: "REQ_REQUESTS"}); + if (response.error) { + console.error(response.error); + return; + } + // there are no rules to apply + if (!response.length) return; + // loop through response, parse the rules, push to array and log + const rules = []; + for (let i = 0; i < response.length; i++) { + const rule = response[i]; + const code = JSON.parse(rule.code); + // check if an array or single rule + if (Array.isArray(code)) { + code.forEach(rule => rules.push(rule)); + console.info(`Setting session rule: ${rule.name} (${code.length})`); + } else { + rules.push(code); + console.info(`Setting session rule: ${rule.name}`); + } + } + // generate unique ids for all rules to ensure no repeats + const ids = randomNumberSet(1000, rules.length); + rules.map((rule, index) => rule.id = ids[index]); + try { + await browser.declarativeNetRequest.updateSessionRules({addRules: rules}); + } catch (error) { + console.error(`Error setting session rules: ${error}`); + return; + } + console.info(`Finished setting ${rules.length} session rules`); +} + +async function clearAllSessionRules() { + const rules = await browser.declarativeNetRequest.getSessionRules(); + if (!rules.length) return; + console.info(`Clearing ${rules.length} session rules`); + const ruleIds = rules.map(a => a.id); + await browser.declarativeNetRequest.updateSessionRules({ + removeRuleIds: ruleIds + }); +} + +function randomNumberSet(max, count) { + // generates a set of random unique numbers + // returns an array + const numbers = new Set(); + while (numbers.size < count) { + numbers.add((Math.floor(Math.random() * (max - 1 + 1)) + 1)); + } + return [...numbers]; +} + +browser.runtime.onStartup.addListener(async () => { + // on startup get declarativeNetRequests + // and set the requests for the session + // should also check and refresh when: + // 1. popup opens (done) + // 2. a new save event in the page occurs + // 3. the refresh button is pushed in the popup + await setSessionRules(); + await getContextMenuItems(); +}); browser.tabs.onActivated.addListener(setBadgeCount); browser.windows.onFocusChanged.addListener(setBadgeCount); browser.webNavigation.onCompleted.addListener(setBadgeCount); diff --git a/extension/Userscripts Extension/Resources/content.js b/extension/Userscripts Extension/Resources/content.js index 70298821..631d0ac1 100644 --- a/extension/Userscripts Extension/Resources/content.js +++ b/extension/Userscripts Extension/Resources/content.js @@ -1,113 +1,84 @@ // store code received let data; +// instance unique id +const uid = Math.random().toString(36).substring(2, 8); // determines whether strict csp injection has already run (JS only) -let cspFallbackAttempted = 0; -// track whether event listener added -let beforeunload = 0; -// unique id for api messaging -const uid = Math.random().toString(36).substr(2, 8); -// keep reference to platform -let platform; +let cspFallbackAttempted = false; -// request code immediately -browser.runtime.sendMessage({name: "REQ_USERSCRIPTS"}, response => { - // save code to data var so cspFallback can be attempted - data = response.code; - if (Object.keys(data).length !== 0) parseCode(data); +// request code as soon as possible +browser.runtime.sendMessage({name: "REQ_USERSCRIPTS", uid: uid}, response => { + if (!response) { + console.error("REQ_USERSCRIPTS returned undefined"); + return; + } + if (response.error) { + console.error(response.error); + return; + } + // save response locally in case CSP events occur + data = response; + for (let i = 0; i < data.files.js.length; i++) { + const userscript = data.files.js[i]; + processJS( + userscript.scriptObject.name, + userscript.scriptObject.filename, + userscript.code, + userscript.scriptObject["inject-into"], + userscript.scriptObject["run-at"], + false + ); + } + for (let i = 0; i < data.files.css.length; i++) { + const userstyle = data.files.css[i]; + injectCSS(userstyle.name, userstyle.code); + } }); -function sortByWeight(o) { - const sorted = {}; - Object.keys(o).sort((a, b) => o[b].weight - o[a].weight).forEach(key => sorted[key] = o[key]); - return sorted; -} - -function injectCSS(name, code) { - // there's no fallback if blocked by CSP - // future fix?: https://wicg.github.io/construct-stylesheets/ - console.info(`Injecting ${name} %c(css)`, "color: #60f36c"); - const tag = document.createElement("style"); - tag.textContent = code; - document.head.appendChild(tag); +function processJS(name, filename, code, scope, timing, fallback) { + switch (timing) { + case "document-start": + if (document.readyState === "loading") { + document.addEventListener("readystatechange", function() { + if (document.readyState === "interactive") { + injectJS(name, filename, code, scope, fallback); + } + }); + } else { + injectJS(name, filename, code, scope, fallback); + } + break; + case "document-end": + if (document.readyState !== "loading") { + injectJS(name, filename, code, scope, fallback); + } else { + document.addEventListener("DOMContentLoaded", function() { + injectJS(name, filename, code, scope, fallback); + }); + } + break; + case "document-idle": + if (document.readyState === "complete") { + injectJS(name, filename, code, scope, fallback); + } else { + document.addEventListener("readystatechange", e => { + if (document.readyState === "complete") { + injectJS(name, filename, code, scope, fallback); + } + }); + } + break; + } } -function injectJS(filename, code, scope, timing, grants, fallback) { - // include api methods - const gmVals = []; - const usVals = []; - const includedFunctions = []; - // when a csp violation occurs, the scope is set to "content", when previously it's "auto" - // if the scope isn't changed back to "auto" pre-injection, the scriptDataKey will be null - // and the fallback attempt will fail - // this will change back to "content" below - scope = fallback ? "auto" : scope; - let scriptDataKey; - if (timing === "context-menu") { - scriptDataKey = data.js["context-menu"][scope][filename]; +function injectJS(name, filename, code, scope, fallback) { + code = `(function() {${code}\n//# sourceURL=${filename.replace(/\s/g, "-")}\n})();`; + // if fallback or cspFallbackAttempted true and scope is auto, change scope to content + if (scope === "auto" && (fallback || cspFallbackAttempted)) { + scope = "content"; + console.warn(`Attempting fallback injection for ${name}`); } else { - scriptDataKey = data.js[scope][timing][filename]; + console.info(`Injecting ${name} %c(js)`, "color: #fff600"); } - console.info(`Injecting ${scriptDataKey.scriptObject.name} %c(js)`, "color: #e4f360"); - const scriptData = { - "script": scriptDataKey.scriptObject, - "scriptHandler": data.scriptHandler, - "scriptHandlerVersion": data.scriptHandlerVersion, - "scriptMetaStr": scriptDataKey.scriptMetaStr - }; - // change the scope back so it properly inject on fallback attempt - if (fallback) scope = "content"; - // TODO: use us_info instead of const variables - let api = `const uid = "${uid}";\nconst filename = "${filename}";`; - const us_info = {filename: filename, info: scriptData, uid: uid}; - api += `\nconst us_info = ${JSON.stringify(us_info)}`; - // all scripts get acces to GM.info and GM_info - api += "\nconst GM_info = us_info.info;\n"; - gmVals.push("info: us_info.info"); - grants.forEach(grant => { - if (grant === "GM.openInTab") { - api += `\n${us_openInTab}`; - gmVals.push("openInTab: us_openInTab"); - } else if (grant === "US.closeTab") { - api += `\n${us_closeTab}`; - usVals.push("closeTab: us_closeTab"); - } else if (grant === "GM.setValue") { - api += `\n${us_setValue}`; - gmVals.push("setValue: us_setValue"); - } else if (grant === "GM.getValue") { - api += `\n${us_getValue}`; - gmVals.push("getValue: us_getValue"); - } else if (grant === "GM.deleteValue") { - api += `\n${us_deleteValue}`; - gmVals.push("deleteValue: us_deleteValue"); - } else if (grant === "GM.listValues") { - api += `\n${us_listValues}`; - gmVals.push("listValues: us_listValues"); - } else if (grant === "GM_addStyle") { - api += `\n${us_addStyleSync}\nconst GM_addStyle = us_addStyleSync;`; - } else if (grant === "GM.addStyle") { - api += `\n${us_addStyle}\n`; - gmVals.push("addStyle: us_addStyle"); - } else if (grant === "GM.setClipboard") { - api += `\n${us_setClipboard}`; - gmVals.push("setClipboard: us_setClipboard"); - } else if (grant === "GM_setClipboard") { - api += `\n${us_setClipboardSync}\nconst GM_setClipboard = us_setClipboardSync;`; - } else if (grant === "GM_xmlhttpRequest" || grant === "GM.xmlHttpRequest") { - if (!includedFunctions.includes(xhr.name)) { - api += `\n${xhr}`; - includedFunctions.push(xhr.name); - } - if (grant === "GM_xmlhttpRequest") { - api += "\nconst GM_xmlhttpRequest = xhr;\n"; - } else if (grant === "GM.xmlHttpRequest") { - gmVals.push("xmlHttpRequest: xhr"); - } - } - }); - // create api aliases - const GM = `const GM = {${gmVals.join(",")}};`; - const US = `const US = {${usVals.join(",")}};`; - code = `(function() {\n${api}\n${GM}\n${US}\n${code}\n//# sourceURL=${filename.replace(/\s/g, "-")}\n})();`; if (scope !== "content") { const tag = document.createElement("script"); tag.textContent = code; @@ -117,90 +88,9 @@ function injectJS(filename, code, scope, timing, grants, fallback) { } } -function processJS(filename, code, scope, timing, grants, fallback) { - // this is about to get ugly - if (timing === "document-start") { - if (document.readyState === "loading") { - document.addEventListener("readystatechange", function() { - if (document.readyState === "interactive") { - injectJS(filename, code, scope, timing, grants, fallback); - } - }); - } else { - injectJS(filename, code, scope, timing, grants, fallback); - } - } else if (timing === "document-end") { - if (document.readyState !== "loading") { - injectJS(filename, code, scope, timing, grants, fallback); - } else { - document.addEventListener("DOMContentLoaded", function() { - injectJS(filename, code, scope, timing, grants, fallback); - }); - } - } else if (timing === "document-idle") { - if (document.readyState === "complete") { - injectJS(filename, code, scope, timing, grants, fallback); - } else { - document.addEventListener("readystatechange", function(e) { - if (document.readyState === "complete") { - injectJS(filename, code, scope, timing, grants, fallback); - } - }); - } - } -} - -function parseCode(data, fallback = false) { - // get css/js code separately - for (const type in data) { - // separate code type object (ie. {"css":{ ... }} {"js": { ... }}) - const codeTypeObject = data[type]; - // will be used for ordered code injection - let sorted = {}; - if (type === "css") { - sorted = sortByWeight(codeTypeObject); - for (const filename in sorted) { - const code = sorted[filename].code; - const name = sorted[filename].name; - // css is only injected into the page scope after DOMContentLoaded event - if (document.readyState !== "loading") { - injectCSS(name, code); - } else { - document.addEventListener("DOMContentLoaded", function() { - injectCSS(name, code); - }); - } - } - } else if (type === "js") { - // js code can be context scoped to the content script, page, or auto - // if auto is set, page scope is attempted, if fails content scope attempted - for (let scope in codeTypeObject) { - // context menu scripts will be handled in event listener below - if (scope === "context-menu") continue; - // get the nested scoped objects, separated by timing - const scopeObject = codeTypeObject[scope]; - // possible execution timings - const timings = ["document-start", "document-end", "document-idle"]; - timings.forEach(timing => { - // get the nested timing objects, separated by filename, skip if empty - const timingObject = scopeObject[timing]; - if (Object.keys(timingObject).length !== 0) { - sorted = sortByWeight(timingObject); - for (const filename in sorted) { - const code = sorted[filename].code; - const grants = sorted[filename].scriptObject.grant; - // when block by csp rules, auto scope script will auto retry injection - if (fallback) { - console.warn(`Attempting fallback injection for ${filename}`); - scope = "content"; - } - processJS(filename, code, scope, timing, grants, fallback); - } - } - }); - } - } - } +function injectCSS(name, code) { + console.info(`Injecting ${name} %c(css)`, "color: #60f36c"); + browser.runtime.sendMessage({name: "API_ADD_STYLE_SYNC", css: code}); } function cspFallback(e) { @@ -208,332 +98,183 @@ function cspFallback(e) { // 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 + // since it's fair to assume injection was blocked for all userscripts if (e.effectiveDirective === "script-src") { // get all "auto" code - // since other extensions can trigger a security policy violation event + // since other code can trigger a security policy violation event // make sure data var is not undefined before attempting fallback - if (data && Object.keys(data.js.auto).length !== 0 && cspFallbackAttempted < 1) { - const n = {"js": {"auto": {}}}; - n.js.auto = data.js.auto; - parseCode(n, true); + if (!data || cspFallbackAttempted) return; + // update global that tracks security policy violations + cspFallbackAttempted = true; + // loop through all js files if they are @inject-into: auto, attempt re-injection + for (let i = 0; i < data.files.js.length; i++) { + const userscript = data.files.js[i]; + if (userscript.scriptObject["inject-into"] !== "auto") continue; + processJS( + userscript.scriptObject.name, + userscript.scriptObject.filename, + userscript.code, + userscript.scriptObject["inject-into"], + userscript.scriptObject["run-at"], + true + ); } - cspFallbackAttempted = 1; } } -async function processJSContextMenuItems() { - // if not top window, stop execution - if (window !== window.top) return; - // context menu injection is macOS exclusive - // check if platform is stored - if (!platform) { - const response = await browser.runtime.sendMessage({name: "REQ_PLATFORM"}); - if (response.error) console.error(response.error); - if (response.platform) platform = response.platform; - } - // if not macOS, stop execution - if (platform !== "macos") return; - const contextMenuCodeObject = data.js["context-menu"]; - for (const scope in contextMenuCodeObject) { - const scopeObject = contextMenuCodeObject[scope]; - for (const filename in scopeObject) { - const name = scopeObject[filename].scriptObject.name; - if (document.readyState === "complete") { - addContextMenuItem(filename, name); - } else { - window.addEventListener("load", () => { - addContextMenuItem(filename, name); - }); - } - } - } -} - -function addContextMenuItem(filename, name) { - // when context menu item found, create a unique menuItemId and clean name - // the menuItemId will be passed back and forth between content and background - // for that reason use the current url + filename for the menuItemId - // when this file gets an run request, which file to run can be parsed from the menuItemId - // construct url from window.location since url params in href can break match pattern - // run on window load since urls can change during the load process - - // potential bug? https://developer.apple.com/forums/thread/685273 - // https://stackoverflow.com/q/68431201 - let pathname = window.location.pathname; - if (pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1); - const url = window.location.protocol + "//" + window.location.hostname + pathname; - - const menuItemId = url + "&$&" + filename; - const message = {name: "CONTEXT_CREATE", menuItemId: menuItemId, title: name, url: url}; - browser.runtime.sendMessage(message, response => { - // avoid adding unnecessary event listeners - if (!beforeunload) { - window.addEventListener("beforeunload", () => { - // beforeunload doesn't always fire on page refresh? - // OK since we wouldn't want to remove the context menu items when that happens - // BAD for when user disabled a context-menu script then refreshes... - // b/c of this all context menu items for a url will be removed/remade on refresh - browser.runtime.sendMessage({name: "CONTEXT_REMOVE", menuItemId: menuItemId}); +function handleApiMessages(e) { + // only respond to messages with matching unique id and have a name value + if (e.data.id !== uid || !e.data.name) return; + const id = e.data.id; + const name = e.data.name; + const pid = e.data.pid; + let message; + const respMessage = {name: name.replace("API_", "RESP_"), id: id, pid: pid}; + switch (name) { + case "API_OPEN_TAB": + message = {name: name, url: e.data.url, active: e.data.active}; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + window.postMessage(respMessage); }); - beforeunload = 1; - } - }); -} - -// api - https://developer.chrome.com/docs/extensions/mv3/content_scripts/#host-page-communication -function us_openInTab(url, openInBackground) { - const pid = Math.random().toString(36).substring(1, 9); - return new Promise(resolve => { - const callback = e => { - if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_OPEN_TAB") return; - resolve(e.data.response); - window.removeEventListener("message", callback); - }; - window.addEventListener("message", callback); - const active = (openInBackground === true) ? false : true; - window.postMessage({id: uid, pid: pid, name: "API_OPEN_TAB", url: url, active: active}); - }); -} - -function us_closeTab(tabId) { - const pid = Math.random().toString(36).substring(1, 9); - return new Promise(resolve => { - const callback = e => { - if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_CLOSE_TAB") return; - resolve(e.data.response); - window.removeEventListener("message", callback); - }; - window.addEventListener("message", callback); - window.postMessage({id: uid, pid: pid, name: "API_CLOSE_TAB", tabId: tabId}); - }); -} - -function us_setValue(key, value) { - const pid = Math.random().toString(36).substring(1, 9); - return new Promise(resolve => { - const callback = e => { - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_SET_VALUE" || e.data.filename !== filename) return; - resolve(e.data.response); - window.removeEventListener("message", callback); - }; - window.addEventListener("message", callback); - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, pid: pid, name: "API_SET_VALUE", filename: filename, key: key, value: value}); - }); -} - -function us_getValue(key, defaultValue) { - const pid = Math.random().toString(36).substring(1, 9); - return new Promise(resolve => { - const callback = e => { - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_GET_VALUE" || e.data.filename !== filename) return; - const response = e.data.response; - resolve(response); - window.removeEventListener("message", callback); - }; - window.addEventListener("message", callback); - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, pid: pid, name: "API_GET_VALUE", filename: filename, key: key, defaultValue: defaultValue}); - }); -} - -function us_listValues() { - const pid = Math.random().toString(36).substring(1, 9); - return new Promise(resolve => { - const callback = e => { - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_LIST_VALUES" || e.data.filename !== filename) return; - resolve(e.data.response); - window.removeEventListener("message", callback); - }; - window.addEventListener("message", callback); - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, pid: pid, name: "API_LIST_VALUES", filename: filename}); - }); -} - -function us_deleteValue(key) { - const pid = Math.random().toString(36).substring(1, 9); - return new Promise(resolve => { - const callback = e => { - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_DELETE_VALUE" || e.data.filename !== filename) return; - resolve(e.data.response); - window.removeEventListener("message", callback); - }; - window.addEventListener("message", callback); - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, pid: pid, name: "API_DELETE_VALUE", filename: filename, key: key}); - }); -} - -function us_addStyleSync(css) { - window.postMessage({id: uid, name: "API_ADD_STYLE_SYNC", css: css}); - return css; -} - -function us_addStyle(css) { - const pid = Math.random().toString(36).substring(1, 9); - return new Promise(resolve => { - const callback = e => { - if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_ADD_STYLE") return; - resolve(e.data.response); - window.removeEventListener("message", callback); - }; - window.addEventListener("message", callback); - window.postMessage({id: uid, pid: pid, name: "API_ADD_STYLE", css: css}); - }); -} - -function us_setClipboard(data, type) { - const pid = Math.random().toString(36).substring(1, 9); - return new Promise(resolve => { - const callback = e => { - if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_SET_CLIPBOARD") return; - resolve(e.data.response); - if (!e.data.response) console.error("clipboard write failed"); - window.removeEventListener("message", callback); - }; - window.addEventListener("message", callback); - window.postMessage({id: uid, pid: pid, name: "API_SET_CLIPBOARD", data: data, type: type}); - }); -} - -function us_setClipboardSync(data, type) { - // there's actually no sync method since a promise needs to be sent to bg page - // however make a dummy sync method for compatibility - window.postMessage({id: uid, name: "API_SET_CLIPBOARD", data: data, type: type}); - return undefined; -} - -// 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 -// the content script sends the xhr details to the background script -// the background script sends messages back to the content script for all xhr events -// the content script relays these messages back to the context where xhr was called -// if xhr was called with event handler functions they will be executed when those relays come in -function xhr(details) { - // if details didn't include url, do nothing - if (!details.url) return; - // create unique id for the xhr - const xhrId = Math.random().toString(36).substring(1, 9); - // strip out functions from details, kind of hacky - const detailsParsed = JSON.parse(JSON.stringify(details)); - // check which functions are included in the original details object - // add a bool to indicate if event listeners should be attached - if (details.onabort) detailsParsed.onabort = true; - if (details.onerror) detailsParsed.onerror = true; - if (details.onload) detailsParsed.onload = true; - if (details.onloadend) detailsParsed.onloadend = true; - if (details.onloadstart) detailsParsed.onloadstart = true; - if (details.onprogress) detailsParsed.onprogress = true; - if (details.onreadystatechange) detailsParsed.onreadystatechange = true; - if (details.ontimeout) detailsParsed.ontimeout = true; - // abort function gets returned when this function is called - const abort = () => { - window.postMessage({id: uid, name: "API_XHR_ABORT_INJ", xhrId: xhrId}); - }; - const callback = e => { - const name = e.data.name; - const response = e.data.response; - // ensure callback is responding to the proper message - if ( - e.data.id !== uid - || e.data.xhrId !== xhrId - || !name - || !name.startsWith("RESP_API_XHR_CS") - ) return; - if (name === "RESP_API_XHR_CS") { - // ignore - } else if (name.includes("ABORT") && details.onabort) { - details.onabort(response); - } else if (name.includes("ERROR") && details.onerror) { - details.onerror(response); - } else if (name === "RESP_API_XHR_CS_LOAD" && details.onload) { - details.onload(response); - } else if (name.includes("LOADEND") && details.onloadend) { - details.onloadend(response); - // remove event listener when xhr is complete - window.removeEventListener("message", callback); - } else if (name.includes("LOADSTART") && details.onloadstart) { - details.onloadtstart(response); - } else if (name.includes("PROGRESS") && details.onprogress) { - details.onprogress(response); - } else if (name.includes("READYSTATECHANGE") && details.onreadystatechange) { - details.onreadystatechange(response); - } else if (name.includes("TIMEOUT") && details.ontimeout) { - details.ontimeout(response); - } - }; - window.addEventListener("message", callback); - window.postMessage({id: uid, name: "API_XHR_INJ", details: detailsParsed, xhrId: xhrId}); - return {abort: abort}; + break; + case "API_CLOSE_TAB": + message = {name: name, tabId: e.data.tabId}; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + window.postMessage(respMessage); + }); + break; + case "API_SET_CLIPBOARD": + message = {name: name, data: e.data.data, type: e.data.type}; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + window.postMessage(respMessage); + }); + break; + case "API_SET_VALUE": + message = { + name: name, + filename: e.data.filename, + key: e.data.key, + value: e.data.value + }; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + respMessage.filename = e.data.filename; + window.postMessage(respMessage); + }); + break; + case "API_GET_VALUE": + message = { + name: name, + filename: e.data.filename, + pid: pid, + key: e.data.key, + defaultValue: e.data.defaultValue + }; + browser.runtime.sendMessage(message, response => { + const undef = response === `undefined--${pid}`; + respMessage.response = undef ? undefined : response; + respMessage.filename = e.data.filename; + window.postMessage(respMessage); + }); + break; + case "API_DELETE_VALUE": + message = {name: name, filename: e.data.filename, key: e.data.key}; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + respMessage.filename = e.data.filename; + window.postMessage(respMessage); + }); + break; + case "API_LIST_VALUES": + message = {name: name, filename: e.data.filename}; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + respMessage.filename = e.data.filename; + window.postMessage(respMessage); + }); + break; + case "API_ADD_STYLE": + message = {name: name, css: e.data.css}; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + window.postMessage(respMessage); + }); + break; + case "API_ADD_STYLE_SYNC": + message = {name: name, css: e.data.css}; + browser.runtime.sendMessage(message); + break; + case "API_XHR_INJ": + message = { + name: "API_XHR_CS", + details: e.data.details, + id: id, + xhrId: e.data.xhrId + }; + browser.runtime.sendMessage(message, response => { + respMessage.response = response; + respMessage.xhrId = e.data.xhrId; + window.postMessage(respMessage); + }); + break; + case "API_XHR_ABORT_INJ": + message = {name: "API_XHR_ABORT_CS", xhrId: e.data.xhrId}; + browser.runtime.sendMessage(message); + break; + } } -// listen for messages from background, popup, etc... +// listens for messages from background, popup, etc... browser.runtime.onMessage.addListener((request, sender, sendResponse) => { const name = request.name; - if (name === "CONTEXT_RUN") { - // only run context menu script in top window - if (window !== window.top) return; - // get the filename from the menuItemId sent with the request - const filename = request.menuItemId.split("&$&")[1]; - // clone the context menu scripts object from the data var - const contextMenuCodeObject = data.js["context-menu"]; - let found = false; - // loop through all the context menu scripts to find match against filename - for (let scope in contextMenuCodeObject) { - for (const fn in contextMenuCodeObject[scope]) { - if (fn === filename) { - // get code from object and send for injection along with filename & scope - const code = contextMenuCodeObject[scope][filename].code; - const grants = contextMenuCodeObject[scope][filename].scriptObject.grant; - // if strict csp already detected change auto scoped scripts to content - let fallback = false; - if (cspFallbackAttempted && scope === "auto") { - console.warn(`Attempting fallback injection for ${filename}`); - scope = "content"; - fallback = true; - } - scope = cspFallbackAttempted && scope === "auto" ? "content" : scope; - injectJS(filename, code, scope, "context-menu", grants, fallback); - found = true; - break; - } - } - if (found) break; - } - } else if (name.startsWith("RESP_API_XHR_BG_")) { + if (name.startsWith("RESP_API_XHR_BG_")) { // only respond to messages on the correct content script if (request.id !== uid) return; - const resp = request.response; - const n = name.replace("_BG_", "_CS_"); + const xhrResponse = request.response; + const responseName = name.replace("_BG_", "_CS_"); // arraybuffer responses had their data converted, convert it back to arraybuffer - if (request.response.responseType === "arraybuffer" && resp.response) { + if (xhrResponse.responseType === "arraybuffer" && xhrResponse.response) { try { - const r = new Uint8Array(resp.response).buffer; - resp.response = r; + const buffer = new Uint8Array(xhrResponse.response).buffer; + xhrResponse.response = buffer; } catch (error) { console.error("error parsing xhr arraybuffer response", error); } // blob responses had their data converted, convert it back to blob - } else if (request.response.responseType === "blob" && resp.response && resp.response.data) { + } else if ( + request.response.responseType === "blob" + && xhrResponse.response + && xhrResponse.response.data + ) { fetch(request.response.response.data) .then(res => res.blob()) .then(b => { - resp.response = b; - window.postMessage({name: n, response: resp, id: request.id, xhrId: request.xhrId}); + xhrResponse.response = b; + window.postMessage({ + name: responseName, + response: xhrResponse, + id: request.id, + xhrId: request.xhrId + }); }); } // blob response will execute its own postMessage call if (request.response.responseType !== "blob") { - window.postMessage({name: n, response: resp, id: request.id, xhrId: request.xhrId}); + window.postMessage({ + name: responseName, + response: xhrResponse, + id: request.id, xhrId: + request.xhrId + }); } - } else if (["USERSCRIPT_INSTALL_00", "USERSCRIPT_INSTALL_01", "USERSCRIPT_INSTALL_02"].includes(name)) { + } else if ( + name === "USERSCRIPT_INSTALL_00" + || name === "USERSCRIPT_INSTALL_01" + || name === "USERSCRIPT_INSTALL_02" + ) { const types = [ "text/plain", "application/ecmascript", @@ -542,90 +283,35 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { "text/javascript" ]; if (!document.contentType || types.indexOf(document.contentType) === -1) { - // only allow installation if contentType is in list above sendResponse({invalid: true}); } else { - const content = document.body.innerText; - browser.runtime.sendMessage({name: name, content: content}, response => { + const message = {name: name, content: document.body.innerText}; + browser.runtime.sendMessage(message, response => { sendResponse(response); }); return true; } - } -}); + } else 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; -// listen for message from api -window.addEventListener("message", e => { - // only respond to messages that have matching unique id and have a name value - if (e.data.id !== uid || !e.data.name) return; - const id = e.data.id; - const name = e.data.name; - const pid = e.data.pid; - let message; - if (name === "API_OPEN_TAB") { - // ignore requests that don't supply a url - if (!e.data.url) return; - message = {name: "API_OPEN_TAB", url: e.data.url, active: e.data.active}; - browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, pid: pid, name: "RESP_OPEN_TAB", response: response}); - }); - } else if (name === "API_CLOSE_TAB") { - browser.runtime.sendMessage({name: "API_CLOSE_TAB", tabId: e.data.tabId}, response => { - window.postMessage({id: id, pid: pid, name: "RESP_CLOSE_TAB", response: response}); - }); - } else if (name === "API_SET_CLIPBOARD") { - browser.runtime.sendMessage({name: "API_SET_CLIPBOARD", data: e.data.data, type: e.data.type}, response => { - window.postMessage({id: id, pid: pid, name: "RESP_SET_CLIPBOARD", response: response}); - }); - } else if (name === "API_SET_VALUE") { - message = {name: "API_SET_VALUE", filename: e.data.filename, key: e.data.key, value: e.data.value}; - browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, pid: pid, name: "RESP_SET_VALUE", filename: e.data.filename, response: response}); - }); - } else if (name === "API_GET_VALUE") { - message = {name: "API_GET_VALUE", filename: e.data.filename, pid: pid, key: e.data.key, defaultValue: e.data.defaultValue}; - browser.runtime.sendMessage(message, response => { - const resp = response === `undefined--${pid}` ? undefined : response; - window.postMessage({id: id, pid: pid, name: "RESP_GET_VALUE", filename: e.data.filename, response: resp}); - }); - } else if (name === "API_DELETE_VALUE") { - message = {name: "API_DELETE_VALUE", filename: e.data.filename, key: e.data.key}; - browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, pid: pid, name: "RESP_DELETE_VALUE", filename: e.data.filename, response: response}); - }); - } else if (name === "API_LIST_VALUES") { - message = {name: "API_LIST_VALUES", filename: e.data.filename}; - browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, pid: pid, name: "RESP_LIST_VALUES", filename: e.data.filename, response: response}); - }); - } else if (name === "API_ADD_STYLE") { - try { - message = {name: "API_ADD_STYLE", css: e.data.css}; - browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, pid: pid, name: "RESP_ADD_STYLE", response: response}); - }); - } catch (e) { - console.log(e); - } - } else if (name === "API_ADD_STYLE_SYNC") { - try { - message = {name: "API_ADD_STYLE_SYNC", css: e.data.css}; - browser.runtime.sendMessage(message); - } catch (e) { - console.log(e); + // loop through context-menu scripts saved to data object and find match + // if no match found, nothing will execute and error will log + const filename = request.menuItemId; + for (let i = 0; i < data.files.menu.length; i++) { + const item = data.files.menu[i]; + if (item.scriptObject.filename === filename) { + console.info(`Injecting ${filename} %c(js)`, "color: #fff600"); + sendResponse({code: item.code}); + return; + } } - } else if (name === "API_XHR_INJ") { - message = {name: "API_XHR_CS", details: e.data.details, id: id, xhrId: e.data.xhrId}; - browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, name: "RESP_API_XHR_CS", response: response, xhrId: e.data.xhrId}); - }); - } else if (name === "API_XHR_ABORT_INJ") { - message = {name: "API_XHR_ABORT_CS", xhrId: e.data.xhrId}; - browser.runtime.sendMessage(message); + console.error(`Couldn't find ${filename} code!`); } }); -// when userscript fails due to a CSP and has @inject-into value of auto +// listens for messages from api methods calls in content script or current page +window.addEventListener("message", handleApiMessages); +// when userscript fails due to a CSP and has @inject-into value of auto or page document.addEventListener("securitypolicyviolation", cspFallback); -// create context menu items as needed -document.addEventListener("contextmenu", processJSContextMenuItems); diff --git a/extension/Userscripts Extension/Resources/manifest.json b/extension/Userscripts Extension/Resources/manifest.json index 65048ebf..563d724c 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.1.0", + "version": "4.2.0", "icons": { "48": "images/icon-48.png", "96": "images/icon-96.png", @@ -35,6 +35,7 @@ "", "clipboardWrite", "contextMenus", + "declarativeNetRequest", "menus", "nativeMessaging", "storage", diff --git a/extension/Userscripts Extension/Resources/page.html b/extension/Userscripts Extension/Resources/page.html index 9e1ebc8e..d0a7c906 100644 --- a/extension/Userscripts Extension/Resources/page.html +++ b/extension/Userscripts Extension/Resources/page.html @@ -1 +1 @@ -Userscripts Safari
\ No newline at end of file +Userscripts Safari
\ No newline at end of file diff --git a/extension/Userscripts Extension/Resources/page.js b/extension/Userscripts Extension/Resources/page.js index d872fa3e..ada155d3 100644 --- a/extension/Userscripts Extension/Resources/page.js +++ b/extension/Userscripts Extension/Resources/page.js @@ -962,6 +962,10 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o return { set, update, subscribe }; } + /** + * @param {number} ms millisecond timestamp + * @returns {string} + */ function formatDate(ms) { const d = new Date(ms); const yr = new Intl.DateTimeFormat("en", {year: "numeric"}).format(d); @@ -973,9 +977,15 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } function uniqueId() { - return Math.random().toString(36).substr(2, 8); + return Math.random().toString(36).substring(2, 10); } + // TODO: describe the items array that should get passed to this function + /** + * @param {Array} array + * @param {("lastModifiedAsc"|"lastModifiedDesc"|"nameAsc"|"nameDesc")} order + * @returns + */ function sortBy(array, order) { if (order === "nameAsc") { array.sort((a, b) => a.name.localeCompare(b.name)); @@ -991,7 +1001,14 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o return array; } - function newScriptDefault(description, name, type) { + /** + * + * @param {string} description + * @param {string} name + * @param {("css"|"js")} type + * @returns {string} + */ + function newScriptDefault(description, name, type) { if (type === "css") { return `/* ==UserStyle==\n@name ${name}\n@description ${description}\n@match \n==/UserStyle== */`; } else if (type === "js") { @@ -999,6 +1016,92 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } } + /** + * @param {string} text editor code + * @returns {{match: boolean, meta: boolean} | {key: string, value: string, text: string}[]} + */ + function parseMetadata(text) { + const groupsRe = /(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)/; + const groups = text.match(groupsRe); + // userscript code doesn't match the regex expression + // could be missing opening/closing tags, malformed + // or missing metadata between opening/closing tags (group 2 in regex exp) + if (!groups) { + return {match: false}; + } + + // userscript code matches but content between opening/closing tag missing + // ex. opening/closing tags present, but newline characters between the tags + const metas = groups[2]; + if (!metas) return {match: true, meta: false}; + + const metadata = []; + const metaArray = metas.split("\n"); + + for (let i = 0; i < metaArray.length; i++) { + const metaRegex = /^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]*([^\s]+[^\r\n\t\v\f]*)?/; + const meta = metaArray[i]; + const parts = meta.match(metaRegex); + if (parts) metadata.push({key: parts[1], value: parts[2], text: parts[0]}); + } + + // if there is content between the opening/closing tags, match will be found + // this additionally checks that there's at least one properly formed key + // if not keys found, assume metadata is missing + // checking that required keys are present will happen elsewhere + if (!Object.keys(metadata).length) return {match: true, meta: false}; + + return metadata; + } + + const validGrants = new Set([ + "GM.addStyle", + "GM.deleteValue", + "GM.getValue", + "GM.info", + "GM.listValues", + "GM.openInTab", + "GM.setClipboard", + "GM.setValue", + "GM.xmlHttpRequest", + "GM_addStyle", + "GM_info", + "GM_setClipboard", + "GM_xmlhttpRequest" + ]); + + const validKeys = new Set([ + "author", + "description", + "downloadURL", + "exclude", + "exclude-match", + "grant", + "include", + "inject-into", + "match", + "name", + "noframes", + "require", + "run-at", + "updateURL", + "version", + "weight" + ]); + + function notificationStore() { + const {subscribe, update} = writable([]); + const add = item => { + update(a => { + a.push(item); + return a; + }); + }; + const remove = id => update(a => a.filter(b => b.id !== id)); + return {subscribe, add, remove}; + } + const notifications = notificationStore(); + function logStore() { const {subscribe, set, update} = writable([]); const add = (message, type, notify) => { @@ -1016,19 +1119,6 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } const log = logStore(); - function notificationStore() { - const {subscribe, update} = writable([]); - const add = item => { - update(a => { - a.push(item); - return a; - }); - }; - const remove = id => update(a => a.filter(b => b.id !== id)); - return {subscribe, add, remove}; - } - const notifications = notificationStore(); - function stateStore() { const {subscribe, update} = writable(["init"]); // store oldState to see how state transitioned @@ -1318,7 +1408,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o // button click events bubble to document body // can use to determine if button triggered body event listener func // event.target = button, e.target = body event listener trigger el - if (e.target != event.target) { + if (e.target !== event.target) { // the body event listener not triggered by button // if omitted, dropdown would close immediately $$invalidate(3, active = false); @@ -1470,7 +1560,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (113:4) {#if query} + // (66:4) {#if query} function create_if_block(ctx) { let iconbutton; let current; @@ -1642,7 +1732,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o items, $items = $items.map(item => { const visible = item.filename.toLowerCase().includes(query.trim().toLowerCase()); - if (visible != item.visible) return { ...item, visible }; + if (visible !== item.visible) return { ...item, visible }; return item; }), $items @@ -1843,13 +1933,13 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o return { c() { div = element("div"); - attr(div, "class", div_class_value = "script__tag " + (/*type*/ ctx[0] ? "script__tag--" + /*type*/ ctx[0] : "") + " svelte-p9vdxd"); + attr(div, "class", div_class_value = "script__tag " + `script__tag--${/*type*/ ctx[0]}` + " svelte-1rzbr97"); }, m(target, anchor) { insert(target, div, anchor); }, p(ctx, [dirty]) { - if (dirty & /*type*/ 1 && div_class_value !== (div_class_value = "script__tag " + (/*type*/ ctx[0] ? "script__tag--" + /*type*/ ctx[0] : "") + " svelte-p9vdxd")) { + if (dirty & /*type*/ 1 && div_class_value !== (div_class_value = "script__tag " + `script__tag--${/*type*/ ctx[0]}` + " svelte-1rzbr97")) { attr(div, "class", div_class_value); } }, @@ -2034,7 +2124,14 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o let current; let mounted; let dispose; - tag = new Tag({ props: { type: /*data*/ ctx[0].type } }); + + tag = new Tag({ + props: { + type: /*data*/ ctx[0].request + ? "request" + : /*data*/ ctx[0].type + } + }); toggle = new Toggle({ props: { checked: !/*data*/ ctx[0].disabled } @@ -2089,7 +2186,11 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o p(new_ctx, [dirty]) { ctx = new_ctx; const tag_changes = {}; - if (dirty & /*data*/ 1) tag_changes.type = /*data*/ ctx[0].type; + + if (dirty & /*data*/ 1) tag_changes.type = /*data*/ ctx[0].request + ? "request" + : /*data*/ ctx[0].type; + tag.$set(tag_changes); if ((!current || dirty & /*data*/ 1) && t1_value !== (t1_value = /*data*/ ctx[0].name + "")) set_data(t1, t1_value); const toggle_changes = {}; @@ -2171,7 +2272,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o function formatDescription(str) { if (str && str.length > 120) { $$invalidate(2, showTitle = true); - return str.substring(0, 120).trim() + "..."; + return `${str.substring(0, 120).trim()}...`; } else { $$invalidate(2, showTitle = false); return str; @@ -17351,6 +17452,47 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o instance$7.display.wrapper.removeAttribute("style"); } + /** + * @param {{set: Set, setName: string, message: string}} + * @returns {{from: {line: number, ch: number}, + * to: {line: number, ch: number}, message: string, severity: string}} + */ + function makeErrors({ set, setName, message }) { + const errors = []; + + //for (let i = 0; i < array.length; i++) { + set.forEach(el => { + //const el = array[i]; + let regex = new RegExp(`^${el}$`); + + if (setName === "invalidKeys") { + regex = new RegExp(`^// @${el}.*?$`); + } + + const cursor = instance$7.getSearchCursor(regex); + const ranges = []; + + while (cursor.findNext()) { + ranges.push({ anchor: cursor.from(), head: cursor.to() }); + } + + for (let j = 0; j < ranges.length; j++) { + const range = ranges[j]; + + const err = { + from: range?.anchor, + to: range?.head, + severity: "warning", + message + }; + + errors.push(err); + } + }); + + return errors; + } + function getValue() { return instance$7.getValue(); } @@ -17384,7 +17526,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o let searchActive = false; // linter options - https://jshint.com/docs/options/ - const lintOptions = { asi: true, esversion: 9 }; + const lintOptions = { async: true, getAnnotations: linter }; function init() { // do lint settings check @@ -17394,7 +17536,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o instance$7 = codemirror.fromTextArea(textarea, { mode: "javascript", autoCloseBrackets: $settings.autoCloseBrackets, - continueComments: false, + continueComments: true, foldGutter: true, lineNumbers: true, lineWrapping: true, @@ -17417,12 +17559,10 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o "Cmd-/": "toggleComment", "Cmd-S": () => saveHandler(), "Cmd-F": () => activateSearch(), - "Esc": () => { - $$invalidate(2, searchActive = false); - }, + "Esc": () => $$invalidate(2, searchActive = false), Tab: cm => { // convert tabs to spaces and add invisible elements - var s = Array(cm.getOption("indentUnit") + 1).join(" "); + const s = Array(cm.getOption("indentUnit") + 1).join(" "); cm.replaceSelection(s); } @@ -17471,7 +17611,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o if (// check if setting is enabled $settings.autoHint && // ensure hinting not active already !cm.state.completionActive && // not first position on the line - cm.getCursor().ch != 0 && // only hint when 1-2 key combos + cm.getCursor().ch !== 0 && // only hint when 1-2 key combos keysPressed.length < 3 && // valid keys combo validKeys) cm.showHint({ completeSingle: false }); } @@ -17501,6 +17641,118 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o instance$7.refresh(); } + function linter(text, updateLinting, options) { + + // only lint in javascript mode + if (instance$7?.options.mode !== "javascript") return; + + // normal javascript linting through CodeMirror addon and jshint + let errors = codemirror.lint.javascript(text, { asi: true, esversion: 9 }); + + // errors from custom checks & metadata parser will populate this array + let customErrors = []; + + { + // check if whitespace characters precede userscript tags + if ((/^\s/).test(text)) { + customErrors.push({ + from: { line: 0, ch: 0 }, + to: { line: 0, ch: 0 }, + severity: "warning", + message: "Userscript starts with whitespace characters." + }); + } + + // parse the code metadata + const parsedMetadata = parseMetadata(text); + + // check parser result for initial errors + if (parsedMetadata.match === false) { + customErrors.push({ + from: { line: 0, ch: 0 }, + to: { line: 0, ch: 0 }, + severity: "error", + message: "Userscript metadata missing or improperly formatted." + }); + } else if (parsedMetadata.meta === false) { + customErrors.push({ + from: { line: 0, ch: 0 }, + to: { line: 0, ch: 0 }, + severity: "error", + message: "Userscript metadata missing." + }); + } + + // run additional checks + const invalidKeys = new Set(); + + let invalidKeysErrors = []; + const invalidGrants = new Set(); + let invalidGrantsErrors = []; + const emptyKeyValues = new Set(); + let emptyKeyValuesErrors = []; + + // if metadata fails initial check, it won't be array + // don't run the additional checks if initial checks failed + if (Array.isArray(parsedMetadata)) { + // find invalid keys or empty key values + for (let i = 0; i < parsedMetadata.length; i++) { + const { key, value: val, text } = parsedMetadata[i]; + + if (!validKeys.has(key)) { + invalidKeys.add(key); + } + + if (key !== "noframes" && !invalidKeys.has(key) && !val) { + emptyKeyValues.add(text); + } + } + + // find unsupported @grant methods + const grants = parsedMetadata.filter(a => a.key === "grant"); + + for (let i = 0; i < grants.length; i++) { + const { value, text } = grants[i]; + + if (!validGrants.has(value)) { + invalidGrants.add(text); + } + } + } + + invalidKeysErrors = makeErrors({ + set: invalidKeys, + setName: "invalidKeys", + message: "Unsupported metadata key." + }); + + emptyKeyValuesErrors = makeErrors({ + set: emptyKeyValues, + setName: "emptyKeyValues", + message: "Key requires a value." + }); + + invalidGrantsErrors = makeErrors({ + set: invalidGrants, + setName: "invalidGrants", + message: "@grant method not supported." + }); + + // combine all custom error arrays + customErrors = [ + ...customErrors, + ...invalidKeysErrors, + ...emptyKeyValuesErrors, + ...invalidGrantsErrors + ]; + } + + // if the custom linter returned errors, add them to errors array + if (customErrors.length) errors = [...errors, ...customErrors]; + + updateLinting(errors); + } + function discardChanges() { if ($state.includes("ready")) { instance$7.setValue(savedCode); @@ -17517,9 +17769,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }); } - const func = () => { - $$invalidate(2, searchActive = false); - }; + const func = () => $$invalidate(2, searchActive = false); function switch_instance_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { @@ -17641,7 +17891,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o return child_ctx; } - // (230:8) + // (181:8) function create_default_slot$1(ctx) { let li0; let t1; @@ -17692,7 +17942,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (237:8) {#if $state.includes("items-loading")} + // (188:8) {#if $state.includes("items-loading")} function create_if_block_1(ctx) { let loader; let current; @@ -17721,7 +17971,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (240:8) {#each list as item (item.filename)} + // (191:8) {#each list as item (item.filename)} function create_each_block(key_1, ctx) { let first; let switch_instance; @@ -17819,7 +18069,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (249:4) {#if showCount} + // (200:4) {#if showCount} function create_if_block$5(ctx) { let div; let t0_value = /*list*/ ctx[3].length + ""; @@ -17833,7 +18083,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o div = element("div"); t0 = text(t0_value); t1 = text(" Items"); - attr(div, "class", "sidebar__count svelte-11mx0re"); + attr(div, "class", "sidebar__count svelte-23v8pd"); }, m(target, anchor) { insert(target, div, anchor); @@ -17942,13 +18192,13 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o t4 = space(); if (if_block1) if_block1.c(); - attr(div0, "class", "sidebar__filter svelte-11mx0re"); - attr(div1, "class", "sidebar__header svelte-11mx0re"); - attr(div2, "class", "sidebar__body svelte-11mx0re"); + attr(div0, "class", "sidebar__filter svelte-23v8pd"); + attr(div1, "class", "sidebar__header svelte-23v8pd"); + attr(div2, "class", "sidebar__body svelte-23v8pd"); attr(div3, "class", div3_class_value = "sidebar " + (!/*$settings*/ ctx[4].descriptions ? "sidebar--compact" - : "") + " svelte-11mx0re"); + : "") + " svelte-23v8pd"); }, m(target, anchor) { insert(target, div3, anchor); @@ -18045,7 +18295,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o if (!current || dirty & /*$settings*/ 16 && div3_class_value !== (div3_class_value = "sidebar " + (!/*$settings*/ ctx[4].descriptions ? "sidebar--compact" - : "") + " svelte-11mx0re")) { + : "") + " svelte-23v8pd")) { attr(div3, "class", div3_class_value); } }, @@ -18129,7 +18379,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o const namePrefix = type === "js" ? "NewScript-" : "NewStyle-"; const name = namePrefix + random; - const filename = name + "." + type; + const filename = `${name}.${type}`; const description = "This is your new file, start writing code"; const item = { @@ -18160,7 +18410,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o // can occur when user clicks a non-temp item while a temp item exists const temp = $items.find(i => i.temp); - if ((temp && temp != item || cmChanged()) && !warn()) return; + if ((temp && temp !== item || cmChanged()) && !warn()) return; // the editor has changed or the above scenario is true // the user has been warned and has accepted the warning @@ -18173,7 +18423,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } // remove the temp item if needed - if (temp && temp != item) items.update(i => i.filter(a => !a.temp)); + if (temp && temp !== item) items.update(i => i.filter(a => !a.temp)); // set the saved and session code variables properly const savedCode = item.temp ? null : item.content; @@ -18191,7 +18441,8 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o await tick(); const cm = cmGetInstance(); - const mode = item.type === "js" ? "javascript" : item.type; + let mode = "javascript"; + if (item.type === "css") mode = "css"; cm.setOption("mode", mode); cm.setValue(item.content); cm.clearHistory(); @@ -18275,11 +18526,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o const click_handler = () => state.add("settings"); const click_handler_1 = () => newItem("css"); const click_handler_2 = () => newItem("js"); - - const func = (item, e) => { - toggleItem(e, item); - }; - + const func = (item, e) => toggleItem(e, item); const click_handler_3 = item => activate(item); let disabled; let list; @@ -18291,7 +18538,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } if ($$self.$$.dirty & /*$items, $settings*/ 65552) { - $$invalidate(3, list = sortBy($items, $settings.sortOrder).filter(a => a.visible != false)); + $$invalidate(3, list = sortBy($items, $settings.sortOrder).filter(a => a.visible !== false)); } if ($$self.$$.dirty & /*list*/ 8) { @@ -18366,7 +18613,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (268:4) {#if !activeItem} + // (158:4) {#if !activeItem} function create_if_block_5(ctx) { let div; @@ -18374,7 +18621,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o c() { div = element("div"); div.textContent = "No Item Selected"; - attr(div, "class", "editor__empty svelte-cbo01e"); + attr(div, "class", "editor__empty svelte-5bq9yj"); }, m(target, anchor) { insert(target, div, anchor); @@ -18385,7 +18632,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (289:20) {:else} + // (179:20) {:else} function create_else_block(ctx) { let t0; let t1; @@ -18409,7 +18656,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (287:35) + // (177:35) function create_if_block_4(ctx) { let t; @@ -18427,7 +18674,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (285:37) + // (175:37) function create_if_block_3(ctx) { let t0; let span; @@ -18440,7 +18687,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o span = element("span"); t1 = text("remotely fetched"); t2 = text(", check carefully before saving!"); - attr(span, "class", "info svelte-cbo01e"); + attr(span, "class", "info svelte-5bq9yj"); attr(span, "title", /*remote*/ ctx[4]); }, m(target, anchor) { @@ -18462,7 +18709,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (283:58) + // (173:58) function create_if_block_2(ctx) { let t0; let span; @@ -18495,7 +18742,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (281:58) + // (171:58) function create_if_block_1$1(ctx) { let t; @@ -18513,7 +18760,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (279:20) {#if $state.includes("saving")} + // (169:20) {#if $state.includes("saving")} function create_if_block$6(ctx) { let t; @@ -18659,19 +18906,19 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o t11 = space(); button1 = element("button"); t12 = text("Save"); - attr(div0, "class", "editor__title truncate svelte-cbo01e"); - attr(div1, "class", "svelte-cbo01e"); - attr(div3, "class", "editor__status svelte-cbo01e"); - attr(div4, "class", "editor__header__content svelte-cbo01e"); - attr(div5, "class", "editor__header__buttons svelte-cbo01e"); - attr(div6, "class", "editor__header svelte-cbo01e"); - attr(div7, "class", "editor__code svelte-cbo01e"); + attr(div0, "class", "editor__title truncate svelte-5bq9yj"); + attr(div1, "class", "svelte-5bq9yj"); + attr(div3, "class", "editor__status svelte-5bq9yj"); + attr(div4, "class", "editor__header__content svelte-5bq9yj"); + attr(div5, "class", "editor__header__buttons svelte-5bq9yj"); + attr(div6, "class", "editor__header svelte-5bq9yj"); + attr(div7, "class", "editor__code svelte-5bq9yj"); button0.disabled = button0_disabled_value = /*disabled*/ ctx[9] || /*discardDisabled*/ ctx[7]; - attr(button0, "class", "svelte-cbo01e"); + attr(button0, "class", "svelte-5bq9yj"); button1.disabled = button1_disabled_value = /*disabled*/ ctx[9] || /*saveDisabled*/ ctx[8]; - attr(button1, "class", "svelte-cbo01e"); - attr(div8, "class", "editor__footer svelte-cbo01e"); - attr(div9, "class", "editor svelte-cbo01e"); + attr(button1, "class", "svelte-5bq9yj"); + attr(div8, "class", "editor__footer svelte-5bq9yj"); + attr(div9, "class", "editor svelte-5bq9yj"); }, m(target, anchor) { insert(target, div9, anchor); @@ -18864,12 +19111,12 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o items.update(i => { const index = i.findIndex(a => a.active); - const disabled = i[index].disabled != undefined + const disabled = i[index].disabled !== undefined ? i[index].disabled : false; const type = i[index].type; - const visible = i[index].visible != undefined ? i[index].visible : true; + const visible = i[index].visible !== undefined ? i[index].visible : true; i[index] = response; i[index].active = true; i[index].disabled = disabled; @@ -18880,6 +19127,12 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o // set the newly saved file contents in codemirror instance cmSetSavedCode(response.content); + + // refresh session rules + browser.runtime.sendMessage({ name: "REFRESH_SESSION_RULES" }); + + // refresh context-menu scripts + browser.runtime.sendMessage({ name: "REFRESH_CONTEXT_MENU_SCRIPTS" }); } state.remove("saving"); @@ -18891,7 +19144,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o const link = document.createElement("a"); const content = codemirror.getValue(); const filename = activeItem.filename; - link.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(content)); + link.setAttribute("href", `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`); link.setAttribute("download", filename); link.style.display = "none"; document.body.appendChild(link); @@ -18946,7 +19199,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o log.add(response.error, "error", true); } else { items.update(i => i.filter(a => !a.active)); - log.add("Successfully trashed " + activeItem.filename, "info", false); + log.add(`Successfully trashed ${activeItem.filename}`, "info", false); } } @@ -18998,7 +19251,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o $$invalidate(1, name = activeItem.name); $$invalidate(4, remote = activeItem.remote); $$invalidate(5, temp = activeItem.temp); - $$invalidate(2, type = activeItem.type); + $$invalidate(2, type = activeItem.request ? "request" : activeItem.type); $$invalidate(0, canUpdate = activeItem.canUpdate); // on load if temp item, disabled discard and enable save, if not disable both @@ -19281,7 +19534,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o t22 = space(); div26 = element("div"); div16 = element("div"); - div16.innerHTML = `
General Settings
`; + div16.innerHTML = `
General Settings
`; t24 = space(); div18 = element("div"); div17 = element("div"); @@ -19337,51 +19590,51 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o a2 = element("a"); a2.textContent = "supporting the project"; t53 = text("."); - attr(div0, "class", "svelte-1ibrghz"); - attr(div1, "class", "modal__title svelte-1ibrghz"); - attr(div2, "class", "svelte-1ibrghz"); - attr(div3, "class", "modal__row svelte-1ibrghz"); - attr(div4, "class", "svelte-1ibrghz"); - attr(div5, "class", "modal__row svelte-1ibrghz"); - attr(div6, "class", "svelte-1ibrghz"); - attr(div7, "class", "modal__row svelte-1ibrghz"); - attr(div8, "class", "svelte-1ibrghz"); - attr(div9, "class", "modal__row svelte-1ibrghz"); - attr(div10, "class", "svelte-1ibrghz"); - attr(div11, "class", "modal__row svelte-1ibrghz"); - attr(div12, "class", "svelte-1ibrghz"); + attr(div0, "class", "svelte-9f6q4c"); + attr(div1, "class", "modal__title svelte-9f6q4c"); + attr(div2, "class", "svelte-9f6q4c"); + attr(div3, "class", "modal__row svelte-9f6q4c"); + attr(div4, "class", "svelte-9f6q4c"); + attr(div5, "class", "modal__row svelte-9f6q4c"); + attr(div6, "class", "svelte-9f6q4c"); + attr(div7, "class", "modal__row svelte-9f6q4c"); + attr(div8, "class", "svelte-9f6q4c"); + attr(div9, "class", "modal__row svelte-9f6q4c"); + attr(div10, "class", "svelte-9f6q4c"); + attr(div11, "class", "modal__row svelte-9f6q4c"); + attr(div12, "class", "svelte-9f6q4c"); option0.__value = "2"; option0.value = option0.__value; option1.__value = "4"; option1.value = option1.__value; if (/*$settings*/ ctx[3].tabSize === void 0) add_render_callback(() => /*select_change_handler*/ ctx[13].call(select)); - attr(div13, "class", "modal__row svelte-1ibrghz"); + attr(div13, "class", "modal__row svelte-9f6q4c"); attr(div14, "class", "modal__section"); - attr(div16, "class", "modal__title svelte-1ibrghz"); - attr(div17, "class", "svelte-1ibrghz"); + attr(div16, "class", "modal__title svelte-9f6q4c"); + attr(div17, "class", "svelte-9f6q4c"); toggle_class(div17, "red", !/*$settings*/ ctx[3].active); - attr(div18, "class", "modal__row svelte-1ibrghz"); - attr(div19, "class", "svelte-1ibrghz"); - attr(div20, "class", "modal__row svelte-1ibrghz"); - attr(div21, "class", "svelte-1ibrghz"); - attr(div22, "class", "truncate svelte-1ibrghz"); - attr(div23, "class", "modal__row saveLocation svelte-1ibrghz"); - attr(div24, "class", "blacklist svelte-1ibrghz"); + attr(div18, "class", "modal__row svelte-9f6q4c"); + attr(div19, "class", "svelte-9f6q4c"); + attr(div20, "class", "modal__row svelte-9f6q4c"); + attr(div21, "class", "svelte-9f6q4c"); + 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, "spellcheck", "false"); textarea.value = /*blacklisted*/ ctx[2]; textarea.disabled = textarea_disabled_value = /*$state*/ ctx[4].includes("blacklist-saving") || /*blacklistSaving*/ ctx[1]; - attr(textarea, "class", "svelte-1ibrghz"); - attr(div25, "class", "modal__row modal__row--wrap svelte-1ibrghz"); + attr(textarea, "class", "svelte-9f6q4c"); + attr(div25, "class", "modal__row modal__row--wrap svelte-9f6q4c"); attr(div26, "class", "modal__section"); - attr(div27, "class", "modal__title svelte-1ibrghz"); + attr(div27, "class", "modal__title svelte-9f6q4c"); attr(a0, "href", "https://github.com/quoid/userscripts"); attr(a1, "href", "https://apps.apple.com/us/app/userscripts/id1463298887"); attr(a2, "href", "https://github.com/quoid/userscripts#support"); - attr(p, "class", "svelte-1ibrghz"); + attr(p, "class", "svelte-9f6q4c"); attr(div28, "class", "modal__section"); - attr(div29, "class", "modal svelte-1ibrghz"); - attr(div30, "class", "settings svelte-1ibrghz"); + attr(div29, "class", "modal svelte-9f6q4c"); + attr(div30, "class", "settings svelte-9f6q4c"); }, m(target, anchor) { insert(target, div30, anchor); @@ -19643,7 +19896,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o const val = blacklist.value.split(",").map(item => item.trim()).filter(n => n); // compare blacklist input to saved blacklist - if ([...val].sort().toString() != [...$settings.blacklist].sort().toString()) { + if ([...val].sort().toString() !== [...$settings.blacklist].sort().toString()) { settings.updateSingleSetting("blacklist", val); // when blacklistSaving, visual indication of saving occurs on element @@ -19683,10 +19936,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }); } - const click_handler_8 = () => { - state.remove("settings"); - }; - + const click_handler_8 = () => state.remove("settings"); let blacklisted; $$self.$$.update = () => { @@ -19852,7 +20102,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o return child_ctx; } - // (107:0) {#if $state.includes("init")} + // (65:0) {#if $state.includes("init")} function create_if_block_1$2(ctx) { let div; let html_tag; @@ -19913,14 +20163,14 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (112:8) {:else} + // (70:8) {:else} function create_else_block$1(ctx) { let span; return { c() { span = element("span"); - span.textContent = "Initializing page..."; + span.textContent = "Initializing app..."; attr(span, "class", "svelte-17m9goi"); }, m(target, anchor) { @@ -19932,14 +20182,14 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (110:8) {#if $state.includes("init-error")} + // (68:8) {#if $state.includes("init-error")} function create_if_block_2$1(ctx) { let span; return { c() { span = element("span"); - span.textContent = "Failed to initialize app, check the console!"; + span.textContent = "Failed to initialize app, check the browser console"; attr(span, "class", "svelte-17m9goi"); }, m(target, anchor) { @@ -19951,7 +20201,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (122:4) {#each $notifications as item (item.id)} + // (80:4) {#each $notifications as item (item.id)} function create_each_block$1(key_1, ctx) { let first; let notification; @@ -19999,7 +20249,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }; } - // (126:0) {#if $state.includes("settings")} + // (84:0) {#if $state.includes("settings")} function create_if_block$8(ctx) { let settings_1; let current; @@ -20224,7 +20474,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o component_subscribe($$self, log, $$value => $$invalidate(4, $log = $$value)); component_subscribe($$self, state, $$value => $$invalidate(0, $state = $$value)); component_subscribe($$self, notifications, $$value => $$invalidate(1, $notifications = $$value)); - let logger = []; + const logger = []; // app proportions can get messed up when opening/closing new tabs async function windowResize() { @@ -20236,6 +20486,13 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o document.documentElement.removeAttribute("style"); } + // currently inactive, but could be used to globally prevent auto text replacement in app + // function preventAutoTextReplacements(e) { + // if (e.inputType === "insertReplacementText" && e.data === ". ") { + // e.preventDefault(); + // e.target.value += " "; + // } + // } onMount(async () => { log.add("Requesting initialization data", "info", false); const initData = await browser.runtime.sendNativeMessage({ name: "PAGE_INIT_DATA" }); diff --git a/extension/Userscripts Extension/Resources/popup.html b/extension/Userscripts Extension/Resources/popup.html index 27c86ca9..8e3c7eae 100644 --- a/extension/Userscripts Extension/Resources/popup.html +++ b/extension/Userscripts Extension/Resources/popup.html @@ -1 +1 @@ -Userscripts Safari
\ No newline at end of file +Userscripts Safari
\ No newline at end of file diff --git a/extension/Userscripts Extension/Resources/popup.js b/extension/Userscripts Extension/Resources/popup.js index 94409f66..90817eae 100644 --- a/extension/Userscripts Extension/Resources/popup.js +++ b/extension/Userscripts Extension/Resources/popup.js @@ -1115,13 +1115,13 @@ return { c() { div = element("div"); - attr(div, "class", div_class_value = "script__tag " + (/*type*/ ctx[0] ? "script__tag--" + /*type*/ ctx[0] : "") + " svelte-p9vdxd"); + attr(div, "class", div_class_value = "script__tag " + `script__tag--${/*type*/ ctx[0]}` + " svelte-1rzbr97"); }, m(target, anchor) { insert(target, div, anchor); }, p(ctx, [dirty]) { - if (dirty & /*type*/ 1 && div_class_value !== (div_class_value = "script__tag " + (/*type*/ ctx[0] ? "script__tag--" + /*type*/ ctx[0] : "") + " svelte-p9vdxd")) { + if (dirty & /*type*/ 1 && div_class_value !== (div_class_value = "script__tag " + `script__tag--${/*type*/ ctx[0]}` + " svelte-1rzbr97")) { attr(div, "class", div_class_value); } }, @@ -1159,7 +1159,7 @@ c() { div = element("div"); div.textContent = "SUB"; - attr(div, "class", "subframe svelte-1lgjchf"); + attr(div, "class", "subframe svelte-51jdr7"); }, m(target, anchor) { insert(target, div, anchor); @@ -1184,7 +1184,12 @@ let mounted; let dispose; let if_block = /*subframe*/ ctx[3] && create_if_block$1(); - tag = new Tag({ props: { type: /*type*/ ctx[2] } }); + + tag = new Tag({ + props: { + type: /*request*/ ctx[4] ? "request" : /*type*/ ctx[2] + } + }); return { c() { @@ -1197,9 +1202,9 @@ if (if_block) if_block.c(); t3 = space(); create_component(tag.$$.fragment); - attr(span, "class", "svelte-1lgjchf"); - attr(div0, "class", "truncate svelte-1lgjchf"); - attr(div1, "class", div1_class_value = "item " + (/*enabled*/ ctx[0] ? "enabled" : "disabled") + " svelte-1lgjchf"); + attr(span, "class", "svelte-51jdr7"); + attr(div0, "class", "truncate svelte-51jdr7"); + attr(div1, "class", div1_class_value = "item " + (/*enabled*/ ctx[0] ? "enabled" : "disabled") + " svelte-51jdr7"); }, m(target, anchor) { insert(target, div1, anchor); @@ -1214,7 +1219,7 @@ current = true; if (!mounted) { - dispose = listen(div1, "click", /*click_handler*/ ctx[4]); + dispose = listen(div1, "click", /*click_handler*/ ctx[5]); mounted = true; } }, @@ -1233,10 +1238,10 @@ } const tag_changes = {}; - if (dirty & /*type*/ 4) tag_changes.type = /*type*/ ctx[2]; + if (dirty & /*request, type*/ 20) tag_changes.type = /*request*/ ctx[4] ? "request" : /*type*/ ctx[2]; tag.$set(tag_changes); - if (!current || dirty & /*enabled*/ 1 && div1_class_value !== (div1_class_value = "item " + (/*enabled*/ ctx[0] ? "enabled" : "disabled") + " svelte-1lgjchf")) { + if (!current || dirty & /*enabled*/ 1 && div1_class_value !== (div1_class_value = "item " + (/*enabled*/ ctx[0] ? "enabled" : "disabled") + " svelte-51jdr7")) { attr(div1, "class", div1_class_value); } }, @@ -1264,6 +1269,7 @@ let { name } = $$props; let { type } = $$props; let { subframe } = $$props; + let { request = false } = $$props; function click_handler(event) { bubble($$self, event); @@ -1274,9 +1280,10 @@ if ("name" in $$props) $$invalidate(1, name = $$props.name); if ("type" in $$props) $$invalidate(2, type = $$props.type); if ("subframe" in $$props) $$invalidate(3, subframe = $$props.subframe); + if ("request" in $$props) $$invalidate(4, request = $$props.request); }; - return [enabled, name, type, subframe, click_handler]; + return [enabled, name, type, subframe, request, click_handler]; } class PopupItem extends SvelteComponent { @@ -1287,7 +1294,8 @@ enabled: 0, name: 1, type: 2, - subframe: 3 + subframe: 3, + request: 4 }); } } @@ -1335,7 +1343,7 @@ }; } - // (33:8) {#if loading && showLoaderOnDisabled} + // (34:8) {#if loading && showLoaderOnDisabled} function create_if_block$2(ctx) { let loader; let current; @@ -1377,7 +1385,7 @@ }; } - // (36:18)
+ // (37:18)
function fallback_block(ctx) { let div; @@ -1576,7 +1584,7 @@ return child_ctx; } - // (25:0) {:else} + // (27:0) {:else} function create_else_block$1(ctx) { let div2; let html_tag; @@ -1598,7 +1606,7 @@ br = element("br"); t2 = space(); div0 = element("div"); - div0.textContent = "Check Again"; + div0.textContent = "Check Updates"; html_tag = new HtmlTag(t0); attr(div0, "class", "link svelte-1v987ms"); attr(div1, "class", "svelte-1v987ms"); @@ -1633,7 +1641,7 @@ }; } - // (9:0) {#if updates.length} + // (10:0) {#if updates.length} function create_if_block$3(ctx) { let each_blocks = []; let each_1_lookup = new Map(); @@ -1708,7 +1716,7 @@ }; } - // (10:4) {#each updates as item (item.name)} + // (11:4) {#each updates as item (item.name)} function create_each_block(key_1, ctx) { let div1; let div0; @@ -1881,7 +1889,7 @@ return child_ctx; } - // (16:25) + // (17:25) function create_if_block_1(ctx) { let ul; let li; @@ -1938,14 +1946,14 @@ t11 = space(); button1 = element("button"); button1.textContent = "Install"; - attr(li, "class", "userscript--name svelte-tdpz3j"); - attr(ul, "class", "svelte-tdpz3j"); - attr(div0, "class", "badge--icon svelte-tdpz3j"); - attr(div1, "class", "badge--text svelte-tdpz3j"); - attr(div2, "class", "badge svelte-tdpz3j"); - attr(button0, "class", "cancel svelte-tdpz3j"); - attr(button1, "class", "install svelte-tdpz3j"); - attr(div3, "class", "buttons svelte-tdpz3j"); + attr(li, "class", "userscript--name svelte-p5i392"); + attr(ul, "class", "svelte-p5i392"); + attr(div0, "class", "badge--icon svelte-p5i392"); + attr(div1, "class", "badge--text svelte-p5i392"); + attr(div2, "class", "badge svelte-p5i392"); + attr(button0, "class", "cancel svelte-p5i392"); + attr(button1, "class", "install svelte-p5i392"); + attr(div3, "class", "buttons svelte-p5i392"); }, m(target, anchor) { insert(target, ul, anchor); @@ -2072,7 +2080,7 @@ }; } - // (10:4) {#if installError} + // (11:4) {#if installError} function create_if_block$4(ctx) { let div1; let html_tag; @@ -2080,19 +2088,20 @@ let div0; let t2; let p; + let t3; return { c() { div1 = element("div"); t0 = space(); div0 = element("div"); - div0.textContent = "The usercript can not be installed."; + div0.textContent = "Couldn't install userscript"; t2 = space(); p = element("p"); - p.textContent = "Error: OK GO"; + t3 = text(/*installError*/ ctx[1]); html_tag = new HtmlTag(t0); - attr(p, "class", "svelte-tdpz3j"); - attr(div1, "class", "install__error svelte-tdpz3j"); + attr(p, "class", "svelte-p5i392"); + attr(div1, "class", "install__error svelte-p5i392"); }, m(target, anchor) { insert(target, div1, anchor); @@ -2101,15 +2110,18 @@ append(div1, div0); append(div1, t2); append(div1, p); + append(p, t3); + }, + p(ctx, dirty) { + if (dirty & /*installError*/ 2) set_data(t3, /*installError*/ ctx[1]); }, - p: noop, d(detaching) { if (detaching) detach(div1); } }; } - // (19:12) {#if userscript.description} + // (20:12) {#if userscript.description} function create_if_block_6(ctx) { let li; let t_value = /*userscript*/ ctx[0].description + ""; @@ -2119,7 +2131,7 @@ c() { li = element("li"); t = text(t_value); - attr(li, "class", "userscript--description svelte-tdpz3j"); + attr(li, "class", "userscript--description svelte-p5i392"); }, m(target, anchor) { insert(target, li, anchor); @@ -2134,7 +2146,7 @@ }; } - // (22:12) {#if userscript.match} + // (23:12) {#if userscript.match} function create_if_block_5(ctx) { let li; let div; @@ -2157,8 +2169,8 @@ each_blocks[i].c(); } - attr(div, "class", "svelte-tdpz3j"); - attr(li, "class", "userscript--field svelte-tdpz3j"); + attr(div, "class", "svelte-p5i392"); + attr(li, "class", "userscript--field svelte-p5i392"); }, m(target, anchor) { insert(target, li, anchor); @@ -2200,7 +2212,7 @@ }; } - // (25:20) {#each userscript.match as match} + // (26:20) {#each userscript.match as match} function create_each_block_3(ctx) { let div; let t_value = /*match*/ ctx[13] + ""; @@ -2210,7 +2222,7 @@ c() { div = element("div"); t = text(t_value); - attr(div, "class", "truncate svelte-tdpz3j"); + attr(div, "class", "truncate svelte-p5i392"); }, m(target, anchor) { insert(target, div, anchor); @@ -2225,7 +2237,7 @@ }; } - // (30:12) {#if userscript.include} + // (31:12) {#if userscript.include} function create_if_block_4(ctx) { let li; let div; @@ -2248,8 +2260,8 @@ each_blocks[i].c(); } - attr(div, "class", "svelte-tdpz3j"); - attr(li, "class", "userscript--field svelte-tdpz3j"); + attr(div, "class", "svelte-p5i392"); + attr(li, "class", "userscript--field svelte-p5i392"); }, m(target, anchor) { insert(target, li, anchor); @@ -2291,7 +2303,7 @@ }; } - // (33:20) {#each userscript.include as include} + // (34:20) {#each userscript.include as include} function create_each_block_2(ctx) { let div; let t_value = /*include*/ ctx[10] + ""; @@ -2301,7 +2313,7 @@ c() { div = element("div"); t = text(t_value); - attr(div, "class", "truncate svelte-tdpz3j"); + attr(div, "class", "truncate svelte-p5i392"); }, m(target, anchor) { insert(target, div, anchor); @@ -2316,7 +2328,7 @@ }; } - // (38:12) {#if userscript.require} + // (39:12) {#if userscript.require} function create_if_block_3(ctx) { let li; let div; @@ -2339,8 +2351,8 @@ each_blocks[i].c(); } - attr(div, "class", "svelte-tdpz3j"); - attr(li, "class", "userscript--field svelte-tdpz3j"); + attr(div, "class", "svelte-p5i392"); + attr(li, "class", "userscript--field svelte-p5i392"); }, m(target, anchor) { insert(target, li, anchor); @@ -2382,7 +2394,7 @@ }; } - // (41:20) {#each userscript.require as require} + // (42:20) {#each userscript.require as require} function create_each_block_1(ctx) { let div; let t_value = /*require*/ ctx[7] + ""; @@ -2392,7 +2404,7 @@ c() { div = element("div"); t = text(t_value); - attr(div, "class", "truncate svelte-tdpz3j"); + attr(div, "class", "truncate svelte-p5i392"); }, m(target, anchor) { insert(target, div, anchor); @@ -2407,7 +2419,7 @@ }; } - // (46:12) {#if userscript.grant} + // (47:12) {#if userscript.grant} function create_if_block_2(ctx) { let li; let div; @@ -2430,8 +2442,8 @@ each_blocks[i].c(); } - attr(div, "class", "svelte-tdpz3j"); - attr(li, "class", "userscript--field svelte-tdpz3j"); + attr(div, "class", "svelte-p5i392"); + attr(li, "class", "userscript--field svelte-p5i392"); }, m(target, anchor) { insert(target, li, anchor); @@ -2473,7 +2485,7 @@ }; } - // (49:20) {#each userscript.grant as grant} + // (50:20) {#each userscript.grant as grant} function create_each_block$1(ctx) { let div; let t_value = /*grant*/ ctx[4] + ""; @@ -2483,7 +2495,7 @@ c() { div = element("div"); t = text(t_value); - attr(div, "class", "svelte-tdpz3j"); + attr(div, "class", "svelte-p5i392"); }, m(target, anchor) { insert(target, div, anchor); @@ -2513,7 +2525,7 @@ c() { div = element("div"); if (if_block) if_block.c(); - attr(div, "class", "view--install svelte-tdpz3j"); + attr(div, "class", "view--install svelte-p5i392"); }, m(target, anchor) { insert(target, div, anchor); @@ -2581,7 +2593,7 @@ return child_ctx; } - // (31:0) {:else} + // (33:0) {:else} function create_else_block$2(ctx) { let div; @@ -2589,7 +2601,7 @@ c() { div = element("div"); div.textContent = "No valid files found in directory"; - attr(div, "class", "none"); + attr(div, "class", "none svelte-rd8r5o"); }, m(target, anchor) { insert(target, div, anchor); @@ -2603,7 +2615,7 @@ }; } - // (19:0) {#if allItems.length} + // (20:0) {#if allItems.length} function create_if_block$5(ctx) { let div; let each_blocks = []; @@ -2627,7 +2639,7 @@ each_blocks[i].c(); } - attr(div, "class", div_class_value = "items view--all " + (/*rowColorsAll*/ ctx[2] || "") + " svelte-d4295n"); + attr(div, "class", div_class_value = "items view--all " + (/*rowColorsAll*/ ctx[2] || "") + " svelte-rd8r5o"); toggle_class(div, "disabled", /*disabled*/ ctx[4]); }, m(target, anchor) { @@ -2647,7 +2659,7 @@ check_outros(); } - if (!current || dirty & /*rowColorsAll*/ 4 && div_class_value !== (div_class_value = "items view--all " + (/*rowColorsAll*/ ctx[2] || "") + " svelte-d4295n")) { + if (!current || dirty & /*rowColorsAll*/ 4 && div_class_value !== (div_class_value = "items view--all " + (/*rowColorsAll*/ ctx[2] || "") + " svelte-rd8r5o")) { attr(div, "class", div_class_value); } @@ -2681,7 +2693,7 @@ }; } - // (21:8) {#each list as item (item.filename)} + // (22:8) {#each list as item (item.filename)} function create_each_block$2(key_1, ctx) { let first; let popupitem; @@ -2696,7 +2708,8 @@ enabled: !/*item*/ ctx[6].disabled, name: /*item*/ ctx[6].name, subframe: /*item*/ ctx[6].subframe, - type: /*item*/ ctx[6].type + type: /*item*/ ctx[6].type, + request: /*item*/ ctx[6].request ? true : false } }); @@ -2722,6 +2735,7 @@ if (dirty & /*list*/ 8) popupitem_changes.name = /*item*/ ctx[6].name; if (dirty & /*list*/ 8) popupitem_changes.subframe = /*item*/ ctx[6].subframe; if (dirty & /*list*/ 8) popupitem_changes.type = /*item*/ ctx[6].type; + if (dirty & /*list*/ 8) popupitem_changes.request = /*item*/ ctx[6].request ? true : false; popupitem.$set(popupitem_changes); }, i(local) { @@ -2863,12 +2877,12 @@ return child_ctx; } - // (487:0) {#if !active} + // (500:0) {#if !active} function create_if_block_10(ctx) { return { c: noop, m: noop, d: noop }; } - // (490:0) {#if showInstallPrompt} + // (503:0) {#if showInstallPrompt} function create_if_block_9(ctx) { let div; let t0; @@ -2910,7 +2924,7 @@ }; } - // (495:0) {#if error} + // (508:0) {#if error} function create_if_block_8(ctx) { let div; let t0; @@ -2960,7 +2974,7 @@ }; } - // (517:8) {:else} + // (536:8) {:else} function create_else_block$3(ctx) { let div; let each_blocks = []; @@ -3033,7 +3047,7 @@ }; } - // (515:35) + // (534:35) function create_if_block_7(ctx) { let div; @@ -3055,7 +3069,7 @@ }; } - // (511:28) + // (524:28) function create_if_block_6$1(ctx) { let div; let t0; @@ -3066,7 +3080,7 @@ return { c() { div = element("div"); - t0 = text("Something went wrong: "); + t0 = text("Something went wrong: \n "); span = element("span"); span.textContent = "click to retry"; attr(span, "class", "link svelte-1w80sz6"); @@ -3093,7 +3107,7 @@ }; } - // (509:8) {#if inactive} + // (522:8) {#if inactive} function create_if_block_5$1(ctx) { let div; @@ -3115,7 +3129,7 @@ }; } - // (506:4) {#if loading} + // (519:4) {#if loading} function create_if_block_4$1(ctx) { let loader; let current; @@ -3155,7 +3169,7 @@ }; } - // (519:16) {#each list as item (item.filename)} + // (538:16) {#each list as item (item.filename)} function create_each_block$3(key_1, ctx) { let first; let popupitem; @@ -3170,7 +3184,8 @@ enabled: !/*item*/ ctx[48].disabled, name: /*item*/ ctx[48].name, subframe: /*item*/ ctx[48].subframe, - type: /*item*/ ctx[48].type + type: /*item*/ ctx[48].type, + request: /*item*/ ctx[48].request ? true : false } }); @@ -3196,6 +3211,7 @@ if (dirty[0] & /*list*/ 4194304) popupitem_changes.name = /*item*/ ctx[48].name; if (dirty[0] & /*list*/ 4194304) popupitem_changes.subframe = /*item*/ ctx[48].subframe; if (dirty[0] & /*list*/ 4194304) popupitem_changes.type = /*item*/ ctx[48].type; + if (dirty[0] & /*list*/ 4194304) popupitem_changes.request = /*item*/ ctx[48].request ? true : false; popupitem.$set(popupitem_changes); }, i(local) { @@ -3214,7 +3230,7 @@ }; } - // (532:0) {#if !inactive && platform === "macos"} + // (552:0) {#if !inactive && platform === "macos"} function create_if_block_3$1(ctx) { let div1; let div0; @@ -3247,7 +3263,7 @@ }; } - // (567:18) + // (589:18) function create_if_block_2$1(ctx) { let view; let current; @@ -3297,7 +3313,7 @@ }; } - // (553:22) + // (575:22) function create_if_block_1$1(ctx) { let view; let current; @@ -3347,7 +3363,7 @@ }; } - // (537:0) {#if showUpdates} + // (559:0) {#if showUpdates} function create_if_block$6(ctx) { let view; let current; @@ -3400,7 +3416,7 @@ }; } - // (568:4) {showAll = false; refreshView()}} showLoaderOnDisabled={false} > + // (590:4) { showAll = false; refreshView(); }} showLoaderOnDisabled={false} > function create_default_slot_2(ctx) { let allitemsview; let current; @@ -3440,7 +3456,7 @@ }; } - // (554:4) showInstall = false} showLoaderOnDisabled={true} > + // (576:4) showInstall = false} showLoaderOnDisabled={true} > function create_default_slot_1(ctx) { let installview; let current; @@ -3484,7 +3500,7 @@ }; } - // (538:4) showUpdates = false} showLoaderOnDisabled={true} abortClick={abortUpdates} abort={showUpdates} > + // (560:4) showUpdates = false} showLoaderOnDisabled={true} abortClick={abortUpdates} abort={showUpdates} > function create_default_slot(ctx) { let updateview; let current; @@ -3921,7 +3937,7 @@ try { lastUpdateCheckObj = await browser.storage.local.get(["lastUpdateCheck"]); } catch(error) { - console.error("Error checking extension storage " + error); + console.error(`Error checking extension storage ${error}`); return false; } @@ -3949,7 +3965,7 @@ return false; } - console.log((timestampMs - lastUpdateCheck) / (1000 * 60 * 60) + " hours have passed"); + console.log(`${(timestampMs - lastUpdateCheck) / (1000 * 60 * 60)} hours have passed`); console.log("running update check"); // otherwise run the check @@ -4024,12 +4040,12 @@ async function updateItem(item) { $$invalidate(3, disabled = true); - const tabs = await browser.tabs.query({ currentWindow: true, active: true }); - const url = tabs[0].url; + const currentTab = await browser.tabs.getCurrent(); + const url = currentTab.url; const frameUrls = []; if (url) { - const frames = await browser.webNavigation.getAllFrames({ tabId: tabs[0].id }); + const frames = await browser.webNavigation.getAllFrames({ tabId: currentTab.id }); frames.forEach(frame => frameUrls.push(frame.url)); } @@ -4111,7 +4127,7 @@ $$invalidate(19, showAll = true); $$invalidate(20, allItems = response.items); } else if (response.error) { - console.log("Error opening save location: " + response.error); + console.log(`Error opening save location: ${response.error}`); $$invalidate(0, error = response.error); } @@ -4126,7 +4142,7 @@ try { pltfm = await browser.runtime.sendNativeMessage({ name: "REQ_PLATFORM" }); } catch(error) { - console.log("Error for pltfm promise: " + error); + console.log(`Error for pltfm promise: ${error}`); $$invalidate(11, initError = true); $$invalidate(2, loading = false); return; @@ -4148,7 +4164,7 @@ try { init = await browser.runtime.sendNativeMessage({ name: "POPUP_INIT" }); } catch(error) { - console.log("Error for init promise: " + error); + console.log(`Error for init promise: ${error}`); $$invalidate(11, initError = true); $$invalidate(2, loading = false); return; @@ -4163,15 +4179,26 @@ $$invalidate(1, active = init.initData.active === "true" ? true : false); } + // refresh session rules + browser.runtime.sendMessage({ name: "REFRESH_SESSION_RULES" }); + + // refresh context-menu scripts + browser.runtime.sendMessage({ name: "REFRESH_CONTEXT_MENU_SCRIPTS" }); + // set popup height resize(); // get matches const extensionPageUrl = browser.runtime.getURL("page.html"); - const tabs = await browser.tabs.query({ currentWindow: true, active: true }); - const url = tabs[0].url; - const frameUrls = []; + const currentTab = await browser.tabs.getCurrent(); + const url = currentTab.url; + + if (!url) { + $$invalidate(2, loading = false); + $$invalidate(3, disabled = false); + return; + } if (url === extensionPageUrl) { // disable popup on extension page @@ -4181,18 +4208,29 @@ return; } - if (url) { - const frames = await browser.webNavigation.getAllFrames({ tabId: tabs[0].id }); - frames.forEach(frame => frameUrls.push(frame.url)); + const frameUrls = new Set(); + const frames = await browser.webNavigation.getAllFrames({ tabId: currentTab.id }); + + for (let i = 0; i < frames.length; i++) { + const frameUrl = frames[i].url; + + if (frameUrl !== url && frameUrl.startsWith("http")) { + frameUrls.add(frameUrl); + } } - const message = { name: "POPUP_MATCHES", url, frameUrls }; + const message = { + name: "POPUP_MATCHES", + url, + frameUrls: Array.from(frameUrls) + }; + let matches; try { matches = await browser.runtime.sendNativeMessage(message); } catch(error) { - console.log("Error for matches promise: " + error); // response = await browser.runtime.sendMessage(message); + console.log(`Error for matches promise: ${error}`); // response = await browser.runtime.sendMessage(message); $$invalidate(11, initError = true); $$invalidate(2, loading = false); return; @@ -4221,7 +4259,7 @@ $$invalidate(21, abort = true); updatesResponse = await browser.runtime.sendNativeMessage({ name: "POPUP_UPDATES" }); } catch(error) { - console.error("Error for updates promise: " + error); + console.error(`Error for updates promise: ${error}`); $$invalidate(11, initError = true); $$invalidate(2, loading = false); $$invalidate(21, abort = false); @@ -4256,10 +4294,10 @@ // 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(tabs[0].id, { name: "USERSCRIPT_INSTALL_00" }); + const response = await browser.tabs.sendMessage(currentTab.id, { name: "USERSCRIPT_INSTALL_00" }); if (response.error) { - console.log("Error checking .user.js url: " + response.error); + console.log(`Error checking .user.js url: ${response.error}`); $$invalidate(0, error = response.error); } else if (!response.invalid) { // the response will contain the string to display @@ -4276,8 +4314,6 @@ async function resize() { if (!platform || platform === "macos") return; - - // special styling for ipados and split views clearTimeout(resizeTimer); resizeTimer = setTimeout( @@ -4308,8 +4344,8 @@ if (err) addHeight += err.offsetHeight; windowHeight = window.outerHeight - (headerHeight + addHeight); - $$invalidate(7, main.style.height = windowHeight + "px", main); - $$invalidate(7, main.style.paddingBottom = headerHeight + addHeight + "px", main); + $$invalidate(7, main.style.height = `${windowHeight}px`, main); + $$invalidate(7, main.style.paddingBottom = `${headerHeight + addHeight}px`, main); }, 25 ); @@ -4323,10 +4359,10 @@ $$invalidate(16, showInstall = true); // get the active tab - const tabs = await browser.tabs.query({ currentWindow: true, active: true }); + const currentTab = await browser.tabs.getCurrent(); // send content script a message on the active tab - const response = await browser.tabs.sendMessage(tabs[0].id, { name: "USERSCRIPT_INSTALL_01" }); + 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 @@ -4336,7 +4372,7 @@ // the content script will then send a response here // if the response includes an error, display it in the view if (response.error) { - console.log("Can not install userscript: " + response.error); + console.log(`Can not install userscript: ${response.error}`); $$invalidate(18, installViewUserscriptError = response.error); } else { $$invalidate(17, installViewUserscript = response); @@ -4356,10 +4392,10 @@ $$invalidate(16, showInstall = false); // get the active tab - const tabs = await browser.tabs.query({ currentWindow: true, active: true }); + 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(tabs[0].id, { name: "USERSCRIPT_INSTALL_02" }); + const response = await browser.tabs.sendMessage(currentTab.id, { name: "USERSCRIPT_INSTALL_02" }); if (response.error) { $$invalidate(0, error = response.error); diff --git a/extension/Userscripts Extension/SafariWebExtensionHandler.swift b/extension/Userscripts Extension/SafariWebExtensionHandler.swift index 58c0079a..0c3bf641 100644 --- a/extension/Userscripts Extension/SafariWebExtensionHandler.swift +++ b/extension/Userscripts Extension/SafariWebExtensionHandler.swift @@ -15,7 +15,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { // send standard error when there's an issue parsing inbound message var inBoundError = false // these if/else if statement are formatted so that they can be neatly collapsed in Xcode - // typically the "else if" would be on the same line as the preceding statements close backet + // typically the "else if" would be on the same line as the preceding statements close bracket // ie. } else if { if name == "REQ_PLATFORM" { let platform = getPlatform() @@ -23,18 +23,31 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } else if name == "REQ_USERSCRIPTS" { if let url = message?["url"] as? String, let isTop = message?["isTop"] as? Bool { if - checkDefaultDirectories(), let matches = getInjectionFilenames(url), let code = getCode(matches, isTop) { - response.userInfo = [SFExtensionMessageKey: ["code": code]] + response.userInfo = [SFExtensionMessageKey: code] } else { - response.userInfo = [SFExtensionMessageKey: ["error": "couldn't get userscripts"]] + response.userInfo = [SFExtensionMessageKey: ["error": "REQ_USERSCRIPTS failed"]] } } else { inBoundError = true } } + else if name == "REQ_REQUESTS" { + if let requestScripts = getRequestScripts() { + response.userInfo = [SFExtensionMessageKey: requestScripts] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get requestScripts"]] + } + } + else if name == "REQ_CONTEXT_MENU_SCRIPTS" { + if let contextMenuScripts = getContextMenuScripts() { + response.userInfo = [SFExtensionMessageKey: contextMenuScripts] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get contextMenuScripts"]] + } + } else if name == "POPUP_BADGE_COUNT" { #if os(macOS) if let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { @@ -73,32 +86,6 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get updates"]] } } - else if name == "POPUP_INITX" { - if let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { - let platform = getPlatform() - let manifest = getManifest() - if - checkDefaultDirectories(), - checkSettings(), - let files = getAllFiles(), - updateManifestMatches(files), - updateManifestRequired(files), - purgeManifest(files), - let matches = getPopupMatches(url, frameUrls), - let active = manifest.settings["active"], - let updates = checkForRemoteUpdates() - { - let r = [ - "active": active, "items": matches, "platform": platform, "updates": updates - ] as [String : Any] - response.userInfo = [SFExtensionMessageKey: r] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to run init sequence"]] - } - } else { - inBoundError = true - } - } else if name == "POPUP_UPDATE_ALL" { if popupUpdateAll(), let updates = checkForRemoteUpdates() { response.userInfo = [SFExtensionMessageKey: ["updates": updates]] @@ -201,7 +188,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } else if name == "PAGE_INIT_DATA" { #if os(macOS) - if let settings = getInitData() { + if let settings = getInitData(), checkDefaultDirectories() { response.userInfo = [SFExtensionMessageKey: settings] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get init data"]] diff --git a/extension/Userscripts.xcodeproj/project.pbxproj b/extension/Userscripts.xcodeproj/project.pbxproj index 426a0d70..972f026d 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 = 20; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS/Info.plist"; @@ -679,7 +679,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; 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 = 20; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS/Info.plist"; @@ -720,7 +720,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; 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 = 20; + CURRENT_PROJECT_VERSION = 24; 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.1; + MARKETING_VERSION = 1.2; 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 = 20; + CURRENT_PROJECT_VERSION = 24; 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.1; + MARKETING_VERSION = 1.2; 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 = 48; + CURRENT_PROJECT_VERSION = 52; 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.1.0; + MARKETING_VERSION = 4.2.0; 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 = 48; + CURRENT_PROJECT_VERSION = 52; 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.1.0; + MARKETING_VERSION = 4.2.0; 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 = 48; + CURRENT_PROJECT_VERSION = 52; 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.1.0; + MARKETING_VERSION = 4.2.0; 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 = 48; + CURRENT_PROJECT_VERSION = 52; 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.1.0; + MARKETING_VERSION = 4.2.0; 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 44690877..c1ab95dc 100644 --- a/extension/UserscriptsTests/UserscriptsTests.swift +++ b/extension/UserscriptsTests/UserscriptsTests.swift @@ -88,6 +88,30 @@ class UserscriptsTests: XCTestCase { } XCTAssert(result.count == urls.count) } + + 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 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") + } + } + } + XCTAssert(result.count == urls.count) + } func testPerformanceExample() throws { // This is an example of a performance test case. diff --git a/gulpfile.js b/gulpfile.js index 6ad2f12b..8051388b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,28 +6,27 @@ const autoprefixer = require("autoprefixer"); const htmlmin = require("gulp-html-minifier-terser"); const dom = require("gulp-dom"); const rename = require("gulp-rename"); - -const directory = process.env.NODE_ENV === "popup" ? "popup" : "page"; +const directory = process.env.NODE_ENV === "popup" ? "popup" : "page"; const copyLocation = "./temp"; const destLocation = "./extension/Userscripts Extension/Resources"; // clone public directory to avoid prefixing development assets function copy() { - return src("./public/" + directory + "/**/*") + return src(`./public/${directory}/**/*`) .pipe(dest(copyLocation)); } -// autoprefix select stylesheets and overwrite in place +// autoprefix select stylesheets and overwrite in place, at copy location function autoprefix() { - return src([copyLocation + "/build/bundle.css", copyLocation + "/css/global.css"]) + return src([`${copyLocation}/build/bundle.css`, `${copyLocation}/css/global.css`]) .pipe(postcss([ autoprefixer({overrideBrowserslist: ["safari >= 13"]}) ])) .pipe(dest(file => file.base)); } -// inline assets +// inline assets, minify css and remove comment in monolithic html file function inlineAssets() { return src(`${copyLocation}/index.html`) .pipe(inline({ @@ -55,7 +54,7 @@ function bundleJS() { }); return result; }, false)) - .pipe(rename(directory + ".js")) + .pipe(rename(`${directory}.js`)) .pipe(dest(destLocation)); } @@ -69,11 +68,11 @@ function removeTags() { parent.removeChild(script); }); const f = this.createElement("script"); - f.setAttribute("src", directory + ".js"); + f.setAttribute("src", `${directory}.js`); this.body.appendChild(f); return this; }, false)) - .pipe(rename(directory + ".html")) + .pipe(rename(`${directory}.html`)) .pipe(dest(destLocation)); } diff --git a/package-lock.json b/package-lock.json index 60f86b4e..de4e9936 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "cm-show-invisibles": "^3.1.0", "codemirror": "^5.58.2", "del": "^6.0.0", - "eslint": "^7.11.0", + "eslint": "^8.10.0", "eslint-plugin-svelte3": "^2.7.3", "gulp": "^4.0.2", "gulp-dom": "^1.0.0", @@ -85,26 +85,31 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", - "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.0.tgz", + "integrity": "sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w==", "dev": true, "dependencies": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^12.1.0", + "debug": "^4.3.2", + "espree": "^9.3.1", + "globals": "^13.9.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "lodash": "^4.17.19", + "js-yaml": "^4.1.0", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/@eslint/eslintrc/node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -114,6 +119,38 @@ "node": ">= 4" } }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -280,9 +317,9 @@ "dev": true }, "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -314,10 +351,13 @@ } }, "node_modules/acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, "node_modules/acorn-walk": { "version": "6.2.0", @@ -353,15 +393,6 @@ "uri-js": "^4.2.2" } }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -769,15 +800,6 @@ "node": ">=0.10.0" } }, - "node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -1229,6 +1251,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/chalk/node_modules/color-convert": { @@ -1952,9 +1977,9 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -2281,12 +2306,6 @@ "integrity": "sha512-x5uZWXcVNYkTh4JubD7KSC1VMKz0vZwJUqVwY3ihsW0bst1BXDe494Uqbg3Y0fDGVjJqA8vEeGuvO5foyH2+qw==", "dev": true }, - "node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -2296,18 +2315,6 @@ "once": "^1.4.0" } }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", @@ -2506,46 +2513,44 @@ } }, "node_modules/eslint": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.11.0.tgz", - "integrity": "sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.10.0.tgz", + "integrity": "sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.0.0", - "@eslint/eslintrc": "^0.1.3", + "@eslint/eslintrc": "^1.2.0", + "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.0", - "esquery": "^1.2.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.19", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "table": "^5.2.3", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -2553,7 +2558,10 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-plugin-svelte3": { @@ -2563,78 +2571,126 @@ "dev": true }, "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" } }, "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "eslint-visitor-keys": "^2.0.0" }, "engines": { - "node": ">=6" + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" } }, "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, "engines": { - "node": ">=4" + "node": ">=10" } }, "node_modules/eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, "node_modules/eslint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true, "engines": { "node": ">= 4" } }, - "node_modules/espree": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", - "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.3.0" + "argparse": "^2.0.1" }, - "engines": { - "node": "^10.12.0 || >=12.0.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/espree": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", + "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", "dev": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.3.0" + }, "engines": { - "node": ">=4" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/esprima": { @@ -2651,9 +2707,9 @@ } }, "node_modules/esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -2663,9 +2719,9 @@ } }, "node_modules/esquery/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { "node": ">=4.0" @@ -2684,9 +2740,9 @@ } }, "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { "node": ">=4.0" @@ -3004,15 +3060,15 @@ } }, "node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "dependencies": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" }, "engines": { - "node": ">=4" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/file-uri-to-path": { @@ -3209,35 +3265,22 @@ } }, "node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "flatted": "^3.1.0", + "rimraf": "^3.0.2" }, "engines": { - "node": ">=4" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, "node_modules/flush-write-stream": { @@ -3553,15 +3596,18 @@ } }, "node_modules/globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", "dev": true, "dependencies": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby": { @@ -4362,19 +4408,10 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { "is-extglob": "^2.1.1" @@ -4983,6 +5020,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -6619,15 +6662,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -6922,12 +6956,15 @@ } }, "node_modules/regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" } }, "node_modules/relateurl": { @@ -7490,20 +7527,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -7875,41 +7898,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/string.prototype.trimend": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", @@ -8017,6 +8005,9 @@ "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/style-search": { @@ -8201,37 +8192,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/stylelint/node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/stylelint/node_modules/flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", - "dev": true - }, "node_modules/stylelint/node_modules/global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -8545,21 +8505,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/terser": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", @@ -8793,12 +8738,15 @@ } }, "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typedarray": { @@ -9305,18 +9253,6 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -9500,31 +9436,62 @@ } }, "@eslint/eslintrc": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", - "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.0.tgz", + "integrity": "sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w==", "dev": true, "requires": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^12.1.0", + "debug": "^4.3.2", + "espree": "^9.3.1", + "globals": "^13.9.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "lodash": "^4.17.19", + "js-yaml": "^4.1.0", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } } } }, + "@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -9672,9 +9639,9 @@ "dev": true }, "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", "dev": true }, "acorn-globals": { @@ -9696,10 +9663,11 @@ } }, "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} }, "acorn-walk": { "version": "6.2.0", @@ -9729,12 +9697,6 @@ "uri-js": "^4.2.2" } }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, "ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -10060,12 +10022,6 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, "async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -11027,9 +10983,9 @@ } }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" @@ -11302,12 +11258,6 @@ "integrity": "sha512-x5uZWXcVNYkTh4JubD7KSC1VMKz0vZwJUqVwY3ihsW0bst1BXDe494Uqbg3Y0fDGVjJqA8vEeGuvO5foyH2+qw==", "dev": true }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11317,15 +11267,6 @@ "once": "^1.4.0" } }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", @@ -11490,55 +11431,83 @@ } }, "eslint": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.11.0.tgz", - "integrity": "sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.10.0.tgz", + "integrity": "sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@eslint/eslintrc": "^0.1.3", + "@eslint/eslintrc": "^1.2.0", + "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.0", - "esquery": "^1.2.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.19", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "table": "^5.2.3", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } } } }, @@ -11549,55 +11518,55 @@ "dev": true }, "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", "dev": true, "requires": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } } }, "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "eslint-visitor-keys": "^2.0.0" }, "dependencies": { "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true } } }, "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true }, "espree": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", - "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", + "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", "dev": true, "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.3.0" } }, "esprima": { @@ -11607,18 +11576,18 @@ "dev": true }, "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { "estraverse": "^5.1.0" }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } @@ -11633,9 +11602,9 @@ }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } @@ -11906,12 +11875,12 @@ } }, "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" } }, "file-uri-to-path": { @@ -12077,31 +12046,19 @@ "dev": true }, "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "flatted": "^3.1.0", + "rimraf": "^3.0.2" } }, "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, "flush-write-stream": { @@ -12357,12 +12314,12 @@ } }, "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" } }, "globby": { @@ -12997,16 +12954,10 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -13493,6 +13444,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -14779,12 +14736,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -15031,9 +14982,9 @@ } }, "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "relateurl": { @@ -15475,17 +15426,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - } - }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -15799,34 +15739,6 @@ "safe-buffer": "~5.1.0" } }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, "string.prototype.trimend": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", @@ -16041,31 +15953,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", - "dev": true - }, "global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -16329,18 +16216,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - } - }, "terser": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", @@ -16530,9 +16405,9 @@ } }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, "typedarray": { @@ -16965,15 +16840,6 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", diff --git a/package.json b/package.json index 8cb81f9b..8e6c2a9d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "cm-show-invisibles": "^3.1.0", "codemirror": "^5.58.2", "del": "^6.0.0", - "eslint": "^7.11.0", + "eslint": "^8.10.0", "eslint-plugin-svelte3": "^2.7.3", "gulp": "^4.0.2", "gulp-dom": "^1.0.0", diff --git a/public/page/index.html b/public/page/index.html index 1afc287c..702ec971 100644 --- a/public/page/index.html +++ b/public/page/index.html @@ -6,8 +6,8 @@ Userscripts Safari - - + + diff --git a/public/popup/css/global.css b/public/popup/css/global.css index 342768c8..cce11fd5 100644 --- a/public/popup/css/global.css +++ b/public/popup/css/global.css @@ -39,12 +39,22 @@ body { body.ipados { width: 18rem; + height: 100vh; +} + +body.ipados #app { + height: 100%; } body.ios { + height: 100vh; width: 100vw; } +body.ios #app { + height: 100%; +} + noscript { display: block; } diff --git a/public/popup/css/reset.css b/public/popup/css/reset.css deleted file mode 100644 index 9ea2b45a..00000000 --- a/public/popup/css/reset.css +++ /dev/null @@ -1,53 +0,0 @@ -*, -*::before, -*::after { - box-sizing: border-box; -} - -body, -p { - margin: 0; -} - -ul { - list-style: none; - margin: 0; - padding: 0; -} - -button, -input, -select, -textarea { - font-family: inherit; - font-size: 100%; - line-height: inherit; - margin: 0; -} - -button, -input[type="button"], -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; -} - -input[type="search"] { - -webkit-appearance: textfield; -} - -::-webkit-input-placeholder { - color: inherit; - opacity: 0.54; -} - -::-webkit-search-decoration, -::-webkit-search-cancel-button, -::-webkit-search-results-button, -::-webkit-search-results-decoration { - -webkit-appearance: none; -} - -img { - border-style: none; -} diff --git a/public/popup/css/variables.css b/public/popup/css/variables.css deleted file mode 100644 index b6f01c06..00000000 --- a/public/popup/css/variables.css +++ /dev/null @@ -1,51 +0,0 @@ -:root { - --border-radius: 0.188rem; - --box-shadow: 0 0.5rem 1rem 0 rgba(0, 0, 0, 0.25); - --color-bg-primary: #323639; - --color-bg-secondary: #2f3337; - --color-script-highlighted: rgba(116, 178, 235, 0.1); - --color-black: #1d2023; - --color-blue: #74b1eb; - --color-green: #60f36c; - --color-grey: rgba(255, 255, 255, 0.15); - --color-red: #ff453a; - --color-yellow: #e4f360; - --letter-spacing-large: -0.031rem; - --letter-spacing-default: -0.029rem; - --letter-spacing-medium: -0.018rem; - --letter-spacing-small: -0.008rem; - --opacity-disabled: 0.3; - --text-color-primary: rgb(255, 255, 255); - --text-color-secondary: rgba(255, 255, 255, 0.65); - --text-color-disabled: rgba(255, 255, 255, 0.4); - --font-family: system-ui, -apple-system, "Helvetica Neue", "Helvetica", sans-serif; - --text-default: 1rem/1.5rem var(--font-family); - --text-large: 1.25rem/1.5rem var(--font-family); - --text-medium: 0.875rem/1.313rem var(--font-family); - --text-small: 0.719rem/1rem var(--font-family); - - /* editor variables */ - --editor-font: monaco, monospace; - --editor-font-size: 14px; - --editor-line-height: 24px; - --editor-invisible: rgba(255, 255, 255, 0.15); - --editor-active-line: var(--color-bg-secondary); - --editor-selected-bg: rgba(116, 178, 235, 0.35); - --editor-matched-highlight: rgba(116, 178, 235, 0.2); - --editor-search-highlight: rgba(255, 166, 0, 0.3); - --editor-number: #77e26a; - --editor-comment: rgba(255, 255, 255, 0.35); - --editor-def: #efc371; - --editor-default: #cdcfd1; - --editor-keyword: #96c3ed; - --editor-atom: #59ebf5; - --editor-operator: #8c99a7; - --editor-property: #e86c8a; - --editor-string: #f5eea2; - --editor-string-2: #cdabff; - --editor-error: var(--color-red); - --editor-cursor: #e3e7eb; - --editor-matching-bracket-color: #fff; - --editor-matching-bracket-border: var(--editor-number); - --editor-non-matching-bracket: var(--editor-error); -} diff --git a/public/popup/index.html b/public/popup/index.html index 779c5a41..0e790a80 100644 --- a/public/popup/index.html +++ b/public/popup/index.html @@ -5,8 +5,8 @@ Userscripts Safari - - + + diff --git a/public/page/css/reset.css b/public/shared/reset.css similarity index 100% rename from public/page/css/reset.css rename to public/shared/reset.css diff --git a/public/page/css/variables.css b/public/shared/variables.css similarity index 74% rename from public/page/css/variables.css rename to public/shared/variables.css index c894af7e..acdc671d 100644 --- a/public/page/css/variables.css +++ b/public/shared/variables.css @@ -1,13 +1,13 @@ :root { --border-radius: 0.188rem; - --box-shadow: 0 0.5rem 1rem 0 rgba(0, 0, 0, 0.25); + --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); --color-bg-primary: #323639; --color-bg-secondary: #2f3337; --color-script-highlighted: #364049; /* rgba(116, 178, 235, 0.1); */ --color-black: #1d2023; --color-blue: #74b1eb; --color-green: #60f36c; - --color-grey: rgba(255, 255, 255, 0.15); + --color-grey: rgb(255 255 255 / 0.15); --color-red: #ff453a; --color-yellow: #e4f360; --letter-spacing-large: -0.031rem; @@ -15,9 +15,9 @@ --letter-spacing-medium: -0.018rem; --letter-spacing-small: -0.008rem; --opacity-disabled: 0.3; - --text-color-primary: rgb(255, 255, 255); - --text-color-secondary: rgba(255, 255, 255, 0.65); - --text-color-disabled: rgba(255, 255, 255, 0.4); + --text-color-primary: rgb(255 255 255); + --text-color-secondary: rgb(255 255 255 / 0.65); + --text-color-disabled: rgb(255 255 255 / 0.4); --font-family: system-ui, -apple-system, "Helvetica Neue", "Helvetica", sans-serif; --text-default: 1rem/1.5rem var(--font-family); --text-large: 1.25rem/1.5rem var(--font-family); @@ -28,13 +28,13 @@ --editor-font: monaco, monospace; --editor-font-size: 14px; --editor-line-height: 24px; - --editor-invisible: rgba(255, 255, 255, 0.15); + --editor-invisible: rgb(255 255 255 / 0.15); --editor-active-line: var(--color-bg-secondary); - --editor-selected-bg: rgba(116, 178, 235, 0.35); - --editor-matched-highlight: rgba(116, 178, 235, 0.2); - --editor-search-highlight: rgba(255, 166, 0, 0.3); + --editor-selected-bg: rgb(116 178 235 / 0.35); + --editor-matched-highlight: rgb(116 178 235 / 0.2); + --editor-search-highlight: rgb(255 166 0 / 0.3); --editor-number: #77e26a; - --editor-comment: rgba(255, 255, 255, 0.35); + --editor-comment: rgb(255 255 255 / 0.35); --editor-def: #efc371; --editor-default: #cdcfd1; --editor-keyword: #96c3ed; diff --git a/rollup.config.js b/rollup.config.js index 01bcaae2..fcd4eb41 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,3 +1,4 @@ +import fs from "fs"; import svelte from "rollup-plugin-svelte"; import resolve from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; @@ -6,11 +7,12 @@ import css from "rollup-plugin-css-only"; import inlineSvg from "rollup-plugin-inline-svg"; import multi from "@rollup/plugin-multi-entry"; - const production = !process.env.ROLLUP_WATCH; -const directory = process.env.NODE_ENV === "popup" ? "popup" : "page"; -let input = ["src/" + directory + "/dev.js", "src/" + directory + "/main.js"]; -if (production) input = "src/" + directory + "/main.js"; +const directory = process.env.NODE_ENV === "popup" ? "popup" : "page"; + +//let input = [`src/${directory}/dev.js`, `src/${directory}/main.js`]; +let input = ["src/shared/dev.js", `src/${directory}/main.js`]; +if (production) input = `src/${directory}/main.js`; function serve() { let server; @@ -24,7 +26,7 @@ function serve() { if (server) return; server = require("child_process").spawn( "npm", - ["run", "start:" + directory, "--", "--dev"], + ["run", `start:${directory}`, "--", "--dev"], { stdio: ["ignore", "inherit", "inherit"], shell: true @@ -42,14 +44,16 @@ export default { sourcemap: !production, format: "iife", name: "app", - file: "public/" + directory + "/build/bundle.js" + file: `public/${directory}/build/bundle.js` }, inlineDynamicImports: true, plugins: [ multi(), css({ - output: "public/" + directory + "/build/lib.css" + output: `public/${directory}/build/lib.css` }), + copyAndWatch("./public/shared/variables.css", "variables.css"), + copyAndWatch("./public/shared/reset.css", "reset.css"), inlineSvg({}), svelte({ // enable run-time checks when not in production @@ -76,10 +80,33 @@ export default { // Watch the `public` directory and refresh the // browser on changes when not in production !production && livereload({ - watch: ["public/" + directory, "public/shared"] + watch: [`public/${directory}`] }) ], watch: { clearScreen: false } }; + +/** + * Simple plugin for watching and copying asset for use in app. + * @see {@link https://dev.to/lukap/creating-a-rollup-plugin-to-copy-and-watch-a-file-3hi2 Tutorial} + * @see {@link https://rollupjs.org/guide/en/#build-hooks Rollup Build Hooks} + * @param {string} inputFilePath + * @param {string} outputFilename + */ +function copyAndWatch(inputFilePath, outputFilename) { + return { + name: "copy-and-watch", + async buildStart() { + this.addWatchFile(inputFilePath); + }, + async generateBundle() { + this.emitFile({ + type: "asset", + fileName: outputFilename, + source: fs.readFileSync(inputFilePath) + }); + } + }; +} diff --git a/src/page/App.svelte b/src/page/App.svelte index 16e8f5ce..462b664d 100644 --- a/src/page/App.svelte +++ b/src/page/App.svelte @@ -8,7 +8,7 @@ import Notification from "./Components/Notification.svelte"; import logo from "../shared/img/logo.svg"; - let logger = []; + const logger = []; $: $log.some(item => { if (!logger.includes(item)) { @@ -32,13 +32,12 @@ } // currently inactive, but could be used to globally prevent auto text replacement in app - // eslint-disable-next-line no-unused-vars - function preventAutoTextReplacements(e) { - if (e.inputType === "insertReplacementText" && e.data === ". ") { - e.preventDefault(); - e.target.value += " "; - } - } + // function preventAutoTextReplacements(e) { + // if (e.inputType === "insertReplacementText" && e.data === ". ") { + // e.preventDefault(); + // e.target.value += " "; + // } + // } onMount(async () => { log.add("Requesting initialization data", "info", false); @@ -60,6 +59,30 @@ state.remove("items-loading"); }); + + + +{#if $state.includes("init")} +
+ {@html logo} + {#if $state.includes("init-error")} + Failed to initialize app, check the browser console + {:else} + Initializing app... + {/if} +
+{/if} +
+ + +
+
    + {#each $notifications as item (item.id)} + notifications.remove(item.id)} {item}/> + {/each} +
+{#if $state.includes("settings")}{/if} + - - - -{#if $state.includes("init")} -
- {@html logo} - {#if $state.includes("init-error")} - Failed to initialize app, check the console! - {:else} - Initializing page... - {/if} -
-{/if} -
- - -
-
    - {#each $notifications as item (item.id)} - notifications.remove(item.id)} {item}/> - {/each} -
-{#if $state.includes("settings")}{/if} diff --git a/src/page/Components/Editor/CodeMirror.svelte b/src/page/Components/Editor/CodeMirror.svelte index f8a0dc8d..501bb83e 100644 --- a/src/page/Components/Editor/CodeMirror.svelte +++ b/src/page/Components/Editor/CodeMirror.svelte @@ -28,6 +28,7 @@ import CodeMirror from "../../codemirror.js"; import {settings, state} from "../../store.js"; import EditorSearch from "./EditorSearch.svelte"; + import {parseMetadata, validGrants, validKeys} from "../../utils"; // the function to be called when save keybind is pressed export let saveHandler = () => {}; @@ -50,7 +51,10 @@ let searchActive = false; // linter options - https://jshint.com/docs/options/ - const lintOptions = {asi: true, esversion: 9}; + const lintOptions = { + async: true, + getAnnotations: linter + }; // update settings when changed $: if (instance) { @@ -101,16 +105,16 @@ // create codemirror instance instance = CodeMirror.fromTextArea(textarea, { - mode: "javascript", + mode: "javascript", autoCloseBrackets: $settings.autoCloseBrackets, - continueComments: false, + continueComments: true, foldGutter: true, lineNumbers: true, lineWrapping: true, matchBrackets: true, smartIndent: true, styleActiveLine: true, - indentUnit: parseInt($settings.tabSize), + indentUnit: parseInt($settings.tabSize), showInvisibles: $settings.showInvisibles, tabSize: parseInt($settings.tabSize), highlightSelectionMatches: false, @@ -124,10 +128,10 @@ "Cmd-/": "toggleComment", "Cmd-S": () => saveHandler(), "Cmd-F": () => activateSearch(), - "Esc": () => {searchActive = false}, + "Esc": () => searchActive = false, Tab: cm => { // convert tabs to spaces and add invisible elements - var s = Array(cm.getOption("indentUnit") + 1).join(" "); + const s = Array(cm.getOption("indentUnit") + 1).join(" "); cm.replaceSelection(s); } } @@ -162,7 +166,7 @@ function autoHint(cm) { // check for valid key combinations - const validKeys = keysPressed.every(v => { + const validKeys = keysPressed.every(v => { // if a single key press, only show hint if letter/number if (keysPressed.length === 1) { return v.startsWith("Digit") || v.startsWith("Key"); @@ -177,7 +181,7 @@ // ensure hinting not active already && !cm.state.completionActive // not first position on the line - && cm.getCursor().ch != 0 + && cm.getCursor().ch !== 0 // only hint when 1-2 key combos && keysPressed.length < 3 // valid keys combo @@ -206,8 +210,8 @@ if ( changeObj.origin === "+input" && changeObj.text.length === 1 - && changeObj.text[0] === ". ") - { + && changeObj.text[0] === ". " + ) { changeObj.update(changeObj.from, changeObj.to, [" "]); } } @@ -230,6 +234,140 @@ instance.refresh(); } + /** + * @param {{set: Set, setName: string, message: string}} + * @returns {{from: {line: number, ch: number}, + * to: {line: number, ch: number}, message: string, severity: string}} + */ + function makeErrors({set, setName, message}) { + const errors = []; + //for (let i = 0; i < array.length; i++) { + set.forEach(el => { + //const el = array[i]; + let regex = new RegExp(`^${el}$`); + if (setName === "invalidKeys") { + regex = new RegExp(`^// @${el}.*?$`); + } + const cursor = instance.getSearchCursor(regex); + const ranges = []; + while (cursor.findNext()) { + ranges.push({anchor: cursor.from(), head: cursor.to()}); + } + for (let j = 0; j < ranges.length; j++) { + const range = ranges[j]; + const err = { + from: range?.anchor, + to: range?.head, + severity: "warning", + message: message + }; + errors.push(err); + } + }); + return errors; + } + + function linter(text, updateLinting, options) { + // toggle for custom metadata linting + const customLinter = true; + // only lint in javascript mode + if (instance?.options.mode !== "javascript") return; + // normal javascript linting through CodeMirror addon and jshint + let errors = CodeMirror.lint.javascript(text, { + asi: true, esversion: 9 + }); + // errors from custom checks & metadata parser will populate this array + let customErrors = []; + if (customLinter) { + // check if whitespace characters precede userscript tags + if (/^\s/.test(text)) { + customErrors.push({ + from: {line: 0, ch: 0}, + to: {line: 0, ch: 0}, + severity: "warning", + message: "Userscript starts with whitespace characters." + }); + } + // parse the code metadata + const parsedMetadata = parseMetadata(text); + + // check parser result for initial errors + if (parsedMetadata.match === false) { + customErrors.push({ + from: {line: 0, ch: 0}, + to: {line: 0, ch: 0}, + severity: "error", + message: "Userscript metadata missing or improperly formatted." + }); + } else if (parsedMetadata.meta === false) { + customErrors.push({ + from: {line: 0, ch: 0}, + to: {line: 0, ch: 0}, + severity: "error", + message: "Userscript metadata missing." + }); + } + + // run additional checks + const invalidKeys = new Set(); + let invalidKeysErrors = []; + const invalidGrants = new Set(); + let invalidGrantsErrors = []; + const emptyKeyValues = new Set(); + let emptyKeyValuesErrors = []; + + // if metadata fails initial check, it won't be array + // don't run the additional checks if initial checks failed + if (Array.isArray(parsedMetadata)) { + // find invalid keys or empty key values + for (let i = 0; i < parsedMetadata.length; i++) { + const {key, value: val, text} = parsedMetadata[i]; + if (!validKeys.has(key)) { + invalidKeys.add(key); + } + if (key !== "noframes" && !invalidKeys.has(key) && !val) { + emptyKeyValues.add(text); + } + } + + // find unsupported @grant methods + const grants = parsedMetadata.filter(a => a.key === "grant"); + for (let i = 0; i < grants.length; i++) { + const {value, text} = grants[i]; + if (!validGrants.has(value)) { + invalidGrants.add(text); + } + } + } + invalidKeysErrors = makeErrors({ + set: invalidKeys, + setName: "invalidKeys", + message: "Unsupported metadata key." + }); + emptyKeyValuesErrors = makeErrors({ + set: emptyKeyValues, + setName: "emptyKeyValues", + message: "Key requires a value." + }); + invalidGrantsErrors = makeErrors({ + set: invalidGrants, + setName: "invalidGrants", + message: "@grant method not supported." + }); + + // combine all custom error arrays + customErrors = [ + ...customErrors, + ...invalidKeysErrors, + ...emptyKeyValuesErrors, + ...invalidGrantsErrors + ]; + } + // if the custom linter returned errors, add them to errors array + if (customErrors.length) errors = [...errors, ...customErrors]; + updateLinting(errors); + } + // function that is called for editor component when clicking discard button export function discardChanges() { if ($state.includes("ready")) { @@ -257,7 +395,7 @@ this={EditorSearch} active={searchActive} bind:this={search} - closeHandler={() => {searchActive = false}} + closeHandler={() => searchActive = false} instance={instance} /> {/if} diff --git a/src/page/Components/Editor/Editor.svelte b/src/page/Components/Editor/Editor.svelte index 00bea0c8..3ec99568 100644 --- a/src/page/Components/Editor/Editor.svelte +++ b/src/page/Components/Editor/Editor.svelte @@ -30,7 +30,7 @@ name = activeItem.name; remote = activeItem.remote; temp = activeItem.temp; - type = activeItem.type; + type = activeItem.request ? "request" : activeItem.type; canUpdate = activeItem.canUpdate; // on load if temp item, disabled discard and enable save, if not disable both if (temp) { @@ -57,9 +57,9 @@ // overwrite item in items store items.update(i => { const index = i.findIndex(a => a.active); - const disabled = i[index].disabled != undefined ? i[index].disabled : false; + const disabled = i[index].disabled !== undefined ? i[index].disabled : false; const type = i[index].type; - const visible = i[index].visible != undefined ? i[index].visible : true; + const visible = i[index].visible !== undefined ? i[index].visible : true; i[index] = response; i[index].active = true; i[index].disabled = disabled; @@ -69,6 +69,10 @@ }); // set the newly saved file contents in codemirror instance cmSetSavedCode(response.content); + // refresh session rules + browser.runtime.sendMessage({name: "REFRESH_SESSION_RULES"}); + // refresh context-menu scripts + browser.runtime.sendMessage({name: "REFRESH_CONTEXT_MENU_SCRIPTS"}); } state.remove("saving"); } @@ -79,7 +83,7 @@ const link = document.createElement("a"); const content = codemirror.getValue(); const filename = activeItem.filename; - link.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(content)); + link.setAttribute("href", `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`); link.setAttribute("download", filename); link.style.display = "none"; document.body.appendChild(link); @@ -125,7 +129,7 @@ log.add(response.error, "error", true); } else { items.update(i => i.filter(a => !a.active)); - log.add("Successfully trashed " + activeItem.filename, "info", false); + log.add(`Successfully trashed ${activeItem.filename}`, "info", false); } } state.remove("trashing"); @@ -146,6 +150,72 @@ } } + +
+ {#if $state.includes("editor-loading") || $state.includes("fetching")} + + {/if} + {#if !activeItem} +
No Item Selected
+ {/if} +
+
+
+ +
{name}
+
+
+
+ {#if $state.includes("saving")} + Saving... + {:else if $state.includes("trashing")} + (◞‸◟) + {:else if $state.includes("updating")} + Updating code, cancel request + {:else if remote} + Code was remotely fetched, check carefully before saving! + {:else if temp} + Ready for code! + {:else} + Last modified: {lastModified} + {/if} +
+
+
+
+ + + +
+
+
+ +
+ +
+ - -
- {#if $state.includes("editor-loading") || $state.includes("fetching")} - - {/if} - {#if !activeItem} -
No Item Selected
- {/if} -
-
-
- -
{name}
-
-
-
- {#if $state.includes("saving")} - Saving... - {:else if $state.includes("trashing")} - (◞‸◟) - {:else if $state.includes("updating")} - Updating code, cancel request - {:else if remote} - Code was remotely fetched, check carefully before saving! - {:else if temp} - Ready for code! - {:else} - Last modified: {lastModified} - {/if} -
-
-
-
- - - -
-
-
- -
- -
diff --git a/src/page/Components/Editor/EditorSearch.svelte b/src/page/Components/Editor/EditorSearch.svelte index 3b5d1a91..e55ae011 100644 --- a/src/page/Components/Editor/EditorSearch.svelte +++ b/src/page/Components/Editor/EditorSearch.svelte @@ -104,7 +104,7 @@ cm.scrollIntoView({from: ranges[i].anchor, to: ranges[i].head}, 20); // mark currently selected element marks.forEach(marker => marker.clear()); - const m = cm.markText(ranges[i].anchor, ranges[i].head, {className: "cm-search-mark"}); + const m = cm.markText(ranges[i].anchor, ranges[i].head, {className: "cm-search-mark"}); marks.push(m); // increment display index after getting data from array // display index is always +1 compared to index within array @@ -126,11 +126,29 @@ cm.scrollIntoView({from: ranges[i].anchor, to: ranges[i].head}, 20); // mark currently selected element marks.forEach(marker => marker.clear()); - const m = cm.markText(ranges[i].anchor, ranges[i].head, {className: "cm-search-mark"}); + const m = cm.markText(ranges[i].anchor, ranges[i].head, {className: "cm-search-mark"}); marks.push(m); } } + +{#if active} + +{/if} + -{#if active} - -{/if} diff --git a/src/page/Components/Notification.svelte b/src/page/Components/Notification.svelte index c0c8c8dd..23dffbc7 100644 --- a/src/page/Components/Notification.svelte +++ b/src/page/Components/Notification.svelte @@ -11,6 +11,19 @@ const icon = item.type === "error" ? iconError : item.type === "info" ? iconInfo : iconWarn; + +
  • +
    {@html icon}
    + {item.message} + +
  • + - -
  • -
    {@html icon}
    - {item.message} - -
  • diff --git a/src/page/Components/Settings.svelte b/src/page/Components/Settings.svelte index a1eb8b98..2bb1cc4e 100644 --- a/src/page/Components/Settings.svelte +++ b/src/page/Components/Settings.svelte @@ -20,7 +20,7 @@ const val = blacklist.value.split(",").map(item => item.trim()).filter(n => n); // compare blacklist input to saved blacklist - if ([...val].sort().toString() != [...$settings.blacklist].sort().toString()) { + if ([...val].sort().toString() !== [...$settings.blacklist].sort().toString()) { settings.updateSingleSetting("blacklist", val); // when blacklistSaving, visual indication of saving occurs on element // the visual save indication is mostly ux only indicates a setting save was attempted @@ -48,17 +48,134 @@ // close all open extension pages const url = browser.runtime.getURL("page.html"); const close = []; - const tabs = await browser.tabs.query({}); + const tabs = await browser.tabs.query({}); tabs.forEach(tab => tab.url === url && close.push(tab.id)); if (close.length > 0) browser.tabs.remove(close); } + +
    state.remove("settings")} + in:fade={{duration: 150}} + out:fade={{duration: 150, delay: 75}} +> + +
    + - -
    {state.remove("settings")}} - in:fade={{duration: 150}} - out:fade={{duration: 150, delay: 75}} -> - -
    diff --git a/src/page/Components/Sidebar/Sidebar.svelte b/src/page/Components/Sidebar/Sidebar.svelte index 2b0e9929..5591ba2b 100644 --- a/src/page/Components/Sidebar/Sidebar.svelte +++ b/src/page/Components/Sidebar/Sidebar.svelte @@ -20,7 +20,7 @@ // disable buttons accordingly $: disabled = !$state.includes("ready"); - $: list = sortBy($items, $settings.sortOrder).filter(a => a.visible != false); + $: list = sortBy($items, $settings.sortOrder).filter(a => a.visible !== false); // always scroll to an active item // when sorting is changed, a save occurs, etc... will scroll to active item @@ -43,7 +43,7 @@ const random = uniqueId(); const namePrefix = type === "js" ? "NewScript-" : "NewStyle-"; const name = namePrefix + random; - const filename = name + "." + type; + const filename = `${name}.${type}`; const description = "This is your new file, start writing code"; const item = { content: content || newScriptDefault(description, name, type), @@ -70,17 +70,17 @@ // check if there's a temp item and it's not the item to be activated // can occur when user clicks a non-temp item while a temp item exists const temp = $items.find(i => i.temp); - if ((temp && (temp != item) || cmChanged()) && !warn()) return; + if (((temp && (temp !== item)) || cmChanged()) && !warn()) return; // the editor has changed or the above scenario is true // the user has been warned and has accepted the warning // if another item already active, deactivate it const activeItem = $items.find(i => i.active); - if (activeItem) { + if (activeItem) { activeItem.active = false; $items = $items; } // remove the temp item if needed - if (temp && (temp != item)) items.update(i => i.filter(a => !a.temp)); + if (temp && (temp !== item)) items.update(i => i.filter(a => !a.temp)); // set the saved and session code variables properly const savedCode = item.temp ? null : item.content; @@ -95,7 +95,8 @@ // set up editor after activating file await tick(); const cm = cmGetInstance(); - const mode = item.type === "js" ? "javascript" : item.type; + let mode = "javascript"; + if (item.type === "css") mode = "css"; cm.setOption("mode", mode); cm.setValue(item.content); cm.clearHistory(); @@ -166,6 +167,41 @@ sidebarTimeout = setTimeout(() => showCount = true, 750); } + + + - - diff --git a/src/page/Components/Sidebar/SidebarFilter.svelte b/src/page/Components/Sidebar/SidebarFilter.svelte index 3d3f8cf5..7624ab1f 100644 --- a/src/page/Components/Sidebar/SidebarFilter.svelte +++ b/src/page/Components/Sidebar/SidebarFilter.svelte @@ -16,7 +16,7 @@ function filter(query) { $items = $items.map(item => { const visible = item.filename.toLowerCase().includes(query.trim().toLowerCase()); - if (visible != item.visible) return {...item, visible}; + if (visible !== item.visible) return {...item, visible}; return item; }); } @@ -25,6 +25,49 @@ settings.updateSingleSetting("sortOrder", order); } + +
    + + +
  • updateSortOrder("lastModifiedAsc")} + > + Last Modified: Asc{sortOrder === "lastModifiedAsc" ? " *" : ""} +
  • +
  • updateSortOrder("lastModifiedDesc")} + > + Last Modified: Desc{sortOrder === "lastModifiedDesc" ? " *" : ""} +
  • +
  • updateSortOrder("nameAsc")} + > + Name: Asc{sortOrder === "nameAsc" ? " *" : ""} +
  • +
  • updateSortOrder("nameDesc")} + > + Name: Desc{sortOrder === "nameDesc" ? " *" : ""} +
  • +
    + {#if query} + query = ""} {disabled}/> + {/if} +
    + - -
    - - -
  • updateSortOrder("lastModifiedAsc")} - > - Last Modified: Asc{sortOrder === "lastModifiedAsc" ? " *" : ""} -
  • -
  • updateSortOrder("lastModifiedDesc")} - > - Last Modified: Desc{sortOrder === "lastModifiedDesc" ? " *" : ""} -
  • -
  • updateSortOrder("nameAsc")} - > - Name: Asc{sortOrder === "nameAsc" ? " *" : ""} -
  • -
  • updateSortOrder("nameDesc")} - > - Name: Desc{sortOrder === "nameDesc" ? " *" : ""} -
  • -
    - {#if query} - query = ""} {disabled}/> - {/if} -
    diff --git a/src/page/Components/Sidebar/SidebarItem.svelte b/src/page/Components/Sidebar/SidebarItem.svelte index 6b5b3066..06f24c78 100644 --- a/src/page/Components/Sidebar/SidebarItem.svelte +++ b/src/page/Components/Sidebar/SidebarItem.svelte @@ -14,7 +14,7 @@ function formatDescription(str) { if (str && str.length > 120) { showTitle = true; - return str.substring(0, 120).trim() + "..."; + return `${str.substring(0, 120).trim()}...`; } else { showTitle = false; return str; @@ -23,6 +23,29 @@ $: description = formatDescription(data.description); + +
    +
    + +
    {data.name}
    + +
    + {#if description} +
    + {description} +
    + {/if} +
    + - -
    -
    - -
    {data.name}
    - -
    - {#if description} -
    - {description} -
    - {/if} -
    diff --git a/src/page/dev.js b/src/page/dev.js deleted file mode 100644 index af2d27ce..00000000 --- a/src/page/dev.js +++ /dev/null @@ -1,308 +0,0 @@ -import {parse} from "./utils"; - - -async function getRemoteFileContents(url) { - let r = {}; - await fetch(url).then(response => { - if (!response.ok) throw Error(response.statusText); - return response.text(); - }).then(text => { - r.contents = text; - }).catch(error => { - console.log(error); - r.error = "Remote url bad response!"; - }); - return r; -} - -// example file contents -const exampleCSSContent = `/* ==UserStyle== -@name Example CSS UserStyle -@description This is an example of a UserStyle. It applies css to webpages rather than js. -@match https://userstyles.org/* -==/UserStyle== */ -#id, -.class, -.pseudo-element::after { - background-color: tomato; -} -`; - -const exampleJSContent = `// ==UserScript== -// @name Example JS Userscript -// @description This a standard userscript with javascript code -// @match https://github.com/quoid/userscripts -// @exclude-match *://*.* -// @version 1.0 -// @updateURL https://www.k21p.com/example.user.js -// @noframes -// ==/UserScript== -console.log("I am an example userscript"); -`; - -// dummy file directory -let files = [ - { - content: exampleCSSContent, - filename: "Example CSS UserStyle.css", - lastModified: 1606009623000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript2.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript3.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript4.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript5.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript6.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript7.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript8.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript9.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript10.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript11.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript12.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript13.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript14.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript15.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript16.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript17.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript18.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript19.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript20.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript21.js", - lastModified: 1605862023000 - }, - { - content: exampleJSContent, - filename: "Example JS Userscript22.js", - lastModified: 1605862023000 - }, -]; - -function saveFile(content, lastMod, newFilename, oldName) { - const ind = files.findIndex(f => f.filename === oldName); - const s = { - content: content, - filename: newFilename, - lastModified: lastMod - }; - if (ind != -1) { - // overwrite at index - files[ind] = s; - } else { - // add to beginning of array - files.unshift(s); - } -} - -const _browser = { - runtime: { - async sendNativeMessage(message, responseCallback) { - console.log(`Got message: ${message.name}`); - let response = {}; - const name = message.name; - if (name === "PAGE_INIT_DATA") { - response = { - active: "true", - autoCloseBrackets: "true", - autoHint: "true", - blacklist: [], - descriptions: "true", - languageCode: "en", - lint: "true", - log: "false", - saveLocation: "/Users/someone/Library/Directory", - showCount: "true", - showInvisibles: "true", - sortOrder: "lastModifiedDesc", - tabSize: "4", - version: "4.0.0" - }; - } else if (name === "PAGE_ALL_FILES") { - response = []; - files.forEach(file => { - const content = file.content; - const parsed = parse(content); - const metadata = parsed.metadata; - const canUpdate = (metadata.version && metadata.updateURL) ? true : false; - const scriptData = { - canUpdate: canUpdate, - content: content, - description: metadata.description[0], - disabled: false, - filename: file.filename, - lastModified: file.lastModified, - metadata: metadata, - name: metadata.name[0], - type: file.filename.substring(file.filename.lastIndexOf(".") + 1) - }; - response.push(scriptData); - }); - } else if (name === "TOGGLE_ITEM") { - response = {success: true}; - //response = {error: true}; - } else if (name === "PAGE_UPDATE_SETTINGS") { - response = {success: true}; - } else if (name === "PAGE_UPDATE_BLACKLIST") { - response = {success: true}; - } else if (name === "PAGE_NEW_REMOTE") { - let r = await getRemoteFileContents(message.url); - response = r; - } else if (name === "PAGE_SAVE") { - console.log(message); - const newContent = message.content; - const oldFilename = message.item.filename; - const parsed = parse(newContent); - const lastModified = Date.now(); - let canUpdate = false; - // script failed to parse - if (!parsed) { - return {error: "save failed, file has invalid metadata"}; - } - const name = parsed.metadata.name[0]; - const newFilename = `${name}.${message.item.type}`; - // filename length too long - if (newFilename.length > 255) { - return {error: "save failed, filename too long!"}; - } - - // check if file can be remotely updated - if (parsed.metadata.version && parsed.metadata.updateURL) { - canUpdate = true; - } - - let success = { - canUpdate: canUpdate, - content: newContent, - filename: newFilename, - lastModified: lastModified, - name: name, - }; - - // add description if in file metadata - if (parsed.metadata.description) { - success.description = parsed.metadata.description[0]; - } - - // overwriting - if (newFilename.toLowerCase() === oldFilename.toLowerCase()) { - saveFile(newContent, lastModified, newFilename, oldFilename); - return success; - } - - // not overwriting, check if filename for that type is taken - if (files.find(a => a.filename.toLowerCase() === newFilename.toLowerCase())) { - return {error: "save failed, name already taken!"}; - } - - // not overwriting but all validation passed - saveFile(newContent, lastModified, newFilename, oldFilename); - return success; - } else if (name === "PAGE_UPDATE") { - response.content = message.content + "\n//I was updated"; - } - if (!responseCallback) { - return new Promise(resolve => setTimeout(() => resolve(response), 500)); - } - setTimeout(() => { - responseCallback(response); - }, 500); - } - }, - tabs: { - query(message, responseCallback) { - const response = [ - {id: 1, url: "https://www.filmgarb.com/"}, - {id: 2, url: "https://www.k21p.com/"} - ]; - if (!responseCallback) { - return new Promise(resolve => setTimeout(() => resolve(response), 500)); - } - setTimeout(() => { - responseCallback(response); - }, 500); - } - } -}; - -window.browser = _browser; diff --git a/src/page/store.js b/src/page/store.js index 1d34972a..f366e2c2 100644 --- a/src/page/store.js +++ b/src/page/store.js @@ -1,6 +1,19 @@ import {writable} from "svelte/store"; import {uniqueId} from "./utils.js"; +function notificationStore() { + const {subscribe, update} = writable([]); + const add = item => { + update(a => { + a.push(item); + return a; + }); + }; + const remove = id => update(a => a.filter(b => b.id !== id)); + return {subscribe, add, remove}; +} +export const notifications = notificationStore(); + function logStore() { const {subscribe, set, update} = writable([]); const add = (message, type, notify) => { @@ -18,19 +31,6 @@ function logStore() { } export const log = logStore(); -function notificationStore() { - const {subscribe, update} = writable([]); - const add = item => { - update(a => { - a.push(item); - return a; - }); - }; - const remove = id => update(a => a.filter(b => b.id !== id)); - return {subscribe, add, remove}; -} -export const notifications = notificationStore(); - function stateStore() { const {subscribe, update} = writable(["init"]); // store oldState to see how state transitioned diff --git a/src/page/utils.js b/src/page/utils.js index e17998ff..c8ff9225 100644 --- a/src/page/utils.js +++ b/src/page/utils.js @@ -1,3 +1,7 @@ +/** + * @param {number} ms millisecond timestamp + * @returns {string} + */ export function formatDate(ms) { const d = new Date(ms); const yr = new Intl.DateTimeFormat("en", {year: "numeric"}).format(d); @@ -9,9 +13,24 @@ export function formatDate(ms) { } export function uniqueId() { - return Math.random().toString(36).substr(2, 8); + return Math.random().toString(36).substring(2, 10); } +/** + * awaitable function for waiting an arbitrary amount of time + * @param {number} ms the amount of time to wait in milliseconds + * @returns {Promise} + */ +export function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// TODO: describe the items array that should get passed to this function +/** + * @param {Array} array + * @param {("lastModifiedAsc"|"lastModifiedDesc"|"nameAsc"|"nameDesc")} order + * @returns + */ export function sortBy(array, order) { if (order === "nameAsc") { array.sort((a, b) => a.name.localeCompare(b.name)); @@ -27,7 +46,14 @@ export function sortBy(array, order) { return array; } -export function newScriptDefault(description, name, type) { +/** + * + * @param {string} description + * @param {string} name + * @param {("css"|"js")} type + * @returns {string} + */ +export function newScriptDefault(description, name, type) { if (type === "css") { return `/* ==UserStyle==\n@name ${name}\n@description ${description}\n@match \n==/UserStyle== */`; } else if (type === "js") { @@ -35,6 +61,10 @@ export function newScriptDefault(description, name, type) { } } +/** + * @param {string} str + * @returns {?{code: string, content: str, metablock: string, metadata: {string: string[]}}} + */ export function parse(str) { if (typeof str != "string") return null; const blocksReg = /(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))/; @@ -49,8 +79,8 @@ export function parse(str) { const metadata = {}; const metaArray = metas.split("\n"); metaArray.forEach(function(m) { - var parts = m.trim().match(/^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)/); - var parts2 = m.trim().match(/^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$/); + const parts = m.trim().match(/^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)/); + const parts2 = m.trim().match(/^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$/); if (parts) { metadata[parts[1]] = metadata[parts[1]] || []; metadata[parts[1]].push(parts[2]); @@ -70,3 +100,76 @@ export function parse(str) { metadata: metadata }; } + +/** + * @param {string} text editor code + * @returns {{match: boolean, meta: boolean} | {key: string, value: string, text: string}[]} + */ +export function parseMetadata(text) { + const groupsRe = /(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)/; + const groups = text.match(groupsRe); + // userscript code doesn't match the regex expression + // could be missing opening/closing tags, malformed + // or missing metadata between opening/closing tags (group 2 in regex exp) + if (!groups) { + return {match: false}; + } + + // userscript code matches but content between opening/closing tag missing + // ex. opening/closing tags present, but newline characters between the tags + const metas = groups[2]; + if (!metas) return {match: true, meta: false}; + + const metadata = []; + const metaArray = metas.split("\n"); + + for (let i = 0; i < metaArray.length; i++) { + const metaRegex = /^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]*([^\s]+[^\r\n\t\v\f]*)?/; + const meta = metaArray[i]; + const parts = meta.match(metaRegex); + if (parts) metadata.push({key: parts[1], value: parts[2], text: parts[0]}); + } + + // if there is content between the opening/closing tags, match will be found + // this additionally checks that there's at least one properly formed key + // if not keys found, assume metadata is missing + // checking that required keys are present will happen elsewhere + if (!Object.keys(metadata).length) return {match: true, meta: false}; + + return metadata; +} + +export const validGrants = new Set([ + "GM.addStyle", + "GM.deleteValue", + "GM.getValue", + "GM.info", + "GM.listValues", + "GM.openInTab", + "GM.setClipboard", + "GM.setValue", + "GM.xmlHttpRequest", + "GM_addStyle", + "GM_info", + "GM_setClipboard", + "GM_xmlhttpRequest" +]); + +export const validKeys = new Set([ + "author", + "description", + "downloadURL", + "exclude", + "exclude-match", + "grant", + "include", + "inject-into", + "match", + "name", + "noframes", + "require", + "run-at", + "updateURL", + "version", + "weight" +]); diff --git a/src/popup/App.svelte b/src/popup/App.svelte index 73e4e492..ef8a72e2 100644 --- a/src/popup/App.svelte +++ b/src/popup/App.svelte @@ -78,11 +78,11 @@ async function updateItem(item) { disabled = true; - const tabs = await browser.tabs.query({currentWindow: true, active: true}); - const url = tabs[0].url; + const currentTab = await browser.tabs.getCurrent(); + const url = currentTab.url; const frameUrls = []; if (url) { - const frames = await browser.webNavigation.getAllFrames({tabId: tabs[0].id}); + const frames = await browser.webNavigation.getAllFrames({tabId: currentTab.id}); frames.forEach(frame => frameUrls.push(frame.url)); } const message = { @@ -147,7 +147,7 @@ async function openExtensionPage() { const url = browser.runtime.getURL("page.html"); - const tabs = await browser.tabs.query({}); + const tabs = await browser.tabs.query({}); for (let i = 0; i < tabs.length; i++) { if (tabs[i].url === url) { await browser.windows.update(tabs[i].windowId, {focused: true}); @@ -176,7 +176,7 @@ try { lastUpdateCheckObj = await browser.storage.local.get(["lastUpdateCheck"]); } catch (error) { - console.error("Error checking extension storage " + error); + console.error(`Error checking extension storage ${error}`); return false; } // if extension storage doesn't have key, run the check @@ -200,7 +200,7 @@ return false; } - console.log(((timestampMs - lastUpdateCheck) / (1000 * 60 * 60)) + " hours have passed"); + console.log(`${(timestampMs - lastUpdateCheck) / (1000 * 60 * 60)} hours have passed`); console.log("running update check"); // otherwise run the check return true; @@ -216,7 +216,7 @@ showAll = true; allItems = response.items; } else if (response.error) { - console.log("Error opening save location: " + response.error); + console.log(`Error opening save location: ${response.error}`); error = response.error; } disabled = false; @@ -229,7 +229,7 @@ try { pltfm = await browser.runtime.sendNativeMessage({name: "REQ_PLATFORM"}); } catch (error) { - console.log("Error for pltfm promise: " + error); + console.log(`Error for pltfm promise: ${error}`); initError = true; loading = false; return; @@ -249,7 +249,7 @@ try { init = await browser.runtime.sendNativeMessage({name: "POPUP_INIT"}); } catch (error) { - console.log("Error for init promise: " + error); + console.log(`Error for init promise: ${error}`); initError = true; loading = false; return; @@ -263,31 +263,44 @@ active = init.initData.active === "true" ? true : false; } + // refresh session rules + browser.runtime.sendMessage({name: "REFRESH_SESSION_RULES"}); + // refresh context-menu scripts + browser.runtime.sendMessage({name: "REFRESH_CONTEXT_MENU_SCRIPTS"}); + // set popup height resize(); // get matches const extensionPageUrl = browser.runtime.getURL("page.html"); - const tabs = await browser.tabs.query({currentWindow: true, active: true}); - const url = tabs[0].url; - const frameUrls = []; + const currentTab = await browser.tabs.getCurrent(); + const url = currentTab.url; + if (!url) { + loading = false; + disabled = false; + return; + } if (url === extensionPageUrl) { // disable popup on extension page inactive = true; loading = false; return; } - if (url) { - const frames = await browser.webNavigation.getAllFrames({tabId: tabs[0].id}); - frames.forEach(frame => frameUrls.push(frame.url)); + const frameUrls = new Set(); + const frames = await browser.webNavigation.getAllFrames({tabId: currentTab.id}); + for (let i = 0; i < frames.length; i++) { + const frameUrl = frames[i].url; + if (frameUrl !== url && frameUrl.startsWith("http")) { + frameUrls.add(frameUrl); + } } - const message = {name: "POPUP_MATCHES", url: url, frameUrls: frameUrls}; + const message = {name: "POPUP_MATCHES", url: url, frameUrls: Array.from(frameUrls)}; let matches; try { matches = await browser.runtime.sendNativeMessage(message); // response = await browser.runtime.sendMessage(message); } catch (error) { - console.log("Error for matches promise: " + error); + console.log(`Error for matches promise: ${error}`); initError = true; loading = false; return; @@ -312,7 +325,7 @@ abort = true; updatesResponse = await browser.runtime.sendNativeMessage({name: "POPUP_UPDATES"}); } catch (error) { - console.error("Error for updates promise: " + error); + console.error(`Error for updates promise: ${error}`); initError = true; loading = false; abort = false; @@ -344,9 +357,9 @@ // 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(tabs[0].id, {name: "USERSCRIPT_INSTALL_00"}); + const response = await browser.tabs.sendMessage(currentTab.id, {name: "USERSCRIPT_INSTALL_00"}); if (response.error) { - console.log("Error checking .user.js url: " + response.error); + console.log(`Error checking .user.js url: ${response.error}`); error = response.error; } else if (!response.invalid) { // the response will contain the string to display @@ -372,7 +385,6 @@ async function resize() { if (!platform || platform === "macos") return; - // special styling for ipados and split views clearTimeout(resizeTimer); resizeTimer = setTimeout(async () => { if (platform === "ipados") { @@ -394,10 +406,10 @@ let addHeight = 0; // if warn or error elements visible, also subtract that from applied height if (warn) addHeight += warn.offsetHeight; - if (err) addHeight += err.offsetHeight; + if (err) addHeight += err.offsetHeight; windowHeight = (window.outerHeight - (headerHeight + addHeight)); - main.style.height = windowHeight + "px"; - main.style.paddingBottom = (headerHeight + addHeight) + "px"; + main.style.height = `${windowHeight}px`; + main.style.paddingBottom = `${headerHeight + addHeight}px`; }, 25); } @@ -407,9 +419,9 @@ // show the install view showInstall = true; // get the active tab - const tabs = await browser.tabs.query({currentWindow: true, active: true}); + const currentTab = await browser.tabs.getCurrent(); // send content script a message on the active tab - const response = await browser.tabs.sendMessage(tabs[0].id, {name: "USERSCRIPT_INSTALL_01"}); + 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 @@ -419,7 +431,7 @@ // if the response includes an error, display it in the view if (response.error) { - console.log("Can not install userscript: " + response.error); + console.log(`Can not install userscript: ${response.error}`); installViewUserscriptError = response.error; } else { installViewUserscript = response; @@ -435,9 +447,9 @@ // go back to main view showInstall = false; // get the active tab - const tabs = await browser.tabs.query({currentWindow: true, active: true}); + 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(tabs[0].id, {name: "USERSCRIPT_INSTALL_02"}); + const response = await browser.tabs.sendMessage(currentTab.id, {name: "USERSCRIPT_INSTALL_02"}); if (response.error) { error = response.error; disabled = false; @@ -456,6 +468,7 @@ resize(); }); +
    Popup inactive on extension page
    {:else if initError}
    - Something went wrong:  window.location.reload()}> click to retry + Something went wrong:  + window.location.reload()} + > + click to retry +
    {:else if items.length < 1}
    No matched userscripts
    @@ -522,6 +541,7 @@ name={item.name} subframe={item.subframe} type={item.type} + request={item.request ? true : false} on:click={() => toggleItem(item)} /> {/each} @@ -531,7 +551,9 @@
    {#if !inactive && platform === "macos"} {/if} {#if showUpdates} @@ -568,7 +590,10 @@ {showAll = false; refreshView()}} + closeClick={() => { + showAll = false; + refreshView(); + }} showLoaderOnDisabled={false} > {/if} + -
    - -
    {name}
    - {#if subframe}
    SUB
    {/if} - -
    diff --git a/src/popup/Components/View.svelte b/src/popup/Components/View.svelte index 58664c90..e67587e6 100644 --- a/src/popup/Components/View.svelte +++ b/src/popup/Components/View.svelte @@ -20,6 +20,7 @@ }; } +
    {headerTitle} @@ -37,6 +38,7 @@ {/if}
    + diff --git a/src/popup/Components/Views/InstallView.svelte b/src/popup/Components/Views/InstallView.svelte index df3e7fe5..a4212435 100644 --- a/src/popup/Components/Views/InstallView.svelte +++ b/src/popup/Components/Views/InstallView.svelte @@ -6,12 +6,13 @@ export let installCancelClick; export let installConfirmClick; +
    {#if installError}
    {@html iconError} -
    The usercript can not be installed.
    -

    Error: OK GO

    +
    Couldn't install userscript
    +

    {installError}

    {:else if userscript}
      @@ -57,17 +58,27 @@
      Be sure you trust the author before installing. Nefarious code can exploit your security and privacy.
    - - + +
    {/if}
    + - -
    - -
      - -
    • At least one slot is required...
    • -
      -
    -
    diff --git a/src/shared/Components/IconButton.svelte b/src/shared/Components/IconButton.svelte index c9bb5a00..e871ff1e 100644 --- a/src/shared/Components/IconButton.svelte +++ b/src/shared/Components/IconButton.svelte @@ -5,6 +5,7 @@ export let title = undefined; export let notification = false; + + -
    - {@html iconLoader} - {#if abort} -
    - Fetching resources, cancel request -
    - {/if} -
    diff --git a/src/shared/Components/Tag.svelte b/src/shared/Components/Tag.svelte index 89870cf4..27abcf7e 100644 --- a/src/shared/Components/Tag.svelte +++ b/src/shared/Components/Tag.svelte @@ -1,7 +1,9 @@ + +
    + -
    diff --git a/src/shared/Components/Toggle.svelte b/src/shared/Components/Toggle.svelte index 53e70e71..37bd8c94 100644 --- a/src/shared/Components/Toggle.svelte +++ b/src/shared/Components/Toggle.svelte @@ -3,11 +3,13 @@ export let disabled = false; export let title = undefined; + +