Closed Bug 1751346 Opened 4 years ago Closed 1 month ago

Support accessing ObservableArray attributes from XrayWrappers / extension content scripts

Categories

(Core :: DOM: Bindings (WebIDL), task)

task

Tracking

()

RESOLVED FIXED
153 Branch
Tracking Status
firefox153 --- fixed

People

(Reporter: edgar, Assigned: robwu)

References

(Blocks 1 open bug)

Details

(Keywords: dev-doc-complete, Whiteboard: [addons-jira])

Attachments

(2 files)

No description provided.
Blocks: 1766909

Hi, we're also having the issue mentioned on 1770592 at Walnut.

Basically, we're getting adoptedStyleSheets as Opaque and can't iterate over it.

Just adding this comment here as requested by @emilio on this Twitter thread.

Thanks again for the support!

Duplicate of this bug: 1827104
Blocks: 1817675

Edgar do you know how difficult this would be to implement? Any hints? It seems this causes some extensions to use some rather terrible workarounds like this, causing performance issues.

Flags: needinfo?(echen)
Blocks: 1928865

Talking with Edgar about this, unfortunately, we didn't have quick thoughts on the implementation or the complexity, and we were unable to allocate time to think about this thoroughly due to other higher priorities. I added this into the team backlog. Emilio/Rob, please let me know if my team should give this higher priority in our roadmap.

Flags: needinfo?(echen)
Summary: Support accessing ObservableArray attributes from XrayWrappers → Support accessing ObservableArray attributes from XrayWrappers / extension content scripts

Rob's workaround in #1827104 looks elegant but unfortunately now shadow.adoptedStyleSheets.push is not a function

Flags: needinfo?(rob)

(In reply to th3rdtim from comment #5)

Rob's workaround in #1827104 looks elegant but unfortunately now shadow.adoptedStyleSheets.push is not a function

I edited the example; .wrappedJSObject was missing, now I added it.

Flags: needinfo?(rob)

When I saw this issue at first, I thought that the array assigned to adoptedStyleSheets would somehow continue to be observed. That is not the case, as the following snippet demonstrates (in Firefox 150):

sheet = new CSSStyleSheet();
sheet.replaceSync("*{background:pink}");

a = document.adoptedStyleSheets;
b = [sheet];
document.adoptedStyleSheets = b;
console.log(document.adoptedStyleSheets === a); // still true, despite the "array" having been overwritten!
console.log(document.adoptedStyleSheets === b); // false
console.log(document.adoptedStyleSheets[0]=== b[0]); // true
document.adoptedStyleSheets = [];
console.log(document.adoptedStyleSheets === a); // still true, despite the "array" having been overwritten again!
console.log(a.__proto__); // null

Additionally, there is strict type validation:

document.adoptedStyleSheets = [null]
// Uncaught TypeError: Document.adoptedStyleSheets setter: Element of value being assigned is not an object.

document.adoptedStyleSheets = [{}]
// Uncaught TypeError: Document.adoptedStyleSheets setter: Element of value being assigned does not implement interface CSSStyleSheet.

document.adoptedStyleSheets=[Object.create(new CSSStyleSheet())]
// Uncaught TypeError: Document.adoptedStyleSheets setter: Element of value being assigned does not implement interface CSSStyleSheet.

These semantics should be feasible in Xrays.

Above, I added unit tests to verify the behavior of adoptedStyleSheets in extensions. The tests currently fail.
In the test I also added a minimal fixup to get mosts tests to work, by waiving the xrays (with the downside of page modifications to Array.prototype directly affecting the document.adoptedStyleSheets.push(sheet) call):

function patchASS(documentOrShadowRoot) {
  Object.defineProperty(documentOrShadowRoot, "adoptedStyleSheets", {
    configurable: true,
    get() {
      return this.wrappedJSObject.adoptedStyleSheets;
    },
    set(v) {
      v = globalThis.cloneInto(v, window, { wrapReflectors: true });
      this.wrappedJSObject.adoptedStyleSheets = v;
    },
  });
}
patchASS(document);
patchASS(ShadowRoot.prototype);

Now the next step is to figure out how to fix the implementation such that:

  • web pages and content script operate on the same view of adoptedStyleSheets
  • Array.prototype modifications from the web page do not affect the content script. That the following identities hold:
    • document.adoptedStyleSheets.wrappedJSObject.push === window.wrappedJSObject.Array.prototype.push
    • document.adoptedStyleSheets.push === Array.prototype.push

The entry point for the observable array getter is at GetObservableArrayBackingObject: https://searchfox.org/firefox-main/rev/c836d51da4179d7b456d02d22e2f40c8b1a9b3d7/dom/bindings/BindingUtils.cpp#3647-3653
I am aware that a linked bug's patch (https://phabricator.services.mozilla.com/D145045) improved the situation before, by returning an (opaque) wrapper instead of throwing outright.
The desired effect is for a XrayWrapper to be returned.

As for how that can be implemented? If only I knew...

I think that the correct solution is to update GetXrayType to recognize observable arrays at and then return a xray handler that does the right thing, at https://searchfox.org/firefox-main/rev/c836d51da4179d7b456d02d22e2f40c8b1a9b3d7/js/xpconnect/wrappers/XrayWrapper.cpp#126,146 :

  if (mozilla::dom::IsObservableArrayProxy(obj)) {
    return XrayForOpaqueObject; // TODO: This is the current behavior. Replace with the right one
  }

Optimistically I tried XrayForDOMObject and XrayForJSObject. That triggered assertion failures.
I also tried NotXray. And that seemed to work ?!! Can't be that easy, right?

EDIT: While it caused some tests to pass (notably the document.adoptedStyleSheets[0] = sheet case), it did not fix all of them.

Looks like the above gets all cases to work except for = [] assignment, due to the following error to be thrown:

Accessing from Xray wrapper is not supported.

from https://searchfox.org/firefox-main/rev/c836d51da4179d7b456d02d22e2f40c8b1a9b3d7/dom/bindings/Codegen.py#23595-23596

EDIT: The test_adoptedStyleSheets_protected_by_xrays test fails too, using NotXrays means that the page can interfere with the methods from the proxy, just like the JS-based work-around in comment 9.

See Also: → 1759053

To get rid of the Xray wrapper error from comment 11, we need to unwrap the xray and enter its realm. Coincidentally an unrelated refactor landed recently that did just that.

To get the observable array semantics to work without page's modifications affecting the content script, I managed to get XrayWrapper to behave as desired by treating observable arrays as a JS xray, along with a special case to assume the Array prototype (since adoptedStyleSheets does not have a prototype).

It is strange that on the web, document.adoptedStyleSheets.__proto__ is null despite it effectively inheriting from Array.prototype. In Chrome, its __proto__ is Array.prototype as expected, FWIW.

Assignee: nobody → rob
Whiteboard: [addons-jira]
Duplicate of this bug: 1770592
Duplicate of this bug: 1817675
Duplicate of this bug: 1928865
See Also: → 2036318

We usually don't document minor bugfixes in MDN, but given the impact on extensions, and the fact that the work-around (.wrappedJSObject) results in slightly less robust code (due to the ability for web pages to intercept API calls on objects after the use of .wrappedJSObject), I have put up a PR to document the fix in the 153 release notes on MDN:

https://github.com/mdn/content/pull/44274

Status: NEW → RESOLVED
Closed: 1 month ago
Resolution: --- → FIXED
Target Milestone: --- → 153 Branch
QA Whiteboard: [qa-triage-done-c154/b153]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: