Add FrameAncestryHandler (#1351)
This commit is contained in:
parent
8f97ca0aac
commit
356e7f5274
196
ext/fg/js/frame-ancestry-handler.js
Normal file
196
ext/fg/js/frame-ancestry-handler.js
Normal file
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global
|
||||
* api
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class FrameAncestryHandler {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param frameId The frame ID of the current frame the instance is instantiated in.
|
||||
*/
|
||||
constructor(frameId) {
|
||||
this._frameId = frameId;
|
||||
this._isPrepared = false;
|
||||
this._requestMessageId = 'FrameAncestryHandler.requestFrameInfo';
|
||||
this._responseMessageId = `${this._requestMessageId}.response`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the frame ID that the instance is instantiated in.
|
||||
*/
|
||||
get frameId() {
|
||||
return this._frameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes event event listening.
|
||||
*/
|
||||
prepare() {
|
||||
if (this._isPrepared) { return; }
|
||||
window.addEventListener('message', this._onWindowMessage.bind(this), false);
|
||||
this._isPrepared = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
* starting from the nearest ancestor.
|
||||
* @param timeout The maximum time to wait to receive a response to frame information requests.
|
||||
* @returns An array of frame IDs corresponding to the ancestors of the current frame.
|
||||
*/
|
||||
getFrameAncestryInfo(timeout=5000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const targetWindow = window.parent;
|
||||
if (window === targetWindow) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueId = generateId(16);
|
||||
const responseMessageId = this._responseMessageId;
|
||||
const results = [];
|
||||
let resultsExpectedCount = null;
|
||||
let resultsCount = 0;
|
||||
let timer = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (timer !== null) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
chrome.runtime.onMessage.removeListener(onMessage);
|
||||
};
|
||||
const onMessage = (message, sender, sendResponse) => {
|
||||
// Validate message
|
||||
if (
|
||||
typeof message !== 'object' ||
|
||||
message === null ||
|
||||
message.action !== responseMessageId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {params} = message;
|
||||
if (params.uniqueId !== uniqueId) { return; } // Wrong ID
|
||||
|
||||
const {frameId, index, more} = params;
|
||||
console.log({frameId, index, more});
|
||||
if (typeof results[index] !== 'undefined') { return; } // Invalid repeat
|
||||
|
||||
// Add result
|
||||
results[index] = frameId;
|
||||
++resultsCount;
|
||||
if (!more) {
|
||||
resultsExpectedCount = index + 1;
|
||||
}
|
||||
|
||||
if (resultsExpectedCount !== null && resultsCount >= resultsExpectedCount) {
|
||||
// Cleanup
|
||||
cleanup();
|
||||
sendResponse();
|
||||
|
||||
// Finish
|
||||
resolve(results);
|
||||
} else {
|
||||
resetTimeout();
|
||||
}
|
||||
};
|
||||
const onTimeout = () => {
|
||||
timer = null;
|
||||
cleanup();
|
||||
reject(new Error(`Request for parent frame ID timed out after ${timeout}ms`));
|
||||
};
|
||||
const resetTimeout = () => {
|
||||
if (timer !== null) { clearTimeout(timer); }
|
||||
timer = setTimeout(onTimeout, timeout);
|
||||
};
|
||||
|
||||
// Start
|
||||
chrome.runtime.onMessage.addListener(onMessage);
|
||||
resetTimeout();
|
||||
this._requestFrameInfo(targetWindow, uniqueId, this._frameId, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_onWindowMessage(event) {
|
||||
const {data} = event;
|
||||
if (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
data.action === this._requestMessageId
|
||||
) {
|
||||
try {
|
||||
this._onRequestFrameInfo(data.params);
|
||||
} catch (e) {
|
||||
// NOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestFrameInfo({uniqueId, originFrameId, index}) {
|
||||
if (
|
||||
typeof uniqueId !== 'string' ||
|
||||
typeof originFrameId !== 'number' ||
|
||||
!this._isNonNegativeInteger(index)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {parent} = window;
|
||||
const more = (window !== parent);
|
||||
|
||||
const responseParams = {uniqueId, frameId: this._frameId, index, more};
|
||||
this._safeSendMessageToFrame(originFrameId, this._responseMessageId, responseParams);
|
||||
|
||||
if (more) {
|
||||
this._requestFrameInfo(parent, uniqueId, originFrameId, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
async _safeSendMessageToFrame(frameId, action, params) {
|
||||
try {
|
||||
await api.sendMessageToFrame(frameId, action, params);
|
||||
} catch (e) {
|
||||
// NOP
|
||||
}
|
||||
}
|
||||
|
||||
_requestFrameInfo(targetWindow, uniqueId, originFrameId, index) {
|
||||
targetWindow.postMessage({
|
||||
action: this._requestMessageId,
|
||||
params: {uniqueId, originFrameId, index}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
_isNonNegativeInteger(value) {
|
||||
return (
|
||||
typeof value === 'number' &&
|
||||
Number.isFinite(value) &&
|
||||
value >= 0 &&
|
||||
Math.floor(value) === value
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user