Skip to content

[ContentFeature] de-decouple build specific code from content-features #1545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 13, 2025
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
3 changes: 0 additions & 3 deletions injected/entry-points/android.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*/
import { load, init } from '../src/content-scope-features.js';
import { processConfig, isGloballyDisabled } from './../src/utils';
import { isTrackerOrigin } from '../src/trackers';
import { AndroidMessagingConfig } from '../../messaging/index.js';

function initCode() {
Expand Down Expand Up @@ -33,8 +32,6 @@ function initCode() {

load({
platform: processedConfig.platform,
trackerLookup: processedConfig.trackerLookup,
documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup),
site: processedConfig.site,
bundledConfig: processedConfig.bundledConfig,
messagingConfig: processedConfig.messagingConfig,
Expand Down
3 changes: 0 additions & 3 deletions injected/entry-points/apple.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*/
import { load, init } from '../src/content-scope-features.js';
import { processConfig, isGloballyDisabled, platformSpecificFeatures } from './../src/utils';
import { isTrackerOrigin } from '../src/trackers';
import { WebkitMessagingConfig, TestTransportConfig } from '../../messaging/index.js';

function initCode() {
Expand Down Expand Up @@ -44,8 +43,6 @@ function initCode() {

load({
platform: processedConfig.platform,
trackerLookup: processedConfig.trackerLookup,
documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup),
site: processedConfig.site,
bundledConfig: processedConfig.bundledConfig,
messagingConfig: processedConfig.messagingConfig,
Expand Down
5 changes: 0 additions & 5 deletions injected/entry-points/chrome.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* @module Chrome integration
*/
import { isTrackerOrigin } from '../src/trackers';
import { computeLimitedSiteObject } from '../src/utils';

/**
Expand Down Expand Up @@ -39,8 +38,6 @@ function randomString() {
}

function init() {
const trackerLookup = import.meta.trackerLookup;
const documentOriginIsTracker = isTrackerOrigin(trackerLookup);
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
const bundledConfig = $BUNDLED_CONFIG$;
const randomMethodName = '_d' + randomString();
Expand All @@ -54,9 +51,7 @@ function init() {
platform: {
name: 'extension'
},
trackerLookup: ${JSON.stringify(trackerLookup)},
site: ${JSON.stringify(siteObject)},
documentOriginIsTracker: ${documentOriginIsTracker},
bundledConfig: ${JSON.stringify(bundledConfig)}
})
// Define a random function we call later.
Expand Down
5 changes: 0 additions & 5 deletions injected/entry-points/extension-mv3.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@
* @module main world integration for Chrome MV3 and Firefox (enhanced) MV2
*/
import { load, init, update } from '../src/content-scope-features.js';
import { isTrackerOrigin } from '../src/trackers.js';
import { computeLimitedSiteObject } from '../src/utils.js';

const secret = (crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32).toString().replace('0.', '');

const trackerLookup = import.meta.trackerLookup;

load({
platform: {
name: 'extension',
},
trackerLookup,
documentOriginIsTracker: isTrackerOrigin(trackerLookup),
site: computeLimitedSiteObject(),
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
bundledConfig: $BUNDLED_CONFIG$,
Expand Down
7 changes: 2 additions & 5 deletions injected/entry-points/integration.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { load, init } from '../src/content-scope-features.js';
import { isTrackerOrigin } from '../src/trackers';
import { TestTransportConfig } from '../../messaging/index.js';
function getTopLevelURL() {
try {
Expand All @@ -17,7 +16,6 @@ function getTopLevelURL() {

function generateConfig() {
const topLevelUrl = getTopLevelURL();
const trackerLookup = import.meta.trackerLookup;
return {
debug: false,
sessionKey: 'randomVal',
Expand All @@ -30,7 +28,6 @@ function generateConfig() {
allowlisted: false,
enabledFeatures: ['fingerprintingCanvas', 'fingerprintingScreenSize', 'navigatorInterface', 'cookie'],
},
trackerLookup,
};
}

Expand Down Expand Up @@ -84,12 +81,12 @@ async function initCode() {
};
},
});

load({
// @ts-expect-error Types of property 'name' are incompatible.
platform: processedConfig.platform,
trackerLookup: processedConfig.trackerLookup,
documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup),
site: processedConfig.site,
bundledConfig: processedConfig.bundledConfig,
messagingConfig: processedConfig.messagingConfig,
});

Expand Down
3 changes: 0 additions & 3 deletions injected/entry-points/windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*/
import { load, init } from '../src/content-scope-features.js';
import { processConfig, isGloballyDisabled, platformSpecificFeatures } from './../src/utils';
import { isTrackerOrigin } from '../src/trackers';
import { WindowsMessagingConfig } from '../../messaging/index.js';

function initCode() {
Expand Down Expand Up @@ -31,8 +30,6 @@ function initCode() {

load({
platform: processedConfig.platform,
trackerLookup: processedConfig.trackerLookup,
documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup),
site: processedConfig.site,
bundledConfig: processedConfig.bundledConfig,
messagingConfig: processedConfig.messagingConfig,
Expand Down
159 changes: 159 additions & 0 deletions injected/src/config-feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { immutableJSONPatch } from 'immutable-json-patch';
import { camelcase, computeEnabledFeatures, matchHostname, parseFeatureSettings } from './utils.js';

export default class ConfigFeature {
/** @type {import('./utils.js').RemoteConfig | undefined} */
#bundledConfig;

/** @type {string} */
name;

/** @type {{ debug?: boolean, desktopModeEnabled?: boolean, forcedZoomEnabled?: boolean, featureSettings?: Record<string, unknown>, assets?: import('./content-feature.js').AssetConfig | undefined, site: import('./content-feature.js').Site, messagingConfig?: import('@duckduckgo/messaging').MessagingConfig } | null} */
#args;

/**
* @param {string} name
* @param {import('./content-scope-features.js').LoadArgs} args
*/
constructor(name, args) {
this.name = name;
const { bundledConfig, site, platform } = args;
this.#bundledConfig = bundledConfig;
this.#args = args;
// If we have a bundled config, treat it as a regular config
// This will be overriden by the remote config if it is available
if (this.#bundledConfig && this.#args) {
const enabledFeatures = computeEnabledFeatures(bundledConfig, site.domain, platform.version);
this.#args.featureSettings = parseFeatureSettings(bundledConfig, enabledFeatures);
}
}

get args() {
return this.#args;
}

set args(args) {
this.#args = args;
}

get featureSettings() {
return this.#args?.featureSettings;
}

/**
* Given a config key, interpret the value as a list of domain overrides, and return the elements that match the current page
* Consider using patchSettings instead as per `getFeatureSetting`.
* @param {string} featureKeyName
* @return {any[]}
* @protected
*/
matchDomainFeatureSetting(featureKeyName) {
const domain = this.args?.site.domain;
if (!domain) return [];
const domains = this._getFeatureSettings()?.[featureKeyName] || [];
return domains.filter((rule) => {
if (Array.isArray(rule.domain)) {
return rule.domain.some((domainRule) => {
return matchHostname(domain, domainRule);
});
}
return matchHostname(domain, rule.domain);
});
}

/**
* Return the settings object for a feature
* @param {string} [featureName] - The name of the feature to get the settings for; defaults to the name of the feature
* @returns {any}
*/
_getFeatureSettings(featureName) {
const camelFeatureName = featureName || camelcase(this.name);
return this.featureSettings?.[camelFeatureName];
}

/**
* For simple boolean settings, return true if the setting is 'enabled'
* For objects, verify the 'state' field is 'enabled'.
* This allows for future forwards compatibility with more complex settings if required.
* For example:
* ```json
* {
* "toggle": "enabled"
* }
* ```
* Could become later (without breaking changes):
* ```json
* {
* "toggle": {
* "state": "enabled",
* "someOtherKey": 1
* }
* }
* ```
* This also supports domain overrides as per `getFeatureSetting`.
* @param {string} featureKeyName
* @param {string} [featureName]
* @returns {boolean}
*/
getFeatureSettingEnabled(featureKeyName, featureName) {
const result = this.getFeatureSetting(featureKeyName, featureName);
if (typeof result === 'object') {
return result.state === 'enabled';
}
return result === 'enabled';
}

/**
* Return a specific setting from the feature settings
* If the "settings" key within the config has a "domains" key, it will be used to override the settings.
* This uses JSONPatch to apply the patches to settings before getting the setting value.
* For example.com getFeatureSettings('val') will return 1:
* ```json
* {
* "settings": {
* "domains": [
* {
* "domain": "example.com",
* "patchSettings": [
* { "op": "replace", "path": "/val", "value": 1 }
* ]
* }
* ]
* }
* }
* ```
* "domain" can either be a string or an array of strings.

* For boolean states you should consider using getFeatureSettingEnabled.
* @param {string} featureKeyName
* @param {string} [featureName]
* @returns {any}
*/
getFeatureSetting(featureKeyName, featureName) {
let result = this._getFeatureSettings(featureName);
if (featureKeyName === 'domains') {
throw new Error('domains is a reserved feature setting key name');
}
const domainMatch = [...this.matchDomainFeatureSetting('domains')].sort((a, b) => {
return a.domain.length - b.domain.length;
});
for (const match of domainMatch) {
if (match.patchSettings === undefined) {
continue;
}
try {
result = immutableJSONPatch(result, match.patchSettings);
} catch (e) {
console.error('Error applying patch settings', e);
}
}
return result?.[featureKeyName];
}

/**
* @returns {import('./utils.js').RemoteConfig | undefined}
**/
get bundledConfig() {
return this.#bundledConfig;
}
}
Loading
Loading