dojo.provide("parus.digitalSignature");

dojo.require("dojo.data.ItemFileReadStore");
dojo.require("parus.parus8");

if (!parus.digitalSignature) dojo.mixin(parus, {
  digitalSignature: {}
});

if ( !! window.Promise && !! (dojo.isWebKit || dojo.isFF || dojo.isEdge)) {
  dojo.eval(dojo.cache("parus", "digitalSignatureAsync.js"));
}

dojo.mixin(parus.digitalSignature, {

  pluginDef: null,
  pluginLoaded: false,
  pluginTimedOut: false,
  pluginUnavailable: false,

  //plugin version
  version: {
    major: 0,
    minor: 0,
    build: 0
  },
  //browser extension version
  extension_version: null,
  extension_id: null,

  pluginDownloadURL: "https://www.cryptopro.ru/products/cades/plugin/get_2_0",
  extensionDownloadURLForFF: "https://www.cryptopro.ru/sites/default/files/products/cades/extensions/firefox_cryptopro_extension_latest.xpi",
  extensionDownloadURLForOpera: "https://addons.opera.com/en/extensions/details/cryptopro-extension-for-cades-browser-plug-in",

  pluginNotInstalled: function() {
    return "Расширение для работы с электронными подписями не найдено или не включено. <br/>" + "Для работы необходимо наличие установленного расширения для браузера, <br/>" + "которое можно получить по адресу: " + "<a href=\"" + this.pluginDownloadURL + "\">" + this.pluginDownloadURL + "</a>";
  },

  extensionNotInstalledForFF: function() {
    return "Расширение для работы с электронными подписями не найдено или не включено. <br/>" + "Для работы необходимо наличие установленного расширения для браузера, " + "которое можно получить по адресу: " + "<a href=\"" + this.extensionDownloadURLForFF + "\">" + this.extensionDownloadURLForFF + "</a>";
  },

  extensionNotInstalledForOpera: function() {
    return "Расширение для работы с электронными подписями не найдено или не включено. <br/>" + "Для работы необходимо наличие установленного расширения для браузера, " + "которое можно получить по адресу: " + "<a href=\"" + this.extensionDownloadURLForOpera + "\">" + this.extensionDownloadURLForOpera + "</a>";
  },

  pluginVersionUnsupported: function() {
    return "Установленная версия расширения для работы с электронными подписями устарела. <br/>" + "Актуальную версию можно получить по адресу: " + "<a href=\"" + this.pluginDownloadURL + "\">" + this.pluginDownloadURL + "</a>";
  },

  pluginNotActivated: function() {
    return "Расширение для работы с электронными подписями загружено, но не может быть активировано автоматически. <br/>" + "Разрешите работу расширения в автоматическом режиме в настройках вашего браузера. <br/>" + "Для этого установите \"разрешать всегда\" для расширения " + "<span style=\"color:blue\">CryptoPro CAdES NPAPI Browser Plug-in</span>.";
  },

  pluginActivateError: function() {
    return "Расширение для работы с электронными подписями загружено, но не удается создать объекты. <br/>" + "Проверьте настройки вашего браузера.";
  },

  browserNotSupported: function() {
    return "Расширение для работы с электронными подписями не поддерживает работу с данным браузером. <br/>";
  },

  oldEdgeNotSupported: function() {
    return "К сожалению, браузер Edge не поддерживается, обновитесь до Edge версии >= 79. <br/>";
  },

  signIsInvalid: function(message) {
    return "Проверка не подтвердила подлинности подписи.<br>" + ((dojo.isFF && message.match(/.*NPObject.*/)) ? "" : ("Дополнительная информация: " + message));
  },

  decimalToHexString: function(number) {
    if (number < 0) {
      number = 0xFFFFFFFF + number + 1;
    }
    return number.toString(16).toUpperCase();
  },

  correctBase64Data: function(value) {
    return value.replace(/(\r\n|\n|\r)/gm, "");
  },

  onPluginLoaded: function() {
    console.log("onPluginLoaded callback fired");
    this.pluginLoaded = true;
  },

  onGetExtensionVersion: function(value) {
    this.extension_version = value;
    console.log("Extension version: " + this.extension_version);
  },

  onGetExtensionID: function(value) {
    this.extension_id = value;
    console.log("Extension ID: " + this.extension_id);
  },

  canAsync: function() {
    return !!cadesplugin.CreateObjectAsync;
  },

  checkPlugin: function() {
    if (this.pluginDef) return this.pluginDef;

    this.pluginDef = new dojo.Deferred();

    //plugin options
    //window.cadesplugin_load_timeout = 20 * 1000; //default value
    window.cadesplugin_skip_extension_install = true;
    window.cadesplugin_extension_loaded_callback = dojo.hitch(this, this.onPluginLoaded);

    dojo.eval(dojo.cache("parus", "cadesplugin_api.js"));

    //IE do not fire this callback, because extension is not needed - call it manually
    if (dojo.isIE) this.onPluginLoaded();

    var canPromise = !! window.Promise; //window.cadesplugin is Promise or Object?
    if (dojo.isEdge) {
      this.rejectPlugin(this.oldEdgeNotSupported());
    }
    else if (canPromise) {
      cadesplugin.then(
      dojo.hitch(this, function() {
        if (this.canAsync()) this.checkPluginAsync();
        else this.checkPluginNPAPI();
      }), dojo.hitch(this, this.rejectPlugin));
    }
    else {
      window.addEventListener("message", dojo.hitch(this, function(event) {
        if (event.data == "cadesplugin_loaded") {
          this.checkPluginNPAPI();
        }
        else if (event.data == "cadesplugin_load_error") {
          this.rejectPlugin();
        }
      }), false);
      window.postMessage("cadesplugin_echo_request", "*");
    };

    return this.pluginDef;
  },

  checkPluginNPAPI: function() {
    try {
      cadesplugin.get_extension_version(dojo.hitch(this, this.onGetExtensionVersion));
      cadesplugin.get_extension_id(dojo.hitch(this, this.onGetExtensionID));

      var about = cadesplugin.CreateObject("CAdESCOM.About");
      if (about) {
        this.pluginDef.resolve();

        dojo.mixin(this.version, {
          major: about.MajorVersion,
          minor: about.MinorVersion,
          build: about.BuildVersion
        });

        console.log("Plugin version: " + this.version.major + '.' + this.version.minor + '.' + this.version.build);
      }
    }
    catch (e) {
      console.log("checkPluginNPAPI", e);
      this.rejectPlugin();
    }
  },

  rejectPlugin: function(error) {
    if (error === "Истекло время ожидания загрузки плагина") this.pluginTimedOut = true;
    else if (error === "Плагин недоступен") this.pluginUnavailable = true;

    if (error && !this.pluginTimedOut && !this.pluginUnavailable) { //skip timeout and unavailable error - check is plugin state: installed or not, activated or not
      this.pluginDef.reject(error);
    }
    else {
      var mimetype = navigator.mimeTypes["application/x-cades"];
      if (mimetype) {
        if (mimetype.enabledPlugin) this.pluginDef.reject(this.pluginActivateError());
        else this.pluginDef.reject(this.pluginNotActivated());
      }
      else {
        if (this.pluginLoaded && this.pluginTimedOut) this.pluginDef.reject(this.pluginNotActivated())

        //browser extension is not installed with CryptoPro plugin -> extension requires manual installation
        else if (!this.pluginLoaded && dojo.isFF) this.pluginDef.reject(this.extensionNotInstalledForFF());
        else if (!this.pluginLoaded && dojo.isChromiumOpera /*|| dojo.isYandex*/ ) this.pluginDef.reject(this.extensionNotInstalledForOpera());

        else this.pluginDef.reject(this.pluginNotInstalled());
      }
    }
  },

  checkPluginVersion: function(requiredVersion) {
    var result = false;
    if (this.version.major == requiredVersion.major) if (this.version.major == requiredVersion.major) if (this.version.minor == requiredVersion.minor) result = true;
    else result = this.version.minor > requiredVersion.minor;
    else result = this.version.major > requiredVersion.major;
    else result = this.version.major > requiredVersion.major;

    if (!result) parus.parus8.errorMessage(this.pluginVersionUnsupported());

    return result;
  },

  getGOSTMethodsByOid: function(algoOid, allowRaise) {
    var result = {
      version: 0,
      keySize: 0,
      signMethod: "",
      digestMethod: ""
    };

    if (algoOid == "1.2.643.7.1.1.1.1") { // алгоритм подписи ГОСТ Р 34.10-2012 с ключом 256 бит
      result.version = 2012;
      result.keySize = 256;
      result.signMethod = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102012-gostr34112012-256";
      result.digestMethod = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256";
    }
    else if (algoOid == "1.2.643.7.1.1.1.2") { // алгоритм подписи ГОСТ Р 34.10-2012 с ключом 512 бит
      result.version = 2012;
      result.keySize = 512;
      result.signMethod = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102012-gostr34112012-512";
      result.digestMethod = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-512";
    }
    else if (algoOid == "1.2.643.2.2.19") { // алгоритм ГОСТ Р 34.10-2001
      result.version = 2001;
      result.keySize = 0; //all key sizes
      result.signMethod = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102001-gostr3411";
      result.digestMethod = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr3411";
    }
    else if (allowRaise) {
      throw "Поддерживается XML подпись сертификатами с алгоритмами ГОСТ Р 34.10-2012 и ГОСТ Р 34.10-2001: " + algoOid;
    }

    return result;
  },

  isAllowedThumbprint: function(value, allowedThumbprints) {
    if (allowedThumbprints) {
      value = value.toString();
      for (var i = 0; i < allowedThumbprints.length; i++) {
        if (allowedThumbprints[i] == value) return true;
      }
    }
    else return true;
  },

  certificatesStore: function(allowedThumbprints) {
    var def = new dojo.Deferred();

    setTimeout(dojo.hitch(this, function() {
      if (this.canAsync()) this.certificatesStoreAsync(def, allowedThumbprints);
      else {
        try {
          var certificates = [];

          var certStore = cadesplugin.CreateObject("CAdESCOM.Store");
          certStore.Open();
          try {
            var certCount = 0;
            // cades plugin bug workaround - "Certificates" collection not assigned when no certificates in store.
            try {
              certCount = certStore.Certificates.Count
            }
            catch (e) {
              certCount = 0;
            }

            var currentDate = new Date();
            for (var i = 1; i <= certCount; i++) {
              var cert = certStore.Certificates.Item(i);
              if (cert && (currentDate < new Date(cert.ValidToDate)) && cert.HasPrivateKey() //&& cert.IsValid().Result
              && this.isAllowedThumbprint(cert.Thumbprint, allowedThumbprints)) {
                var GOSTMethods = this.getGOSTMethodsByOid(cert.PublicKey().Algorithm.Value, false);
                certificates.push({
                  SERIALNUMBER: cert.SerialNumber,
                  THUMBPRINT: cert.Thumbprint,
                  ISSUEDTO: cert.GetInfo(cadesplugin.CAPICOM_CERT_INFO_SUBJECT_SIMPLE_NAME),
                  ISSUER: cert.GetInfo(cadesplugin.CAPICOM_CERT_INFO_ISSUER_SIMPLE_NAME),
                  VALIDFROMDATE: new Date(cert.ValidFromDate),
                  VALIDTODATE: new Date(cert.ValidToDate),
                  OID: cert.PublicKey().Algorithm.Value,
                  GOST_VERSION: GOSTMethods.version,
                  GOST_KEYSIZE: GOSTMethods.keySize
                });
              }
            }
          }
          finally {
            certStore.Close();
          }

          def.resolve(new dojo.data.ItemFileReadStore({
            data: {
              items: certificates,
              identifier: "THUMBPRINT"
            }
          }));
        }
        catch (e) {
          def.reject(cadesplugin.getLastError(e));
        }
      }
    }), 0);

    return def;
  },

  signData: function(signInfo, certificateThumbprint) {
    var def = new dojo.Deferred();

    setTimeout(dojo.hitch(this, function() {
      if (this.canAsync()) this.signDataAsync(def, signInfo, certificateThumbprint);
      else {
        try {
          var certStore = cadesplugin.CreateObject("CAdESCOM.Store");
          certStore.Open();
          try {
            var signer = cadesplugin.CreateObject("CAdESCOM.CPSigner");
            signer.Certificate = certStore.Certificates.Find(
            cadesplugin.CAPICOM_CERTIFICATE_FIND_SHA1_HASH, certificateThumbprint).Item(1);
            signer.Options = cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN;

            var signedData = cadesplugin.CreateObject("CAdESCOM.CadesSignedData");
            signedData.ContentEncoding = cadesplugin.CADESCOM_BASE64_TO_BINARY;

            dojo.forEach(signInfo, function(item) {
              signedData.Content = this.correctBase64Data(item.data);
              item.signedData = signedData.SignCades(signer, cadesplugin.CADESCOM_CADES_BES, true, cadesplugin.CADESCOM_ENCODE_BASE64);
            }, this);

            def.resolve(signInfo);
          }
          finally {
            certStore.Close();
          }
        }
        catch (e) {
          def.reject(cadesplugin.getLastError(e));
        }
      }
    }), 0);

    return def;
  },

  checkSign: function(signInfo) {
    var def = new dojo.Deferred();

    setTimeout(dojo.hitch(this, function() {
      if (this.canAsync()) this.checkSignAsync(def, signInfo);
      else {
        try {
          var signedData = cadesplugin.CreateObject("CAdESCOM.CadesSignedData");
          signedData.ContentEncoding = cadesplugin.CADESCOM_BASE64_TO_BINARY;
          dojo.forEach(signInfo, function(item) {
            try {
              signedData.Content = this.correctBase64Data(item.data);
              signedData.VerifyCades(item.signedData, cadesplugin.CADESCOM_CADES_BES, true);
              item.checkResult = true;
            }
            catch (e) {
              item.checkResult = false;
              item.checkError = this.signIsInvalid(cadesplugin.getLastError(e));
            }
          }, this);

          def.resolve(signInfo);
        }
        catch (e) {
          def.reject(cadesplugin.getLastError(e));
        }
      }
    }), 0);

    return def;
  },

  signXMLData: function(signInfo, certificateThumbprint, isTemplated) {
    var def = new dojo.Deferred();

    setTimeout(dojo.hitch(this, function() {
      if (this.canAsync()) this.signXMLDataAsync(def, signInfo, certificateThumbprint, isTemplated);
      else {
        try {
          var certStore = cadesplugin.CreateObject("CAdESCOM.Store");
          certStore.Open();
          try {
            var signer = cadesplugin.CreateObject("CAdESCOM.CPSigner");
            var cert = certStore.Certificates.Find(
            cadesplugin.CAPICOM_CERTIFICATE_FIND_SHA1_HASH, certificateThumbprint).Item(1);
            signer.Certificate = cert;
            signer.Options = cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN;

            var signedXML = cadesplugin.CreateObject("CAdESCOM.SignedXML");
            if (isTemplated) signedXML.SignatureType = cadesplugin.CADESCOM_XML_SIGNATURE_TYPE_TEMPLATE;
            else {
              var GOSTMethods = this.getGOSTMethodsByOid(cert.PublicKey().Algorithm.Value, true);
              signedXML.SignatureType = cadesplugin.CADESCOM_XML_SIGNATURE_TYPE_ENVELOPED;
              signedXML.SignatureMethod = GOSTMethods.signMethod;
              signedXML.DigestMethod = GOSTMethods.digestMethod;
            }

            dojo.forEach(signInfo, function(item) {
              signedXML.Content = item.data;
              item.signedData = signedXML.Sign(signer);
            });

            def.resolve(signInfo);
          }
          finally {
            certStore.Close();
          }
        }
        catch (e) {
          def.reject(cadesplugin.getLastError(e));
        }
      }
    }), 0);

    return def;
  },

  checkXMLSign: function(signInfo) {
    var def = new dojo.Deferred();

    setTimeout(dojo.hitch(this, function() {
      if (this.canAsync()) this.checkXMLSignAsync(def, signInfo);
      else {
        try {
          var signedData = cadesplugin.CreateObject("CAdESCOM.SignedXML");
          dojo.forEach(signInfo, function(item) {
            try {
              signedData.Verify(item.signedData);
              item.checkResult = true;
            }
            catch (e) {
              item.checkResult = false;
              item.checkError = cadesplugin.getLastError(e);
            }
          });

          def.resolve(signInfo);
        }
        catch (e) {
          def.reject(cadesplugin.getLastError(e));
        }
      }
    }), 0);

    return def;
  }
});