diff options
Diffstat (limited to 'ext/fg/js/popup.js')
-rw-r--r-- | ext/fg/js/popup.js | 184 |
1 files changed, 156 insertions, 28 deletions
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index f5cb6f77..7db53f0d 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -17,7 +17,6 @@ /* global * DOM - * apiGetMessageToken * apiInjectStylesheet * apiOptionsGet */ @@ -39,8 +38,9 @@ class Popup { this._contentScale = 1.0; this._containerSizeContentScale = null; this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); - this._messageToken = null; this._previousOptionsContextSource = null; + this._containerSecret = null; + this._containerToken = null; this._container = document.createElement('iframe'); this._container.className = 'yomichan-float'; @@ -216,40 +216,154 @@ class Popup { return injectPromise; } + _initializeFrame(frame, targetOrigin, frameId, setupFrame, timeout=10000) { + return new Promise((resolve, reject) => { + const tokenMap = new Map(); + let timer = null; + let containerLoadedResolve = null; + let containerLoadedReject = null; + const containerLoaded = new Promise((resolve2, reject2) => { + containerLoadedResolve = resolve2; + containerLoadedReject = reject2; + }); + + const postMessage = (action, params) => { + const contentWindow = frame.contentWindow; + if (contentWindow === null) { throw new Error('Frame missing content window'); } + + let validOrigin = true; + try { + validOrigin = (contentWindow.location.origin === targetOrigin); + } catch (e) { + // NOP + } + if (!validOrigin) { throw new Error('Unexpected frame origin'); } + + contentWindow.postMessage({action, params}, targetOrigin); + }; + + const onMessage = (message) => { + onMessageInner(message); + return false; + }; + + const onMessageInner = async (message) => { + try { + if (!isObject(message)) { return; } + const {action, params} = message; + if (!isObject(params)) { return; } + await containerLoaded; + if (timer === null) { return; } // Done + + switch (action) { + case 'popupPrepared': + { + const {secret} = params; + const token = yomichan.generateId(16); + tokenMap.set(secret, token); + postMessage('initialize', {secret, token, frameId}); + } + break; + case 'popupInitialized': + { + const {secret, token} = params; + const token2 = tokenMap.get(secret); + if (typeof token2 !== 'undefined' && token === token2) { + cleanup(); + resolve({secret, token}); + } + } + break; + } + } catch (e) { + cleanup(); + reject(e); + } + }; + + const onLoad = () => { + if (containerLoadedResolve === null) { + cleanup(); + reject(new Error('Unexpected load event')); + return; + } + + if (Popup.isFrameAboutBlank(frame)) { + return; + } + + containerLoadedResolve(); + containerLoadedResolve = null; + containerLoadedReject = null; + }; + + const cleanup = () => { + if (timer === null) { return; } // Done + clearTimeout(timer); + timer = null; + + containerLoadedResolve = null; + if (containerLoadedReject !== null) { + containerLoadedReject(new Error('Terminated')); + containerLoadedReject = null; + } + + chrome.runtime.onMessage.removeListener(onMessage); + frame.removeEventListener('load', onLoad); + }; + + // Start + timer = setTimeout(() => { + cleanup(); + reject(new Error('Timeout')); + }, timeout); + + chrome.runtime.onMessage.addListener(onMessage); + frame.addEventListener('load', onLoad); + + // Prevent unhandled rejections + containerLoaded.catch(() => {}); // NOP + + setupFrame(frame); + }); + } + async _createInjectPromise() { - if (this._messageToken === null) { - this._messageToken = await apiGetMessageToken(); - } + this._injectStyles(); + + const {secret, token} = await this._initializeFrame(this._container, this._targetOrigin, this._frameId, (frame) => { + frame.removeAttribute('src'); + frame.removeAttribute('srcdoc'); + frame.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); + this._observeFullscreen(true); + this._onFullscreenChanged(); + }); + this._containerSecret = secret; + this._containerToken = token; + // Configure + const messageId = yomichan.generateId(16); const popupPreparedPromise = yomichan.getTemporaryListenerResult( chrome.runtime.onMessage, - ({action, params}, {resolve}) => { + (message, {resolve}) => { if ( - action === 'popupPrepareCompleted' && - isObject(params) && - params.targetPopupId === this._id + isObject(message) && + message.action === 'popupConfigured' && + isObject(message.params) && + message.params.messageId === messageId ) { resolve(); } } ); - - const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null); - this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); - this._container.addEventListener('load', () => { - this._invokeApi('prepare', { - popupInfo: { - id: this._id, - parentFrameId - }, - optionsContext: this._optionsContext, - childrenSupported: this._childrenSupported, - scale: this._contentScale - }); + this._invokeApi('configure', { + messageId, + frameId: this._frameId, + popupId: this._id, + optionsContext: this._optionsContext, + childrenSupported: this._childrenSupported, + scale: this._contentScale }); - this._observeFullscreen(true); - this._onFullscreenChanged(); - this._injectStyles(); return popupPreparedPromise; } @@ -267,6 +381,8 @@ class Popup { this._container.removeAttribute('src'); this._container.removeAttribute('srcdoc'); + this._containerSecret = null; + this._containerToken = null; this._injectPromise = null; this._injectPromiseComplete = false; } @@ -401,11 +517,12 @@ class Popup { } _invokeApi(action, params={}) { - const token = this._messageToken; + const secret = this._containerSecret; + const token = this._containerToken; const contentWindow = this._container.contentWindow; - if (token === null || contentWindow === null) { return; } + if (secret === null || token === null || contentWindow === null) { return; } - contentWindow.postMessage({action, params, token}, this._targetOrigin); + contentWindow.postMessage({action, params, secret, token}, this._targetOrigin); } _getFrameParentElement() { @@ -653,6 +770,17 @@ class Popup { injectedStylesheets.set(id, styleNode); return styleNode; } + + static isFrameAboutBlank(frame) { + try { + const contentDocument = frame.contentDocument; + if (contentDocument === null) { return false; } + const url = contentDocument.location.href; + return /^about:blank(?:[#?]|$)/.test(url); + } catch (e) { + return false; + } + } } Popup._injectedStylesheets = new Map(); |