/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
/* vim: set sw=2 ts=2 sts=2 et: */
/*
 *  Copyright © 2009 Xan López
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2, or (at your option)
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

/* Portions of this file based on Chromium code.
 * License block as follows:
 *
 * Copyright (c) 2009 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 *
 * The LICENSE file from Chromium can be found in the LICENSE.chromium
 * file.
 */

#include "config.h"

#include "ephy-profile-migration.h"

#include "ephy-debug.h"
#include "ephy-file-helpers.h"
#ifdef ENABLE_NSS
#include "ephy-nss-glue.h"
#endif

#include <glib/gi18n.h>
#include <gnome-keyring.h>
#include <libsoup/soup-gnome.h>

/*
 * What to do to add new migration steps:
 *  - Bump PROFILE_MIGRATION_VERSION
 *  - Add your function at the end of the 'migrators' array
 */

#define PROFILE_MIGRATION_VERSION 4

typedef void (*EphyProfileMigrator) (void);

static void
migrate_cookies ()
{
  const char *cookies_file_sqlite = "cookies.sqlite";
  const char *cookies_file_txt = "cookies.txt";
  char *src_sqlite = NULL, *src_txt = NULL, *dest = NULL;

  dest = g_build_filename (ephy_dot_dir (), cookies_file_sqlite, NULL);
  /* If we already have a cookies.sqlite file, do nothing */
  if (g_file_test (dest, G_FILE_TEST_EXISTS))
    goto out;

  src_sqlite = g_build_filename (ephy_dot_dir (), "mozilla",
                                 "epiphany", cookies_file_sqlite, NULL);
  src_txt = g_build_filename (ephy_dot_dir (), "mozilla",
                              "epiphany", cookies_file_txt, NULL);

  /* First check if we have a cookies.sqlite file in Mozilla */
  if (g_file_test (src_sqlite, G_FILE_TEST_EXISTS)) {
    GFile *gsrc, *gdest;

    /* Copy the file */
    gsrc = g_file_new_for_path (src_sqlite);
    gdest = g_file_new_for_path (dest);

    if (!g_file_copy (gsrc, gdest, 0, NULL, NULL, NULL, NULL))
      g_warning (_("Failed to copy cookies file from Mozilla."));

    g_object_unref (gsrc);
    g_object_unref (gdest);
  } else if (g_file_test (src_txt, G_FILE_TEST_EXISTS)) {
    /* Create a SoupCookieJarSQLite with the contents of the txt file */
    GSList *cookies, *p;
    SoupCookieJar *txt, *sqlite;

    txt = soup_cookie_jar_text_new (src_txt, TRUE);
    sqlite = soup_cookie_jar_sqlite_new (dest, FALSE);
    cookies = soup_cookie_jar_all_cookies (txt);

    for (p = cookies; p; p = p->next) {
      SoupCookie *cookie = (SoupCookie*)p->data;
      /* Cookie is stolen, so we won't free it */
      soup_cookie_jar_add_cookie (sqlite, cookie);
    }

    g_slist_free (cookies);
    g_object_unref (txt);
    g_object_unref (sqlite);
  }
  
 out:
  g_free (src_sqlite);
  g_free (src_txt);
  g_free (dest);
}

#ifdef ENABLE_NSS
static gchar*
_g_utf8_substr(const gchar* string, gint start, gint end)
{
  gchar *start_ptr, *output;
  gsize len_in_bytes;
  glong str_len = g_utf8_strlen (string, -1);

  if (start > str_len || end > str_len)
    return NULL;

  start_ptr = g_utf8_offset_to_pointer (string, start);
  len_in_bytes = g_utf8_offset_to_pointer (string, end) -  start_ptr + 1;
  output = g_malloc0 (len_in_bytes + 1);

  return g_utf8_strncpy (output, start_ptr, end - start + 1);
}

static char*
decrypt (const char *data)
{
  unsigned char *plain;
  gsize out_len;

  /* The old style password is encoded in base64. They are identified
   * by a leading '~'. Otherwise, we should decrypt the text.
   */
  plain = g_base64_decode (data, &out_len);
  if (data[0] != '~') {
    char *decrypted;

    decrypted = ephy_nss_glue_decrypt (plain, out_len);
    g_free (plain);
    plain = (unsigned char*)decrypted;
  }

  return (char*)plain;
}

static void
parse_and_decrypt_signons (const char *signons,
                           gboolean handle_forms)
{
  int version;
  gchar **lines;
  int i;
  guint length;

  lines = g_strsplit (signons, "\r\n", -1);
  if (!g_ascii_strncasecmp (lines[0], "#2c", 3))
    version = 1;
  else if (!g_ascii_strncasecmp (lines[0], "#2d", 3))
    version = 2;
  else if (!g_ascii_strncasecmp (lines[0], "#2e", 3))
    version = 3;
  else
    goto out;

  /* Skip the never-saved list */
  for (i = 1; lines[i] && !g_str_equal (lines[i], "."); i++) {
    ;
  }

  i++;

  /*
   * Read saved passwords. The information is stored in blocks
   * separated by lines that only contain a dot. We find a block by
   * the separator and parse them one by one.
   */
  length = g_strv_length (lines);

  while (i < length) {
    size_t begin = i;
    size_t end = i + 1;
    const char *realmBracketBegin = " (";
    const char *realmBracketEnd = ")";
    SoupURI *uri = NULL;
    char *realm = NULL;

    while (lines[end] && !g_str_equal (lines[end], "."))
      end++;

    i = end + 1;

    /* A block has at least five lines */
    if (end - begin < 5)
      continue;
    
    /* The first line is the site URL.
     * For HTTP authentication logins, the URL may contain http realm,
     * which will be in bracket:
     *   sitename:8080 (realm)
     */
    if (g_strstr_len (lines[begin], -1, realmBracketBegin)) {
      char *start_ptr, *end_ptr;
      char *full_url, *url;
      glong start, end;

      /* In this case the scheme may not exist. We assume that the
       * scheme is HTTP.
       */
      if (!g_strstr_len (lines[begin], -1, "://"))
        full_url = g_strconcat ("http://", lines[begin], NULL);
      else
        full_url = g_strdup (lines[begin]);

      start_ptr = g_strstr_len (full_url, -1, realmBracketBegin);
      start = g_utf8_pointer_to_offset (full_url, start_ptr);
      url = _g_utf8_substr (full_url, 0, start);
      url = g_strstrip (url);
      uri = soup_uri_new (url);
      g_free (url);

      start += strlen (realmBracketBegin);
      end_ptr = g_strstr_len (full_url, -1, realmBracketEnd) -1;
      end = g_utf8_pointer_to_offset (full_url, end_ptr);
      realm = _g_utf8_substr (full_url, start, end);

      g_free (full_url);
    } else {
      /* Don't have HTTP realm. It is the URL that the following
       * password belongs to.
       */
      uri = soup_uri_new (lines[begin]);
    }

    if (!SOUP_URI_VALID_FOR_HTTP (uri)) {
      soup_uri_free (uri);
      g_free (realm);
      continue;
    }

    ++begin;

    /* There may be multiple username/password pairs for this site.
     * In this case, they are saved in one block without a separated
     * line (contains a dot).
     */
    while (begin + 4 < end) {
      char *username = NULL;
      char *password = NULL;
      char *form_username = NULL;
      char *form_password = NULL;
      guint32 item_id;

      /* The username */
      if (handle_forms) {
        form_username = g_strdup (lines[begin++]);
      } else {
        begin++; /* Skip username element */
      }
      username = decrypt (lines[begin++]);

      /* The password */
      /* The element name has a leading '*' */
      if (lines[begin][0] == '*') {
        if (handle_forms) {
          form_password = g_strdup (lines[begin++]);
        } else {
          begin++; /* Skip password element */
        }
        password = decrypt (lines[begin++]);
      } else {
        /* Maybe the file is broken, skip to the next block */
        g_free (username);
        break;
      }

      /* The action attribute for from the form element. This line
       * exists in version 2 or above
       */
      if (version >= 2) {
        if (begin < end)
          /* Skip it */ ;
        begin++;

        /* Version 3 has an extra line for further use */
        if (version == 3)
          begin++;
      }

      if (handle_forms && !realm &&
          username && password &&
          !g_str_equal (username, "") &&
          !g_str_equal (form_username, "") &&
          !g_str_equal (form_password, "*")) {
        char *u = soup_uri_to_string (uri, FALSE);
        /* We skip the '*' at the beginning of form_password. */
        _ephy_profile_store_form_auth_data (u,
                                            form_username,
                                            form_password+1,
                                            username,
                                            password);
        g_free (u);
      } else if (!handle_forms && realm &&
                 username && password &&
                 !g_str_equal (username, "") &&
                 form_username == NULL && form_password == NULL) {
        gnome_keyring_set_network_password_sync (NULL,
                                                 username,
                                                 realm,
                                                 uri->host,
                                                 NULL,
                                                 uri->scheme,
                                                 NULL,
                                                 uri->port, 
                                                 password,
                                                 &item_id);
      }

      g_free (username);
      g_free (password);
      g_free (form_username);
      g_free (form_password);
    }

    soup_uri_free (uri);
    g_free (realm);
  }

 out:
  g_strfreev (lines);
}
#endif

static void
migrate_passwords ()
{
#ifdef ENABLE_NSS
  char *dest, *contents, *gecko_passwords_backup;
  gsize length;
  GError *error = NULL;

  dest = g_build_filename (ephy_dot_dir (),
                           "mozilla", "epiphany", "signons3.txt",
                           NULL);
  if (!g_file_test (dest, G_FILE_TEST_EXISTS)) {
    g_free (dest);
    dest = g_build_filename (ephy_dot_dir (),
                             "mozilla", "epiphany", "signons2.txt",
                             NULL);
    if (!g_file_test (dest, G_FILE_TEST_EXISTS)) {
      g_free (dest);
      return;
    }
  }

  if (!ephy_nss_glue_init ())
    return;

  if (!g_file_get_contents (dest, &contents, &length, &error)) {
    g_free (dest);
  }

  parse_and_decrypt_signons (contents, FALSE);

  /* Save the contents in a backup directory for future data
     extraction when we support more features */
  gecko_passwords_backup = g_build_filename (ephy_dot_dir (),
                                             "gecko-passwords.txt", NULL);

  if (!g_file_set_contents (gecko_passwords_backup, contents,
                            -1, &error)) {
    g_error_free (error);
  }

  g_free (gecko_passwords_backup);
  g_free (contents);

  ephy_nss_glue_close ();
#endif
}

static void
migrate_passwords2 ()
{
#ifdef ENABLE_NSS
  char *dest, *contents;
  gsize length;
  GError *error = NULL;

  dest = g_build_filename (ephy_dot_dir (),
                           "gecko-passwords.txt",
                           NULL);
  if (!g_file_test (dest, G_FILE_TEST_EXISTS)) {
    g_free (dest);
    return;
  }

  if (!ephy_nss_glue_init ())
    return;

  if (!g_file_get_contents (dest, &contents, &length, &error)) {
    g_free (dest);
  }

  parse_and_decrypt_signons (contents, TRUE);
  g_free (contents);

  ephy_nss_glue_close ();
#endif
}

const EphyProfileMigrator migrators[] = {
  migrate_cookies,
  migrate_passwords,
 /* Yes, again! Version 2 had some bugs, so we need to run
    migrate_passwords again to possibly migrate more passwords*/
  migrate_passwords,
  /* Very similar to migrate_passwords, but this migrates
   * login/passwords for page forms, which we previously ignored */
  migrate_passwords2
};

static void
store_form_password_cb (GnomeKeyringResult result,
                        guint32 id,
                        gpointer data)
{
  /* FIXME: should we do anything if the operation failed? */
}

static void
normalize_and_prepare_uri (SoupURI *uri,
                           const char *form_username,
                           const char *form_password)
{
  g_return_if_fail (uri != NULL);

  /* We normalize https? schemes here so that we use passwords
   * we stored in https sites in their http counterparts, and
   * vice-versa. */
  if (g_str_equal (uri->scheme, SOUP_URI_SCHEME_HTTPS))
    soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTP);

  soup_uri_set_path (uri, NULL);

  /* Store the form login and password names encoded in the
   * URL. A bit of an abuse of keyring, but oh well */
  soup_uri_set_query_from_fields (uri,
                                  FORM_USERNAME_KEY,
                                  form_username,
                                  FORM_PASSWORD_KEY,
                                  form_password,
                                  NULL);
}

void
_ephy_profile_store_form_auth_data (const char *uri,
                                    const char *form_username,
                                    const char *form_password,
                                    const char *username,
                                    const char *password)
{
  SoupURI *fake_uri;
  char *fake_uri_str;

  g_return_if_fail (uri);
  g_return_if_fail (form_username);
  g_return_if_fail (form_password);
  g_return_if_fail (username);
  g_return_if_fail (password);

  fake_uri = soup_uri_new (uri);
  if (fake_uri == NULL)
    return;

  normalize_and_prepare_uri (fake_uri, form_username, form_password);
  fake_uri_str = soup_uri_to_string (fake_uri, FALSE);

  gnome_keyring_set_network_password (NULL,
                                      username,
                                      NULL,
                                      fake_uri_str,
                                      NULL,
                                      fake_uri->scheme,
                                      NULL,
                                      fake_uri->port,
                                      password,
                                      (GnomeKeyringOperationGetIntCallback)store_form_password_cb,
                                      NULL,
                                      NULL);
  soup_uri_free (fake_uri);
  g_free (fake_uri_str);
}

void
_ephy_profile_query_form_auth_data (const char *uri,
                                    const char *form_username,
                                    const char *form_password,
                                    GnomeKeyringOperationGetListCallback callback,
                                    gpointer data,
                                    GDestroyNotify destroy_data)
{
  SoupURI *key;
  char *key_str;

  g_return_if_fail (uri);
  g_return_if_fail (form_username);
  g_return_if_fail (form_password);

  key = soup_uri_new (uri);
  g_return_if_fail (key);

  normalize_and_prepare_uri (key, form_username, form_password);

  key_str = soup_uri_to_string (key, FALSE);

  LOG ("Querying Keyring: %s", key_str);
  gnome_keyring_find_network_password (NULL,
                                       NULL,
                                       key_str,
                                       NULL,
                                       NULL,
                                       NULL,
                                       0,
                                       callback,
                                       data,
                                       destroy_data);
  soup_uri_free (key);
  g_free (key_str);
}

#define PROFILE_MIGRATION_FILE ".migrated"

void
_ephy_profile_migrate ()
{
  int latest, i;
  char *migrated_file, *contents;

  /* Figure out the latest migration that occured */
  migrated_file = g_build_filename (ephy_dot_dir (),
                                    PROFILE_MIGRATION_FILE,
                                    NULL);
  if (g_file_test (migrated_file, G_FILE_TEST_EXISTS)) {
    gsize size;
    int result;

    g_file_get_contents (migrated_file, &contents, &size, NULL);
    result = sscanf(contents, "%d", &latest);
    g_free (contents);

    if (result != 1) {
      g_warning (_("Failed to read latest migration marker, aborting profile migration."));
      g_free (migrated_file);
      return;
    }
  } else
    /* Never migrated */
    latest = 0;
  
  for (i = latest; i < PROFILE_MIGRATION_VERSION; i++) {
    EphyProfileMigrator m;

    /* No need to run the password migration twice in a row. It
       appears twice in the list for the benefit of people that were
       using the development snapshots, since an early version didn't
       migrate all passwords correctly. */
    if (i == 1)
      continue;

    m = migrators[i];
    m();
  }

  /* Write down the latest migration */
  contents = g_strdup_printf ("%d", PROFILE_MIGRATION_VERSION);
  g_file_set_contents (migrated_file, contents, -1, NULL);
  g_free (contents);
  g_free (migrated_file);
}

