Skip to content

Instantly share code, notes, and snippets.

@bradenmacdonald
Created March 29, 2019 21:35
Show Gist options
  • Save bradenmacdonald/46cb68df51fb5fa45e6411355fae242b to your computer and use it in GitHub Desktop.
Save bradenmacdonald/46cb68df51fb5fa45e6411355fae242b to your computer and use it in GitHub Desktop.
import { bind } from 'bind-decorator';
import * as React from 'react';
import { XBlocksApi } from 'global/api';
import styles from 'global/styles';
import uiMessages from 'ui/components/displayMessages';
import { WrappedMessage } from 'utils';
import messages from '../../displayMessages';
import { wrapBlockHtmlForIFrame } from './wrap';
declare const SECURE_ORIGIN_XBLOCK_BOOTSTRAP_HTML_URL: string;
interface BlockProps {
usageKey: string;
}
const enum BlockLoadingState {
LOADING,
READY,
ERROR,
}
interface BlockState {
loadingState: BlockLoadingState;
initialHtml: string;
iFrameHeight: number;
}
/**
* React component that displays an XBlock in a sandboxed IFrame.
*
* The IFrame is resized responsively so that it fits the content height.
*
* We use an IFrame so that the XBlock code, including user-authored HTML,
* cannot access things like the user's cookies, nor can it make GET/POST
* requests as the user. However, it is allowed to call any XBlock handlers.
*/
export class Block extends React.Component<BlockProps, BlockState> {
private iframeRef: React.RefObject<HTMLIFrameElement>;
constructor(props: BlockProps) {
super(props);
this.iframeRef = React.createRef();
this.state = {
iFrameHeight: 400,
initialHtml: '',
loadingState: BlockLoadingState.LOADING,
};
}
/**
* Load the XBlock data from the LMS and then inject it into our IFrame.
*/
public async componentDidMount() {
try {
// First load the XBlock fragment data:
const data = await XBlocksApi.view({id: this.props.usageKey, viewName: 'student_view'});
const urlResources = data.resources.filter((r) => r.kind === 'url');
const html = wrapBlockHtmlForIFrame(
data.content,
urlResources.filter((r) => r.mimetype === 'application/javascript').map((r) => r.data),
urlResources.filter((r) => r.mimetype === 'text/css').map((r) => r.data),
);
// Prepare to receive messages from the IFrame.
// Messages are the only way that the code in the IFrame can communicate
// with the surrounding frontend UI.
window.addEventListener('message', this.receivedWindowMessage);
// Load the XBlock HTML into the IFrame:
this.setState({
initialHtml: html,
loadingState: BlockLoadingState.READY,
});
} catch (err) {
console.error(err); // tslint:disable-line:no-console
this.setState({loadingState: BlockLoadingState.ERROR});
}
}
public componentWillUnmount() {
window.removeEventListener('message', this.receivedWindowMessage);
}
public render() {
if (this.state.loadingState === BlockLoadingState.READY) {
return (
<div className={styles.lxBlock} style={{height: `${this.state.iFrameHeight}px`}}>
<iframe ref={this.iframeRef} src={SECURE_ORIGIN_XBLOCK_BOOTSTRAP_HTML_URL} sandbox={[
'allow-forms',
'allow-modals',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin', // This is only secure IF the IFrame source
// is served from a completely different domain name
// e.g. labxchange-xblocks.net vs www.labxchange.org
'allow-scripts',
'allow-top-navigation-by-user-activation',
].join(' ')}></iframe>
</div>
);
} else if (this.state.loadingState === BlockLoadingState.LOADING) {
return (<div>
<WrappedMessage message={uiMessages.uiLoading} />
</div>);
} else {
return (<div>
<WrappedMessage message={messages.libraryContentError} />
</div>);
}
}
/**
* Handle any messages we receive from the XBlock Runtime code in the IFrame.
* See wrap.ts to see the code that sends these messages.
*/
@bind private async receivedWindowMessage(event: MessageEvent) {
if (this.iframeRef.current === null || event.source !== this.iframeRef.current.contentWindow) {
return; // This is some other random message.
}
const method = event.data.method;
const frame = this.iframeRef.current.contentWindow!;
const replyKey = event.data.replyKey;
const sendReply = async (data: any) => {
frame.postMessage({...data, replyKey}, '*');
};
if (method === 'bootstrap') {
sendReply({initialHtml: this.state.initialHtml});
} else if (method === 'get_handler_url') {
sendReply({
handlerUrl: await this.getSecureHandlerUrl(event.data.usageId),
});
} else if (method === 'update_frame_height') {
this.setState({iFrameHeight: event.data.height});
}
}
/**
* Helper method which gets a "secure handler URL" from the backend
* A "secure handler URL" is a URL that the XBlock runtime can use even from
* within its sandboxed IFrame. (The IFrame is considered a different origin,
* and normally, cross-origin handler requests would be blocked).
*
* @param uageKey The usage key of the XBlock whose handlers you want to call.
*/
private async getSecureHandlerUrl(usageKey: string) {
const data = await XBlocksApi.handlerUrl({id: usageKey});
return data.handlerUrl;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment