/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
 * Tests utility functions contained in `source-utils.js`
 */

const curl = require("resource://devtools/client/shared/curl.js");
const Curl = curl.Curl;
const CurlUtils = curl.CurlUtils;

// Test `Curl.generateCommand` headers forwarding/filtering
add_task(async function () {
  const request = {
    url: "https://example.com/form/",
    method: "GET",
    headers: [
      { name: "Host", value: "example.com" },
      {
        name: "User-Agent",
        value:
          "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0",
      },
      { name: "Accept", value: "*/*" },
      { name: "Accept-Language", value: "en-US,en;q=0.5" },
      { name: "Accept-Encoding", value: "gzip, deflate, br" },
      { name: "Origin", value: "https://example.com" },
      { name: "Connection", value: "keep-alive" },
      { name: "Referer", value: "https://example.com/home/" },
      { name: "Content-Type", value: "text/plain" },
    ],
    responseHeaders: [],
    httpVersion: "HTTP/2.0",
  };

  const cmd = Curl.generateCommand(request);
  const curlParams = parseCurl(cmd);

  ok(
    !headerTypeInParams(curlParams, "Host"),
    "host header ignored - to be generated from url"
  );
  ok(
    exactHeaderInParams(curlParams, "Accept: */*"),
    "accept header present in curl command"
  );
  ok(
    exactHeaderInParams(
      curlParams,
      "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"
    ),
    "user-agent header present in curl command"
  );
  ok(
    exactHeaderInParams(curlParams, "Accept-Language: en-US,en;q=0.5"),
    "accept-language header present in curl output"
  );
  ok(
    exactHeaderInParams(curlParams, "Accept-Encoding: gzip, deflate, br"),
    "accept-encoding header present in curl output"
  );
  ok(
    exactHeaderInParams(curlParams, "Origin: https://example.com"),
    "origin header present in curl output"
  );
  ok(
    exactHeaderInParams(curlParams, "Connection: keep-alive"),
    "connection header present in curl output"
  );
  ok(
    exactHeaderInParams(curlParams, "Referer: https://example.com/home/"),
    "referer header present in curl output"
  );
  ok(
    exactHeaderInParams(curlParams, "Content-Type: text/plain"),
    "content-type header present in curl output"
  );
  ok(!inParams(curlParams, "--data"), "no data param in GET curl output");
  ok(
    !inParams(curlParams, "--data-raw"),
    "no raw data param in GET curl output"
  );
});

// Test `Curl.generateCommand` URL glob handling
add_task(async function () {
  let request = {
    url: "https://example.com/",
    method: "GET",
    headers: [],
    responseHeaders: [],
    httpVersion: "HTTP/2.0",
  };

  let cmd = Curl.generateCommand(request);
  let curlParams = parseCurl(cmd);

  ok(
    !inParams(curlParams, "--globoff"),
    "no globoff param in curl output when not needed"
  );

  request = {
    url: "https://example.com/[]",
    method: "GET",
    headers: [],
    responseHeaders: [],
    httpVersion: "HTTP/2.0",
  };

  cmd = Curl.generateCommand(request);
  curlParams = parseCurl(cmd);

  ok(
    inParams(curlParams, "--globoff"),
    "globoff param present in curl output when needed"
  );
});

// Test `Curl.generateCommand` data POSTing
add_task(async function () {
  const request = {
    url: "https://example.com/form/",
    method: "POST",
    headers: [
      { name: "Content-Length", value: "1000" },
      { name: "Content-Type", value: "text/plain" },
    ],
    responseHeaders: [],
    httpVersion: "HTTP/2.0",
    postDataText: "A piece of plain payload text",
  };

  const cmd = Curl.generateCommand(request);
  const curlParams = parseCurl(cmd);

  ok(
    !headerTypeInParams(curlParams, "Content-Length"),
    "content-length header ignored - curl generates new one"
  );
  ok(
    exactHeaderInParams(curlParams, "Content-Type: text/plain"),
    "content-type header present in curl output"
  );
  ok(
    inParams(curlParams, "--data-raw"),
    '"--data-raw" param present in curl output'
  );
  ok(
    inParams(curlParams, `--data-raw ${quote(request.postDataText)}`),
    "proper payload data present in output"
  );
});

// Test `Curl.generateCommand` data POSTing - not post data
add_task(async function () {
  const request = {
    url: "https://example.com/form/",
    method: "POST",
    headers: [
      { name: "Content-Length", value: "1000" },
      { name: "Content-Type", value: "text/plain" },
    ],
    responseHeaders: [],
    httpVersion: "HTTP/2.0",
  };

  const cmd = Curl.generateCommand(request);
  const curlParams = parseCurl(cmd);

  ok(
    !inParams(curlParams, "--data-raw"),
    '"--data-raw" param not present in curl output'
  );

  const methodIndex = curlParams.indexOf("-X");

  ok(
    methodIndex !== -1 && curlParams[methodIndex + 1] === "POST",
    "request method explicit is POST"
  );
});

// Test `Curl.generateCommand` multipart data POSTing
add_task(async function () {
  const boundary = "----------14808";
  const request = {
    url: "https://example.com/form/",
    method: "POST",
    headers: [
      {
        name: "Content-Type",
        value: `multipart/form-data; boundary=${boundary}`,
      },
    ],
    responseHeaders: [],
    httpVersion: "HTTP/2.0",
    postDataText: [
      `--${boundary}`,
      'Content-Disposition: form-data; name="field_one"',
      "",
      "value_one",
      `--${boundary}`,
      'Content-Disposition: form-data; name="field_two"',
      "",
      "value two",
      `--${boundary}--`,
      "",
    ].join("\r\n"),
  };

  const cmd = Curl.generateCommand(request);

  // Check content type
  const contentTypePos = cmd.indexOf(headerParamPrefix("Content-Type"));
  const contentTypeParam = headerParam(
    `Content-Type: multipart/form-data; boundary=${boundary}`
  );
  Assert.notStrictEqual(
    contentTypePos,
    -1,
    "content type header present in curl output"
  );
  equal(
    cmd.substr(contentTypePos, contentTypeParam.length),
    contentTypeParam,
    "proper content type header present in curl output"
  );

  // Check binary data
  const dataBinaryPos = cmd.indexOf("--data-binary");
  const dataBinaryParam = `--data-binary ${isWin() ? "" : "$"}${escapeNewline(
    quote(request.postDataText)
  )}`;
  Assert.notStrictEqual(
    dataBinaryPos,
    -1,
    "--data-binary param present in curl output"
  );
  equal(
    cmd.substr(dataBinaryPos, dataBinaryParam.length),
    dataBinaryParam,
    "proper multipart data present in curl output"
  );
});

// Test `CurlUtils.removeBinaryDataFromMultipartText` doesn't change text data
add_task(async function () {
  const boundary = "----------14808";
  const postTextLines = [
    `--${boundary}`,
    'Content-Disposition: form-data; name="field_one"',
    "",
    "value_one",
    `--${boundary}`,
    'Content-Disposition: form-data; name="field_two"',
    "",
    "value two",
    `--${boundary}--`,
    "",
  ];

  const cleanedText = CurlUtils.removeBinaryDataFromMultipartText(
    postTextLines.join("\r\n"),
    boundary
  );
  equal(
    cleanedText,
    postTextLines.join("\r\n"),
    "proper non-binary multipart text unchanged"
  );
});

// Test `CurlUtils.removeBinaryDataFromMultipartText` removes binary data
add_task(async function () {
  const boundary = "----------14808";
  const postTextLines = [
    `--${boundary}`,
    'Content-Disposition: form-data; name="field_one"',
    "",
    "value_one",
    `--${boundary}`,
    'Content-Disposition: form-data; name="field_two"; filename="file_field_two.txt"',
    "",
    "file content",
    `--${boundary}--`,
    "",
  ];

  const cleanedText = CurlUtils.removeBinaryDataFromMultipartText(
    postTextLines.join("\r\n"),
    boundary
  );
  postTextLines.splice(7, 1);
  equal(
    cleanedText,
    postTextLines.join("\r\n"),
    "file content removed from multipart text"
  );
});

// Test `Curl.generateCommand` add --compressed flag
add_task(async function () {
  let request = {
    url: "https://example.com/",
    method: "GET",
    headers: [],
    responseHeaders: [],
    httpVersion: "HTTP/2.0",
  };

  let cmd = Curl.generateCommand(request);
  let curlParams = parseCurl(cmd);

  ok(
    !inParams(curlParams, "--compressed"),
    "no compressed param in curl output when not needed"
  );

  request = {
    url: "https://example.com/",
    method: "GET",
    headers: [],
    responseHeaders: [{ name: "Content-Encoding", value: "gzip" }],
    httpVersion: "HTTP/2.0",
  };

  cmd = Curl.generateCommand(request);
  curlParams = parseCurl(cmd);

  ok(
    inParams(curlParams, "--compressed"),
    "compressed param present in curl output when needed"
  );
});

function isWin() {
  return Services.appinfo.OS === "WINNT";
}

const QUOTE = isWin() ? '"' : "'";

// Quote a string, escape the quotes inside the string
function quote(str) {
  let escaped;
  if (isWin()) {
    escaped = str.replace(new RegExp(QUOTE, "g"), `${QUOTE}${QUOTE}`);
  } else {
    escaped = str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`);
  }
  return QUOTE + escaped + QUOTE;
}

function escapeNewline(txt) {
  if (isWin()) {
    // Add `"` to close quote, then escape newline outside of quote, then start new quote
    return txt.replace(/[\r\n]{1,2}/g, '"^$&$&"');
  }
  return txt.replace(/\r/g, "\\r").replace(/\n/g, "\\n");
}

// Header param is formatted as -H "Header: value" or -H 'Header: value'
function headerParam(h) {
  return "-H " + quote(h);
}

// Header param prefix is formatted as `-H "HeaderName` or `-H 'HeaderName`
function headerParamPrefix(headerName) {
  return `-H ${QUOTE}${headerName}`;
}

// If any params startswith `-H "HeaderName` or `-H 'HeaderName`
function headerTypeInParams(curlParams, headerName) {
  return curlParams.some(param =>
    param.toLowerCase().startsWith(headerParamPrefix(headerName).toLowerCase())
  );
}

function exactHeaderInParams(curlParams, header) {
  return curlParams.some(param => param === headerParam(header));
}

function inParams(curlParams, param) {
  return curlParams.some(p => p.startsWith(param));
}

// Parse complete curl command to array of params. Can be applied to simple headers/data,
// but will not on WIN with sophisticated values of --data-binary with e.g. escaped quotes
function parseCurl(curlCmd) {
  // This monster regexp parses the command line into an array of arguments,
  // recognizing quoted args with matching quotes and escaped quotes inside:
  // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
  const matchRe = /[-A-Za-z1-9]+(?: \$?([\"'])(?:\\\1|.)*?\1)?/g;
  return curlCmd.match(matchRe);
}
