Restrict new windows in replay + improved service worker loading + better error messages (#126)
* embeds: - support target rewriting with named iframe as per webrecorder/wombat#94 - refactor sw init: detect additional errors, such as CORS issues in nested iframes - support cross-origin replaybase for embeds - add 'requirecrossoriginiframe' to restrict embed to only be loaded in subdomain iframes, otherwise showing - rename noSandbox -> sandbox, not enabled by default init: set appName on service worker init docs: - add example embed to embedding doc - update docs for noSandbox -> sandbox - add warning if deprecated 'noSandbox' flag is used, explaining that it is now deprecated - update embedding and CHANGES.md with latest features - add tweet-example.wacz - add embed.html to test embeds bump to 1.7.0-beta.1
This commit is contained in:
+10
-6
@@ -1,11 +1,10 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { wrapCss, rwpLogo, IS_APP, VERSION, clickOnSpacebarPress } from "./misc";
|
||||
|
||||
import { registerSW } from "./pageutils";
|
||||
|
||||
import fasHelp from "@fortawesome/fontawesome-free/svgs/solid/question-circle.svg";
|
||||
import fasArrowLeft from "@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg";
|
||||
import fasArrowRight from "@fortawesome/fontawesome-free/svgs/solid/arrow-right.svg";
|
||||
import { SWManager } from "./swmanager";
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
@@ -28,9 +27,8 @@ class ReplayWebApp extends LitElement
|
||||
|
||||
this.loadInfo = null;
|
||||
|
||||
if (swName) {
|
||||
registerSW(swName);
|
||||
}
|
||||
this.swmanager = new SWManager({name: swName, appName: this.appName});
|
||||
this.swmanager.register().catch(() => this.swErrorMsg = this.swmanager.renderErrorReport(this.mainLogo));
|
||||
|
||||
this.safariKeyframes();
|
||||
}
|
||||
@@ -56,7 +54,9 @@ class ReplayWebApp extends LitElement
|
||||
collPageUrl: { type: String },
|
||||
pageTitle: { type: String },
|
||||
pageReplay: { type: Boolean },
|
||||
source: { type: String }
|
||||
source: { type: String },
|
||||
|
||||
swErrorMsg: { type: Object },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -248,6 +248,10 @@ class ReplayWebApp extends LitElement
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (this.embed && this.swErrorMsg) {
|
||||
return this.swErrorMsg;
|
||||
}
|
||||
|
||||
return html`
|
||||
${!this.embed || this.embed === "full" ? this.renderNavBar() : ""}
|
||||
|
||||
|
||||
+41
-19
@@ -1,9 +1,8 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { registerSW } from "./pageutils";
|
||||
|
||||
import { wrapCss, rwpLogo } from "./misc";
|
||||
import { SWManager } from "./swmanager";
|
||||
|
||||
|
||||
var scriptSrc = document.currentScript && document.currentScript.src;
|
||||
@@ -23,6 +22,7 @@ class Embed extends LitElement
|
||||
// eslint-disable-next-line no-undef
|
||||
this.swName = __SW_NAME__;
|
||||
this.mainElementName = "replay-app-main";
|
||||
this.appName = "ReplayWeb.page";
|
||||
this.view = "replay";
|
||||
this.ts = "";
|
||||
this.url = "";
|
||||
@@ -33,12 +33,14 @@ class Embed extends LitElement
|
||||
this.paramString = null;
|
||||
this.deepLink = false;
|
||||
this.newWindowBase = "";
|
||||
this.swInited = false;
|
||||
this.inited = false;
|
||||
this.embed = null;
|
||||
this.reloadCount = 0;
|
||||
this.noSandbox = false;
|
||||
this.sandbox = false;
|
||||
this.noWebWorker = false;
|
||||
this.noCache = false;
|
||||
// deprecated;
|
||||
this.noSandbox = null;
|
||||
this.logo = rwpLogo;
|
||||
}
|
||||
|
||||
@@ -66,12 +68,13 @@ class Embed extends LitElement
|
||||
coll: { type: String },
|
||||
config: { type: String },
|
||||
|
||||
swInited: { type: Boolean },
|
||||
inited: { type: Boolean },
|
||||
|
||||
paramString: { type: String },
|
||||
hashString: { type: String },
|
||||
|
||||
deepLink: { type: Boolean },
|
||||
sandbox: { type: Boolean },
|
||||
noSandbox: { type: Boolean },
|
||||
noWebWorker: { type: Boolean },
|
||||
noCache: { type: Boolean },
|
||||
@@ -79,16 +82,34 @@ class Embed extends LitElement
|
||||
|
||||
newWindowBase: { type: String },
|
||||
|
||||
errorMessage: { type: String }
|
||||
errorMessage: { type: String },
|
||||
|
||||
requireSubdomainIframe: {type: Boolean}
|
||||
};
|
||||
}
|
||||
|
||||
async doRegister() {
|
||||
const replaybaseURL = new URL(this.replaybase, window.location.href);
|
||||
|
||||
this.isCrossOrigin = replaybaseURL.origin !== window.location.origin;
|
||||
|
||||
if (this.isCrossOrigin) {
|
||||
this.inited = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const name = this.swName;
|
||||
const appName = this.appName;
|
||||
const scope = this.replaybase;
|
||||
const requireSubdomainIframe = this.requireSubdomainIframe;
|
||||
|
||||
this.swmanager = new SWManager({name, scope, requireSubdomainIframe, appName});
|
||||
|
||||
try {
|
||||
await registerSW(this.swName, this.replaybase);
|
||||
this.swInited = true;
|
||||
await this.swmanager.register();
|
||||
this.inited = true;
|
||||
} catch (e) {
|
||||
this.errorMessage = e;
|
||||
this.errorMessage = this.swmanager.renderErrorReport(this.logo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +137,9 @@ class Embed extends LitElement
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (this.noSandbox) {
|
||||
console.warn("The noSandbox flag is deprecated. ReplayWeb.page does not add a sandbox by default. To enable sandboxing, use 'sandbox' flag instead. This may result in PDFs not loading and pages opening in new windows, but may be more secure in some situations");
|
||||
}
|
||||
this.doRegister();
|
||||
|
||||
window.addEventListener("message", (event) => this.handleMessage(event));
|
||||
@@ -257,8 +281,8 @@ class Embed extends LitElement
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.paramString && this.hashString && this.swInited ? html`
|
||||
<iframe sandbox="${ifDefined(!this.noSandbox ?
|
||||
${this.paramString && this.hashString && this.inited ? html`
|
||||
<iframe sandbox="${ifDefined(this.sandbox ?
|
||||
"allow-downloads allow-modals allow-orientation-lock allow-pointer-lock\
|
||||
allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts\
|
||||
allow-same-origin allow-forms" : undefined)}"
|
||||
@@ -268,17 +292,15 @@ class Embed extends LitElement
|
||||
|
||||
` : html``}
|
||||
|
||||
${this.errorMessage ? html`
|
||||
<section class="full-width">
|
||||
<div class="has-text-centered">
|
||||
<fa-icon class="logo" id="wrlogo" size="2.5rem" .svg=${this.logo} aria-hidden="true"></fa-icon>
|
||||
</div>
|
||||
<div class="error">${this.errorMessage}</div>
|
||||
</section>
|
||||
`: ""}`;
|
||||
${this.errorMessage}
|
||||
`;
|
||||
}
|
||||
|
||||
onLoad(event) {
|
||||
if (this.isCrossOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const win = event.target.contentWindow;
|
||||
const doc = event.target.contentDocument;
|
||||
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@ import { wrapCss, IS_APP } from "./misc";
|
||||
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
import { getSWErrorMsg, parseURLSchemeHostPath } from "./pageutils";
|
||||
import { parseURLSchemeHostPath } from "./pageutils";
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
@@ -125,11 +125,11 @@ class Loader extends LitElement
|
||||
|
||||
this.percent = this.currentSize = this.totalSize = 0;
|
||||
|
||||
const noSWError = getSWErrorMsg();
|
||||
// const noSWError = getSWErrorMsg();
|
||||
|
||||
if (noSWError) {
|
||||
if (this.loadInfo && this.loadInfo.swError) {
|
||||
this.state = "errored";
|
||||
this.error = noSWError;
|
||||
this.error = this.loadInfo.swError;
|
||||
this.errorAllowRetry = false;
|
||||
return;
|
||||
}
|
||||
|
||||
+1
-58
@@ -1,59 +1,3 @@
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
let appName = "ReplayWeb.page";
|
||||
|
||||
// ===========================================================================
|
||||
function setAppName(newAppName) {
|
||||
appName = newAppName;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
function registerSW(name = "sw.js", scope = "./") {
|
||||
let resolve, reject;
|
||||
|
||||
const p = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
const errMsg = getSWErrorMsg();
|
||||
|
||||
if (errMsg) {
|
||||
console.error(errMsg);
|
||||
reject(errMsg);
|
||||
}
|
||||
|
||||
register(scope + name, {
|
||||
registrationOptions: { scope },
|
||||
registered () {
|
||||
console.log("Service worker is registered");
|
||||
resolve();
|
||||
},
|
||||
|
||||
error (error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
reject(`${appName} could not be loaded due to the following error:\n${error.toString()}`);
|
||||
}
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
function getSWErrorMsg() {
|
||||
if (navigator.serviceWorker) {
|
||||
return null;
|
||||
}
|
||||
if (window.location.protocol === "http:") {
|
||||
return `\
|
||||
Sorry, the ${appName} system must be loaded from an HTTPS URL, but was loaded from: ${window.location.host}.
|
||||
Please try loading this page from an HTTPS URL`;
|
||||
} else {
|
||||
return `Sorry, ${appName} won't work in this browser as Service Workers are not supported.
|
||||
Please try a different browser.
|
||||
(Service Workers are disabled in Firefox in Private Mode. If Using Private Mode in Firefox, try regular mode)`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
async function digestMessage(message, hashtype) {
|
||||
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
|
||||
@@ -155,5 +99,4 @@ function parseURLSchemeHostPath(url) {
|
||||
}
|
||||
|
||||
|
||||
export { digestMessage, tsToDate, getTS, getPageDateTS, getReplayLink, sourceToId, parseURLSchemeHostPath,
|
||||
registerSW, getSWErrorMsg, setAppName };
|
||||
export { digestMessage, tsToDate, getTS, getPageDateTS, getReplayLink, sourceToId, parseURLSchemeHostPath };
|
||||
|
||||
+1
-1
@@ -298,7 +298,7 @@ class Replay extends LitElement
|
||||
</div>` : html`
|
||||
|
||||
<div class="iframe-container">
|
||||
<iframe class="iframe-main" @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
|
||||
<iframe class="iframe-main" name="___wb_replay_top_frame" @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
|
||||
src="${this.iframeUrl}" title="${title}"></iframe>
|
||||
|
||||
${this.showAuth ? html`
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { html } from "lit";
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
export class SWManager
|
||||
{
|
||||
constructor({name = "sw.js", scope = "./", appName = "ReplayWeb.page", requireSubdomainIframe = false} = {}) {
|
||||
this.name = name;
|
||||
this.scope = scope;
|
||||
this.appName = appName;
|
||||
this.requireSubdomainIframe = requireSubdomainIframe;
|
||||
|
||||
this.errorMsg = null;
|
||||
}
|
||||
|
||||
setAppName(newAppName) {
|
||||
this.appName = newAppName;
|
||||
}
|
||||
|
||||
register() {
|
||||
let resolve, reject;
|
||||
|
||||
const p = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
this.errorMsg = this.getSWErrorMsg();
|
||||
|
||||
if (this.errorMsg) {
|
||||
console.error(this.errorMsg);
|
||||
reject(this.errorMsg);
|
||||
}
|
||||
|
||||
const handleError = (error) => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
this.errorMsg = this.getCrossOriginIframeMsg();
|
||||
if (!this.errorMsg) {
|
||||
this.errorMsg = `${this.appName} could not be loaded due to the following error:\n${error.toString()}`;
|
||||
}
|
||||
|
||||
reject(this.errorMsg);
|
||||
};
|
||||
|
||||
register(this.scope + this.name, {
|
||||
registrationOptions: { scope: this.scope },
|
||||
registered () {
|
||||
console.log("Service worker is registered");
|
||||
resolve();
|
||||
},
|
||||
|
||||
error(errMsg) { handleError(errMsg); }
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
getCrossOriginIframeMsg() {
|
||||
if (this.isCrossOriginIframe()) {
|
||||
return `\
|
||||
Sorry, Service Workers can not be used in cross-origin iframes.
|
||||
This web archive embed is loaded from ${window.location.origin} but the top page is on a different origin.
|
||||
The embed must be loaded from the same origin or a subdomain.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
isCrossOriginIframe() {
|
||||
if (window.parent === window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.top.location.href === "";
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
getSWErrorMsg() {
|
||||
if (navigator.serviceWorker) {
|
||||
|
||||
// must be loaded from a cross-origin (eg. subdomain)
|
||||
if (this.requireSubdomainIframe && !this.isCrossOriginIframe()) {
|
||||
return `Sorry, due to security settings, this ${this.appName} embed only be viewed within a subdomain iframe.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// loaded from a cross-origin iframe that's *not* a subdomain
|
||||
const msg = this.getCrossOriginIframeMsg();
|
||||
if (msg) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
return `
|
||||
Sorry, the ${this.appName} system must be loaded from an HTTPS URL (or localhost), but was loaded from: ${window.location.host}.
|
||||
Please try loading this page from an HTTPS URL`;
|
||||
}
|
||||
|
||||
// Detect firefox private mode
|
||||
if ("MozAppearance" in document.documentElement.style) {
|
||||
return "Sorry, Service Workers are disabled in Firefox in Private Mode. Please try loading this page in regular mode instead.";
|
||||
}
|
||||
|
||||
return `Sorry, ${this.appName} won't work in this browser as Service Workers are not supported in this window.
|
||||
Please try a different browser.`;
|
||||
|
||||
}
|
||||
|
||||
renderErrorReport(logo, override) {
|
||||
const msg = this.errorMsg || override;
|
||||
|
||||
if (!msg) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return html`
|
||||
<section class="is-fullwidth">
|
||||
<div class="has-text-centered">
|
||||
<fa-icon style="margin: 1em;flex-grow: 1;" id="wrlogo" size="2.5rem" .svg=${logo} aria-hidden="true"></fa-icon>
|
||||
</div>
|
||||
<div style="white-space: pre-wrap; text-align: center" >${msg}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user