/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';

const agentEmitter = require('../../agent-emitter');
const htmlUtil = require('../../util/html-util');
const httpCommon = require('./common');

const ruleId = 'cache-controls-missing';

/**
 * Checks if http-equiv is one of the following
 * below
 *
 * @param {String} httpequiv value of http-equiv meta attr
 */
function applicableHttpEquiv(httpequiv) {
  return (
    httpequiv == 'cache-control' ||
    httpequiv == 'pragma' ||
    httpequiv == 'expires'
  );
}
/**
 * Checks every meta html tag from body for http-equiv attributes.
 * If attr exists it will check the content attr if http-equiv is cache-control
 * Otherwise it will add value of attr if cache-control, pragma or expires
 */
function checkMetaTags({ body, cacheControlHeader, instructions }) {
  const metaTags = htmlUtil.getElements('meta', body);
  metaTags.forEach((tag) => {
    const httpequiv = htmlUtil.getAttribute('http-equiv', tag);
    if (httpequiv) {
      const content = htmlUtil.getAttribute('content', tag);
      if (httpequiv == 'cache-control') {
        const [
          containsMetaNoCache,
          containsMetaNoStore
        ] = checkCacheControlValue(content);
        if (containsMetaNoCache && containsMetaNoStore) {
          return;
        }

        const [containsNoCache, containsNoStore] = checkCacheControlValue(
          cacheControlHeader
        );

        // got no-store from headers and no-cache from meta
        // or no-cache from headers and no-store from meta
        if (
          (containsNoStore && containsMetaNoCache) ||
          (containsNoCache && containsMetaNoStore)
        ) {
          return;
        }
      }

      // if we make it this far push the values into instructions
      if (applicableHttpEquiv(httpequiv)) {
        instructions.push({
          type: 'META tag',
          name: httpequiv,
          value: tag
        });
      }
    }
  });
}

/**
 * Checks the value of cache-control
 * either header or meta content attr value
 * for if it contains no-cache or no-store
 *
 * @param {String} value value of cache-control
 * @return {Array} [no-cache present, no-store present]
 */
function checkCacheControlValue(value = '') {
  if (Array.isArray(value)) {
    let noCache = false;
    let noStore = false;

    value.forEach((directive) => {
      if (directive.toLowerCase().includes('no-cache')) {
        noCache = true;
      }
      if (directive.toLowerCase().includes('no-store')) {
        noStore = true;
      }
    });
    return [noCache, noStore];
  } else {
    value = value.toLowerCase();
    return [value.includes('no-cache'), value.includes('no-store')];
  }
}

module.exports = ({ common }) => {
  const httpRule = {};
  /**
   * Checks the cache-control header or content attribute of a meta
   * tag in HTML body if http-equiv is cache-control for the presence
   * of both no-cache and no-store
   *
   * @param {Object} responseHeaders the headers of response
   * @param {String} body HTML body
   */
  httpRule.check = function(responseHeaders, body) {
    const instructions = [];

    // de-dupe; this will be re-emitted for parseableBody handlers anyway
    if (httpCommon.isParseableResponse(responseHeaders) && !body) {
      return;
    }
    const cacheControlHeader = responseHeaders['cache-control'];

    // save the Pragma Header
    if (responseHeaders['pragma']) {
      instructions.push({
        type: 'Header',
        name: 'pragma',
        value: responseHeaders['pragma']
      });
    }

    if (cacheControlHeader) {
      const [containsNoCache, containsNoStore] = checkCacheControlValue(
        cacheControlHeader
      );

      // got everything from header so we don't care about meta tags
      if (containsNoCache && containsNoStore) {
        return;
      } else {
        // save the instructions in case meta tags are missing
        instructions.push({
          type: 'Header',
          name: 'cache-control',
          value: cacheControlHeader
        });
      }
    }

    // we checked the headers now time to check the HTML content
    checkMetaTags({ body, cacheControlHeader, instructions });

    if (instructions.length) {
      return {
        data: JSON.stringify(instructions)
      };
    }
  };

  /**
   * Sets up listeners for parseableResponse and send.
   * Once events emitted, it will check the headers/html
   * content and emit finding if logic in httpRule.check is
   * truthy
   *
   * @param {Object} rule rule policy
   *
   */
  httpRule.handle = function(rule) {
    require('./parseable-response-emitter').hook();

    // on could add the event name to the policy node for this rule
    // so that we don't have to manually
    // I should just do this inline
    agentEmitter.on(
      'parseableResponse',
      function({ request, responseHeaders, bodyLC }) {
        const trigger = httpRule.check(responseHeaders, bodyLC);
        if (trigger) {
          common.emitFinding({ ruleId, request, props: trigger });
        }
      },
      rule
    );

    agentEmitter.on(
      'send',
      function({ request, responseHeaders }) {
        const trigger = httpRule.check(responseHeaders);
        if (trigger) {
          common.emitFinding({ ruleId, request, props: trigger });
        }
      },
      rule
    );
  };

  return httpRule;
};
