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' } }); // 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://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 (e.origin.startsWith(trustedOrigin)) { 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.incompatibleWithScratch && !confirm(this.props.intl.formatMessage(messages.incompatible))) { // 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 ( ); } } 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);