const cheerio = require('cheerio');

// The oembed module provides an oembed query service for embedding
// third-party website content, such as YouTube videos. The service includes
// enhancements and substitutes for several services that do not support
// oembed or do not support it well, and it is possible to add more by
// extending the `enhanceOembetter` method. The server-side
// code provides a query route with caching. The browser-side code
// provides methods to make a query, and also to make a query and then
// immediately display the result.
//
// Also see the [oembetter](https://www.npmjs.com/package/oembetter) npm module and
// the [oembed](http://oembed.com/) documentation.
//
// Sites to be embedded need to be allowlisted, to avoid XSS attacks. Many
// widely trusted sites are already allowlisted.
//
// Your `allowlist` option is concatenated with `oembetter`'s standard
// allowlist, plus wufoo.com, infogr.am, and slideshare.net.
//
// Your `endpoints` option is concatenated with `oembetter`'s standard
// endpoints list.

module.exports = {
  options: {
    alias: 'oembed',
    cacheLifetime: 60 * 60
  },
  init(self) {
    self.createOembetter();
    self.enhanceOembetter();
    self.enableBrowserData();
  },
  methods(self) {
    return {

      // Creates an instance of the `oembetter` module and adds the standard
      // allowlist. Called by `afterConstruct`.

      createOembetter() {
        self.oembetter = require('oembetter')();
        // Don't permit oembed of untrusted sites, which could
        // lead to XSS attacks

        self.oembetter.allowlist(self.oembetter.suggestedAllowlist.concat(
          self.options.allowlist || [],
          [
            'wufoo.com',
            'infogr.am',
            'slideshare.net'
          ])
        );
        self.oembetter.endpoints(
          self.oembetter.suggestedEndpoints.concat(self.options.endpoints || [])
        );
      },

      // Enhances oembetter to support services better or to support services
      // that have no oembed support by default. Called by `afterConstruct`.
      // Extend this method to add additional `oembetter` filters.

      enhanceOembetter() {
        require('./lib/youtube.js')(self, self.oembetter);
        require('./lib/vimeo.js')(self, self.oembetter);
        require('./lib/wufoo.js')(self, self.oembetter);
        require('./lib/infogram.js')(self, self.oembetter);
      },

      // This method fetches the specified URL, determines its best embedded
      // representation via `oembetter`, and on success returns an
      // object containing the oembed API response from the service provider.
      //
      // If `options.alwaysIframe` is true, the result is a simple
      // iframe of the URL. If `options.iframeHeight` is set, the iframe
      // has that height in pixels, otherwise it is left to CSS. These
      // iframe-related options are not used in core Apostrophe and remain
      // available to project-level application code for backwards compatibility
      // purposes only.
      //
      // The `options` object is passed on to `oembetter.fetch`.
      //
      // Responses are automatically cached, by default for one hour. See the
      // cacheLifetime option to the module.
      async query(req, url, options) {
        if (!options) {
          options = {};
        }
        if (!options.headers) {
          options = {
            ...options,
            headers: {
              // Enables access to vimeo private videos locked to this domain
              Referer: req.baseUrlWithPrefix
            }
          };
        }
        if (!options.headers.Referer) {
          options.headers = {
            ...options.headers,
            Referer: req.baseUrlWithPrefix
          };
        }
        // Tolerant URL handling
        url = self.apos.launder.url(url, null, true);
        if (!url) {
          throw self.apos.error('invalid', req.t('apostrophe:oembedVideoUrlInvalid'));
        }
        const key = url + ':' + JSON.stringify(options);
        let response = await self.apos.cache.get('@apostrophecms/oembed', key);
        if (response !== undefined) {
          return response;
        }
        if (options.alwaysIframe) {
          response = await self.iframe(req, url, options);
        } else {
          try {
            response = await require('util').promisify(self.oembetter.fetch)(url, options);
          } catch (err) {
            throw self.apos.error('invalid', req.t('apostrophe:oembedVideoUrlInvalid'));
          }
        }
        // Make non-secure URLs protocol relative and
        // let the browser upgrade them to https if needed
        function makeProtocolRelative(s) {
          s = s.replace(/^http:\/\//, '//');
          return s.replace(/(["'])http:\/\//g, '$1//');
        }
        if (response.thumbnail_url) {
          response.thumbnail_url = makeProtocolRelative(response.thumbnail_url);
        }
        if (response.html) {
          response.html = makeProtocolRelative(response.html);
        }
        // cache oembed responses for one hour
        await self.apos.cache.set('@apostrophecms/oembed', key, response, self.options.cacheLifetime);
        return response;
      },
      // Not currently used. Present for backwards compatibility
      // in the event that application code used it directly.
      //
      // Given a URL, return an oembed response for it
      // which just iframes the URL given. Fetches the page
      // first to get the title property.
      //
      // If options.iframeHeight is set, use that # of
      // pixels, otherwise do not specify & let CSS do it.
      //
      // Called by `self.query` if the `alwaysIframe` option
      // is true.
      async iframe(req, url, options) {
        const body = await self.apos.http.get(url);
        const $ = cheerio.load(body);
        let title = url;
        if ($) {
          title = $('meta[property="og:title"]').attr('content') || $('title').text();
          if (!title) {
            // A common goof these days
            title = $('h1').text();
            if (!title) {
              // Oh c'mon
              title = url;
            }
          }
        }
        let style = '';
        let html = '<iframe STYLE src="' + self.escapeHtml(url) + '" class="apos-always-iframe"></iframe>';
        if (options.iframeHeight) {
          style = 'style="height:' + options.iframeHeight + 'px"';
        }
        html = html.replace('STYLE', style);
        return {
          title,
          type: 'rich',
          html
        };
      },
      // Returns browser-side javascript to load a given
      // cross-domain js file dynamically and then run
      // the javascript code in the `then` string argument.
      // `script` should be a URL pointing to the third-party
      // js file and may start with // to autoselect
      // http or https depending on how the page was loaded.
      //
      // You may supply an id attribute for the script tag.
      // Some services rely on these (infogr.am).
      //
      // You may also supply the ID of an element that the
      // script should be inserted immediately before. Some
      // services try to infer how they should behave from the
      // context the script tag is in (infogr.am).
      //
      // This code was inspired by the wufoo embed code and
      // is used to dynamically load wufoo and other services
      // that use js-based embed codes. See the oembetter
      // filter in `wufoo.js`.
      afterScriptLoads(script, beforeId, scriptId, then) {
        if (script.match(/^\/\//)) {
          script = '(\'https:\' == d.location.protocol ? \'https:\' : \'http:\') + \'' + script + '\'';
        } else {
          script = '\'' + script + '\'';
        }
        if (scriptId) {
          scriptId = 's.id = "' + scriptId + '"; ';
        } else {
          scriptId = '';
        }
        let before;
        if (beforeId) {
          before = 'd.getElementById("' + beforeId + '")';
        } else {
          before = 'd.getElementsByTagName(t)[0]';
        }
        return '<script type="text/javascript">' + '(function(d, t) {' + 'var s = d.createElement(t);' + 's.src = ' + script + ';' + scriptId + 's.onload = s.onreadystatechange = function() {' + 'var rs = this.readyState; if (rs) if (rs != \'complete\') if (rs != \'loaded\') return;' + then + '};' + 'var scr = ' + before + ', par = scr.parentNode; par.insertBefore(s, scr);' + '})(document, \'script\');' + '</script>';
      },
      getBrowserData(req) {
        // api is public
        return {
          action: self.action
        };
      }
    };
  },
  apiRoutes(self) {
    return {
      // Simple API to self.query, with caching. Accepts url and
      // alwaysIframe parameters; alwaysIframe is assumed false
      // if not provided. The response is a JSON object as returned
      // by apos.oembed. A GET request because it is cache-friendly.
      get: {
        async query(req) {
          const url = self.apos.launder.string(req.query.url);
          const options = {
            // This feature is no longer available via the
            // oembed proxy because it posed an SSRF risk and
            // was never used in core Apostrophe widgets
            alwaysIframe: false
          };

          const result = await self.query(req, url, options);
          return result;
        }
      }
    };
  }
};
