penguinmod-editor-2 / src /containers /extension-library.jsx
soiz1's picture
Upload 2891 files
6bcb42f verified
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 (
<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);