/**
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 BaseSensor = require('../../hooks/frameworks/base');
const patcher = require('../../hooks/patcher');
const { PATCH_TYPES } = require('../../constants');
const constants = require('../../constants');
const SourceEvent = require('../models/source-event');
const { emitSinkEvent } = require('../../hooks/frameworks/common');

const {
  INPUT_TYPES: { GENERIC_XML },
  SINK_TYPES: { XML_STRING }
} = constants;
const ID = 'libxmljs';

class LibXMLjsSensor extends BaseSensor {
  constructor(agent) {
    super({ id: ID, agent });
  }

  install({ ModuleHook }) {
    ModuleHook.resolve({ name: 'libxmljs' }, hookLibXml.bind(this));
    // libxmljs2 is a fork of libxml that has identical signatures.
    // the only difference is that libxmljs2 is used by juice-shop and thus
    // we're missing sinks in one of our most important sample apps.
    ModuleHook.resolve({ name: 'libxmljs2' }, hookLibXml.bind(this));

    return this;
  }
}

function hookLibXml(libxmljs) {
  patcher.patch(libxmljs, 'parseXmlString', {
    name: 'libxmljs.parseXmlString',
    patchType: PATCH_TYPES.PROTECT_SINK,
    alwaysRun: true,
    pre: (wrapCtx) => {
      checkAndEmit.call(this, wrapCtx);
    }
  });

  patcher.patch(libxmljs, 'parseXml', {
    name: 'libxmljs.parseXml',
    patchType: PATCH_TYPES.PROTECT_SINK,
    pre: (wrapCtx) => {
      checkAndEmit.call(this, wrapCtx);
    }
  });
}

function checkAndEmit(wrapCtx) {
  const { args } = wrapCtx;
  const parserOptions = getParserArg(args);

  if (parserOptions && parserOptions.noent) {
    emitSourceEvent(this.emitter, wrapCtx.args[0], GENERIC_XML);
    emitSinkEvent(wrapCtx.args[0], XML_STRING, ID);
  }
}

/**
 * Returns the <code>parserOptions</code> argument
 * passed to <code>parseXmlString</code>.
 * @param {*[]} args Arguments passed to parseXmlString
 * @returns {Object}
 */
function getParserArg(args) {
  return args[1];
}

/**
 * Will create a SourceEvent and emit the <code>source</code>
 * event on the privided sensor.
 * @param {String} xml The XML string data
 * @param {Symbol} type the type of event
 */
function emitSourceEvent(emitter, xml, type) {
  if (!xml || !xml.length) {
    return;
  }
  emitter.emit('protect.source', createSourceEvent(xml, type));
}

/**
 * Given data and a type, will construct a new <code>SourceEvent.</code>
 * @param {*} data The data to use to create event
 * @param {Symbol} type The type of event
 * @returns {SinkEvent}
 */
function createSourceEvent(data, type) {
  return new SourceEvent({ data, id: ID, type });
}

module.exports = LibXMLjsSensor;
