Skip to content

Commit

Permalink
catch all calls to iframe.contentDocument
Browse files Browse the repository at this point in the history
  • Loading branch information
chargome committed Nov 16, 2024
1 parent d6cc9ad commit 048b9e7
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 43 deletions.
6 changes: 4 additions & 2 deletions packages/rrdom/src/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
RRDocument,
Mirror,
} from '.';
import { getIFrameContentDocument } from './util';

const NAMESPACES: Record<string, string> = {
svg: 'http://www.w3.org/2000/svg',
Expand Down Expand Up @@ -170,8 +171,9 @@ function diffBeforeUpdatingChildren(
const newRRElement = newTree as IRRElement;
switch (newRRElement.tagName) {
case 'IFRAME': {
const oldContentDocument = (oldTree as HTMLIFrameElement)
.contentDocument;
const oldContentDocument = getIFrameContentDocument(
oldTree as HTMLIFrameElement,
);
// If the iframe is cross-origin, the contentDocument will be null.
if (!oldContentDocument) break;
// IFrame element doesn't have child nodes, so here we update its content document separately.
Expand Down
4 changes: 3 additions & 1 deletion packages/rrdom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
IRRText,
IRRComment,
} from './document';
import { getIFrameContentDocument } from './util';

export class RRDocument extends BaseRRDocumentImpl(RRNode) {
private UNSERIALIZED_STARTING_ID = -2;
Expand Down Expand Up @@ -313,7 +314,7 @@ export function buildFromDom(
}

if (node.nodeName === 'IFRAME') {
const iframeDoc = (node as HTMLIFrameElement).contentDocument;
const iframeDoc = getIFrameContentDocument(node as HTMLIFrameElement);
iframeDoc && walk(iframeDoc, rrNode);
} else if (
node.nodeType === NodeType.DOCUMENT_NODE ||
Expand Down Expand Up @@ -485,3 +486,4 @@ export { RRNode };

export { diff, createOrGetNode, ReplayerHandler } from './diff';
export * from './document';
export { getIFrameContentDocument, getIFrameContentWindow } from './util';
27 changes: 27 additions & 0 deletions packages/rrdom/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Get the content document of an iframe.
* Catching errors is necessary because some older browsers block access to the content document of a sandboxed iframe.
*/
export function getIFrameContentDocument(iframe?: HTMLIFrameElement) {
if (iframe) {
try {
return iframe.contentDocument;
} catch (e) {
// noop
}
}
}

/**
* Get the content window of an iframe.
* Catching errors is necessary because some older browsers block access to the content document of a sandboxed iframe.
*/
export function getIFrameContentWindow(iframe?: HTMLIFrameElement) {
if (iframe) {
try {
return iframe.contentWindow;
} catch (e) {
// noop
}
}
}
13 changes: 8 additions & 5 deletions packages/rrweb/src/record/iframe-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
mutationCallBack,
} from '@sentry-internal/rrweb-types';
import type { StylesheetManager } from './stylesheet-manager';
import { getIFrameContentDocument } from '@sentry-internal/rrdom';

export interface IframeManagerInterface {
crossOriginIframeMirror: CrossOriginIframeMirror;
Expand Down Expand Up @@ -109,14 +110,16 @@ export class IframeManager implements IframeManagerInterface {
});
this.loadListener?.(iframeEl);

const iframeDoc = getIFrameContentDocument(iframeEl);

if (
iframeEl.contentDocument &&
iframeEl.contentDocument.adoptedStyleSheets &&
iframeEl.contentDocument.adoptedStyleSheets.length > 0
iframeDoc &&
iframeDoc.adoptedStyleSheets &&
iframeDoc.adoptedStyleSheets.length > 0
)
this.stylesheetManager.adoptStyleSheets(
iframeEl.contentDocument.adoptedStyleSheets,
this.mirror.getId(iframeEl.contentDocument),
iframeDoc.adoptedStyleSheets,
this.mirror.getId(iframeDoc),
);
}
private handleMessage(message: MessageEvent | CrossOriginIframeMessageEvent) {
Expand Down
6 changes: 5 additions & 1 deletion packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
getShadowHost,
closestElementOfNode,
} from '../utils';
import { getIFrameContentDocument } from '@sentry-internal/rrdom';

type DoubleLinkedListNode = {
previous: DoubleLinkedListNode | null;
Expand Down Expand Up @@ -628,7 +629,10 @@ export default class MutationBuffer {
attributeName === 'src' &&
!this.keepIframeSrcFn(value as string)
) {
if (!(target as HTMLIFrameElement).contentDocument) {
const iframeDoc = getIFrameContentDocument(
target as HTMLIFrameElement,
);
if (!iframeDoc) {
// we can't record it directly as we can't see into it
// preserve the src attribute so a decision can be taken at replay time
attributeName = 'rr_src';
Expand Down
13 changes: 9 additions & 4 deletions packages/rrweb/src/record/shadow-dom-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
import { patch, inDom, setTimeout } from '../utils';
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
import { isNativeShadowDom } from '@sentry-internal/rrweb-snapshot';
import {
getIFrameContentDocument,
getIFrameContentWindow,
} from '@sentry-internal/rrdom';

type BypassOptions = Omit<
MutationBufferParam,
Expand Down Expand Up @@ -122,15 +126,16 @@ export class ShadowDomManager implements ShadowDomManagerInterface {
* Monkey patch 'attachShadow' of an IFrameElement to observe newly added shadow doms.
*/
public observeAttachShadow(iframeElement: HTMLIFrameElement) {
if (!iframeElement.contentWindow || !iframeElement.contentDocument) return;

const iframeDoc = getIFrameContentDocument(iframeElement);
const iframeWindow = getIFrameContentWindow(iframeElement);
if (!iframeDoc || !iframeWindow) return;
this.patchAttachShadow(
(
iframeElement.contentWindow as Window & {
iframeWindow as Window & {
Element: { prototype: Element };
}
).Element,
iframeElement.contentDocument,
iframeDoc,
);
}

Expand Down
72 changes: 42 additions & 30 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
buildFromDom,
diff,
getDefaultSN,
getIFrameContentDocument,
getIFrameContentWindow,
} from '@sentry-internal/rrdom';
import type {
RRNode,
Expand Down Expand Up @@ -266,10 +268,11 @@ export class Replayer {
}
},
};
if (this.iframe.contentDocument)
const iframeDoc = getIFrameContentDocument(this.iframe);
if (iframeDoc)
try {
diff(
this.iframe.contentDocument,
iframeDoc,
this.virtualDom,
replayerHandler,
this.virtualDom.mirror,
Expand Down Expand Up @@ -553,7 +556,8 @@ export class Replayer {
this.service.send({ type: 'PAUSE' });
this.service.send({ type: 'PLAY', payload: { timeOffset } });
}
this.iframe.contentDocument
const iframeDoc = getIFrameContentDocument(this.iframe);
iframeDoc
?.getElementsByTagName('html')[0]
?.classList.remove('rrweb-paused');
this.emitter.emit(ReplayerEvents.Start);
Expand All @@ -567,9 +571,8 @@ export class Replayer {
this.play(timeOffset);
this.service.send({ type: 'PAUSE' });
}
this.iframe.contentDocument
?.getElementsByTagName('html')[0]
?.classList.add('rrweb-paused');
const iframeDoc = getIFrameContentDocument(this.iframe);
iframeDoc?.getElementsByTagName('html')[0]?.classList.add('rrweb-paused');
this.emitter.emit(ReplayerEvents.Pause);
}

Expand Down Expand Up @@ -638,13 +641,11 @@ export class Replayer {
this.iframe.setAttribute('sandbox', attributes.join(' '));
this.disableInteract();
this.wrapper.appendChild(this.iframe);
if (this.iframe.contentWindow && this.iframe.contentDocument) {
smoothscrollPolyfill(
this.iframe.contentWindow,
this.iframe.contentDocument,
);

polyfill(this.iframe.contentWindow as IWindow);
const iframeDoc = getIFrameContentDocument(this.iframe);
const iframeWindow = getIFrameContentWindow(this.iframe);
if (iframeWindow && iframeDoc) {
smoothscrollPolyfill(iframeWindow, iframeDoc);
polyfill(iframeWindow as IWindow);
}
}

Expand Down Expand Up @@ -816,7 +817,8 @@ export class Replayer {
event: fullSnapshotEvent & { timestamp: number },
isSync = false,
) {
if (!this.iframe.contentDocument) {
const iframeDoc = getIFrameContentDocument(this.iframe);
if (!iframeDoc) {
return this.warn('Looks like your replayer has been destroyed.');
}
if (Object.keys(this.legacy_missingNodeRetryMap).length) {
Expand Down Expand Up @@ -850,24 +852,23 @@ export class Replayer {

this.mirror.reset();
rebuild(event.data.node, {
doc: this.iframe.contentDocument,
doc: iframeDoc,
afterAppend,
cache: this.cache,
mirror: this.mirror,
});
afterAppend(this.iframe.contentDocument, event.data.node.id);
afterAppend(iframeDoc, event.data.node.id);

for (const { mutationInQueue, builtNode } of collected) {
this.attachDocumentToIframe(mutationInQueue, builtNode);
this.newDocumentQueue = this.newDocumentQueue.filter(
(m) => m !== mutationInQueue,
);
}
const { documentElement, head } = this.iframe.contentDocument;
const { documentElement, head } = iframeDoc;
this.insertStyleRules(documentElement, head);
if (!this.service.state.matches('playing')) {
const iframeHtmlElement =
this.iframe.contentDocument.getElementsByTagName('html')[0];
const iframeHtmlElement = iframeDoc.getElementsByTagName('html')[0];

iframeHtmlElement && iframeHtmlElement.classList.add('rrweb-paused');
}
Expand Down Expand Up @@ -927,16 +928,20 @@ export class Replayer {
: this.mirror;
type TNode = typeof mirror extends Mirror ? Node : RRNode;
type TMirror = typeof mirror extends Mirror ? Mirror : RRDOMMirror;
const iframeContentDoc = getIFrameContentDocument(
iframeEl as HTMLIFrameElement,
);

const collected: AppendedIframe[] = [];
const afterAppend = (builtNode: Node, id: number) => {
this.collectIframeAndAttachDocument(collected, builtNode);
const sn = (mirror as TMirror).getMeta(builtNode as unknown as TNode);
if (
sn?.type === NodeType.Element &&
sn?.tagName.toUpperCase() === 'HTML'
sn?.tagName.toUpperCase() === 'HTML' &&
iframeContentDoc
) {
const { documentElement, head } = iframeEl.contentDocument!;
const { documentElement, head } = iframeContentDoc;
this.insertStyleRules(
documentElement as HTMLElement | RRElement,
head as HTMLElement | RRElement,
Expand All @@ -955,14 +960,14 @@ export class Replayer {
};

buildNodeWithSN(mutation.node, {
doc: iframeEl.contentDocument! as Document,
doc: iframeContentDoc as Document,
mirror: mirror as Mirror,
hackCss: true,
skipChild: false,
afterAppend,
cache: this.cache,
});
afterAppend(iframeEl.contentDocument! as Document, mutation.node.id);
afterAppend(iframeContentDoc as Document, mutation.node.id);

for (const { mutationInQueue, builtNode } of collected) {
this.attachDocumentToIframe(mutationInQueue, builtNode);
Expand Down Expand Up @@ -993,7 +998,8 @@ export class Replayer {
* pause when loading style sheet, resume when loaded all timeout exceed
*/
private waitForStylesheetLoad() {
const head = this.iframe.contentDocument?.head;
const iframeDoc = getIFrameContentDocument(this.iframe);
const head = iframeDoc?.head;
if (head) {
const unloadSheets: Set<HTMLLinkElement> = new Set();
let timer: ReturnType<typeof setTimeout> | -1;
Expand Down Expand Up @@ -1433,7 +1439,7 @@ export class Replayer {
: d.fontSource,
d.descriptors,
);
this.iframe.contentDocument?.fonts.add(fontFace);
getIFrameContentDocument(this.iframe)?.fonts.add(fontFace);
} catch (error) {
this.warn(error);
}
Expand All @@ -1460,7 +1466,10 @@ export class Replayer {
// Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events.
if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) {
this.usingVirtualDom = true;
buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom);
const iframeDoc = getIFrameContentDocument(this.iframe);
if (iframeDoc) {
buildFromDom(iframeDoc, this.mirror, this.virtualDom);
}
// If these legacy missing nodes haven't been resolved, they should be converted to virtual nodes.
if (Object.keys(this.legacy_missingNodeRetryMap).length) {
for (const key in this.legacy_missingNodeRetryMap) {
Expand Down Expand Up @@ -1559,7 +1568,8 @@ export class Replayer {
};

const appendNode = (mutation: addedNodeMutation) => {
if (!this.iframe.contentDocument) {
const iframeDoc = getIFrameContentDocument(this.iframe);
if (!iframeDoc) {
return this.warn('Looks like your replayer has been destroyed.');
}
let parent: Node | null | ShadowRoot | RRNode = mirror.getNode(
Expand Down Expand Up @@ -1601,7 +1611,7 @@ export class Replayer {
? mirror.getNode(mutation.node.rootId)
: this.usingVirtualDom
? this.virtualDom
: this.iframe.contentDocument;
: iframeDoc;
if (isSerializedIframe<typeof parent>(parent, mirror)) {
this.attachDocumentToIframe(
mutation,
Expand Down Expand Up @@ -1893,7 +1903,8 @@ export class Replayer {
return this.debugNodeNotFound(d, d.id);
}
const sn = this.mirror.getMeta(target);
if (target === this.iframe.contentDocument) {
const iframeDoc = getIFrameContentDocument(this.iframe);
if (target === iframeDoc) {
this.iframe.contentWindow?.scrollTo({
top: d.y,
left: d.x,
Expand Down Expand Up @@ -2235,7 +2246,8 @@ export class Replayer {
}

private hoverElements(el: Element) {
const rootElement = this.lastHoveredRootNode || this.iframe.contentDocument;
const iframeDoc = getIFrameContentDocument(this.iframe);
const rootElement = this.lastHoveredRootNode || iframeDoc;

// Sometimes this throws because `querySelectorAll` is not a function,
// unsure of value of rootElement when this occurs
Expand Down

0 comments on commit 048b9e7

Please sign in to comment.