Skip to content
This repository has been archived by the owner on Feb 25, 2023. It is now read-only.

Refactor FrameOffsetForwarder #1353

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
1 change: 1 addition & 0 deletions dev/data/manifest-variants.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"fg/js/text-source-range.js",
"fg/js/text-source-element.js",
"fg/js/popup-factory.js",
"fg/js/frame-ancestry-handler.js",
"fg/js/frame-offset-forwarder.js",
"fg/js/popup-proxy.js",
"fg/js/popup-window.js",
Expand Down
1 change: 1 addition & 0 deletions ext/bg/popup-preview.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<script src="/fg/js/text-source-element.js"></script>
<script src="/fg/js/popup-factory.js"></script>
<script src="/fg/js/frontend.js"></script>
<script src="/fg/js/frame-ancestry-handler.js"></script>
<script src="/fg/js/frame-offset-forwarder.js"></script>
<script src="/bg/js/settings/popup-preview-frame.js"></script>

Expand Down
76 changes: 75 additions & 1 deletion ext/fg/js/frame-ancestry-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* This class is used to return the ancestor frame IDs for the current frame.
* This is a workaround to using the `webNavigation.getAllFrames` API, which
* would require an additional permission that is otherwise unnecessary.
* It is also used to track the correlation between child frame elements and their IDs.
*/
class FrameAncestryHandler {
/**
Expand Down Expand Up @@ -54,6 +55,14 @@ class FrameAncestryHandler {
this._isPrepared = true;
}

/**
* Returns whether or not this frame is the root frame in the tab.
* @returns `true` if it is the root, otherwise `false`.
*/
isRootFrame() {
return (window === window.parent);
}

/**
* Gets the frame ancestry information for the current frame. If the frame is the
* root frame, an empty array is returned. Otherwise, an array of frame IDs is returned,
Expand All @@ -68,6 +77,26 @@ class FrameAncestryHandler {
return await this._getFrameAncestryInfoPromise;
}

/**
* Gets the frame element of a child frame given a frame ID.
* For this function to work, the `getFrameAncestryInfo` function needs to have
* been invoked previously.
* @param frameId The frame ID of the child frame to get.
* @returns The element corresponding to the frame with ID `frameId`, otherwise `null`.
*/
getChildFrameElement(frameId) {
const frameInfo = this._childFrameMap.get(frameId);
if (typeof frameInfo === 'undefined') { return null; }

let {frameElement} = frameInfo;
if (typeof frameElement === 'undefined') {
frameElement = this._findFrameElementWithContentWindow(frameInfo.window);
frameInfo.frameElement = frameElement;
}

return frameElement;
}

// Private

_getFrameAncestryInfo(timeout=5000) {
Expand Down Expand Up @@ -166,7 +195,7 @@ class FrameAncestryHandler {
}

if (!this._childFrameMap.has(childFrameId)) {
this._childFrameMap.set(childFrameId, {window: source});
this._childFrameMap.set(childFrameId, {window: source, frameElement: void 0});
}

if (more) {
Expand All @@ -192,4 +221,49 @@ class FrameAncestryHandler {
Math.floor(value) === value
);
}

_findFrameElementWithContentWindow(contentWindow) {
// Check frameElement, for non-null same-origin frames
try {
const {frameElement} = contentWindow;
if (frameElement !== null) { return frameElement; }
} catch (e) {
// NOP
}

// Check frames
const frameTypes = ['iframe', 'frame', 'embed'];
for (const frameType of frameTypes) {
for (const frame of document.getElementsByTagName(frameType)) {
if (frame.contentWindow === contentWindow) {
return frame;
}
}
}

// Check for shadow roots
const rootElements = [document.documentElement];
while (rootElements.length > 0) {
const rootElement = rootElements.shift();
const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const element = walker.currentNode;

if (element.contentWindow === contentWindow) {
return element;
}

const shadowRoot = (
element.shadowRoot ||
element.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions
);
if (shadowRoot) {
rootElements.push(shadowRoot);
}
}
}

// Not found
return null;
}
}
159 changes: 26 additions & 133 deletions ext/fg/js/frame-offset-forwarder.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,162 +16,55 @@
*/

/* global
* FrameAncestryHandler
* api
*/

class FrameOffsetForwarder {
constructor(frameId) {
this._frameId = frameId;
this._isPrepared = false;
this._cacheMaxSize = 1000;
this._frameCache = new Set();
this._unreachableContentWindowCache = new Set();
this._windowMessageHandlers = new Map([
['getFrameOffset', this._onMessageGetFrameOffset.bind(this)]
]);
this._frameAncestryHandler = new FrameAncestryHandler(frameId);
}

prepare() {
if (this._isPrepared) { return; }
window.addEventListener('message', this._onMessage.bind(this), false);
this._isPrepared = true;
this._frameAncestryHandler.prepare();
api.crossFrame.registerHandlers([
['FrameOffsetForwarder.getChildFrameRect', {async: false, handler: this._onMessageGetChildFrameRect.bind(this)}]
]);
}

async getOffset() {
if (window === window.parent) {
if (this._frameAncestryHandler.isRootFrame()) {
return [0, 0];
}

const uniqueId = generateId(16);

const frameOffsetPromise = yomichan.getTemporaryListenerResult(
chrome.runtime.onMessage,
({action, params}, {resolve}) => {
if (action === 'frameOffset' && isObject(params) && params.uniqueId === uniqueId) {
resolve(params);
}
},
5000
);

this._getFrameOffsetParent([0, 0], uniqueId, this._frameId);

const {offset} = await frameOffsetPromise;
return offset;
}

// Private

_onMessage(event) {
const data = event.data;
if (data === null || typeof data !== 'object') { return; }

try {
const {action, params} = event.data;
const handler = this._windowMessageHandlers.get(action);
if (typeof handler !== 'function') { return; }
handler(params, event);
} catch (e) {
// NOP
}
}

_onMessageGetFrameOffset({offset, uniqueId, frameId}, e) {
let sourceFrame = null;
if (!this._unreachableContentWindowCache.has(e.source)) {
sourceFrame = this._findFrameWithContentWindow(e.source);
}
if (sourceFrame === null) {
// closed shadow root etc.
this._addToCache(this._unreachableContentWindowCache, e.source);
this._replyFrameOffset(null, uniqueId, frameId);
return;
}

const [forwardedX, forwardedY] = offset;
const {x, y} = sourceFrame.getBoundingClientRect();
offset = [forwardedX + x, forwardedY + y];
const ancestorFrameIds = await this._frameAncestryHandler.getFrameAncestryInfo();

if (window === window.parent) {
this._replyFrameOffset(offset, uniqueId, frameId);
} else {
this._getFrameOffsetParent(offset, uniqueId, frameId);
let childFrameId = this._frameId;
const promises = [];
for (const frameId of ancestorFrameIds) {
promises.push(api.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId}));
childFrameId = frameId;
}
}

_findFrameWithContentWindow(contentWindow) {
const ELEMENT_NODE = Node.ELEMENT_NODE;
for (const elements of this._getFrameElementSources()) {
while (elements.length > 0) {
const element = elements.shift();
if (element.contentWindow === contentWindow) {
this._addToCache(this._frameCache, element);
return element;
}
const results = await Promise.all(promises);

const shadowRoot = (
element.shadowRoot ||
element.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions
);
if (shadowRoot) {
for (const child of shadowRoot.children) {
if (child.nodeType === ELEMENT_NODE) {
elements.push(child);
}
}
}

for (const child of element.children) {
if (child.nodeType === ELEMENT_NODE) {
elements.push(child);
}
}
}
let xOffset = 0;
let yOffset = 0;
for (const {x, y} of results) {
xOffset += x;
yOffset += y;
}

return null;
return [xOffset, yOffset];
}

*_getFrameElementSources() {
const frameCache = [];
for (const frame of this._frameCache) {
// removed from DOM
if (!frame.isConnected) {
this._frameCache.delete(frame);
continue;
}
frameCache.push(frame);
}
yield frameCache;
// will contain duplicates, but frame elements are cheap to handle
yield [...document.querySelectorAll('frame,iframe')];
yield [document.documentElement];
}

_addToCache(cache, value) {
let freeSlots = this._cacheMaxSize - cache.size;
if (freeSlots <= 0) {
for (const cachedValue of cache) {
cache.delete(cachedValue);
++freeSlots;
if (freeSlots > 0) { break; }
}
}
cache.add(value);
}
// Private

_getFrameOffsetParent(offset, uniqueId, frameId) {
window.parent.postMessage({
action: 'getFrameOffset',
params: {
offset,
uniqueId,
frameId
}
}, '*');
}
_onMessageGetChildFrameRect({frameId}) {
const frameElement = this._frameAncestryHandler.getChildFrameElement(frameId);
if (frameElement === null) { return null; }

_replyFrameOffset(offset, uniqueId, frameId) {
api.sendMessageToFrame(frameId, 'frameOffset', {offset, uniqueId});
const {x, y, width, height} = frameElement.getBoundingClientRect();
return {x, y, width, height};
}
}
9 changes: 7 additions & 2 deletions ext/fg/js/popup-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class PopupProxy extends EventDispatcher {
this._ownerFrameId = ownerFrameId;
this._frameOffsetForwarder = frameOffsetForwarder;

this._frameOffset = null;
this._frameOffset = [0, 0];
this._frameOffsetPromise = null;
this._frameOffsetUpdatedAt = null;
this._frameOffsetExpireTimeout = 1000;
Expand Down Expand Up @@ -194,7 +194,12 @@ class PopupProxy extends EventDispatcher {
async _updateFrameOffsetInner(now) {
this._frameOffsetPromise = this._frameOffsetForwarder.getOffset();
try {
const offset = await this._frameOffsetPromise;
let offset = null;
try {
offset = await this._frameOffsetPromise;
} catch (e) {
// NOP
}
this._frameOffset = offset !== null ? offset : [0, 0];
if (offset === null) {
this.trigger('offsetNotFound');
Expand Down
1 change: 1 addition & 0 deletions ext/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"fg/js/text-source-range.js",
"fg/js/text-source-element.js",
"fg/js/popup-factory.js",
"fg/js/frame-ancestry-handler.js",
"fg/js/frame-offset-forwarder.js",
"fg/js/popup-proxy.js",
"fg/js/popup-window.js",
Expand Down
1 change: 1 addition & 0 deletions ext/mixed/js/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,7 @@ class Display extends EventDispatcher {
'/fg/js/popup-proxy.js',
'/fg/js/popup-window.js',
'/fg/js/popup-factory.js',
'/fg/js/frame-ancestry-handler.js',
'/fg/js/frame-offset-forwarder.js',
'/fg/js/frontend.js'
]);
Expand Down