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 { 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 {
|
||||
savedCodeFound
|
||||
savedCodeFound,
|
||||
updateMain,
|
||||
lockUntrustedCode
|
||||
} from '../../common/app/routes/challenges/redux/actions';
|
||||
|
||||
const legecyPrefixes = [
|
||||
const legacyPrefixes = [
|
||||
'Bonfire: ',
|
||||
'Waypoint: ',
|
||||
'Zipline: ',
|
||||
'Basejump: ',
|
||||
'Checkpoint: '
|
||||
];
|
||||
const legacyPostfix = 'Val';
|
||||
|
||||
function getCode(id, legacy) {
|
||||
function getCode(id) {
|
||||
if (store.has(id)) {
|
||||
return store.get(id);
|
||||
}
|
||||
if (store.has(legacy)) {
|
||||
const code = '' + store.get(legacy);
|
||||
store.remove(legacy);
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLegacyCode(legacy) {
|
||||
const key = legacy + legacyPostfix;
|
||||
let code = null;
|
||||
if (store.has(key)) {
|
||||
code = '' + store.get(key);
|
||||
store.remove(key);
|
||||
return code;
|
||||
}
|
||||
return legecyPrefixes.reduce((code, prefix) => {
|
||||
return legacyPrefixes.reduce((code, prefix) => {
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
return store.get(prefix + legacy + 'Val');
|
||||
return store.get(prefix + key);
|
||||
}, null);
|
||||
}
|
||||
|
||||
export default function codeStorageSaga(actions$, getState) {
|
||||
return actions$
|
||||
.filter(({ type }) => (
|
||||
type === types.saveCode ||
|
||||
type === types.loadCode
|
||||
))
|
||||
.map(({ type }) => {
|
||||
const { id = '', files = {}, legacyKey = '' } = getState().challengesApp;
|
||||
if (type === types.saveCode) {
|
||||
store.set(id, files);
|
||||
return null;
|
||||
}
|
||||
const codeFound = getCode(id, legacyKey);
|
||||
if (codeFound) {
|
||||
return savedCodeFound(codeFound);
|
||||
}
|
||||
function legacyToFile(code, files, key) {
|
||||
return { [key]: updateContents(code, files[key]) };
|
||||
}
|
||||
|
||||
export function clearCodeSaga(actions, getState) {
|
||||
return actions
|
||||
::ofType(types.clearSavedCode)
|
||||
.map(() => {
|
||||
const { challengesApp: { id = '' } } = getState();
|
||||
store.remove(id);
|
||||
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.updateMain
|
||||
))
|
||||
// if isCodeLockedTrue do not run challenges
|
||||
.filter(() => !getState().challengesApp.isCodeLocked)
|
||||
.debounce(750)
|
||||
.flatMapLatest(({ type }) => {
|
||||
const state = getState();
|
||||
|
@ -2,6 +2,7 @@ import Rx, { Observable, Subject } from 'rx';
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import loopProtect from 'loop-protect';
|
||||
/* eslint-enable import/no-unresolved */
|
||||
import { ofType } from '../../common/utils/get-actions-of-type';
|
||||
import types from '../../common/app/routes/challenges/redux/types';
|
||||
import {
|
||||
updateOutput,
|
||||
@ -89,12 +90,13 @@ export default function frameSaga(actions$, getState, { window, document }) {
|
||||
const proxyLogger$ = new Subject();
|
||||
const runTests$ = window.__common[testId + 'Ready$'] =
|
||||
new Subject();
|
||||
const result$ = actions$
|
||||
.filter(({ type }) => (
|
||||
type === types.frameMain ||
|
||||
type === types.frameTests ||
|
||||
type === types.frameOutput
|
||||
))
|
||||
const result$ = actions$::ofType(
|
||||
types.frameMain,
|
||||
types.frameTests,
|
||||
types.frameOutput
|
||||
)
|
||||
// if isCodeLocked is true do not frame user code
|
||||
.filter(() => !getState().challengesApp.isCodeLocked)
|
||||
.map(action => {
|
||||
if (action.type === types.frameMain) {
|
||||
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,
|
||||
className: React.PropTypes.string,
|
||||
href: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
points: React.PropTypes.func,
|
||||
onClick: React.PropTypes.func,
|
||||
points: React.PropTypes.number,
|
||||
title: React.PropTypes.node
|
||||
},
|
||||
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
} from './selectors';
|
||||
import { updateCurrentChallenge } from '../routes/challenges/redux/actions';
|
||||
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';
|
||||
|
||||
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 {
|
||||
executeChallenge,
|
||||
updateMain,
|
||||
updateFile,
|
||||
loadCode
|
||||
} from '../../redux/actions';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
state => state.challengesApp.id,
|
||||
state => state.challengesApp.tests,
|
||||
state => state.challengesApp.files,
|
||||
state => state.challengesApp.key,
|
||||
(
|
||||
{ showPreview, mode },
|
||||
id,
|
||||
tests,
|
||||
files = {},
|
||||
key = ''
|
||||
) => ({
|
||||
id,
|
||||
content: files[key] && files[key].contents || '',
|
||||
file: files[key],
|
||||
showPreview,
|
||||
@ -38,7 +40,6 @@ const mapStateToProps = createSelector(
|
||||
const bindableActions = {
|
||||
executeChallenge,
|
||||
updateFile,
|
||||
updateMain,
|
||||
loadCode
|
||||
};
|
||||
|
||||
@ -46,18 +47,23 @@ export class Challenge extends PureComponent {
|
||||
static displayName = 'Challenge';
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
showPreview: PropTypes.bool,
|
||||
content: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
updateFile: PropTypes.func,
|
||||
executeChallenge: PropTypes.func,
|
||||
updateMain: PropTypes.func,
|
||||
loadCode: PropTypes.func
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadCode();
|
||||
this.props.updateMain();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.id !== nextProps.id) {
|
||||
this.props.loadCode();
|
||||
}
|
||||
}
|
||||
|
||||
renderPreview(showPreview) {
|
||||
|
@ -12,7 +12,8 @@ import { challengeSelector } from '../../redux/selectors';
|
||||
import {
|
||||
openBugModal,
|
||||
updateHint,
|
||||
executeChallenge
|
||||
executeChallenge,
|
||||
unlockUntrustedCode
|
||||
} from '../../redux/actions';
|
||||
import { makeToast } from '../../../../toasts/redux/actions';
|
||||
import { toggleHelpChat } from '../../../../redux/actions';
|
||||
@ -22,7 +23,8 @@ const bindableActions = {
|
||||
executeChallenge,
|
||||
updateHint,
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
openBugModal,
|
||||
unlockUntrustedCode
|
||||
};
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
@ -31,20 +33,23 @@ const mapStateToProps = createSelector(
|
||||
state => state.challengesApp.tests,
|
||||
state => state.challengesApp.output,
|
||||
state => state.challengesApp.hintIndex,
|
||||
state => state.challengesApp.isCodeLocked,
|
||||
(
|
||||
{ challenge: { title, description, hints = [] } = {} },
|
||||
windowHeight,
|
||||
navHeight,
|
||||
tests,
|
||||
output,
|
||||
hintIndex
|
||||
hintIndex,
|
||||
isCodeLocked
|
||||
) => ({
|
||||
title,
|
||||
description,
|
||||
height: windowHeight - navHeight - 20,
|
||||
tests,
|
||||
output,
|
||||
hint: hints[hintIndex]
|
||||
hint: hints[hintIndex],
|
||||
isCodeLocked
|
||||
})
|
||||
);
|
||||
|
||||
@ -65,7 +70,8 @@ export class SidePanel extends PureComponent {
|
||||
updateHint: PropTypes.func,
|
||||
makeToast: PropTypes.func,
|
||||
toggleHelpChat: PropTypes.func,
|
||||
openBugModal: PropTypes.func
|
||||
openBugModal: PropTypes.func,
|
||||
unlockUntrustedCode: PropTypes.func
|
||||
};
|
||||
|
||||
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
||||
@ -106,7 +112,9 @@ export class SidePanel extends PureComponent {
|
||||
updateHint,
|
||||
makeToast,
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
openBugModal,
|
||||
isCodeLocked,
|
||||
unlockUntrustedCode
|
||||
} = this.props;
|
||||
const style = {};
|
||||
if (height) {
|
||||
@ -135,9 +143,11 @@ export class SidePanel extends PureComponent {
|
||||
<ToolPanel
|
||||
executeChallenge={ executeChallenge }
|
||||
hint={ hint }
|
||||
isCodeLocked={ isCodeLocked }
|
||||
makeToast={ makeToast }
|
||||
openBugModal={ openBugModal }
|
||||
toggleHelpChat={ toggleHelpChat }
|
||||
unlockUntrustedCode={ unlockUntrustedCode }
|
||||
updateHint={ updateHint }
|
||||
/>
|
||||
<Output output={ output }/>
|
||||
|
@ -1,7 +1,14 @@
|
||||
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';
|
||||
|
||||
const unlockWarning = (
|
||||
<Tooltip id='tooltip'>
|
||||
<h4>
|
||||
<strong>Careful!</strong> Only run code you trust
|
||||
</h4>
|
||||
</Tooltip>
|
||||
);
|
||||
export default class ToolPanel extends PureComponent {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
@ -14,8 +21,10 @@ export default class ToolPanel extends PureComponent {
|
||||
executeChallenge: PropTypes.func,
|
||||
updateHint: PropTypes.func,
|
||||
hint: PropTypes.string,
|
||||
isCodeLocked: PropTypes.bool,
|
||||
toggleHelpChat: PropTypes.func,
|
||||
openBugModal: PropTypes.func
|
||||
openBugModal: PropTypes.func,
|
||||
unlockUntrustedCode: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
makeHint() {
|
||||
@ -51,16 +60,25 @@ export default class ToolPanel extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
hint,
|
||||
executeChallenge,
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
} = this.props;
|
||||
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 (
|
||||
<div>
|
||||
{ this.renderHint(hint, this.makeHint) }
|
||||
<Button
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
@ -69,6 +87,28 @@ export default class ToolPanel extends PureComponent {
|
||||
>
|
||||
Run tests (ctrl + enter)
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
hint,
|
||||
isCodeLocked,
|
||||
executeChallenge,
|
||||
toggleHelpChat,
|
||||
openBugModal,
|
||||
unlockUntrustedCode
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
{ this.renderHint(hint, this.makeHint) }
|
||||
{
|
||||
this.renderExecute(
|
||||
isCodeLocked,
|
||||
executeChallenge,
|
||||
unlockUntrustedCode
|
||||
)
|
||||
}
|
||||
<div className='button-spacer' />
|
||||
<ButtonGroup
|
||||
className='input-group'
|
||||
|
@ -22,6 +22,11 @@ export const fetchChallengeCompleted = createAction(
|
||||
);
|
||||
export const resetUi = createAction(types.resetUi);
|
||||
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 fetchChallengesCompleted = createAction(
|
||||
@ -85,6 +90,7 @@ export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
||||
export const saveCode = createAction(types.saveCode);
|
||||
export const loadCode = createAction(types.loadCode);
|
||||
export const savedCodeFound = createAction(types.savedCodeFound);
|
||||
export const clearSavedCode = createAction(types.clearSavedCode);
|
||||
|
||||
|
||||
// video challenges
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import types from './types';
|
||||
import { moveToNextChallenge } from './actions';
|
||||
import {
|
||||
moveToNextChallenge,
|
||||
clearSavedCode
|
||||
} from './actions';
|
||||
|
||||
import { challengeSelector } from './selectors';
|
||||
import { randomCompliment } from '../../../utils/get-words';
|
||||
@ -24,7 +27,8 @@ function postChallenge(url, username, _csrf, challengeInfo) {
|
||||
updateUserChallenge(
|
||||
username,
|
||||
{ ...challengeInfo, lastUpdated, completedDate }
|
||||
)
|
||||
),
|
||||
clearSavedCode()
|
||||
);
|
||||
})
|
||||
.catch(createErrorObservable);
|
||||
|
@ -45,6 +45,7 @@ const initialUiState = {
|
||||
shouldShowQuestions: false
|
||||
};
|
||||
const initialState = {
|
||||
isCodeLocked: false,
|
||||
id: '',
|
||||
challenge: '',
|
||||
helpChatRoom: 'Help',
|
||||
@ -88,6 +89,14 @@ const mainReducer = handleActions(
|
||||
0 :
|
||||
state.hintIndex + 1
|
||||
}),
|
||||
[types.lockUntrustedCode]: state => ({
|
||||
...state,
|
||||
isCodeLocked: true
|
||||
}),
|
||||
[types.unlockUntrustedCode]: state => ({
|
||||
...state,
|
||||
isCodeLocked: false
|
||||
}),
|
||||
[types.executeChallenge]: state => ({
|
||||
...state,
|
||||
tests: state.tests.map(test => ({ ...test, err: false, pass: false }))
|
||||
|
@ -17,6 +17,8 @@ export default createTypes([
|
||||
'replaceChallenge',
|
||||
'resetUi',
|
||||
'updateHint',
|
||||
'lockUntrustedCode',
|
||||
'unlockUntrustedCode',
|
||||
|
||||
// map
|
||||
'updateFilter',
|
||||
@ -49,6 +51,7 @@ export default createTypes([
|
||||
'saveCode',
|
||||
'loadCode',
|
||||
'savedCodeFound',
|
||||
'clearSavedCode',
|
||||
|
||||
// video challenges
|
||||
'toggleQuestionView',
|
||||
|
@ -1,42 +1,17 @@
|
||||
import { compose } from 'redux';
|
||||
import flow from 'lodash/flow';
|
||||
import { bonfire, html, js } from '../../utils/challengeTypes';
|
||||
import { decodeScriptTags } from '../../../utils/encode-decode';
|
||||
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 = ['']) {
|
||||
seedData = Array.isArray(seedData) ? seedData : [seedData];
|
||||
return seedData.reduce((seed, line) => '' + seed + line + '\n', '\n');
|
||||
}
|
||||
|
||||
export function buildSeed({ challengeSeed = [] } = {}) {
|
||||
return compose(
|
||||
decodeSafeTags,
|
||||
arrayToString
|
||||
return flow(
|
||||
arrayToString,
|
||||
decodeScriptTags
|
||||
)(challengeSeed);
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { Observable } from 'rx';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import { types } from './actions';
|
||||
import combineSagas from '../../../utils/combine-sagas';
|
||||
import { makeToast } from '../../../toasts/redux/actions';
|
||||
import { fetchChallenges } from '../../challenges/redux/actions';
|
||||
import {
|
||||
@ -14,6 +13,7 @@ import {
|
||||
import { userSelector } from '../../../redux/selectors';
|
||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||
import langs from '../../../../utils/supported-languages';
|
||||
import combineSagas from '../../../../utils/combine-sagas';
|
||||
|
||||
const urlMap = {
|
||||
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) {
|
||||
const length = types.length;
|
||||
return actions
|
||||
|
Reference in New Issue
Block a user