WikiEducationFoundation/WikiEduDashboard

View on GitHub
app/assets/javascripts/components/common/ArticleViewer/utils/ArticleViewerAPI.js

Summary

Maintainability
B
5 hrs
Test Coverage
F
56%
import fetch from 'cross-fetch';

export class ArticleViewerAPI {
  constructor({ builder }) {
    this.builder = builder;
  }

  __whocolorStatus(html, whocolor) {
    if (html && whocolor) {
      return { whocolorFetched: true };
    } else if (!html && whocolor) {
      return { whocolorFailed: true };
    }
  }

  __processHtml(html, whocolor) {
    const status = this.__whocolorStatus(html, whocolor);
    if (!html) return status;

    // The mediawiki parse API returns the same HTML as the rendered article on
    // Wikipedia. This means relative links to other articles are broken.
    // Here we turn them into full urls pointing back to the wiki.
    // However, the page-local anchor links for footnotes and references are
    // fine; they should link to the footnotes within the ArticleViewer.
    const absoluteLink = `<a href="${this.builder.wikiURL()}/`;
    // This matches links that don't start with # or http. These are
    // assumed to be relative links to other wiki pages. It supports
    // an optional title attribute like `title="Property:P31"`, which is
    // included in Wikidata links, but will strip that title out.
    const relativeLinkMatcher = /(<a (title="[\w\d:]+" )?href=")(?!http)[^#]/g;
    return {
      html: html.replace(relativeLinkMatcher, absoluteLink),
      ...status
    };
  }

  __setException(response) {
    if (response.status === 0) {
      return 'Not connect.\n Verify Network.';
    } else if (response.status.toString() === '404') {
      return 'Requested page not found. [404]';
    } else if (response.status.toString() === '500') {
      return 'Internal Server Error [500].';
    }

    return `Uncaught Error.\n${response.statusText}`;
  }

  __handleFetchResponse(response) {
    if (!response.ok) throw new Error(this.__setException(response));
    return response.json();
  }

  // This function sets up a timer for the request to the highlighting
  // API endpoint so that we can make requests on a delay.
  __wikiwhoColorURLTimedRequestPromise(timeout, lastRevisionId) {
    const url = this.builder.wikiwhoColorURL(lastRevisionId);
    return new Promise((resolve, reject) => {
      const headers = { 'Content-Type': 'application/javascript' };
      setTimeout(() => {
        fetch(`${url}?origin=*`, { headers })
          .then(response => (response.ok ? resolve(response) : reject(response)));
      }, timeout * 1000);
    });
  }

  fetchParsedArticle(lastRevisionId) {
    const url = this.builder.parsedArticleURL(lastRevisionId);
    // Adding `origin=*` allows for requests to go to en.wikipedia.org
    // as referenced by this URL:
    // https://www.mediawiki.org/wiki/API:Cross-site_requests#CORS_usage
    return fetch(`${url}&origin=*`, {
      headers: {
        'Content-Type': 'application/javascript'
      }
    }).then(response => this.__handleFetchResponse(response))
      .then((response) => {
        if (response.error) throw new Error(this.__setException({ status: 404 }));
        return {
          articlePageId: response.parse.pageid,
          fetched: true,
          parsedArticle: this.__processHtml(response.parse.text['*'])
        };
      });
  }

  generateWhocolorHtml() {
    const url = this.builder.wikiwhoColorRevisionURL();
    return fetch(`${url}&origin=*`, {
      headers: {
        'Content-Type': 'application/javascript'
      }
    }).then(response => this.__handleFetchResponse(response));
  }

  fetchWhocolorHtml(lastRevisionId) {
    let attempts = 0;
    const MAX_RETRY_ATTEMPTS = 5;

    // This function is defined in this way so that the variable name
    // will be hoisted, allowing it to call itself.
    function colorURLRequest(timeout = 0) {
      return this.__wikiwhoColorURLTimedRequestPromise(timeout, lastRevisionId)
        .then(response => response.json())
        .then((response) => {
          if (response.success) return Promise.resolve(response);

          // If the data isn't already available on the wikiwho server,
          // it may return a 200 response with `success: false`.
          // In this case, we will retry a few times.
          attempts += 1;
          if (attempts <= MAX_RETRY_ATTEMPTS) return colorURLRequest.call(this, attempts);

          // Handle the case when the key 'info' is not present in response.
          const info = response.info ? response.info : '';

          const err = `Request failed after ${MAX_RETRY_ATTEMPTS} attempts. ${info}`;
          throw new Error(err);
        });
    }

    return colorURLRequest.call(this)
      .then(response => this.__processHtml(response.extended_html, true));
  }

  fetchUserIds() {
    const url = this.builder.wikiUserQueryURL();
    return fetch(`${url}&origin=*`, {
      headers: {
        'Content-Type': 'application/javascript'
      }
    }).then(response => this.__handleFetchResponse(response));
  }

  fetchWikitextMetaData() {
    const url = this.builder.wikiwhoColorRevisionURL();
    return fetch(`${url}&origin=*`, {
      headers: {
        'Content-Type': 'application/javascript'
      }
    }).then(response => this.__handleFetchResponse(response))
      .then((response) => {
        if (response.error) throw new Error(this.__setException({ status: 404 }));
        const revisionId = 'revisions'; // Get the first (and presumably only) revision ID
        const revisionData = response[revisionId]?.[0];
        if (!revisionData) throw new Error('Invalid response data');
        const { tokens } = Object.values(revisionData)[0];
        return { tokensForRevision: tokens };
      });
  }
}

export default ArticleViewerAPI;