summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/addons/MatchPattern.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/addons/MatchPattern.jsm')
-rw-r--r--toolkit/modules/addons/MatchPattern.jsm352
1 files changed, 352 insertions, 0 deletions
diff --git a/toolkit/modules/addons/MatchPattern.jsm b/toolkit/modules/addons/MatchPattern.jsm
new file mode 100644
index 000000000..4dff81fd2
--- /dev/null
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["MatchPattern", "MatchGlobs", "MatchURLFilters"];
+
+/* globals MatchPattern, MatchGlobs */
+
+const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "data"];
+const PERMITTED_SCHEMES_REGEXP = PERMITTED_SCHEMES.join("|");
+
+// This function converts a glob pattern (containing * and possibly ?
+// as wildcards) to a regular expression.
+function globToRegexp(pat, allowQuestion) {
+ // Escape everything except ? and *.
+ pat = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
+
+ if (allowQuestion) {
+ pat = pat.replace(/\?/g, ".");
+ } else {
+ pat = pat.replace(/\?/g, "\\?");
+ }
+ pat = pat.replace(/\*/g, ".*");
+ return new RegExp("^" + pat + "$");
+}
+
+// These patterns follow the syntax in
+// https://developer.chrome.com/extensions/match_patterns
+function SingleMatchPattern(pat) {
+ if (pat == "<all_urls>") {
+ this.schemes = PERMITTED_SCHEMES;
+ this.hostMatch = () => true;
+ this.pathMatch = () => true;
+ } else if (!pat) {
+ this.schemes = [];
+ } else {
+ let re = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
+ let match = re.exec(pat);
+ if (!match) {
+ Cu.reportError(`Invalid match pattern: '${pat}'`);
+ this.schemes = [];
+ return;
+ }
+
+ if (match[1] == "*") {
+ this.schemes = ["http", "https"];
+ } else {
+ this.schemes = [match[1]];
+ }
+
+ // We allow the host to be empty for file URLs.
+ if (match[2] == "" && this.schemes[0] != "file") {
+ Cu.reportError(`Invalid match pattern: '${pat}'`);
+ this.schemes = [];
+ return;
+ }
+
+ this.host = match[2];
+ this.hostMatch = this.getHostMatcher(match[2]);
+
+ let pathMatch = globToRegexp(match[3], false);
+ this.pathMatch = pathMatch.test.bind(pathMatch);
+ }
+}
+
+SingleMatchPattern.prototype = {
+ getHostMatcher(host) {
+ // This code ignores the port, as Chrome does.
+ if (host == "*") {
+ return () => true;
+ }
+ if (host.startsWith("*.")) {
+ let suffix = host.substr(2);
+ let dotSuffix = "." + suffix;
+
+ return ({host}) => host === suffix || host.endsWith(dotSuffix);
+ }
+ return uri => uri.host === host;
+ },
+
+ matches(uri, ignorePath = false) {
+ return (
+ this.schemes.includes(uri.scheme) &&
+ this.hostMatch(uri) &&
+ (ignorePath || (
+ this.pathMatch(uri.cloneIgnoringRef().path)
+ ))
+ );
+ },
+};
+
+this.MatchPattern = function(pat) {
+ this.pat = pat;
+ if (!pat) {
+ this.matchers = [];
+ } else if (pat instanceof String || typeof(pat) == "string") {
+ this.matchers = [new SingleMatchPattern(pat)];
+ } else {
+ this.matchers = pat.map(p => new SingleMatchPattern(p));
+ }
+};
+
+MatchPattern.prototype = {
+ // |uri| should be an nsIURI.
+ matches(uri) {
+ return this.matchers.some(matcher => matcher.matches(uri));
+ },
+
+ matchesIgnoringPath(uri) {
+ return this.matchers.some(matcher => matcher.matches(uri, true));
+ },
+
+ // Checks that this match pattern grants access to read the given
+ // cookie. |cookie| should be an |nsICookie2| instance.
+ matchesCookie(cookie) {
+ // First check for simple matches.
+ let secureURI = NetUtil.newURI(`https://${cookie.rawHost}/`);
+ if (this.matchesIgnoringPath(secureURI)) {
+ return true;
+ }
+
+ let plainURI = NetUtil.newURI(`http://${cookie.rawHost}/`);
+ if (!cookie.isSecure && this.matchesIgnoringPath(plainURI)) {
+ return true;
+ }
+
+ if (!cookie.isDomain) {
+ return false;
+ }
+
+ // Things get tricker for domain cookies. The extension needs to be able
+ // to read any cookies that could be read any host it has permissions
+ // for. This means that our normal host matching checks won't work,
+ // since the pattern "*://*.foo.example.com/" doesn't match ".example.com",
+ // but it does match "bar.foo.example.com", which can read cookies
+ // with the domain ".example.com".
+ //
+ // So, instead, we need to manually check our filters, and accept any
+ // with hosts that end with our cookie's host.
+
+ let {host, isSecure} = cookie;
+
+ for (let matcher of this.matchers) {
+ let schemes = matcher.schemes;
+ if (schemes.includes("https") || (!isSecure && schemes.includes("http"))) {
+ if (matcher.host.endsWith(host)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ },
+
+ serialize() {
+ return this.pat;
+ },
+};
+
+// Globs can match everything. Be careful, this DOES NOT filter by allowed schemes!
+this.MatchGlobs = function(globs) {
+ this.original = globs;
+ if (globs) {
+ this.regexps = Array.from(globs, (glob) => globToRegexp(glob, true));
+ } else {
+ this.regexps = [];
+ }
+};
+
+MatchGlobs.prototype = {
+ matches(str) {
+ return this.regexps.some(regexp => regexp.test(str));
+ },
+ serialize() {
+ return this.original;
+ },
+};
+
+// Match WebNavigation URL Filters.
+this.MatchURLFilters = function(filters) {
+ if (!Array.isArray(filters)) {
+ throw new TypeError("filters should be an array");
+ }
+
+ if (filters.length == 0) {
+ throw new Error("filters array should not be empty");
+ }
+
+ this.filters = filters;
+};
+
+MatchURLFilters.prototype = {
+ matches(url) {
+ let uri = NetUtil.newURI(url);
+ // Set uriURL to an empty object (needed because some schemes, e.g. about doesn't support nsIURL).
+ let uriURL = {};
+ if (uri instanceof Ci.nsIURL) {
+ uriURL = uri;
+ }
+
+ // Set host to a empty string by default (needed so that schemes without an host,
+ // e.g. about, can pass an empty string for host based event filtering as expected).
+ let host = "";
+ try {
+ host = uri.host;
+ } catch (e) {
+ // 'uri.host' throws an exception with some uri schemes (e.g. about).
+ }
+
+ let port;
+ try {
+ port = uri.port;
+ } catch (e) {
+ // 'uri.port' throws an exception with some uri schemes (e.g. about),
+ // in which case it will be |undefined|.
+ }
+
+ let data = {
+ // NOTE: This properties are named after the name of their related
+ // filters (e.g. `pathContains/pathEquals/...` will be tested against the
+ // `data.path` property, and the same is done for the `host`, `query` and `url`
+ // components as well).
+ path: uriURL.filePath,
+ query: uriURL.query,
+ host,
+ port,
+ url,
+ };
+
+ // If any of the filters matches, matches returns true.
+ return this.filters.some(filter => this.matchURLFilter({filter, data, uri, uriURL}));
+ },
+
+ matchURLFilter({filter, data, uri, uriURL}) {
+ // Test for scheme based filtering.
+ if (filter.schemes) {
+ // Return false if none of the schemes matches.
+ if (!filter.schemes.some((scheme) => uri.schemeIs(scheme))) {
+ return false;
+ }
+ }
+
+ // Test for exact port matching or included in a range of ports.
+ if (filter.ports) {
+ let port = data.port;
+ if (port === -1) {
+ // NOTE: currently defaultPort for "resource" and "chrome" schemes defaults to -1,
+ // for "about", "data" and "javascript" schemes defaults to undefined.
+ if (["resource", "chrome"].includes(uri.scheme)) {
+ port = undefined;
+ } else {
+ port = Services.io.getProtocolHandler(uri.scheme).defaultPort;
+ }
+ }
+
+ // Return false if none of the ports (or port ranges) is verified
+ return filter.ports.some((filterPort) => {
+ if (Array.isArray(filterPort)) {
+ let [lower, upper] = filterPort;
+ return port >= lower && port <= upper;
+ }
+
+ return port === filterPort;
+ });
+ }
+
+ // Filters on host, url, path, query:
+ // hostContains, hostEquals, hostSuffix, hostPrefix,
+ // urlContains, urlEquals, ...
+ for (let urlComponent of ["host", "path", "query", "url"]) {
+ if (!this.testMatchOnURLComponent({urlComponent, data, filter})) {
+ return false;
+ }
+ }
+
+ // urlMatches is a regular expression string and it is tested for matches
+ // on the "url without the ref".
+ if (filter.urlMatches) {
+ let urlWithoutRef = uri.specIgnoringRef;
+ if (!urlWithoutRef.match(filter.urlMatches)) {
+ return false;
+ }
+ }
+
+ // originAndPathMatches is a regular expression string and it is tested for matches
+ // on the "url without the query and the ref".
+ if (filter.originAndPathMatches) {
+ let urlWithoutQueryAndRef = uri.resolve(uriURL.filePath);
+ // The above 'uri.resolve(...)' will be null for some URI schemes
+ // (e.g. about).
+ // TODO: handle schemes which will not be able to resolve the filePath
+ // (e.g. for "about:blank", 'urlWithoutQueryAndRef' should be "about:blank" instead
+ // of null)
+ if (!urlWithoutQueryAndRef ||
+ !urlWithoutQueryAndRef.match(filter.originAndPathMatches)) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ testMatchOnURLComponent({urlComponent: key, data, filter}) {
+ // Test for equals.
+ // NOTE: an empty string should not be considered a filter to skip.
+ if (filter[`${key}Equals`] != null) {
+ if (data[key] !== filter[`${key}Equals`]) {
+ return false;
+ }
+ }
+
+ // Test for contains.
+ if (filter[`${key}Contains`]) {
+ let value = (key == "host" ? "." : "") + data[key];
+ if (!data[key] || !value.includes(filter[`${key}Contains`])) {
+ return false;
+ }
+ }
+
+ // Test for prefix.
+ if (filter[`${key}Prefix`]) {
+ if (!data[key] || !data[key].startsWith(filter[`${key}Prefix`])) {
+ return false;
+ }
+ }
+
+ // Test for suffix.
+ if (filter[`${key}Suffix`]) {
+ if (!data[key] || !data[key].endsWith(filter[`${key}Suffix`])) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ serialize() {
+ return this.filters;
+ },
+};