/* * Copyright (c) 2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ const platfWebViewName = "platformWebView"; function initializePlatfWebView(webViewImports) { const BEGIN_SHOWING_DIRECT = 0; const BEGIN_SHOWING_ENABLE_JAVASCRIPT = 1; const BEGIN_SHOWING_CONTENT_BLOCKED = 2; const EVENT_WEBVIEW_CHANNEL = 0; const EVENT_WEBVIEW_MESSAGE = 1; const EVENT_WEBVIEW_PERMISSION_ALLOW = 2; const EVENT_WEBVIEW_PERMISSION_BLOCK = 3; const EVENT_WEBVIEW_PERMISSION_CLEAR = 4; const EVENT_CHANNEL_OPEN = 0; const EVENT_CHANNEL_CLOSE = 1; const EVENT_MESSAGE_STRING = 0; const EVENT_MESSAGE_BINARY = 1; const utf8Decoder = new TextDecoder("utf-8"); var supported = false; var cspSupport = false; var supportForce = runtimeOpts.forceWebViewSupport; var enableCSP = runtimeOpts.enableWebViewCSP; if(supportForce) { supported = true; cspSupport = true; }else { supported = false; cspSupport = false; try { var tmp = /** @type {HTMLIFrameElement} */ (document.createElement("iframe")); supported = tmp != null && (typeof tmp.allow === "string") && (typeof tmp.sandbox === "object"); cspSupport = enableCSP && supported && (typeof tmp.csp === "string"); }catch(ex) { eagError("Error checking iframe support"); eagError(ex); } } if(!supported) { eagError("This browser does not meet the safety requirements for webview support, this feature will be disabled"); }else if(!cspSupport && enableCSP) { eagWarn("This browser does not support CSP attribute on iframes! (try Chrome)"); } const requireSSL = location.protocol && "https:" === location.protocol.toLowerCase(); var webviewResetSerial = 0; /** @type {HTMLElement} */ var currentIFrameContainer = null; /** @type {HTMLElement} */ var currentAllowJavaScript = null; /** @type {HTMLIFrameElement} */ var currentIFrame = null; /** @type {function(MessageEvent)|null} */ var currentMessageHandler = null; window.addEventListener("message", /** @type {function(Event)} */ (function(/** MessageEvent */ evt) { if(currentMessageHandler && evt.source !== window) { currentMessageHandler(evt); } })); /** * @return {boolean} */ webViewImports["checkSupported"] = function() { return supported; }; /** * @return {boolean} */ webViewImports["checkCSPSupported"] = function() { return cspSupport; }; /** * @param {string} ch * @param {string} str */ webViewImports["sendStringMessage"] = function(ch, str) { try { var w; if(currentIFrame != null && (w = currentIFrame.contentWindow) != null) { w.postMessage({ "ver": 1, "channel": ch, "type": "string", "data": str }, "*"); }else { eagError("Server tried to send the WebView a message, but the message channel is not open!"); } }catch(/** Error */ ex) { eagStackTrace(ERROR, "Failed to send string message to WebView!", ex); } }; /** * @param {string} ch * @param {Uint8Array} bin */ webViewImports["sendBinaryMessage"] = function(ch, bin) { try { var w; if(currentIFrame != null && (w = currentIFrame.contentWindow) != null) { const copiedArray = new Uint8Array(bin.length); copiedArray.set(bin, 0); w.postMessage({ "ver": 1, "channel": ch, "type": "binary", "data": copiedArray.buffer }, "*"); }else { eagError("Server tried to send the WebView a message, but the message channel is not open!"); } }catch(/** Error */ ex) { eagStackTrace(ERROR, "Failed to send string message to WebView!", ex); } }; /** * @param {number} state * @param {Object} options * @param {number} x * @param {number} y * @param {number} w * @param {number} h */ webViewImports["beginShowing"] = function(state, options, x, y, w, h) { if(!supported) { return; } try { setupShowing(x, y, w, h); switch(state) { case BEGIN_SHOWING_DIRECT: beginShowingDirect(options); break; case BEGIN_SHOWING_ENABLE_JAVASCRIPT: if(options["contentMode"] === 1) { const copiedBlob = new Uint8Array(options["blob"].length); copiedBlob.set(options["blob"], 0); options["blob"] = copiedBlob; } beginShowingEnableJavaScript(options); break; case BEGIN_SHOWING_CONTENT_BLOCKED: if(options["contentMode"] === 1) { const copiedBlob = new Uint8Array(options["blob"].length); copiedBlob.set(options["blob"], 0); options["blob"] = copiedBlob; } beginShowingContentBlocked(options); break; default: break; } }catch(/** Error */ ex) { eagStackTrace(ERROR, "Failed to begin showing WebView!", ex); } }; /** * @param {number} x * @param {number} y * @param {number} w * @param {number} h */ function setupShowing(x, y, w, h) { if(currentIFrameContainer !== null) { endShowingImpl(); } currentIFrameContainer = /** @type {HTMLElement} */ (document.createElement("div")); currentIFrameContainer.classList.add("_eaglercraftX_webview_container_element"); currentIFrameContainer.style.border = "5px solid #333333"; currentIFrameContainer.style.zIndex = "11"; currentIFrameContainer.style.position = "absolute"; currentIFrameContainer.style.backgroundColor = "#DDDDDD"; currentIFrameContainer.style.fontFamily = "sans-serif"; resizeImpl(x, y, w, h); parentElement.appendChild(currentIFrameContainer); } /** * @param {HTMLIFrameElement} iframeElement * @param {string} str */ function setAllowSafe(iframeElement, str) { iframeElement.allow = str; return iframeElement.allow === str; } /** * @param {HTMLIFrameElement} iframeElement * @param {Array} args */ function setSandboxSafe(iframeElement, args) { const theSandbox = iframeElement.sandbox; for(var i = 0; i < args.length; ++i) { theSandbox.add(args[i]); } for(var i = 0; i < args.length; ++i) { if(!theSandbox.contains(args[i])) { return false; } } for(var i = 0; i < theSandbox.length; ++i) { if(!args.find(itm => itm === theSandbox[i])) { return false; } } return true; } function beginShowingDirect(options) { if(!supportForce) { currentIFrame = /** @type {HTMLIFrameElement} */ (document.createElement("iframe")); currentIFrame.referrerPolicy = "strict-origin"; const sandboxArgs = [ "allow-downloads" ]; if(options["scriptEnabled"]) { sandboxArgs.push("allow-scripts"); sandboxArgs.push("allow-pointer-lock"); } if(!setAllowSafe(currentIFrame, "") || !setSandboxSafe(currentIFrame, sandboxArgs)) { eagError("Caught safety exception while opening webview!"); if(currentIFrame !== null) { currentIFrame.remove(); currentIFrame = null; } eagError("Things you can try:"); eagError("1. Set window.eaglercraftXOpts.forceWebViewSupport to true"); eagError("2. Set window.eaglercraftXOpts.enableWebViewCSP to false"); eagError("(these settings may compromise security)"); beginShowingSafetyError(); return; } }else { currentIFrame = /** @type {HTMLIFrameElement} */ (document.createElement("iframe")); currentIFrame.allow = ""; currentIFrame.referrerPolicy = "strict-origin"; currentIFrame.sandbox.add("allow-downloads"); if(options["scriptEnabled"]) { currentIFrame.sandbox.add("allow-scripts"); currentIFrame.sandbox.add("allow-pointer-lock"); } } currentIFrame.credentialless = true; currentIFrame.loading = "lazy"; var cspWarn = false; if(options["contentMode"] === 1) { if(enableCSP && cspSupport) { if(typeof currentIFrame.csp === "string") { var csp = "default-src 'none';"; var protos = options["strictCSPEnable"] ? "" : (requireSSL ? " https:" : " http: https:"); if(options["scriptEnabled"]) { csp += (" script-src 'unsafe-eval' 'unsafe-inline' data: blob:" + protos + ";"); csp += (" style-src 'unsafe-eval' 'unsafe-inline' data: blob:" + protos + ";"); csp += (" img-src data: blob:" + protos + ";"); csp += (" font-src data: blob:" + protos + ";"); csp += (" child-src data: blob:" + protos + ";"); csp += (" frame-src data: blob:;"); csp += (" media-src data: mediastream: blob:" + protos + ";"); csp += (" connect-src data: blob:" + protos + ";"); csp += (" worker-src data: blob:" + protos + ";"); }else { csp += (" style-src data: 'unsafe-inline'" + protos + ";"); csp += (" img-src data:" + protos + ";"); csp += (" font-src data:" + protos + ";"); csp += (" media-src data:" + protos + ";"); } currentIFrame.csp = csp; }else { eagWarn("This browser does not support CSP attribute on iframes! (try Chrome)"); cspWarn = true; } }else { cspWarn = true; } if(cspWarn && options["strictCSPEnable"]) { eagWarn("Strict CSP was requested for this webview, but that feature is not available!"); } }else { cspWarn = true; } currentIFrame.style.border = "none"; currentIFrame.style.backgroundColor = "white"; currentIFrame.style.width = "100%"; currentIFrame.style.height = "100%"; currentIFrame.classList.add("_eaglercraftX_webview_iframe_element"); currentIFrameContainer.appendChild(currentIFrame); if(options["contentMode"] === 1) { const decodedText = utf8Decoder.decode(options["blob"]); options["blob"] = null; currentIFrame.srcdoc = decodedText; }else { currentIFrame.src = options["uri"]; } const resetSer = webviewResetSerial; const curIFrame = currentIFrame; let focusTracker = false; currentIFrame.addEventListener("mouseover", function(/** Event */ evt) { if(resetSer === webviewResetSerial && curIFrame === currentIFrame) { if(!focusTracker) { focusTracker = true; currentIFrame.contentWindow.focus(); } } }); currentIFrame.addEventListener("mouseout", function(/** Event */ evt) { if(resetSer === webviewResetSerial && curIFrame === currentIFrame) { if(focusTracker) { focusTracker = false; window.focus(); } } }); if(options["scriptEnabled"] && options["serverMessageAPIEnabled"]) { currentMessageHandler = function(/** MessageEvent */ evt) { if(resetSer === webviewResetSerial && curIFrame === currentIFrame && evt.source === curIFrame.contentWindow) { /** @type {Object} */ const obj = evt.data; if((typeof obj === "object") && (obj["ver"] === 1) && ((typeof obj["channel"] === "string") && obj["channel"]["length"] > 0)) { if(typeof obj["open"] === "boolean") { pushEvent(EVENT_TYPE_WEBVIEW, EVENT_WEBVIEW_CHANNEL, { "eventType": (obj["open"] ? EVENT_CHANNEL_OPEN : EVENT_CHANNEL_CLOSE), "channelName": obj["channel"] }); return; }else if(typeof obj["data"] === "string") { pushEvent(EVENT_TYPE_WEBVIEW, EVENT_WEBVIEW_MESSAGE, { "eventType": EVENT_MESSAGE_STRING, "channelName": obj["channel"], "eventData": obj["data"] }); return; }else if(obj["data"] instanceof ArrayBuffer) { pushEvent(EVENT_TYPE_WEBVIEW, EVENT_WEBVIEW_MESSAGE, { "eventType": EVENT_MESSAGE_BINARY, "channelName": obj["channel"], "eventData": obj["data"] }); return; } } eagWarn("WebView sent an invalid message!"); }else { eagWarn("Recieved message from on dead IFrame handler: (#{}) {}", resetSer, curIFrame.src); } }; } eagInfo("WebView is loading: \"{}\"", options["contentMode"] === 1 ? "about:srcdoc" : currentIFrame.src); eagInfo("JavaScript: {}, Strict CSP: {}, Message API: {}", options["scriptEnabled"], options["strictCSPEnable"] && !cspWarn, options["serverMessageAPIEnabled"]); } function beginShowingEnableJSSetup() { if(currentAllowJavaScript !== null) { ++webviewResetSerial; currentAllowJavaScript.remove(); currentAllowJavaScript = null; } currentAllowJavaScript = /** @type {HTMLElement} */ (document.createElement("div")); currentAllowJavaScript.style.backgroundColor = "white"; currentAllowJavaScript.style.width = "100%"; currentAllowJavaScript.style.height = "100%"; currentAllowJavaScript.classList.add("_eaglercraftX_webview_permission_screen"); currentIFrameContainer.appendChild(currentAllowJavaScript); } function beginShowingEnableJavaScript(options) { beginShowingEnableJSSetup(); var strictCSPMarkup; if(options["contentMode"] !== 1) { strictCSPMarkup = "Impossible"; }else if(!cspSupport || !enableCSP) { strictCSPMarkup = "Unsupported"; }else if(options["strictCSPEnable"]) { strictCSPMarkup = "Enabled"; }else { strictCSPMarkup = "Disabled"; } var messageAPIMarkup; if(options["serverMessageAPIEnabled"]) { messageAPIMarkup = "Enabled"; }else { messageAPIMarkup = "Disabled"; } currentAllowJavaScript.innerHTML = "
" + "

 Allow JavaScript

" + "

" + "

Strict CSP: " + strictCSPMarkup + " | " + "Message API: " + messageAPIMarkup + "

" + "

Remember my choice

" + "

 " + "

"; const serial = webviewResetSerial; if(options["contentMode"] !== 1) { const urlStr = options["url"]; currentAllowJavaScript.querySelector("._eaglercraftX_permission_target_url").innerText = urlStr.length() > 255 ? (urlStr.substring(0, 253) + "...") : urlStr; } currentAllowJavaScript.querySelector("._eaglercraftX_allow_javascript").addEventListener("click", function(/** Event */ evt) { if(webviewResetSerial === serial && currentAllowJavaScript !== null) { const chkbox = currentAllowJavaScript.querySelector("._eaglercraftX_remember_javascript"); if(chkbox !== null && chkbox.checked) { pushEvent(EVENT_TYPE_WEBVIEW, EVENT_WEBVIEW_PERMISSION_ALLOW, null); } currentAllowJavaScript.remove(); currentAllowJavaScript = null; ++webviewResetSerial; beginShowingDirect(options); } }); currentAllowJavaScript.querySelector("._eaglercraftX_block_javascript").addEventListener("click", function(/** Event */ evt) { if(webviewResetSerial === serial && currentAllowJavaScript !== null) { const chkbox = currentAllowJavaScript.querySelector("._eaglercraftX_remember_javascript"); if(chkbox !== null && chkbox.checked) { pushEvent(EVENT_TYPE_WEBVIEW, EVENT_WEBVIEW_PERMISSION_BLOCK, null); } beginShowingContentBlocked(options); } }); } function beginShowingContentBlocked(options) { beginShowingEnableJSSetup(); currentAllowJavaScript.innerHTML = "

" + " Content Blocked

" + "

You chose to block JavaScript execution for this embed

" + "

"; const serial = webviewResetSerial; currentAllowJavaScript.querySelector("._eaglercraftX_re_evaluate_javascript").addEventListener("click", function(/** Event */ evt) { if(webviewResetSerial === serial && currentAllowJavaScript !== null) { pushEvent(EVENT_TYPE_WEBVIEW, EVENT_WEBVIEW_PERMISSION_CLEAR, null); beginShowingEnableJavaScript(options); } }); } function beginShowingSafetyError() { beginShowingEnableJSSetup(); currentAllowJavaScript.innerHTML = "

" + " IFrame Safety Error

" + "

The content cannot be displayed safely!

" + "

Check console for more details

"; } /** * @param {number} x * @param {number} y * @param {number} w * @param {number} h */ function resizeImpl(x, y, w, h) { if(currentIFrameContainer) { const s = window.devicePixelRatio; currentIFrameContainer.style.top = "" + (y / s) + "px"; currentIFrameContainer.style.left = "" + (x / s) + "px"; currentIFrameContainer.style.width = "" + ((w / s) - 10) + "px"; currentIFrameContainer.style.height = "" + ((h / s) - 10) + "px"; } } function endShowingImpl() { ++webviewResetSerial; if(currentIFrame) { currentIFrame.remove(); currentIFrame = null; } currentMessageHandler = null; if(currentAllowJavaScript) { currentAllowJavaScript.remove(); currentAllowJavaScript = null; } if(currentIFrameContainer) { currentIFrameContainer.remove(); currentIFrameContainer = null; } window.focus(); } webViewImports["resize"] = resizeImpl; webViewImports["endShowing"] = endShowingImpl; } function initializeNoPlatfWebView(webViewImports) { setUnsupportedFunc(webViewImports, platfWebViewName, "checkSupported"); setUnsupportedFunc(webViewImports, platfWebViewName, "checkCSPSupported"); setUnsupportedFunc(webViewImports, platfWebViewName, "sendStringMessage"); setUnsupportedFunc(webViewImports, platfWebViewName, "sendBinaryMessage"); setUnsupportedFunc(webViewImports, platfWebViewName, "beginShowing"); setUnsupportedFunc(webViewImports, platfWebViewName, "resize"); setUnsupportedFunc(webViewImports, platfWebViewName, "endShowing"); }