import PropTypes from 'prop-types'; import React from 'react'; import bindAll from 'lodash.bindall'; import {connect} from 'react-redux'; import log from '../lib/log'; import CustomExtensionModalComponent from '../components/tw-custom-extension-modal/custom-extension-modal.jsx'; import {closeCustomExtensionModal} from '../reducers/modals'; import {manuallyTrustExtension, isTrustedExtension} from './tw-security-manager.jsx'; import {getPersistedUnsandboxed, setPersistedUnsandboxed} from '../lib/tw-persisted-unsandboxed.js'; /** * @param {Blob} blob Blob * @returns {Promise} data: uri */ const readAsDataURL = blob => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(new Error(`Could not read extension as data URL: ${reader.error}`)); reader.readAsDataURL(blob); }); class CustomExtensionModal extends React.Component { constructor (props) { super(props); bindAll(this, [ 'handleChangeFiles', 'handleChangeURL', 'handleClose', 'handleKeyDown', 'handleLoadExtension', 'handleSwitchToFile', 'handleSwitchToURL', 'handleSwitchToText', 'handleChangeText', 'handleDragOver', 'handleDragLeave', 'handleDrop', 'handleChangeUnsandboxed' ]); this.state = { type: 'url', url: '', files: null, text: '', unsandboxed: getPersistedUnsandboxed() }; } /** * @returns {Promise} List of extension URLs to load. */ getExtensionURLs () { if (this.state.type === 'url') { return Promise.resolve([ this.state.url ]); } if (this.state.type === 'file') { const files = Array.from(this.state.files); return Promise.all(files.map(readAsDataURL)); } if (this.state.type === 'text') { return Promise.resolve([ `data:application/javascript,${encodeURIComponent(this.state.text)}` ]); } return Promise.reject(new Error('Unknown type')); } hasValidInput () { if (this.state.type === 'url') { try { const parsed = new URL(this.state.url); return ( parsed.protocol === 'https:' || parsed.protocol === 'http:' || parsed.protocol === 'data:' ); } catch (e) { return false; } } if (this.state.type === 'file') { return !!this.state.files; } if (this.state.type === 'text') { return !!this.state.text; } return false; } handleChangeFiles (files) { this.setState({ files }); } handleChangeURL (e) { this.setState({ url: e.target.value }); } handleClose () { this.props.onClose(); } handleKeyDown (e) { if (e.key === 'Enter' && this.hasValidInput()) { e.preventDefault(); this.handleLoadExtension(); } } async handleLoadExtension () { this.handleClose(); try { const urls = await this.getExtensionURLs(); if (this.state.type !== 'url') { setPersistedUnsandboxed(this.state.unsandboxed); if (this.state.unsandboxed) { for (const url of urls) { manuallyTrustExtension(url); } } } for (const url of urls) { await this.props.vm.extensionManager.loadExtensionURL(url); } } catch (err) { log.error(err); // eslint-disable-next-line no-alert alert(err); } } handleSwitchToFile () { this.setState({ type: 'file' }); } handleSwitchToURL () { this.setState({ type: 'url' }); } handleSwitchToText () { this.setState({ type: 'text' }); } handleChangeText (e) { this.setState({ text: e.target.value }); } handleDragOver (e) { if (e.dataTransfer.types.includes('Files')) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; } } handleDragLeave () { } handleDrop (e) { const files = e.dataTransfer.files; if (files.length) { e.preventDefault(); this.setState({ type: 'file', files }); } } isUnsandboxed () { if (this.state.type === 'url') { return isTrustedExtension(this.state.url); } return this.state.unsandboxed; } canChangeUnsandboxed () { return this.state.type !== 'url'; } handleChangeUnsandboxed (e) { this.setState({ unsandboxed: e.target.checked }); } render () { return ( ); } } CustomExtensionModal.propTypes = { onClose: PropTypes.func, vm: PropTypes.shape({ extensionManager: PropTypes.shape({ loadExtensionURL: PropTypes.func }) }) }; const mapStateToProps = state => ({ vm: state.scratchGui.vm }); const mapDispatchToProps = dispatch => ({ onClose: () => dispatch(closeCustomExtensionModal()) }); export default connect( mapStateToProps, mapDispatchToProps )(CustomExtensionModal);