forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			253 lines
		
	
	
	
		
			9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			253 lines
		
	
	
	
		
			9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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";
 | |
| 
 | |
| /* global ExtensionCommon, ExtensionAPI, Services, XPCOMUtils, ExtensionUtils */
 | |
| 
 | |
| const { AddonManager } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/AddonManager.sys.mjs"
 | |
| );
 | |
| const { WebRequest } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/WebRequest.sys.mjs"
 | |
| );
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   AddonSearchEngine: "resource://gre/modules/AddonSearchEngine.sys.mjs",
 | |
| });
 | |
| 
 | |
| // eslint-disable-next-line mozilla/reject-importGlobalProperties
 | |
| XPCOMUtils.defineLazyGlobalGetters(this, ["ChannelWrapper"]);
 | |
| 
 | |
| const SEARCH_TOPIC_ENGINE_MODIFIED = "browser-search-engine-modified";
 | |
| 
 | |
| this.addonsSearchDetection = class extends ExtensionAPI {
 | |
|   getAPI(context) {
 | |
|     const { extension } = context;
 | |
| 
 | |
|     // We want to temporarily store the first monitored URLs that have been
 | |
|     // redirected, indexed by request IDs, so that the background script can
 | |
|     // find the add-on IDs to report.
 | |
|     this.firstMatchedUrls = {};
 | |
| 
 | |
|     return {
 | |
|       addonsSearchDetection: {
 | |
|         // `getMatchPatterns()` returns a map where each key is an URL pattern
 | |
|         // to monitor and its corresponding value is a list of add-on IDs
 | |
|         // (search engines).
 | |
|         //
 | |
|         // Note: We don't return a simple list of URL patterns because the
 | |
|         // background script might want to lookup add-on IDs for a given URL in
 | |
|         // the case of server-side redirects.
 | |
|         async getMatchPatterns() {
 | |
|           const patterns = {};
 | |
| 
 | |
|           try {
 | |
|             await Services.search.promiseInitialized;
 | |
|             const visibleEngines = await Services.search.getEngines();
 | |
| 
 | |
|             visibleEngines.forEach(engine => {
 | |
|               if (!(engine instanceof lazy.AddonSearchEngine)) {
 | |
|                 return;
 | |
|               }
 | |
|               const { _extensionID, _urls } = engine.wrappedJSObject;
 | |
| 
 | |
|               if (!_extensionID) {
 | |
|                 // OpenSearch engines don't have an extension ID.
 | |
|                 return;
 | |
|               }
 | |
| 
 | |
|               _urls
 | |
|                 // We only want to collect "search URLs" (and not "suggestion"
 | |
|                 // ones for instance). See `URL_TYPE` in `SearchUtils.sys.mjs`.
 | |
|                 .filter(({ type }) => type === "text/html")
 | |
|                 .forEach(({ template }) => {
 | |
|                   // If this is changed, double check the code in the background
 | |
|                   // script because `webRequestCancelledHandler` splits patterns
 | |
|                   // on `*` to retrieve URL prefixes.
 | |
|                   const pattern = template.split("?")[0] + "*";
 | |
| 
 | |
|                   // Multiple search engines could register URL templates that
 | |
|                   // would become the same URL pattern as defined above so we
 | |
|                   // store a list of extension IDs per URL pattern.
 | |
|                   if (!patterns[pattern]) {
 | |
|                     patterns[pattern] = [];
 | |
|                   }
 | |
| 
 | |
|                   // We exclude built-in search engines because we don't need
 | |
|                   // to report them.
 | |
|                   if (
 | |
|                     !patterns[pattern].includes(_extensionID) &&
 | |
|                     !_extensionID.endsWith("@search.mozilla.org")
 | |
|                   ) {
 | |
|                     patterns[pattern].push(_extensionID);
 | |
|                   }
 | |
|                 });
 | |
|             });
 | |
|           } catch (err) {
 | |
|             console.error(err);
 | |
|           }
 | |
| 
 | |
|           return patterns;
 | |
|         },
 | |
| 
 | |
|         // `getAddonVersion()` returns the add-on version if it exists.
 | |
|         async getAddonVersion(addonId) {
 | |
|           const addon = await AddonManager.getAddonByID(addonId);
 | |
| 
 | |
|           return addon && addon.version;
 | |
|         },
 | |
| 
 | |
|         // `getPublicSuffix()` returns the public suffix/Effective TLD Service
 | |
|         // of the given URL.
 | |
|         // See: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIEffectiveTLDService
 | |
|         async getPublicSuffix(url) {
 | |
|           try {
 | |
|             return Services.eTLD.getBaseDomain(Services.io.newURI(url));
 | |
|           } catch (err) {
 | |
|             console.error(err);
 | |
|             return null;
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         // `onSearchEngineModified` is an event that occurs when the list of
 | |
|         // search engines has changed, e.g., a new engine has been added or an
 | |
|         // engine has been removed.
 | |
|         //
 | |
|         // See: https://searchfox.org/mozilla-central/rev/cb44fc4f7bb84f2a18fedba64c8563770df13e34/toolkit/components/search/SearchUtils.sys.mjs#185-193
 | |
|         onSearchEngineModified: new ExtensionCommon.EventManager({
 | |
|           context,
 | |
|           name: "addonsSearchDetection.onSearchEngineModified",
 | |
|           register: fire => {
 | |
|             const onSearchEngineModifiedObserver = (
 | |
|               aSubject,
 | |
|               aTopic,
 | |
|               aData
 | |
|             ) => {
 | |
|               if (
 | |
|                 aTopic !== SEARCH_TOPIC_ENGINE_MODIFIED ||
 | |
|                 // We are only interested in these modified types.
 | |
|                 !["engine-added", "engine-removed", "engine-changed"].includes(
 | |
|                   aData
 | |
|                 )
 | |
|               ) {
 | |
|                 return;
 | |
|               }
 | |
| 
 | |
|               fire.async();
 | |
|             };
 | |
| 
 | |
|             Services.obs.addObserver(
 | |
|               onSearchEngineModifiedObserver,
 | |
|               SEARCH_TOPIC_ENGINE_MODIFIED
 | |
|             );
 | |
| 
 | |
|             return () => {
 | |
|               Services.obs.removeObserver(
 | |
|                 onSearchEngineModifiedObserver,
 | |
|                 SEARCH_TOPIC_ENGINE_MODIFIED
 | |
|               );
 | |
|             };
 | |
|           },
 | |
|         }).api(),
 | |
| 
 | |
|         // `onRedirected` is an event fired after a request has stopped and
 | |
|         // this request has been redirected once or more. The registered
 | |
|         // listeners will received the following properties:
 | |
|         //
 | |
|         //   - `addonId`: the add-on ID that redirected the request, if any.
 | |
|         //   - `firstUrl`: the first monitored URL of the request that has
 | |
|         //      been redirected.
 | |
|         //   - `lastUrl`: the last URL loaded for the request, after one or
 | |
|         //      more redirects.
 | |
|         onRedirected: new ExtensionCommon.EventManager({
 | |
|           context,
 | |
|           name: "addonsSearchDetection.onRedirected",
 | |
|           register: (fire, filter) => {
 | |
|             const stopListener = event => {
 | |
|               if (event.type != "stop") {
 | |
|                 return;
 | |
|               }
 | |
| 
 | |
|               const wrapper = event.currentTarget;
 | |
|               const { channel, id: requestId } = wrapper;
 | |
| 
 | |
|               // When we detected a redirect, we read the request property,
 | |
|               // hoping to find an add-on ID corresponding to the add-on that
 | |
|               // initiated the redirect. It might not return anything when the
 | |
|               // redirect is a search server-side redirect but it can also be
 | |
|               // caused by an error.
 | |
|               let addonId;
 | |
|               try {
 | |
|                 addonId = channel
 | |
|                   ?.QueryInterface(Ci.nsIPropertyBag)
 | |
|                   ?.getProperty("redirectedByExtension");
 | |
|               } catch (err) {
 | |
|                 console.error(err);
 | |
|               }
 | |
| 
 | |
|               const firstUrl = this.firstMatchedUrls[requestId];
 | |
|               // We don't need this entry anymore.
 | |
|               delete this.firstMatchedUrls[requestId];
 | |
| 
 | |
|               const lastUrl = wrapper.finalURL;
 | |
| 
 | |
|               if (!firstUrl || !lastUrl) {
 | |
|                 // Something went wrong but there is nothing we can do at this
 | |
|                 // point.
 | |
|                 return;
 | |
|               }
 | |
| 
 | |
|               fire.sync({ addonId, firstUrl, lastUrl });
 | |
|             };
 | |
| 
 | |
|             const listener = ({ requestId, url, originUrl }) => {
 | |
|               // We exclude requests not originating from the location bar,
 | |
|               // bookmarks and other "system-ish" requests.
 | |
|               if (originUrl !== undefined) {
 | |
|                 return;
 | |
|               }
 | |
| 
 | |
|               // Keep the first monitored URL that was redirected for the
 | |
|               // request until the request has stopped.
 | |
|               if (!this.firstMatchedUrls[requestId]) {
 | |
|                 this.firstMatchedUrls[requestId] = url;
 | |
| 
 | |
|                 const wrapper = ChannelWrapper.getRegisteredChannel(
 | |
|                   requestId,
 | |
|                   context.extension.policy,
 | |
|                   context.xulBrowser.frameLoader.remoteTab
 | |
|                 );
 | |
| 
 | |
|                 wrapper.addEventListener("stop", stopListener);
 | |
|               }
 | |
|             };
 | |
| 
 | |
|             WebRequest.onBeforeRedirect.addListener(
 | |
|               listener,
 | |
|               // filter
 | |
|               {
 | |
|                 types: ["main_frame"],
 | |
|                 urls: ExtensionUtils.parseMatchPatterns(filter.urls),
 | |
|               },
 | |
|               // info
 | |
|               [],
 | |
|               // listener details
 | |
|               {
 | |
|                 addonId: extension.id,
 | |
|                 policy: extension.policy,
 | |
|                 blockingAllowed: false,
 | |
|               }
 | |
|             );
 | |
| 
 | |
|             return () => {
 | |
|               WebRequest.onBeforeRedirect.removeListener(listener);
 | |
|             };
 | |
|           },
 | |
|         }).api(),
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| };
 | 
