Skip to content
This repository has been archived by the owner on Dec 3, 2020. It is now read-only.

Fix #109: Disable extraction outside of allowlisted sites. #138

Merged
merged 1 commit into from
Oct 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/background/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
*/

import config from 'commerce/config';
import {handleConfigMessage} from 'commerce/config/background';
import {CONFIG_MESSAGE_TYPE} from 'commerce/config/content';
import {handleExtractedProductData} from 'commerce/background/extraction';
import {handleNotificationClicked, handlePriceAlerts} from 'commerce/background/price_alerts';
import {handleWebRequest, updatePrices} from 'commerce/background/price_updates';
Expand All @@ -29,6 +31,15 @@ import {loadStateFromStorage} from 'commerce/state/sync';
}
});

// Setup config listener; returns for onMessage listeners can't be consistent
// as returning a value prevents other listeners from returning values.
/* eslint-disable consistent-return */
browser.runtime.onMessage.addListener(async (message, sender) => {
if (message.type === CONFIG_MESSAGE_TYPE) {
return handleConfigMessage(message, sender);
}
});

// Display price alerts when they are inserted into the state.
// This includes the initial load from extension storage below.
store.subscribe(handlePriceAlerts);
Expand Down
94 changes: 73 additions & 21 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,108 @@
/* 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/. */
/* eslint-disable no-unused-vars */

/**
* Config values that are shared between files or otherwise useful to have in
* a separate file. Config values can be overridden by setting a pref at the
* subtree [email protected].
*
* Content scripts cannot access the preference API, and thus cannot use this
* module to get config values. Use commerce/config/content instead to use
* message passing to fetch the config values from the background script.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was very confused by the new config folder until I read this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, it's kinda confusing.

* @module
*/

const DEFAULTS = {
class Value {
constructor(defaultValue) {
this.defaultValue = defaultValue;
}

async get(name) {
throw new Error('The Value config class cannot be used directly; use a type-specific subclass.');
}
}

class StringValue extends Value {
async get(name) {
return browser.shoppingPrefs.getCharPref(name, this.defaultValue);
}
}

class IntValue extends Value {
async get(name) {
return browser.shoppingPrefs.getIntPref(name, this.defaultValue);
}
}

class BoolValue extends Value {
async get(name) {
return browser.shoppingPrefs.getBoolPref(name, this.defaultValue);
}
}

class ListValue extends Value {
async get(name) {
const prefValue = await browser.shoppingPrefs.getCharPref(name, this.defaultValue.join(','));
return prefValue.split(',');
}
}

const CONFIG = {
/** Time to wait between price checks for a product */
priceCheckInterval: 1000 * 60 * 60 * 6, // 6 hours
priceCheckInterval: new IntValue(1000 * 60 * 60 * 6), // 6 hours

/** Time to wait between checking if we should fetch new prices */
priceCheckTimeoutInterval: 1000 * 60 * 15, // 15 minutes
priceCheckTimeoutInterval: new IntValue(1000 * 60 * 15), // 15 minutes

/** Delay before removing iframes created during price checks */
iframeTimeout: 1000 * 60, // 1 minute
iframeTimeout: new IntValue(1000 * 60), // 1 minute

// URLs to files within the extension
browserActionUrl: browser.extension.getURL('/browser_action/index.html'),
browserActionUrl: new StringValue(browser.extension.getURL('/browser_action/index.html')),

// Price alert config
alertPercentThershold: 5, // 5%
alertAbsoluteThreshold: 1000, // $10
alertPercentThershold: new IntValue(5), // 5%
alertAbsoluteThreshold: new IntValue(1000), // $10

/** Color of the toolbar badge for showing active price alerts. */
badgeAlertBackground: '#00FEFF',
badgeAlertBackground: new StringValue('#00FEFF'),

/** Color of the toolbar badge when a product on the current page is trackable. */
badgeDetectBackground: '#33F70C',
badgeDetectBackground: new StringValue('#33F70C'),

/** URL for the add-on's page on support.mozilla.org */
supportUrl: 'https://support.mozilla.org',
supportUrl: new StringValue('https://support.mozilla.org'),

/** URL for the add-on's feedback form */
feedbackUrl: 'https://www.mozilla.org',
feedbackUrl: new StringValue('https://www.mozilla.org'),

/** List of domains that extraction is performed on. */
extractionAllowlist: new ListValue([
'amazon.com',
'www.amazon.com',
'smile.amazon.com',
'bestbuy.com',
'www.bestbuy.com',
'ebay.com',
'www.ebay.com',
'homedepot.com',
'www.homedepot.com',
'walmart.com',
'www.walmart.com',
'mkelly.me',
'www.mkelly.me',
]),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this being hardcoded in, can we generate this list from fallback_extraction_selectors.js (available after a rebase)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing that would essentially require fallback extraction support for all sites that we ever support, which isn't really maintainable. Keeping them separate means more typing for sites that are supported, but also allows for sites that we only support via Fathom extraction.

};

export default {
async get(configName) {
const defaultValue = DEFAULTS[configName];
switch (typeof defaultValue) {
case 'string':
return browser.shoppingPrefs.getCharPref(configName, defaultValue);
case 'number':
return browser.shoppingPrefs.getIntPref(configName, defaultValue);
case 'boolean':
return browser.shoppingPrefs.getBoolPref(configName, defaultValue);
default:
throw new Error(`Invalid config type ${typeof defaultValue} for config ${configName}`);
const value = CONFIG[configName];
if (!value) {
throw new Error(`Invalid config ${configName}`);
}

return value.get(configName);
},
};
15 changes: 15 additions & 0 deletions src/config/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* 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/. */

import config from 'commerce/config';

/**
* Listener for messages from content scripts to the background page to fetch
* config values.
* @module
*/

export async function handleConfigMessage(message) {
return config.get(message.name);
}
17 changes: 17 additions & 0 deletions src/config/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* 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/. */

/**
* Communication from content scripts to the background page for fetching config
* values.
* @module
*/

export const CONFIG_MESSAGE_TYPE = 'config';

export default {
async get(configName) {
return browser.runtime.sendMessage({type: CONFIG_MESSAGE_TYPE, name: configName});
},
};
29 changes: 21 additions & 8 deletions src/product_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* "document_idle", which is after all DOM content has been loaded.
*/

import config from 'commerce/config/content';
import extractProductWithFathom from 'commerce/extraction/fathom_extraction';
import extractProductWithFallback from 'commerce/extraction/fallback_extraction';

Expand All @@ -30,17 +31,29 @@ async function getProductInfo() {
});
}

(function main() {
(async function main() {
// If we're in an iframe, don't bother extracting a product EXCEPT if we were
// started by the background script for a price check.
const isInIframe = window !== window.top;
const isBackgroundUpdate = window.location.hash === '#moz-commerce-background';
if (!isInIframe || isBackgroundUpdate) {
// Make sure the page has finished loading, as JS could alter the DOM.
if (document.readyState === 'complete') {
getProductInfo();
} else {
window.addEventListener('load', getProductInfo);
}
if (isInIframe && !isBackgroundUpdate) {
return;
}

// Only perform extraction on allowlisted sites. Background updates get a
// pass; we don't want to accidentally freeze updates for products that are
// being tracked no matter what.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this -- if users can only track products on allowlisted sites in the first place, then how would a background update ever load a page that wasn't an allowlisted site?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they tracked a product on a webpage that was previously allowed, but is no longer allowed (whether due to an update, testing the demo add-on, or something else). It's a bit of an edge case but could happen between add-on updates.

const url = new URL(document.location.href);
const allowList = await config.get('extractionAllowlist');
const allowAll = allowList.length === 1 && allowList[0] === '*';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you're building this in for post-MVP where we remove the 5 site limit? Why wouldn't we just remove the allowList check and list from config if we were opening up extraction to all sites?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more for non-developers who want to test the add-on out on many sites without hardcoding them into the pref and can't make their own build with the config changed.

if (!allowAll && !isBackgroundUpdate && !allowList.includes(url.host)) {
return;
}

// Make sure the page has finished loading, as JS could alter the DOM.
if (document.readyState === 'complete') {
getProductInfo();
} else {
window.addEventListener('load', getProductInfo);
}
}());