Index: lib/snippets.js |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/lib/snippets.js |
@@ -0,0 +1,182 @@ |
+/* |
+ * This file is part of Adblock Plus <https://adblockplus.org/>, |
+ * Copyright (C) 2006-present eyeo GmbH |
+ * |
+ * Adblock Plus is free software: you can redistribute it and/or modify |
+ * it under the terms of the GNU General Public License version 3 as |
+ * published by the Free Software Foundation. |
+ * |
+ * Adblock Plus is distributed in the hope that it will be useful, |
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of |
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
+ * GNU General Public License for more details. |
+ * |
+ * You should have received a copy of the GNU General Public License |
+ * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
+ */ |
+ |
+"use strict"; |
+ |
+const {defaultMatcher} = require("matcher"); |
+const {RegExpFilter, WhitelistFilter} = require("filterClasses"); |
+const {verifySignature} = require("rsa"); |
+const {extractHostFromFrame, isThirdParty, stringifyURL} = require("url"); |
+const {checkWhitelisted} = require("whitelisting"); |
+const {FilterNotifier} = require("filterNotifier"); |
+const devtools = require("devtools"); |
+const info = require("info"); |
+ |
+const {typeMap} = RegExpFilter; |
+ |
+const remoteURL = "https://easylist-downloads.adblockplus.org/snippets.js"; |
+ |
+let libraries = {local: "", remote: ""}; |
+let executableCode = new Map(); |
+ |
+function fetchText(url) |
+{ |
+ return fetch(url, {cache: "no-cache"}).then( |
+ response => response.ok ? response.text() : "" |
+ ); |
+} |
+ |
+function updateLibrary(name, text) |
+{ |
+ libraries[name] = text; |
+ |
+ executableCode.clear(); |
+} |
+ |
+function checkLibrarySignature(url, text) |
+{ |
+ return fetchText(`${url}.sig`).then( |
+ signature => fetchText(browser.extension.getURL("/key")).then( |
+ key => verifySignature(key.replace(/=/g, ""), signature, text) |
+ ) |
+ ); |
+} |
+ |
+function loadLibrary(name, url, {verify = true} = {}) |
+{ |
+ fetchText(url).then(text => |
+ { |
+ if (text != libraries[name]) |
+ { |
+ let check = verify ? checkLibrarySignature(url, text) : |
+ Promise.resolve(true); |
+ check.then(ok => |
+ { |
+ if (ok) |
+ updateLibrary(name, text); |
+ }); |
+ } |
+ }); |
+} |
+ |
+function loadLocalLibrary() |
+{ |
+ loadLibrary("local", browser.extension.getURL("/snippets.js"), |
+ {verify: false}); |
+} |
+ |
+function loadRemoteLibrary() |
+{ |
+ loadLibrary("remote", remoteURL); |
+} |
+ |
+function getExecutableCode(snippet) |
+{ |
+ let code = executableCode.get(snippet); |
+ if (code) |
+ return code; |
+ |
+ code = ` |
+ "use strict"; |
+ { |
+ let localImports = Object.create(null); |
+ let remoteImports = Object.create(null); |
+ new Function("exports", ${JSON.stringify(libraries.local)})( |
+ localImports |
+ ); |
+ new Function("exports", ${JSON.stringify(libraries.remote)})( |
+ remoteImports |
+ ); |
+ let imports = Object.assign(Object.create(null), |
+ localImports, |
+ remoteImports); |
+ let key = ${JSON.stringify(snippet)}; |
+ if (Object.prototype.hasOwnProperty.call(imports, key)) |
+ { |
+ let value = imports[key]; |
+ if (typeof value == "function") |
+ value(); |
+ } |
+ } |
+ `; |
+ |
+ executableCode.set(snippet, code); |
+ return code; |
+} |
+ |
+function injectCode(snippet, tabId, frameId) |
+{ |
+ try |
+ { |
+ browser.tabs.executeScript(tabId, { |
+ code: getExecutableCode(snippet), |
+ frameId, |
+ matchAboutBlank: true, |
+ runAt: "document_start" |
+ }); |
+ } |
+ catch (error) |
+ { |
+ } |
+} |
+ |
+loadLocalLibrary(); |
+ |
+// Only Chrome supports dynamic loading of JS. |
+if (info.platform == "chromium") |
+{ |
+ loadRemoteLibrary(); |
+ |
+ // Download every 24 hours. |
+ setInterval(loadRemoteLibrary, 24 * 60 * 60 * 1000); |
+} |
+ |
+browser.webNavigation.onCommitted.addListener(({tabId, frameId, url}) => |
+{ |
+ // There's a bug in Chrome that causes webNavigation.onCommitted to get |
+ // dispatched twice if there's a URL filter present, therefore we must listen |
+ // for all URLs and do an explicit check here. |
+ // https://crbug.com/827855 |
+ if (!/^(https?:\/\/|about:blank\b|about:srcdoc\b)/.test(url)) |
+ return; |
+ |
+ let urlObject = new URL(url); |
+ let urlString = stringifyURL(urlObject); |
+ let frame = ext.getFrame(tabId, frameId); |
+ let hostname = extractHostFromFrame(frame); |
+ let thirdParty = isThirdParty(urlObject, hostname); |
+ |
+ let filter = defaultMatcher.matchesAny(urlString, typeMap.SNIPPET, hostname, |
+ thirdParty, null, true); |
+ if (!filter) |
+ return; |
+ |
+ let page = new ext.Page({id: tabId, url}); |
+ |
+ if (checkWhitelisted(page, frame)) |
+ return; |
+ |
+ devtools.logRequest(page, urlString, "SNIPPET", hostname, thirdParty, null, |
+ true, filter); |
+ FilterNotifier.emit("filter.hitCount", filter, 0, 0, page); |
+ |
+ if (filter instanceof WhitelistFilter) |
+ return; |
+ |
+ for (let snippet of filter.snippets) |
+ injectCode(snippet, tabId, frameId); |
+}); |