From 356e7f52749aaecb981bbc8286dd55d6d74e6343 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 6 Feb 2021 14:32:21 -0500 Subject: [PATCH] Add FrameAncestryHandler (#1351) --- ext/fg/js/frame-ancestry-handler.js | 196 ++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 ext/fg/js/frame-ancestry-handler.js diff --git a/ext/fg/js/frame-ancestry-handler.js b/ext/fg/js/frame-ancestry-handler.js new file mode 100644 index 00000000..d51467f8 --- /dev/null +++ b/ext/fg/js/frame-ancestry-handler.js @@ -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 . + */ + +/* 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 + ); + } +}