/**
 * @copyright
 * ====================================================================
 * Copyright (c) 2003 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 * @endcopyright
 *
 * @file Prompter.cpp
 * @brief Implementation of the class Prompter
 */

#include "Prompter.h"
#include "Pool.h"
#include "JNIUtil.h"
#include "JNIStringHolder.h"
#include "../include/org_tigris_subversion_javahl_PromptUserPassword2.h"
#include "svn_client.h"
#include "svn_private_config.h"

/**
 * Constructor
 * @param jprompter     a global reference to the Java callback object
 * @param v2            the callback objects implements PromptUserPassword2
 * @param v3            the callback objects implements PromptUserPassword3
 */
Prompter::Prompter(jobject jprompter, bool v2, bool v3)
{
  m_prompter = jprompter;
  m_version2 = v2;
  m_version3 = v3;
}

Prompter::~Prompter()
{
  if (m_prompter!= NULL)
    {
      // Since the reference to the Java object is a global one, it
      // has to be deleted.
      JNIEnv *env = JNIUtil::getEnv();
      env->DeleteGlobalRef(m_prompter);
    }
}

/**
 * Create a C++ peer object for the Java callback object
 *
 * @param jprompter     Java callback object
 * @return              C++ peer object
 */
Prompter *Prompter::makeCPrompter(jobject jprompter)
{
  // If we have no Java object, we need no C++ object.
  if (jprompter == NULL)
    return NULL;

  JNIEnv *env = JNIUtil::getEnv();

  // Sanity check that the Java object implements PromptUserPassword.
  jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword");
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;

  if (!env->IsInstanceOf(jprompter, clazz))
    {
      env->DeleteLocalRef(clazz);
      return NULL;
    }
  env->DeleteLocalRef(clazz);
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;

  // Check if PromptUserPassword2 is implemented by the Java object.
  jclass clazz2 = env->FindClass(JAVA_PACKAGE"/PromptUserPassword2");
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;

  bool v2 = env->IsInstanceOf(jprompter, clazz2) ? true: false;
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;

  env->DeleteLocalRef(clazz2);
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;

  bool v3 = false;
  if (v2)
    {
      // Check if PromptUserPassword3 is implemented by the Java object.
      jclass clazz3 = env->FindClass(JAVA_PACKAGE"/PromptUserPassword3");
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      v3 = env->IsInstanceOf(jprompter, clazz3) ? true: false;
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      env->DeleteLocalRef(clazz3);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;
    }

  // Create a new global ref for the Java object, because it is
  // longer used that this call.
  jobject myPrompt = env->NewGlobalRef(jprompter);
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;

  // Create the C++ peer.
  return new Prompter(myPrompt, v2, v3);
}

/**
 * Retrieve the username from the Java object
 * @return Java string for the username or NULL
 */
jstring Prompter::username()
{
  JNIEnv *env = JNIUtil::getEnv();

  // The method id will not change during the time this library is
  // loaded, so it can be cached.
  static jmethodID mid = 0;

  if (mid == 0)
    {
      jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword");
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      mid = env->GetMethodID(clazz, "getUsername", "()Ljava/lang/String;");
      if (JNIUtil::isJavaExceptionThrown() || mid == 0)
        return NULL;

      env->DeleteLocalRef(clazz);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;
    }

  jstring ret = static_cast<jstring>(env->CallObjectMethod(m_prompter, mid));
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;

  return ret;
}

/**
 * Retrieve the password from the Java object
 * @return Java string for the password or NULL
 */
jstring Prompter::password()
{
  JNIEnv *env = JNIUtil::getEnv();

  // The method id will not change during the time this library is
  // loaded, so it can be cached.
  static jmethodID mid = 0;

  if (mid == 0)
    {
      jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword");
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      mid = env->GetMethodID(clazz, "getPassword", "()Ljava/lang/String;");
      if (JNIUtil::isJavaExceptionThrown() || mid == 0)
        return NULL;

      env->DeleteLocalRef(clazz);
      if (JNIUtil::isJavaExceptionThrown())
        return false;
    }

  jstring ret = static_cast<jstring>(env->CallObjectMethod(m_prompter, mid));
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;

  return ret;
}
/**
 * Ask the user a question, which can be answered by yes/no.
 * @param realm         the server realm, for which this question is asked
 * @param question      the question to ask the user
 * @param yesIsDefault  flag if the yes-button should be the default button
 * @return flag who the user answered the question
 */
bool Prompter::askYesNo(const char *realm, const char *question,
                        bool yesIsDefault)
{
  JNIEnv *env = JNIUtil::getEnv();

  // The method id will not change during the time this library is
  // loaded, so it can be cached.
  static jmethodID mid = 0;

  if (mid == 0)
    {
      jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword");
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      mid = env->GetMethodID(clazz, "askYesNo",
                             "(Ljava/lang/String;Ljava/lang/String;Z)Z");
      if (JNIUtil::isJavaExceptionThrown() || mid == 0)
        return false;

      env->DeleteLocalRef(clazz);
      if (JNIUtil::isJavaExceptionThrown())
        return false;
    }

  // convert the texts to Java strings
  jstring jrealm = JNIUtil::makeJString(realm);
  if (JNIUtil::isJavaExceptionThrown())
    return false;

  jstring jquestion = JNIUtil::makeJString(question);
  if (JNIUtil::isJavaExceptionThrown())
    return false;

  // execute the callback
  jboolean ret = env->CallBooleanMethod(m_prompter, mid, jrealm, jquestion,
                                        yesIsDefault ? JNI_TRUE : JNI_FALSE);
  if (JNIUtil::isJavaExceptionThrown())
    return false;

  // delete the Java strings
  env->DeleteLocalRef(jquestion);
  if (JNIUtil::isJavaExceptionThrown())
    return false;

  env->DeleteLocalRef(jrealm);
  if (JNIUtil::isJavaExceptionThrown())
    return false;

  return ret ? true:false;
}

const char *Prompter::askQuestion(const char *realm, const char *question,
                                  bool showAnswer, bool maySave)
{
  JNIEnv *env = JNIUtil::getEnv();
  if (m_version3)
    {
      static jmethodID mid = 0;
      static jmethodID mid2 = 0;
      if (mid == 0)
        {
          jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword3");
          if (JNIUtil::isJavaExceptionThrown())
            return NULL;

          mid = env->GetMethodID(clazz, "askQuestion",
                                 "(Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String;");
          if (JNIUtil::isJavaExceptionThrown() || mid == 0)
            return NULL;

          mid2 = env->GetMethodID(clazz, "userAllowedSave", "()Z");
          if (JNIUtil::isJavaExceptionThrown() || mid == 0)
            return NULL;

          env->DeleteLocalRef(clazz);
          if (JNIUtil::isJavaExceptionThrown())
            return NULL;
        }

      jstring jrealm = JNIUtil::makeJString(realm);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      jstring jquestion = JNIUtil::makeJString(question);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      jstring janswer = static_cast<jstring>(
                                             env->CallObjectMethod(m_prompter, mid, jrealm, jquestion,
                                                                   showAnswer ? JNI_TRUE : JNI_FALSE,
                                                                   maySave ? JNI_TRUE : JNI_FALSE));
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      env->DeleteLocalRef(jquestion);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      env->DeleteLocalRef(jrealm);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      JNIStringHolder answer(janswer);
      if (answer != NULL)
        {
          m_answer = answer;
          m_maySave = env->CallBooleanMethod(m_prompter, mid2) ? true: false;
          if (JNIUtil::isJavaExceptionThrown())
            return NULL;
        }
      else
        {
          m_answer = "";
          m_maySave = false;
        }
      return m_answer.c_str();
    }
  else
    {
      static jmethodID mid = 0;
      if (mid == 0)
        {
          jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword");
          if (JNIUtil::isJavaExceptionThrown())
            return NULL;

          mid = env->GetMethodID(clazz, "askQuestion",
                                 "(Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;");
          if (JNIUtil::isJavaExceptionThrown() || mid == 0)
            return NULL;

          env->DeleteLocalRef(clazz);
          if (JNIUtil::isJavaExceptionThrown())
            return NULL;
        }

      jstring jrealm = JNIUtil::makeJString(realm);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      jstring jquestion = JNIUtil::makeJString(question);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      jstring janswer = static_cast<jstring>(
                                             env->CallObjectMethod(m_prompter, mid, jrealm, jquestion,
                                                                   showAnswer ? JNI_TRUE : JNI_FALSE));
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      env->DeleteLocalRef(jquestion);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      env->DeleteLocalRef(jrealm);
      if (JNIUtil::isJavaExceptionThrown())
        return NULL;

      JNIStringHolder answer(janswer);
      if (answer != NULL)
        {
          m_answer = answer;
          if (maySave)
            m_maySave = askYesNo(realm, _("May save the answer ?"), true);
          else
            m_maySave = false;
        }
      else
        {
          m_answer = "";
          m_maySave = false;
        }
      return m_answer.c_str();
    }
}

int Prompter::askTrust(const char *question, bool maySave)
{
  if (m_version2)
    {
      static jmethodID mid = 0;
      JNIEnv *env = JNIUtil::getEnv();
      if (mid == 0)
        {
          jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword2");
          if (JNIUtil::isJavaExceptionThrown())
            return -1;

          mid = env->GetMethodID(clazz, "askTrustSSLServer",
                                 "(Ljava/lang/String;Z)I");
          if (JNIUtil::isJavaExceptionThrown() || mid == 0)
            return -1;

          env->DeleteLocalRef(clazz);
          if (JNIUtil::isJavaExceptionThrown())
            return -1;
        }
      jstring jquestion = JNIUtil::makeJString(question);
      if (JNIUtil::isJavaExceptionThrown())
        return -1;

      jint ret = env->CallIntMethod(m_prompter, mid, jquestion,
                                    maySave ? JNI_TRUE : JNI_FALSE);
      if (JNIUtil::isJavaExceptionThrown())
        return -1;

      env->DeleteLocalRef(jquestion);
      if (JNIUtil::isJavaExceptionThrown())
        return -1;

      return ret;
    }
  else
    {
      std::string q = question;
      if (maySave)
        q += _("(R)eject, accept (t)emporarily or accept (p)ermanently?");
      else
        q += _("(R)eject or accept (t)emporarily?");

      const char *answer = askQuestion(NULL, q.c_str(), true, false);
      if (*answer == 't' || *answer == 'T')
        return
          org_tigris_subversion_javahl_PromptUserPassword2_AcceptTemporary;
      else if (maySave && (*answer == 'p' || *answer == 'P'))
        return
          org_tigris_subversion_javahl_PromptUserPassword2_AcceptPermanently;
      else
        return org_tigris_subversion_javahl_PromptUserPassword2_Reject;
    }
  return -1;
}

bool Prompter::prompt(const char *realm, const char *pi_username, bool maySave)
{
  JNIEnv *env = JNIUtil::getEnv();
  if (m_version3)
    {
      static jmethodID mid = 0;
      static jmethodID mid2 = 0;
      if (mid == 0)
        {
          jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword3");
          if (JNIUtil::isJavaExceptionThrown())
            return false;

          mid = env->GetMethodID(clazz, "prompt",
                                 "(Ljava/lang/String;Ljava/lang/String;Z)Z");
          if (JNIUtil::isJavaExceptionThrown() || mid == 0)
            return false;

          mid2 = env->GetMethodID(clazz, "userAllowedSave", "()Z");
          if (JNIUtil::isJavaExceptionThrown() || mid == 0)
            return false;

          env->DeleteLocalRef(clazz);
          if (JNIUtil::isJavaExceptionThrown())
            return false;
        }

      jstring jrealm = JNIUtil::makeJString(realm);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      jstring jusername = JNIUtil::makeJString(pi_username);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      jboolean ret = env->CallBooleanMethod(m_prompter, mid, jrealm,
                                            jusername,
                                            maySave ? JNI_TRUE: JNI_FALSE);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      env->DeleteLocalRef(jusername);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      env->DeleteLocalRef(jrealm);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      m_maySave = env->CallBooleanMethod(m_prompter, mid2) ? true : false;
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      return ret ? true:false;
    }
  else
    {
      static jmethodID mid = 0;
      if (mid == 0)
        {
          jclass clazz = env->FindClass(JAVA_PACKAGE"/PromptUserPassword");
          if (JNIUtil::isJavaExceptionThrown())
            return false;

          mid = env->GetMethodID(clazz, "prompt",
                                 "(Ljava/lang/String;Ljava/lang/String;)Z");
          if (JNIUtil::isJavaExceptionThrown() || mid == 0)
            return false;

          env->DeleteLocalRef(clazz);
          if (JNIUtil::isJavaExceptionThrown())
            return false;
        }

      jstring jrealm = JNIUtil::makeJString(realm);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      jstring jusername = JNIUtil::makeJString(pi_username);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      jboolean ret = env->CallBooleanMethod(m_prompter, mid, jrealm,
                                            jusername);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      env->DeleteLocalRef(jusername);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      env->DeleteLocalRef(jrealm);
      if (JNIUtil::isJavaExceptionThrown())
        return false;

      if (maySave)
        m_maySave = askYesNo(realm, _("May save the answer ?"), true);
      else
        m_maySave = false;

      return ret ? true:false;
    }
}

svn_auth_provider_object_t *Prompter::getProviderSimple()
{
  apr_pool_t *pool = JNIUtil::getRequestPool()->pool();
  svn_auth_provider_object_t *provider;
  svn_client_get_simple_prompt_provider(&provider,
                                        simple_prompt,
                                        this,
                                        2, /* retry limit */
                                        pool);

  return provider;
}

svn_auth_provider_object_t *Prompter::getProviderUsername()
{
  apr_pool_t *pool = JNIUtil::getRequestPool()->pool();
  svn_auth_provider_object_t *provider;
  svn_client_get_username_prompt_provider(&provider,
                                          username_prompt,
                                          this,
                                          2, /* retry limit */
                                          pool);

  return provider;
}

svn_auth_provider_object_t *Prompter::getProviderServerSSLTrust()
{
  apr_pool_t *pool = JNIUtil::getRequestPool()->pool();
  svn_auth_provider_object_t *provider;
  svn_client_get_ssl_server_trust_prompt_provider
    (&provider, ssl_server_trust_prompt, this, pool);

  return provider;
}

svn_auth_provider_object_t *Prompter::getProviderClientSSL()
{
  apr_pool_t *pool = JNIUtil::getRequestPool()->pool();
  svn_auth_provider_object_t *provider;
  svn_client_get_ssl_client_cert_prompt_provider(&provider,
                                                 ssl_client_cert_prompt,
                                                 this,
                                                 2 /* retry limit */,
                                                 pool);

  return provider;
}

svn_auth_provider_object_t *Prompter::getProviderClientSSLPassword()
{
  apr_pool_t *pool = JNIUtil::getRequestPool()->pool();
  svn_auth_provider_object_t *provider;
  svn_client_get_ssl_client_cert_pw_prompt_provider
    (&provider, ssl_client_cert_pw_prompt, this, 2 /* retry limit */,
     pool);

  return provider;
}

svn_error_t *Prompter::simple_prompt(svn_auth_cred_simple_t **cred_p,
                                     void *baton,
                                     const char *realm, const char *username,
                                     svn_boolean_t may_save,
                                     apr_pool_t *pool)
{
  Prompter *that = (Prompter*)baton;
  svn_auth_cred_simple_t *ret =
    (svn_auth_cred_simple_t*)apr_pcalloc(pool, sizeof(*ret));
  if (!that->prompt(realm, username, may_save ? true : false))
    return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
                            _("User canceled dialog"));
  jstring juser = that->username();
  JNIStringHolder user(juser);
  if (user == NULL)
    return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
                            _("User canceled dialog"));
  ret->username = apr_pstrdup(pool,user);
  jstring jpass = that->password();
  JNIStringHolder pass(jpass);
  if (pass == NULL)
    return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
                            _("User canceled dialog"));
  else
    {
      ret->password  = apr_pstrdup(pool, pass);
      ret->may_save = that->m_maySave;
    }
  *cred_p = ret;

  return SVN_NO_ERROR;
}

svn_error_t *Prompter::username_prompt(svn_auth_cred_username_t **cred_p,
                                       void *baton,
                                       const char *realm,
                                       svn_boolean_t may_save,
                                       apr_pool_t *pool)
{
  Prompter *that = (Prompter*)baton;
  svn_auth_cred_username_t *ret =
    (svn_auth_cred_username_t*)apr_pcalloc(pool, sizeof(*ret));
  const char *user = that->askQuestion(realm, _("Username: "), true,
                                       may_save ? true : false);
  if (user == NULL)
    return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
                            _("User canceled dialog"));
  ret->username = apr_pstrdup(pool,user);
  ret->may_save = that->m_maySave;
  *cred_p = ret;

  return SVN_NO_ERROR;
}

svn_error_t *
Prompter::ssl_server_trust_prompt(svn_auth_cred_ssl_server_trust_t **cred_p,
                                  void *baton,
                                  const char *realm,
                                  apr_uint32_t failures,
                                  const svn_auth_ssl_server_cert_info_t *cert_info,
                                  svn_boolean_t may_save,
                                  apr_pool_t *pool)
{
  Prompter *that = (Prompter*)baton;
  svn_auth_cred_ssl_server_trust_t *ret =
    (svn_auth_cred_ssl_server_trust_t*)apr_pcalloc(pool, sizeof(*ret));

  std::string question = _("Error validating server certificate for ");
  question += realm;
  question += ":\n";

  if (failures & SVN_AUTH_SSL_UNKNOWNCA)
    {
      question += _(" - Unknown certificate issuer\n");
      question += _("   Fingerprint: ");
      question += cert_info->fingerprint;
      question += "\n";
      question += _("   Distinguished name: ");
      question += cert_info->issuer_dname;
      question += "\n";
    }

  if (failures & SVN_AUTH_SSL_CNMISMATCH)
    {
      question += _(" - Hostname mismatch (");
      question += cert_info->hostname;
      question += _(")\n");
    }

  if (failures & SVN_AUTH_SSL_NOTYETVALID)
    {
      question += _(" - Certificate is not yet valid\n");
      question += _("   Valid from ");
      question += cert_info->valid_from;
      question += "\n";
    }

  if (failures & SVN_AUTH_SSL_EXPIRED)
    {
      question += _(" - Certificate is expired\n");
      question += _("   Valid until ");
      question += cert_info->valid_until;
      question += "\n";
    }

  switch(that->askTrust(question.c_str(), may_save ? true : false))
    {
    case org_tigris_subversion_javahl_PromptUserPassword2_AcceptTemporary:
      *cred_p = ret;
      ret->may_save = FALSE;
      break;
    case org_tigris_subversion_javahl_PromptUserPassword2_AcceptPermanently:
      *cred_p = ret;
      ret->may_save = TRUE;
      ret->accepted_failures = failures;
      break;
    default:
      *cred_p = NULL;
    }
  return SVN_NO_ERROR;
}

svn_error_t *
Prompter::ssl_client_cert_prompt(svn_auth_cred_ssl_client_cert_t **cred_p,
                                 void *baton,
                                 const char *realm,
                                 svn_boolean_t may_save,
                                 apr_pool_t *pool)
{
  Prompter *that = (Prompter*)baton;
  svn_auth_cred_ssl_client_cert_t *ret =
    (svn_auth_cred_ssl_client_cert_t*)apr_pcalloc(pool, sizeof(*ret));
  const char *cert_file =
    that->askQuestion(realm, _("client certificate filename: "), true,
                      may_save ? true : false);
  if (cert_file == NULL)
    return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
                            _("User canceled dialog"));
  ret->cert_file = apr_pstrdup(pool, cert_file);
  ret->may_save = that->m_maySave;
  *cred_p = ret;
  return SVN_NO_ERROR;
}

svn_error_t *
Prompter::ssl_client_cert_pw_prompt(svn_auth_cred_ssl_client_cert_pw_t **cred_p,
                                    void *baton,
                                    const char *realm,
                                    svn_boolean_t may_save,
                                    apr_pool_t *pool)
{
  Prompter *that = (Prompter*)baton;
  svn_auth_cred_ssl_client_cert_pw_t *ret =
    (svn_auth_cred_ssl_client_cert_pw_t*)apr_pcalloc(pool, sizeof(*ret));
  const char *info = that->askQuestion(realm,
                                       _("client certificate passphrase: "),
                                       false, may_save ? true : false);
  if (info == NULL)
    return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
                            _("User canceled dialog"));
  ret->password = apr_pstrdup(pool, info);
  ret->may_save = that->m_maySave;
  *cred_p = ret;
  return SVN_NO_ERROR;
}
