Example #1
0
export function trailingSlashesMiddleware(
  req,
  res,
  next,
  { _config = config } = {},
) {
  const UrlParts = req.originalUrl.split('?');
  const UrlSlashSeparated = UrlParts[0].replace(/^\//, '').split('/');

  // If part of this URL should include a lang or clientApp, check for one
  // and make sure they're valid.
  if (isValidLang(UrlSlashSeparated[0], { _config })) {
    UrlSlashSeparated[0] = '$lang';
  }
  if (isValidClientApp(UrlSlashSeparated[1], { _config })) {
    UrlSlashSeparated[1] = '$clientApp';
  } else if (isValidClientApp(UrlSlashSeparated[0], { _config })) {
    // It's possible there is a clientApp in the first part of the URL if this
    // URL is in validLocaleUrlExceptions.
    UrlSlashSeparated[0] = '$clientApp';
  }

  const urlToCheck = `/${UrlSlashSeparated.join('/')}`;

  // If the URL doesn't end with a trailing slash, and it isn't an exception,
  // we'll add a trailing slash.
  if (
    !isValidTrailingSlashUrlException(urlToCheck, { _config }) &&
    UrlParts[0].substr(-1) !== '/'
  ) {
    UrlParts[0] = `${UrlParts[0]}/`;
    return res.redirect(301, UrlParts.join('?'));
  }

  if (isValidTrailingSlashUrlException(urlToCheck, { _config })) {
    log.info(
      `Not adding a trailing slash; validTrailingSlashUrlException found: ${urlToCheck}`,
    );
  }

  return next();
}
export function prefixMiddleWare(req, res, next, { _config = config } = {}) {
  // Split on slashes after removing the leading slash.
  const URLParts = req.originalUrl.replace(/^\//, '').split('/');
  log.debug(URLParts);

  // Get lang and app parts from the URL. At this stage they may be incorrect
  // or missing.
  const [langFromURL, appFromURL] = URLParts;

  // Get language from URL or fall-back to detecting it from accept-language header.
  const acceptLanguage = req.headers['accept-language'];
  const { lang, isLangFromHeader } = getLanguage({ lang: langFromURL, acceptLanguage });

  const hasValidLang = isValidLang(langFromURL);
  const hasValidClientApp = isValidClientApp(appFromURL, { _config });

  let prependedLang = false;

  if ((hasValidLang && langFromURL !== lang) ||
      (!hasValidLang && hasValidClientApp)) {
    // Replace the first part of the URL if:
    // * It's valid and we've mapped it e.g: pt -> pt-PT.
    // * The lang is invalid but we have a valid application
    //   e.g. /bogus/firefox/.
    log.info(`Replacing lang in URL ${URLParts[0]} -> ${lang}`);
    URLParts[0] = lang;
  } else if (!hasValidLang) {
    // If lang wasn't valid or was missing prepend one.
    log.info(`Prepending lang to URL: ${lang}`);
    URLParts.splice(0, 0, lang);
    prependedLang = true;
  }

  let isApplicationFromHeader = false;

  if (!hasValidClientApp && prependedLang &&
      isValidClientApp(URLParts[1], { _config })) {
    // We skip prepending an app if we'd previously prepended a lang and the
    // 2nd part of the URL is now a valid app.
    log.info('Application in URL is valid following prepending a lang.');
  } else if (!hasValidClientApp) {
    // If the app supplied is not valid we need to prepend one.
    const application = getClientApp(req.headers['user-agent']);
    log.info(`Prepending application to URL: ${application}`);
    URLParts.splice(1, 0, application);
    isApplicationFromHeader = true;
  }

  // Redirect to the new URL.
  // For safety we'll deny a redirect to a URL starting with '//' since
  // that will be treated as a protocol-free URL.
  const newURL = `/${URLParts.join('/')}`;
  if (newURL !== req.originalUrl && !newURL.startsWith('//')) {
    // Collect vary headers to apply to the redirect
    // so we can make it cacheable.
    // TODO: Make the redirects cacheable by adding expires headers.
    const varyHeaders = [];
    if (isLangFromHeader) {
      varyHeaders.push('accept-language');
    }
    if (isApplicationFromHeader) {
      varyHeaders.push('user-agent');
    }
    res.set('vary', varyHeaders);
    return res.redirect(302, newURL);
  }

  // Add the data to res.locals to be utilised later.
  /* eslint-disable no-param-reassign */
  const [newLang, newApp] = URLParts;
  res.locals.lang = newLang;
  res.locals.clientApp = newApp;
  /* eslint-enable no-param-reassign */

  return next();
}
Example #3
0
    match({ location: req.url, routes }, (
      err, redirectLocation, renderProps
    ) => {
      cookie.plugToRequest(req, res);

      if (err) {
        log.error({ err, req });
        return showErrorPage(res, 500);
      }

      if (!renderProps) {
        return showErrorPage(res, 404);
      }

      const store = createStore();
      const token = cookie.load(config.get('cookieName'));
      if (token) {
        store.dispatch(setJwt(token));
      }
      // Get SRI for deployed services only.
      const sriData = (isDeployed) ? JSON.parse(
        fs.readFileSync(path.join(config.get('basePath'), 'dist/sri.json'))
      ) : {};

      // Check the lang supplied by res.locals.lang for validity
      // or fall-back to the default.
      const lang = isValidLang(res.locals.lang) ?
        res.locals.lang : config.get('defaultLang');
      const dir = getDirection(lang);
      const locale = langToLocale(lang);
      store.dispatch(setLang(lang));
      if (res.locals.clientApp) {
        store.dispatch(setClientApp(res.locals.clientApp));
      } else {
        log.warn(`No clientApp for this URL: ${req.url}`);
      }

      function hydrateOnClient(props = {}) {
        const pageProps = {
          appName: appInstanceName,
          assets: webpackIsomorphicTools.assets(),
          htmlLang: lang,
          htmlDir: dir,
          includeSri: isDeployed,
          noScriptStyles,
          sriData,
          store,
          trackingEnabled: convertBoolean(config.get('trackingEnabled')),
          ...props,
        };

        const HTML = ReactDOM.renderToString(
          <ServerHtml {...pageProps} />);
        res.send(`<!DOCTYPE html>\n${HTML}`);
      }

      // Set disableSSR to true to debug
      // client-side-only render.
      if (config.get('disableSSR') === true) {
        return Promise.resolve(hydrateOnClient());
      }

      return loadOnServer({ ...renderProps, store })
        .then(() => {
          // eslint-disable-next-line global-require
          let i18nData = {};
          try {
            if (locale !== langToLocale(config.get('defaultLang'))) {
              // eslint-disable-next-line global-require, import/no-dynamic-require
              i18nData = require(
                `../../locale/${locale}/${appInstanceName}.js`);
            }
          } catch (e) {
            log.info(
              `Locale JSON not found or required for locale: "${locale}"`);
            log.info(
              `Falling back to default lang: "${config.get('defaultLang')}".`);
          }
          const i18n = makeI18n(i18nData, lang);

          const InitialComponent = (
            <I18nProvider i18n={i18n}>
              <Provider store={store} key="provider">
                <ReduxAsyncConnect {...renderProps} />
              </Provider>
            </I18nProvider>
          );

          const asyncConnectLoadState = store.getState().reduxAsyncConnect.loadState || {};
          const reduxResult = getReduxConnectError(asyncConnectLoadState);
          if (reduxResult.status) {
            return showErrorPage(res, reduxResult.status);
          }

          return hydrateOnClient({ component: InitialComponent });
        })
        .catch((error) => {
          log.error({ err: error });
          return showErrorPage(res, 500);
        });
    });
Example #4
0
    match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
      cookie.plugToRequest(req, res);

      if (err) {
        log.error({ err, req });
        return showErrorPage(res, 500);
      }

      if (!renderProps) {
        return showErrorPage(res, 404);
      }

      const store = createStore();
      const token = cookie.load(config.get('cookieName'));
      if (token) {
        store.dispatch(setJWT(token));
      }
      // Get SRI for deployed services only.
      const sriData = (isDeployed) ? JSON.parse(
        fs.readFileSync(path.join(config.get('basePath'), 'dist/sri.json'))
      ) : {};

      // Check the lang supplied by res.locals.lang for validity
      // or fall-back to the default.
      const lang = isValidLang(res.locals.lang) ? res.locals.lang : config.get('defaultLang');
      const dir = getDirection(lang);
      const locale = langToLocale(lang);
      store.dispatch(setLang(lang));

      function hydrateOnClient(props = {}) {
        const pageProps = {
          appName: appInstanceName,
          assets: webpackIsomorphicTools.assets(),
          htmlLang: lang,
          htmlDir: dir,
          includeSri: isDeployed,
          sriData,
          store,
          trackingEnabled: convertBoolean(config.get('trackingEnabled')),
          ...props,
        };

        const HTML = ReactDOM.renderToString(
          <ServerHtml {...pageProps} />);
        res.send(`<!DOCTYPE html>\n${HTML}`);
      }

      // Set disableSSR to true to debug
      // client-side-only render.
      if (config.get('disableSSR') === true) {
        return Promise.resolve(hydrateOnClient());
      }

      return loadOnServer({ ...renderProps, store })
        .then(() => {
          // eslint-disable-next-line global-require
          let jedData = {};
          try {
            if (locale !== langToLocale(config.get('defaultLang'))) {
              // eslint-disable-next-line global-require, import/no-dynamic-require
              jedData = require(`json!../../locale/${locale}/${appInstanceName}.json`);
            }
          } catch (e) {
            log.info(`Locale JSON not found or required for locale: "${locale}"`);
            log.info(`Falling back to default lang: "${config.get('defaultLang')}".`);
          }
          const i18n = new Jed(jedData);
          const InitialComponent = (
            <I18nProvider i18n={i18n}>
              <Provider store={store} key="provider">
                <ReduxAsyncConnect {...renderProps} />
              </Provider>
            </I18nProvider>
          );

          const asyncConnectLoadState = store.getState().reduxAsyncConnect.loadState || {};

          // Create a list of any apiErrors detected.
          const apiErrors = Object.keys(asyncConnectLoadState)
            .map((item) => asyncConnectLoadState[item].error)
            .filter((item) => item);

          if (apiErrors.length === 1) {
            // If we have a single API error reflect that in the page's response.
            const apiStatus = apiErrors[0].response.status;
            return showErrorPage(res, apiStatus);
          } else if (apiErrors.length > 1) {
            // Otherwise we have multiple api errors it should be logged
            // and throw a 500.
            log.error(apiErrors);
            return showErrorPage(res, 500);
          }

          return hydrateOnClient({ component: InitialComponent });
        })
        .catch((error) => {
          log.error({ err: error });
          return showErrorPage(res, 500);
        });
    });
 it('should see bogus value as invalid lang', () => {
   expect(utils.isValidLang('awooga')).toEqual(false);
 });
 it('should see pt as an invalid lang since it requires mapping', () => {
   expect(utils.isValidLang('pt')).toEqual(false);
 });
 it('should see en-US as a valid lang', () => {
   expect(utils.isValidLang('en-US')).toEqual(true);
 });
 it('should see incorrect type as invalid lang', () => {
   expect(utils.isValidLang(1)).toEqual(false);
 });
 it('should see pt as an invalid lang since it requires mapping', () => {
   assert.equal(utils.isValidLang('pt'), false);
 });
 it('should see en_US as an invalid lang', () => {
   expect(utils.isValidLang('en_US')).toEqual(false);
 });
Example #11
0
 it('should see bogus value as invalid lang', () => {
   assert.equal(utils.isValidLang('awooga'), false);
 });
Example #12
0
 it('should see incorrect type as invalid lang', () => {
   assert.equal(utils.isValidLang(1), false);
 });
Example #13
0
 it('should see en-US as a valid lang', () => {
   assert.equal(utils.isValidLang('en-US'), true);
 });
Example #14
0
 it('should see en_US as an invalid lang', () => {
   assert.equal(utils.isValidLang('en_US'), false);
 });
Example #15
0
export function prefixMiddleware(req, res, next, { _config = config } = {}) {
  // Split on slashes after removing the leading slash.
  const URLParts = req.originalUrl.replace(/^\//, '').split('/');

  // Get lang and app parts from the URL. At this stage they may be incorrect
  // or missing.
  const [langFromURL, appFromURL] = URLParts;

  // Get language from URL or fall-back to detecting it from accept-language
  // header.
  const acceptLanguage = req.headers['accept-language'];
  const { lang, isLangFromHeader } = getLanguage({
    lang: langFromURL,
    acceptLanguage,
  });
  // Get the application from the UA if one wasn't specified in the URL (or
  // if it turns out to be invalid).
  const application = getClientApp(req.headers['user-agent']);
  // clientApp values that are allowed through to the router
  // TODO: This can be removed when we upgrade to react-router v4.
  const clientAppRoutes = _config.get('clientAppRoutes');

  const hasValidLang = isValidLang(langFromURL);
  const hasValidLocaleException = isValidLocaleUrlException(appFromURL, {
    _config,
  });
  const hasValidClientApp = isValidClientApp(appFromURL, { _config });
  let hasValidClientAppUrlException = isValidClientAppUrlException(appFromURL, {
    _config,
  });

  let isApplicationFromHeader = false;
  let prependedOrMovedApplication = false;

  if (hasValidLocaleException) {
    log.info(oneLine`Second part of URL is a locale exception (${URLParts[1]});
      make sure the clientApp is valid`);

    // Normally we look for a clientApp in the second part of a URL, but URLs
    // that match a locale exception don't have a locale so we look for the
    // clientApp in the first part of the URL.
    if (!isValidClientApp(langFromURL, { _config })) {
      URLParts[0] = application;
      isApplicationFromHeader = true;
      prependedOrMovedApplication = true;
    }
  } else if (
    (hasValidLang && langFromURL !== lang) ||
    hasValidClientApp ||
    hasValidClientAppUrlException
  ) {
    // Replace the first part of the URL if:
    // * It's valid and we've mapped it e.g: pt -> pt-PT.
    // * The lang is invalid but we have a valid application
    //   e.g. /bogus/firefox/.
    log.info(`Replacing lang in URL ${URLParts[0]} -> ${lang}`);
    URLParts[0] = lang;
  } else if (isValidLocaleUrlException(URLParts[0], { _config })) {
    log.info(`Prepending clientApp to URL: ${application}`);
    URLParts.splice(0, 0, application);
    isApplicationFromHeader = true;
    prependedOrMovedApplication = true;
  } else if (!hasValidLang) {
    // If lang wasn't valid or was missing prepend one.
    log.info(`Prepending lang to URL: ${lang}`);
    URLParts.splice(0, 0, lang);
    // If we've prepended the lang to the URL we need to re-check our
    // URL exception and make sure it's valid.
    hasValidClientAppUrlException = isValidClientAppUrlException(URLParts[1], {
      _config,
    });
  }

  if (!hasValidClientApp && isValidClientApp(URLParts[1], { _config })) {
    // We skip prepending an app if we'd previously prepended a lang and the
    // 2nd part of the URL is now a valid app.
    log.info('Application in URL is valid following prepending a lang.');
  } else if (prependedOrMovedApplication) {
    log.info(
      'URL is valid because we added/changed the first part to a clientApp.',
    );
  } else if (hasValidLocaleException || hasValidClientAppUrlException) {
    if (
      clientAppRoutes.includes(URLParts[1]) === false &&
      (hasValidLang || hasValidLocaleException)
    ) {
      log.info('Exception in URL found; we fallback to addons-server.');
      // TODO: Remove this once upgraded to react-router 4.
      res.status(404).end(oneLine`This page does not exist in addons-frontend.
        Returning 404; this error should trigger upstream (usually
        addons-server) to return a valid response.`);
      return next();
    }
    log.info('Exception in URL found; prepending lang to URL.');
  } else if (!hasValidClientApp) {
    // If the app supplied is not valid we need to prepend one.
    log.info(`Prepending application to URL: ${application}`);
    URLParts.splice(1, 0, application);
    isApplicationFromHeader = true;
  }

  // Redirect to the new URL.
  // For safety we'll deny a redirect to a URL starting with '//' since
  // that will be treated as a protocol-free URL.
  const newURL = `/${URLParts.join('/')}`;
  if (newURL !== req.originalUrl && !newURL.startsWith('//')) {
    // Collect vary headers to apply to the redirect
    // so we can make it cacheable.
    // TODO: Make the redirects cacheable by adding expires headers.
    if (isLangFromHeader) {
      res.vary('accept-language');
    }
    if (isApplicationFromHeader) {
      res.vary('user-agent');
    }
    return res.redirect(301, newURL);
  }

  // Add the data to res.locals to be utilised later.
  /* eslint-disable no-param-reassign */
  const [newLang, newApp] = URLParts;
  res.locals.lang = newLang;
  // The newApp part of the URL might not be a client application
  // so it's important to re-check that here before assuming it's good.
  res.locals.clientApp = isValidClientApp(newApp) ? newApp : application;
  // Get detailed info on the current user agent so we can make sure add-ons
  // are compatible with the current clientApp/version combo.
  res.locals.userAgent = req.headers['user-agent'];
  /* eslint-enable no-param-reassign */

  return next();
}