penguinmod-editor-2 / src /lib /project-fetcher-hoc.jsx
soiz1's picture
Upload 2891 files
6bcb42f verified
import React from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl } from 'react-intl';
import bindAll from 'lodash.bindall';
import { connect } from 'react-redux';
import protobufBundle from './project.protobuf.json';
import protobuf from 'protobufjs';
import JSZip from 'jszip';
let protoRoot = protobuf.Root.fromJSON(protobufBundle);
let Project = protoRoot.lookupType('project.Project');
import { setProjectUnchanged } from '../reducers/project-changed';
import {
LoadingStates,
getIsCreatingNew,
getIsFetchingWithId,
getIsLoading,
getIsShowingProject,
onFetchedProjectData,
projectError,
setProjectId
} from '../reducers/project-state';
import {
activateTab,
BLOCKS_TAB_INDEX
} from '../reducers/editor-tab';
import log from './log';
import storage from './storage';
import { MISSING_PROJECT_ID } from './tw-missing-project';
import VM from 'scratch-vm';
import * as progressMonitor from '../components/loader/tw-progress-monitor';
// TW: Temporary hack for project tokens
const fetchProjectToken = projectId => {
if (projectId === '0') {
return Promise.resolve(null);
}
// Parse ?token=abcdef
const searchParams = new URLSearchParams(location.search);
if (searchParams.has('token')) {
return Promise.resolve(searchParams.get('token'));
}
// Parse #1?token=abcdef
const hashParams = new URLSearchParams(location.hash.split('?')[1]);
if (hashParams.has('token')) {
return Promise.resolve(hashParams.get('token'));
}
return fetch(`https://projects.penguinmod.com/api/v1/projects/getproject?projectID=${projectId}&requestType=metadata`)
.then(r => {
if (!r.ok) return null;
return r.json();
})
.then(dataOrNull => {
const token = dataOrNull ? dataOrNull.id : null;
return token;
})
.catch(err => {
log.error(err);
return null;
});
};
function protobufToJson(buffer) {
const message = Project.decode(buffer);
const json = Project.toObject(message);
const newJson = {
targets: [],
monitors: [],
extensionData: {},
extensions: json.extensions,
extensionURLs: {},
meta: {
semver: json.metaSemver,
vm: json.metaVm,
agent: json.metaAgent || ""
},
customFonts: json.fonts
};
for (const target of json.targets) {
let newTarget = {
isStage: target.isStage || false,
name: target.name,
variables: {},
lists: {},
broadcasts: {},
customVars: [],
blocks: {},
comments: {},
currentCostume: target.currentCostume,
costumes: [],
sounds: [],
id: target.id,
volume: target.volume,
layerOrder: target.layerOrder,
tempo: target.tempo,
videoTransparency: target.videoTransparency,
videoState: target.videoState,
textToSpeechLanguage: target.textToSpeechLanguage || null,
visible: target.visible,
x: target.x,
y: target.y,
size: target.size,
direction: target.direction,
draggable: target.draggable,
rotationStyle: target.rotationStyle
};
if (newTarget.isStage) {
delete newTarget.visible, delete newTarget.size, delete newTarget.direction, delete newTarget.draggable, delete newTarget.rotationStyle;
}
for (const variable in target.variables) {
newTarget.variables[variable] = [target.variables[variable].name, target.variables[variable].value];
}
for (const list in target.lists) {
newTarget.lists[list] = [target.lists[list].name, target.lists[list].value || []];
}
for (const broadcast in target.broadcasts) {
newTarget.broadcasts[broadcast] = target.broadcasts[broadcast];
}
for (const customVar in target.customVars) {
newTarget.customVars.push(target.customVars[customVar]);
}
for (const block in target.blocks) {
if (target.blocks[block].is_variable_reporter) {
newTarget.blocks[block] = [
target.blocks[block].varReporterBlock.first_num,
target.blocks[block].varReporterBlock.name,
target.blocks[block].varReporterBlock.id,
target.blocks[block].varReporterBlock.second_num,
target.blocks[block].varReporterBlock.third_num,
]
continue;
}
newTarget.blocks[block] = {
opcode: target.blocks[block].opcode,
next: target.blocks[block].next || null,
parent: target.blocks[block].parent || null,
inputs: {},
fields: {},
shadow: target.blocks[block].shadow,
topLevel: target.blocks[block].topLevel,
x: target.blocks[block].x,
y: target.blocks[block].y
}
if (target.blocks[block].mutation) {
newTarget.blocks[block].mutation = {
tagName: target.blocks[block].mutation.tagName,
proccode: target.blocks[block].mutation.proccode,
argumentids: target.blocks[block].mutation.argumentids,
argumentnames: target.blocks[block].mutation.argumentnames,
argumentdefaults: target.blocks[block].mutation.argumentdefaults,
warp: target.blocks[block].mutation.warp,
returns: target.blocks[block].mutation._returns,
edited: target.blocks[block].mutation.edited,
optype: target.blocks[block].mutation.optype,
color: target.blocks[block].mutation.color,
hasnext: target.blocks[block].next ? true : false,
children: []
}
}
for (const input in target.blocks[block].inputs) {
newTarget.blocks[block].inputs[input] = JSON.parse(target.blocks[block].inputs[input]);
}
for (const field in target.blocks[block].fields) {
newTarget.blocks[block].fields[field] = JSON.parse(target.blocks[block].fields[field]);
}
}
for (const comment in target.comments) {
newTarget.comments[comment] = target.comments[comment];
}
for (const costume in target.costumes) {
newTarget.costumes[costume] = target.costumes[costume];
}
for (const sound in target.sounds) {
newTarget.sounds[sound] = target.sounds[sound];
}
newJson.targets.push(newTarget);
}
for (const monitor in json.monitors) {
let newMonitor = {
id: json.monitors[monitor].id,
mode: json.monitors[monitor].mode,
opcode: json.monitors[monitor].opcode,
params: json.monitors[monitor].params,
spriteName: json.monitors[monitor].spriteName || null,
value: json.monitors[monitor].value,
width: json.monitors[monitor].width,
height: json.monitors[monitor].height,
x: json.monitors[monitor].x,
y: json.monitors[monitor].y,
visible: json.monitors[monitor].visible,
sliderMin: json.monitors[monitor].sliderMin,
sliderMax: json.monitors[monitor].sliderMax,
isDiscrete: json.monitors[monitor].isDiscrete
}
newJson.monitors.push(newMonitor);
}
for (const extensionData in json.antiSigmaExtensionData) {
// "legacy" shit
newJson.extensionData[extensionData] = json.antiSigmaExtensionData[extensionData];
}
for (const extensionData in json.extensionData) {
if (json.extensionData[extensionData].parse) {
newJson.extensionData[extensionData] = JSON.parse(json.extensionData[extensionData].data);
} else {
newJson.extensionData[extensionData] = json.extensionData[extensionData].data;
}
}
for (const extensionURL in json.extensionURLs) {
newJson.extensionURLs[extensionURL] = json.extensionURLs[extensionURL];
}
return newJson;
}
/* Higher Order Component to provide behavior for loading projects by id. If
* there's no id, the default project is loaded.
* @param {React.Component} WrappedComponent component to receive projectData prop
* @returns {React.Component} component with project loading behavior
*/
const ProjectFetcherHOC = function (WrappedComponent) {
class ProjectFetcherComponent extends React.Component {
constructor(props) {
super(props);
bindAll(this, [
'fetchProject'
]);
storage.setProjectHost(props.projectHost);
storage.setProjectToken(props.projectToken);
storage.setAssetHost(props.assetHost);
storage.setTranslatorFunction(props.intl.formatMessage);
// props.projectId might be unset, in which case we use our default;
// or it may be set by an even higher HOC, and passed to us.
// Either way, we now know what the initial projectId should be, so
// set it in the redux store.
if (
props.projectId !== '' &&
props.projectId !== null &&
typeof props.projectId !== 'undefined'
) {
this.props.setProjectId(props.projectId.toString());
}
}
componentDidUpdate(prevProps) {
if (prevProps.projectHost !== this.props.projectHost) {
storage.setProjectHost(this.props.projectHost);
}
if (prevProps.projectToken !== this.props.projectToken) {
storage.setProjectToken(this.props.projectToken);
}
if (prevProps.assetHost !== this.props.assetHost) {
storage.setAssetHost(this.props.assetHost);
}
if (this.props.isFetchingWithId && !prevProps.isFetchingWithId) {
this.fetchProject(this.props.reduxProjectId, this.props.loadingState);
}
if (this.props.isShowingProject && !prevProps.isShowingProject) {
this.props.onProjectUnchanged();
}
if (this.props.isShowingProject && (prevProps.isLoadingProject || prevProps.isCreatingNew)) {
this.props.onActivateTab(BLOCKS_TAB_INDEX);
}
}
fetchProject(projectId, loadingState) {
// tw: clear and stop the VM before fetching
// these will also happen later after the project is fetched, but fetching may take a while and
// the project shouldn't be running while fetching the new project
this.props.vm.clear();
this.props.vm.stop();
let assetPromise;
// In case running in node...
let projectUrl = typeof URLSearchParams === 'undefined' ?
null :
new URLSearchParams(location.search).get('project_url');
if (projectUrl) {
if (!projectUrl.startsWith('http:') && !projectUrl.startsWith('https:')) {
projectUrl = `https://${projectUrl}`;
}
assetPromise = progressMonitor.fetchWithProgress(projectUrl)
.then(r => {
this.props.vm.runtime.renderer.setPrivateSkinAccess(false);
if (!r.ok) {
throw new Error(`Request returned status ${r.status}`);
}
return r.arrayBuffer();
})
.then(buffer => ({ data: buffer }));
} else {
// patch for default project
if (projectId === '0') {
storage.setProjectToken(projectId);
assetPromise = storage.load(storage.AssetType.Project, projectId, storage.DataFormat.JSON);
} else {
projectUrl = `https://projects.penguinmod.com/api/v1/projects/getprojectwrapper?safe=true&projectId=${projectId}`
assetPromise = progressMonitor.fetchWithProgress(projectUrl)
.then(async r => {
this.props.vm.runtime.renderer.setPrivateSkinAccess(false);
if (!r.ok) {
throw new Error(`Request returned status ${r.status}`);
}
const project = await r.json();
const json = protobufToJson(new Uint8Array(project.project.data));
// now get the assets
let zip = new JSZip();
zip.file("project.json", JSON.stringify(json));
for (const asset of project.assets) {
zip.file(asset.id, new Uint8Array(asset.buffer.data).buffer);
}
const arrayBuffer = await zip.generateAsync({ type: "arraybuffer" });
return arrayBuffer;
})
.then(buffer => ({ data: buffer }))
.catch(error => {
console.log(error)
})
}
}
return assetPromise
.then(projectAsset => {
// tw: If the project data appears to be HTML, then the result is probably an nginx 404 page,
// and the "missing project" project should be loaded instead.
// See: https://projects.scratch.mit.edu/9999999999999999999999
if (projectAsset && projectAsset.data) {
const firstChar = projectAsset.data[0];
if (firstChar === '<' || firstChar === '<'.charCodeAt(0)) {
return storage.load(storage.AssetType.Project, MISSING_PROJECT_ID, storage.DataFormat.JSON);
}
}
return projectAsset;
})
.then(projectAsset => {
if (projectAsset) {
this.props.onFetchedProjectData(projectAsset.data, loadingState);
} else {
// pm: Failed to grab data, use the "fetch" API as a backup
// we shouldnt be interrupted by the fetch replacement in tw-progress-monitor
// as it uses projects.scratch.mit.edu still
fetch(projectUrl).then(async res => {
if (!res.ok) {
// Treat failure to load as an error
// Throw to be caught by catch later on
throw new Error('Could not find project; ' + projectUrl);
}
const project = await res.json();
const json = protobufToJson(new Uint8Array(project.project.data));
// now get the assets
let zip = new JSZip();
zip.file("project.json", JSON.stringify(json));
for (const asset of project.assets) {
zip.file(asset.id, new Uint8Array(asset.buffer.data).buffer);
}
const arrayBuffer = await zip.generateAsync({ type: "arraybuffer" });
this.props.onFetchedProjectData(arrayBuffer, loadingState);
}).catch(err => {
throw new Error('Could not find project; ' + err);
})
}
})
.catch(err => {
this.props.onError(err);
log.error(err);
});
}
render() {
const {
/* eslint-disable no-unused-vars */
assetHost,
intl,
isLoadingProject: isLoadingProjectProp,
loadingState,
onActivateTab,
onError: onErrorProp,
onFetchedProjectData: onFetchedProjectDataProp,
onProjectUnchanged,
projectHost,
projectId,
reduxProjectId,
setProjectId: setProjectIdProp,
/* eslint-enable no-unused-vars */
isFetchingWithId: isFetchingWithIdProp,
...componentProps
} = this.props;
return (
<WrappedComponent
fetchingProject={isFetchingWithIdProp}
{...componentProps}
/>
);
}
}
ProjectFetcherComponent.propTypes = {
assetHost: PropTypes.string,
canSave: PropTypes.bool,
intl: intlShape.isRequired,
isCreatingNew: PropTypes.bool,
isFetchingWithId: PropTypes.bool,
isLoadingProject: PropTypes.bool,
isShowingProject: PropTypes.bool,
loadingState: PropTypes.oneOf(LoadingStates),
onActivateTab: PropTypes.func,
onError: PropTypes.func,
onFetchedProjectData: PropTypes.func,
onProjectUnchanged: PropTypes.func,
projectHost: PropTypes.string,
projectToken: PropTypes.string,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
setProjectId: PropTypes.func,
vm: PropTypes.instanceOf(VM)
};
ProjectFetcherComponent.defaultProps = {
assetHost: 'https://assets.scratch.mit.edu',
projectHost: 'https://projects.scratch.mit.edu'
};
const mapStateToProps = state => ({
isCreatingNew: getIsCreatingNew(state.scratchGui.projectState.loadingState),
isFetchingWithId: getIsFetchingWithId(state.scratchGui.projectState.loadingState),
isLoadingProject: getIsLoading(state.scratchGui.projectState.loadingState),
isShowingProject: getIsShowingProject(state.scratchGui.projectState.loadingState),
loadingState: state.scratchGui.projectState.loadingState,
reduxProjectId: state.scratchGui.projectState.projectId,
vm: state.scratchGui.vm
});
const mapDispatchToProps = dispatch => ({
onActivateTab: tab => dispatch(activateTab(tab)),
onError: error => dispatch(projectError(error)),
onFetchedProjectData: (projectData, loadingState) =>
dispatch(onFetchedProjectData(projectData, loadingState)),
setProjectId: projectId => dispatch(setProjectId(projectId)),
onProjectUnchanged: () => dispatch(setProjectUnchanged())
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
{}, stateProps, dispatchProps, ownProps
);
return injectIntl(connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(ProjectFetcherComponent));
};
export {
ProjectFetcherHOC as default
};