renderReview({ review, key }: RenderReviewParams) { const { i18n } = this.props; let byLine; let reviewBody; if (review) { const timestamp = i18n.moment(review.created).fromNow(); // L10n: Example: "from Jose, last week" byLine = i18n.sprintf( i18n.gettext('from %(authorName)s, %(timestamp)s'), { authorName: review.userName, timestamp }); const reviewBodySanitized = sanitizeHTML(nl2br(review.body), ['br']); // eslint-disable-next-line react/no-danger reviewBody = <p dangerouslySetInnerHTML={reviewBodySanitized} />; } else { byLine = <LoadingText />; reviewBody = <p><LoadingText /></p>; } return ( <li className="AddonReviewList-li" key={key}> <h3>{review ? review.title : <LoadingText />}</h3> {reviewBody} <div className="AddonReviewList-by-line"> {review ? <Rating styleName="small" rating={review.rating} readOnly /> : null } {byLine} </div> </li> ); }
render() { const { i18n } = this.props; const fileAnIssueText = i18n.sprintf( i18n.gettext(` If you are signed in and think this message is an error, please <a href="%(url)s">file an issue</a>. Tell us where you came from and what you were trying to access, and we'll fix the issue.`), { url: 'https://github.com/mozilla/addons-frontend/issues/new/' }, ); // TODO: Check for signed in state and offer different messages. // TODO: Offer a sign in link/button inside the error page. /* eslint-disable react/no-danger */ return ( <NestedStatus code={401}> <Card className="ErrorPage NotAuthorized" header={i18n.gettext('Not Authorized')} > <p> {i18n.gettext(` Sorry, but you aren't authorized to access this page. If you aren't signed in, try signing in using the link at the top of the page.`)} </p> <SuggestedPages /> <p dangerouslySetInnerHTML={sanitizeHTML(fileAnIssueText, ['a'])} /> </Card> </NestedStatus> ); /* eslint-enable react/no-danger */ }
render() { const { i18n, minVersion, reason, } = this.props; let message; if (typeof reason === 'undefined') { throw new Error('AddonCompatibilityError requires a "reason" prop'); } if (typeof minVersion === 'undefined') { throw new Error('minVersion is required; it cannot be undefined'); } if (reason === INCOMPATIBLE_FIREFOX_FOR_IOS) { message = i18n.gettext( 'Firefox for iOS does not currently support add-ons.'); } else if (reason === INCOMPATIBLE_UNDER_MIN_VERSION) { message = i18n.gettext( 'This add-on does not support your version of Firefox.'); } else { // Unknown reasons are fine on the Disco Pane because we don't // care about non-FF clients. message = i18n.gettext('This add-on does not support your browser.'); } return ( <div className="AddonCompatibilityError" dangerouslySetInnerHTML={sanitizeHTML(message, ['a'])} /> ); }
render() { const { children, className, header, i18n } = this.props; const { expanded } = this.state; const readMoreLink = ( <a className="ShowMoreCard-expand-link" href="#show-more" onClick={this.onClick} dangerouslySetInnerHTML={sanitizeHTML( i18n.gettext( // l10n: The "Expand to" text is for screenreaders so the link // makes sense out of context. The HTML makes it hidden from // non-screenreaders and must stay. '<span class="visually-hidden">Expand to</span> Read more' ), ['span'] )} /> ); return ( <Card className={classNames('ShowMoreCard', className, { 'ShowMoreCard--expanded': expanded })} header={header} footerLink={expanded ? null : readMoreLink} > <div className="ShowMoreCard-contents" ref={(ref) => { this.contents = ref; }} > {children} </div> </Card> ); }
render() { const { i18n, lang, log, minVersion, reason, userAgentInfo, } = this.props; const downloadUrl = `https://www.mozilla.org/${lang}/firefox/`; let message; if (typeof reason === 'undefined') { throw new Error('AddonCompatibilityError requires a "reason" prop'); } if (typeof minVersion === 'undefined') { throw new Error('minVersion is required; it cannot be undefined'); } if (reason === INCOMPATIBLE_NOT_FIREFOX) { message = i18n.sprintf(i18n.gettext(`You need to <a href="%(downloadUrl)s">download Firefox</a> to install this add-on.` ), { downloadUrl }); } else if (reason === INCOMPATIBLE_NO_OPENSEARCH) { message = i18n.gettext( 'Your version of Firefox does not support search plugins.'); } else if (reason === INCOMPATIBLE_FIREFOX_FOR_IOS) { message = i18n.gettext( 'Firefox for iOS does not currently support add-ons.'); } else if (reason === INCOMPATIBLE_UNDER_MIN_VERSION) { message = i18n.sprintf(i18n.gettext(`This add-on requires a <a href="%(downloadUrl)s">newer version of Firefox</a> (at least version %(minVersion)s). You are using Firefox %(yourVersion)s.` ), { downloadUrl, minVersion, yourVersion: userAgentInfo.browser.version, }); } else { // This is an unknown reason code and a custom error message should // be added. log.warn( 'Unknown reason code supplied to AddonCompatibilityError', reason); message = i18n.sprintf(i18n.gettext(`Your browser does not support add-ons. You can <a href="%(downloadUrl)s">download Firefox</a> to install this add-on.` ), { downloadUrl }); } return ( <div className="AddonCompatibilityError" dangerouslySetInnerHTML={sanitizeHTML(message, ['a'])} /> ); }
render() { const { addonName, i18n, imageURL, show } = this.props; if (!show) { return null; } invariant(addonName, 'addonName is required when show=true'); invariant(imageURL, 'imageURL is required when show=true'); return ( <div className="InfoDialog" role="dialog" aria-labelledby="InfoDialog-title" aria-describedby="InfoDialog-description" > <div className="InfoDialog-info"> <div className="InfoDialog-logo"> <img src={imageURL} alt={addonName} /> </div> <div className="InfoDialog-copy"> <p className="InfoDialog-title" // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext( `%(strongStart)s%(name)s%(strongEnd)s has been added`, ), { name: addonName, strongEnd: '</strong>', strongStart: '<strong>', }, ), ['strong'], )} /> <p className="InfoDialog-description"> {i18n.gettext( 'Manage your add-ons by clicking Add-ons in the menu.', )} </p> </div> </div> <button className="InfoDialog-button" onClick={this.closeInfoDialog} type="button" > {i18n.gettext('OK')} </button> </div> ); }
render() { const { addon, i18n } = this.props; const averageDailyUsers = addon.average_daily_users; const isTheme = addon.type === ADDON_TYPE_THEME; const resultClassnames = classNames('SearchResult', { 'SearchResult--theme': isTheme, }); // Fall-back to default icon if invalid icon url. const iconURL = getAddonIconUrl(addon); const themeURL = (addon.theme_data && isAllowedOrigin(addon.theme_data.previewURL)) ? addon.theme_data.previewURL : null; const imageURL = isTheme ? themeURL : iconURL; // Sets classes to handle fallback if theme preview is not available. const iconWrapperClassnames = classNames('SearchResult-icon-wrapper', { 'SearchResult-icon-wrapper--no-theme-image': ( isTheme && imageURL === null ), }); /* eslint-disable react/no-danger */ return ( <li className={resultClassnames}> <Link to={`/addon/${addon.slug}/`} className="SearchResult-link" ref={(el) => { this.name = el; }} > <div className={iconWrapperClassnames}> {imageURL ? ( <img className="SearchResult-icon" src={imageURL} alt="" /> ) : ( <p className="SearchResult-notheme"> {i18n.gettext('No theme preview available')} </p> )} </div> <div className="SearchResult-contents"> <h2 className="SearchResult-name">{addon.name}</h2> <p className="SearchResult-summary" dangerouslySetInnerHTML={sanitizeHTML(addon.summary)} /> <div className="SearchResult-metadata"> <div className="SearchResult-rating"> <Rating rating={addon.ratings.average} readOnly styleName="small" /> </div> { addon.authors && addon.authors.length ? ( <h3 className="SearchResult-author SearchResult--meta-section"> {addon.authors[0].name} </h3> ) : null } </div> </div> <h3 className="SearchResult-users SearchResult--meta-section"> <Icon className="SearchResult-users-icon" name="user-fill" /> <span className="SearchResult-users-text"> {i18n.sprintf(i18n.ngettext( '%(total)s user', '%(total)s users', averageDailyUsers), { total: i18n.formatNumber(averageDailyUsers) }, )} </span> </h3> </Link> </li> ); /* eslint-enable react/no-danger */ }
render() { const { className, errorHandler, hasSubmitted, i18n, isSubmitting, uiVisible, user, } = this.props; return ( <div className={makeClassName('ReportUserAbuse', className, { 'ReportUserAbuse--is-expanded': uiVisible, })} > {errorHandler.renderErrorIfPresent()} {!uiVisible && !hasSubmitted && ( <Button buttonType="neutral" className="ReportUserAbuse-show-more" disabled={!user} onClick={this.showReportUI} puffy > {i18n.gettext('Report this user for abuse')} </Button> )} {!hasSubmitted && ( <div className="ReportUserAbuse-form"> <h2 className="ReportUserAbuse-header"> {i18n.gettext('Report this user for abuse')} </h2> <p /* eslint-disable react/no-danger */ dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext( `If you think this user is violating %(linkTagStart)sMozilla's Add-on Policies%(linkTagEnd)s, please report this user to Mozilla.`, ), { linkTagStart: '<a href="https://developer.mozilla.org/en-US/Add-ons/AMO/Policy/Reviews">', linkTagEnd: '</a>', }, ), ['a'], )} /* eslint-enable react/no-danger */ /> <p> {i18n.gettext( `Please don't use this form to report bugs or contact this user; your report will only be sent to Mozilla and not to this user.`, )} </p> <DismissibleTextForm isSubmitting={isSubmitting} onDismiss={this.hideReportUI} onSubmit={this.sendReport} placeholder={i18n.gettext( 'Explain how this user is violating our policies.', )} submitButtonText={i18n.gettext('Send abuse report')} submitButtonInProgressText={i18n.gettext('Sending abuse report')} /> </div> )} {hasSubmitted && ( <div className="ReportUserAbuse--report-sent"> <h3 className="ReportUserAbuse-header"> {i18n.gettext('You reported this user for abuse')} </h3> <p> {i18n.gettext( `We have received your report. Thanks for letting us know about your concerns with this user.`, )} </p> <p> {i18n.gettext( `We can't respond to every abuse report but we'll look into this issue.`, )} </p> </div> )} </div> ); }
renderResult() { const { addon, i18n, _isAllowedOrigin, showMetadata, showSummary, } = this.props; const averageDailyUsers = addon ? addon.average_daily_users : null; // Fall-back to default icon if invalid icon url. const iconURL = getAddonIconUrl(addon); let imageURL = iconURL; if (addon && isTheme(addon.type)) { let themeURL = getPreviewImage(addon); if (!themeURL && addon && addon.type === ADDON_TYPE_THEME) { themeURL = addon.themeData && _isAllowedOrigin(addon.themeData.previewURL) ? addon.themeData.previewURL : null; } imageURL = themeURL; } // Sets classes to handle fallback if theme preview is not available. const iconWrapperClassnames = makeClassName('SearchResult-icon-wrapper', { 'SearchResult-icon-wrapper--no-theme-image': isTheme && imageURL === null, }); let addonAuthors = null; const addonAuthorsData = addon && addon.authors && addon.authors.length ? addon.authors : null; if (!addon || addonAuthorsData) { // TODO: list all authors. // https://github.com/mozilla/addons-frontend/issues/4461 const author = addonAuthorsData && addonAuthorsData[0]; addonAuthors = ( <h3 className="SearchResult-author SearchResult--meta-section"> {author ? author.name : <LoadingText />} </h3> ); } let summary = null; if (showSummary) { const summaryProps = {}; if (addon) { summaryProps.dangerouslySetInnerHTML = sanitizeHTML(addon.summary); } else { summaryProps.children = <LoadingText />; } summary = <p className="SearchResult-summary" {...summaryProps} />; } return ( <div className="SearchResult-result"> <div className={iconWrapperClassnames}> {imageURL ? ( <img className={makeClassName('SearchResult-icon', { 'SearchResult-icon--loading': !addon, })} src={imageURL} alt={addon ? `${addon.name}` : ''} /> ) : ( <p className="SearchResult-notheme"> {i18n.gettext('No theme preview available')} </p> )} </div> <div className="SearchResult-contents"> <h2 className="SearchResult-name"> {addon ? addon.name : <LoadingText />} </h2> {summary} {showMetadata ? ( <div className="SearchResult-metadata"> <div className="SearchResult-rating"> <Rating rating={addon && addon.ratings ? addon.ratings.average : 0} readOnly styleSize="small" /> </div> {addonAuthors} </div> ) : null} {addon && addon.notes && ( <div className="SearchResult-note"> <h4 className="SearchResult-note-header"> <Icon name="comments-blue" /> {i18n.gettext('Add-on note')} </h4> <p className="SearchResult-note-content" // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML(nl2br(addon.notes), [ 'br', ])} /> </div> )} </div> {!addon || (addon && addon.type !== ADDON_TYPE_OPENSEARCH) ? ( <h3 className="SearchResult-users SearchResult--meta-section"> <Icon className="SearchResult-users-icon" name="user-fill" /> <span className="SearchResult-users-text"> {averageDailyUsers !== null && averageDailyUsers !== undefined ? ( i18n.sprintf( i18n.ngettext( '%(total)s user', '%(total)s users', averageDailyUsers, ), { total: i18n.formatNumber(averageDailyUsers) }, ) ) : ( <LoadingText width={90} /> )} </span> </h3> ) : null} </div> ); }
render() { const { i18n, addon, OverallRating } = 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>', }); const iconUrl = isAllowedOrigin(addon.icon_url) ? addon.icon_url : fallbackIcon; return ( <div className="AddonDetail"> <header> <div className="icon"> <img alt="" src={iconUrl} /> <LikeButton /> </div> <div className="title"> <h1 dangerouslySetInnerHTML={sanitizeHTML(title, ['a', 'span'])} /> <InstallButton slug={addon.slug} /> </div> <div className="description"> <p dangerouslySetInnerHTML={sanitizeHTML(addon.summary)} /> </div> </header> <section className="addon-metadata"> <h2 className="visually-hidden">{i18n.gettext('Extension Metadata')}</h2> <AddonMeta /> </section> <hr /> <section className="screenshots"> <h2>{i18n.gettext('Screenshots')}</h2> <ScreenShots /> </section> <hr /> <section className="about"> <h2>{i18n.gettext('About this extension')}</h2> <div dangerouslySetInnerHTML={sanitizeHTML(nl2br(addon.description), allowedDescriptionTags)} /> </section> <hr /> <section className="overall-rating"> <h2>{i18n.gettext('Rate your experience')}</h2> <OverallRating addonName={addon.name} addonId={addon.id} version={addon.current_version} /> </section> </div> ); }
render() { const { i18n } = this.props; const title = i18n.gettext('About Firefox Add-ons'); return ( <Card className="StaticPage" header={title}> <Helmet> <title>{title}</title> </Helmet> <HeadMetaTags description={i18n.gettext(`The official Mozilla site for downloading Firefox extensions and themes. Add new features and change the browser’s appearance to customize your web experience.`)} title={title} /> <HeadLinks /> <div className="StaticPageWrapper"> <div id="about"> <p> {i18n.gettext(`Addons.mozilla.org (AMO), is Mozilla's official site for discovering and installing add-ons for the Firefox browser. Add-ons help you modify and personalize your browsing experience by adding new features to Firefox, enhancing your interactions with Web content, and changing the way your browser looks.`)} </p> <p // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext(`If you are looking for add-ons for Thunderbird or SeaMonkey, please visit %(startTBLink)saddons.thunderbird.net%(endTBLink)s or %(startSMLink)saddons.thunderbird.net/seamonkey%(endSMLink)s.`), { startTBLink: '<a href="https://addons.thunderbird.net/thunderbird/">', endTBLink: '</a>', startSMLink: '<a href="https://addons.thunderbird.net/seamonkey/">', endSMLink: '</a>', }, ), ['a'], )} /> </div> <section> <h2>{i18n.gettext('A community of creators')}</h2> <p> {i18n.gettext(`The add-ons listed here are created by thousands of developers and theme designers from all over the world, ranging from individual hobbyists to large corporations. Some add-ons listed on AMO have been automatically published and may be subject to review by a team of editors once publically listed.`)} </p> </section> <section> <h2>{i18n.gettext(`Get involved`)}</h2> <p> {i18n.gettext(`Mozilla is a non-profit champion of the Internet, we build Firefox to help keep it healthy, open and accessible. Add-ons support user choice and customization in Firefox, and you can contribute in the following ways:`)} </p> <ul> <li // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext(`%(startLink)sMake your own add-on%(endLink)s. We provide free hosting and update services and can help you reach a large audience of users.`), { startLink: '<a href="https://addons.mozilla.org/developers/">', endLink: '</a>', }, ), ['a'], )} /> <li // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext(`Help improve this website. It's open source, and you can file bugs and submit patches. You can get started with a %(startGoodFirstBugLink)sgood first bug%(endGoodFirstBugLink)s or view all open issues for AMO’s %(startAddonsServerRepoLink)sserver%(endAddonsServerRepoLink)s and %(startAddonsFrontendRepoLink)sfrontend%(endAddonsFrontendRepoLink)s on Github.`), { startGoodFirstBugLink: '<a href="https://github.com/search?l=&q=repo:mozilla/addons+repo:mozilla/addons-frontend+repo:mozilla/addons-linter+repo:mozilla/addons-server+label:%22contrib:+good+first+bug%22&ref=advsearch&state=open&type=Issues">', endGoodFirstBugLink: '</a>', startAddonsServerRepoLink: '<a href="https://github.com/mozilla/addons-server/issues">', endAddonsServerRepoLink: '</a>', startAddonsFrontendRepoLink: '<a href="https://github.com/mozilla/addons-frontend/issues">', endAddonsFrontendRepoLink: '</a>', }, ), ['a'], )} /> </ul> <p> {i18n.gettext( `If you want to contribute but are not quite as technical, there are still ways to help:`, )} </p> <ul> <li // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext( `Participate in our %(startLink)sforum%(endLink)s.`, ), { startLink: '<a href="https://discourse.mozilla-community.org/c/add-ons">', endLink: '</a>', }, ), ['a'], )} /> <li> {i18n.gettext(`Leave feedback for your favorite add-ons. Add-on authors are more likely to improve their add-ons and create new ones when they know people appreciate their work.`)} </li> <li> {i18n.gettext(`Tell your friends and family that Firefox is a fast, secure browser that protects their privacy, and they can use add-ons to make it their own!`)} </li> </ul> <p // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext( `To see more ways you can contribute to the add-on community, please visit our %(startLink)swiki%(endLink)s`, ), { startLink: '<a href="https://wiki.mozilla.org/Add-ons/Contribute">', endLink: '</a>', }, ), ['a'], )} /> </section> <section> <h2>{i18n.gettext('Get support')}</h2> <p // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext(`If you would like to learn more about how to manage add-ons in Firefox, or need to find general Firefox support, please visit %(startSUMOLink)sSupport%(endSUMOLink)s Mozilla. If you don't find an answer there, you can %(startForumLink)sask on our community forum%(endForumLink)s.`), { startSUMOLink: '<a href="https://support.mozilla.org/products/firefox/manage-preferences-and-add-ons-firefox/install-and-manage-add-ons">', endSUMOLink: '</a>', startForumLink: '<a href="https://discourse.mozilla-community.org/c/add-ons">', endForumLink: '</a>', }, ), ['a'], )} /> <p // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext( `%(startLink)sInformation about how to contact Mozilla's add-ons team can be found here%(endLink)s.`, ), { startLink: '<a href="https://wiki.mozilla.org/Add-ons#Getting_in_touch">', endLink: '</a>', }, ), ['a'], )} /> </section> </div> </Card> ); }
render() { const { addon, className, errorHandler, i18n } = this.props; const showNotes = addon.notes || this.props.uiState.editingNote; const iconURL = getAddonIconUrl(addon); return ( <li className={makeClassName( 'EditableCollectionAddon', `EditableCollectionAddon--${addon.type}`, className, )} > <div className="EditableCollectionAddon-details"> <img className="EditableCollectionAddon-icon" src={iconURL} alt={addon.name} /> <h2 className="EditableCollectionAddon-name">{addon.name}</h2> </div> <div className="EditableCollectionAddon-buttons"> <div className={makeClassName('EditableCollectionAddon-leaveNote', { 'EditableCollectionAddon-leaveNote--hidden': showNotes, })} > <Button buttonType="action" className="EditableCollectionAddon-leaveNote-button" micro onClick={this.onEditNote} > {i18n.gettext('Leave a note')} </Button> </div> <Button buttonType="alert" className="EditableCollectionAddon-remove-button" micro name={addon.id} onClick={this.onRemoveAddon} > {i18n.gettext('Remove')} </Button> </div> {showNotes && ( <div className="EditableCollectionAddon-notes"> <h4 className="EditableCollectionAddon-notes-header"> <Icon name="comments-blue" /> {i18n.gettext('User comment')} </h4> {this.props.uiState.editingNote ? ( <React.Fragment> {errorHandler.renderErrorIfPresent()} <DismissibleTextForm className="EditableCollectionAddon-notes-form" id={`${normalizeFileNameId(__filename)}-${extractId( this.props, )}`} microButtons onDelete={addon.notes ? this.onDeleteNote : null} onDismiss={this.onDismissNoteForm} onSubmit={this.onSaveNote} placeholder={i18n.gettext('Add a comment about this add-on.')} submitButtonText={i18n.gettext('Save')} text={addon.notes || null} /> </React.Fragment> ) : ( <div className="EditableCollectionAddon-notes-read-only"> <span className="EditableCollectionAddon-notes-content" // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( nl2br(addon.notes || ''), ['br'], )} /> <div className="EditableCollectionAddon-notes-buttons"> <Button buttonType="action" className="EditableCollectionAddon-notes-edit-button" micro onClick={this.onEditNote} > {i18n.gettext('Edit')} </Button> </div> </div> )} </div> )} </li> ); }
render() { const { i18n } = this.props; // We use `getLocalizedTextWithLinkParts()` two times to create all the // variables needed to display both the `Link` components and texts // before/after. let linkParts = getLocalizedTextWithLinkParts({ i18n, text: i18n.gettext( `Try visiting the page later, as the theme or extension may become available again. Alternatively, you may be able to find what you’re looking for in one of the available %(linkStart)sextensions%(linkEnd)s or %(secondLinkStart)sthemes%(secondLinkEnd)s.`, ), // By setting `linkStart`/`linkEnd` here, we can reuse // `getLocalizedTextWithLinkParts()` directly after. otherVars: { secondLinkStart: '%(linkStart)s', secondLinkEnd: '%(linkEnd)s', }, }); linkParts = { first: linkParts, second: getLocalizedTextWithLinkParts({ i18n, text: linkParts.afterLinkText, }), }; return ( <NestedStatus code={404}> <Card className="ErrorPage NotFound" header={i18n.gettext('Oops! We can’t find that page')} > <p> {i18n.gettext(`If you’ve followed a link from another site for an extension or theme, that item is no longer available. This could be because:`)} </p> <ul> <li> {i18n.gettext(`The developer removed it. Developers commonly do this because they no longer support the extension or theme, or have replaced it.`)} </li> <li> {i18n.gettext(`Mozilla removed it. This can happen when issues are found during the review of the extension or theme, or the extension or theme has been abusing the terms and conditions for addons.mozilla.org. The developer has the opportunity to resolve the issues and make the add-on available again.`)} </li> </ul> <p className="ErrorPage-paragraph-with-links" // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext(`If you’ve followed a link on this site, you’ve have found a mistake. Help us fix the link by <a href="%(url)s">filing an issue</a>. Tell us where you came from and what you were looking for, and we'll get it sorted.`), { url: 'https://github.com/mozilla/addons-frontend/issues/new/', }, ), ['a'], )} /> <p className="ErrorPage-paragraph-with-links"> {linkParts.first.beforeLinkText} <Link to={`/${visibleAddonType(ADDON_TYPE_EXTENSION)}/`}> {linkParts.first.innerLinkText} </Link> {linkParts.second.beforeLinkText} <Link to={`/${visibleAddonType(ADDON_TYPE_THEME)}/`}> {linkParts.second.innerLinkText} </Link> {linkParts.second.afterLinkText} </p> </Card> </NestedStatus> ); }
render() { const { addon, errorHandler, location, match: { params: { reviewId }, }, i18n, pageSize, reviewCount, reviews, } = this.props; if (errorHandler.hasError()) { log.warn('Captured API Error:', errorHandler.capturedError); // The following code attempts to recover from a 401 returned // by fetchAddon() but may accidentally catch a 401 from // fetchReviews(). Oh well. // TODO: support multiple error handlers, see // https://github.com/mozilla/addons-frontend/issues/3101 // // 401 and 403 are for an add-on lookup is made to look like a 404 on purpose. // See https://github.com/mozilla/addons-frontend/issues/3061 if ( errorHandler.capturedError.responseStatusCode === 401 || errorHandler.capturedError.responseStatusCode === 403 || errorHandler.capturedError.responseStatusCode === 404 ) { return <NotFound />; } } // When reviews have not loaded yet, make a list of 4 empty reviews // as a placeholder. const allReviews = reviews ? // Remove the Featured Review from the array. // TODO: Remove this code and use the API to filter out the featured // review once https://github.com/mozilla/addons-server/issues/9424 // is fixed. reviews.filter((review) => review.id.toString() !== reviewId) : Array(4).fill(null); const iconUrl = getAddonIconUrl(addon); const iconImage = ( <img className="AddonReviewList-header-icon-image" src={iconUrl} alt={i18n.gettext('Add-on icon')} /> ); let header; if (addon) { header = i18n.sprintf(i18n.gettext('Reviews for %(addonName)s'), { addonName: addon.name, }); } else { header = <LoadingText />; } const addonReviewCount = addon && addon.ratings ? addon.ratings.text_count : null; let addonName; let reviewCountHTML; if (addon && addonReviewCount !== null) { addonName = <Link to={this.addonURL()}>{addon.name}</Link>; reviewCountHTML = i18n.sprintf( i18n.ngettext( '%(total)s review for this add-on', '%(total)s reviews for this add-on', addonReviewCount, ), { total: i18n.formatNumber(addonReviewCount), }, ); } else { addonName = <LoadingText />; reviewCountHTML = <LoadingText />; } const authorProps = {}; if (addon && addon.authors) { const authorList = addon.authors.map((author) => { if (author.url) { return oneLine` <a class="AddonReviewList-addon-author-link" href="${author.url}" >${author.name}</a>`; } return author.name; }); const title = i18n.sprintf( // translators: Example: by The Author, The Next Author i18n.gettext('by %(authorList)s'), { addonName: addon.name, authorList: authorList.join(', '), }, ); authorProps.dangerouslySetInnerHTML = sanitizeHTML(title, ['a', 'span']); } else { authorProps.children = <LoadingText />; } /* eslint-disable jsx-a11y/heading-has-content */ const authorsHTML = ( <h3 className="AddonReviewList-header-authors" {...authorProps} /> ); /* eslint-enable jsx-a11y/heading-has-content */ const paginator = addon && reviewCount && pageSize && reviewCount > pageSize ? ( <Paginate LinkComponent={Link} count={reviewCount} currentPage={this.getCurrentPage(location)} pathname={this.url()} perPage={pageSize} /> ) : null; const metaHeader = ( <div className="AddonReviewList-header"> <div className="AddonReviewList-header-icon"> {addon ? <Link to={this.addonURL()}>{iconImage}</Link> : iconImage} </div> <div className="AddonReviewList-header-text"> <h1 className="visually-hidden">{header}</h1> <h2 className="AddonReviewList-header-addonName">{addonName}</h2> {authorsHTML} </div> </div> ); let addonAverage; if (addon && addon.ratings) { const averageRating = i18n.formatNumber(addon.ratings.average.toFixed(1)); addonAverage = i18n.sprintf( // translators: averageRating is a localized number, such as 4.5 // in English or ٤٫٧ in Arabic. i18n.gettext('%(averageRating)s star average'), { averageRating }, ); } return ( <div className={makeClassName( 'AddonReviewList', addon && addon.type ? [`AddonReviewList--${addon.type}`] : null, )} > {addon && ( <Helmet> <title>{header}</title> {reviewId && <meta name="robots" content="noindex, follow" />} </Helmet> )} {errorHandler.renderErrorIfPresent()} <Card header={metaHeader} className="AddonReviewList-addon"> <div className="AddonReviewList-overallRatingStars"> <Rating rating={addon && addon.ratings && addon.ratings.average} readOnly yellowStars /> <div className="AddonReviewList-addonAverage"> {addon ? addonAverage : <LoadingText minWidth={20} />} </div> </div> <RatingsByStar addon={addon} /> </Card> <div className="AddonReviewList-reviews"> {reviewId && ( <FeaturedAddonReview addon={addon} reviewId={reviewId} /> )} {allReviews.length ? ( <CardList className="AddonReviewList-reviews-listing" footer={paginator} header={reviewCountHTML} > <ul> {allReviews.map((review, index) => { return ( <li key={String(index)}> <AddonReviewCard addon={addon} review={review} /> </li> ); })} </ul> </CardList> ) : null} </div> </div> ); }
render() { const { abuseReport, addon, errorHandler, i18n, loading } = this.props; if (!addon) { return null; } if (abuseReport && abuseReport.message) { return ( <div className="ReportAbuseButton ReportAbuseButton--report-sent"> <h3 className="ReportAbuseButton-header"> {i18n.gettext('You reported this add-on for abuse')} </h3> <p className="ReportAbuseButton-first-paragraph"> {i18n.gettext( `We have received your report. Thanks for letting us know about your concerns with this add-on.`, )} </p> <p> {i18n.gettext( `We can't respond to every abuse report but we'll look into this issue.`, )} </p> </div> ); } const prefaceText = i18n.sprintf( i18n.gettext( `If you think this add-on violates %(linkTagStart)sMozilla's add-on policies%(linkTagEnd)s or has security or privacy issues, please report these issues to Mozilla using this form.`, ), { linkTagStart: '<a href="https://developer.mozilla.org/en-US/Add-ons/AMO/Policy/Reviews">', linkTagEnd: '</a>', }, ); // The button prompt mentions abuse to make it clear that you can't // use it to report general issues (like bugs) about the add-on. // See https://github.com/mozilla/addons-frontend/issues/4025#issuecomment-349103373 const prompt = i18n.gettext('Report this add-on for abuse'); /* eslint-disable react/no-danger */ return ( <div className={makeClassName('ReportAbuseButton', { 'ReportAbuseButton--is-expanded': abuseReport.uiVisible, })} > <div className="ReportAbuseButton--preview"> <Button buttonType="neutral" className="ReportAbuseButton-show-more" onClick={this.showReportUI} puffy > {prompt} </Button> </div> <div className="ReportAbuseButton--expanded"> <h3 className="ReportAbuseButton-header">{prompt}</h3> <p className="ReportAbuseButton-first-paragraph" dangerouslySetInnerHTML={sanitizeHTML(prefaceText, ['a'])} /> <p> {i18n.gettext( `Please don't use this form to report bugs or request add-on features; this report will be sent to Mozilla and not to the add-on developer.`, )} </p> {errorHandler.renderErrorIfPresent()} <DismissibleTextForm id={normalizeFileNameId(__filename)} isSubmitting={loading} onSubmit={this.sendReport} submitButtonText={i18n.gettext('Send abuse report')} submitButtonInProgressText={i18n.gettext('Sending abuse report')} onDismiss={this.dismissReportUI} dismissButtonText={i18n.gettext('Dismiss')} placeholder={i18n.gettext( 'Explain how this add-on is violating our policies.', )} /> </div> </div> ); /* eslint-enable react/no-danger */ }
render() { const { errorHandler, i18n, review } = this.props; const { reviewBody } = this.state; if (!review || !review.id || !review.reviewAddon.slug) { throw new Error(`Unexpected review property: ${JSON.stringify(review)}`); } let placeholder; let promptText; if (review.score && review.score > 3) { promptText = i18n.gettext( `Tell the world why you think this extension is fantastic! Please follow our %(linkStart)sreview guidelines%(linkEnd)s.`, ); placeholder = i18n.gettext( 'Tell us what you love about this extension. Be specific and concise.', ); } else { promptText = i18n.gettext( `Tell the world about this extension. Please follow our %(linkStart)sreview guidelines%(linkEnd)s.`, ); placeholder = i18n.gettext( 'Tell us about your experience with this extension. ' + 'Be specific and concise.', ); } const prompt = i18n.sprintf(promptText, { linkStart: '<a href="/review_guide">', linkEnd: '</a>', }); const overlayClassName = 'AddonReview-overlay'; return ( <OverlayCard visibleOnLoad onEscapeOverlay={this.props.onEscapeOverlay} className={overlayClassName} id={overlayClassName} > <h2 className="AddonReview-header">{i18n.gettext('Write a review')}</h2> {/* eslint-disable react/no-danger */} <p className="AddonReview-prompt" dangerouslySetInnerHTML={sanitizeHTML(prompt, ['a'])} /> {/* eslint-enable react/no-danger */} <UserRating styleSize="large" review={review} onSelectRating={this.onSelectRating} /> <form className="AddonReview-form" onSubmit={this.onSubmit}> <div className="AddonReview-form-input"> {errorHandler.renderErrorIfPresent()} <label htmlFor="AddonReview-textarea" className="visually-hidden"> {i18n.gettext('Review text')} </label> <textarea id="AddonReview-textarea" ref={(ref) => { this.reviewTextarea = ref; }} className="AddonReview-textarea" onInput={this.onBodyInput} name="review" value={reviewBody} placeholder={placeholder} /> </div> <input className="AddonReview-submit" type="submit" value={i18n.gettext('Submit review')} /> </form> </OverlayCard> ); }
it('removes `target` attribute on links', () => { const html = '<a href="http://example.org" target="_blank">link</a>'; expect(sanitizeHTML(html, ['a'])).toEqual({ __html: '<a href="http://example.org">link</a>', }); });
it('does not change links', () => { const html = '<a href="http://example.org">link</a>'; expect(sanitizeHTML(html, ['a'])).toEqual({ __html: html, }); });
render() { const { errorHandler, i18n, isUnsubscribed, match } = this.props; const { token, notificationName } = match.params; const editProfileLink = replaceStringsWithJSX({ text: i18n.gettext( 'You can edit your notification settings by %(linkStart)sediting your profile%(linkEnd)s.', ), replacements: [ [ 'linkStart', 'linkEnd', (text) => ( <Link key="edit-profile" to="/users/edit"> {text} </Link> ), ], ], }); return ( <div className="UsersUnsubscribe"> <Helmet> <title>{i18n.gettext('Unsubscribe')}</title> </Helmet> {errorHandler.hasError() ? ( errorHandler.renderError() ) : ( <Card header={ isUnsubscribed ? ( i18n.gettext('You are successfully unsubscribed!') ) : ( <LoadingText /> ) } > {isUnsubscribed ? ( <p className="UsersUnsubscribe-content-explanation" // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( // translators: a list of notifications will be displayed under this prompt. i18n.gettext( `The email address %(strongStart)s%(email)s%(strongEnd)s will no longer get messages when:`, ), { strongStart: '<strong>', strongEnd: '</strong>', email: base64url.decode(token), }, ), ['strong'], )} /> ) : ( <p className="UsersUnsubscribe-content-explanation"> <LoadingText minWidth={40} /> </p> )} <blockquote className="UsersUnsubscribe-content-notification"> {isUnsubscribed ? ( getNotificationDescription(i18n, notificationName) ) : ( <LoadingText range={30} /> )} </blockquote> <p className="UsersUnsubscribe-content-edit-profile"> {isUnsubscribed ? editProfileLink : <LoadingText />} </p> </Card> )} </div> ); }
render() { const { errorHandler, i18n, onCancel, review, flashMessage, puffyButtons, } = this.props; const isReply = review.isDeveloperReply; const reviewGuideLink = i18n.sprintf( i18n.gettext( 'Please follow our %(linkStart)sreview guidelines%(linkEnd)s.', ), { linkStart: '<a href="/review_guide">', linkEnd: '</a>', }, ); /* eslint-disable react/no-danger */ const formFooter = !isReply ? ( <span dangerouslySetInnerHTML={sanitizeHTML(reviewGuideLink, ['a'])} /> ) : ( undefined ); /* eslint-enable react/no-danger */ const placeholder = i18n.gettext( 'Write about your experience with this add-on.', ); let submitButtonText = i18n.gettext('Submit review'); let submitButtonInProgressText = i18n.gettext('Submitting review'); if (review.body) { submitButtonText = isReply ? i18n.gettext('Update reply') : i18n.gettext('Update review'); submitButtonInProgressText = isReply ? i18n.gettext('Updating reply') : i18n.gettext('Updating review'); } return ( <div className="AddonReviewManager"> {errorHandler.renderErrorIfPresent()} {!isReply && ( <div className="AddonReviewManager-starRating"> <span>{i18n.gettext('Your star rating:')}</span> <Rating className="AddonReviewManager-Rating" onSelectRating={this.onSubmitRating} rating={ flashMessage === STARTED_SAVE_RATING ? undefined : review.score } styleSize="small" yellowStars /> <span className={makeClassName('AddonReviewManager-savedRating', { 'AddonReviewManager-savedRating-hidden': flashMessage !== STARTED_SAVE_RATING && flashMessage !== SAVED_RATING, })} > {flashMessage === STARTED_SAVE_RATING ? i18n.gettext('Saving') : i18n.gettext('Saved')} </span> </div> )} <DismissibleTextForm dismissButtonText={i18n.gettext('Cancel')} formFooter={formFooter} isSubmitting={flashMessage === STARTED_SAVE_REVIEW} onDismiss={onCancel} onSubmit={this.onSubmitReview} placeholder={placeholder} puffyButtons={puffyButtons} submitButtonText={submitButtonText} submitButtonInProgressText={submitButtonInProgressText} text={review.body} /> </div> ); }