* Validate source window before handling messages * Add unregisterHandler to CrossFrameAPI * Refactor the process FrameAncestryHandler uses to get ancestor frame IDs * Store a mapping of child frame information * Update getFrameAncestryInfo to only run once
196 lines
6.4 KiB
JavaScript
196 lines
6.4 KiB
JavaScript
/*
|
|
* 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._responseMessageIdBase = `${this._requestMessageId}.response.`;
|
|
this._getFrameAncestryInfoPromise = null;
|
|
this._childFrameMap = new Map();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
async getFrameAncestryInfo() {
|
|
if (this._getFrameAncestryInfoPromise === null) {
|
|
this._getFrameAncestryInfoPromise = this._getFrameAncestryInfo(5000);
|
|
}
|
|
return await this._getFrameAncestryInfoPromise;
|
|
}
|
|
|
|
// Private
|
|
|
|
_getFrameAncestryInfo(timeout=5000) {
|
|
return new Promise((resolve, reject) => {
|
|
const targetWindow = window.parent;
|
|
if (window === targetWindow) {
|
|
resolve([]);
|
|
return;
|
|
}
|
|
|
|
const uniqueId = generateId(16);
|
|
let nonce = generateId(16);
|
|
const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`;
|
|
const results = [];
|
|
let timer = null;
|
|
|
|
const cleanup = () => {
|
|
if (timer !== null) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
api.crossFrame.unregisterHandler(responseMessageId);
|
|
};
|
|
const onMessage = (params) => {
|
|
if (params.nonce !== nonce) { return null; }
|
|
|
|
// Add result
|
|
const {frameId, more} = params;
|
|
results.push(frameId);
|
|
nonce = generateId(16);
|
|
|
|
if (!more) {
|
|
// Cleanup
|
|
cleanup();
|
|
|
|
// Finish
|
|
resolve(results);
|
|
}
|
|
return {nonce};
|
|
};
|
|
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
|
|
api.crossFrame.registerHandlers([[responseMessageId, {async: false, handler: onMessage}]]);
|
|
resetTimeout();
|
|
const frameId = this._frameId;
|
|
this._requestFrameInfo(targetWindow, frameId, frameId, uniqueId, nonce);
|
|
});
|
|
}
|
|
|
|
_onWindowMessage(event) {
|
|
const {source} = event;
|
|
if (source === window || source.parent !== window) { return; }
|
|
|
|
const {data} = event;
|
|
if (
|
|
typeof data === 'object' &&
|
|
data !== null &&
|
|
data.action === this._requestMessageId
|
|
) {
|
|
this._onRequestFrameInfo(data.params, source);
|
|
}
|
|
}
|
|
|
|
async _onRequestFrameInfo(params, source) {
|
|
try {
|
|
let {originFrameId, childFrameId, uniqueId, nonce} = params;
|
|
if (
|
|
!this._isNonNegativeInteger(originFrameId) ||
|
|
typeof uniqueId !== 'string' ||
|
|
typeof nonce !== 'string'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const frameId = this._frameId;
|
|
const {parent} = window;
|
|
const more = (window !== parent);
|
|
const responseParams = {frameId, nonce, more};
|
|
const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`;
|
|
|
|
try {
|
|
const response = await api.crossFrame.invoke(originFrameId, responseMessageId, responseParams);
|
|
if (response === null) { return; }
|
|
nonce = response.nonce;
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
if (!this._childFrameMap.has(childFrameId)) {
|
|
this._childFrameMap.set(childFrameId, {window: source});
|
|
}
|
|
|
|
if (more) {
|
|
this._requestFrameInfo(parent, originFrameId, frameId, uniqueId, nonce);
|
|
}
|
|
} catch (e) {
|
|
// NOP
|
|
}
|
|
}
|
|
|
|
_requestFrameInfo(targetWindow, originFrameId, childFrameId, uniqueId, nonce) {
|
|
targetWindow.postMessage({
|
|
action: this._requestMessageId,
|
|
params: {originFrameId, childFrameId, uniqueId, nonce}
|
|
}, '*');
|
|
}
|
|
|
|
_isNonNegativeInteger(value) {
|
|
return (
|
|
typeof value === 'number' &&
|
|
Number.isFinite(value) &&
|
|
value >= 0 &&
|
|
Math.floor(value) === value
|
|
);
|
|
}
|
|
}
|