Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 43 additions & 20 deletions src/amo/components/AddonDetail.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
import React, { PropTypes } from 'react';

import translate from 'core/i18n/translate';

import AddonMeta from 'amo/components/AddonMeta';
import InstallButton from 'disco/components/InstallButton';
import LikeButton from 'amo/components/LikeButton';
import ScreenShots from 'amo/components/ScreenShots';
import SearchBox from 'amo/components/SearchBox';
import translate from 'core/i18n/translate';
import { nl2br, sanitizeHTML } from 'core/utils';


import 'amo/css/AddonDetail.scss';

export class AddonDetail extends React.Component {
export const allowedDescriptionTags = [
'a',
'abbr',
'acronym',
'b',
'blockquote',
'br',
'code',
'em',
'i',
'li',
'ol',
'strong',
'ul',
];

class AddonDetail extends React.Component {
static propTypes = {
i18n: PropTypes.object,
addon: PropTypes.shape({
name: PropTypes.string.isRequired,
authors: PropTypes.array.isRequired,
slug: PropTypes.string.isRequired,
}),
}

render() {
const { i18n } = this.props;
const { i18n, addon } = this.props;

const authorList = addon.authors.map(
(author) => `<a href="${author.url}">${author.name}</a>`);

const title = i18n.sprintf(
// L10n: Example: The Add-On <span>by The Author</span>
i18n.gettext('%(addonName)s %(startSpan)sby %(authorList)s%(endSpan)s'), {
addonName: addon.name,
authorList: authorList.join(', '),
startSpan: '<span class="author">',
endSpan: '</span>',
});

return (
<div className="AddonDetail">
Expand All @@ -28,13 +61,11 @@ export class AddonDetail extends React.Component {
<LikeButton />
</div>
<div className="title">
<h1>Placeholder Add-on Title
<span className="author">by <a href="#">AwesomeAddons</a></span></h1>
<InstallButton slug="placeholder" />
<h1 dangerouslySetInnerHTML={sanitizeHTML(title, ['a', 'span'])}></h1>
<InstallButton slug={addon.slug} />
</div>
<div className="description">
<p>Lorem ipsum dolor sit amet, dicat graece partiendo cu usu.
Vis recusabo accusamus et.</p>
<p dangerouslySetInnerHTML={sanitizeHTML(addon.summary)}></p>
</div>
</header>

Expand All @@ -54,17 +85,9 @@ export class AddonDetail extends React.Component {

<section className="about">
<h2>{i18n.gettext('About this extension')}</h2>
<p>Lorem ipsum dolor sit amet, dicat graece partiendo cu usu. Vis
recusabo accusamus et, vitae scriptorem in vel. Sed ei eleifend
molestiae deseruisse, sit mucius noster mentitum ex. Eu pro illum
iusto nemore, te legere antiopam sit. Suas simul ad usu, ex putent
timeam fierent eum. Dicam equidem cum cu. Vel ea vidit timeam.</p>

<p>Eu nam dicant oportere, et per habeo euismod denique, te appetere
temporibus mea. Ad solum reprehendunt vis, sea eros accusata senserit
an, eam utinam theophrastus in. Debet consul vis ex. Mei an iusto
delicatissimi, ut timeam electram maiestatis nam, te petentium
intellegebat ius. Ei legere everti.</p>
<div dangerouslySetInnerHTML={sanitizeHTML(nl2br(addon.description),
allowedDescriptionTags)}>
</div>
</section>
</div>
);
Expand Down
32 changes: 29 additions & 3 deletions src/amo/containers/DetailPage.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
import React from 'react';
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { asyncConnect } from 'redux-async-connect';
import { connect } from 'react-redux';

import AddonDetail from 'amo/components/AddonDetail';
import translate from 'core/i18n/translate';
import { loadAddonIfNeeded } from 'core/utils';

export class DetailPage extends React.Component {
static propTypes = {
addon: PropTypes.object,
}

export default class DetailPage extends React.Component {
render() {
return (
<div>
<AddonDetail />
<AddonDetail {...this.props} />
</div>
);
}
}

function mapStateToProps(state, ownProps) {
const { slug } = ownProps.params;
return {
addon: state.addons[slug],
slug,
};
}

export default compose(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely nicer 👍

asyncConnect([{
deferred: true,
promise: loadAddonIfNeeded,
}]),
connect(mapStateToProps),
translate({ withRef: true }),
)(DetailPage);
12 changes: 11 additions & 1 deletion src/core/purify.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import createDOMPurify from 'dompurify';
import universalWindow from 'core/window';

export default createDOMPurify(universalWindow);
const purify = createDOMPurify(universalWindow);
export default purify;

purify.addHook('afterSanitizeAttributes', (node) => {
// Set all elements owning target to target=_blank
// and add rel="noreferrer".
if ('target' in node) {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noreferrer');
}
});
41 changes: 41 additions & 0 deletions src/core/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import camelCase from 'camelcase';
import config from 'config';

import { loadEntities } from 'core/actions';
import { fetchAddon } from 'core/api';
import log from 'core/logger';
import purify from 'core/purify';

export function gettext(str) {
return str;
}
Expand Down Expand Up @@ -61,3 +66,39 @@ export function getClientApp(userAgentString) {
export function isValidClientApp(value, { _config = config } = {}) {
return _config.get('validClientApplications').includes(value);
}

export function sanitizeHTML(text, allowTags = []) {
Copy link
Contributor

@muffinresearch muffinresearch Aug 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this was pre-existing, but allowTags -> allowedTags might be an improvement.

// TODO: Accept tags to allow and run through dom-purify.
return {
__html: purify.sanitize(text, { ALLOWED_TAGS: allowTags }),
};
}

// Convert new lines to HTML breaks.
export function nl2br(text) {
return text.replace(/(?:\r\n|\r|\n)/g, '<br />');
}

export function findAddon(state, slug) {
return state.addons[slug];
}

// asyncConnect() helper for loading an add-on by slug.
//
// This accepts component properties and returns a promise
// that resolves when the requested add-on has been dispatched.
// If the add-on has already been fetched, the add-on value is returned.
//
export function loadAddonIfNeeded(
{ store: { dispatch, getState }, params: { slug } }
) {
const state = getState();
const addon = findAddon(state, slug);
if (addon) {
log.info(`Found addon ${addon.id} in state`);
return addon;
}
log.info(`Fetching addon ${slug} from API`);
return fetchAddon({ slug, api: state.api })
.then(({ entities }) => dispatch(loadEntities(entities)));
}
18 changes: 1 addition & 17 deletions src/disco/components/Addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import React, { PropTypes } from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { connect } from 'react-redux';
import translate from 'core/i18n/translate';
import purify from 'core/purify';

import { sanitizeHTML } from 'core/utils';
import config from 'config';
import themeAction, { getThemeData } from 'disco/themePreview';
import tracking from 'core/tracking';
Expand Down Expand Up @@ -44,22 +44,6 @@ import {

import 'disco/css/Addon.scss';

purify.addHook('afterSanitizeAttributes', (node) => {
// Set all elements owning target to target=_blank
// and add rel="noreferrer".
if ('target' in node) {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noreferrer');
}
});

function sanitizeHTML(text, allowTags = []) {
// TODO: Accept tags to allow and run through dom-purify.
return {
__html: purify.sanitize(text, { ALLOWED_TAGS: allowTags }),
};
}

export class Addon extends React.Component {
static propTypes = {
accentcolor: PropTypes.string,
Expand Down
18 changes: 1 addition & 17 deletions src/search/containers/AddonPage/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { asyncConnect } from 'redux-async-connect';
import { fetchAddon } from 'core/api';
import { loadEntities } from 'core/actions';
import { gettext as _ } from 'core/utils';
import { gettext as _, loadAddonIfNeeded } from 'core/utils';
import NotFound from 'core/components/NotFound';
import JsonData from 'search/components/JsonData';

Expand Down Expand Up @@ -127,20 +125,6 @@ function mapStateToProps(state, ownProps) {
};
}

export function findAddon(state, slug) {
return state.addons[slug];
}

export function loadAddonIfNeeded({ store: { dispatch, getState }, params: { slug } }) {
const state = getState();
const addon = findAddon(state, slug);
if (addon) {
return addon;
}
return fetchAddon({ slug, api: state.api })
.then(({ entities }) => dispatch(loadEntities(entities)));
}

const CurrentAddonPage = asyncConnect([{
deferred: true,
promise: loadAddonIfNeeded,
Expand Down
Loading