diff --git a/CHANGELOG.md b/CHANGELOG.md index 60d90a35c7..dab841d853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # v153.0 (In progress) +### Autofill + +- Added a new `passports` record type (name, country, passport number, issue/expiry dates) with full CRUD on the `Store` API. The schema is laid out sync-ready, but no sync engine is registered yet, so passports are not synced. + ### Nimbus - Enrollment change events (visible to the UDL) now include the feature IDs of the features involved when possible. ([#7391](https://github.com/mozilla/application-services/pull/7391/)) diff --git a/components/autofill/sql/create_shared_schema.sql b/components/autofill/sql/create_shared_schema.sql index 86d4b07057..9015d4838f 100644 --- a/components/autofill/sql/create_shared_schema.sql +++ b/components/autofill/sql/create_shared_schema.sql @@ -90,6 +90,43 @@ CREATE TABLE IF NOT EXISTS credit_cards_tombstones ( time_deleted INTEGER NOT NULL ) WITHOUT ROWID; +-- Passport records. The passport number is stored as plaintext (no field-level +-- encryption). +CREATE TABLE IF NOT EXISTS passports_data ( + guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0), + name TEXT NOT NULL, -- full name on passport + country TEXT NOT NULL, -- ISO 3166 code + passport_number TEXT NOT NULL, + issue_date_month INTEGER, + issue_date_day INTEGER, + issue_date_year INTEGER, + expiry_date_month INTEGER, + expiry_date_day INTEGER, + expiry_date_year INTEGER, + + time_created INTEGER NOT NULL, + time_last_used INTEGER, + time_last_modified INTEGER NOT NULL, + times_used INTEGER NOT NULL, + + /* Same "sync change counter" strategy used by other components. */ + sync_change_counter INTEGER NOT NULL +); + +-- What's on the server as the JSON payload. +CREATE TABLE IF NOT EXISTS passports_mirror ( + guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0), + -- The plain-text sync15 payload. + payload TEXT NOT NULL CHECK(length(payload) != 0) +); + +-- Tombstones are items deleted locally but not deleted in the mirror (ie, ones +-- we are yet to upload) +CREATE TABLE IF NOT EXISTS passports_tombstones ( + guid TEXT PRIMARY KEY CHECK(length(guid) != 0), + time_deleted INTEGER NOT NULL +) WITHOUT ROWID; + -- This table holds key-value metadata for the Autofill component and its consumers. CREATE TABLE IF NOT EXISTS moz_meta ( key TEXT PRIMARY KEY, diff --git a/components/autofill/sql/tests/create_v4_db.sql b/components/autofill/sql/tests/create_v4_db.sql new file mode 100644 index 0000000000..88e4e3c4e5 --- /dev/null +++ b/components/autofill/sql/tests/create_v4_db.sql @@ -0,0 +1,105 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at http://mozilla.org/MPL/2.0/. + +-- Initialize the v3 schema + +CREATE TABLE IF NOT EXISTS addresses_data ( + guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0), + name TEXT NOT NULL, + organization TEXT NOT NULL, -- Company + street_address TEXT NOT NULL, -- (Multiline) + address_level3 TEXT NOT NULL, -- Suburb/Sublocality + address_level2 TEXT NOT NULL, -- City/Town + address_level1 TEXT NOT NULL, -- Province (Standardized code if possible) + postal_code TEXT NOT NULL, + country TEXT NOT NULL, -- ISO 3166 + tel TEXT NOT NULL, -- Stored in E.164 format + email TEXT NOT NULL, + + time_created INTEGER NOT NULL, + time_last_used INTEGER NOT NULL, + time_last_modified INTEGER NOT NULL, + times_used INTEGER NOT NULL, + + sync_change_counter INTEGER NOT NULL +); + +-- What's on the server as the JSON payload. +CREATE TABLE IF NOT EXISTS addresses_mirror ( + guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0), + payload TEXT NOT NULL CHECK(length(payload) != 0) + -- We could also have `modified`, which is in the server response and + -- passed around in the sync code, but we don't have a use-case for using it. +); + +-- Tombstones are items deleted locally but not deleted in the mirror (ie, ones +-- we are yet to upload) +CREATE TABLE IF NOT EXISTS addresses_tombstones ( + guid TEXT PRIMARY KEY CHECK(length(guid) != 0), + time_deleted INTEGER NOT NULL +) WITHOUT ROWID; + +CREATE TABLE IF NOT EXISTS credit_cards_data ( +guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0), +cc_name TEXT NOT NULL, +cc_number_enc TEXT NOT NULL CHECK(length(cc_number_enc) > 20 OR cc_number_enc == ''), +cc_number_last_4 TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4), +cc_exp_month INTEGER, +cc_exp_year INTEGER, +cc_type TEXT NOT NULL, +time_created INTEGER NOT NULL, +time_last_used INTEGER, +time_last_modified INTEGER NOT NULL, +times_used INTEGER NOT NULL, +sync_change_counter INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS credit_cards_mirror ( + guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0), + payload TEXT NOT NULL CHECK(length(payload) != 0) +); + +CREATE TABLE IF NOT EXISTS credit_cards_tombstones ( + guid TEXT PRIMARY KEY CHECK(length(guid) != 0), + time_deleted INTEGER NOT NULL +) WITHOUT ROWID; + +-- This table holds key-value metadata for the Autofill component and its consumers. +CREATE TABLE IF NOT EXISTS moz_meta ( + key TEXT PRIMARY KEY, + value NOT NULL +) WITHOUT ROWID; + +-- Populate it with some data, we test that this makes it through all the migrations. +INSERT INTO credit_cards_data ( + guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month, cc_exp_year, + cc_type, time_created, time_last_used, time_last_modified, times_used, + sync_change_counter +) VALUES ( + "A", "Jane Doe", "012345678901234567890", "1234", 1, 2020, "visa", 0, 1, 2, + 3, 0 +); + +INSERT INTO addresses_data ( + guid, name, organization, street_address, address_level3, + address_level2, address_level1, postal_code, country, tel, + email, time_created, time_last_used, time_last_modified, + times_used, sync_change_counter +) VALUES ( + "A", "Jane John Doe", "Mozilla", "123 Maple lane", "Shelbyville", + "Springfield", "Massachusetts", "12345", "US", "01-234-567-8000", "jane@hotmail.com", 0, + 1, 2, 3, 0 +); + +INSERT INTO addresses_data ( + guid, name, organization, street_address, address_level3, + address_level2, address_level1, postal_code, country, tel, + email, time_created, time_last_used, time_last_modified, + times_used, sync_change_counter +) VALUES ( + "B", "", "Mozilla", "123 Maple lane", "Shelbyville", + "Toronto", "Ontario", "12345", "CA", "01-234-567-8000", "jane@hotmail.com", 0, + 1, 2, 3, 0 +); +PRAGMA user_version=4; diff --git a/components/autofill/src/autofill.udl b/components/autofill/src/autofill.udl index 59b6b8c288..660b270616 100644 --- a/components/autofill/src/autofill.udl +++ b/components/autofill/src/autofill.udl @@ -75,6 +75,38 @@ dictionary Address { i64 times_used; }; +/// What you pass to create or update a passport. +dictionary UpdatablePassportFields { + string name; + string country; + string passport_number; + i64 issue_date_month; + i64 issue_date_day; + i64 issue_date_year; + i64 expiry_date_month; + i64 expiry_date_day; + i64 expiry_date_year; +}; + +/// What you get back as a passport. +dictionary Passport { + string guid; + string name; + string country; + string passport_number; + i64 issue_date_month; + i64 issue_date_day; + i64 issue_date_year; + i64 expiry_date_month; + i64 expiry_date_day; + i64 expiry_date_year; + + i64 time_created; + i64? time_last_used; + i64 time_last_modified; + i64 times_used; +}; + /// Metrics tracking scrubbing of credit cards that cannot be decrypted, see // `scrub_undecryptable_credit_card_data_for_remote_replacement` for more details dictionary CreditCardsDeletionMetrics { @@ -136,6 +168,27 @@ interface Store { [Throws=AutofillApiError] void touch_address(string guid); + [Throws=AutofillApiError] + Passport add_passport(UpdatablePassportFields p); + + [Throws=AutofillApiError] + Passport get_passport(string guid); + + [Throws=AutofillApiError] + sequence get_all_passports(); + + [Throws=AutofillApiError] + i64 count_all_passports(); + + [Throws=AutofillApiError] + void update_passport(string guid, UpdatablePassportFields p); + + [Throws=AutofillApiError] + boolean delete_passport(string guid); + + [Throws=AutofillApiError] + void touch_passport(string guid); + [Throws=AutofillApiError, Self=ByArc] void scrub_encrypted_data(); diff --git a/components/autofill/src/db/mod.rs b/components/autofill/src/db/mod.rs index 74f4db50d2..6527a49e13 100644 --- a/components/autofill/src/db/mod.rs +++ b/components/autofill/src/db/mod.rs @@ -5,6 +5,7 @@ pub mod addresses; pub mod credit_cards; pub mod models; +pub mod passports; pub mod schema; pub mod store; diff --git a/components/autofill/src/db/models/mod.rs b/components/autofill/src/db/models/mod.rs index ef0560eb76..a114777852 100644 --- a/components/autofill/src/db/models/mod.rs +++ b/components/autofill/src/db/models/mod.rs @@ -5,6 +5,7 @@ pub mod address; pub mod credit_card; +pub mod passport; use types::Timestamp; /// Metadata that's common between the records. diff --git a/components/autofill/src/db/models/passport.rs b/components/autofill/src/db/models/passport.rs new file mode 100644 index 0000000000..87f4d27983 --- /dev/null +++ b/components/autofill/src/db/models/passport.rs @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +use super::Metadata; +use rusqlite::Row; +use sync_guid::Guid; + +// What you pass to create or update a passport. +#[derive(Debug, Clone, Default)] +pub struct UpdatablePassportFields { + pub name: String, + pub country: String, + pub passport_number: String, + pub issue_date_month: i64, + pub issue_date_day: i64, + pub issue_date_year: i64, + pub expiry_date_month: i64, + pub expiry_date_day: i64, + pub expiry_date_year: i64, +} + +// "Passport" is what we return to consumers and has most of the metadata. +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +pub struct Passport { + pub guid: String, + pub name: String, + pub country: String, + pub passport_number: String, + pub issue_date_month: i64, + pub issue_date_day: i64, + pub issue_date_year: i64, + pub expiry_date_month: i64, + pub expiry_date_day: i64, + pub expiry_date_year: i64, + + // We expose some of the metadata + pub time_created: i64, + pub time_last_used: Option, + pub time_last_modified: i64, + pub times_used: i64, +} + +// This is used to "externalize" a passport, suitable for handing back to +// consumers. +impl From for Passport { + fn from(ip: InternalPassport) -> Self { + Passport { + guid: ip.guid.to_string(), + name: ip.name, + country: ip.country, + passport_number: ip.passport_number, + issue_date_month: ip.issue_date_month, + issue_date_day: ip.issue_date_day, + issue_date_year: ip.issue_date_year, + expiry_date_month: ip.expiry_date_month, + expiry_date_day: ip.expiry_date_day, + expiry_date_year: ip.expiry_date_year, + // note we can't use u64 in uniffi + time_created: u64::from(ip.metadata.time_created) as i64, + time_last_used: if ip.metadata.time_last_used.0 == 0 { + None + } else { + Some(ip.metadata.time_last_used.0 as i64) + }, + time_last_modified: u64::from(ip.metadata.time_last_modified) as i64, + times_used: ip.metadata.times_used, + } + } +} + +// An "internal" passport is used by the public APIs and by sync. +#[derive(Debug, Clone, Default)] +pub struct InternalPassport { + pub guid: Guid, + pub name: String, + pub country: String, + pub passport_number: String, + pub issue_date_month: i64, + pub issue_date_day: i64, + pub issue_date_year: i64, + pub expiry_date_month: i64, + pub expiry_date_day: i64, + pub expiry_date_year: i64, + pub metadata: Metadata, +} + +impl InternalPassport { + pub fn from_row(row: &Row<'_>) -> Result { + Ok(Self { + guid: Guid::from_string(row.get("guid")?), + name: row.get("name")?, + country: row.get("country")?, + passport_number: row.get("passport_number")?, + issue_date_month: row.get("issue_date_month")?, + issue_date_day: row.get("issue_date_day")?, + issue_date_year: row.get("issue_date_year")?, + expiry_date_month: row.get("expiry_date_month")?, + expiry_date_day: row.get("expiry_date_day")?, + expiry_date_year: row.get("expiry_date_year")?, + metadata: Metadata { + time_created: row.get("time_created")?, + time_last_used: row.get("time_last_used")?, + time_last_modified: row.get("time_last_modified")?, + times_used: row.get("times_used")?, + sync_change_counter: row.get("sync_change_counter")?, + }, + }) + } +} diff --git a/components/autofill/src/db/passports.rs b/components/autofill/src/db/passports.rs new file mode 100644 index 0000000000..c8aa1100e5 --- /dev/null +++ b/components/autofill/src/db/passports.rs @@ -0,0 +1,316 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +use crate::db::{ + models::{ + passport::{InternalPassport, UpdatablePassportFields}, + Metadata, + }, + schema::{PASSPORT_COMMON_COLS, PASSPORT_COMMON_VALS}, +}; +use crate::error::*; + +use rusqlite::{Connection, Transaction}; +use sync_guid::Guid; +use types::Timestamp; + +pub(crate) fn add_passport( + conn: &Connection, + new: UpdatablePassportFields, +) -> Result { + let tx = conn.unchecked_transaction()?; + let now = Timestamp::now(); + + let passport = InternalPassport { + guid: Guid::random(), + name: new.name, + country: new.country, + passport_number: new.passport_number, + issue_date_month: new.issue_date_month, + issue_date_day: new.issue_date_day, + issue_date_year: new.issue_date_year, + expiry_date_month: new.expiry_date_month, + expiry_date_day: new.expiry_date_day, + expiry_date_year: new.expiry_date_year, + metadata: Metadata { + time_created: now, + time_last_modified: now, + ..Default::default() + }, + }; + add_internal_passport(&tx, &passport)?; + tx.commit()?; + Ok(passport) +} + +fn add_internal_passport(tx: &Transaction<'_>, passport: &InternalPassport) -> Result<()> { + tx.execute( + &format!( + "INSERT INTO passports_data ( + {common_cols}, + sync_change_counter + ) VALUES ( + {common_vals}, + :sync_change_counter + )", + common_cols = PASSPORT_COMMON_COLS, + common_vals = PASSPORT_COMMON_VALS, + ), + rusqlite::named_params! { + ":guid": passport.guid, + ":name": passport.name, + ":country": passport.country, + ":passport_number": passport.passport_number, + ":issue_date_month": passport.issue_date_month, + ":issue_date_day": passport.issue_date_day, + ":issue_date_year": passport.issue_date_year, + ":expiry_date_month": passport.expiry_date_month, + ":expiry_date_day": passport.expiry_date_day, + ":expiry_date_year": passport.expiry_date_year, + ":time_created": passport.metadata.time_created, + ":time_last_used": passport.metadata.time_last_used, + ":time_last_modified": passport.metadata.time_last_modified, + ":times_used": passport.metadata.times_used, + ":sync_change_counter": passport.metadata.sync_change_counter, + }, + )?; + Ok(()) +} + +pub(crate) fn get_passport(conn: &Connection, guid: &Guid) -> Result { + let sql = format!( + "SELECT + {common_cols}, + sync_change_counter + FROM passports_data + WHERE guid = :guid", + common_cols = PASSPORT_COMMON_COLS + ); + conn.query_row(&sql, [guid], InternalPassport::from_row) + .map_err(|e| match e { + rusqlite::Error::QueryReturnedNoRows => Error::NoSuchRecord(guid.to_string()), + e => e.into(), + }) +} + +pub(crate) fn get_all_passports(conn: &Connection) -> Result> { + let sql = format!( + "SELECT + {common_cols}, + sync_change_counter + FROM passports_data", + common_cols = PASSPORT_COMMON_COLS + ); + let mut stmt = conn.prepare(&sql)?; + let passports = stmt + .query_map([], InternalPassport::from_row)? + .collect::, _>>()?; + Ok(passports) +} + +pub(crate) fn count_all_passports(conn: &Connection) -> Result { + let sql = "SELECT COUNT(*) FROM passports_data"; + let mut stmt = conn.prepare(sql)?; + let count: i64 = stmt.query_row([], |row| row.get(0))?; + Ok(count) +} + +/// Updates just the "updatable" columns - suitable for exposure as a public +/// API. +pub(crate) fn update_passport( + conn: &Connection, + guid: &Guid, + passport: &UpdatablePassportFields, +) -> Result<()> { + let tx = conn.unchecked_transaction()?; + tx.execute( + "UPDATE passports_data + SET name = :name, + country = :country, + passport_number = :passport_number, + issue_date_month = :issue_date_month, + issue_date_day = :issue_date_day, + issue_date_year = :issue_date_year, + expiry_date_month = :expiry_date_month, + expiry_date_day = :expiry_date_day, + expiry_date_year = :expiry_date_year, + time_last_modified = :time_last_modified, + sync_change_counter = sync_change_counter + 1 + WHERE guid = :guid", + rusqlite::named_params! { + ":name": passport.name, + ":country": passport.country, + ":passport_number": passport.passport_number, + ":issue_date_month": passport.issue_date_month, + ":issue_date_day": passport.issue_date_day, + ":issue_date_year": passport.issue_date_year, + ":expiry_date_month": passport.expiry_date_month, + ":expiry_date_day": passport.expiry_date_day, + ":expiry_date_year": passport.expiry_date_year, + ":time_last_modified": Timestamp::now(), + ":guid": guid, + }, + )?; + tx.commit()?; + Ok(()) +} + +pub(crate) fn delete_passport(conn: &Connection, guid: &Guid) -> Result { + let tx = conn.unchecked_transaction()?; + // execute returns how many rows were affected. + let exists = tx.execute( + "DELETE FROM passports_data WHERE guid = :guid", + rusqlite::named_params! { ":guid": guid }, + )? != 0; + tx.commit()?; + Ok(exists) +} + +pub fn touch(conn: &Connection, guid: &Guid) -> Result<()> { + let tx = conn.unchecked_transaction()?; + let now_ms = Timestamp::now(); + tx.execute( + "UPDATE passports_data + SET time_last_used = :time_last_used, + times_used = times_used + 1, + sync_change_counter = sync_change_counter + 1 + WHERE guid = :guid", + rusqlite::named_params! { + ":time_last_used": now_ms, + ":guid": guid, + }, + )?; + tx.commit()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::test::new_mem_db; + + fn sample_fields(name: &str, number: &str) -> UpdatablePassportFields { + UpdatablePassportFields { + name: name.to_string(), + country: "CA".to_string(), + passport_number: number.to_string(), + issue_date_month: 1, + issue_date_day: 15, + issue_date_year: 2020, + expiry_date_month: 1, + expiry_date_day: 15, + expiry_date_year: 2030, + } + } + + #[test] + fn test_passport_create_and_read() -> Result<()> { + let db = new_mem_db(); + + let saved = add_passport(&db, sample_fields("Jane Doe", "X1234567"))?; + + // add populated the guid and timestamps + assert_ne!(Guid::default(), saved.guid); + assert_ne!(0, saved.metadata.time_created.as_millis()); + assert_ne!(0, saved.metadata.time_last_modified.as_millis()); + + let retrieved = get_passport(&db, &saved.guid)?; + assert_eq!(saved.guid, retrieved.guid); + assert_eq!(retrieved.name, "Jane Doe"); + assert_eq!(retrieved.country, "CA"); + assert_eq!(retrieved.passport_number, "X1234567"); + assert_eq!(retrieved.issue_date_month, 1); + assert_eq!(retrieved.issue_date_day, 15); + assert_eq!(retrieved.issue_date_year, 2020); + assert_eq!(retrieved.expiry_date_year, 2030); + + // deleting removes it + assert!(delete_passport(&db, &saved.guid)?); + assert!(get_passport(&db, &saved.guid).is_err()); + + Ok(()) + } + + #[test] + fn test_passport_missing_guid() { + let db = new_mem_db(); + let guid = Guid::random(); + let result = get_passport(&db, &guid); + assert_eq!( + result.unwrap_err().to_string(), + Error::NoSuchRecord(guid.to_string()).to_string() + ); + } + + #[test] + fn test_passport_read_all() -> Result<()> { + let db = new_mem_db(); + + let a = add_passport(&db, sample_fields("Jane Doe", "A1"))?; + let b = add_passport(&db, sample_fields("John Deer", "B2"))?; + let c = add_passport(&db, sample_fields("Abe Lincoln", "C3"))?; + + assert!(delete_passport(&db, &c.guid)?); + + let all = get_all_passports(&db)?; + assert_eq!(all.len(), 2); + assert_eq!(count_all_passports(&db)?, 2); + + let guids = [all[0].guid.as_str(), all[1].guid.as_str()]; + assert!(guids.contains(&a.guid.as_str())); + assert!(guids.contains(&b.guid.as_str())); + + Ok(()) + } + + #[test] + fn test_passport_update() -> Result<()> { + let db = new_mem_db(); + let saved = add_passport(&db, sample_fields("John Deer", "Z9"))?; + assert_eq!(saved.metadata.sync_change_counter, 0); + + let mut fields = sample_fields("John Doe", "Z9"); + fields.expiry_date_year = 2035; + update_passport(&db, &saved.guid, &fields)?; + + let updated = get_passport(&db, &saved.guid)?; + assert_eq!(updated.name, "John Doe"); + assert_eq!(updated.expiry_date_year, 2035); + // updating bumps the sync change counter + assert_eq!(updated.metadata.sync_change_counter, 1); + + Ok(()) + } + + #[test] + fn test_passport_delete() -> Result<()> { + let db = new_mem_db(); + let saved = add_passport(&db, sample_fields("Jane Doe", "D1"))?; + + assert!(delete_passport(&db, &saved.guid)?); + // deleting a non-existent record returns false + assert!(!delete_passport(&db, &saved.guid)?); + + Ok(()) + } + + #[test] + fn test_passport_touch() -> Result<()> { + let db = new_mem_db(); + let saved = add_passport(&db, sample_fields("Jane Doe", "T1"))?; + assert_eq!(saved.metadata.times_used, 0); + assert_eq!(saved.metadata.sync_change_counter, 0); + + touch(&db, &saved.guid)?; + + let touched = get_passport(&db, &saved.guid)?; + assert_eq!(touched.metadata.times_used, 1); + assert!(touched.metadata.time_last_used.as_millis() > 0); + // touching bumps the sync change counter + assert_eq!(touched.metadata.sync_change_counter, 1); + + Ok(()) + } +} diff --git a/components/autofill/src/db/schema.rs b/components/autofill/src/db/schema.rs index 10944ddaf4..fab83f3473 100644 --- a/components/autofill/src/db/schema.rs +++ b/components/autofill/src/db/schema.rs @@ -68,6 +68,38 @@ pub const CREDIT_CARD_COMMON_VALS: &str = " :time_last_modified, :times_used"; +pub const PASSPORT_COMMON_COLS: &str = " + guid, + name, + country, + passport_number, + issue_date_month, + issue_date_day, + issue_date_year, + expiry_date_month, + expiry_date_day, + expiry_date_year, + time_created, + time_last_used, + time_last_modified, + times_used"; + +pub const PASSPORT_COMMON_VALS: &str = " + :guid, + :name, + :country, + :passport_number, + :issue_date_month, + :issue_date_day, + :issue_date_year, + :expiry_date_month, + :expiry_date_day, + :expiry_date_year, + :time_created, + :time_last_used, + :time_last_modified, + :times_used"; + const CREATE_SHARED_SCHEMA_SQL: &str = include_str!("../../sql/create_shared_schema.sql"); const CREATE_SHARED_TRIGGERS_SQL: &str = include_str!("../../sql/create_shared_triggers.sql"); const CREATE_SYNC_TEMP_TABLES_SQL: &str = include_str!("../../sql/create_sync_temp_tables.sql"); @@ -76,7 +108,7 @@ pub struct AutofillConnectionInitializer; impl ConnectionInitializer for AutofillConnectionInitializer { const NAME: &'static str = "autofill db"; - const END_VERSION: u32 = 4; + const END_VERSION: u32 = 5; fn prepare(&self, conn: &Connection, _db_empty: bool) -> Result<()> { define_functions(conn)?; @@ -107,6 +139,7 @@ impl ConnectionInitializer for AutofillConnectionInitializer { 1 => upgrade_from_v1(db), 2 => upgrade_from_v2(db), 3 => upgrade_from_v3(db), + 4 => upgrade_from_v4(db), _ => Err(Error::IncompatibleVersion(version)), } } @@ -238,6 +271,15 @@ fn upgrade_from_v3(db: &Connection) -> Result<()> { Ok(()) } +fn upgrade_from_v4(db: &Connection) -> Result<()> { + // v4 -> v5 only adds new tables (the passports_* tables), so we can just + // re-run the shared schema, which uses `CREATE TABLE IF NOT EXISTS`. This + // is the approach used by most other components and avoids duplicating the + // table definitions in a separate migration file. + db.execute_batch(CREATE_SHARED_SCHEMA_SQL)?; + Ok(()) +} + pub fn create_empty_sync_temp_tables(db: &Connection) -> Result<()> { debug!("Initializing sync temp tables"); db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)?; @@ -258,6 +300,7 @@ mod tests { const CREATE_V1_DB: &str = include_str!("../../sql/tests/create_v1_db.sql"); const CREATE_V2_DB: &str = include_str!("../../sql/tests/create_v2_db.sql"); const CREATE_V3_DB: &str = include_str!("../../sql/tests/create_v3_db.sql"); + const CREATE_V4_DB: &str = include_str!("../../sql/tests/create_v4_db.sql"); #[test] fn test_create_schema_twice() { @@ -408,4 +451,20 @@ mod tests { assert_eq!(address.guid, "B"); assert_eq!(address.address_level1, "ON"); } + + #[test] + fn test_upgrade_version_4() { + let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V4_DB); + let db = db_file.open(); + + // passports_data must not exist yet at v4. + db.execute_batch("SELECT guid FROM passports_data") + .expect_err("passports_data should not exist at v4"); + + db_file.upgrade_to(5); + + // After upgrading to v5 the table exists. + db.execute_batch("SELECT guid FROM passports_data") + .expect("passports_data should exist at v5"); + } } diff --git a/components/autofill/src/db/store.rs b/components/autofill/src/db/store.rs index 68c738e273..d48c335162 100644 --- a/components/autofill/src/db/store.rs +++ b/components/autofill/src/db/store.rs @@ -4,7 +4,10 @@ use crate::db::models::address::{Address, UpdatableAddressFields}; use crate::db::models::credit_card::{CreditCard, UpdatableCreditCardFields}; -use crate::db::{addresses, credit_cards, credit_cards::CreditCardsDeletionMetrics, AutofillDb}; +use crate::db::models::passport::{Passport, UpdatablePassportFields}; +use crate::db::{ + addresses, credit_cards, credit_cards::CreditCardsDeletionMetrics, passports, AutofillDb, +}; use crate::error::*; use error_support::handle_error; use rusqlite::{ @@ -163,6 +166,53 @@ impl Store { addresses::touch(&self.db.lock().unwrap().writer, &Guid::new(&guid)) } + #[handle_error(Error)] + pub fn add_passport(&self, fields: UpdatablePassportFields) -> ApiResult { + Ok(passports::add_passport(&self.db.lock().unwrap().writer, fields)?.into()) + } + + #[handle_error(Error)] + pub fn get_passport(&self, guid: String) -> ApiResult { + Ok(passports::get_passport(&self.db.lock().unwrap().writer, &Guid::new(&guid))?.into()) + } + + #[handle_error(Error)] + pub fn get_all_passports(&self) -> ApiResult> { + let passports = passports::get_all_passports(&self.db.lock().unwrap().writer)? + .into_iter() + .map(|x| x.into()) + .collect(); + Ok(passports) + } + + #[handle_error(Error)] + pub fn count_all_passports(&self) -> ApiResult { + passports::count_all_passports(&self.db.lock().unwrap().writer) + } + + #[handle_error(Error)] + pub fn update_passport( + &self, + guid: String, + passport: UpdatablePassportFields, + ) -> ApiResult<()> { + passports::update_passport( + &self.db.lock().unwrap().writer, + &Guid::new(&guid), + &passport, + ) + } + + #[handle_error(Error)] + pub fn delete_passport(&self, guid: String) -> ApiResult { + passports::delete_passport(&self.db.lock().unwrap().writer, &Guid::new(&guid)) + } + + #[handle_error(Error)] + pub fn touch_passport(&self, guid: String) -> ApiResult<()> { + passports::touch(&self.db.lock().unwrap().writer, &Guid::new(&guid)) + } + #[handle_error(Error)] pub fn scrub_encrypted_data(self: Arc) -> ApiResult<()> { // scrub the data on disk diff --git a/components/autofill/src/lib.rs b/components/autofill/src/lib.rs index 8aef109bbd..ebbe668680 100644 --- a/components/autofill/src/lib.rs +++ b/components/autofill/src/lib.rs @@ -18,6 +18,7 @@ pub use crate::db::store::get_registered_sync_engine; use crate::db::credit_cards::CreditCardsDeletionMetrics; use crate::db::models::address::*; use crate::db::models::credit_card::*; +use crate::db::models::passport::*; use crate::db::store::Store; use crate::encryption::{create_autofill_key, decrypt_string, encrypt_string}; pub use error::{ApiResult, AutofillApiError, Error, Result};