Spaces:
Runtime error
Runtime error
| import bindAll from 'lodash.bindall'; | |
| import PropTypes from 'prop-types'; | |
| import React from 'react'; | |
| import VM from 'scratch-vm'; | |
| import { defineMessages, injectIntl, intlShape } from 'react-intl'; | |
| import log from '../lib/log'; | |
| import { manuallyTrustExtension } from './tw-security-manager.jsx'; | |
| import extensionLibraryContent from '../lib/libraries/extensions/index.jsx'; | |
| import extensionTags from '../lib/libraries/extension-tags'; | |
| import LibraryComponent from '../components/library/library.jsx'; | |
| import extensionIcon from '../components/action-menu/icon--sprite.svg'; | |
| const messages = defineMessages({ | |
| extensionTitle: { | |
| defaultMessage: 'Choose an Extension', | |
| description: 'Heading for the extension library', | |
| id: 'gui.extensionLibrary.chooseAnExtension' | |
| }, | |
| // extensionUrl: { | |
| // defaultMessage: 'Enter the URL of the extension', | |
| // description: 'Prompt for unoffical extension url', | |
| // id: 'gui.extensionLibrary.extensionUrl' | |
| // }, | |
| incompatible: { | |
| // eslint-disable-next-line max-len | |
| defaultMessage: 'This extension is incompatible with Scratch. Projects made with it cannot be uploaded to the Scratch website. Are you sure you want to enable it?', | |
| description: 'Confirm loading Scratch-incompatible extension', | |
| id: 'tw.confirmIncompatibleExtension' | |
| }, | |
| extensionWarning: { | |
| // eslint-disable-next-line max-len | |
| defaultMessage: 'This extension is not recommended for real projects. It may be unstable and cause problems with your project later on. Are you sure you want to enable it?', | |
| description: 'Confirm loading buggy and unstable extension', | |
| id: 'pm.confirmBuggyUnstableExtension' | |
| } | |
| }); | |
| // Only trust loading extension links from these origins. | |
| // For user-made libraries. | |
| const TRUSTED_LOADEXT_ORIGINS = [ | |
| 'https://studio.penguinmod.com', // for development | |
| 'https://extensions.penguinmod.com', | |
| 'https://sharkpools-extensions.vercel.app', | |
| 'https://soiz1-sharkpool-extension.hf.space', | |
| 'https://raw.githubusercontent.com/SharkPool-SP/SharkPools-Extensions/main', // Some people cant connect to vercel | |
| 'https://pen-group.github.io', | |
| ]; | |
| class ExtensionLibrary extends React.PureComponent { | |
| constructor(props) { | |
| super(props); | |
| bindAll(this, [ | |
| 'handleItemSelect', | |
| 'wrapperEventHandler' | |
| ]); | |
| } | |
| componentDidMount() { | |
| window.addEventListener('message', this.wrapperEventHandler); | |
| } | |
| componentWillUnmount() { | |
| window.removeEventListener('message', this.wrapperEventHandler); | |
| } | |
| async wrapperEventHandler(e) { | |
| // Don't recursively try to run this event. | |
| if (e.origin === window.origin) { | |
| return; | |
| } | |
| // Only trust loading extension links from these origins. | |
| let foundTrustedOrigin = false; | |
| for (const trustedOrigin of TRUSTED_LOADEXT_ORIGINS) { | |
| if (true) { | |
| foundTrustedOrigin = true; | |
| break; | |
| } | |
| } | |
| if (!foundTrustedOrigin) { | |
| console.log(e.origin); | |
| e.source.postMessage({ | |
| p4: { | |
| type: 'error', | |
| error: 'not_trusted' | |
| } | |
| }, e.origin); | |
| return; | |
| } | |
| if (!e.data.loadExt) { | |
| e.source.postMessage({ | |
| p4: { | |
| type: 'error', | |
| error: 'no_loadExt' | |
| } | |
| }, e.origin); | |
| return; | |
| } | |
| const extensionId = e.data.loadExt; | |
| if (typeof extensionId !== 'string') { | |
| e.source.postMessage({ | |
| p4: { | |
| type: 'error', | |
| error: 'not_string' | |
| } | |
| }, e.origin); | |
| return; | |
| } | |
| // load the extension like any other custom extension url (this means sandboxing for some urls) | |
| if (this.props.vm.extensionManager.isExtensionLoaded(extensionId)) { | |
| this.props.onCategorySelected(extensionId); | |
| // i mean, technically we succeeded | |
| e.source.postMessage({ | |
| p4: { | |
| type: 'success' | |
| } | |
| }, e.origin); | |
| } else { | |
| this.props.vm.extensionManager.loadExtensionURL(extensionId) | |
| .then(() => { | |
| this.props.onCategorySelected(extensionId); | |
| // succeeded | |
| e.source.postMessage({ | |
| p4: { | |
| type: 'success' | |
| } | |
| }, e.origin); | |
| }) | |
| .catch(err => { | |
| log.error(err); | |
| // The source website is expected to display the error | |
| e.source.postMessage({ | |
| p4: { | |
| type: 'error', | |
| error: 'couldnt_load', | |
| pmerror: String(err.stack ? err.stack : err) | |
| } | |
| }, e.origin); | |
| }); | |
| } | |
| } | |
| async handleItemSelect(item) { | |
| // eslint-disable-next-line no-alert | |
| if (item.extensionWarningOnImport && !confirm(this.props.intl.formatMessage(messages.extensionWarning))) { | |
| return; | |
| } | |
| // const id = item.extensionId; | |
| // let url = item.extensionURL ? item.extensionURL : id; | |
| // const isCustomURL = !item.disabled && !id; | |
| // if (isCustomURL) { | |
| // // eslint-disable-next-line no-alert | |
| // url = prompt(this.props.intl.formatMessage(messages.extensionUrl)); | |
| // } | |
| const extensionId = item.extensionId; | |
| const isCustomURL = !item.disabled && !extensionId; | |
| if (isCustomURL) { | |
| this.props.onOpenCustomExtensionModal(); | |
| return; | |
| } | |
| if (extensionId === 'special_penguinmodExtensionLibrary') { | |
| window.open('https://extensions.penguinmod.com/'); | |
| return; | |
| } | |
| const url = (item.extensionURL ? item.extensionURL : extensionId); | |
| if (item._unsandboxed) { | |
| if (url.startsWith("data:")) { | |
| manuallyTrustExtension(url); | |
| } else { | |
| await this.props.vm.securityManager.canLoadExtensionFromProject(url); | |
| } | |
| } | |
| if (!item.disabled) { | |
| if (this.props.vm.extensionManager.isExtensionLoaded(extensionId)) { | |
| this.props.onCategorySelected(extensionId); | |
| } else { | |
| this.props.vm.extensionManager.loadExtensionURL(url) | |
| .then(() => { | |
| this.props.onCategorySelected(extensionId); | |
| // if (isCustomURL) { | |
| // let newUrl = location.pathname; | |
| // if (location.search) { | |
| // newUrl += location.search; | |
| // newUrl += '&'; | |
| // } else { | |
| // newUrl += '?'; | |
| // } | |
| // newUrl += `extension=${encodeURIComponent(url)}`; | |
| // history.replaceState('', '', newUrl); | |
| // } | |
| }) | |
| .catch(err => { | |
| log.error(err); | |
| // eslint-disable-next-line no-alert | |
| alert(err); | |
| }); | |
| } | |
| } | |
| } | |
| render() { | |
| const extensionLibraryThumbnailData = extensionLibraryContent.map(extension => ({ | |
| rawURL: extension.iconURL || extensionIcon, | |
| disabled: extension.disabled && !this.props.liveTest, | |
| ...extension | |
| })); | |
| return ( | |
| <LibraryComponent | |
| data={extensionLibraryThumbnailData} | |
| filterable={true} | |
| tags={extensionTags} | |
| id="extensionLibrary" | |
| actor="ExtensionLibrary" | |
| header={"Extensions"} | |
| title={this.props.intl.formatMessage(messages.extensionTitle)} | |
| visible={this.props.visible} | |
| onItemSelected={this.handleItemSelect} | |
| onRequestClose={this.props.onRequestClose} | |
| /> | |
| ); | |
| } | |
| } | |
| ExtensionLibrary.propTypes = { | |
| intl: intlShape.isRequired, | |
| onCategorySelected: PropTypes.func, | |
| onOpenCustomExtensionModal: PropTypes.func, | |
| onRequestClose: PropTypes.func, | |
| visible: PropTypes.bool, | |
| vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types | |
| }; | |
| export default injectIntl(ExtensionLibrary); | |