Skip to content

Commit b6381f7

Browse files
authored
Hook up the details page to data (#912)
* Hook up the details page to data * switch comma to string * whitelist the same description tags as olympia * localize 'by' in author list * move addHook into core/purify * transform windows line breaks into br too * test that some tag attributes are stripped * fix localization of the author by-line * correct some test descriptions * fix missing quotes
1 parent 182629e commit b6381f7

File tree

10 files changed

+476
-165
lines changed

10 files changed

+476
-165
lines changed

src/amo/components/AddonDetail.js

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,56 @@
11
import React, { PropTypes } from 'react';
22

3-
import translate from 'core/i18n/translate';
4-
53
import AddonMeta from 'amo/components/AddonMeta';
64
import InstallButton from 'disco/components/InstallButton';
75
import LikeButton from 'amo/components/LikeButton';
86
import ScreenShots from 'amo/components/ScreenShots';
97
import SearchBox from 'amo/components/SearchBox';
8+
import translate from 'core/i18n/translate';
9+
import { nl2br, sanitizeHTML } from 'core/utils';
1010

1111

1212
import 'amo/css/AddonDetail.scss';
1313

14-
export class AddonDetail extends React.Component {
14+
export const allowedDescriptionTags = [
15+
'a',
16+
'abbr',
17+
'acronym',
18+
'b',
19+
'blockquote',
20+
'br',
21+
'code',
22+
'em',
23+
'i',
24+
'li',
25+
'ol',
26+
'strong',
27+
'ul',
28+
];
29+
30+
class AddonDetail extends React.Component {
1531
static propTypes = {
1632
i18n: PropTypes.object,
33+
addon: PropTypes.shape({
34+
name: PropTypes.string.isRequired,
35+
authors: PropTypes.array.isRequired,
36+
slug: PropTypes.string.isRequired,
37+
}),
1738
}
1839

1940
render() {
20-
const { i18n } = this.props;
41+
const { i18n, addon } = this.props;
42+
43+
const authorList = addon.authors.map(
44+
(author) => `<a href="${author.url}">${author.name}</a>`);
45+
46+
const title = i18n.sprintf(
47+
// L10n: Example: The Add-On <span>by The Author</span>
48+
i18n.gettext('%(addonName)s %(startSpan)sby %(authorList)s%(endSpan)s'), {
49+
addonName: addon.name,
50+
authorList: authorList.join(', '),
51+
startSpan: '<span class="author">',
52+
endSpan: '</span>',
53+
});
2154

2255
return (
2356
<div className="AddonDetail">
@@ -28,13 +61,11 @@ export class AddonDetail extends React.Component {
2861
<LikeButton />
2962
</div>
3063
<div className="title">
31-
<h1>Placeholder Add-on Title
32-
<span className="author">by <a href="#">AwesomeAddons</a></span></h1>
33-
<InstallButton slug="placeholder" />
64+
<h1 dangerouslySetInnerHTML={sanitizeHTML(title, ['a', 'span'])}></h1>
65+
<InstallButton slug={addon.slug} />
3466
</div>
3567
<div className="description">
36-
<p>Lorem ipsum dolor sit amet, dicat graece partiendo cu usu.
37-
Vis recusabo accusamus et.</p>
68+
<p dangerouslySetInnerHTML={sanitizeHTML(addon.summary)}></p>
3869
</div>
3970
</header>
4071

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

5586
<section className="about">
5687
<h2>{i18n.gettext('About this extension')}</h2>
57-
<p>Lorem ipsum dolor sit amet, dicat graece partiendo cu usu. Vis
58-
recusabo accusamus et, vitae scriptorem in vel. Sed ei eleifend
59-
molestiae deseruisse, sit mucius noster mentitum ex. Eu pro illum
60-
iusto nemore, te legere antiopam sit. Suas simul ad usu, ex putent
61-
timeam fierent eum. Dicam equidem cum cu. Vel ea vidit timeam.</p>
62-
63-
<p>Eu nam dicant oportere, et per habeo euismod denique, te appetere
64-
temporibus mea. Ad solum reprehendunt vis, sea eros accusata senserit
65-
an, eam utinam theophrastus in. Debet consul vis ex. Mei an iusto
66-
delicatissimi, ut timeam electram maiestatis nam, te petentium
67-
intellegebat ius. Ei legere everti.</p>
88+
<div dangerouslySetInnerHTML={sanitizeHTML(nl2br(addon.description),
89+
allowedDescriptionTags)}>
90+
</div>
6891
</section>
6992
</div>
7093
);

src/amo/containers/DetailPage.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
1-
import React from 'react';
1+
import React, { PropTypes } from 'react';
2+
import { compose } from 'redux';
3+
import { asyncConnect } from 'redux-async-connect';
4+
import { connect } from 'react-redux';
25

36
import AddonDetail from 'amo/components/AddonDetail';
7+
import translate from 'core/i18n/translate';
8+
import { loadAddonIfNeeded } from 'core/utils';
9+
10+
export class DetailPage extends React.Component {
11+
static propTypes = {
12+
addon: PropTypes.object,
13+
}
414

5-
export default class DetailPage extends React.Component {
615
render() {
716
return (
817
<div>
9-
<AddonDetail />
18+
<AddonDetail {...this.props} />
1019
</div>
1120
);
1221
}
1322
}
23+
24+
function mapStateToProps(state, ownProps) {
25+
const { slug } = ownProps.params;
26+
return {
27+
addon: state.addons[slug],
28+
slug,
29+
};
30+
}
31+
32+
export default compose(
33+
asyncConnect([{
34+
deferred: true,
35+
promise: loadAddonIfNeeded,
36+
}]),
37+
connect(mapStateToProps),
38+
translate({ withRef: true }),
39+
)(DetailPage);

src/core/purify.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
import createDOMPurify from 'dompurify';
22
import universalWindow from 'core/window';
33

4-
export default createDOMPurify(universalWindow);
4+
const purify = createDOMPurify(universalWindow);
5+
export default purify;
6+
7+
purify.addHook('afterSanitizeAttributes', (node) => {
8+
// Set all elements owning target to target=_blank
9+
// and add rel="noreferrer".
10+
if ('target' in node) {
11+
node.setAttribute('target', '_blank');
12+
node.setAttribute('rel', 'noreferrer');
13+
}
14+
});

src/core/utils.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import camelCase from 'camelcase';
22
import config from 'config';
33

4+
import { loadEntities } from 'core/actions';
5+
import { fetchAddon } from 'core/api';
6+
import log from 'core/logger';
7+
import purify from 'core/purify';
8+
49
export function gettext(str) {
510
return str;
611
}
@@ -61,3 +66,39 @@ export function getClientApp(userAgentString) {
6166
export function isValidClientApp(value, { _config = config } = {}) {
6267
return _config.get('validClientApplications').includes(value);
6368
}
69+
70+
export function sanitizeHTML(text, allowTags = []) {
71+
// TODO: Accept tags to allow and run through dom-purify.
72+
return {
73+
__html: purify.sanitize(text, { ALLOWED_TAGS: allowTags }),
74+
};
75+
}
76+
77+
// Convert new lines to HTML breaks.
78+
export function nl2br(text) {
79+
return text.replace(/(?:\r\n|\r|\n)/g, '<br />');
80+
}
81+
82+
export function findAddon(state, slug) {
83+
return state.addons[slug];
84+
}
85+
86+
// asyncConnect() helper for loading an add-on by slug.
87+
//
88+
// This accepts component properties and returns a promise
89+
// that resolves when the requested add-on has been dispatched.
90+
// If the add-on has already been fetched, the add-on value is returned.
91+
//
92+
export function loadAddonIfNeeded(
93+
{ store: { dispatch, getState }, params: { slug } }
94+
) {
95+
const state = getState();
96+
const addon = findAddon(state, slug);
97+
if (addon) {
98+
log.info(`Found addon ${addon.id} in state`);
99+
return addon;
100+
}
101+
log.info(`Fetching addon ${slug} from API`);
102+
return fetchAddon({ slug, api: state.api })
103+
.then(({ entities }) => dispatch(loadEntities(entities)));
104+
}

src/disco/components/Addon.js

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import React, { PropTypes } from 'react';
44
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
55
import { connect } from 'react-redux';
66
import translate from 'core/i18n/translate';
7-
import purify from 'core/purify';
87

8+
import { sanitizeHTML } from 'core/utils';
99
import config from 'config';
1010
import themeAction, { getThemeData } from 'disco/themePreview';
1111
import tracking from 'core/tracking';
@@ -44,22 +44,6 @@ import {
4444

4545
import 'disco/css/Addon.scss';
4646

47-
purify.addHook('afterSanitizeAttributes', (node) => {
48-
// Set all elements owning target to target=_blank
49-
// and add rel="noreferrer".
50-
if ('target' in node) {
51-
node.setAttribute('target', '_blank');
52-
node.setAttribute('rel', 'noreferrer');
53-
}
54-
});
55-
56-
function sanitizeHTML(text, allowTags = []) {
57-
// TODO: Accept tags to allow and run through dom-purify.
58-
return {
59-
__html: purify.sanitize(text, { ALLOWED_TAGS: allowTags }),
60-
};
61-
}
62-
6347
export class Addon extends React.Component {
6448
static propTypes = {
6549
accentcolor: PropTypes.string,

src/search/containers/AddonPage/index.js

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import React, { PropTypes } from 'react';
22
import { connect } from 'react-redux';
33
import { asyncConnect } from 'redux-async-connect';
4-
import { fetchAddon } from 'core/api';
5-
import { loadEntities } from 'core/actions';
6-
import { gettext as _ } from 'core/utils';
4+
import { gettext as _, loadAddonIfNeeded } from 'core/utils';
75
import NotFound from 'core/components/NotFound';
86
import JsonData from 'search/components/JsonData';
97

@@ -127,20 +125,6 @@ function mapStateToProps(state, ownProps) {
127125
};
128126
}
129127

130-
export function findAddon(state, slug) {
131-
return state.addons[slug];
132-
}
133-
134-
export function loadAddonIfNeeded({ store: { dispatch, getState }, params: { slug } }) {
135-
const state = getState();
136-
const addon = findAddon(state, slug);
137-
if (addon) {
138-
return addon;
139-
}
140-
return fetchAddon({ slug, api: state.api })
141-
.then(({ entities }) => dispatch(loadEntities(entities)));
142-
}
143-
144128
const CurrentAddonPage = asyncConnect([{
145129
deferred: true,
146130
promise: loadAddonIfNeeded,

0 commit comments

Comments
 (0)