diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 5f0ba6320d20..bd96c54f4c2a 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -573,6 +573,7 @@ ; [Extensions] @RESPATH@/components/extensions-toolkit.manifest +@RESPATH@/components/extension-process-script.js @RESPATH@/browser/components/extensions-browser.manifest ; Modules diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in index b9b4a3bf85a7..7cd48b38202e 100644 --- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -441,6 +441,7 @@ ; [Extensions] @BINPATH@/components/extensions-toolkit.manifest @BINPATH@/components/extensions-mobile.manifest +@BINPATH@/components/extension-process-script.js ; Features @BINPATH@/features/* diff --git a/toolkit/components/extensions/ExtensionPolicyService.cpp b/toolkit/components/extensions/ExtensionPolicyService.cpp index 731acb403a92..1dfec1b436d4 100644 --- a/toolkit/components/extensions/ExtensionPolicyService.cpp +++ b/toolkit/components/extensions/ExtensionPolicyService.cpp @@ -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 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 win = do_QueryInterface(aSubject); + if (win) { + CheckWindow(win); + } + } else if (!strcmp(aTopic, "document-element-inserted")) { + nsCOMPtr doc = do_QueryInterface(aSubject); + if (doc) { + CheckDocument(doc); + } + } else if (!strcmp(aTopic, "http-on-opening-request")) { + nsCOMPtr 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 loadInfo = aChannel->GetLoadInfo(); + if (!loadInfo) { + return; + } + + auto loadType = loadInfo->GetExternalContentPolicyType(); + if (loadType != nsIContentPolicy::TYPE_DOCUMENT && + loadType != nsIContentPolicy::TYPE_SUBDOCUMENT) { + return; + } + + nsCOMPtr 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 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 doc = aWindow->GetExtantDoc(); + if (!doc || doc->IsInitialDocument()) { + return; + } + + nsCOMPtr aboutBlank; + NS_ENSURE_SUCCESS_VOID(NS_NewURI(getter_AddRefs(aboutBlank), + "about:blank")); + + nsCOMPtr 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 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) diff --git a/toolkit/components/extensions/ExtensionPolicyService.h b/toolkit/components/extensions/ExtensionPolicyService.h index 20d986da16ba..db58e33e0a8b 100644 --- a/toolkit/components/extensions/ExtensionPolicyService.h +++ b/toolkit/components/extensions/ExtensionPolicyService.h @@ -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, WebExtensionPolicy> mExtensions; nsRefPtrHashtable mExtensionHosts; + + nsCOMPtr mObs; }; } // namespace mozilla diff --git a/toolkit/components/extensions/WebExtensionContentScript.h b/toolkit/components/extensions/WebExtensionContentScript.h index 18b6a9a7f0ea..0f6f831ca822 100644 --- a/toolkit/components/extensions/WebExtensionContentScript.h +++ b/toolkit/components/extensions/WebExtensionContentScript.h @@ -48,6 +48,14 @@ public: uint64_t FrameID() const; + nsPIDOMWindowOuter* GetWindow() const + { + if (mObj.is()) { + return mObj.as(); + } + return nullptr; + } + private: void SetURL(const URLInfo& aURL); diff --git a/toolkit/components/extensions/extension-process-script.js b/toolkit/components/extensions/extension-process-script.js index 00033a51f66a..43f59fdf6a76 100644 --- a/toolkit/components/extensions/extension-process-script.js +++ b/toolkit/components/extensions/extension-process-script.js @@ -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(); diff --git a/toolkit/components/extensions/extensions-toolkit.manifest b/toolkit/components/extensions/extensions-toolkit.manifest index 0e1c3c11bdeb..abfcad8ba58a 100644 --- a/toolkit/components/extensions/extensions-toolkit.manifest +++ b/toolkit/components/extensions/extensions-toolkit.manifest @@ -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} diff --git a/toolkit/components/extensions/jar.mn b/toolkit/components/extensions/jar.mn index 4dffa7cc9bfe..97374c1002f4 100644 --- a/toolkit/components/extensions/jar.mn +++ b/toolkit/components/extensions/jar.mn @@ -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 diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build index d826c85abb78..e5afd91d3236 100644 --- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -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', ] diff --git a/toolkit/components/extensions/mozIExtensionProcessScript.idl b/toolkit/components/extensions/mozIExtensionProcessScript.idl new file mode 100644 index 000000000000..0f470d5fef5f --- /dev/null +++ b/toolkit/components/extensions/mozIExtensionProcessScript.idl @@ -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); + +}; diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html index a659fea5fc09..84a08f0eb382 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html @@ -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"); } diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm index 58d7dea05d8f..f9c6a44b6551 100644 --- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -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*$/;