Created
March 29, 2019 21:35
-
-
Save bradenmacdonald/46cb68df51fb5fa45e6411355fae242b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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