Merge pull request #10200 from BerkeleyTrue/fix/code-uri
Fix(challenges): code storage/uri
This commit is contained in:
@ -1,51 +1,148 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
|
import { removeCodeUri, getCodeUri } from '../utils/code-uri';
|
||||||
|
import { ofType } from '../../common/utils/get-actions-of-type';
|
||||||
|
import { updateContents } from '../../common/utils/polyvinyl';
|
||||||
|
import combineSagas from '../../common/utils/combine-sagas';
|
||||||
|
|
||||||
|
import { userSelector } from '../../common/app/redux/selectors';
|
||||||
|
import { makeToast } from '../../common/app/toasts/redux/actions';
|
||||||
import types from '../../common/app/routes/challenges/redux/types';
|
import types from '../../common/app/routes/challenges/redux/types';
|
||||||
import {
|
import {
|
||||||
savedCodeFound
|
savedCodeFound,
|
||||||
|
updateMain,
|
||||||
|
lockUntrustedCode
|
||||||
} from '../../common/app/routes/challenges/redux/actions';
|
} from '../../common/app/routes/challenges/redux/actions';
|
||||||
|
|
||||||
const legecyPrefixes = [
|
const legacyPrefixes = [
|
||||||
'Bonfire: ',
|
'Bonfire: ',
|
||||||
'Waypoint: ',
|
'Waypoint: ',
|
||||||
'Zipline: ',
|
'Zipline: ',
|
||||||
'Basejump: ',
|
'Basejump: ',
|
||||||
'Checkpoint: '
|
'Checkpoint: '
|
||||||
];
|
];
|
||||||
|
const legacyPostfix = 'Val';
|
||||||
|
|
||||||
function getCode(id, legacy) {
|
function getCode(id) {
|
||||||
if (store.has(id)) {
|
if (store.has(id)) {
|
||||||
return store.get(id);
|
return store.get(id);
|
||||||
}
|
}
|
||||||
if (store.has(legacy)) {
|
return null;
|
||||||
const code = '' + store.get(legacy);
|
}
|
||||||
store.remove(legacy);
|
|
||||||
|
function getLegacyCode(legacy) {
|
||||||
|
const key = legacy + legacyPostfix;
|
||||||
|
let code = null;
|
||||||
|
if (store.has(key)) {
|
||||||
|
code = '' + store.get(key);
|
||||||
|
store.remove(key);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
return legecyPrefixes.reduce((code, prefix) => {
|
return legacyPrefixes.reduce((code, prefix) => {
|
||||||
if (code) {
|
if (code) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
return store.get(prefix + legacy + 'Val');
|
return store.get(prefix + key);
|
||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function codeStorageSaga(actions$, getState) {
|
function legacyToFile(code, files, key) {
|
||||||
return actions$
|
return { [key]: updateContents(code, files[key]) };
|
||||||
.filter(({ type }) => (
|
}
|
||||||
type === types.saveCode ||
|
|
||||||
type === types.loadCode
|
export function clearCodeSaga(actions, getState) {
|
||||||
))
|
return actions
|
||||||
.map(({ type }) => {
|
::ofType(types.clearSavedCode)
|
||||||
const { id = '', files = {}, legacyKey = '' } = getState().challengesApp;
|
.map(() => {
|
||||||
if (type === types.saveCode) {
|
const { challengesApp: { id = '' } } = getState();
|
||||||
store.set(id, files);
|
store.remove(id);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const codeFound = getCode(id, legacyKey);
|
|
||||||
if (codeFound) {
|
|
||||||
return savedCodeFound(codeFound);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export function saveCodeSaga(actions, getState) {
|
||||||
|
return actions
|
||||||
|
::ofType(types.saveCode)
|
||||||
|
// do not save challenge if code is locked
|
||||||
|
.filter(() => !getState().challengesApp.isCodeLocked)
|
||||||
|
.map(() => {
|
||||||
|
const { challengesApp: { id = '', files = {} } } = getState();
|
||||||
|
store.set(id, files);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadCodeSaga(actions$, getState, { window, location }) {
|
||||||
|
return actions$
|
||||||
|
::ofType(types.loadCode)
|
||||||
|
.flatMap(() => {
|
||||||
|
let finalFiles;
|
||||||
|
const state = getState();
|
||||||
|
const { user } = userSelector(state);
|
||||||
|
const {
|
||||||
|
challengesApp: {
|
||||||
|
id = '',
|
||||||
|
files = {},
|
||||||
|
legacyKey = '',
|
||||||
|
key
|
||||||
|
}
|
||||||
|
} = state;
|
||||||
|
const codeUriFound = getCodeUri(
|
||||||
|
location,
|
||||||
|
window.decodeURIComponent
|
||||||
|
);
|
||||||
|
if (codeUriFound) {
|
||||||
|
finalFiles = legacyToFile(codeUriFound, files, key);
|
||||||
|
removeCodeUri(location, window.history);
|
||||||
|
return Observable.of(
|
||||||
|
lockUntrustedCode(),
|
||||||
|
makeToast({
|
||||||
|
message: 'I found code in the URI. Loading now.'
|
||||||
|
}),
|
||||||
|
savedCodeFound(finalFiles)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeFound = getCode(id);
|
||||||
|
if (codeFound) {
|
||||||
|
finalFiles = codeFound;
|
||||||
|
} else {
|
||||||
|
const legacyCode = getLegacyCode(legacyKey);
|
||||||
|
if (legacyCode) {
|
||||||
|
finalFiles = legacyToFile(legacyCode, files, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalFiles) {
|
||||||
|
return Observable.of(
|
||||||
|
makeToast({
|
||||||
|
message: 'I found some saved work. Loading now.'
|
||||||
|
}),
|
||||||
|
savedCodeFound(finalFiles),
|
||||||
|
updateMain()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.challengeMap && user.challengeMap[id]) {
|
||||||
|
const userChallenge = user.challengeMap[id];
|
||||||
|
if (userChallenge.files) {
|
||||||
|
finalFiles = userChallenge.files;
|
||||||
|
} else if (userChallenge.solution) {
|
||||||
|
finalFiles = legacyToFile(userChallenge.solution, files, key);
|
||||||
|
}
|
||||||
|
if (finalFiles) {
|
||||||
|
return Observable.of(
|
||||||
|
makeToast({
|
||||||
|
message: 'I found a previous solved solution. Loading now.'
|
||||||
|
}),
|
||||||
|
savedCodeFound(finalFiles),
|
||||||
|
updateMain()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default combineSagas(saveCodeSaga, loadCodeSaga, clearCodeSaga);
|
||||||
|
@ -91,6 +91,8 @@ export default function executeChallengeSaga(action$, getState) {
|
|||||||
type === types.executeChallenge ||
|
type === types.executeChallenge ||
|
||||||
type === types.updateMain
|
type === types.updateMain
|
||||||
))
|
))
|
||||||
|
// if isCodeLockedTrue do not run challenges
|
||||||
|
.filter(() => !getState().challengesApp.isCodeLocked)
|
||||||
.debounce(750)
|
.debounce(750)
|
||||||
.flatMapLatest(({ type }) => {
|
.flatMapLatest(({ type }) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -2,6 +2,7 @@ import Rx, { Observable, Subject } from 'rx';
|
|||||||
/* eslint-disable import/no-unresolved */
|
/* eslint-disable import/no-unresolved */
|
||||||
import loopProtect from 'loop-protect';
|
import loopProtect from 'loop-protect';
|
||||||
/* eslint-enable import/no-unresolved */
|
/* eslint-enable import/no-unresolved */
|
||||||
|
import { ofType } from '../../common/utils/get-actions-of-type';
|
||||||
import types from '../../common/app/routes/challenges/redux/types';
|
import types from '../../common/app/routes/challenges/redux/types';
|
||||||
import {
|
import {
|
||||||
updateOutput,
|
updateOutput,
|
||||||
@ -89,12 +90,13 @@ export default function frameSaga(actions$, getState, { window, document }) {
|
|||||||
const proxyLogger$ = new Subject();
|
const proxyLogger$ = new Subject();
|
||||||
const runTests$ = window.__common[testId + 'Ready$'] =
|
const runTests$ = window.__common[testId + 'Ready$'] =
|
||||||
new Subject();
|
new Subject();
|
||||||
const result$ = actions$
|
const result$ = actions$::ofType(
|
||||||
.filter(({ type }) => (
|
types.frameMain,
|
||||||
type === types.frameMain ||
|
types.frameTests,
|
||||||
type === types.frameTests ||
|
types.frameOutput
|
||||||
type === types.frameOutput
|
)
|
||||||
))
|
// if isCodeLocked is true do not frame user code
|
||||||
|
.filter(() => !getState().challengesApp.isCodeLocked)
|
||||||
.map(action => {
|
.map(action => {
|
||||||
if (action.type === types.frameMain) {
|
if (action.type === types.frameMain) {
|
||||||
return frameMain(action.payload, document, proxyLogger$);
|
return frameMain(action.payload, document, proxyLogger$);
|
||||||
|
79
client/utils/code-uri.js
Normal file
79
client/utils/code-uri.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import flow from 'lodash/flow';
|
||||||
|
import { decodeFcc } from '../../common/utils/encode-decode';
|
||||||
|
|
||||||
|
const queryRegex = /^(\?|#\?)/;
|
||||||
|
export function legacyIsInQuery(query, decode) {
|
||||||
|
let decoded;
|
||||||
|
try {
|
||||||
|
decoded = decode(query);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!decoded || typeof decoded.split !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return decoded
|
||||||
|
.replace(queryRegex, '')
|
||||||
|
.split('&')
|
||||||
|
.reduce(function(found, param) {
|
||||||
|
var key = param.split('=')[0];
|
||||||
|
if (key === 'solution') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKeyInQuery(query, keyToFind = '') {
|
||||||
|
return query
|
||||||
|
.split('&')
|
||||||
|
.reduce((oldValue, param) => {
|
||||||
|
const key = param.split('=')[0];
|
||||||
|
const value = param
|
||||||
|
.split('=')
|
||||||
|
.slice(1)
|
||||||
|
.join('=');
|
||||||
|
|
||||||
|
if (key === keyToFind) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return oldValue;
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLegacySolutionFromQuery(query = '', decode) {
|
||||||
|
return flow(
|
||||||
|
getKeyInQuery,
|
||||||
|
decode,
|
||||||
|
decodeFcc
|
||||||
|
)(query, 'solution');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCodeUri(location, decodeURIComponent) {
|
||||||
|
let query;
|
||||||
|
if (
|
||||||
|
location.search &&
|
||||||
|
legacyIsInQuery(location.search, decodeURIComponent)
|
||||||
|
) {
|
||||||
|
query = location.search.replace(/^\?/, '');
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getLegacySolutionFromQuery(query, decodeURIComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCodeUri(location, history) {
|
||||||
|
if (
|
||||||
|
typeof location.href.split !== 'function' ||
|
||||||
|
typeof history.replaceState !== 'function'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
history.replaceState(
|
||||||
|
history.state,
|
||||||
|
null,
|
||||||
|
location.href.split('?')[0]
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
@ -7,8 +7,8 @@ export default React.createClass({
|
|||||||
'aria-controls': React.PropTypes.string,
|
'aria-controls': React.PropTypes.string,
|
||||||
className: React.PropTypes.string,
|
className: React.PropTypes.string,
|
||||||
href: React.PropTypes.string,
|
href: React.PropTypes.string,
|
||||||
onClick: React.PropTypes.func.isRequired,
|
onClick: React.PropTypes.func,
|
||||||
points: React.PropTypes.func,
|
points: React.PropTypes.number,
|
||||||
title: React.PropTypes.node
|
title: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { updateCurrentChallenge } from '../routes/challenges/redux/actions';
|
import { updateCurrentChallenge } from '../routes/challenges/redux/actions';
|
||||||
import getActionsOfType from '../../utils/get-actions-of-type';
|
import getActionsOfType from '../../utils/get-actions-of-type';
|
||||||
import combineSagas from '../utils/combine-sagas';
|
import combineSagas from '../../utils/combine-sagas';
|
||||||
import { postJSON$ } from '../../utils/ajax-stream';
|
import { postJSON$ } from '../../utils/ajax-stream';
|
||||||
|
|
||||||
const log = debug('fcc:app/redux/load-current-challenge-saga');
|
const log = debug('fcc:app/redux/load-current-challenge-saga');
|
||||||
|
@ -11,22 +11,24 @@ import BugModal from '../Bug-Modal.jsx';
|
|||||||
import { challengeSelector } from '../../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
import {
|
import {
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
updateMain,
|
|
||||||
updateFile,
|
updateFile,
|
||||||
loadCode
|
loadCode
|
||||||
} from '../../redux/actions';
|
} from '../../redux/actions';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
|
state => state.challengesApp.id,
|
||||||
state => state.challengesApp.tests,
|
state => state.challengesApp.tests,
|
||||||
state => state.challengesApp.files,
|
state => state.challengesApp.files,
|
||||||
state => state.challengesApp.key,
|
state => state.challengesApp.key,
|
||||||
(
|
(
|
||||||
{ showPreview, mode },
|
{ showPreview, mode },
|
||||||
|
id,
|
||||||
tests,
|
tests,
|
||||||
files = {},
|
files = {},
|
||||||
key = ''
|
key = ''
|
||||||
) => ({
|
) => ({
|
||||||
|
id,
|
||||||
content: files[key] && files[key].contents || '',
|
content: files[key] && files[key].contents || '',
|
||||||
file: files[key],
|
file: files[key],
|
||||||
showPreview,
|
showPreview,
|
||||||
@ -38,7 +40,6 @@ const mapStateToProps = createSelector(
|
|||||||
const bindableActions = {
|
const bindableActions = {
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
updateFile,
|
updateFile,
|
||||||
updateMain,
|
|
||||||
loadCode
|
loadCode
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,18 +47,23 @@ export class Challenge extends PureComponent {
|
|||||||
static displayName = 'Challenge';
|
static displayName = 'Challenge';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
showPreview: PropTypes.bool,
|
showPreview: PropTypes.bool,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
mode: PropTypes.string,
|
mode: PropTypes.string,
|
||||||
updateFile: PropTypes.func,
|
updateFile: PropTypes.func,
|
||||||
executeChallenge: PropTypes.func,
|
executeChallenge: PropTypes.func,
|
||||||
updateMain: PropTypes.func,
|
|
||||||
loadCode: PropTypes.func
|
loadCode: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadCode();
|
this.props.loadCode();
|
||||||
this.props.updateMain();
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (this.props.id !== nextProps.id) {
|
||||||
|
this.props.loadCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPreview(showPreview) {
|
renderPreview(showPreview) {
|
||||||
|
@ -12,7 +12,8 @@ import { challengeSelector } from '../../redux/selectors';
|
|||||||
import {
|
import {
|
||||||
openBugModal,
|
openBugModal,
|
||||||
updateHint,
|
updateHint,
|
||||||
executeChallenge
|
executeChallenge,
|
||||||
|
unlockUntrustedCode
|
||||||
} from '../../redux/actions';
|
} from '../../redux/actions';
|
||||||
import { makeToast } from '../../../../toasts/redux/actions';
|
import { makeToast } from '../../../../toasts/redux/actions';
|
||||||
import { toggleHelpChat } from '../../../../redux/actions';
|
import { toggleHelpChat } from '../../../../redux/actions';
|
||||||
@ -22,7 +23,8 @@ const bindableActions = {
|
|||||||
executeChallenge,
|
executeChallenge,
|
||||||
updateHint,
|
updateHint,
|
||||||
toggleHelpChat,
|
toggleHelpChat,
|
||||||
openBugModal
|
openBugModal,
|
||||||
|
unlockUntrustedCode
|
||||||
};
|
};
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
@ -31,20 +33,23 @@ const mapStateToProps = createSelector(
|
|||||||
state => state.challengesApp.tests,
|
state => state.challengesApp.tests,
|
||||||
state => state.challengesApp.output,
|
state => state.challengesApp.output,
|
||||||
state => state.challengesApp.hintIndex,
|
state => state.challengesApp.hintIndex,
|
||||||
|
state => state.challengesApp.isCodeLocked,
|
||||||
(
|
(
|
||||||
{ challenge: { title, description, hints = [] } = {} },
|
{ challenge: { title, description, hints = [] } = {} },
|
||||||
windowHeight,
|
windowHeight,
|
||||||
navHeight,
|
navHeight,
|
||||||
tests,
|
tests,
|
||||||
output,
|
output,
|
||||||
hintIndex
|
hintIndex,
|
||||||
|
isCodeLocked
|
||||||
) => ({
|
) => ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
height: windowHeight - navHeight - 20,
|
height: windowHeight - navHeight - 20,
|
||||||
tests,
|
tests,
|
||||||
output,
|
output,
|
||||||
hint: hints[hintIndex]
|
hint: hints[hintIndex],
|
||||||
|
isCodeLocked
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -65,7 +70,8 @@ export class SidePanel extends PureComponent {
|
|||||||
updateHint: PropTypes.func,
|
updateHint: PropTypes.func,
|
||||||
makeToast: PropTypes.func,
|
makeToast: PropTypes.func,
|
||||||
toggleHelpChat: PropTypes.func,
|
toggleHelpChat: PropTypes.func,
|
||||||
openBugModal: PropTypes.func
|
openBugModal: PropTypes.func,
|
||||||
|
unlockUntrustedCode: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
||||||
@ -106,7 +112,9 @@ export class SidePanel extends PureComponent {
|
|||||||
updateHint,
|
updateHint,
|
||||||
makeToast,
|
makeToast,
|
||||||
toggleHelpChat,
|
toggleHelpChat,
|
||||||
openBugModal
|
openBugModal,
|
||||||
|
isCodeLocked,
|
||||||
|
unlockUntrustedCode
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const style = {};
|
const style = {};
|
||||||
if (height) {
|
if (height) {
|
||||||
@ -135,9 +143,11 @@ export class SidePanel extends PureComponent {
|
|||||||
<ToolPanel
|
<ToolPanel
|
||||||
executeChallenge={ executeChallenge }
|
executeChallenge={ executeChallenge }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
|
isCodeLocked={ isCodeLocked }
|
||||||
makeToast={ makeToast }
|
makeToast={ makeToast }
|
||||||
openBugModal={ openBugModal }
|
openBugModal={ openBugModal }
|
||||||
toggleHelpChat={ toggleHelpChat }
|
toggleHelpChat={ toggleHelpChat }
|
||||||
|
unlockUntrustedCode={ unlockUntrustedCode }
|
||||||
updateHint={ updateHint }
|
updateHint={ updateHint }
|
||||||
/>
|
/>
|
||||||
<Output output={ output }/>
|
<Output output={ output }/>
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Button, ButtonGroup } from 'react-bootstrap';
|
import { Button, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
|
|
||||||
|
const unlockWarning = (
|
||||||
|
<Tooltip id='tooltip'>
|
||||||
|
<h4>
|
||||||
|
<strong>Careful!</strong> Only run code you trust
|
||||||
|
</h4>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
export default class ToolPanel extends PureComponent {
|
export default class ToolPanel extends PureComponent {
|
||||||
constructor(...props) {
|
constructor(...props) {
|
||||||
super(...props);
|
super(...props);
|
||||||
@ -14,8 +21,10 @@ export default class ToolPanel extends PureComponent {
|
|||||||
executeChallenge: PropTypes.func,
|
executeChallenge: PropTypes.func,
|
||||||
updateHint: PropTypes.func,
|
updateHint: PropTypes.func,
|
||||||
hint: PropTypes.string,
|
hint: PropTypes.string,
|
||||||
|
isCodeLocked: PropTypes.bool,
|
||||||
toggleHelpChat: PropTypes.func,
|
toggleHelpChat: PropTypes.func,
|
||||||
openBugModal: PropTypes.func
|
openBugModal: PropTypes.func,
|
||||||
|
unlockUntrustedCode: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
makeHint() {
|
makeHint() {
|
||||||
@ -51,24 +60,55 @@ export default class ToolPanel extends PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderExecute(isCodeLocked, executeChallenge, unlockUntrustedCode) {
|
||||||
|
if (isCodeLocked) {
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
overlay={ unlockWarning }
|
||||||
|
placement='right'
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-big'
|
||||||
|
onClick={ unlockUntrustedCode }
|
||||||
|
>
|
||||||
|
Code Locked. Unlock?
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-big'
|
||||||
|
onClick={ executeChallenge }
|
||||||
|
>
|
||||||
|
Run tests (ctrl + enter)
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
hint,
|
hint,
|
||||||
|
isCodeLocked,
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
toggleHelpChat,
|
toggleHelpChat,
|
||||||
openBugModal
|
openBugModal,
|
||||||
|
unlockUntrustedCode
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ this.renderHint(hint, this.makeHint) }
|
{ this.renderHint(hint, this.makeHint) }
|
||||||
<Button
|
{
|
||||||
block={ true }
|
this.renderExecute(
|
||||||
bsStyle='primary'
|
isCodeLocked,
|
||||||
className='btn-big'
|
executeChallenge,
|
||||||
onClick={ executeChallenge }
|
unlockUntrustedCode
|
||||||
>
|
)
|
||||||
Run tests (ctrl + enter)
|
}
|
||||||
</Button>
|
|
||||||
<div className='button-spacer' />
|
<div className='button-spacer' />
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
className='input-group'
|
className='input-group'
|
||||||
|
@ -22,6 +22,11 @@ export const fetchChallengeCompleted = createAction(
|
|||||||
);
|
);
|
||||||
export const resetUi = createAction(types.resetUi);
|
export const resetUi = createAction(types.resetUi);
|
||||||
export const updateHint = createAction(types.updateHint);
|
export const updateHint = createAction(types.updateHint);
|
||||||
|
export const lockUntrustedCode = createAction(types.lockUntrustedCode);
|
||||||
|
export const unlockUntrustedCode = createAction(
|
||||||
|
types.unlockUntrustedCode,
|
||||||
|
() => null
|
||||||
|
);
|
||||||
|
|
||||||
export const fetchChallenges = createAction(types.fetchChallenges);
|
export const fetchChallenges = createAction(types.fetchChallenges);
|
||||||
export const fetchChallengesCompleted = createAction(
|
export const fetchChallengesCompleted = createAction(
|
||||||
@ -85,6 +90,7 @@ export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
|||||||
export const saveCode = createAction(types.saveCode);
|
export const saveCode = createAction(types.saveCode);
|
||||||
export const loadCode = createAction(types.loadCode);
|
export const loadCode = createAction(types.loadCode);
|
||||||
export const savedCodeFound = createAction(types.savedCodeFound);
|
export const savedCodeFound = createAction(types.savedCodeFound);
|
||||||
|
export const clearSavedCode = createAction(types.clearSavedCode);
|
||||||
|
|
||||||
|
|
||||||
// video challenges
|
// video challenges
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
import types from './types';
|
import types from './types';
|
||||||
import { moveToNextChallenge } from './actions';
|
import {
|
||||||
|
moveToNextChallenge,
|
||||||
|
clearSavedCode
|
||||||
|
} from './actions';
|
||||||
|
|
||||||
import { challengeSelector } from './selectors';
|
import { challengeSelector } from './selectors';
|
||||||
import { randomCompliment } from '../../../utils/get-words';
|
import { randomCompliment } from '../../../utils/get-words';
|
||||||
@ -24,7 +27,8 @@ function postChallenge(url, username, _csrf, challengeInfo) {
|
|||||||
updateUserChallenge(
|
updateUserChallenge(
|
||||||
username,
|
username,
|
||||||
{ ...challengeInfo, lastUpdated, completedDate }
|
{ ...challengeInfo, lastUpdated, completedDate }
|
||||||
)
|
),
|
||||||
|
clearSavedCode()
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(createErrorObservable);
|
.catch(createErrorObservable);
|
||||||
|
@ -45,6 +45,7 @@ const initialUiState = {
|
|||||||
shouldShowQuestions: false
|
shouldShowQuestions: false
|
||||||
};
|
};
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
isCodeLocked: false,
|
||||||
id: '',
|
id: '',
|
||||||
challenge: '',
|
challenge: '',
|
||||||
helpChatRoom: 'Help',
|
helpChatRoom: 'Help',
|
||||||
@ -88,6 +89,14 @@ const mainReducer = handleActions(
|
|||||||
0 :
|
0 :
|
||||||
state.hintIndex + 1
|
state.hintIndex + 1
|
||||||
}),
|
}),
|
||||||
|
[types.lockUntrustedCode]: state => ({
|
||||||
|
...state,
|
||||||
|
isCodeLocked: true
|
||||||
|
}),
|
||||||
|
[types.unlockUntrustedCode]: state => ({
|
||||||
|
...state,
|
||||||
|
isCodeLocked: false
|
||||||
|
}),
|
||||||
[types.executeChallenge]: state => ({
|
[types.executeChallenge]: state => ({
|
||||||
...state,
|
...state,
|
||||||
tests: state.tests.map(test => ({ ...test, err: false, pass: false }))
|
tests: state.tests.map(test => ({ ...test, err: false, pass: false }))
|
||||||
|
@ -17,6 +17,8 @@ export default createTypes([
|
|||||||
'replaceChallenge',
|
'replaceChallenge',
|
||||||
'resetUi',
|
'resetUi',
|
||||||
'updateHint',
|
'updateHint',
|
||||||
|
'lockUntrustedCode',
|
||||||
|
'unlockUntrustedCode',
|
||||||
|
|
||||||
// map
|
// map
|
||||||
'updateFilter',
|
'updateFilter',
|
||||||
@ -49,6 +51,7 @@ export default createTypes([
|
|||||||
'saveCode',
|
'saveCode',
|
||||||
'loadCode',
|
'loadCode',
|
||||||
'savedCodeFound',
|
'savedCodeFound',
|
||||||
|
'clearSavedCode',
|
||||||
|
|
||||||
// video challenges
|
// video challenges
|
||||||
'toggleQuestionView',
|
'toggleQuestionView',
|
||||||
|
@ -1,42 +1,17 @@
|
|||||||
import { compose } from 'redux';
|
import flow from 'lodash/flow';
|
||||||
import { bonfire, html, js } from '../../utils/challengeTypes';
|
import { bonfire, html, js } from '../../utils/challengeTypes';
|
||||||
|
import { decodeScriptTags } from '../../../utils/encode-decode';
|
||||||
import protect from '../../utils/empty-protector';
|
import protect from '../../utils/empty-protector';
|
||||||
|
|
||||||
export function encodeScriptTags(value) {
|
|
||||||
return value
|
|
||||||
.replace(/<script>/gi, 'fccss')
|
|
||||||
.replace(/<\/script>/gi, 'fcces');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeSafeTags(value) {
|
|
||||||
return value
|
|
||||||
.replace(/fccss/gi, '<script>')
|
|
||||||
.replace(/fcces/gi, '</script>');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeFormAction(value) {
|
|
||||||
return value.replace(
|
|
||||||
/<form[^>]*>/,
|
|
||||||
val => val.replace(/action(\s*?)=/, 'fccfaa$1=')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeFccfaaAttr(value) {
|
|
||||||
return value.replace(
|
|
||||||
/<form[^>]*>/,
|
|
||||||
val => val.replace(/fccfaa(\s*?)=/, 'action$1=')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function arrayToString(seedData = ['']) {
|
export function arrayToString(seedData = ['']) {
|
||||||
seedData = Array.isArray(seedData) ? seedData : [seedData];
|
seedData = Array.isArray(seedData) ? seedData : [seedData];
|
||||||
return seedData.reduce((seed, line) => '' + seed + line + '\n', '\n');
|
return seedData.reduce((seed, line) => '' + seed + line + '\n', '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSeed({ challengeSeed = [] } = {}) {
|
export function buildSeed({ challengeSeed = [] } = {}) {
|
||||||
return compose(
|
return flow(
|
||||||
decodeSafeTags,
|
arrayToString,
|
||||||
arrayToString
|
decodeScriptTags
|
||||||
)(challengeSeed);
|
)(challengeSeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import { Observable } from 'rx';
|
|||||||
import { push } from 'react-router-redux';
|
import { push } from 'react-router-redux';
|
||||||
|
|
||||||
import { types } from './actions';
|
import { types } from './actions';
|
||||||
import combineSagas from '../../../utils/combine-sagas';
|
|
||||||
import { makeToast } from '../../../toasts/redux/actions';
|
import { makeToast } from '../../../toasts/redux/actions';
|
||||||
import { fetchChallenges } from '../../challenges/redux/actions';
|
import { fetchChallenges } from '../../challenges/redux/actions';
|
||||||
import {
|
import {
|
||||||
@ -14,6 +13,7 @@ import {
|
|||||||
import { userSelector } from '../../../redux/selectors';
|
import { userSelector } from '../../../redux/selectors';
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
import langs from '../../../../utils/supported-languages';
|
import langs from '../../../../utils/supported-languages';
|
||||||
|
import combineSagas from '../../../../utils/combine-sagas';
|
||||||
|
|
||||||
const urlMap = {
|
const urlMap = {
|
||||||
isLocked: 'lockdown',
|
isLocked: 'lockdown',
|
||||||
|
46
common/utils/encode-decode.js
Normal file
46
common/utils/encode-decode.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
|
// we don't store loop protect disable key
|
||||||
|
export function removeNoprotect(val) {
|
||||||
|
return val.replace(/noprotect/gi, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeScriptTags(val) {
|
||||||
|
return val
|
||||||
|
.replace(/<script>/gi, 'fccss')
|
||||||
|
.replace(/<\/script>/gi, 'fcces');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeScriptTags(val) {
|
||||||
|
return val
|
||||||
|
.replace(/fccss/gi, '<script>')
|
||||||
|
.replace(/fcces/gi, '</script>');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeFormAction(val) {
|
||||||
|
return val.replace(
|
||||||
|
// look for attributes in a form
|
||||||
|
/<form[^>]*>/,
|
||||||
|
// val is the string within the opening form tag
|
||||||
|
// look for an `action` attribute, replace it with a fcc tag
|
||||||
|
val => val.replace(/action(\s*?)=/, 'fccfaa$1=')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeFormAction(val) {
|
||||||
|
return val.replace(
|
||||||
|
/<form[^>]*>/,
|
||||||
|
val => val.replace(/fccfaa(\s*?)=/, 'action$1=')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const encodeFcc = flow([
|
||||||
|
removeNoprotect,
|
||||||
|
encodeFormAction,
|
||||||
|
encodeScriptTags
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const decodeFcc = flow([
|
||||||
|
decodeFormAction,
|
||||||
|
decodeScriptTags
|
||||||
|
]);
|
@ -1,3 +1,20 @@
|
|||||||
|
// redux-observable compatible operator
|
||||||
|
export function ofType(...keys) {
|
||||||
|
return this.filter(({ type }) => {
|
||||||
|
const len = keys.length;
|
||||||
|
if (len === 1) {
|
||||||
|
return type === keys[0];
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
if (keys[i] === type) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default function getActionsOfType(actions, ...types) {
|
export default function getActionsOfType(actions, ...types) {
|
||||||
const length = types.length;
|
const length = types.length;
|
||||||
return actions
|
return actions
|
||||||
|
Reference in New Issue
Block a user