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

/* import-globals-from head_cache.js */

"use strict";

const { NetUtil } = ChromeUtils.importESModule(
  "resource://gre/modules/NetUtil.sys.mjs"
);
const { CookieXPCShellUtils } = ChromeUtils.importESModule(
  "resource://testing-common/CookieXPCShellUtils.sys.mjs"
);

// Don't pick up default permissions from profile.
Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");

CookieXPCShellUtils.init(this);

function do_check_throws(f, result, stack) {
  if (!stack) {
    stack = Components.stack.caller;
  }

  try {
    f();
  } catch (exc) {
    if (exc.result == result) {
      return;
    }
    do_throw("expected result " + result + ", caught " + exc, stack);
  }
  do_throw("expected result " + result + ", none thrown", stack);
}

// Helper to step a generator function and catch a StopIteration exception.
function do_run_generator(generator) {
  try {
    generator.next();
  } catch (e) {
    do_throw("caught exception " + e, Components.stack.caller);
  }
}

// Helper to finish a generator function test.
function do_finish_generator_test(generator) {
  executeSoon(function () {
    generator.return();
    do_test_finished();
  });
}

function _observer(generator, topic) {
  Services.obs.addObserver(this, topic);

  this.generator = generator;
  this.topic = topic;
}

_observer.prototype = {
  observe(subject, topic) {
    Assert.equal(this.topic, topic);

    Services.obs.removeObserver(this, this.topic);

    // Continue executing the generator function.
    if (this.generator) {
      do_run_generator(this.generator);
    }

    this.generator = null;
    this.topic = null;
  },
};

// Close the cookie database. If a generator is supplied, it will be invoked
// once the close is complete.
function do_close_profile(generator) {
  // Register an observer for db close.
  new _observer(generator, "cookie-db-closed");

  // Close the db.
  let service = Services.cookies.QueryInterface(Ci.nsIObserver);
  service.observe(null, "profile-before-change", null);
}

function _promise_observer(topic) {
  Services.obs.addObserver(this, topic);

  this.topic = topic;
  return new Promise(resolve => (this.resolve = resolve));
}

_promise_observer.prototype = {
  observe(subject, topic) {
    Assert.equal(this.topic, topic);

    Services.obs.removeObserver(this, this.topic);
    if (this.resolve) {
      this.resolve();
    }

    this.resolve = null;
    this.topic = null;
  },
};

// Close the cookie database. And resolve a promise.
function promise_close_profile() {
  // Register an observer for db close.
  let promise = new _promise_observer("cookie-db-closed");

  // Close the db.
  let service = Services.cookies.QueryInterface(Ci.nsIObserver);
  service.observe(null, "profile-before-change", null);

  return promise;
}

// Load the cookie database.
function promise_load_profile() {
  // Register an observer for read completion.
  let promise = new _promise_observer("cookie-db-read");

  // Load the profile.
  let service = Services.cookies.QueryInterface(Ci.nsIObserver);
  service.observe(null, "profile-do-change", "");

  return promise;
}

// Load the cookie database. If a generator is supplied, it will be invoked
// once the load is complete.
function do_load_profile(generator) {
  // Register an observer for read completion.
  new _observer(generator, "cookie-db-read");

  // Load the profile.
  let service = Services.cookies.QueryInterface(Ci.nsIObserver);
  service.observe(null, "profile-do-change", "");
}

// Set a single session cookie using http and test the cookie count
// against 'expected'
function do_set_single_http_cookie(uri, channel, expected) {
  Services.cookies.setCookieStringFromHttp(uri, "foo=bar", channel);
  Assert.equal(Services.cookies.countCookiesFromHost(uri.host), expected);
}

// Set two cookies; via document.channel and via http request.
async function do_set_cookies(uri, channel, session, expected) {
  let suffix = session ? "" : "; max-age=1000";

  // via document.cookie
  const thirdPartyUrl = "http://third.com/";
  const contentPage = await CookieXPCShellUtils.loadContentPage(thirdPartyUrl);
  await contentPage.spawn(
    [
      {
        cookie: "can=has" + suffix,
        url: uri.spec,
      },
    ],
    async function (obj) {
      await new this.content.Promise(resolve => {
        let doc = this.content.document;
        let ifr = doc.createElement("iframe");
        ifr.src = obj.url;
        doc.body.appendChild(ifr);
        ifr.addEventListener("load", async () => {
          await this.SpecialPowers.spawn(ifr, [obj.cookie], cookie => {
            this.content.document.cookie = cookie;
          });
          resolve();
        });
      });
    }
  );
  await contentPage.close();

  Assert.equal(Services.cookies.countCookiesFromHost(uri.host), expected[0]);

  // via http request
  Services.cookies.setCookieStringFromHttp(uri, "hot=dog" + suffix, channel);
  Assert.equal(Services.cookies.countCookiesFromHost(uri.host), expected[1]);
}

function do_count_cookies() {
  return Services.cookies.cookies.length;
}

// Helper object to store cookie data.
function Cookie(
  name,
  value,
  host,
  path,
  expiry,
  lastAccessed,
  creationTime,
  isSession,
  isSecure,
  isHttpOnly,
  inBrowserElement = false,
  originAttributes = {},
  sameSite = Ci.nsICookie.SAMESITE_UNSET,
  schemeMap = Ci.nsICookie.SCHEME_UNSET,
  isPartitioned = false,
  updateTime = null
) {
  this.name = name;
  this.value = value;
  this.host = host;
  this.path = path;
  this.expiry = expiry;
  this.lastAccessed = lastAccessed;
  this.creationTime = creationTime;
  this.isSession = isSession;
  this.isSecure = isSecure;
  this.isHttpOnly = isHttpOnly;
  this.inBrowserElement = inBrowserElement;
  this.originAttributes = originAttributes;
  this.sameSite = sameSite;
  this.schemeMap = schemeMap;
  this.isPartitioned = isPartitioned;
  this.updateTime = updateTime || creationTime;

  // For pre version 15 compatibility:
  this.rawSameSite = sameSite;

  let strippedHost = host.charAt(0) == "." ? host.slice(1) : host;

  try {
    this.baseDomain = Services.eTLD.getBaseDomainFromHost(strippedHost);
  } catch (e) {
    if (
      e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
      e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
    ) {
      this.baseDomain = strippedHost;
    }
  }
}

// Object representing a database connection and associated statements. The
// implementation varies depending on schema version.
function CookieDatabaseConnection(file, schema) {
  // Manually generate a cookies.sqlite file with appropriate rows, columns,
  // and schema version. If it already exists, just set up our statements.
  let exists = file.exists();

  this.db = Services.storage.openDatabase(file);
  this.schema = schema;
  if (!exists) {
    this.db.schemaVersion = schema;
  }

  switch (schema) {
    case 1: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (       \
             id INTEGER PRIMARY KEY,        \
             name TEXT,                     \
             value TEXT,                    \
             host TEXT,                     \
             path TEXT,                     \
             expiry INTEGER,                \
             isSecure INTEGER,              \
             isHttpOnly INTEGER)"
        );
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           id,                            \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           isSecure,                      \
           isHttpOnly)                    \
           VALUES (                       \
           :id,                           \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :isSecure,                     \
           :isHttpOnly)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies WHERE id = :id"
      );

      break;
    }

    case 2: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (       \
             id INTEGER PRIMARY KEY,        \
             name TEXT,                     \
             value TEXT,                    \
             host TEXT,                     \
             path TEXT,                     \
             expiry INTEGER,                \
             lastAccessed INTEGER,          \
             isSecure INTEGER,              \
             isHttpOnly INTEGER)"
        );
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT OR REPLACE INTO moz_cookies ( \
           id,                            \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           isSecure,                      \
           isHttpOnly)                    \
           VALUES (                       \
           :id,                           \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :isSecure,                     \
           :isHttpOnly)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies WHERE id = :id"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id"
      );

      break;
    }

    case 3: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (       \
            id INTEGER PRIMARY KEY,         \
            baseDomain TEXT,                \
            name TEXT,                      \
            value TEXT,                     \
            host TEXT,                      \
            path TEXT,                      \
            expiry INTEGER,                 \
            lastAccessed INTEGER,           \
            isSecure INTEGER,               \
            isHttpOnly INTEGER)"
        );

        this.db.executeSimpleSQL(
          "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"
        );
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           id,                            \
           baseDomain,                    \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           isSecure,                      \
           isHttpOnly)                    \
           VALUES (                       \
           :id,                           \
           :baseDomain,                   \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :isSecure,                     \
           :isHttpOnly)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies WHERE id = :id"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id"
      );

      break;
    }

    case 4: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (       \
            id INTEGER PRIMARY KEY,         \
            baseDomain TEXT,                \
            name TEXT,                      \
            value TEXT,                     \
            host TEXT,                      \
            path TEXT,                      \
            expiry INTEGER,                 \
            lastAccessed INTEGER,           \
            creationTime INTEGER,           \
            isSecure INTEGER,               \
            isHttpOnly INTEGER              \
            CONSTRAINT moz_uniqueid UNIQUE (name, host, path))"
        );

        this.db.executeSimpleSQL(
          "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"
        );

        this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           baseDomain,                    \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           creationTime,                  \
           isSecure,                      \
           isHttpOnly)                    \
           VALUES (                       \
           :baseDomain,                   \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :creationTime,                 \
           :isSecure,                     \
           :isHttpOnly)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies          \
           WHERE name = :name AND host = :host AND path = :path"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
           WHERE name = :name AND host = :host AND path = :path"
      );

      break;
    }

    case 10: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (                  \
            id INTEGER PRIMARY KEY,                    \
            baseDomain TEXT,                           \
            originAttributes TEXT NOT NULL DEFAULT '', \
            name TEXT,                                 \
            value TEXT,                                \
            host TEXT,                                 \
            path TEXT,                                 \
            expiry INTEGER,                            \
            lastAccessed INTEGER,                      \
            creationTime INTEGER,                      \
            isSecure INTEGER,                          \
            isHttpOnly INTEGER,                        \
            inBrowserElement INTEGER DEFAULT 0,        \
            sameSite INTEGER DEFAULT 0,                \
            rawSameSite INTEGER DEFAULT 0,             \
            CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
        );

        this.db.executeSimpleSQL(
          "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"
        );

        this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
        this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           name,                          \
           value,                         \
           host,                          \
           baseDomain,                    \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           creationTime,                  \
           isSecure,                      \
           isHttpOnly,                    \
           inBrowserElement,              \
           originAttributes,              \
           sameSite,                      \
           rawSameSite                    \
         ) VALUES (                       \
           :name,                         \
           :value,                        \
           :host,                         \
           :baseDomain,                   \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :creationTime,                 \
           :isSecure,                     \
           :isHttpOnly,                   \
           :inBrowserElement,             \
           :originAttributes,             \
           :sameSite,                     \
           :rawSameSite)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies          \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      break;
    }

    case 11: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (                  \
            id INTEGER PRIMARY KEY,                    \
            originAttributes TEXT NOT NULL DEFAULT '', \
            name TEXT,                                 \
            value TEXT,                                \
            host TEXT,                                 \
            path TEXT,                                 \
            expiry INTEGER,                            \
            lastAccessed INTEGER,                      \
            creationTime INTEGER,                      \
            isSecure INTEGER,                          \
            isHttpOnly INTEGER,                        \
            inBrowserElement INTEGER DEFAULT 0,        \
            sameSite INTEGER DEFAULT 0,                \
            rawSameSite INTEGER DEFAULT 0,             \
            CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
        );

        this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
        this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           creationTime,                  \
           isSecure,                      \
           isHttpOnly,                    \
           inBrowserElement,              \
           originAttributes,              \
           sameSite,                      \
           rawSameSite                    \
         ) VALUES (                       \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :creationTime,                 \
           :isSecure,                     \
           :isHttpOnly,                   \
           :inBrowserElement,             \
           :originAttributes,             \
           :sameSite,                     \
           :rawSameSite)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies          \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      break;
    }

    case 12: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (                  \
            id INTEGER PRIMARY KEY,                    \
            originAttributes TEXT NOT NULL DEFAULT '', \
            name TEXT,                                 \
            value TEXT,                                \
            host TEXT,                                 \
            path TEXT,                                 \
            expiry INTEGER,                            \
            lastAccessed INTEGER,                      \
            creationTime INTEGER,                      \
            isSecure INTEGER,                          \
            isHttpOnly INTEGER,                        \
            inBrowserElement INTEGER DEFAULT 0,        \
            sameSite INTEGER DEFAULT 0,                \
            rawSameSite INTEGER DEFAULT 0,             \
            schemeMap INTEGER DEFAULT 0,               \
            CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
        );

        this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
        this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           creationTime,                  \
           isSecure,                      \
           isHttpOnly,                    \
           inBrowserElement,              \
           originAttributes,              \
           sameSite,                      \
           rawSameSite,                   \
           schemeMap                      \
         ) VALUES (                       \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :creationTime,                 \
           :isSecure,                     \
           :isHttpOnly,                   \
           :inBrowserElement,             \
           :originAttributes,             \
           :sameSite,                     \
           :rawSameSite,                  \
           :schemeMap)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies          \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      break;
    }

    case 13:
    case 14: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (                     \
            id INTEGER PRIMARY KEY,                       \
            originAttributes TEXT NOT NULL DEFAULT '',    \
            name TEXT,                                    \
            value TEXT,                                   \
            host TEXT,                                    \
            path TEXT,                                    \
            expiry INTEGER,                               \
            lastAccessed INTEGER,                         \
            creationTime INTEGER,                         \
            isSecure INTEGER,                             \
            isHttpOnly INTEGER,                           \
            inBrowserElement INTEGER DEFAULT 0,           \
            sameSite INTEGER DEFAULT 0,                   \
            rawSameSite INTEGER DEFAULT 0,                \
            schemeMap INTEGER DEFAULT 0,                  \
            isPartitionedAttributeSet INTEGER DEFAULT 0,  \
            CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
        );

        this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
        this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           creationTime,                  \
           isSecure,                      \
           isHttpOnly,                    \
           inBrowserElement,              \
           originAttributes,              \
           sameSite,                      \
           rawSameSite,                   \
           schemeMap,                     \
           isPartitionedAttributeSet      \
         ) VALUES (                       \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :creationTime,                 \
           :isSecure,                     \
           :isHttpOnly,                   \
           :inBrowserElement,             \
           :originAttributes,             \
           :sameSite,                     \
           :rawSameSite,                  \
           :schemeMap,                    \
           :isPartitionedAttributeSet)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies          \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      break;
    }
    case 15:
    case 16: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (                     \
            id INTEGER PRIMARY KEY,                       \
            originAttributes TEXT NOT NULL DEFAULT '',    \
            name TEXT,                                    \
            value TEXT,                                   \
            host TEXT,                                    \
            path TEXT,                                    \
            expiry INTEGER,                               \
            lastAccessed INTEGER,                         \
            creationTime INTEGER,                         \
            isSecure INTEGER,                             \
            isHttpOnly INTEGER,                           \
            inBrowserElement INTEGER DEFAULT 0,           \
            sameSite INTEGER DEFAULT 0,                   \
            schemeMap INTEGER DEFAULT 0,                  \
            isPartitionedAttributeSet INTEGER DEFAULT 0,  \
            CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
        );

        this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
        this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           creationTime,                  \
           isSecure,                      \
           isHttpOnly,                    \
           inBrowserElement,              \
           originAttributes,              \
           sameSite,                      \
           schemeMap,                     \
           isPartitionedAttributeSet      \
         ) VALUES (                       \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :creationTime,                 \
           :isSecure,                     \
           :isHttpOnly,                   \
           :inBrowserElement,             \
           :originAttributes,             \
           :sameSite,                     \
           :schemeMap,                    \
           :isPartitionedAttributeSet)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies          \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      break;
    }

    case 17: {
      if (!exists) {
        this.db.executeSimpleSQL(
          "CREATE TABLE moz_cookies (                     \
            id INTEGER PRIMARY KEY,                       \
            originAttributes TEXT NOT NULL DEFAULT '',    \
            name TEXT,                                    \
            value TEXT,                                   \
            host TEXT,                                    \
            path TEXT,                                    \
            expiry INTEGER,                               \
            lastAccessed INTEGER,                         \
            creationTime INTEGER,                         \
            isSecure INTEGER,                             \
            isHttpOnly INTEGER,                           \
            inBrowserElement INTEGER DEFAULT 0,           \
            sameSite INTEGER DEFAULT 0,                   \
            schemeMap INTEGER DEFAULT 0,                  \
            isPartitionedAttributeSet INTEGER DEFAULT 0,  \
            updateTime INTEGER,                           \
            CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
        );

        this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
        this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
      }

      this.stmtInsert = this.db.createStatement(
        "INSERT INTO moz_cookies (        \
           name,                          \
           value,                         \
           host,                          \
           path,                          \
           expiry,                        \
           lastAccessed,                  \
           creationTime,                  \
           isSecure,                      \
           isHttpOnly,                    \
           inBrowserElement,              \
           originAttributes,              \
           sameSite,                      \
           schemeMap,                     \
           isPartitionedAttributeSet,     \
           updateTime                     \
         ) VALUES (                       \
           :name,                         \
           :value,                        \
           :host,                         \
           :path,                         \
           :expiry,                       \
           :lastAccessed,                 \
           :creationTime,                 \
           :isSecure,                     \
           :isHttpOnly,                   \
           :inBrowserElement,             \
           :originAttributes,             \
           :sameSite,                     \
           :schemeMap,                    \
           :isPartitionedAttributeSet,    \
           :updateTime)"
      );

      this.stmtDelete = this.db.createStatement(
        "DELETE FROM moz_cookies          \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      this.stmtUpdate = this.db.createStatement(
        "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
           WHERE name = :name AND host = :host AND path = :path AND \
                 originAttributes = :originAttributes"
      );

      break;
    }

    default:
      do_throw("unrecognized schemaVersion!");
  }
}

CookieDatabaseConnection.prototype = {
  insertCookie(cookie) {
    if (!(cookie instanceof Cookie)) {
      do_throw("not a cookie");
    }

    switch (this.schema) {
      case 1:
        this.stmtInsert.bindByName("id", cookie.creationTime);
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        break;

      case 2:
        this.stmtInsert.bindByName("id", cookie.creationTime);
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        break;

      case 3:
        this.stmtInsert.bindByName("id", cookie.creationTime);
        this.stmtInsert.bindByName("baseDomain", cookie.baseDomain);
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        break;

      case 4:
        this.stmtInsert.bindByName("baseDomain", cookie.baseDomain);
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("creationTime", cookie.creationTime);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        break;

      case 10:
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("baseDomain", cookie.baseDomain);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("creationTime", cookie.creationTime);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
        this.stmtInsert.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        this.stmtInsert.bindByName("sameSite", cookie.sameSite);
        this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite);
        break;

      case 11:
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("creationTime", cookie.creationTime);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
        this.stmtInsert.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        this.stmtInsert.bindByName("sameSite", cookie.sameSite);
        this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite);
        break;

      case 12:
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("creationTime", cookie.creationTime);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
        this.stmtInsert.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        this.stmtInsert.bindByName("sameSite", cookie.sameSite);
        this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite);
        this.stmtInsert.bindByName("schemeMap", cookie.schemeMap);
        break;

      case 13:
      case 14:
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("creationTime", cookie.creationTime);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
        this.stmtInsert.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        this.stmtInsert.bindByName("sameSite", cookie.sameSite);
        this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite);
        this.stmtInsert.bindByName("schemeMap", cookie.schemeMap);
        this.stmtInsert.bindByName(
          "isPartitionedAttributeSet",
          cookie.isPartitioned
        );
        break;

      case 15:
      case 16:
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("creationTime", cookie.creationTime);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
        this.stmtInsert.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        this.stmtInsert.bindByName("sameSite", cookie.sameSite);
        this.stmtInsert.bindByName("schemeMap", cookie.schemeMap);
        this.stmtInsert.bindByName(
          "isPartitionedAttributeSet",
          cookie.isPartitioned
        );
        break;

      case 17:
        this.stmtInsert.bindByName("name", cookie.name);
        this.stmtInsert.bindByName("value", cookie.value);
        this.stmtInsert.bindByName("host", cookie.host);
        this.stmtInsert.bindByName("path", cookie.path);
        this.stmtInsert.bindByName("expiry", cookie.expiry);
        this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
        this.stmtInsert.bindByName("creationTime", cookie.creationTime);
        this.stmtInsert.bindByName("isSecure", cookie.isSecure);
        this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
        this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
        this.stmtInsert.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        this.stmtInsert.bindByName("sameSite", cookie.sameSite);
        this.stmtInsert.bindByName("schemeMap", cookie.schemeMap);
        this.stmtInsert.bindByName(
          "isPartitionedAttributeSet",
          cookie.isPartitioned
        );
        this.stmtInsert.bindByName("updateTime", cookie.updateTime);
        break;

      default:
        do_throw("unrecognized schemaVersion!");
    }

    do_execute_stmt(this.stmtInsert);
  },

  deleteCookie(cookie) {
    if (!(cookie instanceof Cookie)) {
      do_throw("not a cookie");
    }

    switch (this.db.schemaVersion) {
      case 1:
      case 2:
      case 3:
        this.stmtDelete.bindByName("id", cookie.creationTime);
        break;

      case 4:
        this.stmtDelete.bindByName("name", cookie.name);
        this.stmtDelete.bindByName("host", cookie.host);
        this.stmtDelete.bindByName("path", cookie.path);
        break;

      case 10:
      case 11:
      case 12:
      case 13:
      case 14:
      case 15:
      case 16:
      case 17:
        this.stmtDelete.bindByName("name", cookie.name);
        this.stmtDelete.bindByName("host", cookie.host);
        this.stmtDelete.bindByName("path", cookie.path);
        this.stmtDelete.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        break;

      default:
        do_throw("unrecognized schemaVersion!");
    }

    do_execute_stmt(this.stmtDelete);
  },

  updateCookie(cookie) {
    if (!(cookie instanceof Cookie)) {
      do_throw("not a cookie");
    }

    switch (this.db.schemaVersion) {
      case 1:
        do_throw("can't update a schema 1 cookie!");
        break;
      case 2:
      case 3:
        this.stmtUpdate.bindByName("id", cookie.creationTime);
        this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed);
        break;

      case 4:
        this.stmtDelete.bindByName("name", cookie.name);
        this.stmtDelete.bindByName("host", cookie.host);
        this.stmtDelete.bindByName("path", cookie.path);
        this.stmtUpdate.bindByName("name", cookie.name);
        this.stmtUpdate.bindByName("host", cookie.host);
        this.stmtUpdate.bindByName("path", cookie.path);
        this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed);
        break;

      case 10:
      case 11:
      case 12:
      case 13:
      case 14:
      case 15:
      case 16:
      case 17:
        this.stmtDelete.bindByName("name", cookie.name);
        this.stmtDelete.bindByName("host", cookie.host);
        this.stmtDelete.bindByName("path", cookie.path);
        this.stmtDelete.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        this.stmtUpdate.bindByName("name", cookie.name);
        this.stmtUpdate.bindByName("host", cookie.host);
        this.stmtUpdate.bindByName("path", cookie.path);
        this.stmtUpdate.bindByName(
          "originAttributes",
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
        );
        this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed);
        break;

      default:
        do_throw("unrecognized schemaVersion!");
    }

    do_execute_stmt(this.stmtUpdate);
  },

  close() {
    this.stmtInsert.finalize();
    this.stmtDelete.finalize();
    if (this.stmtUpdate) {
      this.stmtUpdate.finalize();
    }
    this.db.close();

    this.stmtInsert = null;
    this.stmtDelete = null;
    this.stmtUpdate = null;
    this.db = null;
  },
};

function do_get_cookie_file(profile) {
  let file = profile.clone();
  file.append("cookies.sqlite");
  return file;
}

// Count the cookies from 'host' in a database. If 'host' is null, count all
// cookies.
function do_count_cookies_in_db(connection, host) {
  let select = null;
  if (host) {
    select = connection.createStatement(
      "SELECT COUNT(1) FROM moz_cookies WHERE host = :host"
    );
    select.bindByName("host", host);
  } else {
    select = connection.createStatement("SELECT COUNT(1) FROM moz_cookies");
  }

  select.executeStep();
  let result = select.getInt32(0);
  select.reset();
  select.finalize();
  return result;
}

// Execute 'stmt', ensuring that we reset it if it throws.
function do_execute_stmt(stmt) {
  try {
    stmt.executeStep();
    stmt.reset();
  } catch (e) {
    stmt.reset();
    throw e;
  }
}
