-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add API manipulation feature (#1207)
* Change value modification to use get instead * Rename keys based on tech design discussion * Add docs, change defaults of enumerable and configurable * Add typing info * Add simple test cases and for APIs we care about * Change default to rely on wrapProperty code defaults * Change to not passing props if not defined * Add removal task notice * Remove integration config example * Add hasOwnProperty check to remove * Move to using captured global
- Loading branch information
1 parent
b46d850
commit 07d9f81
Showing
10 changed files
with
360 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
injected/integration-test/test-pages/api-manipulation/config/apis.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
{ | ||
"features": { | ||
"apiManipulation": { | ||
"state": "enabled", | ||
"settings": { | ||
"apiChanges": { | ||
"Navigator.prototype.hardwareConcurrency": { | ||
"type": "descriptor", | ||
"getterValue": { | ||
"type": "number", | ||
"value": 222 | ||
} | ||
}, | ||
"Navigator.prototype.userAgent": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.thisDoesNotExist": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.newAPI": { | ||
"type": "descriptor", | ||
"getterValue": { | ||
"type": "number", | ||
"value": 222 | ||
} | ||
}, | ||
"window.name": { | ||
"type": "descriptor", | ||
"getterValue": { | ||
"type": "string", | ||
"value": "newName" | ||
} | ||
}, | ||
"Navigator.prototype.joinAdInterestGroup": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.leaveAdInterestGroup": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.clearOriginJoinedAdInterestGroups": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.updateAdInterestGroups": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.createAuctionNonce": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.runAdAuction": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.adAuctionComponents": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.deprecatedURNToURL": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.deprecatedReplaceInURN": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.getInterestGroupAdAuctionData": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.createAdRequest": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.finalizeAd": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.canLoadAdAuctionFencedFrame": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.deprecatedRunAdAuctionEnforcesKAnonymity": { | ||
"type": "remove" | ||
}, | ||
"Navigator.prototype.protectedAudience": { | ||
"type": "descriptor", | ||
"getterValue": { | ||
"type": "undefined" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
injected/integration-test/test-pages/api-manipulation/index.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width"> | ||
<title>API Interventions</title> | ||
<link rel="stylesheet" href="../shared/style.css"> | ||
</head> | ||
<body> | ||
<p><a href="../../index.html">[Home]</a></p> | ||
<ul> | ||
<li><a href="./pages/apis.html">Message Handlers</a> - <a href="./config/apis.json">Config</a></li> | ||
</ul> | ||
</body> | ||
</html> |
96 changes: 96 additions & 0 deletions
96
injected/integration-test/test-pages/api-manipulation/pages/apis.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width"> | ||
<title>Webcompat shims</title> | ||
<link rel="stylesheet" href="../../shared/style.css"> | ||
</head> | ||
<body> | ||
<script src="../../shared/utils.js"></script> | ||
<p><a href="../index.html">[Webcompat shims]</a></p> | ||
|
||
<p>This page verifies that APIs get modified</p> | ||
|
||
<script> | ||
test('API removal', async () => { | ||
return [ | ||
{ | ||
name: "APIs removal", | ||
result: navigator.userAgent, | ||
expected: undefined | ||
}, | ||
{ | ||
name: "New API definition deletion does nothing", | ||
result: navigator.thisDoesNotExist, | ||
expected: undefined | ||
}, | ||
]; | ||
}); | ||
|
||
test('Existing API modified', async () => { | ||
return [ | ||
{ | ||
name: "New API definition doesn't work", | ||
result: navigator.newAPI, | ||
expected: undefined | ||
}, | ||
{ | ||
name: "APIs modified", | ||
result: navigator.hardwareConcurrency, | ||
expected: 222 | ||
}, | ||
{ | ||
name: "Returns expected value", | ||
result: window.name, | ||
expected: "newName" | ||
}, | ||
{ | ||
name: "Defaults to configurable", | ||
result: Object.getOwnPropertyDescriptor(window, 'name').configurable, | ||
expected: true | ||
}, | ||
{ | ||
name: "Defaults to enumerable", | ||
result: Object.getOwnPropertyDescriptor(window, 'name').enumerable, | ||
expected: true | ||
} | ||
] | ||
}); | ||
|
||
|
||
test('Validate all expected APIs can be removed', async () => { | ||
// These APIs might not exist in all browsers however we should ensure they are removed after the code runs | ||
const result = [] | ||
const APIs = [ | ||
navigator.joinAdInterestGroup, | ||
navigator.leaveAdInterestGroup, | ||
navigator.clearOriginJoinedAdInterestGroups, | ||
navigator.updateAdInterestGroups, | ||
navigator.createAuctionNonce, | ||
navigator.runAdAuction, | ||
navigator.adAuctionComponents, | ||
navigator.deprecatedURNToURL, | ||
navigator.deprecatedReplaceInURN, | ||
navigator.getInterestGroupAdAuctionData, | ||
navigator.createAdRequest, | ||
navigator.finalizeAd, | ||
navigator.canLoadAdAuctionFencedFrame, | ||
navigator.deprecatedRunAdAuctionEnforcesKAnonymity, | ||
navigator.protectedAudience, | ||
] | ||
APIs.forEach(api => { | ||
result.push({ | ||
name: `API ${api} removed`, | ||
result: api, | ||
expected: undefined | ||
}); | ||
}); | ||
return result; | ||
}); | ||
|
||
// eslint-disable-next-line no-undef | ||
renderResults(); | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/** | ||
* This feature allows remote configuration of APIs that exist within the DOM. | ||
* We support removal of APIs and returning different values from getters. | ||
* | ||
* @module API manipulation | ||
*/ | ||
import ContentFeature from '../content-feature'; | ||
// eslint-disable-next-line no-redeclare | ||
import { hasOwnProperty } from '../captured-globals'; | ||
import { processAttr } from '../utils'; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export default class ApiManipulation extends ContentFeature { | ||
init() { | ||
const apiChanges = this.getFeatureSetting('apiChanges'); | ||
if (apiChanges) { | ||
for (const scope in apiChanges) { | ||
const change = apiChanges[scope]; | ||
if (!this.checkIsValidAPIChange(change)) { | ||
continue; | ||
} | ||
this.applyApiChange(scope, change); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Checks if the config API change is valid. | ||
* @param {any} change | ||
* @returns {change is APIChange} | ||
*/ | ||
checkIsValidAPIChange(change) { | ||
if (typeof change !== 'object') { | ||
return false; | ||
} | ||
if (change.type === 'remove') { | ||
return true; | ||
} | ||
if (change.type === 'descriptor') { | ||
if (change.enumerable && typeof change.enumerable !== 'boolean') { | ||
return false; | ||
} | ||
if (change.configurable && typeof change.configurable !== 'boolean') { | ||
return false; | ||
} | ||
return typeof change.getterValue !== 'undefined'; | ||
} | ||
return false; | ||
} | ||
|
||
// TODO move this to schema definition imported from the privacy-config | ||
// Additionally remove checkIsValidAPIChange when this change happens. | ||
// See: https://app.asana.com/0/1201614831475344/1208715421518231/f | ||
/** | ||
* @typedef {Object} APIChange | ||
* @property {"remove"|"descriptor"} type | ||
* @property {import('../utils.js').ConfigSetting} [getterValue] - The value returned from a getter. | ||
* @property {boolean} [enumerable] - Whether the property is enumerable. | ||
* @property {boolean} [configurable] - Whether the property is configurable. | ||
*/ | ||
|
||
/** | ||
* Applies a change to DOM APIs. | ||
* @param {string} scope | ||
* @param {APIChange} change | ||
* @returns {void} | ||
*/ | ||
applyApiChange(scope, change) { | ||
const response = this.getGlobalObject(scope); | ||
if (!response) { | ||
return; | ||
} | ||
const [obj, key] = response; | ||
if (change.type === 'remove') { | ||
this.removeApiMethod(obj, key); | ||
} else if (change.type === 'descriptor') { | ||
this.wrapApiDescriptor(obj, key, change); | ||
} | ||
} | ||
|
||
/** | ||
* Removes a method from an API. | ||
* @param {object} api | ||
* @param {string} key | ||
*/ | ||
removeApiMethod(api, key) { | ||
try { | ||
if (hasOwnProperty.call(api, key)) { | ||
delete api[key]; | ||
} | ||
} catch (e) {} | ||
} | ||
|
||
/** | ||
* Wraps a property with descriptor. | ||
* @param {object} api | ||
* @param {string} key | ||
* @param {APIChange} change | ||
*/ | ||
wrapApiDescriptor(api, key, change) { | ||
const getterValue = change.getterValue; | ||
if (getterValue) { | ||
const descriptor = { | ||
get: () => processAttr(getterValue, undefined), | ||
}; | ||
if ('enumerable' in change) { | ||
descriptor.enumerable = change.enumerable; | ||
} | ||
if ('configurable' in change) { | ||
descriptor.configurable = change.configurable; | ||
} | ||
this.wrapProperty(api, key, descriptor); | ||
} | ||
} | ||
|
||
/** | ||
* Looks up a global object from a scope, e.g. 'Navigator.prototype'. | ||
* @param {string} scope the scope of the object to get to. | ||
* @returns {[object, string]|null} the object at the scope. | ||
*/ | ||
getGlobalObject(scope) { | ||
const parts = scope.split('.'); | ||
// get the last part of the scope | ||
const lastPart = parts.pop(); | ||
if (!lastPart) { | ||
return null; | ||
} | ||
let obj = window; | ||
for (const part of parts) { | ||
obj = obj[part]; | ||
if (!obj) { | ||
return null; | ||
} | ||
} | ||
return [obj, lastPart]; | ||
} | ||
} |
Oops, something went wrong.