/*global Components: false */
/*jshint -W097 */
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */


"use strict";

var EXPORTED_SYMBOLS = ["EnigmailArmor"];

Components.utils.import("resource://enigmail/constants.jsm"); /* global EnigmailConstants: false */
Components.utils.import("resource://enigmail/log.jsm"); /* global EnigmailLog: false */

const Ci = Components.interfaces;

// Locates STRing in TEXT occurring only at the beginning of a line
function indexOfArmorDelimiter(text, str, offset) {
  let currentOffset = offset;

  while (currentOffset < text.length) {
    let loc = text.indexOf(str, currentOffset);

    if (loc === -1 || loc === 0 || text.charAt(loc - 1) == "\n") {
      return loc;
    }

    currentOffset = loc + str.length;
  }

  return -1;
}

function searchBlankLine(str, then) {
  var offset = str.search(/\n\s*\r?\n/);
  if (offset === -1) {
    return "";
  }
  else {
    return then(offset);
  }
}

function indexOfNewline(str, off, then) {
  var offset = str.indexOf("\n", off);
  if (offset === -1) {
    return "";
  }
  else {
    return then(offset);
  }
}

var EnigmailArmor = {
  /**
   * Locates offsets bracketing PGP armored block in text,
   * starting from given offset, and returns block type string.
   *
   * @param text:          String - ASCII armored text
   * @param offset:        Number - offset to start looking for block
   * @param indentStr:     String - prefix that is used for all lines (such as "> ")
   * @param beginIndexObj: Object - o.value will contain offset of first character of block
   * @param endIndexObj:   Object - o.value will contain offset of last character of block (newline)
   * @param indentStrObj:  Object - o.value will contain indent of 1st line
   *
   * @return String - type of block found (e.g. MESSAGE, PUBLIC KEY)
   *           If no block is found, an empty String is returned;
   */
  locateArmoredBlock: function(text, offset, indentStr, beginIndexObj, endIndexObj, indentStrObj) {
    EnigmailLog.DEBUG("armor.jsm: Enigmail.locateArmoredBlock: " + offset + ", '" + indentStr + "'\n");

    beginIndexObj.value = -1;
    endIndexObj.value = -1;

    var beginIndex = indexOfArmorDelimiter(text, indentStr + "-----BEGIN PGP ", offset);

    if (beginIndex == -1) {
      var blockStart = text.indexOf("-----BEGIN PGP ");
      if (blockStart >= 0) {
        var indentStart = text.search(/\n.*-----BEGIN PGP /) + 1;
        indentStrObj.value = text.substring(indentStart, blockStart);
        indentStr = indentStrObj.value;
        beginIndex = indexOfArmorDelimiter(text, indentStr + "-----BEGIN PGP ", offset);
      }
    }

    if (beginIndex == -1)
      return "";

    // Locate newline at end of armor header
    offset = text.indexOf("\n", beginIndex);

    if (offset == -1)
      return "";

    var endIndex = indexOfArmorDelimiter(text, indentStr + "-----END PGP ", offset);

    if (endIndex == -1)
      return "";

    // Locate newline at end of PGP block
    endIndex = text.indexOf("\n", endIndex);

    if (endIndex == -1) {
      // No terminating newline
      endIndex = text.length - 1;
    }

    var blockHeader = text.substr(beginIndex, offset - beginIndex + 1);

    var blockRegex = new RegExp("^" + indentStr +
      "-----BEGIN PGP (.{1,30})-----\\s*\\r?\\n");

    var matches = blockHeader.match(blockRegex);

    var blockType = "";
    if (matches && (matches.length > 1)) {
      blockType = matches[1];
      EnigmailLog.DEBUG("armor.jsm: Enigmail.locateArmoredBlock: blockType=" + blockType + "\n");
    }

    if (blockType == "UNVERIFIED MESSAGE") {
      // Skip any unverified message block
      return EnigmailArmor.locateArmoredBlock(text, endIndex + 1, indentStr, beginIndexObj, endIndexObj, indentStrObj);
    }

    beginIndexObj.value = beginIndex;
    endIndexObj.value = endIndex;

    return blockType;
  },

  /**
   * locateArmoredBlocks returns an array of ASCII Armor block positions
   *
   * @param text: String - text containing ASCII armored block(s)
   *
   * @return Array of objects with the following structure:
   *        obj.begin:     Number
   *        obj.end:       Number
   *        obj.indent:    String
   *        obj.blocktype: String
   *
   *       if no block was found, an empty array is returned
   */
  locateArmoredBlocks: function(text) {
    var beginObj = {};
    var endObj = {};
    var indentStrObj = {};
    var blocks = [];
    var i = 0;
    var b;

    while ((b = EnigmailArmor.locateArmoredBlock(text, i, "", beginObj, endObj, indentStrObj)) !== "") {
      blocks.push({
        begin: beginObj.value,
        end: endObj.value,
        indent: indentStrObj.value ? indentStrObj.value : "",
        blocktype: b
      });

      i = endObj.value;
    }

    EnigmailLog.DEBUG("armor.jsm: locateArmorBlocks: Found " + blocks.length + " Blocks\n");
    return blocks;
  },

  extractSignaturePart: function(signatureBlock, part) {
    EnigmailLog.DEBUG("armor.jsm: Enigmail.extractSignaturePart: part=" + part + "\n");

    return searchBlankLine(signatureBlock, function(offset) {
      return indexOfNewline(signatureBlock, offset + 1, function(offset) {
        var beginIndex = signatureBlock.indexOf("-----BEGIN PGP SIGNATURE-----", offset + 1);
        if (beginIndex == -1) {
          return "";
        }

        if (part === EnigmailConstants.SIGNATURE_TEXT) {
          return signatureBlock.substr(offset + 1, beginIndex - offset - 1).
          replace(/^- -/, "-").
          replace(/\n- -/g, "\n-").
          replace(/\r- -/g, "\r-");
        }

        return indexOfNewline(signatureBlock, beginIndex, function(offset) {
          var endIndex = signatureBlock.indexOf("-----END PGP SIGNATURE-----", offset);
          if (endIndex == -1) {
            return "";
          }

          var signBlock = signatureBlock.substr(offset, endIndex - offset);

          return searchBlankLine(signBlock, function(armorIndex) {
            if (part == EnigmailConstants.SIGNATURE_HEADERS) {
              return signBlock.substr(1, armorIndex);
            }

            return indexOfNewline(signBlock, armorIndex + 1, function(armorIndex) {
              if (part == EnigmailConstants.SIGNATURE_ARMOR) {
                return signBlock.substr(armorIndex, endIndex - armorIndex).
                replace(/\s*/g, "");
              }
              else {
                return "";
              }
            });
          });
        });
      });
    });
  },

  /**
   * Remove all headers from an OpenPGP Armored message and replace them
   * with a set of new headers.
   *
   * @param armorText: String - ASCII armored message
   * @param headers:   Object - key/value pairs of new headers to insert
   *
   * @return String - new armored message
   */
  replaceArmorHeaders: function(armorText, headers) {

    let text = armorText.replace(/\r\n/g, "\n");
    let i = text.search(/\n/);

    if (i < 0) return armorText;
    let m = text.substr(0, i + 1);

    for (let j in headers) {
      m += j + ": " + headers[j] + "\n";
    }

    i = text.search(/\n\n/);
    if (i < 0) return armorText;
    m += text.substr(i + 1);

    return m;
  },

  /**
   * Get a list of all headers found in an armor message
   *
   * @param text String - ASCII armored message
   *
   * @return Object: key/value pairs of headers. All keys are in lowercase.
   */
  getArmorHeaders: function(text) {
    let headers = {};
    let b = this.locateArmoredBlocks(text);

    if (b.length === 0) {
      return headers;
    }

    let msg = text.substr(b[0].begin);

    let lx = new RegExp("\\n" + b[0].indent + "\\r?\\n");
    let hdrEnd = msg.search(lx);
    if (hdrEnd < 0) return headers;

    let lines = msg.substr(0, hdrEnd).split(/\r?\n/);

    let rx = new RegExp("^" + b[0].indent + "([^: ]+)(: )(.*)");
    // skip 1st line (ARMOR-line)
    for (let i = 1; i < lines.length; i++) {
      let m = lines[i].match(rx);
      if (m && m.length >= 4) {
        headers[m[1].toLowerCase()] = m[3];
      }
    }

    return headers;
  }
};
