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 */
  }
Example #3
0
  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'])}
      />
    );
  }
Example #4
0
  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>
    );
  }
Example #5
0
  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>
    );
  }
Example #7
0
  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>
    );
  }
Example #9
0
  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>
    );
  }
Example #11
0
  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>
    );
  }
Example #12
0
  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>
    );
  }
Example #13
0
  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>
    );
  }
Example #15
0
  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,
   });
 });
Example #19
0
  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>
    );
  }