Bug 1368102: Part 5 - Move static content script matching into C++. r=mixedpuppy,zombie

MozReview-Commit-ID: Co04MoscqMx

--HG--
extra : rebase_source : 4a9b4c3146f99b4fd7eb8724224e954a5b3967f2
This commit is contained in:
Kris Maglione 2017-06-04 15:38:11 -07:00
parent 884bb014cb
commit bc5050f651
12 changed files with 277 additions and 139 deletions

View File

@ -573,6 +573,7 @@
; [Extensions]
@RESPATH@/components/extensions-toolkit.manifest
@RESPATH@/components/extension-process-script.js
@RESPATH@/browser/components/extensions-browser.manifest
; Modules

View File

@ -441,6 +441,7 @@
; [Extensions]
@BINPATH@/components/extensions-toolkit.manifest
@BINPATH@/components/extensions-mobile.manifest
@BINPATH@/components/extension-process-script.js
; Features
@BINPATH@/features/*

View File

@ -9,8 +9,17 @@
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozIExtensionProcessScript.h"
#include "nsEscape.h"
#include "nsGkAtoms.h"
#include "nsIChannel.h"
#include "nsIContentPolicy.h"
#include "nsIDocument.h"
#include "nsILoadInfo.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
#include "nsXULAppAPI.h"
namespace mozilla {
@ -24,6 +33,23 @@ using namespace extensions;
"script-src 'self'; object-src 'self';"
#define OBS_TOPIC_PRELOAD_SCRIPT "web-extension-preload-content-script"
#define OBS_TOPIC_LOAD_SCRIPT "web-extension-load-content-script"
static mozIExtensionProcessScript&
ProcessScript()
{
static nsCOMPtr<mozIExtensionProcessScript> sProcessScript;
if (MOZ_UNLIKELY(!sProcessScript)) {
sProcessScript = do_GetService("@mozilla.org/webextensions/extension-process-script;1");
MOZ_RELEASE_ASSERT(sProcessScript);
ClearOnShutdown(&sProcessScript);
}
return *sProcessScript;
}
/*****************************************************************************
* ExtensionPolicyService
*****************************************************************************/
@ -40,6 +66,14 @@ ExtensionPolicyService::GetSingleton()
return *sExtensionPolicyService.get();
}
ExtensionPolicyService::ExtensionPolicyService()
{
mObs = services::GetObserverService();
MOZ_RELEASE_ASSERT(mObs);
RegisterObservers();
}
WebExtensionPolicy*
ExtensionPolicyService::GetByURL(const URLInfo& aURL)
@ -114,6 +148,134 @@ ExtensionPolicyService::DefaultCSP(nsAString& aDefaultCSP) const
}
/*****************************************************************************
* Content script management
*****************************************************************************/
void
ExtensionPolicyService::RegisterObservers()
{
mObs->AddObserver(this, "content-document-global-created", false);
mObs->AddObserver(this, "document-element-inserted", false);
if (XRE_IsContentProcess()) {
mObs->AddObserver(this, "http-on-opening-request", false);
}
}
void
ExtensionPolicyService::UnregisterObservers()
{
mObs->RemoveObserver(this, "content-document-global-created");
mObs->RemoveObserver(this, "document-element-inserted");
if (XRE_IsContentProcess()) {
mObs->RemoveObserver(this, "http-on-opening-request");
}
}
nsresult
ExtensionPolicyService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData)
{
if (!strcmp(aTopic, "content-document-global-created")) {
nsCOMPtr<nsPIDOMWindowOuter> win = do_QueryInterface(aSubject);
if (win) {
CheckWindow(win);
}
} else if (!strcmp(aTopic, "document-element-inserted")) {
nsCOMPtr<nsIDocument> doc = do_QueryInterface(aSubject);
if (doc) {
CheckDocument(doc);
}
} else if (!strcmp(aTopic, "http-on-opening-request")) {
nsCOMPtr<nsIChannel> chan = do_QueryInterface(aSubject);
if (chan) {
CheckRequest(chan);
}
}
return NS_OK;
}
// Checks a request for matching content scripts, and begins pre-loading them
// if necessary.
void
ExtensionPolicyService::CheckRequest(nsIChannel* aChannel)
{
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
if (!loadInfo) {
return;
}
auto loadType = loadInfo->GetExternalContentPolicyType();
if (loadType != nsIContentPolicy::TYPE_DOCUMENT &&
loadType != nsIContentPolicy::TYPE_SUBDOCUMENT) {
return;
}
nsCOMPtr<nsIURI> uri;
if (NS_FAILED(aChannel->GetURI(getter_AddRefs(uri)))) {
return;
}
CheckContentScripts({uri.get(), loadInfo}, true);
}
// Checks a document, just after the document element has been inserted, for
// matching content scripts or extension principals, and loads them if
// necessary.
void
ExtensionPolicyService::CheckDocument(nsIDocument* aDocument)
{
nsCOMPtr<nsPIDOMWindowOuter> win = aDocument->GetWindow();
if (win) {
CheckContentScripts(win.get(), false);
}
}
// Checks for loads of about:blank into new window globals, and loads any
// matching content scripts. about:blank loads do not trigger document element
// inserted events, so they're the only load type that are special cased this
// way.
void
ExtensionPolicyService::CheckWindow(nsPIDOMWindowOuter* aWindow)
{
// We only care about non-initial document loads here. The initial
// about:blank document will usually be re-used to load another document.
nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc();
if (!doc || doc->IsInitialDocument()) {
return;
}
nsCOMPtr<nsIURI> aboutBlank;
NS_ENSURE_SUCCESS_VOID(NS_NewURI(getter_AddRefs(aboutBlank),
"about:blank"));
nsCOMPtr<nsIURI> uri = doc->GetDocumentURI();
bool equal;
if (NS_FAILED(uri->EqualsExceptRef(aboutBlank, &equal)) || !equal) {
return;
}
CheckContentScripts(aWindow, false);
}
void
ExtensionPolicyService::CheckContentScripts(const DocInfo& aDocInfo, bool aIsPreload)
{
for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) {
RefPtr<WebExtensionPolicy> policy = iter.Data();
for (auto& script : policy->ContentScripts()) {
if (script->Matches(aDocInfo)) {
if (aIsPreload) {
ProcessScript().PreloadContentScript(script);
} else {
ProcessScript().LoadContentScript(script, aDocInfo.GetWindow());
}
}
}
}
}
/*****************************************************************************
* nsIAddonPolicyService
*****************************************************************************/
@ -213,7 +375,8 @@ NS_IMPL_CYCLE_COLLECTION(ExtensionPolicyService, mExtensions, mExtensionHosts)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionPolicyService)
NS_INTERFACE_MAP_ENTRY(nsIAddonPolicyService)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAddonPolicyService)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionPolicyService)

View File

@ -12,20 +12,34 @@
#include "nsHashKeys.h"
#include "nsIAddonPolicyService.h"
#include "nsIAtom.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsISupports.h"
#include "nsPointerHashKeys.h"
#include "nsRefPtrHashtable.h"
namespace mozilla {
class nsIChannel;
class nsIObserverService;
class nsIDocument;
class nsIPIDOMWindowOuter;
namespace mozilla {
namespace extensions {
class DocInfo;
}
using extensions::DocInfo;
using extensions::WebExtensionPolicy;
class ExtensionPolicyService final : public nsIAddonPolicyService
, public nsIObserver
{
public:
NS_DECL_CYCLE_COLLECTION_CLASS(ExtensionPolicyService)
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(ExtensionPolicyService,
nsIAddonPolicyService)
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_NSIADDONPOLICYSERVICE
NS_DECL_NSIOBSERVER
static ExtensionPolicyService& GetSingleton();
@ -65,10 +79,21 @@ protected:
virtual ~ExtensionPolicyService() = default;
private:
ExtensionPolicyService() = default;
ExtensionPolicyService();
void RegisterObservers();
void UnregisterObservers();
void CheckRequest(nsIChannel* aChannel);
void CheckDocument(nsIDocument* aDocument);
void CheckWindow(nsPIDOMWindowOuter* aWindow);
void CheckContentScripts(const DocInfo& aDocInfo, bool aIsPreload);
nsRefPtrHashtable<nsPtrHashKey<const nsIAtom>, WebExtensionPolicy> mExtensions;
nsRefPtrHashtable<nsCStringHashKey, WebExtensionPolicy> mExtensionHosts;
nsCOMPtr<nsIObserverService> mObs;
};
} // namespace mozilla

View File

@ -48,6 +48,14 @@ public:
uint64_t FrameID() const;
nsPIDOMWindowOuter* GetWindow() const
{
if (mObj.is<Window>()) {
return mObj.as<Window>();
}
return nullptr;
}
private:
void SetURL(const URLInfo& aURL);

View File

@ -26,11 +26,15 @@ XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
"resource://gre/modules/ExtensionContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPageChild",
"resource://gre/modules/ExtensionPageChild.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
XPCOMUtils.defineLazyGetter(this, "getInnerWindowID", () => ExtensionUtils.getInnerWindowID);
const {
DefaultWeakMap,
getInnerWindowID,
} = ExtensionUtils;
// We need to avoid touching Services.appinfo here in order to prevent
// the wrong version from being cached during xpcshell test startup.
@ -62,10 +66,6 @@ class ScriptMatcher {
this._script = null;
}
get matchAboutBlank() {
return this.matcher.matchAboutBlank;
}
get script() {
if (!this._script) {
this._script = new ExtensionContent.Script(this.extension.realExtension,
@ -81,10 +81,6 @@ class ScriptMatcher {
script.compileScripts();
}
matchesLoadInfo(uri, loadInfo) {
return this.matcher.matchesLoadInfo(uri, loadInfo);
}
matchesWindow(window) {
return this.matcher.matchesWindow(window);
}
@ -157,6 +153,10 @@ class ExtensionGlobal {
}
}
let stubExtensions = new WeakMap();
let scriptMatchers = new DefaultWeakMap(matcher => new ScriptMatcher(stubExtensions.get(matcher.extension),
matcher));
// Responsible for creating ExtensionContexts and injecting content
// scripts into them when new documents are created.
DocumentManager = {
@ -176,32 +176,6 @@ DocumentManager = {
Services.obs.removeObserver(this, "document-element-inserted");
},
// Initialize listeners that we need when any extension content script is
// enabled.
initMatchers() {
if (isContentProcess) {
Services.obs.addObserver(this, "http-on-opening-request");
}
},
uninitMatchers() {
if (isContentProcess) {
Services.obs.removeObserver(this, "http-on-opening-request");
}
},
// Initialize listeners that we need when any about:blank content script is
// enabled.
//
// Loads of about:blank are special, and do not trigger "document-element-inserted"
// observers. So if we have any scripts that match about:blank, we also need
// to observe "content-document-global-created".
initAboutBlankMatchers() {
Services.obs.addObserver(this, "content-document-global-created");
},
uninitAboutBlankMatchers() {
Services.obs.removeObserver(this, "content-document-global-created");
},
extensionProcessInitialized: false,
initExtensionProcess() {
if (this.extensionProcessInitialized || !ExtensionManagement.isExtensionProcess) {
@ -243,82 +217,20 @@ DocumentManager = {
}
this.extensionCount++;
for (let script of extension.scripts) {
this.addContentScript(script);
}
this.injectExtensionScripts(extension);
},
uninitExtension(extension) {
for (let script of extension.scripts) {
this.removeContentScript(script);
}
this.extensionCount--;
if (this.extensionCount === 0) {
this.uninit();
}
},
extensionCount: 0,
matchAboutBlankCount: 0,
contentScripts: new Set(),
addContentScript(script) {
if (this.contentScripts.size == 0) {
this.initMatchers();
}
if (script.matchAboutBlank) {
if (this.matchAboutBlankCount == 0) {
this.initAboutBlankMatchers();
}
this.matchAboutBlankCount++;
}
this.contentScripts.add(script);
},
removeContentScript(script) {
this.contentScripts.delete(script);
if (this.contentScripts.size == 0) {
this.uninitMatchers();
}
if (script.matchAboutBlank) {
this.matchAboutBlankCount--;
if (this.matchAboutBlankCount == 0) {
this.uninitAboutBlankMatchers();
}
}
},
// Listeners
observers: {
async "content-document-global-created"(window) {
// We only care about about:blank here, since it doesn't trigger
// "document-element-inserted".
if ((window.location && window.location.href !== "about:blank") ||
// Make sure we only load into frames that belong to tabs, or other
// special areas that we want to load content scripts into.
!this.globals.has(getMessageManager(window))) {
return;
}
// We can't tell for certain whether the final document will actually be
// about:blank at this point, though, so wait for the DOM to finish
// loading and check again before injecting scripts.
await new Promise(resolve => window.addEventListener(
"DOMContentLoaded", resolve, {once: true, capture: true}));
if (window.location.href === "about:blank") {
this.injectWindowScripts(window);
}
},
"document-element-inserted"(document) {
let window = document.defaultView;
if (!document.location || !window ||
@ -328,24 +240,9 @@ DocumentManager = {
return;
}
this.injectWindowScripts(window);
this.loadInto(window);
},
"http-on-opening-request"(subject, topic, data) {
// If this request is a docshell load, check whether any of our scripts
// are likely to be loaded into it, and begin preloading the ones that
// are.
let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
if (loadInfo) {
let {externalContentPolicyType: type} = loadInfo;
if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
this.preloadScripts(subject.URI, loadInfo);
}
}
},
"tab-content-frameloader-created"(global) {
this.initGlobal(global);
},
@ -359,30 +256,14 @@ DocumentManager = {
injectExtensionScripts(extension) {
for (let window of this.enumerateWindows()) {
for (let script of extension.scripts) {
for (let script of extension.policy.contentScripts) {
if (script.matchesWindow(window)) {
script.injectInto(window);
scriptMatchers.get(script).injectInto(window);
}
}
}
},
injectWindowScripts(window) {
for (let script of this.contentScripts) {
if (script.matchesWindow(window)) {
script.injectInto(window);
}
}
},
preloadScripts(uri, loadInfo) {
for (let script of this.contentScripts) {
if (script.matchesLoadInfo(uri, loadInfo)) {
script.preload();
}
}
},
/**
* Checks that all parent frames for the given withdow either have the
* same add-on ID, or are special chrome-privileged documents such as
@ -495,6 +376,8 @@ class StubExtension {
} else {
this.policy = WebExtensionPolicy.getByID(this.id);
}
stubExtensions.set(this.policy, this);
}
shutdown() {
@ -597,5 +480,31 @@ ExtensionManager = {
},
};
function ExtensionProcessScript() {
if (!ExtensionProcessScript.singleton) {
ExtensionProcessScript.singleton = this;
}
return ExtensionProcessScript.singleton;
}
ExtensionProcessScript.singleton = null;
ExtensionProcessScript.prototype = {
classID: Components.ID("{21f9819e-4cdf-49f9-85a0-850af91a5058}"),
QueryInterface: XPCOMUtils.generateQI([Ci.mozIExtensionProcessScript]),
preloadContentScript(contentScript) {
scriptMatchers.get(contentScript).preload();
},
loadContentScript(contentScript, window) {
if (DocumentManager.globals.has(getMessageManager(window))) {
scriptMatchers.get(contentScript).injectInto(window);
}
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ExtensionProcessScript]);
DocumentManager.earlyInit();
ExtensionManager.init();

View File

@ -7,3 +7,7 @@ category webextension-scripts-addon toolkit chrome://extensions/content/ext-c-to
category webextension-schemas events chrome://extensions/content/schemas/events.json
category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
category webextension-schemas types chrome://extensions/content/schemas/types.json
component {21f9819e-4cdf-49f9-85a0-850af91a5058} extension-process-script.js
contract @mozilla.org/webextensions/extension-process-script;1 {21f9819e-4cdf-49f9-85a0-850af91a5058}

View File

@ -4,7 +4,6 @@
toolkit.jar:
% content extensions %content/extensions/
content/extensions/extension-process-script.js
content/extensions/ext-alarms.js
content/extensions/ext-backgroundPage.js
content/extensions/ext-browser-content.js

View File

@ -32,6 +32,7 @@ EXTRA_JS_MODULES += [
]
EXTRA_COMPONENTS += [
'extension-process-script.js',
'extensions-toolkit.manifest',
]
@ -45,6 +46,12 @@ DIRS += [
'webrequest',
]
XPIDL_SOURCES += [
'mozIExtensionProcessScript.idl',
]
XPIDL_MODULE = 'webextensions'
EXPORTS.mozilla = [
'ExtensionPolicyService.h',
]

View File

@ -0,0 +1,16 @@
/* 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/. */
#include "nsISupports.idl"
interface mozIDOMWindowProxy;
[scriptable,uuid(6b09dc51-6caa-4ca7-9d6d-30c87258a630)]
interface mozIExtensionProcessScript : nsISupports
{
void preloadContentScript(in nsISupports contentScript);
void loadContentScript(in nsISupports contentScript, in mozIDOMWindowProxy window);
};

View File

@ -67,7 +67,7 @@ add_task(async function test_contentscript_cache() {
let {ExtensionManager} = Components.utils.import("resource://gre/modules/ExtensionChild.jsm", {});
let ext = ExtensionManager.extensions.get(extensionId);
if (ext) {
if (ext && ext.staticScripts) {
assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process");
}

View File

@ -101,7 +101,12 @@ XPCOMUtils.defineLazyGetter(this, "CertUtils", function() {
XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
PREF_WEBEXT_PERM_PROMPTS, false);
Services.ppmm.loadProcessScript("chrome://extensions/content/extension-process-script.js", true);
// Initialize the WebExtension process script service as early as possible,
// since it needs to be able to track things like new frameLoader globals that
// are created before other framework code has been initialized.
Services.ppmm.loadProcessScript(
"data:,Components.classes['@mozilla.org/webextensions/extension-process-script;1'].getService()",
true);
const INTEGER = /^[1-9]\d*$/;