| 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); | 
| +}); | 
|  |