diff --git a/__tests__/shared/utils/__snapshots__/markdown.js.snap b/__tests__/shared/utils/__snapshots__/markdown.js.snap index 449da8d15c..5dc7969857 100644 --- a/__tests__/shared/utils/__snapshots__/markdown.js.snap +++ b/__tests__/shared/utils/__snapshots__/markdown.js.snap @@ -1203,7 +1203,7 @@ Array [ A list item with a code block:

- <code goes here> + <code> @@ -1294,7 +1294,7 @@ end tell ampersands and angle brackets. For example, this:

, - <div class="footer"> + <div> © 2004 Foo Corporation </div> diff --git a/src/shared/components/Contentful/PasswordScreen/index.jsx b/src/shared/components/Contentful/PasswordScreen/index.jsx deleted file mode 100644 index c574520af5..0000000000 --- a/src/shared/components/Contentful/PasswordScreen/index.jsx +++ /dev/null @@ -1,110 +0,0 @@ -/** - * High order component that apply front-side password protection - * before loading a Contentful Viewport. It uses sessionStorage for working. - */ -import PT from 'prop-types'; -import React from 'react'; -import Viewport from 'components/Contentful/Viewport'; -import TextInput from 'components/GUIKit/TextInput'; - -import './style.scss'; - -export default class PasswordScreen extends React.Component { - state = {}; - - constructor(props) { - super(props); - - this.onSubmit = this.onSubmit.bind(this); - this.onPasswordInput = this.onPasswordInput.bind(this); - } - - onSubmit() { - const { password } = this.props; - this.setState((state) => { - const { inputVal } = state; - return { - authorized: password === inputVal, - errorMsg: password === inputVal ? '' : 'Password incorrect', - }; - }); - } - - onPasswordInput(inputVal) { - const update = { - inputVal, - }; - if (!inputVal) update.errorMsg = ''; - this.setState(update); - } - - render() { - const { - authorized, errorMsg, inputVal, - } = this.state; - const { - viewPortId, preview, spaceName, environment, baseUrl, title, btnText, content, - } = this.props; - return authorized ? ( - - ) : ( -
-
-

{title || 'GET ACCESS WITH PASSWORD'}

-

Please enter the password you were provided

- this.onPasswordInput(val)} - errorMsg={errorMsg} - required - type="password" - onEnterKey={this.onSubmit} - /> -
- -
-
- { - content ? ( - - ) : null - } -
- ); - } -} - -PasswordScreen.defaultProps = { - preview: false, - spaceName: null, - environment: null, - baseUrl: '', - title: 'GET ACCESS WITH PASSWORD', - btnText: 'SUBMIT', - content: null, -}; - -PasswordScreen.propTypes = { - password: PT.string.isRequired, - viewPortId: PT.string.isRequired, - preview: PT.bool, - spaceName: PT.string, - environment: PT.string, - baseUrl: PT.string, - title: PT.string, - btnText: PT.string, - content: PT.shape(), -}; diff --git a/src/shared/components/Contentful/PasswordScreen/style.scss b/src/shared/components/Contentful/PasswordScreen/style.scss deleted file mode 100644 index 74d6ddfd05..0000000000 --- a/src/shared/components/Contentful/PasswordScreen/style.scss +++ /dev/null @@ -1,51 +0,0 @@ -@import "~styles/mixins"; -@import "~components/Contentful/default"; -@import "~components/GUIKit/Assets/Styles/default"; - -.wrapper { - background-color: #e9e9e9; - padding: 86px 0 121px 0; - min-height: 100vh; - - .container { - text-align: center; - border-radius: 10px; - max-width: 544px; - max-height: 371px; - margin: 0 auto; - padding: 31px 65px; - background-color: #fff; - - @include gui-kit-headers; - @include gui-kit-content; - - h4 { - margin-bottom: 5px; - } - - .hint { - font-size: 14px; - margin-bottom: 30px; - } - - .cta { - margin: 50px auto 29px auto; - } - - .submit { - outline: none; - - @include primary-green; - @include md; - - &:disabled, - &:hover:disabled { - background-color: #e9e9e9 !important; - border: none !important; - text-decoration: none !important; - color: #fafafb !important; - box-shadow: none !important; - } - } - } -} diff --git a/src/shared/components/Contentful/Route.jsx b/src/shared/components/Contentful/Route.jsx index 4b88d43585..0d9dccaf47 100644 --- a/src/shared/components/Contentful/Route.jsx +++ b/src/shared/components/Contentful/Route.jsx @@ -12,7 +12,6 @@ import PT from 'prop-types'; import React from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; import Viewport from 'components/Contentful/Viewport'; -import PasswordScreen from 'components/Contentful/PasswordScreen'; import { isomorphy, config } from 'topcoder-react-utils'; import cookies from 'browser-cookies'; import { removeTrailingSlash } from 'utils/url'; @@ -57,7 +56,7 @@ function ChildRoutesLoader(props) { { // eslint-disable-next-line no-nested-ternary fields.viewport - ? (!fields.password ? ( + ? ( - ) : ( - - ) ) : } diff --git a/src/shared/containers/Profile.jsx b/src/shared/containers/Profile.jsx index af011788f8..cbc047eecd 100644 --- a/src/shared/containers/Profile.jsx +++ b/src/shared/containers/Profile.jsx @@ -135,6 +135,20 @@ class ProfileContainer extends React.Component { const title = `${handleParam} | Community Profile | Topcoder`; const description = `Meet Topcoder member ${handleParam} and view their skills and development and design activity. You can also see wins and tenure with Topcoder.`; + const { + copilot, + externalAccounts, + externalLinks, + challenges, + skills, + stats, + lookupData, + badges, + meta, + tcAcademyCertifications, + tcAcademyCourses, + } = this.props; + return ( ) : } @@ -154,9 +180,8 @@ class ProfileContainer extends React.Component { } ProfileContainer.defaultProps = { - achievements: null, copilot: false, - country: '', + challenges: null, externalAccounts: null, externalLinks: null, info: null, @@ -166,12 +191,15 @@ ProfileContainer.defaultProps = { meta: null, memberGroups: null, auth: {}, + badges: {}, + tcAcademyCertifications: [], + tcAcademyCourses: [], }; ProfileContainer.propTypes = { - achievements: PT.arrayOf(PT.shape()), + badges: PT.shape(), + challenges: PT.arrayOf(PT.shape()), copilot: PT.bool, - country: PT.string, externalAccounts: PT.shape(), externalLinks: PT.arrayOf(PT.shape()), handleParam: PT.string.isRequired, @@ -188,14 +216,14 @@ ProfileContainer.propTypes = { lookupData: PT.shape().isRequired, meta: PT.shape(), auth: PT.shape(), + tcAcademyCertifications: PT.arrayOf(PT.shape()), + tcAcademyCourses: PT.arrayOf(PT.shape()), }; const mapStateToProps = (state, ownProps) => ({ challenges: state.members[ownProps.match.params.handle] ? state.members[ownProps.match.params.handle].userMarathons : null, - achievements: state.profile.achievements, copilot: state.profile.copilot, - country: state.profile.country, externalAccounts: state.profile.externalAccounts, externalLinks: state.profile.externalLinks, handleParam: ownProps.match.params.handle, diff --git a/src/shared/containers/ProfileStats.jsx b/src/shared/containers/ProfileStats.jsx index 56ae4ce25e..527e149224 100644 --- a/src/shared/containers/ProfileStats.jsx +++ b/src/shared/containers/ProfileStats.jsx @@ -81,6 +81,13 @@ class ProfileStatsContainer extends React.Component { handleParam, track, subTrack, + stats, + tab, + setTab, + info, + statsDistribution, + statsHistory, + isAlreadyLoadChallenge, } = this.props; if (loadingError || !isValidTrack(track, subTrack)) { @@ -99,7 +106,16 @@ class ProfileStatsContainer extends React.Component { isLoading ? : ( ) } diff --git a/src/shared/containers/Toastr/index.jsx b/src/shared/containers/Toastr/index.jsx index ba313e9de8..4c68fcf82b 100644 --- a/src/shared/containers/Toastr/index.jsx +++ b/src/shared/containers/Toastr/index.jsx @@ -78,7 +78,13 @@ class ExtendedReduxToastr extends ReduxToastr { inMemory={this.toastrFired} addToMemory={() => this._addToMemory(item.id)} item={mergedItem} - {...this.props} + toastrs={this.props.toastrs} + preventDuplicates={this.props.preventDuplicates} + position={this.props.position} + transitionIn={this.props.transitionIn} + transitionOut={this.props.transitionOut} + progressBar={this.props.progressBar} + showCloseButton={this.props.showCloseButton} /> {item.options && item.options.attention && ( diff --git a/src/shared/utils/markdown.js b/src/shared/utils/markdown.js index 5e0883bf04..1b23da60d2 100644 --- a/src/shared/utils/markdown.js +++ b/src/shared/utils/markdown.js @@ -13,6 +13,7 @@ import { Button, PrimaryButton, SecondaryButton } from 'topcoder-react-ui-kit'; import { Link } from 'topcoder-react-utils'; import hljs from 'highlight.js'; import ReactHtmlParser from 'react-html-parser'; +import xss from 'xss'; import sub from 'markdown-it-sub'; import sup from 'markdown-it-sup'; import 'highlight.js/styles/github.css'; @@ -133,6 +134,11 @@ const customComponents = { MMLeaderboard: attrs => ({ type: MMLeaderboard, props: attrs }), }; +const unsafeHtmlTags = [ + 'script', 'style', 'iframe', 'object', 'embed', 'applet', 'base', + 'form', 'meta', 'frame', 'frameset', 'marquee', 'svg', +]; + /** * The following functions are only used internally and should not need to be * changed for new components. @@ -165,6 +171,24 @@ function getProps(token, key) { return normalizeProps(res); } +/** + * Check if the tag is safe to render. + * @param {String} tag + * @returns + */ +function checkForSafeTag(tag) { + return !unsafeHtmlTags.includes(tag); +} + +/** + * Sanitize content + * @param {String} content + * @returns + */ +function sanitizeContent(content) { + return xss(content); +} + /** * Renders tokens with zero nesting. * @param {Object} tokens @@ -184,7 +208,7 @@ function renderToken(tokens, index, md) { return renderTokens(token.children, 0, md); /* eslint-enable no-use-before-define */ case 'text': - return token.content; + return sanitizeContent(token.content); case 'fence': return Highlighter({ codeString: token.content, @@ -204,9 +228,9 @@ function renderToken(tokens, index, md) { } default: return React.createElement( - token.tag, + checkForSafeTag(token.tag) ? token.tag : 'div', getProps(token, index), - token.content || undefined, + sanitizeContent(token.content) || undefined, ); } } @@ -232,7 +256,7 @@ function renderTokens(tokens, startFrom, md) { } else if (level === 0) { if (token.nesting === 1) { output.push(React.createElement( - token.tag, + checkForSafeTag(token.tag) ? token.tag : 'div', getProps(token, pos), renderTokens(tokens, 1 + pos, md), )); @@ -252,11 +276,11 @@ function renderTokens(tokens, startFrom, md) { } props = normalizeProps(props); if (selfClosing) { - output.push(React.createElement(tag, { key: pos, ...props })); + output.push(React.createElement(checkForSafeTag(tag) ? tag : 'div', { key: pos, ...props })); } else { level += 1; output.push(React.createElement( - tag, + checkForSafeTag(tag) ? tag : 'div', { key: pos, ...props }, renderTokens(tokens, pos + 1, md), ));