-
Notifications
You must be signed in to change notification settings - Fork 214
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
fix: Added missing chrome privacy api settings #205
Conversation
66869a3
to
b082082
Compare
src/browser-polyfill.js
Outdated
}, | ||
}; | ||
// Wrap all "network", "services", "websites" APIs in chrome.privacy.* | ||
if (extensionAPIs.privacy) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This dereferences the privacy
namespace even when not used.
Could you make this a lazy getter? Maybe a getter for each of network, services and websites.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, will update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, I have created separated getters for each of the privacyTypes
as you have asked, but I feel like that implementation below is simpler:
// Wrap all "network", "services", "websites" APIs in chrome.privacy.*
const privacy = {
get supported() {
return extensionAPIs.privacy;
},
};
if (privacy.supported) {
apiMetadata.privacy = {};
for (const privacyType of ["network", "services", "websites"]) {
apiMetadata.privacy[privacyType] = {};
for (const privacyName of Object.keys(extensionAPIs.privacy[privacyType])) {
apiMetadata.privacy[privacyType][privacyName] = settingMetadata;
}
}
}
What you think?
src/browser-polyfill.js
Outdated
apiMetadata.privacy = {}; | ||
for (const privacyType of ["network", "services", "websites"]) { | ||
apiMetadata.privacy[privacyType] = {}; | ||
for (const privacyName in extensionAPIs.privacy[privacyType]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would also include enumerable properties from Object.prototype
, which shouldn't be exported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, will update.
Guess using Object.keys()
might be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@Rob--W I have addressed the comments, but seems like there is some problems with ChromeDriver Most probably I won't be available tomorrow, but will ensure to address anything needed the day after. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Rob--W I have addressed the comments, but seems like there is some problems with ChromeDriver
SessionNotCreatedError: session not created: This version of ChromeDriver only supports Chrome version 76
.Most probably I won't be available tomorrow, but will ensure to address anything needed the day after.
We should probably bump the chromedriver version again... As done in #199. If you open a pull request in the same format, I'd approve and merge it.
src/browser-polyfill.js
Outdated
return {}; | ||
}}); | ||
} | ||
if (privacy.supported) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a lazy getter.
As soon as you dereference the extensionAPIs.privacy
object, the browser has to initialize the extension bindings for chrome.privacy
object.
A lazy getter is one where you avoid dereferencing the extensionAPIs.privacy
object until absolutely necessary.
For example, something like this:
apiMetadata.privacy = {
get network() { // <-- This is a getter. You should probably use Object.defineProperty instead.
let target = extensionAPIs.privacy && extensionAPIs.privacy.network;
if (!target) {
let network = {};
for (let k of Object.keys(target)) {
network[k] = settingMetadata;
}
this.network = network; // <-- This caches the result, hence the "lazy getter" is run only once.
return network;
}
},
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I feel so stupid, thanks will update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Rob--W Updated, hope now it looks better.
Done -> #208 |
src/browser-polyfill.js
Outdated
|
||
apiMetadata.privacy = {}; | ||
for (const type of ["network", "services", "websites"]) { | ||
Object.defineProperty(apiMetadata.privacy, type, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add configurable: true,
to the properties of the parameter to Object.defineProperty
, to ensure that the property can be replaced later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
src/browser-polyfill.js
Outdated
return privacyType; | ||
} | ||
}, | ||
set: (value) => value, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only used internally; no setter necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I don't use setters, getters only error is thrown when caching the value as the strict mode
is used.
Not sure why this is not being caught by tests.
Steps to reproduce:
npm run build
.- Load
dist/browser-polyfill.js
into a chrome extension. - execute
browser.privacy.services.passwordSavingEnabled.get({});
Observer behavior:
Uncaught TypeError: Cannot set property services of #<Object> which has only a getter
Expected:
No error is thrown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because you're triggering the setter (at line 505).
There are two ways to replace a getter property:
delete
the original property and assign the value.- Use Object.defineProperty` to replace the property descriptor with one that only has a value.
In a unit test, you should count the number of accesses to chrome.privacy.network
when you dereference the chrome.privacy.network
object.
After looking at the implementation, it is apparent that the metadata is dereferenced only once and cached afterwards. I guess that you don't even have to replace the getter, so the code can be simplified.
implementation where metadata is dereferenced once:
webextension-polyfill/src/browser-polyfill.js
Lines 256 to 261 in 5f7a845
(hasOwnProperty(wrappers, prop) || | |
hasOwnProperty(metadata, prop))) { | |
// This is an object that we need to do some wrapping for the children | |
// of. Create a sub-object wrapper for it with the appropriate child | |
// metadata. | |
value = wrapObject(value, wrappers[prop], metadata[prop]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a unit test, you should count the number of accesses to chrome.privacy.network when you dereference the chrome.privacy.network object.
I don't understand this comment, you want me to update the tests in a way?
After looking at the implementation, it is apparent that the metadata is dereferenced only once and cached afterwards. I guess that you don't even have to replace the getter, so the code can be simplified
Sounds good, then I guess I can just remove the line where I overwrite the getter and that's it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After looking at the implementation, it is apparent that the metadata is dereferenced only once and cached afterwards. I guess that you don't even have to replace the getter, so the code can be simplified
Done, removed the cache. Hope I understood your comment correctly.
In a unit test, you should count the number of accesses to chrome.privacy.network when you dereference the chrome.privacy.network object.
I need a hint here, not sure how I can implement that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is an existing test that verifies that a property is accessed only once:
webextension-polyfill/test/test-proxied-properties.js
Lines 140 to 183 in ad207a8
describe("without side effects", () => { | |
it("should proxy non-wrapped methods", () => { | |
let lazyInitCount = 0; | |
const fakeChrome = { | |
get runtime() { | |
// Chrome lazily initializes API objects by replacing the getter with | |
// the value. The initialization is only allowed to occur once, | |
// after that `undefined` is returned and a warning is printed. | |
// https://chromium.googlesource.com/chromium/src/+/4d6b3a067994ce6dcf0ed9a9efd566c083736952/extensions/renderer/module_system.cc#414 | |
// | |
// The polyfill should invoke the getter only once (on the global chrome object). | |
++lazyInitCount; | |
const onMessage = { | |
addListener(listener) { | |
equal(this, onMessage, "onMessage.addListener should be called on the original chrome.onMessage object"); | |
}, | |
}; | |
const value = {onMessage}; | |
Object.defineProperty(this, "runtime", {value}); | |
return value; | |
}, | |
get tabs() { | |
ok(false, "chrome.tabs should not lazily be initialized without explicit API call"); | |
}, | |
}; | |
return setupTestDOMWindow(fakeChrome).then(window => { | |
// This used to be equal(lazyInitCount, 0, ...), but was changed to | |
// accomodate a change in the implementation of the polyfill. | |
// To verify that APIs are not unnecessarily initialized, the fakeChrome | |
// object has a "tabs" getter that fails the test upon access. | |
equal(lazyInitCount, 1, "chrome.runtime should be initialized because chrome.runtime.id is accessed during polyfill initialization"); | |
window.browser.runtime.onMessage.addListener(() => {}); | |
equal(lazyInitCount, 1, "chrome.runtime should be initialized upon accessing browser.runtime"); | |
window.browser.runtime.onMessage.addListener(() => {}); | |
equal(lazyInitCount, 1, "chrome.runtime should be re-used upon accessing browser.runtime"); | |
window.chrome.runtime.onMessage.addListener(() => {}); | |
equal(lazyInitCount, 1, "chrome.runtime should be re-used upon accessing chrome.runtime"); | |
}); | |
}); | |
}); |
You would have to add a new test that verifies that fakeChrome.privacy.network
is accessed only once, even if the test gets window.browser.privacy.network.somePropertyName
twice.
It would probably also be good to verify that if fakeChrome.privacy.services
is undefined, that window.privacy.network.services
is undefined as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Rob--W I have added a test, looking into existing implementation you provided, but I'm bit unsure about it.
It would probably also be good to verify that if fakeChrome.privacy.services is undefined, that window.privacy.network.services is undefined as well
Not sure if I understood this one, are you referring to something like?
equal(window.browser.privacy.services, undefined);
… has only a getter
e735f85
to
8277c03
Compare
test/test-proxied-properties.js
Outdated
@@ -178,6 +189,12 @@ describe("browser-polyfill", () => { | |||
|
|||
window.chrome.runtime.onMessage.addListener(() => {}); | |||
equal(lazyInitCount, 1, "chrome.runtime should be re-used upon accessing chrome.runtime"); | |||
|
|||
window.browser.privacy.network.networkPredictionEnabled(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't call the function, just check that the returned object looks like a Setting object (object with set, get and clear methods).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
test/test-proxied-properties.js
Outdated
|
||
const networkPredictionEnabled = () => true; | ||
const value = {networkPredictionEnabled}; | ||
Object.defineProperty(fakeChrome.privacy, "network", {value}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't replace the property. Now we're interested in how often privacy.network
is dereferenced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted, done.
It fails, but I can confirm it's not a regression as it was calling twice for the old implementation as well.
Any idea why it's so? Might be a problem with my test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It fails, but I can confirm it's not a regression as it was calling twice for the old implementation as well.
This was wrong assumption, apparently I was running test without running npm run build
first, Sorry for this, will have a look into it later today, the old tests were passing correctly, the problem is in the new implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After taking a closer look into implementation, now I see what is the problem.
The problem is that I can't fix it without touching wrapAPIs()
code, unless I hardcode everything.
I'm thinking about something like:
const settingMetadata = {
clear: {minArgs: 1, maxArgs: 1},
get: {minArgs: 1, maxArgs: 1},
set: {minArgs: 1, maxArgs: 1},
};
apiMetadata.privacy = {"*": {"*": settingMetadata}};
and updating wrapObject()
:
if (hasOwnProperty(metadata, "*")) {
value = wrapObject(value, wrappers[prop], metadata["*"]);
}
I'm quite uncomfortable updating wrapObject()
, but I'll give it a shot and revert it if you say so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Rob--W Done.
test/test-proxied-properties.js
Outdated
@@ -140,6 +140,7 @@ describe("browser-polyfill", () => { | |||
describe("without side effects", () => { | |||
it("should proxy non-wrapped methods", () => { | |||
let lazyInitCount = 0; | |||
let lazyInitPrivacyCount = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you create a new describe(...)
block to test the privacy API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/browser-polyfill.js
Outdated
referrersEnabled: settingMetadata, | ||
}, | ||
}; | ||
apiMetadata.privacy = {"*": {"*": settingMetadata}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not immediately against this implementation, since it is still somewhat readable and concise.
I do wonder whether we should explicitly spell out network
, services
, and websites
.
@rpl Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 from me on spelling out the first level and use the "*" to cover all the properties inside those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @rpl and @Rob--W, done.
I was thinking about 2 different implementation:
const privacyType = {"*": settingMetadata};
apiMetadata.privacy = {
network: privacyType,
services: privacyType,
websites: privacyType,
};
apiMetadata.privacy = {};
for (const type of ["network", "services", "websites"]) {
apiMetadata.privacy[type] = {"*": settingMetadata};
}
If you have preference for one or another please let me know which to use. This seem to be most consistent comparing to settingMetadata
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the current code. It is more readable than the proposed alternatives.
}, | ||
}; | ||
|
||
return setupTestDOMWindow(fakeChrome).then(window => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
equal(lazyInitCount, 0, ...);
to check that chrome.privacy
is not accessed at first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@Rob--W Is there anything blocking this issue to be merged into master? Thanks in advance. |
Thanks for the patch @Manvel ! |
This PR is for fixing #204